//! Token Staking Contract //! //! A staking contract with time-locked deposits and proportional rewards. //! //! # Features //! - Stake tokens for rewards //! - Lock periods with bonus multipliers //! - Claim rewards anytime //! - Emergency unstake (with penalty) //! //! # Methods //! - `init(staking_token, reward_token, reward_rate)` - Initialize staking pool //! - `stake(amount, lock_days)` - Stake tokens with optional lock period //! - `unstake(stake_id)` - Unstake after lock period ends //! - `emergency_unstake(stake_id)` - Force unstake with 10% penalty //! - `claim(stake_id)` - Claim accumulated rewards //! - `get_stake(owner, stake_id) -> StakeInfo` - Get stake details //! - `get_stakes(owner) -> Vec` - Get all stakes for owner //! - `get_pool_info() -> PoolInfo` - Get pool statistics #![no_std] extern crate alloc; use alloc::vec::Vec; use borsh::{BorshDeserialize, BorshSerialize}; use synor_sdk::prelude::*; use synor_sdk::{require, require_auth}; // ==================== Constants ==================== /// Emergency unstake penalty: 10% const EMERGENCY_PENALTY_BPS: u64 = 1000; const BPS_DENOMINATOR: u64 = 10000; /// Seconds per day const SECONDS_PER_DAY: u64 = 86400; /// Lock period bonus multipliers (in basis points, 10000 = 1x) const BONUS_NO_LOCK: u64 = 10000; // 1x for no lock const BONUS_30_DAYS: u64 = 12000; // 1.2x for 30 days const BONUS_90_DAYS: u64 = 15000; // 1.5x for 90 days const BONUS_180_DAYS: u64 = 20000; // 2x for 180 days const BONUS_365_DAYS: u64 = 30000; // 3x for 365 days // ==================== Storage Keys ==================== mod keys { pub const STAKING_TOKEN: &[u8] = b"stk:staking_token"; pub const REWARD_TOKEN: &[u8] = b"stk:reward_token"; pub const REWARD_RATE: &[u8] = b"stk:reward_rate"; // Rewards per second per staked token pub const TOTAL_STAKED: &[u8] = b"stk:total_staked"; pub const TOTAL_REWARDS_PAID: &[u8] = b"stk:rewards_paid"; pub const OWNER: &[u8] = b"stk:owner"; pub const STAKES: &[u8] = b"stk:stakes"; pub const STAKE_COUNT: &[u8] = b"stk:stake_count"; pub const PAUSED: &[u8] = b"stk:paused"; } // ==================== Data Structures ==================== #[derive(BorshSerialize, BorshDeserialize, Clone)] pub struct StakeInfo { /// Unique stake ID pub id: u64, /// Amount staked pub amount: u64, /// Effective amount (with bonus multiplier) pub effective_amount: u64, /// Timestamp when staked pub staked_at: u64, /// Lock end timestamp (0 = no lock) pub lock_until: u64, /// Last time rewards were calculated pub last_reward_time: u64, /// Accumulated unclaimed rewards pub pending_rewards: u64, /// Whether this stake is active pub active: bool, } #[derive(BorshSerialize)] pub struct PoolInfo { pub staking_token: Address, pub reward_token: Address, pub reward_rate: u64, pub total_staked: u64, pub total_rewards_paid: u64, pub paused: bool, } // ==================== Storage Helpers ==================== fn stake_key(owner: &Address, stake_id: u64) -> Vec { let mut key = Vec::with_capacity(42); key.extend_from_slice(owner.as_bytes()); key.extend_from_slice(&stake_id.to_le_bytes()); key } fn get_stake(owner: &Address, stake_id: u64) -> Option { let key = stake_key(owner, stake_id); storage::get_with_suffix::(keys::STAKES, &key) } fn set_stake(owner: &Address, stake_id: u64, stake: &StakeInfo) { let key = stake_key(owner, stake_id); storage::set_with_suffix(keys::STAKES, &key, stake); } fn get_stake_count(owner: &Address) -> u64 { storage::get_with_suffix::(keys::STAKE_COUNT, owner.as_bytes()).unwrap_or(0) } fn increment_stake_count(owner: &Address) -> u64 { let count = get_stake_count(owner) + 1; storage::set_with_suffix(keys::STAKE_COUNT, owner.as_bytes(), &count); count } fn get_bonus_multiplier(lock_days: u64) -> u64 { if lock_days >= 365 { BONUS_365_DAYS } else if lock_days >= 180 { BONUS_180_DAYS } else if lock_days >= 90 { BONUS_90_DAYS } else if lock_days >= 30 { BONUS_30_DAYS } else { BONUS_NO_LOCK } } fn is_paused() -> bool { storage::get::(keys::PAUSED).unwrap_or(false) } // ==================== Entry Points ==================== synor_sdk::entry_point!(init, call); /// Initialize the staking pool. fn init(params: &[u8]) -> Result<()> { #[derive(BorshDeserialize)] struct InitParams { staking_token: Address, reward_token: Address, reward_rate: u64, // Rewards per second per 1e8 staked tokens } let params = InitParams::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected staking_token, reward_token, reward_rate"))?; storage::set(keys::STAKING_TOKEN, ¶ms.staking_token); storage::set(keys::REWARD_TOKEN, ¶ms.reward_token); storage::set(keys::REWARD_RATE, ¶ms.reward_rate); storage::set(keys::OWNER, &caller()); storage::set(keys::TOTAL_STAKED, &0u64); storage::set(keys::TOTAL_REWARDS_PAID, &0u64); storage::set(keys::PAUSED, &false); emit(&PoolCreated { staking_token: params.staking_token, reward_token: params.reward_token, reward_rate: params.reward_rate, }); Ok(()) } /// Handle contract calls. fn call(selector: &[u8], params: &[u8]) -> Result> { let stake_sel = synor_sdk::method_selector("stake"); let unstake_sel = synor_sdk::method_selector("unstake"); let emergency_unstake_sel = synor_sdk::method_selector("emergency_unstake"); let claim_sel = synor_sdk::method_selector("claim"); let get_stake_sel = synor_sdk::method_selector("get_stake"); let get_stakes_sel = synor_sdk::method_selector("get_stakes"); let get_pool_info_sel = synor_sdk::method_selector("get_pool_info"); let pause_sel = synor_sdk::method_selector("pause"); let unpause_sel = synor_sdk::method_selector("unpause"); let set_reward_rate_sel = synor_sdk::method_selector("set_reward_rate"); match selector { s if s == stake_sel => { require!(!is_paused(), Error::invalid_args("Pool is paused")); #[derive(BorshDeserialize)] struct Args { amount: u64, lock_days: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected amount, lock_days"))?; let stake_id = do_stake(args.amount, args.lock_days)?; Ok(borsh::to_vec(&stake_id).unwrap()) } s if s == unstake_sel => { #[derive(BorshDeserialize)] struct Args { stake_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected stake_id"))?; let amount = do_unstake(args.stake_id, false)?; Ok(borsh::to_vec(&amount).unwrap()) } s if s == emergency_unstake_sel => { #[derive(BorshDeserialize)] struct Args { stake_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected stake_id"))?; let amount = do_unstake(args.stake_id, true)?; Ok(borsh::to_vec(&amount).unwrap()) } s if s == claim_sel => { #[derive(BorshDeserialize)] struct Args { stake_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected stake_id"))?; let rewards = do_claim(args.stake_id)?; Ok(borsh::to_vec(&rewards).unwrap()) } s if s == get_stake_sel => { #[derive(BorshDeserialize)] struct Args { owner: Address, stake_id: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected owner, stake_id"))?; let stake = get_stake(&args.owner, args.stake_id); Ok(borsh::to_vec(&stake).unwrap()) } s if s == get_stakes_sel => { #[derive(BorshDeserialize)] struct Args { owner: Address, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected owner"))?; let count = get_stake_count(&args.owner); let mut stakes = Vec::new(); for i in 1..=count { if let Some(stake) = get_stake(&args.owner, i) { if stake.active { stakes.push(stake); } } } Ok(borsh::to_vec(&stakes).unwrap()) } s if s == get_pool_info_sel => { let info = PoolInfo { staking_token: storage::get(keys::STAKING_TOKEN).unwrap_or(Address::zero()), reward_token: storage::get(keys::REWARD_TOKEN).unwrap_or(Address::zero()), reward_rate: storage::get(keys::REWARD_RATE).unwrap_or(0), total_staked: storage::get(keys::TOTAL_STAKED).unwrap_or(0), total_rewards_paid: storage::get(keys::TOTAL_REWARDS_PAID).unwrap_or(0), paused: is_paused(), }; Ok(borsh::to_vec(&info).unwrap()) } s if s == pause_sel => { let owner: Address = storage::get(keys::OWNER).ok_or(Error::Unauthorized)?; require_auth!(owner); storage::set(keys::PAUSED, &true); Ok(Vec::new()) } s if s == unpause_sel => { let owner: Address = storage::get(keys::OWNER).ok_or(Error::Unauthorized)?; require_auth!(owner); storage::set(keys::PAUSED, &false); Ok(Vec::new()) } s if s == set_reward_rate_sel => { let owner: Address = storage::get(keys::OWNER).ok_or(Error::Unauthorized)?; require_auth!(owner); #[derive(BorshDeserialize)] struct Args { new_rate: u64, } let args = Args::try_from_slice(params) .map_err(|_| Error::invalid_args("Expected new_rate"))?; storage::set(keys::REWARD_RATE, &args.new_rate); Ok(Vec::new()) } _ => Err(Error::InvalidMethod), } } // ==================== Internal Functions ==================== fn do_stake(amount: u64, lock_days: u64) -> Result { require!(amount > 0, Error::invalid_args("Amount must be positive")); let staker = caller(); let now = timestamp(); // Calculate effective amount with bonus let multiplier = get_bonus_multiplier(lock_days); let effective_amount = (amount as u128 * multiplier as u128 / BPS_DENOMINATOR as u128) as u64; // Calculate lock end time let lock_until = if lock_days > 0 { now + lock_days * SECONDS_PER_DAY } else { 0 }; // Create new stake let stake_id = increment_stake_count(&staker); let stake = StakeInfo { id: stake_id, amount, effective_amount, staked_at: now, lock_until, last_reward_time: now, pending_rewards: 0, active: true, }; set_stake(&staker, stake_id, &stake); // Update total staked let total: u64 = storage::get(keys::TOTAL_STAKED).unwrap_or(0); storage::set(keys::TOTAL_STAKED, &(total + effective_amount)); emit(&Staked { staker, stake_id, amount, effective_amount, lock_until, }); Ok(stake_id) } fn do_unstake(stake_id: u64, emergency: bool) -> Result { let staker = caller(); let now = timestamp(); let mut stake = get_stake(&staker, stake_id).ok_or(Error::invalid_args("Stake not found"))?; require!(stake.active, Error::invalid_args("Stake already withdrawn")); // Check lock period (unless emergency) if !emergency && stake.lock_until > 0 { require!( now >= stake.lock_until, Error::invalid_args("Stake is still locked") ); } // Calculate and claim pending rewards first let rewards = calculate_rewards(&stake, now); stake.pending_rewards += rewards; // Calculate return amount let return_amount = if emergency && stake.lock_until > now { // Apply penalty for early withdrawal let penalty = stake.amount * EMERGENCY_PENALTY_BPS / BPS_DENOMINATOR; stake.amount - penalty } else { stake.amount }; // Mark stake as inactive stake.active = false; set_stake(&staker, stake_id, &stake); // Update total staked let total: u64 = storage::get(keys::TOTAL_STAKED).unwrap_or(0); storage::set(keys::TOTAL_STAKED, &total.saturating_sub(stake.effective_amount)); emit(&Unstaked { staker, stake_id, amount: return_amount, rewards: stake.pending_rewards, emergency, }); Ok(return_amount) } fn do_claim(stake_id: u64) -> Result { let staker = caller(); let now = timestamp(); let mut stake = get_stake(&staker, stake_id).ok_or(Error::invalid_args("Stake not found"))?; require!(stake.active, Error::invalid_args("Stake not active")); // Calculate rewards let new_rewards = calculate_rewards(&stake, now); let total_rewards = stake.pending_rewards + new_rewards; require!(total_rewards > 0, Error::invalid_args("No rewards to claim")); // Update stake stake.pending_rewards = 0; stake.last_reward_time = now; set_stake(&staker, stake_id, &stake); // Update total rewards paid let total_paid: u64 = storage::get(keys::TOTAL_REWARDS_PAID).unwrap_or(0); storage::set(keys::TOTAL_REWARDS_PAID, &(total_paid + total_rewards)); emit(&RewardsClaimed { staker, stake_id, amount: total_rewards, }); Ok(total_rewards) } fn calculate_rewards(stake: &StakeInfo, now: u64) -> u64 { if !stake.active || now <= stake.last_reward_time { return 0; } let reward_rate: u64 = storage::get(keys::REWARD_RATE).unwrap_or(0); let duration = now - stake.last_reward_time; // rewards = effective_amount * reward_rate * duration / 1e8 (stake.effective_amount as u128 * reward_rate as u128 * duration as u128 / 100_000_000) as u64 } // ==================== Events ==================== use synor_sdk::events::{Event, Topic}; #[derive(BorshSerialize)] struct PoolCreated { staking_token: Address, reward_token: Address, reward_rate: u64, } impl Event for PoolCreated { fn topics(&self) -> alloc::vec::Vec { alloc::vec![ Topic::from_name("PoolCreated"), Topic::from(&self.staking_token), Topic::from(&self.reward_token), ] } fn data(&self) -> alloc::vec::Vec { self.reward_rate.to_le_bytes().to_vec() } } #[derive(BorshSerialize)] struct Staked { staker: Address, stake_id: u64, amount: u64, effective_amount: u64, lock_until: u64, } impl Event for Staked { fn topics(&self) -> alloc::vec::Vec { alloc::vec![ Topic::from_name("Staked"), Topic::from(&self.staker), Topic::from(self.stake_id), ] } fn data(&self) -> alloc::vec::Vec { borsh::to_vec(self).unwrap_or_default() } } #[derive(BorshSerialize)] struct Unstaked { staker: Address, stake_id: u64, amount: u64, rewards: u64, emergency: bool, } impl Event for Unstaked { fn topics(&self) -> alloc::vec::Vec { alloc::vec![ Topic::from_name("Unstaked"), Topic::from(&self.staker), Topic::from(self.stake_id), ] } fn data(&self) -> alloc::vec::Vec { borsh::to_vec(self).unwrap_or_default() } } #[derive(BorshSerialize)] struct RewardsClaimed { staker: Address, stake_id: u64, amount: u64, } impl Event for RewardsClaimed { fn topics(&self) -> alloc::vec::Vec { alloc::vec![ Topic::from_name("RewardsClaimed"), Topic::from(&self.staker), Topic::from(self.stake_id), ] } fn data(&self) -> alloc::vec::Vec { self.amount.to_le_bytes().to_vec() } }