From d14549609f6230fe7b1619ee47cf6323bf6b546b Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Thu, 1 Aug 2024 14:27:47 +0200 Subject: [PATCH 01/16] Stake: Refactor contract and call stake_rewards for the reward distribution related mechanics --- contracts/stake/src/contract.rs | 336 +++++++++++--------------------- contracts/stake/src/storage.rs | 41 +++- 2 files changed, 148 insertions(+), 229 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index de48c0501..5edce7488 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -1,8 +1,8 @@ use phoenix::utils::{convert_i128_to_u128, convert_u128_to_i128}; use soroban_decimal::Decimal; use soroban_sdk::{ - contract, contractimpl, contractmeta, log, panic_with_error, vec, Address, BytesN, Env, String, - Vec, + contract, contractimpl, contractmeta, log, panic_with_error, vec, Address, BytesN, Env, + IntoVal, String, Symbol, Val, Vec, }; use crate::{ @@ -19,8 +19,9 @@ use crate::{ storage::{ get_config, get_stakes, save_config, save_stakes, utils::{ - self, add_distribution, get_admin, get_distributions, get_total_staked_counter, - is_initialized, set_initialized, + self, add_distribution, find_stake_rewards_by_asset, get_admin, get_distributions, + get_stake_rewards, get_total_staked_counter, is_initialized, set_initialized, + set_stake_rewards, }, Config, Stake, }, @@ -45,6 +46,7 @@ pub trait StakingTrait { env: Env, admin: Address, lp_token: Address, + stake_rewards: BytesN<32>, min_bond: i128, min_reward: i128, manager: Address, @@ -56,7 +58,15 @@ pub trait StakingTrait { fn unbond(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64); - fn create_distribution_flow(env: Env, sender: Address, asset: Address); + fn create_distribution_flow( + env: Env, + sender: Address, + asset: Address, + salt: BytesN<32>, + max_complexity: u32, + min_reward: u128, + min_bond: u128, + ); fn distribute_rewards(env: Env); @@ -87,6 +97,9 @@ pub trait StakingTrait { fn query_distributed_rewards(env: Env, asset: Address) -> u128; fn query_undistributed_rewards(env: Env, asset: Address) -> u128; + + // ADMIN + fn stake_rewards_add_users(env: Env, staking_rewards: Address, users: Vec<Address>); } #[contractimpl] @@ -96,6 +109,7 @@ impl StakingTrait for Staking { env: Env, admin: Address, lp_token: Address, + stake_rewards: BytesN<32>, min_bond: i128, min_reward: i128, manager: Address, @@ -147,6 +161,7 @@ impl StakingTrait for Staking { utils::save_admin(&env, &admin); utils::init_total_staked(&env); + set_stake_rewards(&env, &stake_rewards); } fn bond(env: Env, sender: Address, tokens: i128) { @@ -175,18 +190,13 @@ impl StakingTrait for Staking { }; stakes.stakes.push_back(stake); - for distribution_address in get_distributions(&env) { - let mut distribution = get_distribution(&env, &distribution_address); - let stakes: i128 = get_stakes(&env, &sender).total_stake; - let old_power = calc_power(&config, stakes, Decimal::one(), TOKEN_PER_POWER); // while bonding we use Decimal::one() - let new_power = calc_power(&config, stakes + tokens, Decimal::one(), TOKEN_PER_POWER); - update_rewards( - &env, - &sender, + for (_asset, distribution_address) in get_distributions(&env) { + // Call stake_rewards contract to calculate for the rewards + let bond_fn_arg: Vec<Val> = (sender.clone(), stakes.clone()).into_val(&env); + env.invoke_contract::<Val>( &distribution_address, - &mut distribution, - old_power, - new_power, + &Symbol::new(&env, "calculate_bond"), + bond_fn_arg, ); } @@ -211,27 +221,18 @@ impl StakingTrait for Staking { Self::withdraw_rewards(env.clone(), sender.clone()); } - for distribution_address in get_distributions(&env) { - let mut distribution = get_distribution(&env, &distribution_address); - let stakes = get_stakes(&env, &sender).total_stake; - let old_power = calc_power(&config, stakes, Decimal::one(), TOKEN_PER_POWER); // while bonding we use Decimal::one() - let new_power = calc_power( - &config, - stakes - stake_amount, - Decimal::one(), - TOKEN_PER_POWER, - ); - update_rewards( - &env, - &sender, + let mut stakes = get_stakes(&env, &sender); + + for (_asset, distribution_address) in get_distributions(&env) { + // Call stake_rewards contract to update the reward calculations + let unbond_fn_arg: Vec<Val> = (sender.clone(), stakes.clone()).into_val(&env); + env.invoke_contract::<Val>( &distribution_address, - &mut distribution, - old_power, - new_power, + &Symbol::new(&env, "calculate_unbond"), + unbond_fn_arg, ); } - let mut stakes = get_stakes(&env, &sender); remove_stake(&env, &mut stakes.stakes, stake_amount, stake_timestamp); stakes.total_stake -= stake_amount; @@ -246,7 +247,15 @@ impl StakingTrait for Staking { env.events().publish(("unbond", "amount"), stake_amount); } - fn create_distribution_flow(env: Env, sender: Address, asset: Address) { + fn create_distribution_flow( + env: Env, + sender: Address, + asset: Address, + salt: BytesN<32>, + max_complexity: u32, + min_reward: u128, + min_bond: u128, + ) { sender.require_auth(); let manager = get_config(&env).manager; @@ -255,26 +264,21 @@ impl StakingTrait for Staking { log!(env, "Stake: create distribution: Non-authorized creation!"); panic_with_error!(&env, ContractError::Unauthorized); } + let deployed_stake_rewards = env + .deployer() + .with_address(sender, salt) + .deploy(get_stake_rewards(&env)); - let distribution = Distribution { - points_per_share: 1u128, - shares_leftover: 0u64, - distributed_total: 0u128, - withdrawable_total: 0u128, - max_bonus_bps: 0u64, - bonus_per_day_bps: 0u64, - }; + let init_fn = Symbol::new(&env, "initialize"); + let init_fn_args: Vec<Val> = + (owner, asset.clone(), max_complexity, min_reward, min_bond).into_val(&env); + let _: Val = env.invoke_contract(&deployed_stake_rewards, &init_fn, init_fn_args); - let reward_token_client = token_contract::Client::new(&env, &asset); - // add distribution to the vector of distributions - add_distribution(&env, &reward_token_client.address); - save_distribution(&env, &reward_token_client.address, &distribution); - // Create the default reward distribution curve which is just a flat 0 const - save_reward_curve(&env, asset, &Curve::Constant(0)); + add_distribution(&env, &asset, &deployed_stake_rewards); env.events().publish( ("create_distribution_flow", "asset"), - &reward_token_client.address, + &deployed_stake_rewards, ); } @@ -292,89 +296,35 @@ impl StakingTrait for Staking { log!(&env, "Stake: No rewards to distribute!"); return; } - for distribution_address in get_distributions(&env) { - let mut distribution = get_distribution(&env, &distribution_address); - let withdrawable = distribution.withdrawable_total; - - let reward_token_client = token_contract::Client::new(&env, &distribution_address); - // Undistributed rewards are simply all tokens left on the contract - let undistributed_rewards_balance = - reward_token_client.balance(&env.current_contract_address()); - let undistributed_rewards = convert_i128_to_u128(undistributed_rewards_balance); - - let curve = get_reward_curve(&env, &distribution_address).expect("Stake: Distribute reward: Not reward curve exists, probably distribution haven't been created"); - - // Calculate how much we have received since the last time Distributed was called, - // including only the reward config amount that is eligible for distribution. - // This is the amount we will distribute to all mem - let amount = - undistributed_rewards - withdrawable - curve.value(env.ledger().timestamp()); - - if amount == 0 { - continue; - } - - let leftover: u128 = distribution.shares_leftover.into(); - let points = (amount << SHARES_SHIFT) + leftover; - let points_per_share = points / total_rewards_power; - distribution.shares_leftover = (points % total_rewards_power) as u64; - - // Everything goes back to 128-bits/16-bytes - // Full amount is added here to total withdrawable, as it should not be considered on its own - // on future distributions - even if because of calculation offsets it is not fully - // distributed, the error is handled by leftover. - distribution.points_per_share += points_per_share; - distribution.distributed_total += amount; - distribution.withdrawable_total += amount; - - save_distribution(&env, &distribution_address, &distribution); - - env.events().publish( - ("distribute_rewards", "asset"), - &reward_token_client.address, + let stakes = get_total_staked_counter(&env); + for (asset, distribution_address) in get_distributions(&env) { + // Call stake_rewards contract to update the reward calculations + let distr_fn_arg: Val = stakes.into_val(&env); + env.invoke_contract::<Val>( + &distribution_address, + &Symbol::new(&env, "distribute_rewards"), + vec![&env, distr_fn_arg], ); + env.events() - .publish(("distribute_rewards", "amount"), amount); + .publish(("distribute_rewards", "asset"), &asset); } } fn withdraw_rewards(env: Env, sender: Address) { env.events().publish(("withdraw_rewards", "user"), &sender); - let config = get_config(&env); + let stakes = get_stakes(&env, &sender); - for distribution_address in get_distributions(&env) { - // get distribution data for the given reward - let mut distribution = get_distribution(&env, &distribution_address); - // get withdraw adjustment for the given distribution - let mut withdraw_adjustment = - get_withdraw_adjustment(&env, &sender, &distribution_address); - // calculate current reward amount given the distribution and subtracting withdraw - // adjustments - let reward_amount = - withdrawable_rewards(&env, &sender, &distribution, &withdraw_adjustment, &config); - - if reward_amount == 0 { - continue; - } - withdraw_adjustment.withdrawn_rewards += reward_amount; - distribution.withdrawable_total -= reward_amount; - - save_distribution(&env, &distribution_address, &distribution); - save_withdraw_adjustment(&env, &sender, &distribution_address, &withdraw_adjustment); - - let reward_token_client = token_contract::Client::new(&env, &distribution_address); - reward_token_client.transfer( - &env.current_contract_address(), - &sender, - &convert_u128_to_i128(reward_amount), + for (asset, distribution_address) in get_distributions(&env) { + let withdraw_fn_arg: Vec<Val> = (sender.clone(), stakes.clone()).into_val(&env); + env.invoke_contract::<Val>( + &distribution_address, + &Symbol::new(&env, "withdraw_rewards"), + withdraw_fn_arg, ); - env.events().publish( - ("withdraw_rewards", "reward_token"), - &reward_token_client.address, - ); env.events() - .publish(("withdraw_rewards", "reward_amount"), reward_amount); + .publish(("withdraw_rewards", "reward_token"), &asset); } } @@ -388,80 +338,24 @@ impl StakingTrait for Staking { let admin = get_admin(&env); admin.require_auth(); - // Load previous reward curve; it must exist if the distribution exists - // In case of first time funding, it will be a constant 0 curve - let previous_reward_curve = get_reward_curve(&env, &token_address).expect("Stake: Fund distribution: Not reward curve exists, probably distribution haven't been created"); - let max_complexity = get_config(&env).max_complexity; - - let current_time = env.ledger().timestamp(); - if start_time < current_time { - log!( - &env, - "Stake: Fund distribution: Fund distribution start time is too early" - ); - panic_with_error!(&env, ContractError::InvalidTime); - } - - let config = get_config(&env); - if config.min_reward > token_amount { - log!( - &env, - "Stake: Fund distribution: minimum reward amount not reached", - ); - panic_with_error!(&env, ContractError::MinRewardNotEnough); - } - - // transfer tokens to fund distribution - let reward_token_client = token_contract::Client::new(&env, &token_address); - reward_token_client.transfer(&admin, &env.current_contract_address(), &token_amount); - - let end_time = current_time + distribution_duration; - // define a distribution curve starting at start_time with token_amount of tokens - // and ending at end_time with 0 tokens - let new_reward_distribution = Curve::saturating_linear( - (start_time, convert_i128_to_u128(token_amount)), - (end_time, 0), + let fund_distr_fn_arg: Vec<Val> = + (start_time, distribution_duration, token_address.clone()).into_val(&env); + env.invoke_contract::<Val>( + &find_stake_rewards_by_asset(&env, &token_address).unwrap(), + &Symbol::new(&env, "fund_distribution"), + fund_distr_fn_arg, ); - // Validate the the curve locks at most the amount provided and - // also fully unlocks all rewards sent - let (min, max) = new_reward_distribution.range(); - if min != 0 || max > convert_i128_to_u128(token_amount) { - log!(&env, "Stake: Fund distribution: Rewards validation failed"); - panic_with_error!(&env, ContractError::RewardsInvalid); - } - - let new_reward_curve: Curve; - // if the previous reward curve has ended, we can just use the new curve - match previous_reward_curve.end() { - Some(end_distribution_timestamp) if end_distribution_timestamp < current_time => { - new_reward_curve = new_reward_distribution; - } - _ => { - // if the previous distribution is still ongoing, we need to combine the two - new_reward_curve = previous_reward_curve.combine(&env, &new_reward_distribution); - new_reward_curve - .validate_complexity(max_complexity) - .unwrap_or_else(|_| { - log!( - &env, - "Stake: Fund distribution: Curve complexity validation failed" - ); - panic_with_error!(&env, ContractError::InvalidMaxComplexity); - }); - } - } - - save_reward_curve(&env, token_address.clone(), &new_reward_curve); - env.events() .publish(("fund_reward_distribution", "asset"), &token_address); env.events() .publish(("fund_reward_distribution", "amount"), token_amount); env.events() .publish(("fund_reward_distribution", "start_time"), start_time); - env.events() - .publish(("fund_reward_distribution", "end_time"), end_time); + env.events().publish( + ("fund_reward_distribution", "end_time"), + start_time + distribution_duration, + ); } // QUERIES @@ -489,34 +383,20 @@ impl StakingTrait for Staking { } fn query_annualized_rewards(env: Env) -> AnnualizedRewardsResponse { - let now = env.ledger().timestamp(); let mut aprs = vec![&env]; - let config = get_config(&env); let total_stake_amount = get_total_staked_counter(&env); + let apr_fn_arg: Val = total_stake_amount.into_val(&env); - for distribution_address in get_distributions(&env) { - let total_stake_power = - calc_power(&config, total_stake_amount, Decimal::one(), TOKEN_PER_POWER); - if total_stake_power == 0 { - aprs.push_back(AnnualizedReward { - asset: distribution_address.clone(), - amount: String::from_str(&env, "0"), - }); - continue; - } - - // get distribution data for the given reward - let distribution = get_distribution(&env, &distribution_address); - let curve = get_reward_curve(&env, &distribution_address); - let annualized_payout = calculate_annualized_payout(curve, now); - let apr = annualized_payout - / convert_u128_to_i128( - convert_i128_to_u128(total_stake_power) * distribution.points_per_share, - ); + for (_asset, distribution_address) in get_distributions(&env) { + let apr: AnnualizedReward = env.invoke_contract( + &distribution_address, + &Symbol::new(&env, "query_annualized_reward"), + vec![&env, apr_fn_arg], + ); aprs.push_back(AnnualizedReward { asset: distribution_address.clone(), - amount: apr.to_string(&env), + amount: apr.amount, }); } @@ -524,21 +404,20 @@ impl StakingTrait for Staking { } fn query_withdrawable_rewards(env: Env, user: Address) -> WithdrawableRewardsResponse { - let config = get_config(&env); + let stakes = get_stakes(&env, &user); // iterate over all distributions and calculate withdrawable rewards let mut rewards = vec![&env]; - for distribution_address in get_distributions(&env) { - // get distribution data for the given reward - let distribution = get_distribution(&env, &distribution_address); - // get withdraw adjustment for the given distribution - let withdraw_adjustment = get_withdraw_adjustment(&env, &user, &distribution_address); - // calculate current reward amount given the distribution and subtracting withdraw - // adjustments - let reward_amount = - withdrawable_rewards(&env, &user, &distribution, &withdraw_adjustment, &config); + for (_asset, distribution_address) in get_distributions(&env) { + let apr_fn_arg: Val = stakes.into_val(&env); + let ret: WithdrawableReward = env.invoke_contract( + &distribution_address, + &Symbol::new(&env, "query_withdrawable_reward"), + vec![&env, apr_fn_arg], + ); + rewards.push_back(WithdrawableReward { reward_address: distribution_address, - reward_amount, + reward_amount: ret.reward_amount, }); } @@ -556,15 +435,28 @@ impl StakingTrait for Staking { let reward_token_balance = reward_token_client.balance(&env.current_contract_address()); convert_i128_to_u128(reward_token_balance) - distribution.withdrawable_total } + + fn stake_rewards_add_users(env: Env, staking_contract: Address, users: Vec<Address>) { + for user in users { + let stakes = get_stakes(&env, &user); + // Call stake_rewards contract to update the reward calculations + let add_user_fn_arg: Vec<Val> = (user, stakes).into_val(&env); + env.invoke_contract::<Val>( + &staking_contract, + &Symbol::new(&env, "add_user"), + add_user_fn_arg, + ); + } + } } #[contractimpl] impl Staking { #[allow(dead_code)] - pub fn update(env: Env, new_wasm_hash: BytesN<32>) { + pub fn update(env: Env, new_wasm_hash: BytesN<32>, staking_rewards: BytesN<32>) { let admin = get_admin(&env); admin.require_auth(); - + set_stake_rewards(&env, &staking_rewards); env.deployer().update_current_contract_wasm(new_wasm_hash); } } diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index 3fd78a1a3..477579c8f 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -1,4 +1,7 @@ -use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec}; +use soroban_sdk::{ + contract, contractimpl, contractmeta, contracttype, log, symbol_short, vec, Address, BytesN, + Env, IntoVal, String, Symbol, Val, Vec, +}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -81,6 +84,7 @@ pub mod utils { TotalStaked = 1, Distributions = 2, Initialized = 3, + StakeRewards = 4, } impl TryFromVal<Env, DataKey> for Val { @@ -133,22 +137,45 @@ pub mod utils { } // Keep track of all distributions to be able to iterate over them - pub fn add_distribution(e: &Env, asset: &Address) { + pub fn add_distribution(e: &Env, asset: &Address, stake_rewards: &Address) { let mut distributions = get_distributions(e); - if distributions.contains(asset) { - log!(&e, "Stake: Add distribution: Distribution already added"); - panic_with_error!(&e, ContractError::DistributionExists); + for (old_asset, _) in distributions.clone() { + if &old_asset == asset { + log!(&e, "Stake: Add distribution: Distribution already added"); + panic_with_error!(&e, ContractError::DistributionExists); + } } - distributions.push_back(asset.clone()); + distributions.push_back((asset.clone(), stake_rewards.clone())); e.storage() .persistent() .set(&DataKey::Distributions, &distributions); } - pub fn get_distributions(e: &Env) -> Vec<Address> { + pub fn get_distributions(e: &Env) -> Vec<(Address, Address)> { e.storage() .persistent() .get(&DataKey::Distributions) .unwrap_or_else(|| soroban_sdk::vec![e]) } + + pub fn get_stake_rewards(e: &Env) -> BytesN<32> { + e.storage() + .persistent() + .get(&DataKey::StakeRewards) + .unwrap() + } + + pub fn set_stake_rewards(e: &Env, hash: &BytesN<32>) { + e.storage().persistent().set(&DataKey::StakeRewards, hash); + } + + pub fn find_stake_rewards_by_asset(e: &Env, asset: &Address) -> Option<Address> { + let distributions = get_distributions(e); + for (stored_asset, stake_rewards) in distributions.iter() { + if &stored_asset == asset { + return Some(stake_rewards); + } + } + None + } } From e7365b1a425540f718b25bc91a84486f1e3659e7 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Thu, 1 Aug 2024 15:12:52 +0200 Subject: [PATCH 02/16] Stake rewards: Refactor the contract in order to allow stake contract call it automatically --- contracts/stake/src/contract.rs | 2 +- contracts/stake_rewards/src/contract.rs | 114 ++++++++------------ contracts/stake_rewards/src/distribution.rs | 13 +-- contracts/stake_rewards/src/lib.rs | 10 -- contracts/stake_rewards/src/storage.rs | 27 ++++- 5 files changed, 80 insertions(+), 86 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 5edce7488..3dc5126e4 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -339,7 +339,7 @@ impl StakingTrait for Staking { admin.require_auth(); let fund_distr_fn_arg: Vec<Val> = - (start_time, distribution_duration, token_address.clone()).into_val(&env); + (start_time, distribution_duration, token_amount.clone()).into_val(&env); env.invoke_contract::<Val>( &find_stake_rewards_by_asset(&env, &token_address).unwrap(), &Symbol::new(&env, "fund_distribution"), diff --git a/contracts/stake_rewards/src/contract.rs b/contracts/stake_rewards/src/contract.rs index 0487fc7b6..9f952612d 100644 --- a/contracts/stake_rewards/src/contract.rs +++ b/contracts/stake_rewards/src/contract.rs @@ -1,7 +1,7 @@ use phoenix::utils::{convert_i128_to_u128, convert_u128_to_i128}; use soroban_decimal::Decimal; use soroban_sdk::{ - contract, contractimpl, contractmeta, log, panic_with_error, Address, BytesN, Env, String, Vec, + contract, contractimpl, contractmeta, log, panic_with_error, Address, BytesN, Env, String, }; use crate::distribution::calc_power; @@ -14,11 +14,10 @@ use crate::{ }, error::ContractError, msg::{AnnualizedRewardResponse, ConfigResponse, WithdrawableRewardResponse}, - stake_contract, storage::{ get_config, save_config, utils::{self, get_admin, is_initialized, set_initialized}, - Config, + BondingInfo, Config, }, token_contract, }; @@ -47,31 +46,31 @@ pub trait StakingRewardsTrait { min_bond: i128, ); - fn add_multiple_users(env: Env, users: Vec<Address>); + fn add_user(env: Env, user: Address, stakes: BondingInfo); - fn add_user(env: Env, user: Address); + fn calculate_bond(env: Env, sender: Address, stakes: BondingInfo); - fn calculate_bond(env: Env, sender: Address); + fn calculate_unbond(env: Env, sender: Address, stakes: BondingInfo); - fn calculate_unbond(env: Env, sender: Address); + fn distribute_rewards(env: Env, total_staked_amount: i128); - fn distribute_rewards(env: Env); - - fn withdraw_rewards(env: Env, sender: Address); + fn withdraw_rewards(env: Env, sender: Address, stakes: BondingInfo); fn fund_distribution(env: Env, start_time: u64, distribution_duration: u64, token_amount: i128); - fn withdraw_leftover(env: Env, amount: i128); - // QUERIES fn query_config(env: Env) -> ConfigResponse; fn query_admin(env: Env) -> Address; - fn query_annualized_reward(env: Env) -> AnnualizedRewardResponse; + fn query_annualized_reward(env: Env, total_stake_amount: i128) -> AnnualizedRewardResponse; - fn query_withdrawable_reward(env: Env, address: Address) -> WithdrawableRewardResponse; + fn query_withdrawable_reward( + env: Env, + address: Address, + stakes: BondingInfo, + ) -> WithdrawableRewardResponse; fn query_distributed_reward(env: Env, asset: Address) -> u128; @@ -133,22 +132,11 @@ impl StakingRewardsTrait for StakingRewards { utils::save_admin(&env, &admin); } - fn add_multiple_users(env: Env, users: Vec<Address>) { - get_admin(&env).require_auth(); - - for user in users { - StakingRewards::add_user(env.clone(), user); - } - } - - fn add_user(env: Env, user: Address) { + fn add_user(env: Env, user: Address, stakes: BondingInfo) { get_admin(&env).require_auth(); let config = get_config(&env); - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let stakes = stake_client.query_staked(&user); - let new_power = calc_power(&config, stakes.total_stake, Decimal::one(), TOKEN_PER_POWER); let mut distribution = get_distribution(&env, &config.reward_token); update_rewards( @@ -163,14 +151,11 @@ impl StakingRewardsTrait for StakingRewards { env.events().publish(("stake_rewards", "add_user"), &user); } - fn calculate_bond(env: Env, sender: Address) { + fn calculate_bond(env: Env, sender: Address, stakes: BondingInfo) { sender.require_auth(); let config = get_config(&env); - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let stakes = stake_client.query_staked(&sender); - let mut distribution = get_distribution(&env, &config.reward_token); let last_stake = stakes.stakes.last().unwrap(); @@ -193,26 +178,22 @@ impl StakingRewardsTrait for StakingRewards { env.events().publish(("calculate_bond", "user"), &sender); } - fn calculate_unbond(env: Env, sender: Address) { + fn calculate_unbond(env: Env, sender: Address, stakes: BondingInfo) { sender.require_auth(); let config = get_config(&env); // check for rewards and withdraw them let found_rewards: WithdrawableRewardResponse = - Self::query_withdrawable_reward(env.clone(), sender.clone()); + Self::query_withdrawable_reward(env.clone(), sender.clone(), stakes.clone()); if found_rewards.reward_amount != 0 { - Self::withdraw_rewards(env.clone(), sender.clone()); + Self::withdraw_rewards(env.clone(), sender.clone(), stakes.clone()); } let mut distribution = get_distribution(&env, &config.reward_token); - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let stakes = stake_client.query_staked(&sender); - - // TODO FIXME: This is wrong, because the last stake would be removed already - // maybe call calculate_unbond first? + // Stake contract need to call calculate_unbond before it removes the to-be-removed staked let last_stake = stakes.stakes.last().unwrap(); let old_power = calc_power(&config, stakes.total_stake, Decimal::one(), TOKEN_PER_POWER); // while bonding we use Decimal::one() @@ -234,11 +215,9 @@ impl StakingRewardsTrait for StakingRewards { env.events().publish(("calculate_unbond", "user"), &sender); } - fn distribute_rewards(env: Env) { + fn distribute_rewards(env: Env, total_staked_amount: i128) { let config = get_config(&env); - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let total_staked_amount = stake_client.query_total_staked(); let calc_power_result = calc_power( &config, total_staked_amount, @@ -294,7 +273,7 @@ impl StakingRewardsTrait for StakingRewards { .publish(("distribute_rewards", "amount"), amount); } - fn withdraw_rewards(env: Env, sender: Address) { + fn withdraw_rewards(env: Env, sender: Address, stakes: BondingInfo) { env.events().publish(("withdraw_rewards", "user"), &sender); let config = get_config(&env); @@ -304,8 +283,12 @@ impl StakingRewardsTrait for StakingRewards { let mut withdraw_adjustment = get_withdraw_adjustment(&env, &sender, &config.reward_token); // calculate current reward amount given the distribution and subtracting withdraw // adjustments - let reward_amount = - withdrawable_rewards(&env, &sender, &distribution, &withdraw_adjustment, &config); + let reward_amount = withdrawable_rewards( + stakes.total_stake, + &distribution, + &withdraw_adjustment, + &config, + ); if reward_amount == 0 { return; @@ -317,15 +300,13 @@ impl StakingRewardsTrait for StakingRewards { save_withdraw_adjustment(&env, &sender, &config.reward_token, &withdraw_adjustment); // calculate the actual reward amounts - each stake is worth 1/60th per each staked day - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let stakes = stake_client.query_staked(&sender); let reward_multiplier = calc_withdraw_power(&env, &stakes.stakes); let reward_token_client = token_contract::Client::new(&env, &config.reward_token); reward_token_client.transfer( &env.current_contract_address(), &sender, - &convert_u128_to_i128(reward_amount) * reward_multiplier, + &(convert_u128_to_i128(reward_amount) * reward_multiplier), ); env.events().publish( @@ -425,17 +406,6 @@ impl StakingRewardsTrait for StakingRewards { .publish(("fund_reward_distribution", "end_time"), end_time); } - // In case there are leftover tokens due to insufficient APR bonus, admin can clean up tokens - fn withdraw_leftover(env: Env, amount: i128) { - let admin = get_admin(&env); - admin.require_auth(); - token_contract::Client::new(&env, &get_config(&env).reward_token).transfer( - &env.current_contract_address(), - &admin, - &amount, - ); - } - // QUERIES fn query_config(env: Env) -> ConfigResponse { @@ -448,14 +418,16 @@ impl StakingRewardsTrait for StakingRewards { get_admin(&env) } - fn query_annualized_reward(env: Env) -> AnnualizedRewardResponse { + fn query_annualized_reward(env: Env, total_staked_amount: i128) -> AnnualizedRewardResponse { let now = env.ledger().timestamp(); let config = get_config(&env); - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let total_stake_amount = stake_client.query_total_staked(); - let total_stake_power = - calc_power(&config, total_stake_amount, Decimal::one(), TOKEN_PER_POWER); + let total_stake_power = calc_power( + &config, + total_staked_amount, + Decimal::one(), + TOKEN_PER_POWER, + ); if total_stake_power == 0 { return AnnualizedRewardResponse { asset: config.reward_token.clone(), @@ -478,7 +450,11 @@ impl StakingRewardsTrait for StakingRewards { } } - fn query_withdrawable_reward(env: Env, user: Address) -> WithdrawableRewardResponse { + fn query_withdrawable_reward( + env: Env, + user: Address, + stakes: BondingInfo, + ) -> WithdrawableRewardResponse { let config = get_config(&env); // iterate over all distributions and calculate withdrawable rewards // get distribution data for the given reward @@ -487,12 +463,14 @@ impl StakingRewardsTrait for StakingRewards { let withdraw_adjustment = get_withdraw_adjustment(&env, &user, &config.reward_token); // calculate current reward amount given the distribution and subtracting withdraw // adjustments - let reward_amount = - withdrawable_rewards(&env, &user, &distribution, &withdraw_adjustment, &config); + let reward_amount = withdrawable_rewards( + stakes.total_stake, + &distribution, + &withdraw_adjustment, + &config, + ); // calculate the actual reward amounts - each stake is worth 1/60th per each staked day - let stake_client = stake_contract::Client::new(&env, &config.staking_contract); - let stakes = stake_client.query_staked(&user); let reward_multiplier = calc_withdraw_power(&env, &stakes.stakes); let reward_amount = diff --git a/contracts/stake_rewards/src/distribution.rs b/contracts/stake_rewards/src/distribution.rs index 8ae9f8a5a..745607e32 100644 --- a/contracts/stake_rewards/src/distribution.rs +++ b/contracts/stake_rewards/src/distribution.rs @@ -4,7 +4,10 @@ use soroban_sdk::{contracttype, Address, Env, Vec}; use curve::Curve; use soroban_decimal::Decimal; -use crate::{stake_contract, stake_contract::Stake, storage::Config, TOKEN_PER_POWER}; +use crate::{ + storage::{Config, Stake}, + TOKEN_PER_POWER, +}; use phoenix::utils::convert_u128_to_i128; /// How much points is the worth of single token in rewards distribution. @@ -161,19 +164,17 @@ pub fn get_withdraw_adjustment( } pub fn withdrawable_rewards( - env: &Env, - owner: &Address, + // total amount of staked tokens by given user + total_staked: i128, distribution: &Distribution, adjustment: &WithdrawAdjustment, config: &Config, ) -> u128 { let ppw = distribution.shares_per_point; - let stake_client = stake_contract::Client::new(env, &config.staking_contract); - let stakes: i128 = stake_client.query_staked(owner).total_stake; // Decimal::one() represents the standart multiplier per token // 1_000 represents the contsant token per power. TODO: make it configurable - let points = calc_power(config, stakes, Decimal::one(), TOKEN_PER_POWER); + let points = calc_power(config, total_staked, Decimal::one(), TOKEN_PER_POWER); let points = convert_u128_to_i128(ppw) * points; let correction = adjustment.shares_correction; diff --git a/contracts/stake_rewards/src/lib.rs b/contracts/stake_rewards/src/lib.rs index b17ff9b8f..20b4c46bc 100644 --- a/contracts/stake_rewards/src/lib.rs +++ b/contracts/stake_rewards/src/lib.rs @@ -7,16 +7,6 @@ mod storage; pub const TOKEN_PER_POWER: i32 = 1_000; -#[allow(clippy::too_many_arguments)] -pub mod stake_contract { - // The import will code generate: - // - A ContractClient type that can be used to invoke functions on the contract. - // - Any types in the contract that were annotated with #[contracttype]. - soroban_sdk::contractimport!( - file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" - ); -} - #[allow(clippy::too_many_arguments)] pub mod token_contract { // The import will code generate: diff --git a/contracts/stake_rewards/src/storage.rs b/contracts/stake_rewards/src/storage.rs index 914f2d14f..67fea11fe 100644 --- a/contracts/stake_rewards/src/storage.rs +++ b/contracts/stake_rewards/src/storage.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol}; +use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -66,3 +66,28 @@ pub mod utils { e.storage().persistent().get(&DataKey::Admin).unwrap() } } + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct Stake { + /// The amount of staked tokens + pub stake: i128, + /// The timestamp when the stake was made + pub stake_timestamp: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BondingInfo { + /// Vec of stakes sorted by stake timestamp + pub stakes: Vec<Stake>, + /// The rewards debt is a mechanism to determine how much a user has already been credited in terms of staking rewards. + /// Whenever a user deposits or withdraws staked tokens to the pool, the rewards for the user is updated based on the + /// accumulated rewards per share, and the difference is stored as reward debt. When claiming rewards, this reward debt + /// is used to determine how much rewards a user can actually claim. + pub reward_debt: u128, + /// Last time when user has claimed rewards + pub last_reward_time: u64, + /// Total amount of staked tokens + pub total_stake: i128, +} From 081339339a821f15ccfd8dc282dfd93fb2b96574 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Thu, 1 Aug 2024 15:40:28 +0200 Subject: [PATCH 03/16] Stake: Add stake_rewards to test setup harness --- contracts/stake/src/tests.rs | 4 ++-- contracts/stake/src/tests/setup.rs | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/contracts/stake/src/tests.rs b/contracts/stake/src/tests.rs index bce7f557c..6c19073e3 100644 --- a/contracts/stake/src/tests.rs +++ b/contracts/stake/src/tests.rs @@ -1,3 +1,3 @@ -mod bond; -mod distribution; +// mod bond; +// mod distribution; mod setup; diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 31b0ee598..1ab9a8a95 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{testutils::Address as _, Address, Env}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; use crate::{ contract::{Staking, StakingClient}, @@ -9,6 +9,14 @@ pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract:: token_contract::Client::new(env, &env.register_stellar_asset_contract(admin.clone())) } +#[allow(clippy::too_many_arguments)] +pub fn install_stake_rewards_contract(env: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake_rewards.wasm" + ); + env.deployer().upload_contract_wasm(WASM) +} + const MIN_BOND: i128 = 1000; const MIN_REWARD: i128 = 1000; pub const ONE_WEEK: u64 = 604800; @@ -24,10 +32,12 @@ pub fn deploy_staking_contract<'a>( ) -> StakingClient<'a> { let admin = admin.into().unwrap_or(Address::generate(env)); let staking = StakingClient::new(env, &env.register_contract(None, Staking {})); + let stake_rewards_hash = install_stake_rewards_contract(env); staking.initialize( &admin, lp_token, + &stake_rewards_hash, &MIN_BOND, &MIN_REWARD, manager, From ae9b5caae2be0ce7fab540692cc3905672aa8a8d Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Thu, 1 Aug 2024 15:45:57 +0200 Subject: [PATCH 04/16] Stake: Refactor bond tests --- contracts/stake/src/tests.rs | 2 +- contracts/stake/src/tests/bond.rs | 126 ++++++++++++++++-------------- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/contracts/stake/src/tests.rs b/contracts/stake/src/tests.rs index 6c19073e3..696b5ccbd 100644 --- a/contracts/stake/src/tests.rs +++ b/contracts/stake/src/tests.rs @@ -1,3 +1,3 @@ -// mod bond; +mod bond; // mod distribution; mod setup; diff --git a/contracts/stake/src/tests/bond.rs b/contracts/stake/src/tests/bond.rs index 870523ba4..4b98bd5e5 100644 --- a/contracts/stake/src/tests/bond.rs +++ b/contracts/stake/src/tests/bond.rs @@ -7,7 +7,9 @@ use soroban_sdk::{ vec, Address, Env, IntoVal, Symbol, }; -use super::setup::{deploy_staking_contract, deploy_token_contract}; +use super::setup::{ + deploy_staking_contract, deploy_token_contract, install_stake_rewards_contract, +}; use crate::{ contract::{Staking, StakingClient}, @@ -79,6 +81,7 @@ fn test_deploying_stake_twice_should_fail() { first.initialize( &admin, &lp_token.address, + &install_stake_rewards_contract(&env), &100i128, &50i128, &manager, @@ -326,65 +329,65 @@ fn unbond_wrong_user_stake_not_found() { staking.unbond(&user2, &10_000, &non_existing_timestamp); } -#[test] -fn pay_rewards_during_unbond() { - const STAKED_AMOUNT: i128 = 1_000; - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &DEFAULT_COMPLEXITY, - ); - - lp_token.mint(&user, &10_000); - reward_token.mint(&admin, &10_000); - - env.ledger().with_mut(|li| { - li.timestamp = ONE_WEEK; - }); - - staking.create_distribution_flow(&manager, &reward_token.address); - staking.fund_distribution(&ONE_WEEK, &10_000u64, &reward_token.address, &10_000); - - env.ledger().with_mut(|li| { - li.timestamp = ONE_WEEK + 5_000; - }); - staking.bond(&user, &STAKED_AMOUNT); - - staking.distribute_rewards(); - - // user has bonded for 5_000 time, initial rewards are 10_000 - // so user should have 5_000 rewards - // 5_000 rewards are still undistributed - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - 5_000 - ); - assert_eq!( - staking - .query_withdrawable_rewards(&user) - .rewards - .iter() - .map(|reward| reward.reward_amount) - .sum::<u128>(), - 5_000 - ); - assert_eq!(reward_token.balance(&user), 0); - staking.unbond(&user, &STAKED_AMOUNT, &(ONE_WEEK + 5_000)); - assert_eq!(reward_token.balance(&user), 5_000); -} +// #[test] +// fn pay_rewards_during_unbond() { +// const STAKED_AMOUNT: i128 = 1_000; +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &DEFAULT_COMPLEXITY, +// ); +// +// lp_token.mint(&user, &10_000); +// reward_token.mint(&admin, &10_000); +// +// env.ledger().with_mut(|li| { +// li.timestamp = ONE_WEEK; +// }); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// staking.fund_distribution(&ONE_WEEK, &10_000u64, &reward_token.address, &10_000); +// +// env.ledger().with_mut(|li| { +// li.timestamp = ONE_WEEK + 5_000; +// }); +// staking.bond(&user, &STAKED_AMOUNT); +// +// staking.distribute_rewards(); +// +// // user has bonded for 5_000 time, initial rewards are 10_000 +// // so user should have 5_000 rewards +// // 5_000 rewards are still undistributed +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// 5_000 +// ); +// assert_eq!( +// staking +// .query_withdrawable_rewards(&user) +// .rewards +// .iter() +// .map(|reward| reward.reward_amount) +// .sum::<u128>(), +// 5_000 +// ); +// assert_eq!(reward_token.balance(&user), 0); +// staking.unbond(&user, &STAKED_AMOUNT, &(ONE_WEEK + 5_000)); +// assert_eq!(reward_token.balance(&user), 5_000); +// } #[should_panic( expected = "Stake: initialize: Minimum amount of lp share tokens to bond can not be smaller or equal to 0" @@ -399,6 +402,7 @@ fn initialize_staking_contract_should_panic_when_min_bond_invalid() { staking.initialize( &Address::generate(&env), &Address::generate(&env), + &install_stake_rewards_contract(&env), &0, &1_000, &Address::generate(&env), @@ -418,6 +422,7 @@ fn initialize_staking_contract_should_panic_when_min_rewards_invalid() { staking.initialize( &Address::generate(&env), &Address::generate(&env), + &install_stake_rewards_contract(&env), &1_000, &0, &Address::generate(&env), @@ -437,6 +442,7 @@ fn initialize_staking_contract_should_panic_when_max_complexity_invalid() { staking.initialize( &Address::generate(&env), &Address::generate(&env), + &install_stake_rewards_contract(&env), &1_000, &1_000, &Address::generate(&env), From fc4de31c25c8cbb4eddd0627526c8ae0a9e90962 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Thu, 1 Aug 2024 18:25:36 +0200 Subject: [PATCH 05/16] Stake: Finish refactoring bond tests --- contracts/stake/src/contract.rs | 70 ++++++++++----- contracts/stake/src/lib.rs | 9 +- contracts/stake/src/storage.rs | 52 +++++++++++ contracts/stake/src/tests/bond.rs | 139 +++++++++++++++++------------- 4 files changed, 188 insertions(+), 82 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 3dc5126e4..b8e2ed9ea 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -16,6 +16,7 @@ use crate::{ AnnualizedReward, AnnualizedRewardsResponse, ConfigResponse, StakedResponse, WithdrawableReward, WithdrawableRewardsResponse, }, + stake_rewards_contract, storage::{ get_config, get_stakes, save_config, save_stakes, utils::{ @@ -64,8 +65,8 @@ pub trait StakingTrait { asset: Address, salt: BytesN<32>, max_complexity: u32, - min_reward: u128, - min_bond: u128, + min_reward: i128, + min_bond: i128, ); fn distribute_rewards(env: Env); @@ -253,8 +254,8 @@ impl StakingTrait for Staking { asset: Address, salt: BytesN<32>, max_complexity: u32, - min_reward: u128, - min_bond: u128, + min_reward: i128, + min_bond: i128, ) { sender.require_auth(); @@ -266,13 +267,28 @@ impl StakingTrait for Staking { } let deployed_stake_rewards = env .deployer() - .with_address(sender, salt) + .with_address(env.current_contract_address(), salt) .deploy(get_stake_rewards(&env)); - let init_fn = Symbol::new(&env, "initialize"); - let init_fn_args: Vec<Val> = - (owner, asset.clone(), max_complexity, min_reward, min_bond).into_val(&env); - let _: Val = env.invoke_contract(&deployed_stake_rewards, &init_fn, init_fn_args); + stake_rewards_contract::Client::new(&env, &deployed_stake_rewards).initialize( + &owner, + &env.current_contract_address(), + &asset.clone(), + &max_complexity, + &min_reward, + &min_bond, + ); + // let init_fn = Symbol::new(&env, "initialize"); + // let init_fn_args: Vec<Val> = ( + // owner, + // env.current_contract_address(), + // asset.clone(), + // max_complexity, + // min_reward, + // min_bond, + // ) + // .into_val(&env); + // let _: Val = env.invoke_contract(&deployed_stake_rewards, &init_fn, init_fn_args); add_distribution(&env, &asset, &deployed_stake_rewards); @@ -407,13 +423,15 @@ impl StakingTrait for Staking { let stakes = get_stakes(&env, &user); // iterate over all distributions and calculate withdrawable rewards let mut rewards = vec![&env]; + // let apr_fn_arg: Val = (user.clone(), stakes.clone()).into_val(&env); for (_asset, distribution_address) in get_distributions(&env) { - let apr_fn_arg: Val = stakes.into_val(&env); - let ret: WithdrawableReward = env.invoke_contract( - &distribution_address, - &Symbol::new(&env, "query_withdrawable_reward"), - vec![&env, apr_fn_arg], - ); + let ret = stake_rewards_contract::Client::new(&env, &distribution_address) + .query_withdrawable_reward(&user.clone(), &stakes.clone().into()); + // let ret: WithdrawableReward = env.invoke_contract( + // &distribution_address, + // &Symbol::new(&env, "query_withdrawable_reward"), + // vec![&env, apr_fn_arg], + // ); rewards.push_back(WithdrawableReward { reward_address: distribution_address, @@ -425,15 +443,25 @@ impl StakingTrait for Staking { } fn query_distributed_rewards(env: Env, asset: Address) -> u128 { - let distribution = get_distribution(&env, &asset); - distribution.distributed_total + let staking_rewards = find_stake_rewards_by_asset(&env, &asset).unwrap(); + let unds_rew_fn_arg: Val = asset.into_val(&env); + let ret: u128 = env.invoke_contract( + &staking_rewards, + &Symbol::new(&env, "query_distributed_reward"), + vec![&env, unds_rew_fn_arg], + ); + ret } fn query_undistributed_rewards(env: Env, asset: Address) -> u128 { - let distribution = get_distribution(&env, &asset); - let reward_token_client = token_contract::Client::new(&env, &asset); - let reward_token_balance = reward_token_client.balance(&env.current_contract_address()); - convert_i128_to_u128(reward_token_balance) - distribution.withdrawable_total + let staking_rewards = find_stake_rewards_by_asset(&env, &asset).unwrap(); + let unds_rew_fn_arg: Val = asset.into_val(&env); + let ret: u128 = env.invoke_contract( + &staking_rewards, + &Symbol::new(&env, "query_undistributed_reward"), + vec![&env, unds_rew_fn_arg], + ); + ret } fn stake_rewards_add_users(env: Env, staking_contract: Address, users: Vec<Address>) { diff --git a/contracts/stake/src/lib.rs b/contracts/stake/src/lib.rs index 4ff763cef..b94dd92f0 100644 --- a/contracts/stake/src/lib.rs +++ b/contracts/stake/src/lib.rs @@ -16,7 +16,14 @@ pub mod token_contract { ); } -pub use storage::Stake; +pub mod stake_rewards_contract { + // The import will code generate: + // - A ContractClient type that can be used to invoke functions on the contract. + // - Any types in the contract that were annotated with #[contracttype]. + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake_rewards.wasm" + ); +} #[cfg(test)] mod tests; diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index 477579c8f..8afd66c37 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -3,6 +3,8 @@ use soroban_sdk::{ Env, IntoVal, String, Symbol, Val, Vec, }; +use crate::stake_rewards_contract; + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Config { @@ -179,3 +181,53 @@ pub mod utils { None } } + +// Implement `From` trait for conversion between `BondingInfo` structs +impl From<BondingInfo> for stake_rewards_contract::BondingInfo { + fn from(info: BondingInfo) -> Self { + let mut stakes = Vec::new(info.stakes.env()); + for stake in info.stakes.iter() { + stakes.push_back(stake.into()); + } + stake_rewards_contract::BondingInfo { + stakes, + reward_debt: info.reward_debt, + last_reward_time: info.last_reward_time, + total_stake: info.total_stake, + } + } +} + +impl From<stake_rewards_contract::BondingInfo> for BondingInfo { + fn from(info: stake_rewards_contract::BondingInfo) -> Self { + let mut stakes = Vec::new(info.stakes.env()); + for stake in info.stakes.iter() { + stakes.push_back(stake.into()); + } + BondingInfo { + stakes, + reward_debt: info.reward_debt, + last_reward_time: info.last_reward_time, + total_stake: info.total_stake, + } + } +} + +// Implement `From` trait for conversion between `Stake` structs +impl From<Stake> for stake_rewards_contract::Stake { + fn from(stake: Stake) -> Self { + stake_rewards_contract::Stake { + stake: stake.stake, + stake_timestamp: stake.stake_timestamp, + } + } +} + +impl From<stake_rewards_contract::Stake> for Stake { + fn from(stake: stake_rewards_contract::Stake) -> Self { + Stake { + stake: stake.stake, + stake_timestamp: stake.stake_timestamp, + } + } +} diff --git a/contracts/stake/src/tests/bond.rs b/contracts/stake/src/tests/bond.rs index 4b98bd5e5..db9bc6948 100644 --- a/contracts/stake/src/tests/bond.rs +++ b/contracts/stake/src/tests/bond.rs @@ -4,7 +4,7 @@ use pretty_assertions::assert_eq; use soroban_sdk::{ symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Ledger}, - vec, Address, Env, IntoVal, Symbol, + vec, Address, BytesN, Env, IntoVal, Symbol, }; use super::setup::{ @@ -329,65 +329,84 @@ fn unbond_wrong_user_stake_not_found() { staking.unbond(&user2, &10_000, &non_existing_timestamp); } -// #[test] -// fn pay_rewards_during_unbond() { -// const STAKED_AMOUNT: i128 = 1_000; -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &DEFAULT_COMPLEXITY, -// ); -// -// lp_token.mint(&user, &10_000); -// reward_token.mint(&admin, &10_000); -// -// env.ledger().with_mut(|li| { -// li.timestamp = ONE_WEEK; -// }); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// staking.fund_distribution(&ONE_WEEK, &10_000u64, &reward_token.address, &10_000); -// -// env.ledger().with_mut(|li| { -// li.timestamp = ONE_WEEK + 5_000; -// }); -// staking.bond(&user, &STAKED_AMOUNT); -// -// staking.distribute_rewards(); -// -// // user has bonded for 5_000 time, initial rewards are 10_000 -// // so user should have 5_000 rewards -// // 5_000 rewards are still undistributed -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// 5_000 -// ); -// assert_eq!( -// staking -// .query_withdrawable_rewards(&user) -// .rewards -// .iter() -// .map(|reward| reward.reward_amount) -// .sum::<u128>(), -// 5_000 -// ); -// assert_eq!(reward_token.balance(&user), 0); -// staking.unbond(&user, &STAKED_AMOUNT, &(ONE_WEEK + 5_000)); -// assert_eq!(reward_token.balance(&user), 5_000); -// } +#[test] +fn pay_rewards_during_unbond() { + const STAKED_AMOUNT: i128 = 1_000; + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let full_bonding_multiplier = ONE_DAY * 60; + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let manager = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &DEFAULT_COMPLEXITY, + ); + + lp_token.mint(&user, &10_000); + reward_token.mint(&admin, &10_000); + + staking.bond(&user, &STAKED_AMOUNT); + + // Move so that user would have 100% APR from bonding after 60 days + env.ledger().with_mut(|li| { + li.timestamp = full_bonding_multiplier; + }); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + // distribution starts at 6 weeks and lasts for 100 seconds + staking.fund_distribution( + &full_bonding_multiplier, + &100, + &reward_token.address, + &10_000, + ); + + // move to the half time + env.ledger().with_mut(|li| { + li.timestamp = full_bonding_multiplier + 50; + }); + + staking.distribute_rewards(); + + // user should have 5_000 rewards + // 5_000 rewards are still undistributed + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 5_000 + ); + assert_eq!( + staking + .query_withdrawable_rewards(&user) + .rewards + .iter() + .map(|reward| reward.reward_amount) + .sum::<u128>(), + 5_000 + ); + assert_eq!(reward_token.balance(&user), 0); + // user bonded at timestamp 0 + staking.unbond(&user, &STAKED_AMOUNT, &0); + assert_eq!(reward_token.balance(&user), 5_000); +} #[should_panic( expected = "Stake: initialize: Minimum amount of lp share tokens to bond can not be smaller or equal to 0" From b54556afd4a0ae3760fe9c4ddc01df2c109ab964 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 09:44:56 +0200 Subject: [PATCH 06/16] Stake: Add query_distribution query --- contracts/stake/src/contract.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index b8e2ed9ea..e464c0074 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -99,6 +99,8 @@ pub trait StakingTrait { fn query_undistributed_rewards(env: Env, asset: Address) -> u128; + fn query_distribution(env: Env, asset: Address) -> Option<Address>; + // ADMIN fn stake_rewards_add_users(env: Env, staking_rewards: Address, users: Vec<Address>); } @@ -464,6 +466,10 @@ impl StakingTrait for Staking { ret } + fn query_distribution(env: Env, asset: Address) -> Option<Address> { + find_stake_rewards_by_asset(&env, &asset) + } + fn stake_rewards_add_users(env: Env, staking_contract: Address, users: Vec<Address>) { for user in users { let stakes = get_stakes(&env, &user); From 73a7fdb7ae9cc2d1f0b16a9c5f62786aef59db7b Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 10:15:57 +0200 Subject: [PATCH 07/16] Stake: Refactor distribution tests --- contracts/stake/src/contract.rs | 4 +- contracts/stake/src/tests.rs | 2 +- contracts/stake/src/tests/distribution.rs | 2103 +++++++++++---------- contracts/stake/src/tests/setup.rs | 1 + 4 files changed, 1067 insertions(+), 1043 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index e464c0074..51e015d00 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -426,7 +426,7 @@ impl StakingTrait for Staking { // iterate over all distributions and calculate withdrawable rewards let mut rewards = vec![&env]; // let apr_fn_arg: Val = (user.clone(), stakes.clone()).into_val(&env); - for (_asset, distribution_address) in get_distributions(&env) { + for (asset, distribution_address) in get_distributions(&env) { let ret = stake_rewards_contract::Client::new(&env, &distribution_address) .query_withdrawable_reward(&user.clone(), &stakes.clone().into()); // let ret: WithdrawableReward = env.invoke_contract( @@ -436,7 +436,7 @@ impl StakingTrait for Staking { // ); rewards.push_back(WithdrawableReward { - reward_address: distribution_address, + reward_address: asset, reward_amount: ret.reward_amount, }); } diff --git a/contracts/stake/src/tests.rs b/contracts/stake/src/tests.rs index 696b5ccbd..bce7f557c 100644 --- a/contracts/stake/src/tests.rs +++ b/contracts/stake/src/tests.rs @@ -1,3 +1,3 @@ mod bond; -// mod distribution; +mod distribution; mod setup; diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index cefd657a0..58267e010 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -2,7 +2,7 @@ extern crate std; use soroban_sdk::{ symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Ledger}, - vec, Address, Env, IntoVal, String, Symbol, + vec, Address, BytesN, Env, IntoVal, String, Symbol, }; use super::setup::{deploy_staking_contract, deploy_token_contract}; @@ -13,18 +13,18 @@ use crate::{ AnnualizedReward, AnnualizedRewardsResponse, WithdrawableReward, WithdrawableRewardsResponse, }, - tests::setup::{ONE_DAY, ONE_WEEK}, + tests::setup::{ONE_DAY, ONE_WEEK, SIXTY_DAYS}, }; #[test] fn add_distribution_and_distribute_reward() { let env = Env::default(); env.mock_all_auths(); + env.budget().reset_unlimited(); let admin = Address::generate(&env); let user = Address::generate(&env); let manager = Address::generate(&env); - let owner = Address::generate(&env); let lp_token = deploy_token_contract(&env, &admin); let reward_token = deploy_token_contract(&env, &admin); @@ -33,21 +33,36 @@ fn add_distribution_and_distribute_reward() { admin.clone(), &lp_token.address, &manager, - &owner, + &admin, &50u32, ); - staking.create_distribution_flow(&manager, &reward_token.address); + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); assert_eq!( env.auths(), [( - manager.clone(), + admin.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( staking.address.clone(), Symbol::new(&env, "create_distribution_flow"), - (&manager.clone(), reward_token.address.clone()).into_val(&env), + ( + &admin.clone(), + reward_token.address.clone(), + BytesN::from_array(&env, &[1; 32]), + 10u32, + 100i128, + 1i128 + ) + .into_val(&env), )), sub_invocations: std::vec![], } @@ -59,15 +74,18 @@ fn add_distribution_and_distribute_reward() { // bond tokens for user to enable distribution for him lp_token.mint(&user, &1000); + staking.bond(&user, &1000); + + // simulate moving forward 60 days for the full APR multiplier env.ledger().with_mut(|li| { - li.timestamp = ONE_DAY; + li.timestamp = SIXTY_DAYS; }); - staking.bond(&user, &1000); + let staking_rewards = staking.query_distribution(&reward_token.address).unwrap(); let reward_duration = 600; staking.fund_distribution( - &ONE_DAY, + &SIXTY_DAYS, &reward_duration, &reward_token.address, &(reward_amount as i128), @@ -82,24 +100,29 @@ fn add_distribution_and_distribute_reward() { staking.address.clone(), Symbol::new(&env, "fund_distribution"), ( - ONE_DAY, + SIXTY_DAYS, reward_duration, reward_token.address.clone(), reward_amount as i128 ) .into_val(&env), )), - sub_invocations: std::vec![ - (AuthorizedInvocation { + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + staking_rewards.clone(), // Repeat the fund_distribution call + Symbol::new(&env, "fund_distribution"), + (SIXTY_DAYS, reward_duration, reward_amount as i128).into_val(&env), + )), + sub_invocations: std::vec![AuthorizedInvocation { function: AuthorizedFunction::Contract(( reward_token.address.clone(), symbol_short!("transfer"), - (&admin, &staking.address.clone(), reward_amount as i128) + (&admin, &staking_rewards.clone(), reward_amount as i128) .into_val(&env) )), sub_invocations: std::vec![], - }), - ], + },], + },], } ),] ); @@ -111,113 +134,9 @@ fn add_distribution_and_distribute_reward() { ); env.ledger().with_mut(|li| { - li.timestamp = ONE_DAY + reward_duration; - }); - staking.distribute_rewards(); - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - 0 - ); - assert_eq!( - staking.query_distributed_rewards(&reward_token.address), - reward_amount - ); - - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount - } - ] - } - ); - - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), reward_amount as i128); -} - -#[test] -fn two_distributions() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - let reward_token_2 = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - staking.create_distribution_flow(&manager, &reward_token_2.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - reward_token_2.mint(&admin, &((reward_amount * 2) as i128)); - - // bond tokens for user to enable distribution for him - lp_token.mint(&user, &1000); - env.ledger().with_mut(|li| li.timestamp = ONE_DAY); - staking.bond(&user, &1000); - - let reward_duration = 600; - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token_2.address, - &((reward_amount * 2) as i128), - ); - - // distribute rewards during half time - env.ledger().with_mut(|li| { - li.timestamp += 300; - }); - staking.distribute_rewards(); - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: reward_amount / 2 - }, - WithdrawableReward { - reward_address: reward_token_2.address.clone(), - reward_amount - } - ] - } - ); - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), (reward_amount / 2) as i128); - assert_eq!(reward_token_2.balance(&user), reward_amount as i128); - - env.ledger().with_mut(|li| { - li.timestamp += 600; + li.timestamp = SIXTY_DAYS + reward_duration; }); staking.distribute_rewards(); - // first reward token assert_eq!( staking.query_undistributed_rewards(&reward_token.address), 0 @@ -226,18 +145,7 @@ fn two_distributions() { staking.query_distributed_rewards(&reward_token.address), reward_amount ); - // second reward token - assert_eq!( - staking.query_undistributed_rewards(&reward_token_2.address), - 0 - ); - assert_eq!( - staking.query_distributed_rewards(&reward_token_2.address), - reward_amount * 2 - ); - // since half of rewards were already distributed, after full distirubtion - // round another half is ready assert_eq!( staking.query_withdrawable_rewards(&user), WithdrawableRewardsResponse { @@ -245,10 +153,6 @@ fn two_distributions() { &env, WithdrawableReward { reward_address: reward_token.address.clone(), - reward_amount: reward_amount / 2 - }, - WithdrawableReward { - reward_address: reward_token_2.address.clone(), reward_amount } ] @@ -257,909 +161,1028 @@ fn two_distributions() { staking.withdraw_rewards(&user); assert_eq!(reward_token.balance(&user), reward_amount as i128); - assert_eq!(reward_token_2.balance(&user), (reward_amount * 2) as i128); -} - -#[test] -fn four_users_with_different_stakes() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let user2 = Address::generate(&env); - let user3 = Address::generate(&env); - let user4 = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - // bond tokens for users; each user has a different amount staked - env.ledger().with_mut(|li| { - li.timestamp = ONE_WEEK; - }); - - lp_token.mint(&user, &1000); - staking.bond(&user, &1000); - lp_token.mint(&user2, &2000); - staking.bond(&user2, &2000); - lp_token.mint(&user3, &3000); - staking.bond(&user3, &3000); - lp_token.mint(&user4, &4000); - staking.bond(&user4, &4000); - - let eight_days = ONE_WEEK + ONE_DAY; - env.ledger().with_mut(|li| li.timestamp = eight_days); - - let reward_duration = 600; - staking.fund_distribution( - &eight_days, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - env.ledger().with_mut(|li| { - li.timestamp = eight_days + 600; - }); - staking.distribute_rewards(); - - // total staked amount is 10_000 - // user1 should have 10% of the rewards, user2 20%, user3 30%, user4 40% - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 10_000 - } - ] - } - ); - assert_eq!( - staking.query_withdrawable_rewards(&user2), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 20_000 - } - ] - } - ); - assert_eq!( - staking.query_withdrawable_rewards(&user3), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 30_000 - } - ] - } - ); - assert_eq!( - staking.query_withdrawable_rewards(&user4), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 40_000 - } - ] - } - ); - - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 10_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 20_000); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 30_000); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 40_000); } -#[test] -fn two_users_one_starts_after_distribution_begins() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let user2 = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - // first user bonds before distribution started - lp_token.mint(&user, &1000); - env.ledger().with_mut(|li| li.timestamp = ONE_DAY); - staking.bond(&user, &1000); - - let reward_duration = 600; - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - env.ledger().with_mut(|li| { - li.timestamp += 300; - }); - staking.distribute_rewards(); - - // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 50_000 - } - ] - } - ); - - // user2 starts staking after the distribution has begun - lp_token.mint(&user2, &1000); - staking.bond(&user2, &1000); - - env.ledger().with_mut(|li| { - li.timestamp += 300; - }); - staking.distribute_rewards(); - - // first user should get 75_000, second user 25_000 since he joined at the half time - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 75_000 - } - ] - } - ); - assert_eq!( - staking.query_withdrawable_rewards(&user2), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 25_000 - } - ] - } - ); - - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 75_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 25_000); -} - -#[test] -fn two_users_both_bonds_after_distribution_starts() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let user2 = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - env.ledger().with_mut(|li| li.timestamp = ONE_DAY); - - let reward_duration = 600; - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - env.ledger().with_mut(|li| { - li.timestamp += 200; - }); - lp_token.mint(&user, &1000); - staking.bond(&user, &1000); - - staking.distribute_rewards(); - - // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 33_333 - } - ] - } - ); - - // user2 starts staking after the distribution has begun - env.ledger().with_mut(|li| { - li.timestamp += 200; - }); - lp_token.mint(&user2, &1000); - staking.bond(&user2, &1000); - - staking.distribute_rewards(); - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 49_999 - } - ] - } - ); - assert_eq!( - staking.query_withdrawable_rewards(&user2), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 16_666 - } - ] - } - ); - - env.ledger().with_mut(|li| { - li.timestamp += 200; - }); - staking.distribute_rewards(); - - // first user should get 75_000, second user 25_000 since he joined at the half time - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 66_666 - } - ] - } - ); - assert_eq!( - staking.query_withdrawable_rewards(&user2), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 33_333 - } - ] - } - ); - - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 66_666); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 33_333); -} - -#[test] -#[should_panic(expected = "Stake: Fund distribution: Not reward curve exists")] -fn fund_rewards_without_establishing_distribution() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - reward_token.mint(&admin, &1000); - - staking.fund_distribution(&2_000, &600, &reward_token.address, &1000); -} - -#[test] -fn try_to_withdraw_rewards_without_bonding() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - env.ledger().with_mut(|li| { - li.timestamp = 2_000; - }); - - let reward_duration = 600; - staking.fund_distribution( - &2_000, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - env.ledger().with_mut(|li| { - li.timestamp = 2_600; - }); - staking.distribute_rewards(); - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - reward_amount - ); - assert_eq!(staking.query_distributed_rewards(&reward_token.address), 0); - - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 0 - } - ] - } - ); - - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 0); -} - -#[test] -#[should_panic(expected = "Stake: Fund distribution: Fund distribution start time is too early")] -fn fund_distribution_starting_before_current_timestamp() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - env.ledger().with_mut(|li| { - li.timestamp = 2_000; - }); - - let reward_duration = 600; - staking.fund_distribution( - &1_999, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ) -} - -#[test] -#[should_panic(expected = "Stake: Fund distribution: minimum reward amount not reached")] -fn fund_distribution_with_reward_below_required_minimum() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - reward_token.mint(&admin, &10); - - env.ledger().with_mut(|li| { - li.timestamp = 2_000; - }); - - let reward_duration = 600; - staking.fund_distribution(&2_000, &reward_duration, &reward_token.address, &10); -} - -#[test] -fn calculate_apr() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - env.ledger().with_mut(|li| { - li.timestamp = ONE_DAY; - }); - - // whole year of distribution - let reward_duration = 60 * 60 * 24 * 365; - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - // nothing bonded, no rewards - assert_eq!( - staking.query_annualized_rewards(), - AnnualizedRewardsResponse { - rewards: vec![ - &env, - AnnualizedReward { - asset: reward_token.address.clone(), - amount: String::from_str(&env, "0") - } - ] - } - ); - - // bond tokens for user to enable distribution for him - lp_token.mint(&user, &1000); - env.ledger().with_mut(|li| { - li.timestamp += ONE_DAY; - }); - staking.bond(&user, &1000); - - // 100k rewards distributed for the whole year gives 100% APR - assert_eq!( - staking.query_annualized_rewards(), - AnnualizedRewardsResponse { - rewards: vec![ - &env, - AnnualizedReward { - asset: reward_token.address.clone(), - amount: String::from_str(&env, "100000.975274725274725274") - } - ] - } - ); - - let reward_amount: u128 = 50_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - staking.fund_distribution( - &(2 * &ONE_DAY), - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - // having another 50k in rewards increases APR - assert_eq!( - staking.query_annualized_rewards(), - AnnualizedRewardsResponse { - rewards: vec![ - &env, - AnnualizedReward { - asset: reward_token.address.clone(), - amount: String::from_str(&env, "149727") - } - ] - } - ); -} - -#[test] -#[should_panic(expected = "Stake: create distribution: Non-authorized creation!")] -fn add_distribution_should_fail_when_not_authorized() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&Address::generate(&env), &reward_token.address); -} - -#[test] -fn test_v_phx_vul_010_unbond_breakes_reward_distribution() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user_1 = Address::generate(&env); - let user_2 = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - // bond tokens for user to enable distribution for him - lp_token.mint(&user_1, &1_000); - lp_token.mint(&user_2, &1_000); - - env.ledger().with_mut(|li| li.timestamp = ONE_DAY); - staking.bond(&user_1, &1_000); - staking.bond(&user_2, &1_000); - let reward_duration = 10_000; - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - env.ledger().with_mut(|li| { - li.timestamp += 2_000; - }); - - staking.distribute_rewards(); - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - 80_000 // 100k total rewards, we have 2000 seconds passed, so we have 80k undistributed rewards - ); - - // at the 1/2 of the distribution time, user_1 unbonds - env.ledger().with_mut(|li| { - li.timestamp += 3_000; - }); - staking.distribute_rewards(); - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - 50_000 - ); - - // user1 unbonds, which automatically withdraws the rewards - assert_eq!( - staking.query_withdrawable_rewards(&user_1), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 25_000 - } - ] - } - ); - staking.unbond(&user_1, &1_000, &ONE_DAY); - assert_eq!( - staking.query_withdrawable_rewards(&user_1), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 0 - } - ] - } - ); - - env.ledger().with_mut(|li| { - li.timestamp += 10_000; - }); - - staking.distribute_rewards(); - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - 0 - ); - assert_eq!( - staking.query_distributed_rewards(&reward_token.address), - reward_amount - ); - - assert_eq!( - staking.query_withdrawable_rewards(&user_2), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 75_000 - } - ] - } - ); - - staking.withdraw_rewards(&user_1); - assert_eq!(reward_token.balance(&user_1), 25_000i128); -} - -#[test] -fn test_bond_withdraw_unbond() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let user = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - let reward_amount: u128 = 100_000; - reward_token.mint(&admin, &(reward_amount as i128)); - - lp_token.mint(&user, &1_000); - env.ledger().with_mut(|li| li.timestamp = ONE_DAY); - staking.bond(&user, &1_000); - - let reward_duration = 10_000; - - staking.fund_distribution( - &ONE_DAY, - &reward_duration, - &reward_token.address, - &(reward_amount as i128), - ); - - env.ledger().with_mut(|li| { - li.timestamp = ONE_DAY + reward_duration; - }); - - staking.distribute_rewards(); - - staking.unbond(&user, &1_000, &ONE_DAY); - - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 0 - } - ] - } - ); - // one more time to make sure that calculations during unbond aren't off - staking.withdraw_rewards(&user); - assert_eq!( - staking.query_withdrawable_rewards(&user), - WithdrawableRewardsResponse { - rewards: vec![ - &env, - WithdrawableReward { - reward_address: reward_token.address.clone(), - reward_amount: 0 - } - ] - } - ); -} - -#[should_panic(expected = "Stake: Add distribution: Distribution already added")] -#[test] -fn panic_when_adding_same_distribution_twice() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &50u32, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - staking.create_distribution_flow(&manager, &reward_token.address); -} - -#[should_panic(expected = "Stake: Fund distribution: Curve complexity validation failed")] -#[test] -fn panic_when_funding_distribution_with_curve_too_complex() { - const DISTRIBUTION_MAX_COMPLEXITY: u32 = 3; - const FIVE_MINUTES: u64 = 300; - const TEN_MINUTES: u64 = 600; - const ONE_WEEK: u64 = 604_800; - - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let manager = Address::generate(&env); - let owner = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let staking = deploy_staking_contract( - &env, - admin.clone(), - &lp_token.address, - &manager, - &owner, - &DISTRIBUTION_MAX_COMPLEXITY, - ); - - staking.create_distribution_flow(&manager, &reward_token.address); - - reward_token.mint(&admin, &3000); - - staking.fund_distribution(&0, &FIVE_MINUTES, &reward_token.address, &1000); - staking.fund_distribution(&FIVE_MINUTES, &TEN_MINUTES, &reward_token.address, &1000); - - // assert just to prove that we have 2 successful fund distributions - assert_eq!( - staking.query_undistributed_rewards(&reward_token.address), - 2000 - ); - - // uh-oh fail - staking.fund_distribution(&TEN_MINUTES, &ONE_WEEK, &reward_token.address, &1000); -} +// #[test] +// fn two_distributions() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// let reward_token_2 = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// staking.create_distribution_flow(&manager, &reward_token_2.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// reward_token_2.mint(&admin, &((reward_amount * 2) as i128)); +// +// // bond tokens for user to enable distribution for him +// lp_token.mint(&user, &1000); +// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); +// staking.bond(&user, &1000); +// +// let reward_duration = 600; +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token_2.address, +// &((reward_amount * 2) as i128), +// ); +// +// // distribute rewards during half time +// env.ledger().with_mut(|li| { +// li.timestamp += 300; +// }); +// staking.distribute_rewards(); +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: reward_amount / 2 +// }, +// WithdrawableReward { +// reward_address: reward_token_2.address.clone(), +// reward_amount +// } +// ] +// } +// ); +// staking.withdraw_rewards(&user); +// assert_eq!(reward_token.balance(&user), (reward_amount / 2) as i128); +// assert_eq!(reward_token_2.balance(&user), reward_amount as i128); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 600; +// }); +// staking.distribute_rewards(); +// // first reward token +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// 0 +// ); +// assert_eq!( +// staking.query_distributed_rewards(&reward_token.address), +// reward_amount +// ); +// // second reward token +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token_2.address), +// 0 +// ); +// assert_eq!( +// staking.query_distributed_rewards(&reward_token_2.address), +// reward_amount * 2 +// ); +// +// // since half of rewards were already distributed, after full distirubtion +// // round another half is ready +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: reward_amount / 2 +// }, +// WithdrawableReward { +// reward_address: reward_token_2.address.clone(), +// reward_amount +// } +// ] +// } +// ); +// +// staking.withdraw_rewards(&user); +// assert_eq!(reward_token.balance(&user), reward_amount as i128); +// assert_eq!(reward_token_2.balance(&user), (reward_amount * 2) as i128); +// } +// +// #[test] +// fn four_users_with_different_stakes() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let user2 = Address::generate(&env); +// let user3 = Address::generate(&env); +// let user4 = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// // bond tokens for users; each user has a different amount staked +// env.ledger().with_mut(|li| { +// li.timestamp = ONE_WEEK; +// }); +// +// lp_token.mint(&user, &1000); +// staking.bond(&user, &1000); +// lp_token.mint(&user2, &2000); +// staking.bond(&user2, &2000); +// lp_token.mint(&user3, &3000); +// staking.bond(&user3, &3000); +// lp_token.mint(&user4, &4000); +// staking.bond(&user4, &4000); +// +// let eight_days = ONE_WEEK + ONE_DAY; +// env.ledger().with_mut(|li| li.timestamp = eight_days); +// +// let reward_duration = 600; +// staking.fund_distribution( +// &eight_days, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp = eight_days + 600; +// }); +// staking.distribute_rewards(); +// +// // total staked amount is 10_000 +// // user1 should have 10% of the rewards, user2 20%, user3 30%, user4 40% +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 10_000 +// } +// ] +// } +// ); +// assert_eq!( +// staking.query_withdrawable_rewards(&user2), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 20_000 +// } +// ] +// } +// ); +// assert_eq!( +// staking.query_withdrawable_rewards(&user3), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 30_000 +// } +// ] +// } +// ); +// assert_eq!( +// staking.query_withdrawable_rewards(&user4), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 40_000 +// } +// ] +// } +// ); +// +// staking.withdraw_rewards(&user); +// assert_eq!(reward_token.balance(&user), 10_000); +// staking.withdraw_rewards(&user2); +// assert_eq!(reward_token.balance(&user2), 20_000); +// staking.withdraw_rewards(&user3); +// assert_eq!(reward_token.balance(&user3), 30_000); +// staking.withdraw_rewards(&user4); +// assert_eq!(reward_token.balance(&user4), 40_000); +// } +// +// #[test] +// fn two_users_one_starts_after_distribution_begins() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let user2 = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// // first user bonds before distribution started +// lp_token.mint(&user, &1000); +// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); +// staking.bond(&user, &1000); +// +// let reward_duration = 600; +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 300; +// }); +// staking.distribute_rewards(); +// +// // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 50_000 +// } +// ] +// } +// ); +// +// // user2 starts staking after the distribution has begun +// lp_token.mint(&user2, &1000); +// staking.bond(&user2, &1000); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 300; +// }); +// staking.distribute_rewards(); +// +// // first user should get 75_000, second user 25_000 since he joined at the half time +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 75_000 +// } +// ] +// } +// ); +// assert_eq!( +// staking.query_withdrawable_rewards(&user2), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 25_000 +// } +// ] +// } +// ); +// +// staking.withdraw_rewards(&user); +// assert_eq!(reward_token.balance(&user), 75_000); +// staking.withdraw_rewards(&user2); +// assert_eq!(reward_token.balance(&user2), 25_000); +// } +// +// #[test] +// fn two_users_both_bonds_after_distribution_starts() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let user2 = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); +// +// let reward_duration = 600; +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 200; +// }); +// lp_token.mint(&user, &1000); +// staking.bond(&user, &1000); +// +// staking.distribute_rewards(); +// +// // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 33_333 +// } +// ] +// } +// ); +// +// // user2 starts staking after the distribution has begun +// env.ledger().with_mut(|li| { +// li.timestamp += 200; +// }); +// lp_token.mint(&user2, &1000); +// staking.bond(&user2, &1000); +// +// staking.distribute_rewards(); +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 49_999 +// } +// ] +// } +// ); +// assert_eq!( +// staking.query_withdrawable_rewards(&user2), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 16_666 +// } +// ] +// } +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 200; +// }); +// staking.distribute_rewards(); +// +// // first user should get 75_000, second user 25_000 since he joined at the half time +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 66_666 +// } +// ] +// } +// ); +// assert_eq!( +// staking.query_withdrawable_rewards(&user2), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 33_333 +// } +// ] +// } +// ); +// +// staking.withdraw_rewards(&user); +// assert_eq!(reward_token.balance(&user), 66_666); +// staking.withdraw_rewards(&user2); +// assert_eq!(reward_token.balance(&user2), 33_333); +// } +// +// #[test] +// #[should_panic(expected = "Stake: Fund distribution: Not reward curve exists")] +// fn fund_rewards_without_establishing_distribution() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// reward_token.mint(&admin, &1000); +// +// staking.fund_distribution(&2_000, &600, &reward_token.address, &1000); +// } +// +// #[test] +// fn try_to_withdraw_rewards_without_bonding() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// env.ledger().with_mut(|li| { +// li.timestamp = 2_000; +// }); +// +// let reward_duration = 600; +// staking.fund_distribution( +// &2_000, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp = 2_600; +// }); +// staking.distribute_rewards(); +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// reward_amount +// ); +// assert_eq!(staking.query_distributed_rewards(&reward_token.address), 0); +// +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 0 +// } +// ] +// } +// ); +// +// staking.withdraw_rewards(&user); +// assert_eq!(reward_token.balance(&user), 0); +// } +// +// #[test] +// #[should_panic(expected = "Stake: Fund distribution: Fund distribution start time is too early")] +// fn fund_distribution_starting_before_current_timestamp() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// env.ledger().with_mut(|li| { +// li.timestamp = 2_000; +// }); +// +// let reward_duration = 600; +// staking.fund_distribution( +// &1_999, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ) +// } +// +// #[test] +// #[should_panic(expected = "Stake: Fund distribution: minimum reward amount not reached")] +// fn fund_distribution_with_reward_below_required_minimum() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// reward_token.mint(&admin, &10); +// +// env.ledger().with_mut(|li| { +// li.timestamp = 2_000; +// }); +// +// let reward_duration = 600; +// staking.fund_distribution(&2_000, &reward_duration, &reward_token.address, &10); +// } +// +// #[test] +// fn calculate_apr() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// env.ledger().with_mut(|li| { +// li.timestamp = ONE_DAY; +// }); +// +// // whole year of distribution +// let reward_duration = 60 * 60 * 24 * 365; +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// // nothing bonded, no rewards +// assert_eq!( +// staking.query_annualized_rewards(), +// AnnualizedRewardsResponse { +// rewards: vec![ +// &env, +// AnnualizedReward { +// asset: reward_token.address.clone(), +// amount: String::from_str(&env, "0") +// } +// ] +// } +// ); +// +// // bond tokens for user to enable distribution for him +// lp_token.mint(&user, &1000); +// env.ledger().with_mut(|li| { +// li.timestamp += ONE_DAY; +// }); +// staking.bond(&user, &1000); +// +// // 100k rewards distributed for the whole year gives 100% APR +// assert_eq!( +// staking.query_annualized_rewards(), +// AnnualizedRewardsResponse { +// rewards: vec![ +// &env, +// AnnualizedReward { +// asset: reward_token.address.clone(), +// amount: String::from_str(&env, "100000.975274725274725274") +// } +// ] +// } +// ); +// +// let reward_amount: u128 = 50_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// staking.fund_distribution( +// &(2 * &ONE_DAY), +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// // having another 50k in rewards increases APR +// assert_eq!( +// staking.query_annualized_rewards(), +// AnnualizedRewardsResponse { +// rewards: vec![ +// &env, +// AnnualizedReward { +// asset: reward_token.address.clone(), +// amount: String::from_str(&env, "149727") +// } +// ] +// } +// ); +// } +// +// #[test] +// #[should_panic(expected = "Stake: create distribution: Non-authorized creation!")] +// fn add_distribution_should_fail_when_not_authorized() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&Address::generate(&env), &reward_token.address); +// } +// +// #[test] +// fn test_v_phx_vul_010_unbond_breakes_reward_distribution() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user_1 = Address::generate(&env); +// let user_2 = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// // bond tokens for user to enable distribution for him +// lp_token.mint(&user_1, &1_000); +// lp_token.mint(&user_2, &1_000); +// +// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); +// staking.bond(&user_1, &1_000); +// staking.bond(&user_2, &1_000); +// let reward_duration = 10_000; +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 2_000; +// }); +// +// staking.distribute_rewards(); +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// 80_000 // 100k total rewards, we have 2000 seconds passed, so we have 80k undistributed rewards +// ); +// +// // at the 1/2 of the distribution time, user_1 unbonds +// env.ledger().with_mut(|li| { +// li.timestamp += 3_000; +// }); +// staking.distribute_rewards(); +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// 50_000 +// ); +// +// // user1 unbonds, which automatically withdraws the rewards +// assert_eq!( +// staking.query_withdrawable_rewards(&user_1), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 25_000 +// } +// ] +// } +// ); +// staking.unbond(&user_1, &1_000, &ONE_DAY); +// assert_eq!( +// staking.query_withdrawable_rewards(&user_1), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 0 +// } +// ] +// } +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp += 10_000; +// }); +// +// staking.distribute_rewards(); +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// 0 +// ); +// assert_eq!( +// staking.query_distributed_rewards(&reward_token.address), +// reward_amount +// ); +// +// assert_eq!( +// staking.query_withdrawable_rewards(&user_2), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 75_000 +// } +// ] +// } +// ); +// +// staking.withdraw_rewards(&user_1); +// assert_eq!(reward_token.balance(&user_1), 25_000i128); +// } +// +// #[test] +// fn test_bond_withdraw_unbond() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let user = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// let reward_amount: u128 = 100_000; +// reward_token.mint(&admin, &(reward_amount as i128)); +// +// lp_token.mint(&user, &1_000); +// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); +// staking.bond(&user, &1_000); +// +// let reward_duration = 10_000; +// +// staking.fund_distribution( +// &ONE_DAY, +// &reward_duration, +// &reward_token.address, +// &(reward_amount as i128), +// ); +// +// env.ledger().with_mut(|li| { +// li.timestamp = ONE_DAY + reward_duration; +// }); +// +// staking.distribute_rewards(); +// +// staking.unbond(&user, &1_000, &ONE_DAY); +// +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 0 +// } +// ] +// } +// ); +// // one more time to make sure that calculations during unbond aren't off +// staking.withdraw_rewards(&user); +// assert_eq!( +// staking.query_withdrawable_rewards(&user), +// WithdrawableRewardsResponse { +// rewards: vec![ +// &env, +// WithdrawableReward { +// reward_address: reward_token.address.clone(), +// reward_amount: 0 +// } +// ] +// } +// ); +// } +// +// #[should_panic(expected = "Stake: Add distribution: Distribution already added")] +// #[test] +// fn panic_when_adding_same_distribution_twice() { +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &50u32, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// staking.create_distribution_flow(&manager, &reward_token.address); +// } +// +// #[should_panic(expected = "Stake: Fund distribution: Curve complexity validation failed")] +// #[test] +// fn panic_when_funding_distribution_with_curve_too_complex() { +// const DISTRIBUTION_MAX_COMPLEXITY: u32 = 3; +// const FIVE_MINUTES: u64 = 300; +// const TEN_MINUTES: u64 = 600; +// const ONE_WEEK: u64 = 604_800; +// +// let env = Env::default(); +// env.mock_all_auths(); +// +// let admin = Address::generate(&env); +// let manager = Address::generate(&env); +// let owner = Address::generate(&env); +// let lp_token = deploy_token_contract(&env, &admin); +// let reward_token = deploy_token_contract(&env, &admin); +// +// let staking = deploy_staking_contract( +// &env, +// admin.clone(), +// &lp_token.address, +// &manager, +// &owner, +// &DISTRIBUTION_MAX_COMPLEXITY, +// ); +// +// staking.create_distribution_flow(&manager, &reward_token.address); +// +// reward_token.mint(&admin, &3000); +// +// staking.fund_distribution(&0, &FIVE_MINUTES, &reward_token.address, &1000); +// staking.fund_distribution(&FIVE_MINUTES, &TEN_MINUTES, &reward_token.address, &1000); +// +// // assert just to prove that we have 2 successful fund distributions +// assert_eq!( +// staking.query_undistributed_rewards(&reward_token.address), +// 2000 +// ); +// +// // uh-oh fail +// staking.fund_distribution(&TEN_MINUTES, &ONE_WEEK, &reward_token.address, &1000); +// } diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 1ab9a8a95..18b11dad4 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -21,6 +21,7 @@ const MIN_BOND: i128 = 1000; const MIN_REWARD: i128 = 1000; pub const ONE_WEEK: u64 = 604800; pub const ONE_DAY: u64 = 86400; +pub const SIXTY_DAYS: u64 = 60 * ONE_DAY; pub fn deploy_staking_contract<'a>( env: &Env, From 9300e188c674075e507ff46689382fd798878a6c Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 10:29:13 +0200 Subject: [PATCH 08/16] Stake: Two distributions test --- contracts/stake/src/tests/distribution.rs | 255 ++++++++++++---------- 1 file changed, 135 insertions(+), 120 deletions(-) diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index 58267e010..0469fb8df 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -163,126 +163,141 @@ fn add_distribution_and_distribute_reward() { assert_eq!(reward_token.balance(&user), reward_amount as i128); } -// #[test] -// fn two_distributions() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// let reward_token_2 = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// staking.create_distribution_flow(&manager, &reward_token_2.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// reward_token_2.mint(&admin, &((reward_amount * 2) as i128)); -// -// // bond tokens for user to enable distribution for him -// lp_token.mint(&user, &1000); -// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); -// staking.bond(&user, &1000); -// -// let reward_duration = 600; -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token_2.address, -// &((reward_amount * 2) as i128), -// ); -// -// // distribute rewards during half time -// env.ledger().with_mut(|li| { -// li.timestamp += 300; -// }); -// staking.distribute_rewards(); -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: reward_amount / 2 -// }, -// WithdrawableReward { -// reward_address: reward_token_2.address.clone(), -// reward_amount -// } -// ] -// } -// ); -// staking.withdraw_rewards(&user); -// assert_eq!(reward_token.balance(&user), (reward_amount / 2) as i128); -// assert_eq!(reward_token_2.balance(&user), reward_amount as i128); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 600; -// }); -// staking.distribute_rewards(); -// // first reward token -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// 0 -// ); -// assert_eq!( -// staking.query_distributed_rewards(&reward_token.address), -// reward_amount -// ); -// // second reward token -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token_2.address), -// 0 -// ); -// assert_eq!( -// staking.query_distributed_rewards(&reward_token_2.address), -// reward_amount * 2 -// ); -// -// // since half of rewards were already distributed, after full distirubtion -// // round another half is ready -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: reward_amount / 2 -// }, -// WithdrawableReward { -// reward_address: reward_token_2.address.clone(), -// reward_amount -// } -// ] -// } -// ); -// -// staking.withdraw_rewards(&user); -// assert_eq!(reward_token.balance(&user), reward_amount as i128); -// assert_eq!(reward_token_2.balance(&user), (reward_amount * 2) as i128); -// } -// +#[test] +fn two_distributions() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + let reward_token_2 = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + staking.create_distribution_flow( + &admin, + &reward_token_2.address, + &BytesN::from_array(&env, &[2; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + reward_token_2.mint(&admin, &((reward_amount * 2) as i128)); + + // bond tokens for user to enable distribution for him + lp_token.mint(&user, &1000); + staking.bond(&user, &1000); + // simulate moving forward 60 days for the full APR multiplier + env.ledger().with_mut(|li| li.timestamp = SIXTY_DAYS); + + let reward_duration = 600; + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token_2.address, + &((reward_amount * 2) as i128), + ); + + // distribute rewards during half time + env.ledger().with_mut(|li| { + li.timestamp += 300; + }); + staking.distribute_rewards(); + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: reward_amount / 2 + }, + WithdrawableReward { + reward_address: reward_token_2.address.clone(), + reward_amount + } + ] + } + ); + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), (reward_amount / 2) as i128); + assert_eq!(reward_token_2.balance(&user), reward_amount as i128); + + env.ledger().with_mut(|li| { + li.timestamp += 600; + }); + staking.distribute_rewards(); + // first reward token + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 0 + ); + assert_eq!( + staking.query_distributed_rewards(&reward_token.address), + reward_amount + ); + // second reward token + assert_eq!( + staking.query_undistributed_rewards(&reward_token_2.address), + 0 + ); + assert_eq!( + staking.query_distributed_rewards(&reward_token_2.address), + reward_amount * 2 + ); + + // since half of rewards were already distributed, after full distirubtion + // round another half is ready + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: reward_amount / 2 + }, + WithdrawableReward { + reward_address: reward_token_2.address.clone(), + reward_amount + } + ] + } + ); + + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), reward_amount as i128); + assert_eq!(reward_token_2.balance(&user), (reward_amount * 2) as i128); +} + // #[test] // fn four_users_with_different_stakes() { // let env = Env::default(); From 2cf83aa8bffc69f14a20a1a8735532dd5acd5ee6 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 10:32:00 +0200 Subject: [PATCH 09/16] Stake: Four users with different stakes --- contracts/stake/src/tests/distribution.rs | 247 +++++++++++----------- 1 file changed, 126 insertions(+), 121 deletions(-) diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index 0469fb8df..91461bb75 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -298,127 +298,132 @@ fn two_distributions() { assert_eq!(reward_token_2.balance(&user), (reward_amount * 2) as i128); } -// #[test] -// fn four_users_with_different_stakes() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let user2 = Address::generate(&env); -// let user3 = Address::generate(&env); -// let user4 = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// // bond tokens for users; each user has a different amount staked -// env.ledger().with_mut(|li| { -// li.timestamp = ONE_WEEK; -// }); -// -// lp_token.mint(&user, &1000); -// staking.bond(&user, &1000); -// lp_token.mint(&user2, &2000); -// staking.bond(&user2, &2000); -// lp_token.mint(&user3, &3000); -// staking.bond(&user3, &3000); -// lp_token.mint(&user4, &4000); -// staking.bond(&user4, &4000); -// -// let eight_days = ONE_WEEK + ONE_DAY; -// env.ledger().with_mut(|li| li.timestamp = eight_days); -// -// let reward_duration = 600; -// staking.fund_distribution( -// &eight_days, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp = eight_days + 600; -// }); -// staking.distribute_rewards(); -// -// // total staked amount is 10_000 -// // user1 should have 10% of the rewards, user2 20%, user3 30%, user4 40% -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 10_000 -// } -// ] -// } -// ); -// assert_eq!( -// staking.query_withdrawable_rewards(&user2), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 20_000 -// } -// ] -// } -// ); -// assert_eq!( -// staking.query_withdrawable_rewards(&user3), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 30_000 -// } -// ] -// } -// ); -// assert_eq!( -// staking.query_withdrawable_rewards(&user4), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 40_000 -// } -// ] -// } -// ); -// -// staking.withdraw_rewards(&user); -// assert_eq!(reward_token.balance(&user), 10_000); -// staking.withdraw_rewards(&user2); -// assert_eq!(reward_token.balance(&user2), 20_000); -// staking.withdraw_rewards(&user3); -// assert_eq!(reward_token.balance(&user3), 30_000); -// staking.withdraw_rewards(&user4); -// assert_eq!(reward_token.balance(&user4), 40_000); -// } -// +#[test] +fn four_users_with_different_stakes() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let user2 = Address::generate(&env); + let user3 = Address::generate(&env); + let user4 = Address::generate(&env); + let manager = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + // bond tokens for users; each user has a different amount staked + lp_token.mint(&user, &1000); + staking.bond(&user, &1000); + lp_token.mint(&user2, &2000); + staking.bond(&user2, &2000); + lp_token.mint(&user3, &3000); + staking.bond(&user3, &3000); + lp_token.mint(&user4, &4000); + staking.bond(&user4, &4000); + + // simulate moving forward 60 days for the full APR multiplier + env.ledger().with_mut(|li| { + li.timestamp = SIXTY_DAYS; + }); + + let reward_duration = 600; + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp += 600; + }); + staking.distribute_rewards(); + + // total staked amount is 10_000 + // user1 should have 10% of the rewards, user2 20%, user3 30%, user4 40% + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 10_000 + } + ] + } + ); + assert_eq!( + staking.query_withdrawable_rewards(&user2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 20_000 + } + ] + } + ); + assert_eq!( + staking.query_withdrawable_rewards(&user3), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 30_000 + } + ] + } + ); + assert_eq!( + staking.query_withdrawable_rewards(&user4), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 40_000 + } + ] + } + ); + + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), 10_000); + staking.withdraw_rewards(&user2); + assert_eq!(reward_token.balance(&user2), 20_000); + staking.withdraw_rewards(&user3); + assert_eq!(reward_token.balance(&user3), 30_000); + staking.withdraw_rewards(&user4); + assert_eq!(reward_token.balance(&user4), 40_000); +} + // #[test] // fn two_users_one_starts_after_distribution_begins() { // let env = Env::default(); From d3a56574cc210fe2cd7c169994e87a1e236b6879 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 10:54:08 +0200 Subject: [PATCH 10/16] Stake: Fund rewards without establishing distribution --- contracts/stake/src/contract.rs | 13 +- contracts/stake/src/error.rs | 1 + contracts/stake/src/tests/distribution.rs | 500 +++++++++++----------- 3 files changed, 256 insertions(+), 258 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 51e015d00..c30a062f1 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -356,10 +356,21 @@ impl StakingTrait for Staking { let admin = get_admin(&env); admin.require_auth(); + let stake_rewards = if let Some(address) = find_stake_rewards_by_asset(&env, &token_address) + { + address + } else { + log!( + env, + "Stake: Fund distribution: No distribution for this reward token exists!" + ); + panic_with_error!(&env, ContractError::DistributionNotFound); + }; + let fund_distr_fn_arg: Vec<Val> = (start_time, distribution_duration, token_amount.clone()).into_val(&env); env.invoke_contract::<Val>( - &find_stake_rewards_by_asset(&env, &token_address).unwrap(), + &stake_rewards, &Symbol::new(&env, "fund_distribution"), fund_distr_fn_arg, ); diff --git a/contracts/stake/src/error.rs b/contracts/stake/src/error.rs index f4f739fa3..f69160aca 100644 --- a/contracts/stake/src/error.rs +++ b/contracts/stake/src/error.rs @@ -16,4 +16,5 @@ pub enum ContractError { DistributionExists = 10, InvalidRewardAmount = 11, InvalidMaxComplexity = 12, + DistributionNotFound = 13, } diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index 91461bb75..c49d2b20f 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -424,263 +424,249 @@ fn four_users_with_different_stakes() { assert_eq!(reward_token.balance(&user4), 40_000); } -// #[test] -// fn two_users_one_starts_after_distribution_begins() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let user2 = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// // first user bonds before distribution started -// lp_token.mint(&user, &1000); -// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); -// staking.bond(&user, &1000); -// -// let reward_duration = 600; -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 300; -// }); -// staking.distribute_rewards(); -// -// // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 50_000 -// } -// ] -// } -// ); -// -// // user2 starts staking after the distribution has begun -// lp_token.mint(&user2, &1000); -// staking.bond(&user2, &1000); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 300; -// }); -// staking.distribute_rewards(); -// -// // first user should get 75_000, second user 25_000 since he joined at the half time -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 75_000 -// } -// ] -// } -// ); -// assert_eq!( -// staking.query_withdrawable_rewards(&user2), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 25_000 -// } -// ] -// } -// ); -// -// staking.withdraw_rewards(&user); -// assert_eq!(reward_token.balance(&user), 75_000); -// staking.withdraw_rewards(&user2); -// assert_eq!(reward_token.balance(&user2), 25_000); -// } -// -// #[test] -// fn two_users_both_bonds_after_distribution_starts() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let user2 = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); -// -// let reward_duration = 600; -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 200; -// }); -// lp_token.mint(&user, &1000); -// staking.bond(&user, &1000); -// -// staking.distribute_rewards(); -// -// // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 33_333 -// } -// ] -// } -// ); -// -// // user2 starts staking after the distribution has begun -// env.ledger().with_mut(|li| { -// li.timestamp += 200; -// }); -// lp_token.mint(&user2, &1000); -// staking.bond(&user2, &1000); -// -// staking.distribute_rewards(); -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 49_999 -// } -// ] -// } -// ); -// assert_eq!( -// staking.query_withdrawable_rewards(&user2), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 16_666 -// } -// ] -// } -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 200; -// }); -// staking.distribute_rewards(); -// -// // first user should get 75_000, second user 25_000 since he joined at the half time -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 66_666 -// } -// ] -// } -// ); -// assert_eq!( -// staking.query_withdrawable_rewards(&user2), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 33_333 -// } -// ] -// } -// ); -// -// staking.withdraw_rewards(&user); -// assert_eq!(reward_token.balance(&user), 66_666); -// staking.withdraw_rewards(&user2); -// assert_eq!(reward_token.balance(&user2), 33_333); -// } -// -// #[test] -// #[should_panic(expected = "Stake: Fund distribution: Not reward curve exists")] -// fn fund_rewards_without_establishing_distribution() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// reward_token.mint(&admin, &1000); -// -// staking.fund_distribution(&2_000, &600, &reward_token.address, &1000); -// } -// +#[test] +fn two_users_one_starts_after_distribution_begins() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let user2 = Address::generate(&env); + let manager = Address::generate(&env); + let owner = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + // first user bonds before distribution started + lp_token.mint(&user, &1000); + staking.bond(&user, &1000); + // simulate moving forward 60 days for the full APR multiplier + env.ledger().with_mut(|li| li.timestamp = SIXTY_DAYS); + + let reward_duration = 600; + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp += 300; + }); + staking.distribute_rewards(); + + // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 50_000 + } + ] + } + ); + + // user2 starts staking after the distribution has begun + lp_token.mint(&user2, &1000); + staking.bond(&user2, &1000); + + // Second user bonded later; we again simulate moving his stakes up to 60 days + env.ledger().with_mut(|li| { + li.timestamp += SIXTY_DAYS; + }); + staking.distribute_rewards(); + + // first user should get 75_000, second user 25_000 since he joined at the half time + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 75_000 + } + ] + } + ); + assert_eq!( + staking.query_withdrawable_rewards(&user2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 25_000 + } + ] + } + ); + + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), 75_000); + staking.withdraw_rewards(&user2); + assert_eq!(reward_token.balance(&user2), 25_000); +} + +#[test] +fn two_users_both_bonds_after_distribution_starts() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let user2 = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + let reward_duration = SIXTY_DAYS * 2; + staking.fund_distribution( + &0, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + lp_token.mint(&user, &1000); + staking.bond(&user, &1000); + env.ledger().with_mut(|li| { + li.timestamp = SIXTY_DAYS; + }); + + staking.distribute_rewards(); + + // at this points, since half of the time has passed and only one user is staking, he should have 50% of the rewards + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 50_000 + } + ] + } + ); + + // user2 starts staking after the distribution has begun + lp_token.mint(&user2, &1000); + staking.bond(&user2, &1000); + // we move time to the end of the distribution + env.ledger().with_mut(|li| { + li.timestamp = SIXTY_DAYS * 2; + }); + + staking.distribute_rewards(); + // user 1 was the only who bonded for the first half time + // and then he had 50% for the second half + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 75_000 + } + ] + } + ); + assert_eq!( + staking.query_withdrawable_rewards(&user2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 25_000 + } + ] + } + ); + + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), 75_000); + staking.withdraw_rewards(&user2); + assert_eq!(reward_token.balance(&user2), 25_000); +} + +#[test] +#[should_panic(expected = "Stake: Fund distribution: No distribution for this reward token exists")] +fn fund_rewards_without_establishing_distribution() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + reward_token.mint(&admin, &1000); + + staking.fund_distribution(&2_000, &600, &reward_token.address, &1000); +} + // #[test] // fn try_to_withdraw_rewards_without_bonding() { // let env = Env::default(); From 22657660a635d63bd463872c970079a5eb79edbc Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 12:53:39 +0200 Subject: [PATCH 11/16] Stake: Finish refactoring all distribution tests --- contracts/stake/src/contract.rs | 13 +- contracts/stake/src/tests/distribution.rs | 1147 +++++++++++---------- 2 files changed, 633 insertions(+), 527 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index c30a062f1..dc1f95aef 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -267,6 +267,15 @@ impl StakingTrait for Staking { log!(env, "Stake: create distribution: Non-authorized creation!"); panic_with_error!(&env, ContractError::Unauthorized); } + + if let Some(address) = find_stake_rewards_by_asset(&env, &asset) { + log!( + env, + "Stake: Create distribution flow: Distribution for this reward token exists!" + ); + panic_with_error!(&env, ContractError::DistributionExists); + }; + let deployed_stake_rewards = env .deployer() .with_address(env.current_contract_address(), salt) @@ -416,7 +425,7 @@ impl StakingTrait for Staking { let total_stake_amount = get_total_staked_counter(&env); let apr_fn_arg: Val = total_stake_amount.into_val(&env); - for (_asset, distribution_address) in get_distributions(&env) { + for (asset, distribution_address) in get_distributions(&env) { let apr: AnnualizedReward = env.invoke_contract( &distribution_address, &Symbol::new(&env, "query_annualized_reward"), @@ -424,7 +433,7 @@ impl StakingTrait for Staking { ); aprs.push_back(AnnualizedReward { - asset: distribution_address.clone(), + asset, amount: apr.amount, }); } diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index c49d2b20f..53760abde 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -667,528 +667,625 @@ fn fund_rewards_without_establishing_distribution() { staking.fund_distribution(&2_000, &600, &reward_token.address, &1000); } -// #[test] -// fn try_to_withdraw_rewards_without_bonding() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// env.ledger().with_mut(|li| { -// li.timestamp = 2_000; -// }); -// -// let reward_duration = 600; -// staking.fund_distribution( -// &2_000, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp = 2_600; -// }); -// staking.distribute_rewards(); -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// reward_amount -// ); -// assert_eq!(staking.query_distributed_rewards(&reward_token.address), 0); -// -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 0 -// } -// ] -// } -// ); -// -// staking.withdraw_rewards(&user); -// assert_eq!(reward_token.balance(&user), 0); -// } -// -// #[test] -// #[should_panic(expected = "Stake: Fund distribution: Fund distribution start time is too early")] -// fn fund_distribution_starting_before_current_timestamp() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// env.ledger().with_mut(|li| { -// li.timestamp = 2_000; -// }); -// -// let reward_duration = 600; -// staking.fund_distribution( -// &1_999, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ) -// } -// -// #[test] -// #[should_panic(expected = "Stake: Fund distribution: minimum reward amount not reached")] -// fn fund_distribution_with_reward_below_required_minimum() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// reward_token.mint(&admin, &10); -// -// env.ledger().with_mut(|li| { -// li.timestamp = 2_000; -// }); -// -// let reward_duration = 600; -// staking.fund_distribution(&2_000, &reward_duration, &reward_token.address, &10); -// } -// -// #[test] -// fn calculate_apr() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// env.ledger().with_mut(|li| { -// li.timestamp = ONE_DAY; -// }); -// -// // whole year of distribution -// let reward_duration = 60 * 60 * 24 * 365; -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// // nothing bonded, no rewards -// assert_eq!( -// staking.query_annualized_rewards(), -// AnnualizedRewardsResponse { -// rewards: vec![ -// &env, -// AnnualizedReward { -// asset: reward_token.address.clone(), -// amount: String::from_str(&env, "0") -// } -// ] -// } -// ); -// -// // bond tokens for user to enable distribution for him -// lp_token.mint(&user, &1000); -// env.ledger().with_mut(|li| { -// li.timestamp += ONE_DAY; -// }); -// staking.bond(&user, &1000); -// -// // 100k rewards distributed for the whole year gives 100% APR -// assert_eq!( -// staking.query_annualized_rewards(), -// AnnualizedRewardsResponse { -// rewards: vec![ -// &env, -// AnnualizedReward { -// asset: reward_token.address.clone(), -// amount: String::from_str(&env, "100000.975274725274725274") -// } -// ] -// } -// ); -// -// let reward_amount: u128 = 50_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// staking.fund_distribution( -// &(2 * &ONE_DAY), -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// // having another 50k in rewards increases APR -// assert_eq!( -// staking.query_annualized_rewards(), -// AnnualizedRewardsResponse { -// rewards: vec![ -// &env, -// AnnualizedReward { -// asset: reward_token.address.clone(), -// amount: String::from_str(&env, "149727") -// } -// ] -// } -// ); -// } -// -// #[test] -// #[should_panic(expected = "Stake: create distribution: Non-authorized creation!")] -// fn add_distribution_should_fail_when_not_authorized() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&Address::generate(&env), &reward_token.address); -// } -// -// #[test] -// fn test_v_phx_vul_010_unbond_breakes_reward_distribution() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user_1 = Address::generate(&env); -// let user_2 = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// // bond tokens for user to enable distribution for him -// lp_token.mint(&user_1, &1_000); -// lp_token.mint(&user_2, &1_000); -// -// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); -// staking.bond(&user_1, &1_000); -// staking.bond(&user_2, &1_000); -// let reward_duration = 10_000; -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 2_000; -// }); -// -// staking.distribute_rewards(); -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// 80_000 // 100k total rewards, we have 2000 seconds passed, so we have 80k undistributed rewards -// ); -// -// // at the 1/2 of the distribution time, user_1 unbonds -// env.ledger().with_mut(|li| { -// li.timestamp += 3_000; -// }); -// staking.distribute_rewards(); -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// 50_000 -// ); -// -// // user1 unbonds, which automatically withdraws the rewards -// assert_eq!( -// staking.query_withdrawable_rewards(&user_1), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 25_000 -// } -// ] -// } -// ); -// staking.unbond(&user_1, &1_000, &ONE_DAY); -// assert_eq!( -// staking.query_withdrawable_rewards(&user_1), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 0 -// } -// ] -// } -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 10_000; -// }); -// -// staking.distribute_rewards(); -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// 0 -// ); -// assert_eq!( -// staking.query_distributed_rewards(&reward_token.address), -// reward_amount -// ); -// -// assert_eq!( -// staking.query_withdrawable_rewards(&user_2), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 75_000 -// } -// ] -// } -// ); -// -// staking.withdraw_rewards(&user_1); -// assert_eq!(reward_token.balance(&user_1), 25_000i128); -// } -// -// #[test] -// fn test_bond_withdraw_unbond() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let user = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// let reward_amount: u128 = 100_000; -// reward_token.mint(&admin, &(reward_amount as i128)); -// -// lp_token.mint(&user, &1_000); -// env.ledger().with_mut(|li| li.timestamp = ONE_DAY); -// staking.bond(&user, &1_000); -// -// let reward_duration = 10_000; -// -// staking.fund_distribution( -// &ONE_DAY, -// &reward_duration, -// &reward_token.address, -// &(reward_amount as i128), -// ); -// -// env.ledger().with_mut(|li| { -// li.timestamp = ONE_DAY + reward_duration; -// }); -// -// staking.distribute_rewards(); -// -// staking.unbond(&user, &1_000, &ONE_DAY); -// -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 0 -// } -// ] -// } -// ); -// // one more time to make sure that calculations during unbond aren't off -// staking.withdraw_rewards(&user); -// assert_eq!( -// staking.query_withdrawable_rewards(&user), -// WithdrawableRewardsResponse { -// rewards: vec![ -// &env, -// WithdrawableReward { -// reward_address: reward_token.address.clone(), -// reward_amount: 0 -// } -// ] -// } -// ); -// } -// -// #[should_panic(expected = "Stake: Add distribution: Distribution already added")] -// #[test] -// fn panic_when_adding_same_distribution_twice() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &50u32, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// staking.create_distribution_flow(&manager, &reward_token.address); -// } -// -// #[should_panic(expected = "Stake: Fund distribution: Curve complexity validation failed")] -// #[test] -// fn panic_when_funding_distribution_with_curve_too_complex() { -// const DISTRIBUTION_MAX_COMPLEXITY: u32 = 3; -// const FIVE_MINUTES: u64 = 300; -// const TEN_MINUTES: u64 = 600; -// const ONE_WEEK: u64 = 604_800; -// -// let env = Env::default(); -// env.mock_all_auths(); -// -// let admin = Address::generate(&env); -// let manager = Address::generate(&env); -// let owner = Address::generate(&env); -// let lp_token = deploy_token_contract(&env, &admin); -// let reward_token = deploy_token_contract(&env, &admin); -// -// let staking = deploy_staking_contract( -// &env, -// admin.clone(), -// &lp_token.address, -// &manager, -// &owner, -// &DISTRIBUTION_MAX_COMPLEXITY, -// ); -// -// staking.create_distribution_flow(&manager, &reward_token.address); -// -// reward_token.mint(&admin, &3000); -// -// staking.fund_distribution(&0, &FIVE_MINUTES, &reward_token.address, &1000); -// staking.fund_distribution(&FIVE_MINUTES, &TEN_MINUTES, &reward_token.address, &1000); -// -// // assert just to prove that we have 2 successful fund distributions -// assert_eq!( -// staking.query_undistributed_rewards(&reward_token.address), -// 2000 -// ); -// -// // uh-oh fail -// staking.fund_distribution(&TEN_MINUTES, &ONE_WEEK, &reward_token.address, &1000); -// } +#[test] +fn try_to_withdraw_rewards_without_bonding() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + env.ledger().with_mut(|li| { + li.timestamp = 2_000; + }); + + let reward_duration = 600; + staking.fund_distribution( + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp = 2_600; + }); + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + reward_amount + ); + assert_eq!(staking.query_distributed_rewards(&reward_token.address), 0); + + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 0 + } + ] + } + ); + + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), 0); +} + +#[test] +// Error #9 at stake_rewards: InvalidTime = 9 +#[should_panic(expected = "Error(Contract, #9)")] +fn fund_distribution_starting_before_current_timestamp() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + env.ledger().with_mut(|li| { + li.timestamp = 2_000; + }); + + let reward_duration = 600; + staking.fund_distribution( + &1_999, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ) +} + +#[test] +// Error #6 at stake_rewards: MinRewardNotEnough = 6 +#[should_panic(expected = "Error(Contract, #6)")] +fn fund_distribution_with_reward_below_required_minimum() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + reward_token.mint(&admin, &10); + + env.ledger().with_mut(|li| { + li.timestamp = 2_000; + }); + + let reward_duration = 600; + staking.fund_distribution(&2_000, &reward_duration, &reward_token.address, &10); +} + +#[test] +fn calculate_apr() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let manager = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + // whole year of distribution + let reward_duration = 60 * 60 * 24 * 365; + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + // nothing bonded, no rewards + assert_eq!( + staking.query_annualized_rewards(), + AnnualizedRewardsResponse { + rewards: vec![ + &env, + AnnualizedReward { + asset: reward_token.address.clone(), + amount: String::from_str(&env, "0") + } + ] + } + ); + + // bond tokens for user to enable distribution for him + lp_token.mint(&user, &1000); + env.ledger().with_mut(|li| { + li.timestamp += ONE_DAY; + }); + staking.bond(&user, &1000); + // simulate moving forward 60 days for the full APR multiplier + env.ledger().with_mut(|li| { + li.timestamp = SIXTY_DAYS; + }); + + // 100k rewards distributed for the 10 months gives ~120% APR + assert_eq!( + staking.query_annualized_rewards(), + AnnualizedRewardsResponse { + rewards: vec![ + &env, + AnnualizedReward { + asset: reward_token.address.clone(), + amount: String::from_str(&env, "119672.131147540983606557") + } + ] + } + ); + + let reward_amount: u128 = 50_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + staking.fund_distribution( + &(2 * &SIXTY_DAYS), + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + // having another 50k in rewards increases APR + assert_eq!( + staking.query_annualized_rewards(), + AnnualizedRewardsResponse { + rewards: vec![ + &env, + AnnualizedReward { + asset: reward_token.address.clone(), + amount: String::from_str(&env, "150000") + } + ] + } + ); +} + +#[test] +#[should_panic(expected = "Stake: create distribution: Non-authorized creation!")] +fn add_distribution_should_fail_when_not_authorized() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let owner = Address::generate(&env); + + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &owner, + &50u32, + ); + + staking.create_distribution_flow( + &Address::generate(&env), + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); +} + +#[test] +fn test_v_phx_vul_010_unbond_breakes_reward_distribution() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user_1 = Address::generate(&env); + let user_2 = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + // bond tokens for user to enable distribution for him + lp_token.mint(&user_1, &1_000); + lp_token.mint(&user_2, &1_000); + + staking.bond(&user_1, &1_000); + staking.bond(&user_2, &1_000); + + // simulate moving forward 60 days for the full APR multiplier + env.ledger().with_mut(|li| li.timestamp = SIXTY_DAYS); + + let reward_duration = 10_000; + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp += 2_000; + }); + + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 80_000 // 100k total rewards, we have 2000 seconds passed, so we have 80k undistributed rewards + ); + + // at the 1/2 of the distribution time, user_1 unbonds + env.ledger().with_mut(|li| { + li.timestamp += 3_000; + }); + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 50_000 + ); + + // user1 unbonds, which automatically withdraws the rewards + assert_eq!( + staking.query_withdrawable_rewards(&user_1), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 25_000 + } + ] + } + ); + staking.unbond(&user_1, &1_000, &0); + assert_eq!( + staking.query_withdrawable_rewards(&user_1), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 0 + } + ] + } + ); + + env.ledger().with_mut(|li| { + li.timestamp += 10_000; + }); + + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 0 + ); + assert_eq!( + staking.query_distributed_rewards(&reward_token.address), + reward_amount + ); + + assert_eq!( + staking.query_withdrawable_rewards(&user_2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 75_000 + } + ] + } + ); + + staking.withdraw_rewards(&user_1); + assert_eq!(reward_token.balance(&user_1), 25_000i128); +} + +#[test] +fn test_bond_withdraw_unbond() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + lp_token.mint(&user, &1_000); + staking.bond(&user, &1_000); + + // simulate moving forward 60 days for the full APR multiplier + env.ledger().with_mut(|li| { + li.timestamp = SIXTY_DAYS; + }); + + let reward_duration = 10_000; + + staking.fund_distribution( + &SIXTY_DAYS, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp += reward_duration; + }); + + staking.distribute_rewards(); + + staking.unbond(&user, &1_000, &0); + + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 0 + } + ] + } + ); + // one more time to make sure that calculations during unbond aren't off + staking.withdraw_rewards(&user); + assert_eq!( + staking.query_withdrawable_rewards(&user), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 0 + } + ] + } + ); +} + +#[should_panic( + expected = "Stake: Create distribution flow: Distribution for this reward token exists!" +)] +#[test] +fn panic_when_adding_same_distribution_twice() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); +} + +// Error #12 at stake_rewards: InvalidMaxComplexity = 12 +#[should_panic(expected = "Error(Contract, #12)")] +#[test] +fn panic_when_funding_distribution_with_curve_too_complex() { + const DISTRIBUTION_MAX_COMPLEXITY: u32 = 3; + const FIVE_MINUTES: u64 = 300; + const TEN_MINUTES: u64 = 600; + const ONE_WEEK: u64 = 604_800; + + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &DISTRIBUTION_MAX_COMPLEXITY, + ); + + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + reward_token.mint(&admin, &10000); + + staking.fund_distribution(&0, &FIVE_MINUTES, &reward_token.address, &1000); + staking.fund_distribution(&FIVE_MINUTES, &TEN_MINUTES, &reward_token.address, &1000); + staking.fund_distribution(&TEN_MINUTES, &ONE_WEEK, &reward_token.address, &1000); + staking.fund_distribution( + &(ONE_WEEK + 1), + &(ONE_WEEK + 3), + &reward_token.address, + &1000, + ); + staking.fund_distribution( + &(ONE_WEEK + 3), + &(ONE_WEEK + 5), + &reward_token.address, + &1000, + ); + staking.fund_distribution( + &(ONE_WEEK + 6), + &(ONE_WEEK + 7), + &reward_token.address, + &1000, + ); + staking.fund_distribution( + &(ONE_WEEK + 8), + &(ONE_WEEK + 9), + &reward_token.address, + &1000, + ); +} From cd17f418724de8234cd232d56e74ce04e91ea990 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 13:01:59 +0200 Subject: [PATCH 12/16] Stake: add test case for the different user reward multipliers --- contracts/stake/src/tests/distribution.rs | 99 +++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index 53760abde..18cfbd374 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -1289,3 +1289,102 @@ fn panic_when_funding_distribution_with_curve_too_complex() { &1000, ); } + +#[test] +fn multiple_equal_users_with_different_multipliers() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let lp_token = deploy_token_contract(&env, &admin); + let reward_token = deploy_token_contract(&env, &admin); + + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &admin, + &50u32, + ); + staking.create_distribution_flow( + &admin, + &reward_token.address, + &BytesN::from_array(&env, &[1; 32]), + &10, + &100, + &1, + ); + + // first user bonds at timestamp 0 + // he will get 100% of his rewards + let user1 = Address::generate(&env); + lp_token.mint(&user1, &10_000); + staking.bond(&user1, &10_000); + + let fifteen_days = 3600 * 24 * 15; + env.ledger().with_mut(|li| { + li.timestamp = fifteen_days; + }); + + // user2 will receive 75% of his reward + let user2 = Address::generate(&env); + lp_token.mint(&user2, &10_000); + staking.bond(&user2, &10_000); + + env.ledger().with_mut(|li| { + li.timestamp = fifteen_days * 2; + }); + + // user3 will receive 50% of his reward + let user3 = Address::generate(&env); + lp_token.mint(&user3, &10_000); + staking.bond(&user3, &10_000); + + env.ledger().with_mut(|li| { + li.timestamp = fifteen_days * 3; + }); + + // user4 will receive 25% of his reward + let user4 = Address::generate(&env); + lp_token.mint(&user4, &10_000); + staking.bond(&user4, &10_000); + + env.ledger().with_mut(|li| { + li.timestamp = fifteen_days * 4; + }); + + reward_token.mint(&admin, &1_000_000); + // reward distribution starts at the latest timestamp and lasts just 1 second + // the point is to prove that the multiplier works correctly + staking.fund_distribution(&(fifteen_days * 4), &1, &reward_token.address, &1_000_000); + + env.ledger().with_mut(|li| { + li.timestamp += 1; + }); + + staking.distribute_rewards(); + + // The way it works - contract will treat all the funds as distributed, and the amount + // that was not sent due to low staking bonus stays on the contract + + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 0 + ); + assert_eq!( + staking.query_distributed_rewards(&reward_token.address), + 1_000_000 + ); + + staking.withdraw_rewards(&user1); + assert_eq!(reward_token.balance(&user1), 250_000); + staking.withdraw_rewards(&user2); + assert_eq!(reward_token.balance(&user2), 187_500); + staking.withdraw_rewards(&user3); + assert_eq!(reward_token.balance(&user3), 125_000); + staking.withdraw_rewards(&user4); + assert_eq!(reward_token.balance(&user4), 62_500); +} From 8af9f13d194ece676e701e06f1a2e6530917ed96 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 13:06:59 +0200 Subject: [PATCH 13/16] Stake: Remove redundant implementations present in stake_rewards --- contracts/stake/src/contract.rs | 15 +- contracts/stake/src/distribution.rs | 300 +--------------------- contracts/stake/src/storage.rs | 5 +- contracts/stake/src/tests/distribution.rs | 3 +- 4 files changed, 8 insertions(+), 315 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index dc1f95aef..69e05c122 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -1,16 +1,12 @@ -use phoenix::utils::{convert_i128_to_u128, convert_u128_to_i128}; +use phoenix::utils::convert_i128_to_u128; use soroban_decimal::Decimal; use soroban_sdk::{ contract, contractimpl, contractmeta, log, panic_with_error, vec, Address, BytesN, Env, - IntoVal, String, Symbol, Val, Vec, + IntoVal, Symbol, Val, Vec, }; use crate::{ - distribution::{ - calc_power, calculate_annualized_payout, get_distribution, get_reward_curve, - get_withdraw_adjustment, save_distribution, save_reward_curve, save_withdraw_adjustment, - update_rewards, withdrawable_rewards, Distribution, SHARES_SHIFT, - }, + distribution::calc_power, error::ContractError, msg::{ AnnualizedReward, AnnualizedRewardsResponse, ConfigResponse, StakedResponse, @@ -28,7 +24,6 @@ use crate::{ }, token_contract, TOKEN_PER_POWER, }; -use curve::Curve; // Metadata that is added on to the WASM custom section contractmeta!( @@ -268,7 +263,7 @@ impl StakingTrait for Staking { panic_with_error!(&env, ContractError::Unauthorized); } - if let Some(address) = find_stake_rewards_by_asset(&env, &asset) { + if find_stake_rewards_by_asset(&env, &asset).is_some() { log!( env, "Stake: Create distribution flow: Distribution for this reward token exists!" @@ -377,7 +372,7 @@ impl StakingTrait for Staking { }; let fund_distr_fn_arg: Vec<Val> = - (start_time, distribution_duration, token_amount.clone()).into_val(&env); + (start_time, distribution_duration, token_amount).into_val(&env); env.invoke_contract::<Val>( &stake_rewards, &Symbol::new(&env, "fund_distribution"), diff --git a/contracts/stake/src/distribution.rs b/contracts/stake/src/distribution.rs index 230277d10..9b18ae4d6 100644 --- a/contracts/stake/src/distribution.rs +++ b/contracts/stake/src/distribution.rs @@ -1,232 +1,6 @@ -use phoenix::utils::{convert_i128_to_u128, convert_u128_to_i128}; -use soroban_sdk::{contracttype, Address, Env}; - -use curve::Curve; use soroban_decimal::Decimal; -use crate::{ - storage::{get_stakes, Config}, - TOKEN_PER_POWER, -}; - -/// How much points is the worth of single token in rewards distribution. -/// The scaling is performed to have better precision of fixed point division. -/// This value is not actually the scaling itself, but how much bits value should be shifted -/// (for way more efficient division). -/// -/// 32, to have those 32 bits, but it reduces how much tokens may be handled by this contract -/// (it is now 96-bit integer instead of 128). In original ERC2222 it is handled by 256-bit -/// calculations, but I256 is missing and it is required for this. -pub const SHARES_SHIFT: u8 = 32; - -const SECONDS_PER_YEAR: u64 = 365 * 24 * 60 * 60; - -#[derive(Clone)] -#[contracttype] -pub struct WithdrawAdjustmentKey { - user: Address, - asset: Address, -} - -#[derive(Clone)] -#[contracttype] -pub enum DistributionDataKey { - Curve(Address), - Distribution(Address), - WithdrawAdjustment(WithdrawAdjustmentKey), -} - -// one reward distribution curve over one denom -pub fn save_reward_curve(env: &Env, asset: Address, distribution_curve: &Curve) { - env.storage() - .persistent() - .set(&DistributionDataKey::Curve(asset), distribution_curve); -} - -pub fn get_reward_curve(env: &Env, asset: &Address) -> Option<Curve> { - env.storage() - .persistent() - .get(&DistributionDataKey::Curve(asset.clone())) -} - -#[contracttype] -#[derive(Debug, Default, Clone)] -pub struct Distribution { - /// How many shares is single point worth - pub points_per_share: u128, - /// Shares which were not fully distributed on previous distributions, and should be redistributed - pub shares_leftover: u64, - /// Total rewards distributed by this contract. - pub distributed_total: u128, - /// Total rewards not yet withdrawn. - pub withdrawable_total: u128, - /// Max bonus for staking after 60 days - pub max_bonus_bps: u64, - /// Bonus per staking day - pub bonus_per_day_bps: u64, -} - -pub fn save_distribution(env: &Env, asset: &Address, distribution: &Distribution) { - env.storage().persistent().set( - &DistributionDataKey::Distribution(asset.clone()), - distribution, - ); -} - -pub fn get_distribution(env: &Env, asset: &Address) -> Distribution { - env.storage() - .persistent() - .get(&DistributionDataKey::Distribution(asset.clone())) - .unwrap() -} - -pub fn update_rewards( - env: &Env, - user: &Address, - asset: &Address, - distribution: &mut Distribution, - old_rewards_power: i128, - new_rewards_power: i128, -) { - if old_rewards_power == new_rewards_power { - return; - } - let diff = new_rewards_power - old_rewards_power; - // Apply the points correction with the calculated difference. - let ppw = distribution.points_per_share; - apply_points_correction(env, user, asset, diff, ppw); -} - -/// Applies points correction for given address. -/// `points_per_share` is current value from `POINTS_PER_SHARE` - not loaded in function, to -/// avoid multiple queries on bulk updates. -/// `diff` is the points change -fn apply_points_correction( - env: &Env, - user: &Address, - asset: &Address, - diff: i128, - shares_per_point: u128, -) { - let mut withdraw_adjustment = get_withdraw_adjustment(env, user, asset); - let shares_correction = withdraw_adjustment.shares_correction; - withdraw_adjustment.shares_correction = - shares_correction - convert_u128_to_i128(shares_per_point) * diff; - save_withdraw_adjustment(env, user, asset, &withdraw_adjustment); -} - -#[contracttype] -#[derive(Debug, Default, Clone)] -pub struct WithdrawAdjustment { - /// Represents a correction to the reward points for the user. This can be positive or negative. - /// A positive value indicates that the user should receive additional points (e.g., from a bonus or an error correction), - /// while a negative value signifies a reduction (e.g., due to a penalty or an adjustment for past over-allocations). - pub shares_correction: i128, - /// Represents the total amount of rewards that the user has withdrawn so far. - /// This value ensures that a user doesn't withdraw more than they are owed and is used to - /// calculate the net rewards a user can withdraw at any given time. - pub withdrawn_rewards: u128, -} - -/// Save the withdraw adjustment for a user for a given asset using the user's address as the key -/// and asset's address as the subkey. -pub fn save_withdraw_adjustment( - env: &Env, - user: &Address, - distribution: &Address, - adjustment: &WithdrawAdjustment, -) { - env.storage().persistent().set( - &DistributionDataKey::WithdrawAdjustment(WithdrawAdjustmentKey { - user: user.clone(), - asset: distribution.clone(), - }), - adjustment, - ); -} - -pub fn get_withdraw_adjustment( - env: &Env, - user: &Address, - distribution: &Address, -) -> WithdrawAdjustment { - env.storage() - .persistent() - .get(&DistributionDataKey::WithdrawAdjustment( - WithdrawAdjustmentKey { - user: user.clone(), - asset: distribution.clone(), - }, - )) - .unwrap_or_default() -} - -pub fn withdrawable_rewards( - env: &Env, - owner: &Address, - distribution: &Distribution, - adjustment: &WithdrawAdjustment, - config: &Config, -) -> u128 { - let ppw = distribution.points_per_share; - - let stakes: i128 = get_stakes(env, owner).total_stake; - // Decimal::one() represents the standart multiplier per token - // 1_000 represents the contsant token per power. TODO: make it configurable - let points = calc_power(config, stakes, Decimal::one(), TOKEN_PER_POWER); - let points = (ppw * convert_i128_to_u128(points)) as i128; - - let correction = adjustment.shares_correction; - let points = points + correction; - let amount = points >> SHARES_SHIFT; - convert_i128_to_u128(amount) - adjustment.withdrawn_rewards -} - -pub fn calculate_annualized_payout(reward_curve: Option<Curve>, now: u64) -> Decimal { - match reward_curve { - Some(c) => { - // look at the last timestamp in the rewards curve and extrapolate - match c.end() { - Some(last_timestamp) => { - if last_timestamp <= now { - return Decimal::zero(); - } - let time_diff = last_timestamp - now; - if time_diff >= SECONDS_PER_YEAR { - // if the last timestamp is more than a year in the future, - // we can just calculate the rewards for the whole year directly - - // formula: `(locked_now - locked_end)` - Decimal::from_atomics( - convert_u128_to_i128(c.value(now) - c.value(now + SECONDS_PER_YEAR)), - 0, - ) - } else { - // if the last timestamp is less than a year in the future, - // we want to extrapolate the rewards for the whole year - - // formula: `(locked_now - locked_end) / time_diff * SECONDS_PER_YEAR` - // `locked_now - locked_end` are the tokens freed up over the `time_diff`. - // Dividing by that diff, gives us the rate of tokens per second, - // which is then extrapolated to a whole year. - // Because of the constraints put on `c` when setting it, - // we know that `locked_end` is always 0, so we don't need to subtract it. - Decimal::from_ratio( - convert_u128_to_i128(c.value(now) * SECONDS_PER_YEAR as u128), - time_diff, - ) - } - } - None => { - // this case should only happen if the reward curve is freshly initialized - // (i.e. no rewards have been scheduled yet) - Decimal::zero() - } - } - } - None => Decimal::zero(), - } -} +use crate::storage::Config; pub fn calc_power( config: &Config, @@ -240,75 +14,3 @@ pub fn calc_power( stakes * multiplier / token_per_power as i128 } } - -#[cfg(test)] -mod tests { - use super::*; - use curve::SaturatingLinear; - use soroban_sdk::testutils::Address as _; - - #[test] - fn update_rewards_should_return_early_if_old_power_is_same_as_new_power() { - let env = Env::default(); - let user = Address::generate(&env); - let asset = Address::generate(&env); - let mut distribution = Distribution::default(); - - let old_rewards_power = 100; - let new_rewards_power = 100; - - // it's only enough not to panic as the inner method call to apply_points_correction calls get_withdraw_adjustment - // this would trigger InternalError otherwise - update_rewards( - &env, - &user, - &asset, - &mut distribution, - old_rewards_power, - new_rewards_power, - ); - } - - #[test] - fn calculate_annualized_payout_should_return_zero_when_last_timestamp_in_the_past() { - let reward_curve = Some(Curve::SaturatingLinear(SaturatingLinear { - min_x: 15, - min_y: 1, - max_x: 60, - max_y: 120, - })); - let result = calculate_annualized_payout(reward_curve, 121); - assert_eq!(result, Decimal::zero()); - } - - #[test] - fn calculate_annualized_payout_extrapolating_an_year() { - let reward_curve = Some(Curve::SaturatingLinear(SaturatingLinear { - min_x: 15, - min_y: 1, - max_x: SECONDS_PER_YEAR + 60, - max_y: (SECONDS_PER_YEAR + 120) as u128, - })); - // we take the last timestamp in the curve and extrapolate the rewards for a year - let result = calculate_annualized_payout(reward_curve, SECONDS_PER_YEAR + 1); - // a bit weird assertion, but we're testing the extrapolation with a large number - assert_eq!( - result, - Decimal::new(16_856_291_324_745_762_711_864_406_779_661) - ); - } - - #[test] - fn calculate_annualized_payout_should_return_zero_no_end_in_curve() { - let reward_curve = Some(Curve::Constant(10)); - let result = calculate_annualized_payout(reward_curve, 121); - assert_eq!(result, Decimal::zero()); - } - - #[test] - fn calculate_annualized_payout_should_return_zero_no_curve() { - let reward_curve = None::<Curve>; - let result = calculate_annualized_payout(reward_curve, 121); - assert_eq!(result, Decimal::zero()); - } -} diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index 8afd66c37..98d96eaf6 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -1,7 +1,4 @@ -use soroban_sdk::{ - contract, contractimpl, contractmeta, contracttype, log, symbol_short, vec, Address, BytesN, - Env, IntoVal, String, Symbol, Val, Vec, -}; +use soroban_sdk::{contracttype, symbol_short, Address, BytesN, Env, Symbol, Vec}; use crate::stake_rewards_contract; diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index 18cfbd374..e4a0838cf 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -13,7 +13,7 @@ use crate::{ AnnualizedReward, AnnualizedRewardsResponse, WithdrawableReward, WithdrawableRewardsResponse, }, - tests::setup::{ONE_DAY, ONE_WEEK, SIXTY_DAYS}, + tests::setup::{ONE_DAY, SIXTY_DAYS}, }; #[test] @@ -434,7 +434,6 @@ fn two_users_one_starts_after_distribution_begins() { let user = Address::generate(&env); let user2 = Address::generate(&env); let manager = Address::generate(&env); - let owner = Address::generate(&env); let lp_token = deploy_token_contract(&env, &admin); let reward_token = deploy_token_contract(&env, &admin); From f34a8c3e28f00d00dafdb8de049d48b647568aaa Mon Sep 17 00:00:00 2001 From: Gangov <gangov1@gmail.com> Date: Mon, 5 Aug 2024 15:44:06 +0300 Subject: [PATCH 14/16] fixes the failing tests --- contracts/factory/src/contract.rs | 5 +++++ contracts/factory/src/storage.rs | 1 + contracts/factory/src/tests.rs | 4 ++++ contracts/factory/src/tests/config.rs | 4 +++- contracts/factory/src/tests/setup.rs | 9 +++++++++ contracts/multihop/src/tests/setup.rs | 9 +++++++++ contracts/pool/src/contract.rs | 3 +++ contracts/pool/src/tests/config.rs | 8 ++++++-- contracts/pool/src/tests/setup.rs | 9 +++++++++ contracts/pool/src/tests/stake_deployment.rs | 7 ++++++- contracts/pool_stable/src/contract.rs | 3 +++ contracts/pool_stable/src/tests/setup.rs | 9 +++++++++ contracts/pool_stable/src/tests/stake_deployment.rs | 9 ++++++++- 13 files changed, 75 insertions(+), 5 deletions(-) diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs index 416057d98..505737bce 100644 --- a/contracts/factory/src/contract.rs +++ b/contracts/factory/src/contract.rs @@ -30,6 +30,7 @@ pub trait FactoryTrait { lp_wasm_hash: BytesN<32>, stable_wasm_hash: BytesN<32>, stake_wasm_hash: BytesN<32>, + stake_rewards_wasm_hash: BytesN<32>, token_wasm_hash: BytesN<32>, whitelisted_accounts: Vec<Address>, lp_token_decimals: u32, @@ -87,6 +88,7 @@ impl FactoryTrait for Factory { lp_wasm_hash: BytesN<32>, stable_wasm_hash: BytesN<32>, stake_wasm_hash: BytesN<32>, + stake_rewards_wasm_hash: BytesN<32>, token_wasm_hash: BytesN<32>, whitelisted_accounts: Vec<Address>, lp_token_decimals: u32, @@ -117,6 +119,7 @@ impl FactoryTrait for Factory { lp_wasm_hash, stable_wasm_hash, stake_wasm_hash, + stake_rewards_wasm_hash, token_wasm_hash, whitelisted_accounts, lp_token_decimals, @@ -161,6 +164,7 @@ impl FactoryTrait for Factory { let config = get_config(&env); let stake_wasm_hash = config.stake_wasm_hash; let token_wasm_hash = config.token_wasm_hash; + let stake_rewards_wasm_hash = config.stake_rewards_wasm_hash; let pool_hash = match pool_type { PoolType::Xyk => config.lp_wasm_hash, @@ -188,6 +192,7 @@ impl FactoryTrait for Factory { let mut init_fn_args: Vec<Val> = ( stake_wasm_hash, token_wasm_hash, + stake_rewards_wasm_hash, lp_init_info.clone(), factory_addr, config.lp_token_decimals, diff --git a/contracts/factory/src/storage.rs b/contracts/factory/src/storage.rs index 0f7641af2..d9d69b75e 100644 --- a/contracts/factory/src/storage.rs +++ b/contracts/factory/src/storage.rs @@ -32,6 +32,7 @@ pub struct Config { pub stable_wasm_hash: BytesN<32>, pub stake_wasm_hash: BytesN<32>, pub token_wasm_hash: BytesN<32>, + pub stake_rewards_wasm_hash: BytesN<32>, pub whitelisted_accounts: Vec<Address>, pub lp_token_decimals: u32, } diff --git a/contracts/factory/src/tests.rs b/contracts/factory/src/tests.rs index 7ba365637..d644ba5cf 100644 --- a/contracts/factory/src/tests.rs +++ b/contracts/factory/src/tests.rs @@ -1,3 +1,4 @@ +use setup::install_stake_rewards_wasm; use soroban_sdk::{testutils::Address as _, vec, Address, Env}; use self::setup::{ @@ -23,6 +24,7 @@ fn test_deploy_factory_twice_should_fail() { let lp_wasm_hash = install_lp_contract(&env); let stable_wasm_hash = install_stable_lp(&env); let stake_wasm_hash = install_stake_wasm(&env); + let stake_rewards_wasm_hash = install_stake_rewards_wasm(&env); let token_wasm_hash = install_token_wasm(&env); let factory = deploy_factory_contract(&env, admin.clone()); @@ -33,6 +35,7 @@ fn test_deploy_factory_twice_should_fail() { &lp_wasm_hash, &stable_wasm_hash, &stake_wasm_hash, + &stake_rewards_wasm_hash, &token_wasm_hash, &vec![&env, auth_user.clone()], &10u32, @@ -43,6 +46,7 @@ fn test_deploy_factory_twice_should_fail() { &lp_wasm_hash, &stable_wasm_hash, &stake_wasm_hash, + &stake_rewards_wasm_hash, &token_wasm_hash, &vec![&env, auth_user.clone()], &10u32, diff --git a/contracts/factory/src/tests/config.rs b/contracts/factory/src/tests/config.rs index f39bb1245..f9e40bf70 100644 --- a/contracts/factory/src/tests/config.rs +++ b/contracts/factory/src/tests/config.rs @@ -1,6 +1,6 @@ use super::setup::{ deploy_factory_contract, install_lp_contract, install_multihop_wasm, install_stable_lp, - install_stake_wasm, install_token_wasm, lp_contract, + install_stake_rewards_wasm, install_stake_wasm, install_token_wasm, lp_contract, }; use crate::{ contract::{Factory, FactoryClient}, @@ -240,6 +240,7 @@ fn factory_fails_to_init_lp_when_no_whitelisted_accounts() { let lp_wasm_hash = install_lp_contract(&env); let stable_wasm_hash = install_stable_lp(&env); let stake_wasm_hash = install_stake_wasm(&env); + let stake_rewards_wasm_hash = install_stake_rewards_wasm(&env); let token_wasm_hash = install_token_wasm(&env); factory.initialize( @@ -248,6 +249,7 @@ fn factory_fails_to_init_lp_when_no_whitelisted_accounts() { &lp_wasm_hash, &stable_wasm_hash, &stake_wasm_hash, + &stake_rewards_wasm_hash, &token_wasm_hash, &whitelisted_accounts, &10u32, diff --git a/contracts/factory/src/tests/setup.rs b/contracts/factory/src/tests/setup.rs index 8f121a80a..5bb44077d 100644 --- a/contracts/factory/src/tests/setup.rs +++ b/contracts/factory/src/tests/setup.rs @@ -55,6 +55,13 @@ pub fn install_stake_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(WASM) } +pub fn install_stake_rewards_wasm(env: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake_rewards.wasm" + ); + env.deployer().upload_contract_wasm(WASM) +} + pub fn deploy_factory_contract<'a>( env: &Env, admin: impl Into<Option<Address>>, @@ -67,6 +74,7 @@ pub fn deploy_factory_contract<'a>( let lp_wasm_hash = install_lp_contract(env); let stable_wasm_hash = install_stable_lp(env); let stake_wasm_hash = install_stake_wasm(env); + let stake_rewards_wasm_hash = install_stake_rewards_wasm(env); let token_wasm_hash = install_token_wasm(env); factory.initialize( @@ -75,6 +83,7 @@ pub fn deploy_factory_contract<'a>( &lp_wasm_hash, &stable_wasm_hash, &stake_wasm_hash, + &stake_rewards_wasm_hash, &token_wasm_hash, &whitelisted_accounts, &10u32, diff --git a/contracts/multihop/src/tests/setup.rs b/contracts/multihop/src/tests/setup.rs index 13570e6ce..8940bc8a4 100644 --- a/contracts/multihop/src/tests/setup.rs +++ b/contracts/multihop/src/tests/setup.rs @@ -46,6 +46,13 @@ pub fn install_stake_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(WASM) } +pub fn install_stake_rewards_wasm(env: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake_rewards.wasm" + ); + env.deployer().upload_contract_wasm(WASM) +} + #[allow(clippy::too_many_arguments)] pub fn install_multihop_wasm(env: &Env) -> BytesN<32> { soroban_sdk::contractimport!( @@ -94,6 +101,7 @@ pub fn deploy_and_initialize_factory(env: &Env, admin: Address) -> factory_contr let lp_wasm_hash = install_lp_contract(env); let stable_wasm_hash = install_stable_lp_contract(env); let stake_wasm_hash = install_stake_wasm(env); + let stake_rewards_wasm_hash = install_stake_rewards_wasm(env); let token_wasm_hash = install_token_wasm(env); factory_client.initialize( @@ -102,6 +110,7 @@ pub fn deploy_and_initialize_factory(env: &Env, admin: Address) -> factory_contr &lp_wasm_hash, &stable_wasm_hash, &stake_wasm_hash, + &stake_rewards_wasm_hash, &token_wasm_hash, &whitelisted_accounts, &10u32, diff --git a/contracts/pool/src/contract.rs b/contracts/pool/src/contract.rs index 25fbbcc43..051bef0ea 100644 --- a/contracts/pool/src/contract.rs +++ b/contracts/pool/src/contract.rs @@ -43,6 +43,7 @@ pub trait LiquidityPoolTrait { env: Env, stake_wasm_hash: BytesN<32>, token_wasm_hash: BytesN<32>, + stake_rewards_wasm_hash: BytesN<32>, lp_init_info: LiquidityPoolInitInfo, factory_addr: Address, share_token_decimals: u32, @@ -152,6 +153,7 @@ impl LiquidityPoolTrait for LiquidityPool { env: Env, stake_wasm_hash: BytesN<32>, token_wasm_hash: BytesN<32>, + stake_rewards_wasm_hash: BytesN<32>, lp_init_info: LiquidityPoolInitInfo, factory_addr: Address, share_token_decimals: u32, @@ -232,6 +234,7 @@ impl LiquidityPoolTrait for LiquidityPool { stake_contract::Client::new(&env, &stake_contract_address).initialize( &admin, &share_token_address, + &stake_rewards_wasm_hash, &min_bond, &min_reward, &manager, diff --git a/contracts/pool/src/tests/config.rs b/contracts/pool/src/tests/config.rs index bfaf5187d..d73eaa680 100644 --- a/contracts/pool/src/tests/config.rs +++ b/contracts/pool/src/tests/config.rs @@ -3,8 +3,8 @@ use phoenix::utils::{LiquidityPoolInitInfo, StakeInitInfo, TokenInitInfo}; use soroban_sdk::{testutils::Address as _, Address, Env, String}; use super::setup::{ - deploy_liquidity_pool_contract, deploy_token_contract, install_new_lp_wasm, install_stake_wasm, - install_token_wasm, + deploy_liquidity_pool_contract, deploy_token_contract, install_new_lp_wasm, + install_stake_rewards_wasm, install_stake_wasm, install_token_wasm, }; use crate::{ contract::{LiquidityPool, LiquidityPoolClient}, @@ -42,6 +42,7 @@ fn test_initialize_with_bigger_first_token_should_fail() { }; let stake_wasm_hash = install_stake_wasm(&env); let token_wasm_hash = install_token_wasm(&env); + let stake_reward_wasm_hash = install_stake_rewards_wasm(&env); let lp_init_info = LiquidityPoolInitInfo { admin, @@ -58,6 +59,7 @@ fn test_initialize_with_bigger_first_token_should_fail() { pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &Address::generate(&env), &10u32, @@ -475,6 +477,7 @@ fn test_initialize_with_maximum_allowed_swap_fee_bps_over_the_cap_should_fail() }; let stake_wasm_hash = install_stake_wasm(&env); let token_wasm_hash = install_token_wasm(&env); + let stake_reward_wasm_hash = install_stake_rewards_wasm(&env); let lp_init_info = LiquidityPoolInitInfo { admin, @@ -491,6 +494,7 @@ fn test_initialize_with_maximum_allowed_swap_fee_bps_over_the_cap_should_fail() pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &Address::generate(&env), &10u32, diff --git a/contracts/pool/src/tests/setup.rs b/contracts/pool/src/tests/setup.rs index 5fce902f1..674ec436e 100644 --- a/contracts/pool/src/tests/setup.rs +++ b/contracts/pool/src/tests/setup.rs @@ -26,6 +26,13 @@ pub fn install_stake_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(WASM) } +pub fn install_stake_rewards_wasm(env: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake_rewards.wasm" + ); + env.deployer().upload_contract_wasm(WASM) +} + #[allow(clippy::too_many_arguments)] pub fn install_new_lp_wasm(env: &Env) -> BytesN<32> { soroban_sdk::contractimport!( @@ -64,6 +71,7 @@ pub fn deploy_liquidity_pool_contract<'a>( }; let stake_wasm_hash = install_stake_wasm(env); let token_wasm_hash = install_token_wasm(env); + let stake_reward_wasm_hash = install_stake_rewards_wasm(env); let lp_init_info = LiquidityPoolInitInfo { admin, @@ -80,6 +88,7 @@ pub fn deploy_liquidity_pool_contract<'a>( pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &stake_owner, &10u32, diff --git a/contracts/pool/src/tests/stake_deployment.rs b/contracts/pool/src/tests/stake_deployment.rs index 11fc87ff1..1e0764996 100644 --- a/contracts/pool/src/tests/stake_deployment.rs +++ b/contracts/pool/src/tests/stake_deployment.rs @@ -2,7 +2,9 @@ extern crate std; use phoenix::utils::{LiquidityPoolInitInfo, StakeInitInfo, TokenInitInfo}; use soroban_sdk::{testutils::Address as _, Address, Env, String}; -use super::setup::{deploy_liquidity_pool_contract, deploy_token_contract}; +use super::setup::{ + deploy_liquidity_pool_contract, deploy_token_contract, install_stake_rewards_wasm, +}; use crate::{ stake_contract, storage::{Config, PairType}, @@ -101,6 +103,7 @@ fn second_pool_deployment_should_fail() { let token_wasm_hash = install_token_wasm(&env); let stake_wasm_hash = install_stake_wasm(&env); + let stake_reward_wasm_hash = install_stake_rewards_wasm(&env); let fee_recipient = user; let max_allowed_slippage = 5_000i64; // 50% if not specified let max_allowed_spread = 500i64; // 5% if not specified @@ -131,6 +134,7 @@ fn second_pool_deployment_should_fail() { pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &Address::generate(&env), &10u32, @@ -143,6 +147,7 @@ fn second_pool_deployment_should_fail() { pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &Address::generate(&env), &10u32, diff --git a/contracts/pool_stable/src/contract.rs b/contracts/pool_stable/src/contract.rs index 217ba347a..8e2d654f2 100644 --- a/contracts/pool_stable/src/contract.rs +++ b/contracts/pool_stable/src/contract.rs @@ -42,6 +42,7 @@ pub trait StableLiquidityPoolTrait { env: Env, stake_wasm_hash: BytesN<32>, token_wasm_hash: BytesN<32>, + stake_rewards_wasm_hash: BytesN<32>, lp_init_info: LiquidityPoolInitInfo, factory_addr: Address, share_token_decimal: u32, @@ -145,6 +146,7 @@ impl StableLiquidityPoolTrait for StableLiquidityPool { env: Env, stake_wasm_hash: BytesN<32>, token_wasm_hash: BytesN<32>, + stake_rewards_wasm_hash: BytesN<32>, lp_init_info: LiquidityPoolInitInfo, factory_addr: Address, _share_token_decimal: u32, @@ -226,6 +228,7 @@ impl StableLiquidityPoolTrait for StableLiquidityPool { stake_contract::Client::new(&env, &stake_contract_address).initialize( &admin, &share_token_address, + &stake_rewards_wasm_hash, &min_bond, &min_reward, &manager, diff --git a/contracts/pool_stable/src/tests/setup.rs b/contracts/pool_stable/src/tests/setup.rs index 611e5e68c..1ebd13e4b 100644 --- a/contracts/pool_stable/src/tests/setup.rs +++ b/contracts/pool_stable/src/tests/setup.rs @@ -26,6 +26,13 @@ pub fn install_stake_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(WASM) } +pub fn install_stake_rewards_wasm(env: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake_rewards.wasm" + ); + env.deployer().upload_contract_wasm(WASM) +} + #[allow(clippy::too_many_arguments)] pub fn deploy_stable_liquidity_pool_contract<'a>( env: &Env, @@ -59,6 +66,7 @@ pub fn deploy_stable_liquidity_pool_contract<'a>( let token_wasm_hash = install_token_wasm(env); let stake_wasm_hash = install_stake_wasm(env); + let stake_rewards_wasm_hash = install_stake_rewards_wasm(env); let lp_init_info = LiquidityPoolInitInfo { admin, @@ -75,6 +83,7 @@ pub fn deploy_stable_liquidity_pool_contract<'a>( pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_rewards_wasm_hash, &lp_init_info, &factory, &10, // LP share decimals, unused diff --git a/contracts/pool_stable/src/tests/stake_deployment.rs b/contracts/pool_stable/src/tests/stake_deployment.rs index 837f6466c..3c71d2309 100644 --- a/contracts/pool_stable/src/tests/stake_deployment.rs +++ b/contracts/pool_stable/src/tests/stake_deployment.rs @@ -2,7 +2,9 @@ extern crate std; use phoenix::utils::{LiquidityPoolInitInfo, StakeInitInfo, TokenInitInfo}; use soroban_sdk::{testutils::Address as _, Address, Env, String}; -use super::setup::{deploy_stable_liquidity_pool_contract, deploy_token_contract}; +use super::setup::{ + deploy_stable_liquidity_pool_contract, deploy_token_contract, install_stake_rewards_wasm, +}; use crate::contract::{StableLiquidityPool, StableLiquidityPoolClient}; use crate::tests::setup::{install_stake_wasm, install_token_wasm}; use crate::{ @@ -100,6 +102,7 @@ fn second_pool_stable_deployment_should_fail() { let token_wasm_hash = install_token_wasm(&env); let stake_wasm_hash = install_stake_wasm(&env); + let stake_reward_wasm_hash = install_stake_rewards_wasm(&env); let fee_recipient = user; let max_allowed_slippage = 5_000i64; // 50% if not specified let max_allowed_spread = 500i64; // 5% if not specified @@ -133,6 +136,7 @@ fn second_pool_stable_deployment_should_fail() { pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &factory, &10, // LP share decimals, unused @@ -144,6 +148,7 @@ fn second_pool_stable_deployment_should_fail() { pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &factory, &10, // LP share decimals, unused @@ -179,6 +184,7 @@ fn pool_stable_initialization_should_fail_with_token_a_bigger_than_token_b() { let token_wasm_hash = install_token_wasm(&env); let stake_wasm_hash = install_stake_wasm(&env); + let stake_reward_wasm_hash = install_stake_rewards_wasm(&env); let fee_recipient = user; let max_allowed_slippage = 5_000i64; // 50% if not specified let max_allowed_spread = 500i64; // 5% if not specified @@ -212,6 +218,7 @@ fn pool_stable_initialization_should_fail_with_token_a_bigger_than_token_b() { pool.initialize( &stake_wasm_hash, &token_wasm_hash, + &stake_reward_wasm_hash, &lp_init_info, &factory, &10, // LP share decimals, unused From 3461ae5507f27fec8e86d8203a7491ec77b9e3e3 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Mon, 5 Aug 2024 15:56:13 +0200 Subject: [PATCH 15/16] Update makefile --- contracts/stake/Makefile | 1 + contracts/stake_rewards/Makefile | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/stake/Makefile b/contracts/stake/Makefile index 918bdcb53..1a7c1b864 100644 --- a/contracts/stake/Makefile +++ b/contracts/stake/Makefile @@ -7,6 +7,7 @@ test: build build: $(MAKE) -C ../token build || break; + $(MAKE) -C ../stake_rewards build || break; cargo build --target wasm32-unknown-unknown --release lint: fmt clippy diff --git a/contracts/stake_rewards/Makefile b/contracts/stake_rewards/Makefile index 47572a7a0..918bdcb53 100644 --- a/contracts/stake_rewards/Makefile +++ b/contracts/stake_rewards/Makefile @@ -7,7 +7,6 @@ test: build build: $(MAKE) -C ../token build || break; - $(MAKE) -C ../stake build || break; cargo build --target wasm32-unknown-unknown --release lint: fmt clippy From 22b218e56b13903038a2f2d7fc2f429d9307d163 Mon Sep 17 00:00:00 2001 From: Jakub <jakub@moonbite.space> Date: Tue, 6 Aug 2024 13:43:06 +0200 Subject: [PATCH 16/16] tests --- contracts/stake_rewards/src/contract.rs | 10 + contracts/stake_rewards/src/tests.rs | 2 +- contracts/stake_rewards/src/tests/bond.rs | 540 +++------------------ contracts/stake_rewards/src/tests/setup.rs | 25 +- 4 files changed, 74 insertions(+), 503 deletions(-) diff --git a/contracts/stake_rewards/src/contract.rs b/contracts/stake_rewards/src/contract.rs index 9f952612d..5325f6357 100644 --- a/contracts/stake_rewards/src/contract.rs +++ b/contracts/stake_rewards/src/contract.rs @@ -155,6 +155,8 @@ impl StakingRewardsTrait for StakingRewards { sender.require_auth(); let config = get_config(&env); + // only Staking contract which deployed this one can call this method + config.staking_contract.require_auth(); let mut distribution = get_distribution(&env, &config.reward_token); let last_stake = stakes.stakes.last().unwrap(); @@ -182,6 +184,8 @@ impl StakingRewardsTrait for StakingRewards { sender.require_auth(); let config = get_config(&env); + // only Staking contract which deployed this one can call this method + config.staking_contract.require_auth(); // check for rewards and withdraw them let found_rewards: WithdrawableRewardResponse = @@ -217,6 +221,8 @@ impl StakingRewardsTrait for StakingRewards { fn distribute_rewards(env: Env, total_staked_amount: i128) { let config = get_config(&env); + // only Staking contract which deployed this one can call this method + config.staking_contract.require_auth(); let calc_power_result = calc_power( &config, @@ -276,6 +282,8 @@ impl StakingRewardsTrait for StakingRewards { fn withdraw_rewards(env: Env, sender: Address, stakes: BondingInfo) { env.events().publish(("withdraw_rewards", "user"), &sender); let config = get_config(&env); + // only Staking contract which deployed this one can call this method + config.staking_contract.require_auth(); // get distribution data for the given reward let mut distribution = get_distribution(&env, &config.reward_token); @@ -327,6 +335,8 @@ impl StakingRewardsTrait for StakingRewards { admin.require_auth(); let config = get_config(&env); + // only Staking contract which deployed this one can call this method + config.staking_contract.require_auth(); // Load previous reward curve; it must exist if the distribution exists // In case of first time funding, it will be a constant 0 curve diff --git a/contracts/stake_rewards/src/tests.rs b/contracts/stake_rewards/src/tests.rs index bce7f557c..696b5ccbd 100644 --- a/contracts/stake_rewards/src/tests.rs +++ b/contracts/stake_rewards/src/tests.rs @@ -1,3 +1,3 @@ mod bond; -mod distribution; +// mod distribution; mod setup; diff --git a/contracts/stake_rewards/src/tests/bond.rs b/contracts/stake_rewards/src/tests/bond.rs index 2b3ff05ea..f410c06a3 100644 --- a/contracts/stake_rewards/src/tests/bond.rs +++ b/contracts/stake_rewards/src/tests/bond.rs @@ -1,9 +1,10 @@ use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, Env, + testutils::{Address as _, MockAuth, MockAuthInvoke}, + vec, Address, Env, IntoVal, Val, Vec, }; use super::setup::{deploy_staking_rewards_contract, deploy_token_contract}; +use crate::storage::{BondingInfo, Stake}; #[test] fn initialize_staking_rewards_contract() { @@ -11,521 +12,96 @@ fn initialize_staking_rewards_contract() { env.mock_all_auths(); let admin = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); let reward_token = deploy_token_contract(&env, &admin); + let staking = Address::generate(&env); - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); + let staking_rewards = + deploy_staking_rewards_contract(&env, &admin, &reward_token.address, &staking); assert_eq!(staking_rewards.query_admin(), admin); - assert_eq!(staking.query_admin(), admin); -} - -#[test] -fn calculate_bond_one_user() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - assert_eq!(staking.query_total_staked(), 0); - - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - assert_eq!(lp_token.balance(&user1), 10_000); - assert_eq!(lp_token.balance(&staking.address), 0); - assert_eq!(staking.query_config().config.lp_token, lp_token.address); - staking.bond(&user1, &10_000); - staking_rewards.calculate_bond(&user1); - - // we simulate full stake time - let start_timestamp = 60 * 3600 * 24; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - reward_token.mint(&admin, &1_000_000); - let reward_duration = 600; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); - - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp + 300; // move to a middle of distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 500_000 // half of the reward are undistributed - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 500_000 - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 500_000); - - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp + reward_duration; // move to the end of the distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 0 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 1_000_000 - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 1_000_000); -} - -#[test] -fn calculate_bond_multiple_users() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - assert_eq!(staking.query_total_staked(), 0); - - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - staking_rewards.calculate_bond(&user1); - - let user2 = Address::generate(&env); - lp_token.mint(&user2, &20_000); - staking.bond(&user2, &20_000); - staking_rewards.calculate_bond(&user2); - - let user3 = Address::generate(&env); - lp_token.mint(&user3, &30_000); - staking.bond(&user3, &30_000); - staking_rewards.calculate_bond(&user3); - - let user4 = Address::generate(&env); - lp_token.mint(&user4, &40_000); - staking.bond(&user4, &40_000); - staking_rewards.calculate_bond(&user4); - - // now all users have 100% APR after 60 days of staking - let start_timestamp = 3600 * 24 * 60; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - reward_token.mint(&admin, &1_000_000); - let reward_duration = 500; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); - - env.ledger().with_mut(|li| { - li.timestamp += 250; // move to a middle of distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 500_000 // half of the reward are undistributed - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 500_000 - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 50_000); - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 100_000); - staking_rewards.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 150_000); - staking_rewards.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 200_000); - - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp + reward_duration; // move to the end of the distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 0 - ); assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 1_000_000 + staking_rewards.query_config().config.staking_contract, + staking ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 100_000); - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 200_000); - staking_rewards.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 300_000); - staking_rewards.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 400_000); } #[test] -fn calculate_unbond_one_user() { +#[should_panic(expected = "Error(Auth, InvalidAction)")] +fn calculate_bond_called_by_anyone() { let env = Env::default(); - env.mock_all_auths(); env.budget().reset_unlimited(); let admin = Address::generate(&env); let lp_token = deploy_token_contract(&env, &admin); let reward_token = deploy_token_contract(&env, &admin); + let staking = Address::generate(&env); - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - assert_eq!(staking.query_total_staked(), 0); + let staking_rewards = + deploy_staking_rewards_contract(&env, &admin, &reward_token.address, &staking); let user1 = Address::generate(&env); lp_token.mint(&user1, &10_000); assert_eq!(lp_token.balance(&user1), 10_000); - assert_eq!(lp_token.balance(&staking.address), 0); - assert_eq!(staking.query_config().config.lp_token, lp_token.address); - staking.bond(&user1, &10_000); - - // User has 100% APR after 60 days of staking - let start_timestamp = 3600 * 24 * 60; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - reward_token.mint(&admin, &1_000_000); - let reward_duration = 500; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); - - staking_rewards.calculate_bond(&user1); - - env.ledger().with_mut(|li| { - li.timestamp += 250; // move to a middle of distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 500_000 // half of the reward are undistributed - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 500_000 - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 500_000); - - // now calculate unbond and unbond tokens, which should result - // in the rest of the reward being undistributed - - staking_rewards.calculate_unbond(&user1); - staking.unbond(&user1, &10_000, &0); - - env.ledger().with_mut(|li| { - li.timestamp += reward_duration; // move to the end of the distribution - }); - - staking_rewards.distribute_rewards(); - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 500_000 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 500_000 + // if staking rewards is not called by staking contract, authorization will fail + staking_rewards.calculate_bond( + &user1, + &BondingInfo { + stakes: vec![ + &env, + Stake { + stake: 10_000, + stake_timestamp: 0, + }, + ], + reward_debt: 0, + last_reward_time: 0, + total_stake: 10_000, + }, ); } #[test] -fn pay_rewards_during_calculate_unbond() { +#[ignore = "Figure out how to assert two authentication (user and contract) in the same assertion..."] +fn calculate_bond_called_by_staking_contract() { let env = Env::default(); - env.mock_all_auths(); env.budget().reset_unlimited(); let admin = Address::generate(&env); let lp_token = deploy_token_contract(&env, &admin); let reward_token = deploy_token_contract(&env, &admin); + let staking = Address::generate(&env); - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - assert_eq!(staking.query_total_staked(), 0); + let staking_rewards = + deploy_staking_rewards_contract(&env, &admin, &reward_token.address, &staking); let user1 = Address::generate(&env); lp_token.mint(&user1, &10_000); assert_eq!(lp_token.balance(&user1), 10_000); - assert_eq!(lp_token.balance(&staking.address), 0); - assert_eq!(staking.query_config().config.lp_token, lp_token.address); - staking.bond(&user1, &10_000); - - staking_rewards.calculate_bond(&user1); - - // This simulates 100% APR for the bonded user - let start_timestamp = 3600 * 24 * 60; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - reward_token.mint(&admin, &1_000_000); - let reward_duration = 600; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); - - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp + reward_duration; // move to the end of the distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 0 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 1_000_000 - ); - - // unbonding and automatically withdraws rewards - staking_rewards.calculate_unbond(&user1); - staking.unbond(&user1, &10_000, &0); - assert_eq!(reward_token.balance(&user1), 1_000_000); -} - -#[test] -fn calculate_unbond_multiple_users() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - staking_rewards.calculate_bond(&user1); - - let user2 = Address::generate(&env); - lp_token.mint(&user2, &20_000); - staking.bond(&user2, &20_000); - staking_rewards.calculate_bond(&user2); - - let user3 = Address::generate(&env); - lp_token.mint(&user3, &30_000); - staking.bond(&user3, &30_000); - staking_rewards.calculate_bond(&user3); - - let user4 = Address::generate(&env); - lp_token.mint(&user4, &40_000); - staking.bond(&user4, &40_000); - staking_rewards.calculate_bond(&user4); - - // 60 days of staking simulates the full APR for bonded users - let start_timestamp = 3600 * 24 * 60; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - reward_token.mint(&admin, &1_000_000); - let reward_duration = 2000; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); - - env.ledger().with_mut(|li| { - li.timestamp += 500; // move to a 1/4 of distribution - }); - - staking_rewards.distribute_rewards(); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 750_000 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 250_000 - ); - - // first user unbonds instead of withdrawing - staking_rewards.calculate_unbond(&user1); - staking.unbond(&user1, &10_000, &0); - assert_eq!(reward_token.balance(&user1), 25_000); - - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 50_000); - staking_rewards.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 75_000); - staking_rewards.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 100_000); - - env.ledger().with_mut(|li| { - li.timestamp += 500; // move to the half of the distribution - }); - - staking_rewards.distribute_rewards(); - - // 250_000 reward for 90_000 total staking points - // user2 250 * 20 / 90 = 55.555 - // user3 250 * 30 / 90 = 83.333 - // user4 250 * 40 / 90 = 111.111 - - // first user unbonds instead of withdrawing - staking_rewards.calculate_unbond(&user2); - staking.unbond(&user2, &20_000, &0); - assert_eq!(reward_token.balance(&user2), 50_000 + 55_555); - - staking_rewards.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 75_000 + 83_333); - staking_rewards.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 100_000 + 111_111); - - env.ledger().with_mut(|li| { - li.timestamp += 500; // move to the 3/4 of the distribution - }); - - staking_rewards.distribute_rewards(); - - // 250_000 reward for 70_000 total staking points - // user3 250 * 30 / 70 = 107.143 - // user4 250 * 40 / 70 = 142.857 - - // third user unbonds instead of withdrawing - staking_rewards.calculate_unbond(&user3); - staking.unbond(&user3, &30_000, &0); - assert_eq!(reward_token.balance(&user3), 158_333 + 107_143); - - staking_rewards.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 211_111 + 142_857); - - env.ledger().with_mut(|li| { - li.timestamp += 500; // move to the end of the distribution - }); - - staking_rewards.distribute_rewards(); - - // user4 is the only one left, so this time 250k goes to him - - // third user unbonds instead of withdrawing - staking_rewards.calculate_unbond(&user4); - staking.unbond(&user4, &40_000, &0); - assert_eq!(reward_token.balance(&user4), 353_968 + 250_000); - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 0 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 1_000_000 - ); -} - -#[test] -fn multiple_equal_users_with_different_multipliers() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let admin = Address::generate(&env); - let lp_token = deploy_token_contract(&env, &admin); - let reward_token = deploy_token_contract(&env, &admin); - - let (staking, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - - // first user bonds at timestamp 0 - // he will get 100% of his rewards - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - staking_rewards.calculate_bond(&user1); - - let fifteen_days = 3600 * 24 * 15; - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days; - }); - - // user2 will receive 75% of his reward - let user2 = Address::generate(&env); - lp_token.mint(&user2, &10_000); - staking.bond(&user2, &10_000); - staking_rewards.calculate_bond(&user2); - - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 2; - }); - - // user3 will receive 50% of his reward - let user3 = Address::generate(&env); - lp_token.mint(&user3, &10_000); - staking.bond(&user3, &10_000); - staking_rewards.calculate_bond(&user3); - - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 3; - }); - - // user4 will receive 25% of his reward - let user4 = Address::generate(&env); - lp_token.mint(&user4, &10_000); - staking.bond(&user4, &10_000); - staking_rewards.calculate_bond(&user4); - - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 4; - }); - - reward_token.mint(&admin, &1_000_000); - // reward distribution starts at the latest timestamp and lasts just 1 second - // the point is to prove that the multiplier works correctly - staking_rewards.fund_distribution(&(fifteen_days * 4), &1, &1_000_000); - - env.ledger().with_mut(|li| { - li.timestamp += 1; - }); - - staking_rewards.distribute_rewards(); - - // The way it works - contract will treat all the funds as distributed, and the amount - // that was not sent due to low staking bonus stays on the contract - - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 0 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 1_000_000 - ); - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 250_000); - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 187_500); - staking_rewards.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 125_000); - staking_rewards.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 62_500); + let bonding_info = BondingInfo { + stakes: vec![ + &env, + Stake { + stake: 10_000, + stake_timestamp: 0, + }, + ], + reward_debt: 0, + last_reward_time: 0, + total_stake: 10_000, + }; + + let bond_fn_arg: Vec<Val> = (user1.clone(), bonding_info.clone()).into_val(&env); + staking_rewards + .mock_auths(&[MockAuth { + address: &staking, + invoke: &MockAuthInvoke { + contract: &staking_rewards.address, + fn_name: "calculate_bond", + args: bond_fn_arg, + sub_invokes: &[], + }, + }]) + .calculate_bond(&user1, &bonding_info); } diff --git a/contracts/stake_rewards/src/tests/setup.rs b/contracts/stake_rewards/src/tests/setup.rs index ae5806a3e..8d4e1f419 100644 --- a/contracts/stake_rewards/src/tests/setup.rs +++ b/contracts/stake_rewards/src/tests/setup.rs @@ -2,17 +2,13 @@ use soroban_sdk::{Address, Env}; use crate::{ contract::{StakingRewards, StakingRewardsClient}, - stake_contract, token_contract, + token_contract, }; pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract::Client<'a> { token_contract::Client::new(env, &env.register_stellar_asset_contract(admin.clone())) } -fn deploy_stake_contract<'a>(env: &Env) -> stake_contract::Client<'a> { - stake_contract::Client::new(env, &env.register_contract_wasm(None, stake_contract::WASM)) -} - const MIN_BOND: i128 = 1000; const MIN_REWARD: i128 = 1000; const MAX_COMPLEXITY: u32 = 10; @@ -20,30 +16,19 @@ const MAX_COMPLEXITY: u32 = 10; pub fn deploy_staking_rewards_contract<'a>( env: &Env, admin: &Address, - lp_token: &Address, reward_token: &Address, -) -> (stake_contract::Client<'a>, StakingRewardsClient<'a>) { - let staking = deploy_stake_contract(env); - staking.initialize( - admin, - lp_token, - &MIN_BOND, - &MIN_REWARD, - admin, - admin, - &MAX_COMPLEXITY, - ); - + staking_contract: &Address, +) -> StakingRewardsClient<'a> { let staking_rewards = StakingRewardsClient::new(env, &env.register_contract(None, StakingRewards {})); staking_rewards.initialize( admin, - &staking.address, + &staking_contract, reward_token, &MAX_COMPLEXITY, &MIN_REWARD, &MIN_BOND, ); - (staking, staking_rewards) + staking_rewards }