From c169fc8a4792e80b32327c95d9a1e873aab2df77 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:03:25 +0200 Subject: [PATCH 01/34] adds stake from tag v1.0.0 --- Cargo.lock | 1 - contracts/pool_stable/src/tests/setup.rs | 8 - .../pool_stable/src/tests/stake_deployment.rs | 6 +- contracts/stake/Cargo.toml | 4 +- contracts/stake/Makefile | 1 - contracts/stake/src/contract.rs | 493 +++-- contracts/stake/src/distribution.rs | 386 +++- contracts/stake/src/lib.rs | 9 - contracts/stake/src/msg.rs | 2 - contracts/stake/src/storage.rs | 197 +- contracts/stake/src/tests/bond.rs | 139 +- contracts/stake/src/tests/distribution.rs | 1730 ++++++++--------- contracts/stake/src/tests/setup.rs | 100 +- 13 files changed, 1500 insertions(+), 1576 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e30e31212..d8179a6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,7 +882,6 @@ name = "phoenix-stake" version = "1.1.0" dependencies = [ "curve", - "itoa", "phoenix", "pretty_assertions", "soroban-decimal", diff --git a/contracts/pool_stable/src/tests/setup.rs b/contracts/pool_stable/src/tests/setup.rs index 3640e5966..17646308b 100644 --- a/contracts/pool_stable/src/tests/setup.rs +++ b/contracts/pool_stable/src/tests/setup.rs @@ -30,13 +30,6 @@ 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, @@ -69,7 +62,6 @@ 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, diff --git a/contracts/pool_stable/src/tests/stake_deployment.rs b/contracts/pool_stable/src/tests/stake_deployment.rs index d68c9eaf0..9dbbc2d98 100644 --- a/contracts/pool_stable/src/tests/stake_deployment.rs +++ b/contracts/pool_stable/src/tests/stake_deployment.rs @@ -2,9 +2,7 @@ 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, install_stake_rewards_wasm, -}; +use super::setup::{deploy_stable_liquidity_pool_contract, deploy_token_contract}; use crate::contract::{StableLiquidityPool, StableLiquidityPoolClient}; use crate::tests::setup::{install_stake_wasm, install_token_wasm}; use crate::{ @@ -101,7 +99,6 @@ 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 @@ -178,7 +175,6 @@ 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 diff --git a/contracts/stake/Cargo.toml b/contracts/stake/Cargo.toml index 50eda9a29..7872e1b55 100644 --- a/contracts/stake/Cargo.toml +++ b/contracts/stake/Cargo.toml @@ -11,15 +11,13 @@ crate-type = ["cdylib"] [features] testutils = ["soroban-sdk/testutils"] -upgrade = [] [dependencies] soroban-decimal = { workspace = true } curve = { workspace = true } phoenix = { workspace = true } soroban-sdk = { workspace = true } -itoa = { version = "1.0", default-features = false } -[dev-dependencies] +[dev_dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } pretty_assertions = { workspace = true } diff --git a/contracts/stake/Makefile b/contracts/stake/Makefile index 1a7c1b864..918bdcb53 100644 --- a/contracts/stake/Makefile +++ b/contracts/stake/Makefile @@ -7,7 +7,6 @@ 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/src/contract.rs b/contracts/stake/src/contract.rs index 6076cfa0b..093d161a9 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -1,26 +1,33 @@ -use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; +use soroban_decimal::Decimal; use soroban_sdk::{ - contract, contractimpl, contractmeta, log, map, panic_with_error, vec, Address, BytesN, Env, + contract, contractimpl, contractmeta, log, panic_with_error, vec, Address, BytesN, Env, String, Vec, }; +use crate::distribution::calc_power; +use crate::TOKEN_PER_POWER; use crate::{ distribution::{ - calculate_pending_rewards, get_reward_history, get_total_staked_history, - save_reward_history, save_total_staked_history, + 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, }, error::ContractError, - msg::{ConfigResponse, StakedResponse, WithdrawableReward, WithdrawableRewardsResponse}, + msg::{ + AnnualizedReward, AnnualizedRewardsResponse, ConfigResponse, StakedResponse, + WithdrawableReward, WithdrawableRewardsResponse, + }, storage::{ get_config, get_stakes, save_config, save_stakes, utils::{ - self, add_distribution, get_admin_old, get_distributions, get_total_staked_counter, + self, add_distribution, get_admin, get_distributions, get_total_staked_counter, is_initialized, set_initialized, }, - Config, Stake, ADMIN, + Config, Stake, }, token_contract, }; +use curve::Curve; // Metadata that is added on to the WASM custom section contractmeta!( @@ -31,7 +38,6 @@ contractmeta!( #[contract] pub struct Staking; -#[allow(dead_code)] pub trait StakingTrait { // Sets the token contract addresses for this pool #[allow(clippy::too_many_arguments)] @@ -52,10 +58,19 @@ pub trait StakingTrait { fn create_distribution_flow(env: Env, sender: Address, asset: Address); - fn distribute_rewards(env: Env, sender: Address, amount: i128, reward_token: Address); + fn distribute_rewards(env: Env); fn withdraw_rewards(env: Env, sender: Address); + fn fund_distribution( + env: Env, + sender: Address, + start_time: u64, + distribution_duration: u64, + token_address: Address, + token_amount: i128, + ); + // QUERIES fn query_config(env: Env) -> ConfigResponse; @@ -66,15 +81,13 @@ pub trait StakingTrait { fn query_total_staked(env: Env) -> i128; - // fn query_annualized_rewards(env: Env) -> AnnualizedRewardsResponse; + fn query_annualized_rewards(env: Env) -> AnnualizedRewardsResponse; fn query_withdrawable_rewards(env: Env, address: Address) -> WithdrawableRewardsResponse; - fn migrate_admin_key(env: Env) -> Result<(), ContractError>; - - // fn query_distributed_rewards(env: Env, asset: Address) -> u128; + fn query_distributed_rewards(env: Env, asset: Address) -> u128; - // fn query_undistributed_rewards(env: Env, asset: Address) -> u128; + fn query_undistributed_rewards(env: Env, asset: Address) -> u128; } #[contractimpl] @@ -133,16 +146,12 @@ impl StakingTrait for Staking { }; save_config(&env, config); - utils::save_admin_old(&env, &admin); + utils::save_admin(&env, &admin); utils::init_total_staked(&env); - save_total_staked_history(&env, map![&env]); } fn bond(env: Env, sender: Address, tokens: i128) { sender.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let ledger = env.ledger(); let config = get_config(&env); @@ -159,17 +168,30 @@ impl StakingTrait for Staking { lp_token_client.transfer(&sender, &env.current_contract_address(), &tokens); let mut stakes = get_stakes(&env, &sender); - - stakes.total_stake = stakes.total_stake.checked_add(tokens).unwrap_or_else(|| { - log!(&env, "Stake: Bond: overflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); let stake = Stake { stake: tokens, stake_timestamp: ledger.timestamp(), }; - stakes.stakes.push_back(stake); + stakes.total_stake += tokens; + // TODO: Discuss: Add implementation to add stake if another is present in +-24h timestamp to avoid + // creating multiple stakes the same day + + 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, + &distribution_address, + &mut distribution, + old_power, + new_power, + ); + } + stakes.stakes.push_back(stake); save_stakes(&env, &sender, &stakes); utils::increase_total_staked(&env, &tokens); @@ -181,22 +203,39 @@ impl StakingTrait for Staking { fn unbond(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64) { sender.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let config = get_config(&env); - let mut stakes = get_stakes(&env, &sender); + // check for rewards and withdraw them + let found_rewards: WithdrawableRewardsResponse = + Self::query_withdrawable_rewards(env.clone(), sender.clone()); + + if !found_rewards.rewards.is_empty() { + 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, + &distribution_address, + &mut distribution, + old_power, + new_power, + ); + } + let mut stakes = get_stakes(&env, &sender); remove_stake(&env, &mut stakes.stakes, stake_amount, stake_timestamp); - stakes.total_stake = stakes - .total_stake - .checked_sub(stake_amount) - .unwrap_or_else(|| { - log!(&env, "Stake: Unbond: underflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); + stakes.total_stake -= stake_amount; let lp_token_client = token_contract::Client::new(&env, &config.lp_token); lp_token_client.transfer(&env.current_contract_address(), &sender, &stake_amount); @@ -205,207 +244,321 @@ impl StakingTrait for Staking { utils::decrease_total_staked(&env, &stake_amount); env.events().publish(("unbond", "user"), &sender); - env.events().publish(("unbond", "token"), &config.lp_token); - env.events().publish(("unbond", "amount"), stake_amount); + env.events().publish(("bond", "token"), &config.lp_token); + env.events().publish(("bond", "amount"), stake_amount); } fn create_distribution_flow(env: Env, sender: Address, asset: Address) { sender.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let config = get_config(&env); - if sender != config.manager && sender != config.owner { + let manager = get_config(&env).manager; + let owner = get_config(&env).owner; + if sender != manager && sender != owner { log!(env, "Stake: create distribution: Non-authorized creation!"); panic_with_error!(&env, ContractError::Unauthorized); } - add_distribution(&env, &asset); - save_reward_history(&env, &asset, map![&env]); - - env.events() - .publish(("create_distribution_flow", "asset"), &asset); - } + let distribution = Distribution { + shares_per_point: 1u128, + shares_leftover: 0u64, + distributed_total: 0u128, + withdrawable_total: 0u128, + max_bonus_bps: 0u64, + bonus_per_day_bps: 0u64, + }; - fn distribute_rewards(env: Env, sender: Address, amount: i128, reward_token: Address) { - sender.require_auth(); + 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)); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + env.events().publish( + ("create_distribution_flow", "asset"), + &reward_token_client.address, + ); + } - let config = get_config(&env); - if sender != config.manager && sender != config.owner { - log!(env, "Stake: create distribution: Non-authorized creation!"); - panic_with_error!(&env, ContractError::Unauthorized); + fn distribute_rewards(env: Env) { + let total_staked_amount = get_total_staked_counter(&env); + let total_rewards_power = calc_power( + &get_config(&env), + total_staked_amount, + Decimal::one(), + TOKEN_PER_POWER, + ) as u128; + + if total_rewards_power == 0 { + log!(&env, "Stake: No rewards to distribute!"); + return; } - - if !get_distributions(&env).contains(&reward_token) { - log!( - env, - "Stake: Distribute rewards: No distribution for this reward token exists!" + 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 = + reward_token_client.balance(&env.current_contract_address()) as u128; + + 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.shares_per_point += 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, ); - panic_with_error!(&env, ContractError::DistributionNotFound); + env.events() + .publish(("distribute_rewards", "amount"), amount); } - - let current_timestamp = env.ledger().timestamp(); - let total_staked_amount = get_total_staked_counter(&env); - - let mut total_staked_history = get_total_staked_history(&env); - total_staked_history.set(current_timestamp, total_staked_amount as u128); - save_total_staked_history(&env, total_staked_history); - - let mut reward_history = get_reward_history(&env, &reward_token); - reward_history.set(current_timestamp, amount as u128); - save_reward_history(&env, &reward_token, reward_history); - - token_contract::Client::new(&env, &reward_token).transfer( - &sender, - &env.current_contract_address(), - &amount, - ); - - env.events() - .publish(("distribute_rewards", "asset"), &reward_token); } fn withdraw_rewards(env: Env, sender: Address) { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - env.events().publish(("withdraw_rewards", "user"), &sender); + let config = get_config(&env); - let mut 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, + &(reward_amount as i128), + ); - for asset in get_distributions(&env) { - let pending_reward = calculate_pending_rewards(&env, &asset, &stakes); + env.events().publish( + ("withdraw_rewards", "reward_token"), + &reward_token_client.address, + ); env.events() - .publish(("withdraw_rewards", "reward_token"), &asset); + .publish(("withdraw_rewards", "reward_amount"), reward_amount); + } + } - token_contract::Client::new(&env, &asset).transfer( - &env.current_contract_address(), - &sender, - &pending_reward, + fn fund_distribution( + env: Env, + sender: Address, + start_time: u64, + distribution_duration: u64, + token_address: Address, + token_amount: i128, + ) { + sender.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); } - stakes.last_reward_time = env.ledger().timestamp(); - save_stakes(&env, &sender, &stakes); + + 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(&sender, &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, token_amount as u128), (end_time, 0)); + + // 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 > token_amount as u128 { + 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); } // QUERIES fn query_config(env: Env) -> ConfigResponse { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); ConfigResponse { config: get_config(&env), } } fn query_admin(env: Env) -> Address { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - get_admin_old(&env) + get_admin(&env) } fn query_staked(env: Env, address: Address) -> StakedResponse { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let stakes = get_stakes(&env, &address); StakedResponse { - stakes: stakes.stakes, - total_stake: stakes.total_stake, - last_reward_time: stakes.last_reward_time, + stakes: get_stakes(&env, &address).stakes, } } fn query_total_staked(env: Env) -> i128 { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); get_total_staked_counter(&env) } - // fn query_annualized_rewards(env: Env) -> AnnualizedRewardsResponse { - // let mut aprs = vec![&env]; - // let total_stake_amount = get_total_staked_counter(&env); - // let apr_fn_arg: Val = total_stake_amount.into_val(&env); - - // for asset 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, - // amount: apr.amount, - // }); - // } + 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); + + 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 + / (total_stake_power as u128 * distribution.shares_per_point) as i128; + + aprs.push_back(AnnualizedReward { + asset: distribution_address.clone(), + amount: apr.to_string(&env), + }); + } - // AnnualizedRewardsResponse { rewards: aprs } - // } + AnnualizedRewardsResponse { rewards: aprs } + } fn query_withdrawable_rewards(env: Env, user: Address) -> WithdrawableRewardsResponse { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let stakes = get_stakes(&env, &user); + let config = get_config(&env); // iterate over all distributions and calculate withdrawable rewards let mut rewards = vec![&env]; - for asset in get_distributions(&env) { - let pending_reward = calculate_pending_rewards(&env, &asset, &stakes); - + 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); rewards.push_back(WithdrawableReward { - reward_address: asset, - reward_amount: pending_reward as u128, + reward_address: distribution_address, + reward_amount, }); } WithdrawableRewardsResponse { rewards } } - fn migrate_admin_key(env: Env) -> Result<(), ContractError> { - let admin = get_admin_old(&env); - env.storage().instance().set(&ADMIN, &admin); - Ok(()) + fn query_distributed_rewards(env: Env, asset: Address) -> u128 { + let distribution = get_distribution(&env, &asset); + distribution.distributed_total } - // fn query_distributed_rewards(env: Env, asset: Address) -> u128 { - // 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 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 query_undistributed_rewards(env: Env, asset: Address) -> u128 { + let distribution = get_distribution(&env, &asset); + let reward_token_client = token_contract::Client::new(&env, &asset); + reward_token_client.balance(&env.current_contract_address()) as u128 + - distribution.withdrawable_total + } } #[contractimpl] impl Staking { #[allow(dead_code)] pub fn update(env: Env, new_wasm_hash: BytesN<32>) { - let admin = get_admin_old(&env); + let admin = get_admin(&env); admin.require_auth(); + env.deployer().update_current_contract_wasm(new_wasm_hash); } } diff --git a/contracts/stake/src/distribution.rs b/contracts/stake/src/distribution.rs index 33e8f5ac4..a02731aa5 100644 --- a/contracts/stake/src/distribution.rs +++ b/contracts/stake/src/distribution.rs @@ -1,136 +1,312 @@ +use soroban_sdk::{contracttype, Address, Env}; + +use curve::Curve; use soroban_decimal::Decimal; -use soroban_sdk::{contracttype, log, panic_with_error, Address, Env, Map}; -use crate::{error::ContractError, storage::BondingInfo}; -use phoenix::ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; +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_DAY: u64 = 24 * 60 * 60; +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 { - RewardHistory(Address), - TotalStakedHistory, + Curve(Address), + Distribution(Address), + WithdrawAdjustment(WithdrawAdjustmentKey), } -pub fn save_reward_history(e: &Env, reward_token: &Address, reward_history: Map) { - e.storage().persistent().set( - &DistributionDataKey::RewardHistory(reward_token.clone()), - &reward_history, - ); - e.storage().persistent().extend_ttl( - &DistributionDataKey::RewardHistory(reward_token.clone()), - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); +// 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_history(e: &Env, reward_token: &Address) -> Map { - let reward_history = e - .storage() +pub fn get_reward_curve(env: &Env, asset: &Address) -> Option { + env.storage() .persistent() - .get(&DistributionDataKey::RewardHistory(reward_token.clone())) - .unwrap(); - e.storage().persistent().extend_ttl( - &DistributionDataKey::RewardHistory(reward_token.clone()), - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); + .get(&DistributionDataKey::Curve(asset.clone())) +} - reward_history +#[contracttype] +#[derive(Debug, Default, Clone)] +pub struct Distribution { + /// How many shares is single point worth + pub shares_per_point: 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_total_staked_history(e: &Env, total_staked_history: Map) { - e.storage().persistent().set( - &DistributionDataKey::TotalStakedHistory, - &total_staked_history, - ); - e.storage().persistent().extend_ttl( - &DistributionDataKey::TotalStakedHistory, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, +pub fn save_distribution(env: &Env, asset: &Address, distribution: &Distribution) { + env.storage().persistent().set( + &DistributionDataKey::Distribution(asset.clone()), + distribution, ); } -pub fn get_total_staked_history(e: &Env) -> Map { - let total_staked_history = e - .storage() +pub fn get_distribution(env: &Env, asset: &Address) -> Distribution { + env.storage() .persistent() - .get(&DistributionDataKey::TotalStakedHistory) - .unwrap(); - e.storage().persistent().extend_ttl( - &DistributionDataKey::TotalStakedHistory, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, + .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.shares_per_point; + apply_points_correction(env, user, asset, diff, ppw); +} + +/// Applies points correction for given address. +/// `shares_per_point` is current value from `SHARES_PER_POINT` - 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 - shares_per_point as i128 * 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, ); +} - total_staked_history +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 calculate_pending_rewards( +pub fn withdrawable_rewards( env: &Env, - reward_token: &Address, - user_info: &BondingInfo, -) -> i128 { - let current_timestamp = env.ledger().timestamp(); - let last_reward_day = user_info.last_reward_time; - - // Load reward history and total staked history from storage - let reward_history = get_reward_history(env, reward_token); - let total_staked_history = get_total_staked_history(env); - - // Get the keys from the reward history map (which are the days) - let reward_keys = reward_history.keys(); - - let mut pending_rewards: i128 = 0; - - // Find the closest timestamp after last_reward_day - if let Some(first_relevant_day) = reward_keys.iter().find(|&day| day > last_reward_day) { - for staking_reward_day in reward_keys - .iter() - .skip_while(|&day| day < first_relevant_day) - .take_while(|&day| day <= current_timestamp) - { - if let (Some(daily_reward), Some(total_staked)) = ( - reward_history.get(staking_reward_day), - total_staked_history.get(staking_reward_day), - ) { - if total_staked > 0 { - // Calculate multiplier based on the age of each stake - for stake in user_info.stakes.iter() { - // Calculate the user's share of the total staked amount at the time - let user_share = (stake.stake as u128) - .checked_mul(daily_reward) - .and_then(|product| product.checked_div(total_staked)) - .unwrap_or_else(|| { - log!(&env, "Pool Stable: Math error in user share calculation"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - let stake_age_days = (staking_reward_day - .saturating_sub(stake.stake_timestamp)) - / SECONDS_PER_DAY; - if stake_age_days == 0u64 { - continue; - } - let multiplier = if stake_age_days >= 60 { - Decimal::one() - } else { - Decimal::from_ratio(stake_age_days, 60) - }; - - // Apply the multiplier and accumulate the rewards - let adjusted_reward = user_share as i128 * multiplier; - pending_rewards = pending_rewards - .checked_add(adjusted_reward) - .unwrap_or_else(|| { - log!(&env, "Pool Stable: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); + owner: &Address, + distribution: &Distribution, + adjustment: &WithdrawAdjustment, + config: &Config, +) -> u128 { + let ppw = distribution.shares_per_point; + + 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 as i128, Decimal::one(), TOKEN_PER_POWER); + let points = (ppw * points as u128) as i128; + + let correction = adjustment.shares_correction; + let points = points + correction; + let amount = points >> SHARES_SHIFT; + amount as u128 - adjustment.withdrawn_rewards +} + +pub fn calculate_annualized_payout(reward_curve: Option, 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( + (c.value(now) - c.value(now + SECONDS_PER_YEAR)) as i128, + 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( + (c.value(now) * SECONDS_PER_YEAR as u128) as i128, + 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(), } +} - pending_rewards +pub fn calc_power( + config: &Config, + stakes: i128, + multiplier: Decimal, + token_per_power: i32, +) -> i128 { + if stakes < config.min_bond { + 0 + } else { + 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::; + let result = calculate_annualized_payout(reward_curve, 121); + assert_eq!(result, Decimal::zero()); + } } diff --git a/contracts/stake/src/lib.rs b/contracts/stake/src/lib.rs index b94dd92f0..d382485cc 100644 --- a/contracts/stake/src/lib.rs +++ b/contracts/stake/src/lib.rs @@ -16,14 +16,5 @@ pub mod token_contract { ); } -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/msg.rs b/contracts/stake/src/msg.rs index 6a9d116e4..3813c0fed 100644 --- a/contracts/stake/src/msg.rs +++ b/contracts/stake/src/msg.rs @@ -12,8 +12,6 @@ pub struct ConfigResponse { #[derive(Clone, Debug, Eq, PartialEq)] pub struct StakedResponse { pub stakes: Vec, - pub total_stake: i128, - pub last_reward_time: u64, } #[contracttype] diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index e8864a0ec..624cd8467 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -1,9 +1,5 @@ -use phoenix::ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec}; -use crate::stake_rewards_contract; -pub const ADMIN: Symbol = symbol_short!("ADMIN"); - #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Config { @@ -20,31 +16,18 @@ pub struct Config { const CONFIG: Symbol = symbol_short!("CONFIG"); pub fn get_config(env: &Env) -> Config { - let config = env - .storage() + env.storage() .persistent() .get(&CONFIG) - .expect("Stake: Config not set"); - env.storage().persistent().extend_ttl( - &CONFIG, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - - config + .expect("Stake: Config not set") } pub fn save_config(env: &Env, config: Config) { env.storage().persistent().set(&CONFIG, &config); - env.storage().persistent().extend_ttl( - &CONFIG, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); } #[contracttype] -#[derive(Clone, Debug, Eq, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Stake { /// The amount of staked tokens pub stake: i128, @@ -69,7 +52,7 @@ pub struct BondingInfo { } pub fn get_stakes(env: &Env, key: &Address) -> BondingInfo { - let bonding_info = match env.storage().persistent().get::<_, BondingInfo>(key) { + match env.storage().persistent().get::<_, BondingInfo>(key) { Some(stake) => stake, None => BondingInfo { stakes: Vec::new(env), @@ -77,25 +60,11 @@ pub fn get_stakes(env: &Env, key: &Address) -> BondingInfo { last_reward_time: 0u64, total_stake: 0i128, }, - }; - env.storage().persistent().has(&key).then(|| { - env.storage().persistent().extend_ttl( - &key, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - }); - - bonding_info + } } pub fn save_stakes(env: &Env, key: &Address, bonding_info: &BondingInfo) { env.storage().persistent().set(key, bonding_info); - env.storage().persistent().extend_ttl( - &key, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); } pub mod utils { @@ -103,7 +72,6 @@ pub mod utils { use super::*; - use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; use soroban_sdk::{log, panic_with_error, ConversionError, TryFromVal, Val}; #[derive(Clone, Copy)] @@ -113,7 +81,6 @@ pub mod utils { TotalStaked = 1, Distributions = 2, Initialized = 3, - StakeRewards = 4, } impl TryFromVal for Val { @@ -126,63 +93,25 @@ pub mod utils { pub fn is_initialized(e: &Env) -> bool { e.storage() - .instance() + .persistent() .get(&DataKey::Initialized) .unwrap_or(false) } pub fn set_initialized(e: &Env) { - e.storage().instance().set(&DataKey::Initialized, &true); - e.storage() - .instance() - .extend_ttl(PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); - } - - pub fn save_admin_old(e: &Env, address: &Address) { - e.storage().persistent().set(&DataKey::Admin, address); - e.storage().persistent().extend_ttl( - &DataKey::Admin, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - } - - pub fn _save_admin(e: &Env, address: &Address) { - e.storage().instance().set(&ADMIN, &address); - e.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + e.storage().persistent().set(&DataKey::Initialized, &true); } - pub fn get_admin_old(e: &Env) -> Address { - let admin = e.storage().persistent().get(&DataKey::Admin).unwrap(); - e.storage().persistent().extend_ttl( - &DataKey::Admin, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - - admin + pub fn save_admin(e: &Env, address: &Address) { + e.storage().persistent().set(&DataKey::Admin, address) } - pub fn _get_admin(e: &Env) -> Address { - e.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - e.storage().instance().get(&ADMIN).unwrap_or_else(|| { - log!(e, "Stake: Admin not set"); - panic_with_error!(&e, ContractError::AdminNotSet) - }) + pub fn get_admin(e: &Env) -> Address { + e.storage().persistent().get(&DataKey::Admin).unwrap() } pub fn init_total_staked(e: &Env) { e.storage().persistent().set(&DataKey::TotalStaked, &0i128); - e.storage().persistent().extend_ttl( - &DataKey::TotalStaked, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); } pub fn increase_total_staked(e: &Env, amount: &i128) { @@ -190,12 +119,6 @@ pub mod utils { e.storage() .persistent() .set(&DataKey::TotalStaked, &(count + amount)); - - e.storage().persistent().extend_ttl( - &DataKey::TotalStaked, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); } pub fn decrease_total_staked(e: &Env, amount: &i128) { @@ -203,116 +126,32 @@ pub mod utils { e.storage() .persistent() .set(&DataKey::TotalStaked, &(count - amount)); - - e.storage().persistent().extend_ttl( - &DataKey::TotalStaked, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); } pub fn get_total_staked_counter(env: &Env) -> i128 { - let total_staked = env - .storage() + env.storage() .persistent() .get(&DataKey::TotalStaked) - .unwrap(); - env.storage().persistent().extend_ttl( - &DataKey::TotalStaked, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - - total_staked + .unwrap() } // Keep track of all distributions to be able to iterate over them pub fn add_distribution(e: &Env, asset: &Address) { let mut distributions = get_distributions(e); - for old_asset in distributions.clone() { - if &old_asset == asset { - log!(&e, "Stake: Add distribution: Distribution already added"); - panic_with_error!(&e, ContractError::DistributionExists); - } + if distributions.contains(asset) { + log!(&e, "Stake: Add distribution: Distribution already added"); + panic_with_error!(&e, ContractError::DistributionExists); } distributions.push_back(asset.clone()); e.storage() .persistent() .set(&DataKey::Distributions, &distributions); - e.storage().persistent().extend_ttl( - &DataKey::Distributions, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); } pub fn get_distributions(e: &Env) -> Vec
{ - let distributions = e - .storage() - .persistent() - .get(&DataKey::Distributions) - .unwrap_or_else(|| soroban_sdk::vec![e]); e.storage() .persistent() - .has(&DataKey::Distributions) - .then(|| { - e.storage().persistent().extend_ttl( - &DataKey::Distributions, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ) - }); - - distributions - } -} - -// Implement `From` trait for conversion between `BondingInfo` structs -impl From 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 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 for stake_rewards_contract::Stake { - fn from(stake: Stake) -> Self { - stake_rewards_contract::Stake { - stake: stake.stake, - stake_timestamp: stake.stake_timestamp, - } - } -} - -impl From for Stake { - fn from(stake: stake_rewards_contract::Stake) -> Self { - Stake { - stake: stake.stake, - stake_timestamp: stake.stake_timestamp, - } + .get(&DataKey::Distributions) + .unwrap_or_else(|| soroban_sdk::vec![e]) } } diff --git a/contracts/stake/src/tests/bond.rs b/contracts/stake/src/tests/bond.rs index 74dba88b2..f3571a5df 100644 --- a/contracts/stake/src/tests/bond.rs +++ b/contracts/stake/src/tests/bond.rs @@ -1,19 +1,15 @@ -extern crate std; - use pretty_assertions::assert_eq; use soroban_sdk::{ - symbol_short, - testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Ledger}, - vec, Address, Env, IntoVal, Symbol, Vec, + testutils::{Address as _, Ledger}, + vec, Address, Env, }; use super::setup::{deploy_staking_contract, deploy_token_contract}; use crate::{ contract::{Staking, StakingClient}, - msg::{ConfigResponse, StakedResponse}, + msg::ConfigResponse, storage::{Config, Stake}, - tests::setup::{ONE_DAY, ONE_WEEK}, }; const DEFAULT_COMPLEXITY: u32 = 7; @@ -133,36 +129,10 @@ fn bond_simple() { &DEFAULT_COMPLEXITY, ); - env.ledger().with_mut(|li| { - li.timestamp = ONE_WEEK; - }); - lp_token.mint(&user, &10_000); staking.bond(&user, &10_000); - assert_eq!( - env.auths(), - [( - user.clone(), - AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - staking.address.clone(), - Symbol::new(&env, "bond"), - (&user.clone(), 10_000i128,).into_val(&env), - )), - sub_invocations: std::vec![AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - lp_token.address.clone(), - symbol_short!("transfer"), - (&user, &staking.address.clone(), 10_000i128).into_val(&env) - )), - sub_invocations: std::vec![], - },], - } - ),] - ); - let bonds = staking.query_staked(&user).stakes; assert_eq!( bonds, @@ -170,7 +140,7 @@ fn bond_simple() { &env, Stake { stake: 10_000, - stake_timestamp: ONE_WEEK, + stake_timestamp: 0, } ] ); @@ -205,16 +175,16 @@ fn unbond_simple() { lp_token.mint(&user2, &10_000); env.ledger().with_mut(|li| { - li.timestamp += ONE_DAY; + li.timestamp = 2000; }); staking.bond(&user, &10_000); env.ledger().with_mut(|li| { - li.timestamp += ONE_DAY; + li.timestamp = 4000; }); staking.bond(&user, &10_000); staking.bond(&user2, &10_000); env.ledger().with_mut(|li| { - li.timestamp += ONE_DAY; + li.timestamp = 4000; }); staking.bond(&user, &15_000); @@ -222,22 +192,8 @@ fn unbond_simple() { assert_eq!(lp_token.balance(&user), 0); assert_eq!(lp_token.balance(&staking.address), 45_000); - staking.unbond(&user, &10_000, &(ONE_DAY + ONE_DAY)); - - assert_eq!( - env.auths(), - [( - user.clone(), - AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - staking.address.clone(), - Symbol::new(&env, "unbond"), - (&user.clone(), 10_000i128, (ONE_DAY + ONE_DAY)).into_val(&env), - )), - sub_invocations: std::vec![], - } - ),] - ); + let stake_timestamp = 4000; + staking.unbond(&user, &10_000, &stake_timestamp); let bonds = staking.query_staked(&user).stakes; assert_eq!( @@ -246,11 +202,11 @@ fn unbond_simple() { &env, Stake { stake: 10_000, - stake_timestamp: ONE_DAY, + stake_timestamp: 2_000, }, Stake { stake: 15_000, - stake_timestamp: 3 * ONE_DAY, + stake_timestamp: 4_000, } ] ); @@ -309,11 +265,11 @@ fn unbond_wrong_user_stake_not_found() { lp_token.mint(&user2, &10_000); env.ledger().with_mut(|li| { - li.timestamp = ONE_DAY; + li.timestamp = 2_000; }); staking.bond(&user, &10_000); env.ledger().with_mut(|li| { - li.timestamp += ONE_DAY; + li.timestamp = 4_000; }); staking.bond(&user, &10_000); staking.bond(&user2, &10_000); @@ -322,21 +278,19 @@ fn unbond_wrong_user_stake_not_found() { assert_eq!(lp_token.balance(&user2), 0); assert_eq!(lp_token.balance(&staking.address), 30_000); - let non_existing_timestamp = ONE_DAY / 2; - staking.unbond(&user2, &10_000, &non_existing_timestamp); + staking.unbond(&user2, &10_000, &2_000); } #[test] fn pay_rewards_during_unbond() { + const STAKED_AMOUNT: i128 = 1_000; let env = Env::default(); env.mock_all_auths(); - env.cost_estimate().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 owner = Address::generate(&env); let lp_token = deploy_token_contract(&env, &admin); let reward_token = deploy_token_contract(&env, &admin); @@ -345,31 +299,30 @@ fn pay_rewards_during_unbond() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &DEFAULT_COMPLEXITY, ); lp_token.mint(&user, &10_000); - reward_token.mint(&admin, &20_000); + reward_token.mint(&admin, &10_000); - let staked = 1_000; - staking.bond(&user, &staked); + staking.create_distribution_flow(&manager, &reward_token.address); + staking.fund_distribution(&admin, &0u64, &10_000u64, &reward_token.address, &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; + li.timestamp = 5_000; }); + staking.distribute_rewards(); - staking.create_distribution_flow(&admin, &reward_token.address); - - // simulate passing 20 days and distributing 1000 tokens each day - for _ in 0..20 { - staking.distribute_rewards(&admin, &1_000, &reward_token.address); - env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; - }); - } - + // 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) @@ -377,28 +330,12 @@ fn pay_rewards_during_unbond() { .iter() .map(|reward| reward.reward_amount) .sum::(), - 20_000 + 5_000 ); - assert_eq!(reward_token.balance(&user), 0); - - // we first have to withdraw_rewards _before_ unbonding - // as this messes up with the reward calculation - // if we unbond first then we get no rewards - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 20_000); - // user bonded at timestamp 0 - staking.unbond(&user, &staked, &0); - assert_eq!(lp_token.balance(&staking.address), 0); - assert_eq!(lp_token.balance(&user), 9000 + staked); - assert_eq!( - staking.query_staked(&user), - StakedResponse { - stakes: Vec::new(&env), - total_stake: 0i128, - last_reward_time: 6_912_000 - } - ); + assert_eq!(reward_token.balance(&user), 0); + staking.unbond(&user, &STAKED_AMOUNT, &0); + assert_eq!(reward_token.balance(&user), 5_000); } #[should_panic( @@ -409,7 +346,7 @@ fn initialize_staking_contract_should_panic_when_min_bond_invalid() { let env = Env::default(); env.mock_all_auths(); - let staking = StakingClient::new(&env, &env.register(Staking, ())); + let staking = StakingClient::new(&env, &env.register_contract(None, Staking {})); staking.initialize( &Address::generate(&env), @@ -428,7 +365,7 @@ fn initialize_staking_contract_should_panic_when_min_rewards_invalid() { let env = Env::default(); env.mock_all_auths(); - let staking = StakingClient::new(&env, &env.register(Staking, ())); + let staking = StakingClient::new(&env, &env.register_contract(None, Staking {})); staking.initialize( &Address::generate(&env), @@ -447,7 +384,7 @@ fn initialize_staking_contract_should_panic_when_max_complexity_invalid() { let env = Env::default(); env.mock_all_auths(); - let staking = StakingClient::new(&env, &env.register(Staking, ())); + let staking = StakingClient::new(&env, &env.register_contract(None, Staking {})); staking.initialize( &Address::generate(&env), diff --git a/contracts/stake/src/tests/distribution.rs b/contracts/stake/src/tests/distribution.rs index 9a20517d1..92afef1d0 100644 --- a/contracts/stake/src/tests/distribution.rs +++ b/contracts/stake/src/tests/distribution.rs @@ -1,26 +1,24 @@ -extern crate std; use soroban_sdk::{ testutils::{Address as _, Ledger}, - vec, Address, Env, + vec, Address, Env, String, }; use super::setup::{deploy_staking_contract, deploy_token_contract}; use pretty_assertions::assert_eq; -use crate::{ - msg::{WithdrawableReward, WithdrawableRewardsResponse}, - tests::setup::SIXTY_DAYS, +use crate::msg::{ + AnnualizedReward, AnnualizedRewardsResponse, WithdrawableReward, WithdrawableRewardsResponse, }; #[test] fn add_distribution_and_distribute_reward() { let env = Env::default(); env.mock_all_auths(); - env.cost_estimate().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); @@ -29,30 +27,50 @@ fn add_distribution_and_distribute_reward() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &50u32, ); - staking.create_distribution_flow(&admin, &reward_token.address); + staking.create_distribution_flow(&manager, &reward_token.address); - let reward_amount: i128 = 100_000; - reward_token.mint(&admin, &reward_amount); + 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, &1000); staking.bond(&user, &1000); - // simulate moving forward 60 days for the full APR multiplier env.ledger().with_mut(|li| { - li.timestamp = SIXTY_DAYS; + li.timestamp = 2_000; }); - for _ in 0..60 { - staking.distribute_rewards(&admin, &(reward_amount / 60i128), &reward_token.address); - env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; - }); - } + let reward_duration = 600; + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + reward_amount + ); + + env.ledger().with_mut(|li| { + li.timestamp = 2_600; + }); + 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), @@ -61,157 +79,145 @@ fn add_distribution_and_distribute_reward() { &env, WithdrawableReward { reward_address: reward_token.address.clone(), - // dividing 100k / 60 rounding - reward_amount: 99_960_u128 + reward_amount } ] } ); staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 99_960); + assert_eq!(reward_token.balance(&user), reward_amount as i128); } -// #[test] -// fn two_distributions() { -// let env = Env::default(); -// env.mock_all_auths(); -// env.cost_estimate().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 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); + staking.bond(&user, &1000); + + env.ledger().with_mut(|li| { + li.timestamp = 2_000; + }); + + let reward_duration = 600; + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token_2.address, + &((reward_amount * 2) as i128), + ); + + // distribute rewards during half time + env.ledger().with_mut(|li| { + li.timestamp = 2_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 = 2_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(); - env.cost_estimate().budget().reset_unlimited(); let admin = Address::generate(&env); let user = Address::generate(&env); @@ -219,6 +225,7 @@ fn four_users_with_different_stakes() { 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); @@ -228,14 +235,14 @@ fn four_users_with_different_stakes() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &50u32, ); - staking.create_distribution_flow(&admin, &reward_token.address); + staking.create_distribution_flow(&manager, &reward_token.address); - let reward_amount: i128 = 100_000; - reward_token.mint(&admin, &reward_amount); + 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); @@ -247,13 +254,23 @@ fn four_users_with_different_stakes() { 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; + li.timestamp = 2_000; }); - // distribute 100k of rewards once - staking.distribute_rewards(&admin, &reward_amount, &reward_token.address); + let reward_duration = 600; + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp = 2_600; + }); + staking.distribute_rewards(); // total staked amount is 10_000 // user1 should have 10% of the rewards, user2 20%, user3 30%, user4 40% @@ -317,15 +334,15 @@ fn four_users_with_different_stakes() { } #[test] -#[should_panic( - expected = "Stake: Distribute rewards: No distribution for this reward token exists" -)] -fn fund_rewards_without_establishing_distribution() { +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); @@ -335,557 +352,102 @@ fn fund_rewards_without_establishing_distribution() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &50u32, ); - reward_token.mint(&admin, &1000); + staking.create_distribution_flow(&manager, &reward_token.address); - staking.distribute_rewards(&admin, &2_000, &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); + staking.bond(&user, &1000); + + env.ledger().with_mut(|li| { + li.timestamp = 2_000; + }); + + let reward_duration = 600; + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); + + env.ledger().with_mut(|li| { + li.timestamp = 2_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 = 2_600; + }); + 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 + } + ] + } + ); -// #[test] -// fn try_to_withdraw_rewards_without_bonding() { -// let env = Env::default(); -// env.mock_all_auths(); -// env.cost_estimate().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] -// fn calculate_apr() { -// let env = Env::default(); -// env.mock_all_auths(); -// env.cost_estimate().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.cost_estimate().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.cost_estimate().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.cost_estimate().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, -// ); -// } + 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 multiple_equal_users_with_different_multipliers() { +fn two_users_both_bonds_after_distribution_starts() { let env = Env::default(); env.mock_all_auths(); - env.cost_estimate().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); @@ -894,73 +456,130 @@ fn multiple_equal_users_with_different_multipliers() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &50u32, ); - staking.create_distribution_flow(&admin, &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.create_distribution_flow(&manager, &reward_token.address); + + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); - let fifteen_days = 3600 * 24 * 15; env.ledger().with_mut(|li| { - li.timestamp = fifteen_days; + li.timestamp = 2_000; }); - // user2 will receive 75% of his reward - let user2 = Address::generate(&env); - lp_token.mint(&user2, &10_000); - staking.bond(&user2, &10_000); + let reward_duration = 600; + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 2; + li.timestamp = 2_200; }); + lp_token.mint(&user, &1000); + staking.bond(&user, &1000); - // user3 will receive 50% of his reward - let user3 = Address::generate(&env); - lp_token.mint(&user3, &10_000); - staking.bond(&user3, &10_000); + 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 = fifteen_days * 3; + li.timestamp = 2_400; }); + lp_token.mint(&user2, &1000); + staking.bond(&user2, &1000); - // user4 will receive 25% of his reward - let user4 = Address::generate(&env); - lp_token.mint(&user4, &10_000); - staking.bond(&user4, &10_000); + 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 = fifteen_days * 4; + li.timestamp = 2_600; }); + staking.distribute_rewards(); - reward_token.mint(&admin, &1_000_000); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); - - // 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 + // 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(&user1); - assert_eq!(reward_token.balance(&user1), 250_000); + staking.withdraw_rewards(&user); + assert_eq!(reward_token.balance(&user), 66_666); 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); + assert_eq!(reward_token.balance(&user2), 33_333); } #[test] -fn distribute_rewards_daily_multiple_times_different_stakes() { +#[should_panic(expected = "Stake: Fund distribution: Not reward curve exists")] +fn fund_rewards_without_establishing_distribution() { let env = Env::default(); env.mock_all_auths(); - env.cost_estimate().budget().reset_unlimited(); 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); @@ -969,123 +588,165 @@ fn distribute_rewards_daily_multiple_times_different_stakes() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &50u32, ); - staking.create_distribution_flow(&admin, &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); + reward_token.mint(&admin, &1000); - let fifteen_days = 3600 * 24 * 15; - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days; - }); + staking.fund_distribution(&admin, &2_000, &600, &reward_token.address, &1000); +} - // user2 will receive 75% of his reward - let user2 = Address::generate(&env); - lp_token.mint(&user2, &10_000); - staking.bond(&user2, &10_000); +#[test] +fn try_to_withdraw_rewards_without_bonding() { + let env = Env::default(); + env.mock_all_auths(); - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 2; - }); + 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); - // user3 will receive 50% of his reward - let user3 = Address::generate(&env); - lp_token.mint(&user3, &10_000); - staking.bond(&user3, &10_000); + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &owner, + &50u32, + ); - env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 3; - }); + staking.create_distribution_flow(&manager, &reward_token.address); - // user4 will receive 25% of his reward - let user4 = Address::generate(&env); - lp_token.mint(&user4, &10_000); - staking.bond(&user4, &10_000); + let reward_amount: u128 = 100_000; + reward_token.mint(&admin, &(reward_amount as i128)); env.ledger().with_mut(|li| { - li.timestamp = fifteen_days * 4; + li.timestamp = 2_000; }); - reward_token.mint(&admin, &4_000_000); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); - - // 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 + let reward_duration = 600; + staking.fund_distribution( + &admin, + &2_000, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); - 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); - - // 24h later env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; + li.timestamp = 2_600; }); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + reward_amount + ); + assert_eq!(staking.query_distributed_rewards(&reward_token.address), 0); - staking.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 500_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 379_166); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 254_166); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 129_166); + 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)); - // 24h later env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; + li.timestamp = 2_000; }); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); - staking.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 750_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 574_999); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 387_499); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 199_999); + let reward_duration = 600; + staking.fund_distribution( + &admin, + &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); - // 24h later env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; + li.timestamp = 2_000; }); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); - staking.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 1_000_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 774_999); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 524_999); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 274_999); + let reward_duration = 600; + staking.fund_distribution(&admin, &2_000, &reward_duration, &reward_token.address, &10); } #[test] -fn distribute_rewards_daily_multiple_times_same_stakes() { +fn calculate_apr() { let env = Env::default(); env.mock_all_auths(); - env.cost_estimate().budget().reset_unlimited(); - - let user = Address::generate(&env); - let user2 = Address::generate(&env); - let user3 = Address::generate(&env); - let user4 = Address::generate(&env); 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); @@ -1094,103 +755,382 @@ fn distribute_rewards_daily_multiple_times_same_stakes() { admin.clone(), &lp_token.address, &manager, - &admin, + &owner, &50u32, ); - staking.create_distribution_flow(&admin, &reward_token.address); - // bond tokens for users; each user has a different amount staked + 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 = 1706968777; + li.timestamp = 0; }); + // whole year of distribution + let reward_duration = 60 * 60 * 24 * 365; + staking.fund_distribution( + &admin, + &2_000, + &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); staking.bond(&user, &1000); - env.ledger().with_mut(|li| { - li.timestamp = 1714741177; - }); + // 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") + } + ] + } + ); - lp_token.mint(&user2, &2000); - staking.bond(&user2, &2000); + let reward_amount: u128 = 50_000; + reward_token.mint(&admin, &(reward_amount as i128)); + + staking.fund_distribution( + &admin, + &2_000, + &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); +} + +#[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); + staking.bond(&user_1, &1_000); + staking.bond(&user_2, &1_000); + let reward_duration = 10_000; + staking.fund_distribution( + &admin, + &0, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); env.ledger().with_mut(|li| { - li.timestamp = 1714741177; + li.timestamp = 2_000; }); - lp_token.mint(&user3, &3000); - staking.bond(&user3, &3000); + 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 = 1715741177; + li.timestamp = 5_000; }); + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 50_000 + ); - lp_token.mint(&user4, &4000); - staking.bond(&user4, &4000); + // 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 + } + ] + } + ); - // simulate moving forward 60 days for the full APR multiplier env.ledger().with_mut(|li| { - li.timestamp += SIXTY_DAYS; + li.timestamp = 10_000; }); - reward_token.mint(&admin, &4_000_000); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); + staking.distribute_rewards(); + assert_eq!( + staking.query_undistributed_rewards(&reward_token.address), + 0 + ); + assert_eq!( + staking.query_distributed_rewards(&reward_token.address), + reward_amount + ); - // 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_withdrawable_rewards(&user_2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token.address.clone(), + reward_amount: 75_000 + } + ] + } + ); - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 100_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 200_000); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 300_000); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 400_000); + staking.withdraw_rewards(&user_1); + assert_eq!(reward_token.balance(&user_1), 25_000i128); +} - // 24h later - env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; - }); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); +#[test] +fn test_bond_withdraw_unbond() { + let env = Env::default(); + env.mock_all_auths(); - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 200_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 400_000); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 600_000); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 800_000); + 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); - // 24h later - env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; - }); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); + let staking = deploy_staking_contract( + &env, + admin.clone(), + &lp_token.address, + &manager, + &owner, + &50u32, + ); - staking.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 300_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 600_000); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 900_000); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 1_200_000); + 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); + staking.bond(&user, &1_000); + + let reward_duration = 10_000; + + staking.fund_distribution( + &admin, + &0, + &reward_duration, + &reward_token.address, + &(reward_amount as i128), + ); - // 24h later env.ledger().with_mut(|li| { - li.timestamp += 3600 * 24; + li.timestamp = 10_000; }); - staking.distribute_rewards(&admin, &1_000_000, &reward_token.address); + 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!(reward_token.balance(&user), 400_000); - staking.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 800_000); - staking.withdraw_rewards(&user3); - assert_eq!(reward_token.balance(&user3), 1_200_000); - staking.withdraw_rewards(&user4); - assert_eq!(reward_token.balance(&user4), 1_600_000); + 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, + ); - assert_eq!(reward_token.balance(&staking.address), 0); + 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(&admin, &0, &FIVE_MINUTES, &reward_token.address, &1000); + staking.fund_distribution( + &admin, + &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( + &admin, + &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 9d393a5ac..5cbfc4e7e 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, BytesN, Env}; +use soroban_sdk::{testutils::Address as _, Address, Env}; use crate::{ contract::{Staking, StakingClient}, @@ -6,30 +6,11 @@ use crate::{ }; pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract::Client<'a> { - token_contract::Client::new( - env, - &env.register_stellar_asset_contract_v2(admin.clone()) - .address(), - ) -} - -#[allow(clippy::too_many_arguments)] -mod stake_latest { - soroban_sdk::contractimport!( - file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" - ); -} - -#[allow(dead_code)] -fn install_stake_latest_wasm(env: &Env) -> BytesN<32> { - env.deployer().upload_contract_wasm(stake_latest::WASM) + token_contract::Client::new(env, &env.register_stellar_asset_contract(admin.clone())) } 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, @@ -40,7 +21,7 @@ pub fn deploy_staking_contract<'a>( max_complexity: &u32, ) -> StakingClient<'a> { let admin = admin.into().unwrap_or(Address::generate(env)); - let staking = StakingClient::new(env, &env.register(Staking, ())); + let staking = StakingClient::new(env, &env.register_contract(None, Staking {})); staking.initialize( &admin, @@ -53,78 +34,3 @@ pub fn deploy_staking_contract<'a>( ); staking } - -#[cfg(feature = "upgrade")] -use soroban_sdk::{testutils::Ledger, vec}; - -#[test] -#[cfg(feature = "upgrade")] -fn upgrade_stake_contract() { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - let admin = Address::generate(&env); - let user = Address::generate(&env); - - let token_client = deploy_token_contract(&env, &admin); - token_client.mint(&user, &1_000); - - let stake_addr = env.register_contract_wasm(None, stake_v_1_0_0::WASM); - - let stake_v_1_0_0_client = stake_v_1_0_0::Client::new(&env, &stake_addr); - - let manager = Address::generate(&env); - let owner = Address::generate(&env); - - stake_v_1_0_0_client.initialize( - &admin, - &token_client.address, - &10, - &10, - &manager, - &owner, - &10, - ); - - assert_eq!(stake_v_1_0_0_client.query_admin(), admin); - - env.ledger().with_mut(|li| li.timestamp = 100); - stake_v_1_0_0_client.bond(&user, &1_000); - assert_eq!( - stake_v_1_0_0_client.query_staked(&user), - stake_v_1_0_0::StakedResponse { - stakes: vec![ - &env, - stake_v_1_0_0::Stake { - stake: 1_000i128, - stake_timestamp: 100 - } - ] - } - ); - - env.ledger().with_mut(|li| li.timestamp = 10_000); - - let new_stake_wasm = install_stake_latest_wasm(&env); - stake_v_1_0_0_client.update(&new_stake_wasm); - stake_v_1_0_0_client.update(&new_stake_wasm); - - let upgraded_stake_client = stake_latest::Client::new(&env, &stake_addr); - - assert_eq!(upgraded_stake_client.query_admin(), admin); - - env.ledger().with_mut(|li| li.timestamp = 20_000); - - upgraded_stake_client.unbond(&user, &1_000, &100); - assert_eq!( - upgraded_stake_client.query_staked(&user), - stake_latest::StakedResponse { - stakes: vec![&env,], - total_stake: 0i128 - } - ); - - upgraded_stake_client.create_distribution_flow(&owner, &token_client.address); - token_client.mint(&owner, &1_000); - upgraded_stake_client.distribute_rewards(&owner, &1_000, &token_client.address); -} From 99366449adfb42737c5f8d1eef86309511543726 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:16:34 +0200 Subject: [PATCH 02/34] deletes stake_rewards contract --- Cargo.lock | 10 - contracts/stake_rewards/Cargo.toml | 22 - contracts/stake_rewards/Makefile | 21 - contracts/stake_rewards/README.md | 212 ------ contracts/stake_rewards/src/contract.rs | 624 ------------------ contracts/stake_rewards/src/distribution.rs | 405 ------------ contracts/stake_rewards/src/error.rs | 21 - contracts/stake_rewards/src/lib.rs | 21 - contracts/stake_rewards/src/msg.rs | 23 - contracts/stake_rewards/src/storage.rs | 148 ----- contracts/stake_rewards/src/tests.rs | 3 - contracts/stake_rewards/src/tests/bond.rs | 107 --- .../stake_rewards/src/tests/distribution.rs | 542 --------------- contracts/stake_rewards/src/tests/setup.rs | 37 -- 14 files changed, 2196 deletions(-) delete mode 100644 contracts/stake_rewards/Cargo.toml delete mode 100644 contracts/stake_rewards/Makefile delete mode 100644 contracts/stake_rewards/README.md delete mode 100644 contracts/stake_rewards/src/contract.rs delete mode 100644 contracts/stake_rewards/src/distribution.rs delete mode 100644 contracts/stake_rewards/src/error.rs delete mode 100644 contracts/stake_rewards/src/lib.rs delete mode 100644 contracts/stake_rewards/src/msg.rs delete mode 100644 contracts/stake_rewards/src/storage.rs delete mode 100644 contracts/stake_rewards/src/tests.rs delete mode 100644 contracts/stake_rewards/src/tests/bond.rs delete mode 100644 contracts/stake_rewards/src/tests/distribution.rs delete mode 100644 contracts/stake_rewards/src/tests/setup.rs diff --git a/Cargo.lock b/Cargo.lock index d8179a6b2..ac416e9df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -888,16 +888,6 @@ dependencies = [ "soroban-sdk", ] -[[package]] -name = "phoenix-stake-rewards" -version = "1.1.0" -dependencies = [ - "curve", - "phoenix", - "soroban-decimal", - "soroban-sdk", -] - [[package]] name = "phoenix-trader" version = "1.1.0" diff --git a/contracts/stake_rewards/Cargo.toml b/contracts/stake_rewards/Cargo.toml deleted file mode 100644 index 1b23eec86..000000000 --- a/contracts/stake_rewards/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "phoenix-stake-rewards" -version = { workspace = true } -authors = ["Jakub "] -repository = { workspace = true } -edition = { workspace = true } -license = { workspace = true } - -[lib] -crate-type = ["cdylib"] - -[features] -testutils = ["soroban-sdk/testutils"] - -[dependencies] -soroban-decimal = { workspace = true } -curve = { workspace = true } -phoenix = { workspace = true } -soroban-sdk = { workspace = true } - -[dev-dependencies] -soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/stake_rewards/Makefile b/contracts/stake_rewards/Makefile deleted file mode 100644 index 918bdcb53..000000000 --- a/contracts/stake_rewards/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -default: all - -all: lint build test - -test: build - cargo test - -build: - $(MAKE) -C ../token build || break; - cargo build --target wasm32-unknown-unknown --release - -lint: fmt clippy - -fmt: - cargo fmt --all - -clippy: build - cargo clippy --all-targets -- -D warnings - -clean: - cargo clean diff --git a/contracts/stake_rewards/README.md b/contracts/stake_rewards/README.md deleted file mode 100644 index 0e641516f..000000000 --- a/contracts/stake_rewards/README.md +++ /dev/null @@ -1,212 +0,0 @@ -# STAKING - -## Main functionality -Provides staking capabilities, reward distribution and reward management functionalities to the Phoenix DEX. - -## Messages: -`initialize` - -Params: -- `admin`: `Address` of the administrator for the contract -- `lp_token`: `Address` of the liquidity pool used with this stake contract -- `min_bond`: `i128` value showing the minimum required bond -- `min_reward`: `i128` the minimum amount of rewards the user can withdraw. - -Return type: -void - -Description: -Used to set up the staking contract with the initial parameters. - -
- -`bond` - -Params: -- `sender`: `Address` of the user that sends tokens to the stake contract. -- `tokens`: `i128` value representing the number of tokens the user sends. - -Return type: -void - -Description: -Allows for users to stake/bond their lp tokens - -
- -`unbond` - -Params: -- `sender`: `Address` of the user that wants to unbond/unstake their tokens. -- `stake_amount`: `i128` value representing the numbers of stake to be unbond. -- `take_timestamp`: `u64`value used to calculate the correct stake to be removed - -Return type: -void - -Description: -Allows the user remove their staked tokens from the stake contract, with any rewards they may have earned, based on the amount of staked tokens and stake's timestamp. - -
- -`create_distribution_flow` - -Params: -- `sender`: `Address` of the user that creates the flow -- `sender`: `Address` of the user that will be managing the flow -- `asset`: `Address` of the asset that will be used in the distribution flow - -Return type: -void - -Description: -Creates a distribution flow for sending rewards, that are managed by a manager for a specific asset. - -
- -`distribute_rewards` - -Params: -None - -Return type: -void - -Description: -Sends the rewards to all the users that have stakes, on the basis of the current reward distribution rule set and total staked amount. - -
- -`withdraw_rewards` - -Params: -- `sender`: `Address` of the user that wants to withdraw their rewards - -Return type: -void - -Description: -Allows for users to withdraw their rewards from the stake contract. - -
- -`fund_distribution` - -Params: -- `sender`: `Address` of the user that calls this method. -- `start_time`: `u64` value representing the time in which the funding has started. -- `distribution_duration`: `u64` value representing the duration for the distribution in seconds -- `token_address`: `Address` of the token that will be used for the reward distribution -- `token_amount`: `i128` value representing how many tokens will be allocated for the distribution time - - -Return type: -void - -Description: -Sends funds for a reward distribution. - -
- -## Queries: -`query_config` - -Params: -None - -Return type: -`ConfigResponse` struct. - -Description: -Queries the contract `Config` - -
- -`query_admin` - -Params: -None - -Return type: -`Address` struct. - -Description: -Returns the address of the admin for the given stake contract. - -
- -`query_staked` - -Params: -- `address`: `Address` of the stake contract we want to query - -Return type: -`StakedResponse` struct. - -Description: -Provides information about the stakes of a specific address. - -
- -`query_total_staked` - -Params: -None - -Return type: -`i128` - -Description: -Returns the total amount of tokens currently staked in the contract. - -
- -`query_annualized_rewards` - -Params: -None - -Return type: -`AnnualizedRewardsResponse` struct - -Description: -Provides an overview of the annualized rewards for each distributed asset. - -
- -`query_withdrawable_rewards` - -Params: -- `address`: `Address` whose rewards we are searching - -Return type: -`WithdrawableRewardsResponse` struct - -Description: -Queries the amount of rewards that a given address can withdraw. - -
- -`query_distributed_rewards` - -Params: -- `asset`: `Address` of the token for which we query - -Return type: -`u128` - -Description: -Reports the total amount of rewards distributed for a specific asset. - -
- -`query_undistributed_rewards` - -Params: -- `asset`: `Address` of the token for which we query - -Return type: -`u128` - -Description: -Queries the total amount of remaining rewards for a given asset. diff --git a/contracts/stake_rewards/src/contract.rs b/contracts/stake_rewards/src/contract.rs deleted file mode 100644 index 67ada0167..000000000 --- a/contracts/stake_rewards/src/contract.rs +++ /dev/null @@ -1,624 +0,0 @@ -use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; -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, -}; - -use crate::distribution::calc_power; -use crate::storage::ADMIN; -use crate::TOKEN_PER_POWER; -use crate::{ - distribution::{ - calc_withdraw_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, - }, - error::ContractError, - msg::{AnnualizedRewardResponse, ConfigResponse, WithdrawableRewardResponse}, - storage::{ - get_config, save_config, - utils::{self, get_admin_old, is_initialized, set_initialized}, - BondingInfo, Config, - }, - token_contract, -}; -use curve::Curve; - -// Metadata that is added on to the WASM custom section -contractmeta!( - key = "Description", - val = "Phoenix Protocol staking rewards distribution" -); - -#[contract] -pub struct StakingRewards; - -#[allow(dead_code)] -pub trait StakingRewardsTrait { - // Sets the token contract addresses for this pool - #[allow(clippy::too_many_arguments)] - fn initialize( - env: Env, - admin: Address, - staking_contract: Address, - reward_token: Address, - max_complexity: u32, - min_reward: i128, - min_bond: i128, - ); - - fn add_user(env: Env, user: Address, stakes: BondingInfo); - - fn calculate_bond(env: Env, sender: Address, stakes: BondingInfo); - - fn calculate_unbond(env: Env, sender: Address, stakes: BondingInfo, removed_stake: i128); - - fn distribute_rewards(env: Env, total_staked_amount: i128); - - fn withdraw_rewards(env: Env, sender: Address, stakes: BondingInfo); - - fn fund_distribution(env: Env, start_time: u64, distribution_duration: u64, token_amount: i128); - - // QUERIES - - fn query_config(env: Env) -> ConfigResponse; - - fn query_admin(env: Env) -> Address; - - fn query_annualized_reward(env: Env, total_stake_amount: i128) -> AnnualizedRewardResponse; - - fn query_withdrawable_reward( - env: Env, - address: Address, - stakes: BondingInfo, - ) -> WithdrawableRewardResponse; - - fn query_distributed_reward(env: Env, asset: Address) -> u128; - - fn query_undistributed_reward(env: Env, asset: Address) -> u128; - - fn migrate_admin_key(env: Env) -> Result<(), ContractError>; -} - -#[contractimpl] -impl StakingRewardsTrait for StakingRewards { - #[allow(clippy::too_many_arguments)] - fn initialize( - env: Env, - admin: Address, - staking_contract: Address, - reward_token: Address, - max_complexity: u32, - min_reward: i128, - min_bond: i128, - ) { - if is_initialized(&env) { - log!( - &env, - "Stake rewards: Initialize: initializing contract twice is not allowed" - ); - panic_with_error!(&env, ContractError::AlreadyInitialized); - } - - set_initialized(&env); - - env.events().publish( - ("initialize", "StakingRewards rewards distribution contract"), - (), - ); - - let config = Config { - staking_contract, - reward_token: reward_token.clone(), - max_complexity, - min_reward, - min_bond, - }; - save_config(&env, config); - - let distribution = Distribution { - shares_per_point: 1u128, - shares_leftover: 0u64, - distributed_total: 0u128, - withdrawable_total: 0u128, - max_bonus_bps: 0u64, - bonus_per_day_bps: 0u64, - }; - - save_distribution(&env, &reward_token, &distribution); - // Create the default reward distribution curve which is just a flat 0 const - save_reward_curve(&env, reward_token.clone(), &Curve::Constant(0)); - - env.events() - .publish(("create_distribution_flow", "asset"), &reward_token); - - utils::save_admin_old(&env, &admin); - } - - fn add_user(env: Env, user: Address, stakes: BondingInfo) { - let config = get_config(&env); - // only Staking contract which deployed this one can call this method - config.staking_contract.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - 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( - &env, - &user, - &config.reward_token, - &mut distribution, - 0, // old_rewards power is 0 when user didn't register before - new_power, - ); - - env.events().publish(("stake_rewards", "add_user"), &user); - } - - fn calculate_bond(env: Env, sender: Address, stakes: BondingInfo) { - let config = get_config(&env); - // only Staking contract which deployed this one can call this method - config.staking_contract.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - let mut distribution = get_distribution(&env, &config.reward_token); - 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() - let stakes_sum = stakes - .total_stake - .checked_add(last_stake.stake) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: calculate bond: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - let new_power = calc_power(&config, stakes_sum, Decimal::one(), TOKEN_PER_POWER); - update_rewards( - &env, - &sender, - &config.reward_token, - &mut distribution, - old_power, - new_power, - ); - - env.events().publish(("calculate_bond", "user"), &sender); - } - - fn calculate_unbond(env: Env, sender: Address, stakes: BondingInfo, removed_stake: i128) { - sender.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - 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 = - Self::query_withdrawable_reward(env.clone(), sender.clone(), stakes.clone()); - - if found_rewards.reward_amount != 0 { - Self::withdraw_rewards(env.clone(), sender.clone(), stakes.clone()); - } - - let mut distribution = get_distribution(&env, &config.reward_token); - - let old_power = calc_power(&config, stakes.total_stake, Decimal::one(), TOKEN_PER_POWER); // while bonding we use Decimal::one() - let stakes_diff = stakes - .total_stake - .checked_sub(removed_stake) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: Calculate bond: underflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); - let new_power = calc_power(&config, stakes_diff, Decimal::one(), TOKEN_PER_POWER); - update_rewards( - &env, - &sender, - &config.reward_token, - &mut distribution, - old_power, - new_power, - ); - - env.events().publish(("calculate_unbond", "user"), &sender); - } - - 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(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - let calc_power_result = calc_power( - &config, - total_staked_amount, - Decimal::one(), - TOKEN_PER_POWER, - ); - let total_rewards_power = convert_i128_to_u128(calc_power_result); - - if total_rewards_power == 0 { - log!(&env, "Stake rewards: No rewards to distribute!"); - return; - } - let mut distribution = get_distribution(&env, &config.reward_token); - let withdrawable = distribution.withdrawable_total; - - let reward_token_client = token_contract::Client::new(&env, &config.reward_token); - // 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, &config.reward_token).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 - .checked_sub(withdrawable) - .and_then(|diff| diff.checked_sub(curve.value(env.ledger().timestamp()))) - .unwrap_or_else(|| { - log!( - &env, - "Stake Rewards: Distribute Rewards: underflow occured." - ); - panic_with_error!(&env, ContractError::ContractMathError); - }); - - if amount == 0 { - return; - } - - let leftover: u128 = distribution.shares_leftover.into(); - let points = (amount << SHARES_SHIFT) - .checked_add(leftover) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: Distribute Rewards: overflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); - let points_per_share = points.checked_div(total_rewards_power).unwrap_or_else(|| { - log!( - &env, - "Stake Rewards: Distribute Rewards: underflow occured." - ); - panic_with_error!(&env, ContractError::ContractMathError); - }); - 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.shares_per_point = distribution - .shares_per_point - .checked_add(points_per_share) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - distribution.distributed_total = distribution - .distributed_total - .checked_add(amount) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - distribution.withdrawable_total = distribution - .withdrawable_total - .checked_add(amount) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - - save_distribution(&env, &config.reward_token, &distribution); - - env.events().publish( - ("distribute_rewards", "asset"), - &reward_token_client.address, - ); - env.events() - .publish(("distribute_rewards", "amount"), amount); - } - - 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(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - // get distribution data for the given reward - let mut distribution = get_distribution(&env, &config.reward_token); - // get withdraw adjustment for the given distribution - 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, - stakes.total_stake, - &distribution, - &withdraw_adjustment, - &config, - ); - - // calculate the actual reward amounts - each stake is worth 1/60th per each staked day - let reward_multiplier = calc_withdraw_power(&env, &stakes.stakes); - //safe math implemented in decimal - let reward_amount = convert_u128_to_i128(reward_amount) * reward_multiplier; - - if reward_amount == 0 { - return; - } - withdraw_adjustment.withdrawn_rewards = withdraw_adjustment - .withdrawn_rewards - .checked_add(reward_amount as u128) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - - distribution.withdrawable_total = distribution - .withdrawable_total - .checked_sub(reward_amount as u128) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - - save_distribution(&env, &config.reward_token, &distribution); - save_withdraw_adjustment(&env, &sender, &config.reward_token, &withdraw_adjustment); - - let reward_token_client = token_contract::Client::new(&env, &config.reward_token); - reward_token_client.transfer(&env.current_contract_address(), &sender, &reward_amount); - - env.events().publish( - ("withdraw_rewards", "reward_token"), - &reward_token_client.address, - ); - env.events() - .publish(("withdraw_rewards", "reward_amount"), reward_amount); - } - - fn fund_distribution( - env: Env, - start_time: u64, - distribution_duration: u64, - token_amount: i128, - ) { - let admin = get_admin_old(&env); - admin.require_auth(); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - 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 - let previous_reward_curve = get_reward_curve(&env, &config.reward_token).expect("Stake rewards: Fund distribution: Not reward curve exists, probably distribution haven't been created"); - let max_complexity = config.max_complexity; - - let current_time = env.ledger().timestamp(); - if start_time < current_time { - log!( - &env, - "Stake rewards: Fund distribution: Fund distribution start time is too early" - ); - panic_with_error!(&env, ContractError::InvalidTime); - } - - if config.min_reward > token_amount { - log!( - &env, - "Stake rewards: 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, &config.reward_token); - reward_token_client.transfer(&admin, &env.current_contract_address(), &token_amount); - - let end_time = current_time - .checked_add(distribution_duration) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); - // 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), - ); - - // 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 rewards: 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 rewards: Fund distribution: Curve complexity validation failed" - ); - panic_with_error!(&env, ContractError::InvalidMaxComplexity); - }); - } - } - - save_reward_curve(&env, config.reward_token.clone(), &new_reward_curve); - - env.events() - .publish(("fund_reward_distribution", "asset"), &config.reward_token); - 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); - } - - // QUERIES - - fn query_config(env: Env) -> ConfigResponse { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - ConfigResponse { - config: get_config(&env), - } - } - - fn query_admin(env: Env) -> Address { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - get_admin_old(&env) - } - - fn query_annualized_reward(env: Env, total_staked_amount: i128) -> AnnualizedRewardResponse { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let now = env.ledger().timestamp(); - let config = get_config(&env); - - 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(), - amount: String::from_str(&env, "0"), - }; - } - - // get distribution data for the given reward - let distribution = get_distribution(&env, &config.reward_token); - let curve = get_reward_curve(&env, &config.reward_token); - let annualized_payout = calculate_annualized_payout(curve, now); - let apr = annualized_payout - / convert_u128_to_i128( - convert_i128_to_u128(total_stake_power) * distribution.shares_per_point, - ); - - AnnualizedRewardResponse { - asset: config.reward_token.clone(), - amount: apr.to_string(&env), - } - } - - fn query_withdrawable_reward( - env: Env, - user: Address, - stakes: BondingInfo, - ) -> WithdrawableRewardResponse { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let config = get_config(&env); - // iterate over all distributions and calculate withdrawable rewards - // get distribution data for the given reward - let distribution = get_distribution(&env, &config.reward_token); - // get withdraw adjustment for the given distribution - 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, - stakes.total_stake, - &distribution, - &withdraw_adjustment, - &config, - ); - - // calculate the actual reward amounts - each stake is worth 1/60th per each staked day - let reward_multiplier = calc_withdraw_power(&env, &stakes.stakes); - - let reward_amount = - convert_i128_to_u128(convert_u128_to_i128(reward_amount) * reward_multiplier); - - WithdrawableRewardResponse { - reward_address: config.reward_token, - reward_amount, - } - } - - fn query_distributed_reward(env: Env, asset: Address) -> u128 { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - let distribution = get_distribution(&env, &asset); - distribution.distributed_total - } - - fn query_undistributed_reward(env: Env, asset: Address) -> u128 { - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - 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) - .checked_sub(distribution.withdrawable_total) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: underflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }) - } - - fn migrate_admin_key(env: Env) -> Result<(), ContractError> { - let admin = get_admin_old(&env); - env.storage().instance().set(&ADMIN, &admin); - - Ok(()) - } -} - -#[contractimpl] -impl StakingRewards { - #[allow(dead_code)] - pub fn update(env: Env, new_wasm_hash: BytesN<32>) { - let admin = get_admin_old(&env); - admin.require_auth(); - - env.deployer().update_current_contract_wasm(new_wasm_hash); - } -} diff --git a/contracts/stake_rewards/src/distribution.rs b/contracts/stake_rewards/src/distribution.rs deleted file mode 100644 index 1bd55d07e..000000000 --- a/contracts/stake_rewards/src/distribution.rs +++ /dev/null @@ -1,405 +0,0 @@ -use phoenix::utils::convert_i128_to_u128; -use soroban_sdk::{contracttype, log, panic_with_error, Address, Env, Vec}; - -use curve::Curve; -use soroban_decimal::Decimal; - -use crate::{ - error::ContractError, - 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. -/// 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_DAY: u64 = 24 * 60 * 60; -const SECONDS_PER_YEAR: u64 = 365 * SECONDS_PER_DAY; - -#[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 { - env.storage() - .persistent() - .get(&DistributionDataKey::Curve(asset.clone())) -} - -#[contracttype] -#[derive(Debug, Default, Clone)] -pub struct Distribution { - /// How many shares is single point worth - pub shares_per_point: 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 - .checked_sub(old_rewards_power) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: Update Rewards: underflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); - // Apply the points correction with the calculated difference. - let ppw = distribution.shares_per_point; - apply_points_correction(env, user, asset, diff, ppw); -} - -/// Applies points correction for given address. -/// `shares_per_point` is current value from `SHARES_PER_POINT` - 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 = convert_u128_to_i128(shares_per_point) - .checked_mul(diff) - .and_then(|product| shares_correction.checked_sub(product)) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: underflow/overflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); - 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( - // total amount of staked tokens by given user - env: &Env, - total_staked: i128, - distribution: &Distribution, - adjustment: &WithdrawAdjustment, - config: &Config, -) -> u128 { - let ppw = distribution.shares_per_point; - - // 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, total_staked, Decimal::one(), TOKEN_PER_POWER); - let points = convert_u128_to_i128(ppw) - .checked_mul(points) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: overflow"); - panic_with_error!(&env, ContractError::ContractMathError); - }); - - let correction = adjustment.shares_correction; - points - .checked_add(correction) - .and_then(|sum| { - let shifted = sum >> SHARES_SHIFT; - u128::try_from(shifted).ok() - }) - .and_then(|converted| converted.checked_sub(adjustment.withdrawn_rewards)) - .unwrap_or_else(|| { - log!(&env, "Stake Rewards: underflow/overflow occured"); - panic_with_error!(&env, ContractError::ContractMathError); - }) -} - -pub fn calculate_annualized_payout(reward_curve: Option, 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(), - } -} - -pub fn calc_power( - config: &Config, - stakes: i128, - multiplier: Decimal, - token_per_power: i32, -) -> i128 { - if stakes < config.min_bond { - 0 - } else { - stakes * multiplier / token_per_power as i128 - } -} - -// For all user's stakes: -// - if a stake is active <60 days, apply a multiplier 1/60th for each day it is (up to 1.0) -// - if a stake is older, just sum it up -// - weighted average will be used as a final reward's multiplier -pub fn calc_withdraw_power(env: &Env, stakes: &Vec) -> Decimal { - let current_date = env.ledger().timestamp(); - let mut weighted_sum: u128 = 0; - let mut total_weight: u128 = 0; - - for stake in stakes.iter() { - // Calculate the number of days the stake has been active - - let days_active = current_date - .checked_sub(stake.stake_timestamp) - .and_then(|diff| diff.checked_div(SECONDS_PER_DAY)) - .unwrap_or_else(|| { - log!( - &env, - "Stake Rewards: Calc Withdraw Power: underflow/overflow occured." - ); - panic_with_error!(&env, ContractError::ContractMathError); - }); - - // If stake is younger than 60 days, calculate its power - let power = if days_active < 60 { - days_active as u128 - } else { - 60 - }; - - // Add the weighted power to the sum - weighted_sum = power - .checked_mul(convert_i128_to_u128(stake.stake)) - .and_then(|product| weighted_sum.checked_add(product)) - .unwrap_or_else(|| { - log!( - &env, - "Stake Rewards: Calc Withdraw Power: underflow/overflow occured." - ); - panic_with_error!(&env, ContractError::ContractMathError); - }); - // Accumulate the total weight - total_weight = 60u128 - .checked_mul(convert_i128_to_u128(stake.stake)) - .and_then(|product| total_weight.checked_add(product)) - .unwrap_or_else(|| { - log!( - &env, - "Stake Rewards: Calc Withdraw Power: underflow/overflow occured." - ); - panic_with_error!(&env, ContractError::ContractMathError); - }) - } - - // Calculate and return the average staking power - if total_weight > 0 { - Decimal::from_ratio( - convert_u128_to_i128(weighted_sum), - convert_u128_to_i128(total_weight), - ) - } else { - Decimal::zero() - } -} - -#[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::; - let result = calculate_annualized_payout(reward_curve, 121); - assert_eq!(result, Decimal::zero()); - } -} diff --git a/contracts/stake_rewards/src/error.rs b/contracts/stake_rewards/src/error.rs deleted file mode 100644 index 22fbfa760..000000000 --- a/contracts/stake_rewards/src/error.rs +++ /dev/null @@ -1,21 +0,0 @@ -use soroban_sdk::contracterror; - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum ContractError { - AlreadyInitialized = 1, - InvalidMinBond = 2, - InvalidMinReward = 3, - InvalidBond = 4, - Unauthorized = 5, - MinRewardNotEnough = 6, - RewardsInvalid = 7, - StakeNotFound = 8, - InvalidTime = 9, - DistributionExists = 10, - InvalidRewardAmount = 11, - InvalidMaxComplexity = 12, - AdminNotSet = 13, - ContractMathError = 14, -} diff --git a/contracts/stake_rewards/src/lib.rs b/contracts/stake_rewards/src/lib.rs deleted file mode 100644 index 20b4c46bc..000000000 --- a/contracts/stake_rewards/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -// #![no_std] -mod contract; -mod distribution; -mod error; -mod msg; -mod storage; - -pub const TOKEN_PER_POWER: i32 = 1_000; - -#[allow(clippy::too_many_arguments)] -pub mod token_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/soroban_token_contract.wasm" - ); -} - -#[cfg(test)] -mod tests; diff --git a/contracts/stake_rewards/src/msg.rs b/contracts/stake_rewards/src/msg.rs deleted file mode 100644 index ff05a204b..000000000 --- a/contracts/stake_rewards/src/msg.rs +++ /dev/null @@ -1,23 +0,0 @@ -use soroban_sdk::{contracttype, Address, String}; - -use crate::storage::Config; - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ConfigResponse { - pub config: Config, -} - -#[contracttype] -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct AnnualizedRewardResponse { - pub asset: Address, - pub amount: String, -} - -#[contracttype] -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct WithdrawableRewardResponse { - pub reward_address: Address, - pub reward_amount: u128, -} diff --git a/contracts/stake_rewards/src/storage.rs b/contracts/stake_rewards/src/storage.rs deleted file mode 100644 index 527bd10f2..000000000 --- a/contracts/stake_rewards/src/storage.rs +++ /dev/null @@ -1,148 +0,0 @@ -use phoenix::ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; -use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec}; - -pub const ADMIN: Symbol = symbol_short!("ADMIN"); - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Config { - // Address of the staking contract that this reward distribution contract is - // connected to. It can not be changed - pub staking_contract: Address, - // Token that is being distributed through this contract - pub reward_token: Address, - // Maximum complexity of the reward distribution curve; the bigger, the more resources it uses - pub max_complexity: u32, - // Minimum reward amount to be distributed - pub min_reward: i128, - // Security precaution - if bond is too small, don't count it towards the bonding power - pub min_bond: i128, -} -const CONFIG: Symbol = symbol_short!("CONFIG"); -pub fn get_config(env: &Env) -> Config { - let config = env - .storage() - .persistent() - .get(&CONFIG) - .expect("Stake: Config not set"); - - env.storage().persistent().extend_ttl( - &CONFIG, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - - config -} - -pub fn save_config(env: &Env, config: Config) { - env.storage().persistent().set(&CONFIG, &config); - env.storage().persistent().extend_ttl( - &CONFIG, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); -} - -pub mod utils { - use crate::error::ContractError; - - use super::*; - - use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; - use soroban_sdk::{log, panic_with_error, ConversionError, TryFromVal, Val}; - - #[derive(Clone, Copy)] - #[repr(u32)] - pub enum DataKey { - Initialized = 0, - Admin = 1, - } - - impl TryFromVal for Val { - type Error = ConversionError; - - fn try_from_val(_env: &Env, v: &DataKey) -> Result { - Ok((*v as u32).into()) - } - } - - pub fn is_initialized(e: &Env) -> bool { - e.storage() - .persistent() - .get(&DataKey::Initialized) - .unwrap_or(false) - } - - pub fn set_initialized(e: &Env) { - e.storage().persistent().set(&DataKey::Initialized, &true); - e.storage().persistent().extend_ttl( - &DataKey::Initialized, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - } - - pub fn save_admin_old(e: &Env, address: &Address) { - e.storage().persistent().set(&DataKey::Admin, address); - e.storage().persistent().extend_ttl( - &DataKey::Admin, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - } - - pub fn _save_admin(e: &Env, address: &Address) { - e.storage().instance().set(&ADMIN, &address); - e.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - } - - pub fn get_admin_old(e: &Env) -> Address { - let admin = e.storage().persistent().get(&DataKey::Admin).unwrap(); - e.storage().persistent().extend_ttl( - &DataKey::Admin, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, - ); - - admin - } - - pub fn _get_admin(e: &Env) -> Address { - e.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - - e.storage().instance().get(&ADMIN).unwrap_or_else(|| { - log!(e, "Stake Rewards: Admin not set"); - panic_with_error!(&e, ContractError::AdminNotSet) - }) - } -} - -#[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, - /// 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, -} diff --git a/contracts/stake_rewards/src/tests.rs b/contracts/stake_rewards/src/tests.rs deleted file mode 100644 index 696b5ccbd..000000000 --- a/contracts/stake_rewards/src/tests.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod bond; -// mod distribution; -mod setup; diff --git a/contracts/stake_rewards/src/tests/bond.rs b/contracts/stake_rewards/src/tests/bond.rs deleted file mode 100644 index dd535020d..000000000 --- a/contracts/stake_rewards/src/tests/bond.rs +++ /dev/null @@ -1,107 +0,0 @@ -use soroban_sdk::{ - 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() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let reward_token = deploy_token_contract(&env, &admin); - let staking = Address::generate(&env); - - let staking_rewards = - deploy_staking_rewards_contract(&env, &admin, &reward_token.address, &staking); - - assert_eq!(staking_rewards.query_admin(), admin); - assert_eq!( - staking_rewards.query_config().config.staking_contract, - staking - ); -} - -#[test] -#[should_panic(expected = "Error(Auth, InvalidAction)")] -fn calculate_bond_called_by_anyone() { - let env = Env::default(); - env.cost_estimate().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_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); - - // 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] -#[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.cost_estimate().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_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); - - 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 = (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/distribution.rs b/contracts/stake_rewards/src/tests/distribution.rs deleted file mode 100644 index bf0c76f73..000000000 --- a/contracts/stake_rewards/src/tests/distribution.rs +++ /dev/null @@ -1,542 +0,0 @@ -use soroban_sdk::{ - testutils::{Address as _, Ledger}, - vec, Address, Env, String, -}; - -use super::setup::{deploy_staking_rewards_contract, deploy_token_contract}; - -use crate::msg::{AnnualizedRewardResponse, WithdrawableRewardResponse}; - -#[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 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); - - // we simulate the full APR after 60 days of staking - let sixty_days = 3600 * 24 * 60; - - // first user bonds before distribution started - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - staking_rewards.calculate_bond(&user1); - - reward_token.mint(&admin, &1_000_000); - // therefore the distribution must take at least 60 days in this test case - let reward_duration = sixty_days * 3; - // distribution starts at time 0 - staking_rewards.fund_distribution(&0, &reward_duration, &1_000_000); - - env.ledger().with_mut(|li| { - li.timestamp = sixty_days; // distribution already goes for 1/3 of the time - }); - - staking_rewards.distribute_rewards(); - - // at this points, since 1/3 of the time has passed and only one user is staking, he should have 33% of the rewards - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 333_332 - } - ); - - // second user bonds and we are waiting another 60 days for the full APR - 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 = sixty_days * 2; // distribution already goes for 2/3 of the time - }); - - staking_rewards.distribute_rewards(); - - // Now we need to split the previous reward equivalent into a two users - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 333_332 + 166_667, - } - ); - assert_eq!( - staking_rewards.query_withdrawable_reward(&user2), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 166_666 - } - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 499_999); - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 166_666); - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 333_334 - ); -} - -#[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 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); - - // we simulate the full APR after 60 days of staking - let sixty_days = 3600 * 24 * 60; - - reward_token.mint(&admin, &1_000_000); - // therefore the distribution must take at least 60 days in this test case - let reward_duration = sixty_days * 3; - // distribution starts at time 0 - staking_rewards.fund_distribution(&0, &reward_duration, &1_000_000); - - // first user bonds after distribution started - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - staking_rewards.calculate_bond(&user1); - - env.ledger().with_mut(|li| { - li.timestamp = sixty_days; // distribution already goes for 1/3 of the time - }); - - staking_rewards.distribute_rewards(); - - // at this points, since 1/3 of the time has passed and only one user is staking, he should have 33% of the rewards - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 333_332 - } - ); - - // second user bonds and we are waiting another 60 days for the full APR - 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 = sixty_days * 2; // distribution already goes for 2/3 of the time - }); - - staking_rewards.distribute_rewards(); - - // Now we need to split the previous reward equivalent into a two users - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 333_332 + 166_667, - } - ); - assert_eq!( - staking_rewards.query_withdrawable_reward(&user2), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 166_666 - } - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 499_999); - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 166_666); - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 333_334 - ); -} - -#[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 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 start_timestamp = 100; - 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 = 2_600; - }); - staking_rewards.distribute_rewards(); - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 1_000_000 - ); - assert_eq!( - staking_rewards.query_distributed_reward(&reward_token.address), - 0 - ); - - let user = Address::generate(&env); - assert_eq!( - staking_rewards.query_withdrawable_reward(&reward_token.address), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 0 - } - ); - - staking_rewards.withdraw_rewards(&user); - assert_eq!(reward_token.balance(&user), 0); -} - -#[test] -#[should_panic( - expected = "Stake rewards: 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 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 start_timestamp = 100; - env.ledger().with_mut(|li| { - li.timestamp = 150; - }); - - reward_token.mint(&admin, &1_000_000); - let reward_duration = 600; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); -} - -#[test] -#[should_panic(expected = "Stake rewards: 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 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 start_timestamp = 100; - reward_token.mint(&admin, &100); - let reward_duration = 600; - // Min reward is defined in setup as 1_000 tokens - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &999); -} - -#[test] -fn calculate_apr() { - let day_in_seconds = 3600 * 24; - - 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 start_timestamp = day_in_seconds; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - reward_token.mint(&admin, &1_000_000); - // whole year of distribution - let reward_duration = 60 * 60 * 24 * 365; - staking_rewards.fund_distribution(&start_timestamp, &reward_duration, &1_000_000); - - // nothing bonded, no rewards - assert_eq!( - staking_rewards.query_annualized_reward(), - AnnualizedRewardResponse { - asset: reward_token.address.clone(), - amount: String::from_str(&env, "0") - } - ); - - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - - env.ledger().with_mut(|li| { - li.timestamp += day_in_seconds; - }); - - // 100k rewards distributed for the whole year gives 100% APR - assert_eq!( - staking_rewards.query_annualized_reward(), - AnnualizedRewardResponse { - asset: reward_token.address.clone(), - amount: String::from_str(&env, "100000.072802197802197802") - } - ); - - let reward_amount: i128 = 500_000; - reward_token.mint(&admin, &reward_amount); - - staking_rewards.fund_distribution(&(2 * start_timestamp), &reward_duration, &reward_amount); - - // having another 50k in rewards increases APR - assert_eq!( - staking_rewards.query_annualized_reward(), - AnnualizedRewardResponse { - asset: reward_token.address.clone(), - amount: String::from_str(&env, "149726.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 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); - - // bond tokens for user to enable distribution for him - let user1 = Address::generate(&env); - lp_token.mint(&user1, &1_000); - staking.bond(&user1, &1_000); - staking_rewards.calculate_bond(&user1); - - let user2 = Address::generate(&env); - lp_token.mint(&user2, &1_000); - staking.bond(&user2, &1_000); - staking_rewards.calculate_bond(&user2); - - // we simulate full stake time - let start_timestamp = 60 * 3600 * 24; - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp; - }); - - let reward_duration = 10_000; - let reward_amount = 100_000; - reward_token.mint(&admin, &reward_amount); - staking_rewards.fund_distribution( - &start_timestamp, // start distirbution - &reward_duration, - &reward_amount, - ); - - env.ledger().with_mut(|li| { - li.timestamp += 2_000; - }); - - staking_rewards.distribute_rewards(); - assert_eq!( - staking_rewards.query_undistributed_reward(&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_rewards.distribute_rewards(); - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 50_000 - ); - - // user1 unbonds, which automatically withdraws the rewards - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 25_000 - } - ); - staking_rewards.calculate_unbond(&user1); - staking.unbond(&user1, &1_000, &0); // when he bonded - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 0 - } - ); - - env.ledger().with_mut(|li| { - li.timestamp += 10_000; - }); - - 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), - reward_amount as u128 - ); - - assert_eq!( - staking_rewards.query_withdrawable_reward(&user2), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 75_000 - } - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 25_000i128); -} - -#[should_panic(expected = "Stake rewards: Fund distribution: Curve complexity validation failed")] -#[test] -fn panic_when_funding_distribution_with_curve_too_complex() { - let env = Env::default(); - 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, staking_rewards) = - deploy_staking_rewards_contract(&env, &admin, &lp_token.address, &reward_token.address); - - reward_token.mint(&admin, &10_000); - - // Default max complexity in setup.rs is 10 - staking_rewards.fund_distribution(&17, &300, &1000); - staking_rewards.fund_distribution(&15, &280, &1000); - staking_rewards.fund_distribution(&30, &154, &1000); - staking_rewards.fund_distribution(&532, &754, &1000); - staking_rewards.fund_distribution(&210, &423154, &1000); - staking_rewards.fund_distribution(&640, &53254, &1000); -} - -#[test] -fn add_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); - - // first user bonds before distribution started - let user1 = Address::generate(&env); - lp_token.mint(&user1, &10_000); - staking.bond(&user1, &10_000); - // do not calculate bond first - staking_rewards.calculate_bond(&user1); - - // second user bonds after distribution started - let user2 = Address::generate(&env); - lp_token.mint(&user2, &10_000); - staking.bond(&user2, &10_000); - - // 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 + 200; // distribution already goes for 1/3 of the time - }); - - staking_rewards.add_multiple_users(&vec![&env, user1.clone(), user2.clone()]); - - staking_rewards.distribute_rewards(); - - // at this points, since 1/3 of the time has passed and two users are staking, he should have 33% /2 of the rewards - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 166_666 - } - ); - - env.ledger().with_mut(|li| { - li.timestamp = start_timestamp + 400; // distribution already goes for 2/3 of the time - }); - - staking_rewards.distribute_rewards(); - - // Now we need to split the previous reward equivalent into a two users - assert_eq!( - staking_rewards.query_withdrawable_reward(&user1), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 166_666 + 166_666, - } - ); - assert_eq!( - staking_rewards.query_withdrawable_reward(&user2), - WithdrawableRewardResponse { - reward_address: reward_token.address.clone(), - reward_amount: 333_332 - } - ); - - staking_rewards.withdraw_rewards(&user1); - assert_eq!(reward_token.balance(&user1), 333_332); - staking_rewards.withdraw_rewards(&user2); - assert_eq!(reward_token.balance(&user2), 333_332); - assert_eq!( - staking_rewards.query_undistributed_reward(&reward_token.address), - 333_334 - ); -} diff --git a/contracts/stake_rewards/src/tests/setup.rs b/contracts/stake_rewards/src/tests/setup.rs deleted file mode 100644 index 341a5409e..000000000 --- a/contracts/stake_rewards/src/tests/setup.rs +++ /dev/null @@ -1,37 +0,0 @@ -use soroban_sdk::{Address, Env}; - -use crate::{ - contract::{StakingRewards, StakingRewardsClient}, - 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_v2(admin.clone()) - .address(), - ) -} - -const MIN_BOND: i128 = 1000; -const MIN_REWARD: i128 = 1000; -const MAX_COMPLEXITY: u32 = 10; - -pub fn deploy_staking_rewards_contract<'a>( - env: &Env, - admin: &Address, - reward_token: &Address, - staking_contract: &Address, -) -> StakingRewardsClient<'a> { - let staking_rewards = StakingRewardsClient::new(env, &env.register(StakingRewards, ())); - - staking_rewards.initialize( - admin, - staking_contract, - reward_token, - &MAX_COMPLEXITY, - &MIN_REWARD, - &MIN_BOND, - ); - staking_rewards -} From dc545a6890ea2a8fdbc53ca09c34e324449efa03 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:36:26 +0200 Subject: [PATCH 03/34] lints --- contracts/stake/src/tests/bond.rs | 6 +++--- contracts/stake/src/tests/setup.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/stake/src/tests/bond.rs b/contracts/stake/src/tests/bond.rs index f3571a5df..63fb0cbef 100644 --- a/contracts/stake/src/tests/bond.rs +++ b/contracts/stake/src/tests/bond.rs @@ -346,7 +346,7 @@ fn initialize_staking_contract_should_panic_when_min_bond_invalid() { let env = Env::default(); env.mock_all_auths(); - let staking = StakingClient::new(&env, &env.register_contract(None, Staking {})); + let staking = StakingClient::new(&env, &env.register(Staking, ())); staking.initialize( &Address::generate(&env), @@ -365,7 +365,7 @@ fn initialize_staking_contract_should_panic_when_min_rewards_invalid() { let env = Env::default(); env.mock_all_auths(); - let staking = StakingClient::new(&env, &env.register_contract(None, Staking {})); + let staking = StakingClient::new(&env, &env.register(Staking, ())); staking.initialize( &Address::generate(&env), @@ -384,7 +384,7 @@ fn initialize_staking_contract_should_panic_when_max_complexity_invalid() { let env = Env::default(); env.mock_all_auths(); - let staking = StakingClient::new(&env, &env.register_contract(None, Staking {})); + let staking = StakingClient::new(&env, &env.register(Staking, ())); staking.initialize( &Address::generate(&env), diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 5cbfc4e7e..580048ef5 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -6,7 +6,11 @@ use crate::{ }; 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())) + token_contract::Client::new( + env, + &env.register_stellar_asset_contract_v2(admin.clone()) + .address(), + ) } const MIN_BOND: i128 = 1000; @@ -21,7 +25,7 @@ pub fn deploy_staking_contract<'a>( max_complexity: &u32, ) -> StakingClient<'a> { let admin = admin.into().unwrap_or(Address::generate(env)); - let staking = StakingClient::new(env, &env.register_contract(None, Staking {})); + let staking = StakingClient::new(env, &env.register(Staking, ())); staking.initialize( &admin, From 037b49f3be3cd8bc781cc6af5741b65f20d4d6d9 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:21:36 +0200 Subject: [PATCH 04/34] brings back the TTL extension in storage of Stake --- contracts/stake/src/storage.rs | 132 +++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index 624cd8467..e7de26d30 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -1,3 +1,4 @@ +use phoenix::ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec}; #[contracttype] @@ -14,20 +15,34 @@ pub struct Config { pub max_complexity: u32, } const CONFIG: Symbol = symbol_short!("CONFIG"); +pub const ADMIN: Symbol = symbol_short!("ADMIN"); pub fn get_config(env: &Env) -> Config { - env.storage() + let config = env + .storage() .persistent() .get(&CONFIG) - .expect("Stake: Config not set") + .expect("Stake: Config not set"); + env.storage().persistent().extend_ttl( + &CONFIG, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + + config } pub fn save_config(env: &Env, config: Config) { env.storage().persistent().set(&CONFIG, &config); + env.storage().persistent().extend_ttl( + &CONFIG, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } #[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct Stake { /// The amount of staked tokens pub stake: i128, @@ -52,7 +67,7 @@ pub struct BondingInfo { } pub fn get_stakes(env: &Env, key: &Address) -> BondingInfo { - match env.storage().persistent().get::<_, BondingInfo>(key) { + let bonding_info = match env.storage().persistent().get::<_, BondingInfo>(key) { Some(stake) => stake, None => BondingInfo { stakes: Vec::new(env), @@ -60,11 +75,25 @@ pub fn get_stakes(env: &Env, key: &Address) -> BondingInfo { last_reward_time: 0u64, total_stake: 0i128, }, - } + }; + env.storage().persistent().has(&key).then(|| { + env.storage().persistent().extend_ttl( + &key, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + }); + + bonding_info } pub fn save_stakes(env: &Env, key: &Address, bonding_info: &BondingInfo) { env.storage().persistent().set(key, bonding_info); + env.storage().persistent().extend_ttl( + &key, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } pub mod utils { @@ -72,6 +101,7 @@ pub mod utils { use super::*; + use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; use soroban_sdk::{log, panic_with_error, ConversionError, TryFromVal, Val}; #[derive(Clone, Copy)] @@ -81,6 +111,7 @@ pub mod utils { TotalStaked = 1, Distributions = 2, Initialized = 3, + StakeRewards = 4, // maybe deprecated } impl TryFromVal for Val { @@ -93,25 +124,63 @@ pub mod utils { pub fn is_initialized(e: &Env) -> bool { e.storage() - .persistent() + .instance() .get(&DataKey::Initialized) .unwrap_or(false) } pub fn set_initialized(e: &Env) { - e.storage().persistent().set(&DataKey::Initialized, &true); + e.storage().instance().set(&DataKey::Initialized, &true); + e.storage() + .instance() + .extend_ttl(PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); } - pub fn save_admin(e: &Env, address: &Address) { - e.storage().persistent().set(&DataKey::Admin, address) + pub fn save_admin_old(e: &Env, address: &Address) { + e.storage().persistent().set(&DataKey::Admin, address); + e.storage().persistent().extend_ttl( + &DataKey::Admin, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } - pub fn get_admin(e: &Env) -> Address { - e.storage().persistent().get(&DataKey::Admin).unwrap() + pub fn _save_admin(e: &Env, address: &Address) { + e.storage().instance().set(&ADMIN, &address); + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + } + + pub fn get_admin_old(e: &Env) -> Address { + let admin = e.storage().persistent().get(&DataKey::Admin).unwrap(); + e.storage().persistent().extend_ttl( + &DataKey::Admin, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + + admin + } + + pub fn _get_admin(e: &Env) -> Address { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + e.storage().instance().get(&ADMIN).unwrap_or_else(|| { + log!(e, "Stake: Admin not set"); + panic_with_error!(&e, ContractError::AdminNotSet) + }) } pub fn init_total_staked(e: &Env) { e.storage().persistent().set(&DataKey::TotalStaked, &0i128); + e.storage().persistent().extend_ttl( + &DataKey::TotalStaked, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } pub fn increase_total_staked(e: &Env, amount: &i128) { @@ -119,6 +188,12 @@ pub mod utils { e.storage() .persistent() .set(&DataKey::TotalStaked, &(count + amount)); + + e.storage().persistent().extend_ttl( + &DataKey::TotalStaked, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } pub fn decrease_total_staked(e: &Env, amount: &i128) { @@ -126,13 +201,27 @@ pub mod utils { e.storage() .persistent() .set(&DataKey::TotalStaked, &(count - amount)); + + e.storage().persistent().extend_ttl( + &DataKey::TotalStaked, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } pub fn get_total_staked_counter(env: &Env) -> i128 { - env.storage() + let total_staked = env + .storage() .persistent() .get(&DataKey::TotalStaked) - .unwrap() + .unwrap(); + env.storage().persistent().extend_ttl( + &DataKey::TotalStaked, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + + total_staked } // Keep track of all distributions to be able to iterate over them @@ -149,9 +238,22 @@ pub mod utils { } pub fn get_distributions(e: &Env) -> Vec
{ - e.storage() + let distributions = e + .storage() .persistent() .get(&DataKey::Distributions) - .unwrap_or_else(|| soroban_sdk::vec![e]) + .unwrap_or_else(|| soroban_sdk::vec![e]); + e.storage() + .persistent() + .has(&DataKey::Distributions) + .then(|| { + e.storage().persistent().extend_ttl( + &DataKey::Distributions, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ) + }); + + distributions } } From 81b3934c1b7e8db5296e2aac1d23257c1eb1ced1 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:14:19 +0200 Subject: [PATCH 05/34] keys for admin --- contracts/stake/Cargo.toml | 2 +- contracts/stake/src/contract.rs | 8 ++++---- contracts/stake/src/storage.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/stake/Cargo.toml b/contracts/stake/Cargo.toml index 7872e1b55..dd7bdb66e 100644 --- a/contracts/stake/Cargo.toml +++ b/contracts/stake/Cargo.toml @@ -18,6 +18,6 @@ curve = { workspace = true } phoenix = { workspace = true } soroban-sdk = { workspace = true } -[dev_dependencies] +[dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } pretty_assertions = { workspace = true } diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 093d161a9..3c57b0643 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -20,7 +20,7 @@ use crate::{ storage::{ get_config, get_stakes, save_config, save_stakes, utils::{ - self, add_distribution, get_admin, get_distributions, get_total_staked_counter, + self, add_distribution, get_admin_old, get_distributions, get_total_staked_counter, is_initialized, set_initialized, }, Config, Stake, @@ -146,7 +146,7 @@ impl StakingTrait for Staking { }; save_config(&env, config); - utils::save_admin(&env, &admin); + utils::save_admin_old(&env, &admin); utils::init_total_staked(&env); } @@ -471,7 +471,7 @@ impl StakingTrait for Staking { } fn query_admin(env: Env) -> Address { - get_admin(&env) + get_admin_old(&env) } fn query_staked(env: Env, address: Address) -> StakedResponse { @@ -556,7 +556,7 @@ impl StakingTrait for Staking { impl Staking { #[allow(dead_code)] pub fn update(env: Env, new_wasm_hash: BytesN<32>) { - let admin = get_admin(&env); + let admin = get_admin_old(&env); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index e7de26d30..6afb85582 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -15,7 +15,7 @@ pub struct Config { pub max_complexity: u32, } const CONFIG: Symbol = symbol_short!("CONFIG"); -pub const ADMIN: Symbol = symbol_short!("ADMIN"); +pub const _ADMIN: Symbol = symbol_short!("ADMIN"); pub fn get_config(env: &Env) -> Config { let config = env From fe100537521cdea79b5ebca3cf4f9b19de1045f4 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:28:55 +0200 Subject: [PATCH 06/34] instance bump for every msg --- contracts/stake/src/contract.rs | 45 +++++++++++++++++++++++++++++++++ contracts/stake/src/storage.rs | 3 ++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 3c57b0643..20aff8393 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -1,3 +1,4 @@ +use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; use soroban_decimal::Decimal; use soroban_sdk::{ contract, contractimpl, contractmeta, log, panic_with_error, vec, Address, BytesN, Env, String, @@ -152,6 +153,9 @@ impl StakingTrait for Staking { fn bond(env: Env, sender: Address, tokens: i128) { sender.require_auth(); + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let ledger = env.ledger(); let config = get_config(&env); @@ -202,6 +206,9 @@ impl StakingTrait for Staking { fn unbond(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64) { sender.require_auth(); + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let config = get_config(&env); @@ -250,6 +257,9 @@ impl StakingTrait for Staking { fn create_distribution_flow(env: Env, sender: Address, asset: Address) { sender.require_auth(); + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let manager = get_config(&env).manager; let owner = get_config(&env).owner; @@ -281,6 +291,10 @@ impl StakingTrait for Staking { } fn distribute_rewards(env: Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + let total_staked_amount = get_total_staked_counter(&env); let total_rewards_power = calc_power( &get_config(&env), @@ -339,6 +353,10 @@ impl StakingTrait for Staking { } fn withdraw_rewards(env: Env, sender: Address) { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + env.events().publish(("withdraw_rewards", "user"), &sender); let config = get_config(&env); @@ -387,6 +405,9 @@ impl StakingTrait for Staking { token_amount: i128, ) { sender.require_auth(); + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); // Load previous reward curve; it must exist if the distribution exists // In case of first time funding, it will be a constant 0 curve @@ -465,26 +486,41 @@ impl StakingTrait for Staking { // QUERIES fn query_config(env: Env) -> ConfigResponse { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); ConfigResponse { config: get_config(&env), } } fn query_admin(env: Env) -> Address { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); get_admin_old(&env) } fn query_staked(env: Env, address: Address) -> StakedResponse { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); StakedResponse { stakes: get_stakes(&env, &address).stakes, } } fn query_total_staked(env: Env) -> i128 { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); get_total_staked_counter(&env) } fn query_annualized_rewards(env: Env) -> AnnualizedRewardsResponse { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let now = env.ledger().timestamp(); let mut aprs = vec![&env]; let config = get_config(&env); @@ -518,6 +554,9 @@ impl StakingTrait for Staking { } fn query_withdrawable_rewards(env: Env, user: Address) -> WithdrawableRewardsResponse { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let config = get_config(&env); // iterate over all distributions and calculate withdrawable rewards let mut rewards = vec![&env]; @@ -540,11 +579,17 @@ impl StakingTrait for Staking { } fn query_distributed_rewards(env: Env, asset: Address) -> u128 { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let distribution = get_distribution(&env, &asset); distribution.distributed_total } fn query_undistributed_rewards(env: Env, asset: Address) -> u128 { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let distribution = get_distribution(&env, &asset); let reward_token_client = token_contract::Client::new(&env, &asset); reward_token_client.balance(&env.current_contract_address()) as u128 diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index 6afb85582..6f093991c 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -15,7 +15,8 @@ pub struct Config { pub max_complexity: u32, } const CONFIG: Symbol = symbol_short!("CONFIG"); -pub const _ADMIN: Symbol = symbol_short!("ADMIN"); +#[allow(dead_code)] +pub const ADMIN: Symbol = symbol_short!("ADMIN"); pub fn get_config(env: &Env) -> Config { let config = env From a2f8c239c27ac388c79eb1e0caa75f7ed91bf155 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:42:55 +0200 Subject: [PATCH 07/34] extends the ttl in distribution --- contracts/stake/src/distribution.rs | 87 ++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/contracts/stake/src/distribution.rs b/contracts/stake/src/distribution.rs index a02731aa5..a11f88f0a 100644 --- a/contracts/stake/src/distribution.rs +++ b/contracts/stake/src/distribution.rs @@ -1,3 +1,4 @@ +use phoenix::ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; use soroban_sdk::{contracttype, Address, Env}; use curve::Curve; @@ -37,15 +38,35 @@ pub enum DistributionDataKey { // 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); + env.storage().persistent().set( + &DistributionDataKey::Curve(asset.clone()), + distribution_curve, + ); + env.storage().persistent().extend_ttl( + &DistributionDataKey::Curve(asset), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } pub fn get_reward_curve(env: &Env, asset: &Address) -> Option { + let result = env + .storage() + .persistent() + .get(&DistributionDataKey::Curve(asset.clone())); + env.storage() .persistent() - .get(&DistributionDataKey::Curve(asset.clone())) + .has(&DistributionDataKey::Curve(asset.clone())) + .then(|| { + env.storage().persistent().extend_ttl( + &DistributionDataKey::Curve(asset.clone()), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ) + }); + + result } #[contracttype] @@ -70,13 +91,33 @@ pub fn save_distribution(env: &Env, asset: &Address, distribution: &Distribution &DistributionDataKey::Distribution(asset.clone()), distribution, ); + + env.storage().persistent().extend_ttl( + &DistributionDataKey::Distribution(asset.clone()), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ) } pub fn get_distribution(env: &Env, asset: &Address) -> Distribution { - env.storage() + let distribution = env + .storage() .persistent() .get(&DistributionDataKey::Distribution(asset.clone())) - .unwrap() + .unwrap(); + + env.storage() + .persistent() + .has(&DistributionDataKey::Distribution(asset.clone())) + .then(|| { + env.storage().persistent().extend_ttl( + &DistributionDataKey::Distribution(asset.clone()), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ) + }); + + distribution } pub fn update_rewards( @@ -141,6 +182,14 @@ pub fn save_withdraw_adjustment( }), adjustment, ); + env.storage().persistent().extend_ttl( + &DistributionDataKey::WithdrawAdjustment(WithdrawAdjustmentKey { + user: user.clone(), + asset: distribution.clone(), + }), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } pub fn get_withdraw_adjustment( @@ -148,7 +197,8 @@ pub fn get_withdraw_adjustment( user: &Address, distribution: &Address, ) -> WithdrawAdjustment { - env.storage() + let result = env + .storage() .persistent() .get(&DistributionDataKey::WithdrawAdjustment( WithdrawAdjustmentKey { @@ -156,7 +206,28 @@ pub fn get_withdraw_adjustment( asset: distribution.clone(), }, )) - .unwrap_or_default() + .unwrap_or_default(); + + env.storage() + .persistent() + .has(&DistributionDataKey::WithdrawAdjustment( + WithdrawAdjustmentKey { + user: user.clone(), + asset: distribution.clone(), + }, + )) + .then(|| { + env.storage().persistent().extend_ttl( + &DistributionDataKey::WithdrawAdjustment(WithdrawAdjustmentKey { + user: user.clone(), + asset: distribution.clone(), + }), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + }); + + result } pub fn withdrawable_rewards( From 915a048c5383ea32ced8d0e72d63df4a83bd1b79 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:45:39 +0200 Subject: [PATCH 08/34] assertion in test --- contracts/stake/src/tests/bond.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/contracts/stake/src/tests/bond.rs b/contracts/stake/src/tests/bond.rs index 63fb0cbef..3851d822b 100644 --- a/contracts/stake/src/tests/bond.rs +++ b/contracts/stake/src/tests/bond.rs @@ -1,7 +1,10 @@ +extern crate std; + use pretty_assertions::assert_eq; use soroban_sdk::{ - testutils::{Address as _, Ledger}, - vec, Address, Env, + symbol_short, + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Ledger}, + vec, Address, Env, IntoVal, Symbol, }; use super::setup::{deploy_staking_contract, deploy_token_contract}; @@ -133,6 +136,28 @@ fn bond_simple() { staking.bond(&user, &10_000); + assert_eq!( + env.auths(), + [( + user.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + staking.address.clone(), + Symbol::new(&env, "bond"), + (&user.clone(), 10_000i128,).into_val(&env), + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + lp_token.address.clone(), + symbol_short!("transfer"), + (&user, &staking.address.clone(), 10_000i128).into_val(&env) + )), + sub_invocations: std::vec![], + },], + } + ),] + ); + let bonds = staking.query_staked(&user).stakes; assert_eq!( bonds, From 64a30826987d68ad9992b64681b976fd9cc3beff Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:53:17 +0200 Subject: [PATCH 09/34] assertions about auth in bond --- contracts/stake/src/tests/bond.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/contracts/stake/src/tests/bond.rs b/contracts/stake/src/tests/bond.rs index 3851d822b..471c03d17 100644 --- a/contracts/stake/src/tests/bond.rs +++ b/contracts/stake/src/tests/bond.rs @@ -220,6 +220,21 @@ fn unbond_simple() { let stake_timestamp = 4000; staking.unbond(&user, &10_000, &stake_timestamp); + assert_eq!( + env.auths(), + [( + user.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + staking.address.clone(), + Symbol::new(&env, "unbond"), + (&user.clone(), 10_000i128, (stake_timestamp)).into_val(&env), + )), + sub_invocations: std::vec![], + } + ),] + ); + let bonds = staking.query_staked(&user).stakes; assert_eq!( bonds, From bd4cdf3ec6ee12af0dc5a4186e6de1c4a69eff53 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:51:18 +0200 Subject: [PATCH 10/34] better math in stake --- contracts/stake/src/contract.rs | 123 +++++++++++++++++++++++----- contracts/stake/src/distribution.rs | 51 ++++++++++-- contracts/stake/src/storage.rs | 13 ++- 3 files changed, 155 insertions(+), 32 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 20aff8393..b2acfb274 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -176,7 +176,11 @@ impl StakingTrait for Staking { stake: tokens, stake_timestamp: ledger.timestamp(), }; - stakes.total_stake += tokens; + + stakes.total_stake = stakes.total_stake.checked_add(tokens).unwrap_or_else(|| { + log!(&env, "Stake: Bond: overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); // TODO: Discuss: Add implementation to add stake if another is present in +-24h timestamp to avoid // creating multiple stakes the same day @@ -184,7 +188,13 @@ impl StakingTrait for Staking { 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); + + let stakes_sum = stakes.checked_add(tokens).unwrap_or_else(|| { + log!(&env, "Stake: Bond: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + + let new_power = calc_power(&config, stakes_sum, Decimal::one(), TOKEN_PER_POWER); update_rewards( &env, &sender, @@ -224,12 +234,11 @@ impl StakingTrait for Staking { 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, - ); + let stakes_diff = stakes.checked_sub(stake_amount).unwrap_or_else(|| { + log!(&env, "Stake: Unbond: underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + let new_power = calc_power(&config, stakes_diff, Decimal::one(), TOKEN_PER_POWER); update_rewards( &env, &sender, @@ -242,7 +251,13 @@ impl StakingTrait for Staking { let mut stakes = get_stakes(&env, &sender); remove_stake(&env, &mut stakes.stakes, stake_amount, stake_timestamp); - stakes.total_stake -= stake_amount; + stakes.total_stake = stakes + .total_stake + .checked_sub(stake_amount) + .unwrap_or_else(|| { + log!(&env, "Stake: Unbond: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); let lp_token_client = token_contract::Client::new(&env, &config.lp_token); lp_token_client.transfer(&env.current_contract_address(), &sender, &stake_amount); @@ -316,30 +331,67 @@ impl StakingTrait for Staking { let undistributed_rewards = reward_token_client.balance(&env.current_contract_address()) as u128; - let curve = get_reward_curve(&env, &distribution_address).expect("Stake: Distribute reward: Not reward curve exists, probably distribution haven't been created"); + let curve = get_reward_curve(&env, &distribution_address).unwrap_or_else(|| { + log!(&env, "Stake: Distribute reward: Not reward curve exists, probably distribution haven't been created"); + panic_with_error!(&env, ContractError::RewardCurveDoesNotExist); + }); // 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()); + let amount = undistributed_rewards + .checked_sub(withdrawable) + .and_then(|diff| diff.checked_sub(curve.value(env.ledger().timestamp()))) + .unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); 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; + let shifted_left = amount.checked_shl(SHARES_SHIFT.into()).unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + + let points = shifted_left.checked_add(leftover).unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + let points_per_share = points.checked_div(total_rewards_power).unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); 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.shares_per_point += points_per_share; - distribution.distributed_total += amount; - distribution.withdrawable_total += amount; + distribution.shares_per_point = distribution + .shares_per_point + .checked_add(points_per_share) + .unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + distribution.distributed_total = distribution + .distributed_total + .checked_add(amount) + .unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + distribution.withdrawable_total = distribution + .withdrawable_total + .checked_add(amount) + .unwrap_or_else(|| { + log!(&env, "Stake: Distribute Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); save_distribution(&env, &distribution_address, &distribution); @@ -374,8 +426,20 @@ impl StakingTrait for Staking { if reward_amount == 0 { continue; } - withdraw_adjustment.withdrawn_rewards += reward_amount; - distribution.withdrawable_total -= reward_amount; + withdraw_adjustment.withdrawn_rewards = withdraw_adjustment + .withdrawn_rewards + .checked_add(reward_amount) + .unwrap_or_else(|| { + log!(&env, "Stake: Withdraw Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + distribution.withdrawable_total = distribution + .withdrawable_total + .checked_sub(reward_amount) + .unwrap_or_else(|| { + log!(&env, "Stake: Withdraw Rewards: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); save_distribution(&env, &distribution_address, &distribution); save_withdraw_adjustment(&env, &sender, &distribution_address, &withdraw_adjustment); @@ -436,7 +500,12 @@ impl StakingTrait for Staking { let reward_token_client = token_contract::Client::new(&env, &token_address); reward_token_client.transfer(&sender, &env.current_contract_address(), &token_amount); - let end_time = current_time + distribution_duration; + let end_time = current_time + .checked_add(distribution_duration) + .unwrap_or_else(|| { + log!(&env, "Stake: Fund Distribution: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); // 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 = @@ -592,8 +661,18 @@ impl StakingTrait for Staking { .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); let distribution = get_distribution(&env, &asset); let reward_token_client = token_contract::Client::new(&env, &asset); - reward_token_client.balance(&env.current_contract_address()) as u128 - - distribution.withdrawable_total + let reward_token_balance = + reward_token_client.balance(&env.current_contract_address()) as u128; + + reward_token_balance + .checked_sub(distribution.withdrawable_total) + .unwrap_or_else(|| { + log!( + &env, + "Stake: Query Undistributed Rewards: underflow occured." + ); + panic_with_error!(&env, ContractError::ContractMathError); + }) } } diff --git a/contracts/stake/src/distribution.rs b/contracts/stake/src/distribution.rs index a11f88f0a..fdafa3775 100644 --- a/contracts/stake/src/distribution.rs +++ b/contracts/stake/src/distribution.rs @@ -1,10 +1,14 @@ -use phoenix::ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; -use soroban_sdk::{contracttype, Address, Env}; +use phoenix::{ + ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}, + utils::{convert_i128_to_u128, convert_u128_to_i128}, +}; +use soroban_sdk::{contracttype, log, panic_with_error, Address, Env}; use curve::Curve; use soroban_decimal::Decimal; use crate::{ + error::ContractError, storage::{get_stakes, Config}, TOKEN_PER_POWER, }; @@ -131,7 +135,12 @@ pub fn update_rewards( if old_rewards_power == new_rewards_power { return; } - let diff = new_rewards_power - old_rewards_power; + let diff = new_rewards_power + .checked_sub(old_rewards_power) + .unwrap_or_else(|| { + log!(&env, "Stake: Update Rewards: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); // Apply the points correction with the calculated difference. let ppw = distribution.shares_per_point; apply_points_correction(env, user, asset, diff, ppw); @@ -150,7 +159,16 @@ fn apply_points_correction( ) { let mut withdraw_adjustment = get_withdraw_adjustment(env, user, asset); let shares_correction = withdraw_adjustment.shares_correction; - withdraw_adjustment.shares_correction = shares_correction - shares_per_point as i128 * diff; + withdraw_adjustment.shares_correction = (shares_per_point as i128) + .checked_mul(diff) + .and_then(|result| shares_correction.checked_sub(result)) + .unwrap_or_else(|| { + log!( + &env, + "Stake: Apply Points Correction: Underflow/Overflow occured." + ); + panic_with_error!(&env, ContractError::ContractMathError); + }); save_withdraw_adjustment(env, user, asset, &withdraw_adjustment); } @@ -243,12 +261,29 @@ pub fn withdrawable_rewards( // 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 as i128, Decimal::one(), TOKEN_PER_POWER); - let points = (ppw * points as u128) as i128; + let points = convert_u128_to_i128( + ppw.checked_mul(convert_i128_to_u128(points)) + .unwrap_or_else(|| { + log!(&env, "Stake: Withdrawable Rewards: Overflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }), + ); let correction = adjustment.shares_correction; - let points = points + correction; - let amount = points >> SHARES_SHIFT; - amount as u128 - adjustment.withdrawn_rewards + let points = points.checked_add(correction).unwrap_or_else(|| { + log!(&env, "Stake: Withdrawable Rewards: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + let amount = points.checked_shr(SHARES_SHIFT.into()).unwrap_or_else(|| { + log!(&env, "Stake Withdrawable Rewards: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + convert_i128_to_u128(amount) + .checked_sub(adjustment.withdrawn_rewards) + .unwrap_or_else(|| { + log!(&env, "Stake: Withdrawable Rewards: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }) } pub fn calculate_annualized_payout(reward_curve: Option, now: u64) -> Decimal { diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index 6f093991c..d276125c4 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -186,9 +186,13 @@ pub mod utils { pub fn increase_total_staked(e: &Env, amount: &i128) { let count = get_total_staked_counter(e); + let new_sum = count.checked_add(*amount).unwrap_or_else(|| { + log!(&e, "Stake: Increase Total Staked: Overflow occured."); + panic_with_error!(&e, ContractError::ContractMathError); + }); e.storage() .persistent() - .set(&DataKey::TotalStaked, &(count + amount)); + .set(&DataKey::TotalStaked, &new_sum); e.storage().persistent().extend_ttl( &DataKey::TotalStaked, @@ -199,9 +203,14 @@ pub mod utils { pub fn decrease_total_staked(e: &Env, amount: &i128) { let count = get_total_staked_counter(e); + + let new_diff = count.checked_sub(*amount).unwrap_or_else(|| { + log!(&e, "Stake: Increase Total Staked: Overflow occured."); + panic_with_error!(&e, ContractError::ContractMathError); + }); e.storage() .persistent() - .set(&DataKey::TotalStaked, &(count - amount)); + .set(&DataKey::TotalStaked, &new_diff); e.storage().persistent().extend_ttl( &DataKey::TotalStaked, From d7a73fff4d8dcbf737493ecf6debcd2f46435dee Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:27:11 +0200 Subject: [PATCH 11/34] fixes .unwrap() --- contracts/stake/src/storage.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/stake/src/storage.rs b/contracts/stake/src/storage.rs index d276125c4..dcfc2b5b2 100644 --- a/contracts/stake/src/storage.rs +++ b/contracts/stake/src/storage.rs @@ -224,7 +224,11 @@ pub mod utils { .storage() .persistent() .get(&DataKey::TotalStaked) - .unwrap(); + // or maybe .unwrap_or(0) + .unwrap_or_else(|| { + log!(&env, "Stake: Get Total Staked Counter: No value found"); + panic_with_error!(&env, ContractError::StakeNotFound); + }); env.storage().persistent().extend_ttl( &DataKey::TotalStaked, PERSISTENT_LIFETIME_THRESHOLD, From b45597e107988cf5c07ec4fc9ecf43cfa32d4980 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:30:33 +0200 Subject: [PATCH 12/34] logs --- contracts/stake/src/contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index b2acfb274..997112363 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -266,8 +266,8 @@ impl StakingTrait for Staking { utils::decrease_total_staked(&env, &stake_amount); env.events().publish(("unbond", "user"), &sender); - env.events().publish(("bond", "token"), &config.lp_token); - env.events().publish(("bond", "amount"), stake_amount); + env.events().publish(("unbond", "token"), &config.lp_token); + env.events().publish(("unbond", "amount"), stake_amount); } fn create_distribution_flow(env: Env, sender: Address, asset: Address) { From b89193fd756dc836c4bebdd8c2a41ab548011e62 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:30:57 +0200 Subject: [PATCH 13/34] more logs --- contracts/stake/src/contract.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 997112363..ab93f42e1 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -543,13 +543,13 @@ impl StakingTrait for Staking { save_reward_curve(&env, token_address.clone(), &new_reward_curve); env.events() - .publish(("fund_reward_distribution", "asset"), &token_address); + .publish(("fund_distribution", "asset"), &token_address); env.events() - .publish(("fund_reward_distribution", "amount"), token_amount); + .publish(("fund_distribution", "amount"), token_amount); env.events() - .publish(("fund_reward_distribution", "start_time"), start_time); + .publish(("fund_distribution", "start_time"), start_time); env.events() - .publish(("fund_reward_distribution", "end_time"), end_time); + .publish(("fund_distribution", "end_time"), end_time); } // QUERIES From 9c10207477f252d64648f6fb89f4aeb9a44ce5bb Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:38:50 +0200 Subject: [PATCH 14/34] adds missing Error --- contracts/stake/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/stake/src/error.rs b/contracts/stake/src/error.rs index e7020c15e..b885f1ad0 100644 --- a/contracts/stake/src/error.rs +++ b/contracts/stake/src/error.rs @@ -19,4 +19,5 @@ pub enum ContractError { DistributionNotFound = 414, AdminNotSet = 415, ContractMathError = 416, + RewardCurveDoesNotExist = 417, } From bcedbc8116fbdeb41ea16e4db27fa47b0608a9ca Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:54:47 +0200 Subject: [PATCH 15/34] [no ci] test for stake upgrade --- contracts/stake/src/tests/setup.rs | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 580048ef5..3c853c4c7 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -38,3 +38,79 @@ pub fn deploy_staking_contract<'a>( ); staking } + +#[cfg(test)] +#[allow(clippy::too_many_arguments)] +mod tests { + + const TOKEN_WASM: &[u8] = include_bytes!( + "../../../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" + ); + + pub mod token { + // 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/soroban_token_contract.wasm" + ); + } + + #[allow(clippy::too_many_arguments)] + pub mod old_stake { + soroban_sdk::contractimport!(file = "../../artifacts/old_phoenix_stake.wasm"); + } + + use soroban_sdk::{testutils::Address as _, Address}; + use soroban_sdk::{Env, String}; + + #[test] + fn upgrade_staking_contract_and_remove_stake_rewards() { + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().budget().reset_unlimited(); + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let owner = Address::generate(&env); + + let factory_addr = env.register(old_stake::WASM, ()); + let old_stake_client = old_stake::Client::new(&env, &factory_addr); + + let lp_token_addr = env.register( + TOKEN_WASM, + ( + admin.clone(), + 7, + String::from_str(&env, "LP Token"), + String::from_str(&env, "LPT"), + ), + ); + + let lp_token_client = token::Client::new(&env, &lp_token_addr); + + let reward_token_addr = env.register( + TOKEN_WASM, + ( + admin.clone(), + 7, + String::from_str(&env, "Reward Token"), + String::from_str(&env, "RWT"), + ), + ); + + let reward_token_client = token::Client::new(&env, &reward_token_addr); + reward_token_client.mint(&old_stake_client.address, &10_000_000_000_000); + + old_stake_client.initialize( + &admin, + &lp_token_client.address, + &100, + &50, + &manager, + &owner, + &7, + ); + + old_stake_client.create_distribution_flow(&manager, &reward_token_addr); + } +} From 8e0fb59d9ef50ad666b51da3ffbceb97d2b6090a Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:04:15 +0200 Subject: [PATCH 16/34] [no ci] adds old deprecated methods --- contracts/stake/src/contract.rs | 28 +++++++- contracts/stake/src/distribution.rs | 105 +++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index ab93f42e1..3e634f9a9 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -5,7 +5,7 @@ use soroban_sdk::{ Vec, }; -use crate::distribution::calc_power; +use crate::distribution::{calc_power, calculate_pending_rewards_deprecated}; use crate::TOKEN_PER_POWER; use crate::{ distribution::{ @@ -63,6 +63,8 @@ pub trait StakingTrait { fn withdraw_rewards(env: Env, sender: Address); + fn withdraw_rewards_deprecated(env: Env, sender: Address); + fn fund_distribution( env: Env, sender: Address, @@ -460,6 +462,30 @@ impl StakingTrait for Staking { } } + fn withdraw_rewards_deprecated(env: Env, sender: Address) { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + env.events().publish(("withdraw_rewards", "user"), &sender); + + let mut stakes = get_stakes(&env, &sender); + + for asset in get_distributions(&env) { + let pending_reward = calculate_pending_rewards_deprecated(&env, &asset, &stakes); + env.events() + .publish(("withdraw_rewards", "reward_token"), &asset); + + token_contract::Client::new(&env, &asset).transfer( + &env.current_contract_address(), + &sender, + &pending_reward, + ); + } + stakes.last_reward_time = env.ledger().timestamp(); + save_stakes(&env, &sender, &stakes); + } + fn fund_distribution( env: Env, sender: Address, diff --git a/contracts/stake/src/distribution.rs b/contracts/stake/src/distribution.rs index fdafa3775..10816fda0 100644 --- a/contracts/stake/src/distribution.rs +++ b/contracts/stake/src/distribution.rs @@ -2,14 +2,14 @@ use phoenix::{ ttl::{PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}, utils::{convert_i128_to_u128, convert_u128_to_i128}, }; -use soroban_sdk::{contracttype, log, panic_with_error, Address, Env}; +use soroban_sdk::{contracttype, log, panic_with_error, Address, Env, Map}; use curve::Curve; use soroban_decimal::Decimal; use crate::{ error::ContractError, - storage::{get_stakes, Config}, + storage::{get_stakes, BondingInfo, Config}, TOKEN_PER_POWER, }; @@ -23,6 +23,7 @@ use crate::{ /// calculations, but I256 is missing and it is required for this. pub const SHARES_SHIFT: u8 = 32; +const SECONDS_PER_DAY: u64 = 24 * 60 * 60; const SECONDS_PER_YEAR: u64 = 365 * 24 * 60 * 60; #[derive(Clone)] @@ -35,6 +36,8 @@ pub struct WithdrawAdjustmentKey { #[derive(Clone)] #[contracttype] pub enum DistributionDataKey { + RewardHistory(Address), + TotalStakedHistory, Curve(Address), Distribution(Address), WithdrawAdjustment(WithdrawAdjustmentKey), @@ -345,6 +348,104 @@ pub fn calc_power( } } +pub fn calculate_pending_rewards_deprecated( + env: &Env, + reward_token: &Address, + user_info: &BondingInfo, +) -> i128 { + let current_timestamp = env.ledger().timestamp(); + let last_reward_day = user_info.last_reward_time; + + // Load reward history and total staked history from storage + let reward_history = get_reward_history_deprecated(env, reward_token); + let total_staked_history = get_total_staked_history_deprecated(env); + + // Get the keys from the reward history map (which are the days) + let reward_keys = reward_history.keys(); + + let mut pending_rewards: i128 = 0; + + // Find the closest timestamp after last_reward_day + if let Some(first_relevant_day) = reward_keys.iter().find(|&day| day > last_reward_day) { + for staking_reward_day in reward_keys + .iter() + .skip_while(|&day| day < first_relevant_day) + .take_while(|&day| day <= current_timestamp) + { + if let (Some(daily_reward), Some(total_staked)) = ( + reward_history.get(staking_reward_day), + total_staked_history.get(staking_reward_day), + ) { + if total_staked > 0 { + // Calculate multiplier based on the age of each stake + for stake in user_info.stakes.iter() { + // Calculate the user's share of the total staked amount at the time + let user_share = (stake.stake as u128) + .checked_mul(daily_reward) + .and_then(|product| product.checked_div(total_staked)) + .unwrap_or_else(|| { + log!(&env, "Pool Stable: Math error in user share calculation"); + panic_with_error!(&env, ContractError::ContractMathError); + }); + let stake_age_days = (staking_reward_day + .saturating_sub(stake.stake_timestamp)) + / SECONDS_PER_DAY; + if stake_age_days == 0u64 { + continue; + } + let multiplier = if stake_age_days >= 60 { + Decimal::one() + } else { + Decimal::from_ratio(stake_age_days, 60) + }; + + // Apply the multiplier and accumulate the rewards + let adjusted_reward = user_share as i128 * multiplier; + pending_rewards = pending_rewards + .checked_add(adjusted_reward) + .unwrap_or_else(|| { + log!(&env, "Pool Stable: overflow occured"); + panic_with_error!(&env, ContractError::ContractMathError); + }); + } + } + } + } + } + + pending_rewards +} + +pub fn get_reward_history_deprecated(e: &Env, reward_token: &Address) -> Map { + let reward_history = e + .storage() + .persistent() + .get(&DistributionDataKey::RewardHistory(reward_token.clone())) + .unwrap(); + e.storage().persistent().extend_ttl( + &DistributionDataKey::RewardHistory(reward_token.clone()), + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + + reward_history +} + +pub fn get_total_staked_history_deprecated(e: &Env) -> Map { + let total_staked_history = e + .storage() + .persistent() + .get(&DistributionDataKey::TotalStakedHistory) + .unwrap(); + e.storage().persistent().extend_ttl( + &DistributionDataKey::TotalStakedHistory, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); + + total_staked_history +} + #[cfg(test)] mod tests { use super::*; From c9b14c445e100d95278847905c2ed20f84f4d384 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:53:38 +0200 Subject: [PATCH 17/34] wip --- contracts/stake/src/tests/setup.rs | 91 ++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 3c853c4c7..e87f088ea 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -61,38 +61,49 @@ mod tests { soroban_sdk::contractimport!(file = "../../artifacts/old_phoenix_stake.wasm"); } + use old_stake::StakedResponse; + use pretty_assertions::assert_eq; + use soroban_sdk::testutils::Ledger; use soroban_sdk::{testutils::Address as _, Address}; - use soroban_sdk::{Env, String}; + use soroban_sdk::{vec, Env, String}; #[test] fn upgrade_staking_contract_and_remove_stake_rewards() { + const DAY_AS_SECONDS: u64 = 86_400; + let env = Env::default(); env.mock_all_auths(); env.cost_estimate().budget().reset_unlimited(); let admin = Address::generate(&env); let manager = Address::generate(&env); let owner = Address::generate(&env); + let user_1 = Address::generate(&env); + let user_2 = Address::generate(&env); + let user_3 = Address::generate(&env); let factory_addr = env.register(old_stake::WASM, ()); let old_stake_client = old_stake::Client::new(&env, &factory_addr); let lp_token_addr = env.register( - TOKEN_WASM, + token::WASM, ( - admin.clone(), - 7, + Address::generate(&env), + 7u32, String::from_str(&env, "LP Token"), String::from_str(&env, "LPT"), ), ); let lp_token_client = token::Client::new(&env, &lp_token_addr); + lp_token_client.mint(&user_1, &10_000_000_000_000); + lp_token_client.mint(&user_2, &10_000_000_000_000); + lp_token_client.mint(&user_3, &10_000_000_000_000); let reward_token_addr = env.register( TOKEN_WASM, ( - admin.clone(), - 7, + Address::generate(&env), + 7u32, String::from_str(&env, "Reward Token"), String::from_str(&env, "RWT"), ), @@ -111,6 +122,74 @@ mod tests { &7, ); + // after a day the manager creates a distribution flow + env.ledger().with_mut(|li| li.timestamp += DAY_AS_SECONDS); old_stake_client.create_distribution_flow(&manager, &reward_token_addr); + + // another day passes and the users bond + env.ledger().with_mut(|li| li.timestamp += DAY_AS_SECONDS); + old_stake_client.bond(&user_1, &10_000_000_000); // user_1 bonds 1,000 tokens + old_stake_client.bond(&user_2, &20_000_000_000); // user_2 bonds 2,000 tokens + old_stake_client.bond(&user_3, &15_000_000_000); // user_3 bonds 1,500 tokens + + // Assert staked amounts for all users + assert_eq!( + old_stake_client.query_staked(&user_1), + StakedResponse { + last_reward_time: 0, + stakes: vec![ + &env, + old_stake::Stake { + stake: 10_000_000_000, + stake_timestamp: DAY_AS_SECONDS * 2, + } + ], + total_stake: 10_000_000_000 + } + ); + + assert_eq!( + old_stake_client.query_staked(&user_2), + StakedResponse { + last_reward_time: 0, + stakes: vec![ + &env, + old_stake::Stake { + stake: 20_000_000_000, + stake_timestamp: DAY_AS_SECONDS * 2, + } + ], + total_stake: 20_000_000_000 + } + ); + + assert_eq!( + old_stake_client.query_staked(&user_3), + StakedResponse { + last_reward_time: 0, + stakes: vec![ + &env, + old_stake::Stake { + stake: 15_000_000_000, + stake_timestamp: DAY_AS_SECONDS * 2, + } + ], + total_stake: 15_000_000_000 + } + ); + + // 100 days forward after staking let's check the rewards + env.ledger() + .with_mut(|li| li.timestamp += 100 * DAY_AS_SECONDS); + + let user_1_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_1); + let user_2_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_2); + let user_3_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_3); + + soroban_sdk::testutils::arbitrary::std::dbg!( + user_1_withdrawable_rewards, + user_2_withdrawable_rewards, + user_3_withdrawable_rewards + ); } } From 8dac7b358d9c557f5261c10bf652eaba804bd39f Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:45:02 +0200 Subject: [PATCH 18/34] [no ci] wip --- .../old_phoenix_stake.wasm | Bin 0 -> 24379 bytes .../phoenix_stake_rewards.wasm | Bin 0 -> 43579 bytes contracts/stake/src/tests/setup.rs | 12 ++++++------ 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100755 .artifacts_stake_migration_test/old_phoenix_stake.wasm create mode 100755 .artifacts_stake_migration_test/phoenix_stake_rewards.wasm diff --git a/.artifacts_stake_migration_test/old_phoenix_stake.wasm b/.artifacts_stake_migration_test/old_phoenix_stake.wasm new file mode 100755 index 0000000000000000000000000000000000000000..8a57e7ce31c90283413bd24257eef472e08612e1 GIT binary patch literal 24379 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 literal 0 HcmV?d00001 diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index e87f088ea..ec2e3c207 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -43,10 +43,6 @@ pub fn deploy_staking_contract<'a>( #[allow(clippy::too_many_arguments)] mod tests { - const TOKEN_WASM: &[u8] = include_bytes!( - "../../../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" - ); - pub mod token { // The import will code generate: // - A ContractClient type that can be used to invoke functions on the contract. @@ -58,7 +54,9 @@ mod tests { #[allow(clippy::too_many_arguments)] pub mod old_stake { - soroban_sdk::contractimport!(file = "../../artifacts/old_phoenix_stake.wasm"); + soroban_sdk::contractimport!( + file = "../../.artifacts_stake_migration_test/old_phoenix_stake.wasm" + ); } use old_stake::StakedResponse; @@ -100,7 +98,7 @@ mod tests { lp_token_client.mint(&user_3, &10_000_000_000_000); let reward_token_addr = env.register( - TOKEN_WASM, + token::WASM, ( Address::generate(&env), 7u32, @@ -186,6 +184,8 @@ mod tests { let user_2_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_2); let user_3_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_3); + old_stake_client.distribute_rewards(&manager, &100, &reward_token_addr); + soroban_sdk::testutils::arbitrary::std::dbg!( user_1_withdrawable_rewards, user_2_withdrawable_rewards, From dff85f7a17dd47d14e5fc7ab9cc955d24c5b1c10 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:48:47 +0200 Subject: [PATCH 19/34] solves the distribute rewards err --- contracts/stake/src/tests/setup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index ec2e3c207..fa5799714 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -108,7 +108,7 @@ mod tests { ); let reward_token_client = token::Client::new(&env, &reward_token_addr); - reward_token_client.mint(&old_stake_client.address, &10_000_000_000_000); + reward_token_client.mint(&manager, &10_000_000_000_000); old_stake_client.initialize( &admin, From e69a2af84c428820f53bfe731e8e2d1da3315d9b Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:06:17 +0200 Subject: [PATCH 20/34] [no ci] before migration --- contracts/stake/src/tests/setup.rs | 92 ++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index fa5799714..14066cde6 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -59,7 +59,7 @@ mod tests { ); } - use old_stake::StakedResponse; + use old_stake::{StakedResponse, WithdrawableReward, WithdrawableRewardsResponse}; use pretty_assertions::assert_eq; use soroban_sdk::testutils::Ledger; use soroban_sdk::{testutils::Address as _, Address}; @@ -108,7 +108,7 @@ mod tests { ); let reward_token_client = token::Client::new(&env, &reward_token_addr); - reward_token_client.mint(&manager, &10_000_000_000_000); + reward_token_client.mint(&manager, &100_000_000_000_000); old_stake_client.initialize( &admin, @@ -176,20 +176,88 @@ mod tests { } ); - // 100 days forward after staking let's check the rewards + // 30 days forward after staking let's check the rewards env.ledger() - .with_mut(|li| li.timestamp += 100 * DAY_AS_SECONDS); + .with_mut(|li| li.timestamp += 30 * DAY_AS_SECONDS); - let user_1_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_1); - let user_2_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_2); - let user_3_withdrawable_rewards = old_stake_client.query_withdrawable_rewards(&user_3); + assert_eq!( + old_stake_client.query_withdrawable_rewards(&user_1), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 0, + } + ] + } + ); + + assert_eq!( + old_stake_client.query_withdrawable_rewards(&user_2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 0, + } + ] + } + ); + + assert_eq!( + old_stake_client.query_withdrawable_rewards(&user_3), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 0, + } + ] + } + ); - old_stake_client.distribute_rewards(&manager, &100, &reward_token_addr); + old_stake_client.distribute_rewards(&manager, &10_000_000, &reward_token_addr); - soroban_sdk::testutils::arbitrary::std::dbg!( - user_1_withdrawable_rewards, - user_2_withdrawable_rewards, - user_3_withdrawable_rewards + assert_eq!( + old_stake_client.query_withdrawable_rewards(&user_1), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 1_111_111, + } + ] + } + ); + + assert_eq!( + old_stake_client.query_withdrawable_rewards(&user_2), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 2_222_222, + } + ] + } + ); + + assert_eq!( + old_stake_client.query_withdrawable_rewards(&user_3), + WithdrawableRewardsResponse { + rewards: vec![ + &env, + WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 1_666_666, + } + ] + } ); } } From f5640ebd5598c9c172c2ed6f7543cd41c2bac54e Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:22:03 +0200 Subject: [PATCH 21/34] [no ci]] adds deprecated methods for rewards query --- contracts/stake/src/contract.rs | 24 +++++++++++++++++++++- contracts/stake/src/tests/setup.rs | 33 +++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 3e634f9a9..6c4f92f78 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -88,6 +88,8 @@ pub trait StakingTrait { fn query_withdrawable_rewards(env: Env, address: Address) -> WithdrawableRewardsResponse; + fn query_withdrawable_rewards_dep(env: Env, address: Address) -> WithdrawableRewardsResponse; + fn query_distributed_rewards(env: Env, asset: Address) -> u128; fn query_undistributed_rewards(env: Env, asset: Address) -> u128; @@ -467,7 +469,7 @@ impl StakingTrait for Staking { .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - env.events().publish(("withdraw_rewards", "user"), &sender); + env.events().publish(("withdraw_rewards", "DBG"), &sender); let mut stakes = get_stakes(&env, &sender); @@ -649,6 +651,7 @@ impl StakingTrait for Staking { } fn query_withdrawable_rewards(env: Env, user: Address) -> WithdrawableRewardsResponse { + soroban_sdk::testutils::arbitrary::std::dbg!("INSIDE"); env.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); @@ -673,6 +676,25 @@ impl StakingTrait for Staking { WithdrawableRewardsResponse { rewards } } + fn query_withdrawable_rewards_dep(env: Env, user: Address) -> WithdrawableRewardsResponse { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + let stakes = get_stakes(&env, &user); + // iterate over all distributions and calculate withdrawable rewards + let mut rewards = vec![&env]; + for asset in get_distributions(&env) { + let pending_reward = calculate_pending_rewards_deprecated(&env, &asset, &stakes); + + rewards.push_back(WithdrawableReward { + reward_address: asset, + reward_amount: pending_reward as u128, + }); + } + + WithdrawableRewardsResponse { rewards } + } + fn query_distributed_rewards(env: Env, asset: Address) -> u128 { env.storage() .instance() diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 14066cde6..f080a293c 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}, @@ -13,6 +13,14 @@ pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract:: ) } +#[allow(clippy::too_many_arguments)] +pub fn install_stake_wasm(env: &Env) -> BytesN<32> { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" + ); + env.deployer().upload_contract_wasm(WASM) +} + const MIN_BOND: i128 = 1000; const MIN_REWARD: i128 = 1000; @@ -65,6 +73,9 @@ mod tests { use soroban_sdk::{testutils::Address as _, Address}; use soroban_sdk::{vec, Env, String}; + use crate::contract::StakingClient; + use crate::tests::setup::install_stake_wasm; + #[test] fn upgrade_staking_contract_and_remove_stake_rewards() { const DAY_AS_SECONDS: u64 = 86_400; @@ -79,8 +90,8 @@ mod tests { let user_2 = Address::generate(&env); let user_3 = Address::generate(&env); - let factory_addr = env.register(old_stake::WASM, ()); - let old_stake_client = old_stake::Client::new(&env, &factory_addr); + let stake_addr = env.register(old_stake::WASM, ()); + let old_stake_client = old_stake::Client::new(&env, &stake_addr); let lp_token_addr = env.register( token::WASM, @@ -259,5 +270,21 @@ mod tests { ] } ); + + let new_stake_wasm = install_stake_wasm(&env); + + old_stake_client.update(&new_stake_wasm); + old_stake_client.update(&new_stake_wasm); + + let latest_stake_client = StakingClient::new(&env, &stake_addr); + latest_stake_client.update(&new_stake_wasm); + + let result = latest_stake_client.query_withdrawable_rewards(&user_1); + soroban_sdk::testutils::arbitrary::std::dbg!(result); + soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); + + //soroban_sdk::testutils::arbitrary::std::dbg!(reward_token_client.balance(&user_1)); + latest_stake_client.withdraw_rewards_deprecated(&user_1); + //soroban_sdk::testutils::arbitrary::std::dbg!(reward_token_client.balance(&user_1)); } } From 6d5a3cbc43481eef301b3221629050855b273d9f Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:22:52 +0200 Subject: [PATCH 22/34] migration works and users are able to withdraw old rewards --- contracts/stake/src/tests/setup.rs | 113 +++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 7 deletions(-) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index f080a293c..db698d28c 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -74,6 +74,7 @@ mod tests { use soroban_sdk::{vec, Env, String}; use crate::contract::StakingClient; + use crate::msg; use crate::tests::setup::install_stake_wasm; #[test] @@ -90,6 +91,8 @@ mod tests { let user_2 = Address::generate(&env); let user_3 = Address::generate(&env); + let new_user = Address::generate(&env); + let stake_addr = env.register(old_stake::WASM, ()); let old_stake_client = old_stake::Client::new(&env, &stake_addr); @@ -271,20 +274,116 @@ mod tests { } ); + // we upgrade let new_stake_wasm = install_stake_wasm(&env); old_stake_client.update(&new_stake_wasm); - old_stake_client.update(&new_stake_wasm); + //old_stake_client.update(&new_stake_wasm); let latest_stake_client = StakingClient::new(&env, &stake_addr); - latest_stake_client.update(&new_stake_wasm); - let result = latest_stake_client.query_withdrawable_rewards(&user_1); - soroban_sdk::testutils::arbitrary::std::dbg!(result); - soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); + // check the rewards again, this time with the old deprecated method + assert_eq!( + latest_stake_client.query_withdrawable_rewards_dep(&user_1), + msg::WithdrawableRewardsResponse { + rewards: vec![ + &env, + msg::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 1_111_111, + } + ] + } + ); + + assert_eq!( + latest_stake_client.query_withdrawable_rewards_dep(&user_2), + msg::WithdrawableRewardsResponse { + rewards: vec![ + &env, + msg::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 2_222_222, + } + ] + } + ); + + assert_eq!( + latest_stake_client.query_withdrawable_rewards_dep(&user_3), + msg::WithdrawableRewardsResponse { + rewards: vec![ + &env, + msg::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 1_666_666, + } + ] + } + ); - //soroban_sdk::testutils::arbitrary::std::dbg!(reward_token_client.balance(&user_1)); latest_stake_client.withdraw_rewards_deprecated(&user_1); - //soroban_sdk::testutils::arbitrary::std::dbg!(reward_token_client.balance(&user_1)); + latest_stake_client.withdraw_rewards_deprecated(&user_2); + latest_stake_client.withdraw_rewards_deprecated(&user_3); + + // we make sure that there are no more rewards + assert_eq!( + latest_stake_client.query_withdrawable_rewards_dep(&user_1), + msg::WithdrawableRewardsResponse { + rewards: vec![ + &env, + msg::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 0, + } + ] + } + ); + + assert_eq!( + latest_stake_client.query_withdrawable_rewards_dep(&user_2), + msg::WithdrawableRewardsResponse { + rewards: vec![ + &env, + msg::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 0, + } + ] + } + ); + + assert_eq!( + latest_stake_client.query_withdrawable_rewards_dep(&user_3), + msg::WithdrawableRewardsResponse { + rewards: vec![ + &env, + msg::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 0, + } + ] + } + ); + + assert_eq!(reward_token_client.balance(&user_1), 1_111_111); + assert_eq!(reward_token_client.balance(&user_2), 2_222_222); + assert_eq!(reward_token_client.balance(&user_3), 1_666_666); + + // one more day passes by and new_user decides to stake + env.ledger().with_mut(|li| li.timestamp += DAY_AS_SECONDS); + + lp_token_client.mint(&new_user, &10_000_000_000_000); + + soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); + latest_stake_client.bond(&new_user, &10_000_000_000); // new_user also bonds 1,000 tokens + soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); + + // two months pass by + env.ledger() + .with_mut(|li| li.timestamp += 60 * DAY_AS_SECONDS); + + // distribute the rewards + latest_stake_client.distribute_rewards(); } } From ec32a564d9d887003e5fdc137e5e6cd0b12a8e66 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:54:15 +0200 Subject: [PATCH 23/34] wip unbonding fails --- contracts/stake/src/contract.rs | 1 - contracts/stake/src/tests/setup.rs | 55 ++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 6c4f92f78..d1be1fec3 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -651,7 +651,6 @@ impl StakingTrait for Staking { } fn query_withdrawable_rewards(env: Env, user: Address) -> WithdrawableRewardsResponse { - soroban_sdk::testutils::arbitrary::std::dbg!("INSIDE"); env.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index db698d28c..d1814ded6 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -75,6 +75,7 @@ mod tests { use crate::contract::StakingClient; use crate::msg; + use crate::storage::Stake; use crate::tests::setup::install_stake_wasm; #[test] @@ -370,20 +371,68 @@ mod tests { assert_eq!(reward_token_client.balance(&user_2), 2_222_222); assert_eq!(reward_token_client.balance(&user_3), 1_666_666); + // 30 days pass by and this time users directly unbond 1/2 which should also get their + // rewards + env.ledger() + .with_mut(|li| li.timestamp += 30 * DAY_AS_SECONDS); + + soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); + latest_stake_client.unbond(&user_1, &5_000_000_000, &(DAY_AS_SECONDS * 2)); + latest_stake_client.unbond(&user_2, &10_000_000_000, &(DAY_AS_SECONDS * 2)); + latest_stake_client.unbond(&user_3, &7_500_000_000, &(DAY_AS_SECONDS * 2)); + + soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); + assert_eq!( + latest_stake_client.query_staked(&user_1), + msg::StakedResponse { + stakes: vec![ + &env, + Stake { + stake: 5_000_000_000, + stake_timestamp: DAY_AS_SECONDS * 2 + } + ] + } + ); + + assert_eq!( + latest_stake_client.query_staked(&user_2), + msg::StakedResponse { + stakes: vec![ + &env, + Stake { + stake: 10_000_000_000, + stake_timestamp: DAY_AS_SECONDS * 2 + } + ] + } + ); + + assert_eq!( + latest_stake_client.query_staked(&user_3), + msg::StakedResponse { + stakes: vec![ + &env, + Stake { + stake: 7_500_000_000, + stake_timestamp: DAY_AS_SECONDS * 2 + } + ] + } + ); + // one more day passes by and new_user decides to stake env.ledger().with_mut(|li| li.timestamp += DAY_AS_SECONDS); lp_token_client.mint(&new_user, &10_000_000_000_000); - soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); latest_stake_client.bond(&new_user, &10_000_000_000); // new_user also bonds 1,000 tokens - soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); // two months pass by env.ledger() .with_mut(|li| li.timestamp += 60 * DAY_AS_SECONDS); - // distribute the rewards + // distribute and take the rewards latest_stake_client.distribute_rewards(); } } From dc503f619d5e1218970b1a3e28af9f50cf83cd53 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:42:19 +0200 Subject: [PATCH 24/34] [no ci] new error some progress --- contracts/stake/src/contract.rs | 58 ++++++++++++++++++++++++++++++ contracts/stake/src/tests/setup.rs | 51 ++++++++++++++------------ 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index d1be1fec3..2fac3039b 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -57,6 +57,8 @@ pub trait StakingTrait { fn unbond(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64); + fn unbond_deprecated(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64); + fn create_distribution_flow(env: Env, sender: Address, asset: Address); fn distribute_rewards(env: Env); @@ -274,6 +276,62 @@ impl StakingTrait for Staking { env.events().publish(("unbond", "amount"), stake_amount); } + fn unbond_deprecated(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64) { + sender.require_auth(); + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + let config = get_config(&env); + + // check for rewards and withdraw them + let found_rewards: WithdrawableRewardsResponse = + Self::query_withdrawable_rewards_dep(env.clone(), sender.clone()); + + if !found_rewards.rewards.is_empty() { + Self::withdraw_rewards_deprecated(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 stakes_diff = stakes.checked_sub(stake_amount).unwrap_or_else(|| { + log!(&env, "Stake: Unbond: underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + let new_power = calc_power(&config, stakes_diff, Decimal::one(), TOKEN_PER_POWER); + update_rewards( + &env, + &sender, + &distribution_address, + &mut distribution, + old_power, + new_power, + ); + } + + let mut stakes = get_stakes(&env, &sender); + remove_stake(&env, &mut stakes.stakes, stake_amount, stake_timestamp); + stakes.total_stake = stakes + .total_stake + .checked_sub(stake_amount) + .unwrap_or_else(|| { + log!(&env, "Stake: Unbond: Underflow occured."); + panic_with_error!(&env, ContractError::ContractMathError); + }); + + let lp_token_client = token_contract::Client::new(&env, &config.lp_token); + lp_token_client.transfer(&env.current_contract_address(), &sender, &stake_amount); + + save_stakes(&env, &sender, &stakes); + utils::decrease_total_staked(&env, &stake_amount); + + env.events().publish(("unbond", "user"), &sender); + env.events().publish(("unbond", "token"), &config.lp_token); + env.events().publish(("unbond", "amount"), stake_amount); + } + fn create_distribution_flow(env: Env, sender: Address, asset: Address) { sender.require_auth(); env.storage() diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index d1814ded6..79c83970d 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -21,6 +21,13 @@ pub fn install_stake_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(WASM) } +#[allow(clippy::too_many_arguments)] +mod latest_stake { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" + ); +} + const MIN_BOND: i128 = 1000; const MIN_REWARD: i128 = 1000; @@ -76,7 +83,7 @@ mod tests { use crate::contract::StakingClient; use crate::msg; use crate::storage::Stake; - use crate::tests::setup::install_stake_wasm; + use crate::tests::setup::{install_stake_wasm, latest_stake}; #[test] fn upgrade_staking_contract_and_remove_stake_rewards() { @@ -281,15 +288,15 @@ mod tests { old_stake_client.update(&new_stake_wasm); //old_stake_client.update(&new_stake_wasm); - let latest_stake_client = StakingClient::new(&env, &stake_addr); + let latest_stake_client = latest_stake::Client::new(&env, &stake_addr); // check the rewards again, this time with the old deprecated method assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_1), - msg::WithdrawableRewardsResponse { + latest_stake::WithdrawableRewardsResponse { rewards: vec![ &env, - msg::WithdrawableReward { + latest_stake::WithdrawableReward { reward_address: reward_token_addr.clone(), reward_amount: 1_111_111, } @@ -299,10 +306,10 @@ mod tests { assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_2), - msg::WithdrawableRewardsResponse { + latest_stake::WithdrawableRewardsResponse { rewards: vec![ &env, - msg::WithdrawableReward { + latest_stake::WithdrawableReward { reward_address: reward_token_addr.clone(), reward_amount: 2_222_222, } @@ -312,10 +319,10 @@ mod tests { assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_3), - msg::WithdrawableRewardsResponse { + latest_stake::WithdrawableRewardsResponse { rewards: vec![ &env, - msg::WithdrawableReward { + latest_stake::WithdrawableReward { reward_address: reward_token_addr.clone(), reward_amount: 1_666_666, } @@ -330,10 +337,10 @@ mod tests { // we make sure that there are no more rewards assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_1), - msg::WithdrawableRewardsResponse { + latest_stake::WithdrawableRewardsResponse { rewards: vec![ &env, - msg::WithdrawableReward { + latest_stake::WithdrawableReward { reward_address: reward_token_addr.clone(), reward_amount: 0, } @@ -343,10 +350,10 @@ mod tests { assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_2), - msg::WithdrawableRewardsResponse { + latest_stake::WithdrawableRewardsResponse { rewards: vec![ &env, - msg::WithdrawableReward { + latest_stake::WithdrawableReward { reward_address: reward_token_addr.clone(), reward_amount: 0, } @@ -356,10 +363,10 @@ mod tests { assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_3), - msg::WithdrawableRewardsResponse { + latest_stake::WithdrawableRewardsResponse { rewards: vec![ &env, - msg::WithdrawableReward { + latest_stake::WithdrawableReward { reward_address: reward_token_addr.clone(), reward_amount: 0, } @@ -377,17 +384,17 @@ mod tests { .with_mut(|li| li.timestamp += 30 * DAY_AS_SECONDS); soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); - latest_stake_client.unbond(&user_1, &5_000_000_000, &(DAY_AS_SECONDS * 2)); - latest_stake_client.unbond(&user_2, &10_000_000_000, &(DAY_AS_SECONDS * 2)); + latest_stake_client.unbond_deprecated(&user_1, &5_000_000_000, &(DAY_AS_SECONDS * 2)); + latest_stake_client.unbond_deprecated(&user_2, &10_000_000_000, &(DAY_AS_SECONDS * 2)); latest_stake_client.unbond(&user_3, &7_500_000_000, &(DAY_AS_SECONDS * 2)); soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); assert_eq!( latest_stake_client.query_staked(&user_1), - msg::StakedResponse { + latest_stake::StakedResponse { stakes: vec![ &env, - Stake { + latest_stake::Stake { stake: 5_000_000_000, stake_timestamp: DAY_AS_SECONDS * 2 } @@ -397,10 +404,10 @@ mod tests { assert_eq!( latest_stake_client.query_staked(&user_2), - msg::StakedResponse { + latest_stake::StakedResponse { stakes: vec![ &env, - Stake { + latest_stake::Stake { stake: 10_000_000_000, stake_timestamp: DAY_AS_SECONDS * 2 } @@ -410,10 +417,10 @@ mod tests { assert_eq!( latest_stake_client.query_staked(&user_3), - msg::StakedResponse { + latest_stake::StakedResponse { stakes: vec![ &env, - Stake { + latest_stake::Stake { stake: 7_500_000_000, stake_timestamp: DAY_AS_SECONDS * 2 } From 83db94cf3163edbd21c53d880651e63daabe65b8 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Fri, 31 Jan 2025 13:12:11 +0200 Subject: [PATCH 25/34] [no ci] wip --- contracts/stake/src/contract.rs | 11 ++++++++++- contracts/stake/src/tests/setup.rs | 5 +---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 2fac3039b..a9500fcb5 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -277,20 +277,26 @@ impl StakingTrait for Staking { } fn unbond_deprecated(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64) { + env.events().publish(("unbond_deprecated", "280"), &sender); sender.require_auth(); env.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + env.events().publish(("unbond_deprecated", "286"), &sender); let config = get_config(&env); // check for rewards and withdraw them + env.events().publish(("unbond_deprecated", "290"), &sender); let found_rewards: WithdrawableRewardsResponse = Self::query_withdrawable_rewards_dep(env.clone(), sender.clone()); + env.events().publish(("unbond_deprecated", "294"), &sender); if !found_rewards.rewards.is_empty() { + env.events().publish(("unbond_deprecated", "296"), &sender); Self::withdraw_rewards_deprecated(env.clone(), sender.clone()); } + env.events().publish(("unbond_deprecated", "299"), &sender); for distribution_address in get_distributions(&env) { let mut distribution = get_distribution(&env, &distribution_address); @@ -527,10 +533,11 @@ impl StakingTrait for Staking { .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - env.events().publish(("withdraw_rewards", "DBG"), &sender); + env.events().publish(("withdraw_rewards", "536"), &sender); let mut stakes = get_stakes(&env, &sender); + env.events().publish(("withdraw_rewards", "540"), &sender); for asset in get_distributions(&env) { let pending_reward = calculate_pending_rewards_deprecated(&env, &asset, &stakes); env.events() @@ -542,6 +549,8 @@ impl StakingTrait for Staking { &pending_reward, ); } + + env.events().publish(("withdraw_rewards", "553"), &sender); stakes.last_reward_time = env.ledger().timestamp(); save_stakes(&env, &sender, &stakes); } diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 79c83970d..1c394c505 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -80,9 +80,6 @@ mod tests { use soroban_sdk::{testutils::Address as _, Address}; use soroban_sdk::{vec, Env, String}; - use crate::contract::StakingClient; - use crate::msg; - use crate::storage::Stake; use crate::tests::setup::{install_stake_wasm, latest_stake}; #[test] @@ -386,7 +383,7 @@ mod tests { soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); latest_stake_client.unbond_deprecated(&user_1, &5_000_000_000, &(DAY_AS_SECONDS * 2)); latest_stake_client.unbond_deprecated(&user_2, &10_000_000_000, &(DAY_AS_SECONDS * 2)); - latest_stake_client.unbond(&user_3, &7_500_000_000, &(DAY_AS_SECONDS * 2)); + latest_stake_client.unbond_deprecated(&user_3, &7_500_000_000, &(DAY_AS_SECONDS * 2)); soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); assert_eq!( From c4edfc7ffb385692f9c91b879af81c73161d2c70 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:03:47 +0200 Subject: [PATCH 26/34] new error --- contracts/stake/src/contract.rs | 37 +++------------------------------ 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index a9500fcb5..5432a2de9 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -277,53 +277,22 @@ impl StakingTrait for Staking { } fn unbond_deprecated(env: Env, sender: Address, stake_amount: i128, stake_timestamp: u64) { - env.events().publish(("unbond_deprecated", "280"), &sender); sender.require_auth(); + env.storage() .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - env.events().publish(("unbond_deprecated", "286"), &sender); let config = get_config(&env); - // check for rewards and withdraw them - env.events().publish(("unbond_deprecated", "290"), &sender); - let found_rewards: WithdrawableRewardsResponse = - Self::query_withdrawable_rewards_dep(env.clone(), sender.clone()); - - env.events().publish(("unbond_deprecated", "294"), &sender); - if !found_rewards.rewards.is_empty() { - env.events().publish(("unbond_deprecated", "296"), &sender); - Self::withdraw_rewards_deprecated(env.clone(), sender.clone()); - } - env.events().publish(("unbond_deprecated", "299"), &sender); - - 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 stakes_diff = stakes.checked_sub(stake_amount).unwrap_or_else(|| { - log!(&env, "Stake: Unbond: underflow occured."); - panic_with_error!(&env, ContractError::ContractMathError); - }); - let new_power = calc_power(&config, stakes_diff, Decimal::one(), TOKEN_PER_POWER); - update_rewards( - &env, - &sender, - &distribution_address, - &mut distribution, - old_power, - new_power, - ); - } - let mut stakes = get_stakes(&env, &sender); + remove_stake(&env, &mut stakes.stakes, stake_amount, stake_timestamp); stakes.total_stake = stakes .total_stake .checked_sub(stake_amount) .unwrap_or_else(|| { - log!(&env, "Stake: Unbond: Underflow occured."); + log!(&env, "Stake: Unbond: underflow occured."); panic_with_error!(&env, ContractError::ContractMathError); }); From 21a19b000640c56d408e1a3883de56c8a41a1baa Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:32:21 +0200 Subject: [PATCH 27/34] wip - bond fails because of missing distribution --- contracts/stake/src/contract.rs | 6 ---- contracts/stake/src/tests/setup.rs | 50 +++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 5432a2de9..2a324cabc 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -194,12 +194,10 @@ impl StakingTrait for Staking { 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 stakes_sum = stakes.checked_add(tokens).unwrap_or_else(|| { log!(&env, "Stake: Bond: Overflow occured."); panic_with_error!(&env, ContractError::ContractMathError); }); - let new_power = calc_power(&config, stakes_sum, Decimal::one(), TOKEN_PER_POWER); update_rewards( &env, @@ -502,11 +500,8 @@ impl StakingTrait for Staking { .instance() .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); - env.events().publish(("withdraw_rewards", "536"), &sender); - let mut stakes = get_stakes(&env, &sender); - env.events().publish(("withdraw_rewards", "540"), &sender); for asset in get_distributions(&env) { let pending_reward = calculate_pending_rewards_deprecated(&env, &asset, &stakes); env.events() @@ -519,7 +514,6 @@ impl StakingTrait for Staking { ); } - env.events().publish(("withdraw_rewards", "553"), &sender); stakes.last_reward_time = env.ledger().timestamp(); save_stakes(&env, &sender, &stakes); } diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 1c394c505..520198fe2 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -375,24 +375,14 @@ mod tests { assert_eq!(reward_token_client.balance(&user_2), 2_222_222); assert_eq!(reward_token_client.balance(&user_3), 1_666_666); - // 30 days pass by and this time users directly unbond 1/2 which should also get their - // rewards - env.ledger() - .with_mut(|li| li.timestamp += 30 * DAY_AS_SECONDS); - - soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); - latest_stake_client.unbond_deprecated(&user_1, &5_000_000_000, &(DAY_AS_SECONDS * 2)); - latest_stake_client.unbond_deprecated(&user_2, &10_000_000_000, &(DAY_AS_SECONDS * 2)); - latest_stake_client.unbond_deprecated(&user_3, &7_500_000_000, &(DAY_AS_SECONDS * 2)); - - soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); + // query the staked before unbonding assert_eq!( latest_stake_client.query_staked(&user_1), latest_stake::StakedResponse { stakes: vec![ &env, latest_stake::Stake { - stake: 5_000_000_000, + stake: 10000000000, stake_timestamp: DAY_AS_SECONDS * 2 } ] @@ -405,7 +395,7 @@ mod tests { stakes: vec![ &env, latest_stake::Stake { - stake: 10_000_000_000, + stake: 20_000_000_000, stake_timestamp: DAY_AS_SECONDS * 2 } ] @@ -418,19 +408,51 @@ mod tests { stakes: vec![ &env, latest_stake::Stake { - stake: 7_500_000_000, + stake: 15_000_000_000, stake_timestamp: DAY_AS_SECONDS * 2 } ] } ); + // 30 days pass by and this time users directly unbond 1/2 which should also get their + // rewards + env.ledger() + .with_mut(|li| li.timestamp += 30 * DAY_AS_SECONDS); + + latest_stake_client.unbond_deprecated(&user_1, &10000000000, &(172800)); + latest_stake_client.unbond_deprecated(&user_2, &20000000000, &(172800)); + latest_stake_client.unbond_deprecated(&user_3, &15000000000, &(172800)); + + assert_eq!( + latest_stake_client.query_staked(&user_1), + latest_stake::StakedResponse { + stakes: vec![&env,] + } + ); + + assert_eq!( + latest_stake_client.query_staked(&user_2), + latest_stake::StakedResponse { + stakes: vec![&env,] + } + ); + + assert_eq!( + latest_stake_client.query_staked(&user_3), + latest_stake::StakedResponse { + stakes: vec![&env,] + } + ); + // one more day passes by and new_user decides to stake env.ledger().with_mut(|li| li.timestamp += DAY_AS_SECONDS); lp_token_client.mint(&new_user, &10_000_000_000_000); + soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); latest_stake_client.bond(&new_user, &10_000_000_000); // new_user also bonds 1,000 tokens + soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); // two months pass by env.ledger() From 0c39fb9700cc74684ef71c6ef1c7a2b4214b4e55 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:00:46 +0200 Subject: [PATCH 28/34] adds migration for the distributions --- contracts/stake/src/contract.rs | 21 +++++++++++++++++++++ contracts/stake/src/tests/setup.rs | 22 +++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/contracts/stake/src/contract.rs b/contracts/stake/src/contract.rs index 2a324cabc..0f75dfed0 100644 --- a/contracts/stake/src/contract.rs +++ b/contracts/stake/src/contract.rs @@ -762,6 +762,27 @@ impl Staking { env.deployer().update_current_contract_wasm(new_wasm_hash); } + + #[allow(dead_code)] + pub fn migrate_distributions(env: Env) { + let distributions = get_distributions(&env); + + distributions.iter().for_each(|distribution_addr| { + save_distribution( + &env, + &distribution_addr, + &Distribution { + shares_per_point: 1u128, + shares_leftover: 0u64, + distributed_total: 0u128, + withdrawable_total: 0u128, + max_bonus_bps: 0u64, + bonus_per_day_bps: 0u64, + }, + ); + save_reward_curve(&env, distribution_addr, &Curve::Constant(0)); + }) + } } // Function to remove a stake from the vector diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 520198fe2..a65c011c1 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -283,10 +283,12 @@ mod tests { let new_stake_wasm = install_stake_wasm(&env); old_stake_client.update(&new_stake_wasm); - //old_stake_client.update(&new_stake_wasm); let latest_stake_client = latest_stake::Client::new(&env, &stake_addr); + // now we migrate the distributions + latest_stake_client.migrate_distributions(); + // check the rewards again, this time with the old deprecated method assert_eq!( latest_stake_client.query_withdrawable_rewards_dep(&user_1), @@ -450,9 +452,7 @@ mod tests { lp_token_client.mint(&new_user, &10_000_000_000_000); - soroban_sdk::testutils::arbitrary::std::dbg!("BEFORE"); latest_stake_client.bond(&new_user, &10_000_000_000); // new_user also bonds 1,000 tokens - soroban_sdk::testutils::arbitrary::std::dbg!("AFTER"); // two months pass by env.ledger() @@ -460,5 +460,21 @@ mod tests { // distribute and take the rewards latest_stake_client.distribute_rewards(); + + assert_eq!( + latest_stake_client.query_withdrawable_rewards(&new_user), + latest_stake::WithdrawableRewardsResponse { + rewards: vec![ + &env, + latest_stake::WithdrawableReward { + reward_address: reward_token_addr.clone(), + reward_amount: 5_000_000, + } + ] + } + ); + + latest_stake_client.withdraw_rewards(&new_user); + assert_eq!(reward_token_client.balance(&new_user), 5_000_000); } } From 3c366907674cb0ebbe3b1e5aac4a2d1c627b378b Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:41:51 +0200 Subject: [PATCH 29/34] migration test in bash --- .../phoenix_stake_rewards.wasm | Bin 43579 -> 0 bytes contracts/stake/src/tests/migration_test.sh | 325 ++++++++++++++++++ 2 files changed, 325 insertions(+) delete mode 100755 .artifacts_stake_migration_test/phoenix_stake_rewards.wasm create mode 100755 contracts/stake/src/tests/migration_test.sh diff --git a/.artifacts_stake_migration_test/phoenix_stake_rewards.wasm b/.artifacts_stake_migration_test/phoenix_stake_rewards.wasm deleted file mode 100755 index e5af7ce5e081b10b1e7e455e16d3eabcb336e6ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43579 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 diff --git a/contracts/stake/src/tests/migration_test.sh b/contracts/stake/src/tests/migration_test.sh new file mode 100755 index 000000000..7bf5e6fa5 --- /dev/null +++ b/contracts/stake/src/tests/migration_test.sh @@ -0,0 +1,325 @@ +#!/bin/bash +set -e + +# Check for identity string argument +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Configuration +NETWORK="testnet" +IDENTITY="$1" +ADMIN_ADDR=$(soroban keys address $IDENTITY) +DAY_SECONDS=86400 + +# Cleanup previous deployment +rm -rf .stellar + +echo "1. Building and optimizing contracts..." +make build > /dev/null +soroban contract optimize --wasm target/wasm32-unknown-unknown/release/phoenix_stake.wasm +soroban contract optimize --wasm .artifacts_stake_migration_test/old_phoenix_stake.wasm +echo "Contracts optimized" + +echo "2. Deploying old stake contract..." +OLD_STAKE_WASM_HASH=$(soroban contract install \ + --wasm .artifacts_stake_migration_test/old_phoenix_stake.wasm \ + --source $IDENTITY \ + --network $NETWORK) +STAKE_ADDR=$(soroban contract deploy \ + --wasm-hash $OLD_STAKE_WASM_HASH \ + --source $IDENTITY \ + --network $NETWORK) +echo "Old Stake deployed at: $STAKE_ADDR" + +echo "3. Deploying LP and Reward tokens..." +LP_TOKEN_ADDR=$(soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_token_contract.wasm \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + --admin $ADMIN_ADDR \ + --decimal 7 \ + --name LPToken \ + --symbol LPT) +echo "LP TOKEN ADDRESS: $LP_TOKEN_ADDR" + +REWARD_TOKEN_ADDR=$(soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/soroban_token_contract.wasm \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + --admin $ADMIN_ADDR \ + --decimal 7 \ + --name RewardToken \ + --symbol RWT +) +echo "REWARD TOKEN ADDRESS : $REWARD_TOKEN_ADDR" + +echo "Minting rewards tokens to manager" +soroban contract invoke \ + --id $REWARD_TOKEN_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + mint --to $ADMIN_ADDR --amount 100000000000000 + +echo "4. Initializing old stake contract..." +soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + initialize \ + --admin $ADMIN_ADDR \ + --lp_token $LP_TOKEN_ADDR \ + --min_bond 100 \ + --min_reward 50 \ + --manager $ADMIN_ADDR \ + --owner $ADMIN_ADDR \ + --max_complexity 7 + + +echo "5. Creating test users..." +USER1=$(soroban keys address user1 2>/dev/null || { soroban keys generate user1 --network $NETWORK --fund >/dev/null 2>&1; soroban keys address user1; }) +USER2=$(soroban keys address user2 2>/dev/null || { soroban keys generate user2 --network $NETWORK --fund >/dev/null 2>&1; soroban keys address user2; }) +USER3=$(soroban keys address user3 2>/dev/null || { soroban keys generate user3 --network $NETWORK --fund >/dev/null 2>&1; soroban keys address user3; }) +NEW_USER=$(soroban keys address new_user 2>/dev/null || { soroban keys generate new_user --network $NETWORK --fund >/dev/null 2>&1; soroban keys address new_user; }) + +echo "USER1: $USER1" +echo "USER2: $USER2" +echo "USER3: $USER3" +echo "NEW_USER: $NEW_USER" + +for user in $USER1 $USER2 $USER3 $NEW_USER; do + echo "🤑 Will mint to $user" + soroban contract invoke \ + --id $LP_TOKEN_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + mint --to $user --amount 10000000000000 +done + +echo "6. Creating distribution flow..." +soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + create_distribution_flow \ + --sender $ADMIN_ADDR \ + --asset $REWARD_TOKEN_ADDR + +echo "7. Bonding tokens..." +bond_tokens() { + local user=$1 + local amount=$2 + soroban contract invoke \ + --id $STAKE_ADDR \ + --source $user \ + --network $NETWORK \ + -- \ + bond \ + --sender $(soroban keys secret $user) \ + --tokens $amount +} + +bond_tokens user1 10000000000 # 1_000 tokens +bond_tokens user2 20000000000 # 2_000 tokens +bond_tokens user3 15000000000 # 1_500 tokens + +echo "8. Verifying initial stakes..." +verify_stake() { + local user=$1 + local expected=$2 + local stakes=$(soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + query_staked \ + --address $user | jq '.stakes[0].stake') + + if [ $((stakes)) -ne $((expected)) ]; then + echo "Stake verification failed for $user: expected $expected, got $stakes" + exit 1 + fi +} + +verify_stake user1 10000000000 +verify_stake user2 20000000000 +verify_stake user3 15000000000 + +echo "9. Distributing initial rewards..." +# soroban contract invoke \ +# --id $REWARD_TOKEN_ADDR \ +# --source $IDENTITY \ +# --network $NETWORK \ +# -- \ +# mint --to $STAKE_ADDR --amount 100000000000000 + +soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + distribute_rewards \ + --sender $ADMIN_ADDR \ + --amount 10000000 \ + --reward_token $REWARD_TOKEN_ADDR + +echo "10. Verifying initial rewards..." +verify_rewards() { + local user=$1 + local expected=$2 + local rewards=$(soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + query_withdrawable_rewards \ + --address $user | jq '.rewards[0].reward_amount') + + if [ "$rewards" -ne "$expected" ]; then + echo "Reward verification failed for $user: expected $expected, got $rewards" + exit 1 + fi +} + +verify_rewards user1 1111111 +verify_rewards user2 2222222 +verify_rewards user3 1666666 + +echo "11. Upgrading stake contract..." +NEW_STAKE_WASM_HASH=$(soroban contract install \ + --wasm target/wasm32-unknown-unknown/release/phoenix_stake.wasm \ + --source $IDENTITY \ + --network $NETWORK) + +soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + update \ + --new_wasm_hash $NEW_STAKE_WASM_HASH + +echo "12. Migrating distributions..." +soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + migrate_distributions + +echo "13. Withdrawing rewards..." +withdraw_rewards() { + local user=$1 + soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + withdraw_rewards_deprecated \ + --sender $(soroban keys secret $user) +} + +withdraw_rewards user1 +withdraw_rewards user2 +withdraw_rewards user3 + +echo "14. Verifying withdrawn balances..." +verify_balance() { + local user=$1 + local expected=$2 + local balance=$(soroban contract invoke \ + --id $REWARD_TOKEN_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + balance --id $user) + + if [ "$balance" -ne $expected ]; then + echo "Balance verification failed for $user: expected $expected, got $balance" + exit 1 + fi +} + +verify_balance user1 1111111 +verify_balance user2 2222222 +verify_balance user3 1666666 + +echo "15. Unbonding tokens with deprecated API..." +unbond_tokens() { + local user=$1 + local amount=$2 + + STAKE_TIMESTAMP=$(soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + query_staked --address $user | jq '.stakes[0].stake_timestamp') + + soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + unbond_deprecated \ + --sender $(soroban keys secret $user) \ + --stake_amount $amount \ + --stake_timestamp $STAKE_TIMESTAMP +} + + +unbond_tokens user1 10000000000 +unbond_tokens user2 20000000000 +unbond_tokens user3 15000000000 + +echo "16. Verifying empty stakes..." +verify_empty_stakes() { + local user=$1 + local stakes=$(soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + query_staked --address $user | jq '.stakes | length') + + if [ "$stakes" -ne 0 ]; then + echo "Unbond failed for $user, stakes remaining: $stakes" + exit 1 + fi +} + +verify_empty_stakes user1 +verify_empty_stakes user2 +verify_empty_stakes user3 + +echo "17. New user interaction..." +bond_tokens new_user 10000000000 + +echo "18. Final rewards check..." +soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + distribute_rewards + +final_rewards=$(soroban contract invoke \ + --id $STAKE_ADDR \ + --source $IDENTITY \ + --network $NETWORK \ + -- \ + query_withdrawable_rewards --user $NEW_USER | jq -r '.rewards[0].reward_amount') + +if [ $((final_rewards)) -ne 5000000 ]; then + echo "Final rewards check failed: expected 5000000 got $final_rewards" + exit 1 +fi + +echo "All tests completed successfully!" From 106fb6235d68b4ef18f35603a7fd393119f104c2 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:43:07 +0200 Subject: [PATCH 30/34] [no ci] gitignore update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 29304db24..9b33a2e64 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ target/ cobertura.xml **/test_snapshots +.stellar/ From cc5063a9b6ee100a0d56ea8991e149ed108cfe76 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:02:56 +0200 Subject: [PATCH 31/34] migration bash --- contracts/stake/src/tests/migration_test.sh | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/contracts/stake/src/tests/migration_test.sh b/contracts/stake/src/tests/migration_test.sh index 7bf5e6fa5..325ac9e2b 100755 --- a/contracts/stake/src/tests/migration_test.sh +++ b/contracts/stake/src/tests/migration_test.sh @@ -140,7 +140,7 @@ verify_stake() { --network $NETWORK \ -- \ query_staked \ - --address $user | jq '.stakes[0].stake') + --address $user | jq -r '.stakes[0].stake') if [ $((stakes)) -ne $((expected)) ]; then echo "Stake verification failed for $user: expected $expected, got $stakes" @@ -180,9 +180,9 @@ verify_rewards() { --network $NETWORK \ -- \ query_withdrawable_rewards \ - --address $user | jq '.rewards[0].reward_amount') + --user $user | jq '.rewards[0].reward_amount') - if [ "$rewards" -ne "$expected" ]; then + if [ $((rewards)) -ne $((expected)) ]; then echo "Reward verification failed for $user: expected $expected, got $rewards" exit 1 fi @@ -239,7 +239,7 @@ verify_balance() { --source $IDENTITY \ --network $NETWORK \ -- \ - balance --id $user) + balance --id $user | jq - r '.') if [ "$balance" -ne $expected ]; then echo "Balance verification failed for $user: expected $expected, got $balance" @@ -261,7 +261,7 @@ unbond_tokens() { --source $IDENTITY \ --network $NETWORK \ -- \ - query_staked --address $user | jq '.stakes[0].stake_timestamp') + query_staked --address $user | jq -r '.stakes[0].stake_timestamp') soroban contract invoke \ --id $STAKE_ADDR \ @@ -287,7 +287,7 @@ verify_empty_stakes() { --source $IDENTITY \ --network $NETWORK \ -- \ - query_staked --address $user | jq '.stakes | length') + query_staked --address $user | jq -r '.stakes | length') if [ "$stakes" -ne 0 ]; then echo "Unbond failed for $user, stakes remaining: $stakes" @@ -311,14 +311,15 @@ soroban contract invoke \ distribute_rewards final_rewards=$(soroban contract invoke \ - --id $STAKE_ADDR \ + --id $REWARD_TOKEN_ADDR \ --source $IDENTITY \ --network $NETWORK \ -- \ - query_withdrawable_rewards --user $NEW_USER | jq -r '.rewards[0].reward_amount') + balance --id $NEW_USER | jq -r '.') -if [ $((final_rewards)) -ne 5000000 ]; then - echo "Final rewards check failed: expected 5000000 got $final_rewards" +## since we're in testnet and we cannot forward time to generate rewards we can just assume that there are 0 rewards after bonding +if [ $((final_rewards)) -ne 0 ]; then + echo "Final rewards check failed: expected 0 got $final_rewards" exit 1 fi From 8785bc3a9762d01bb51d32b833ec9d9696c19bbf Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:42:14 +0200 Subject: [PATCH 32/34] tesyts --- contracts/stake/src/tests/setup.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index a65c011c1..f959d6ce6 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -452,6 +452,7 @@ mod tests { lp_token_client.mint(&new_user, &10_000_000_000_000); + let time_of_bond = env.ledger().timestamp(); latest_stake_client.bond(&new_user, &10_000_000_000); // new_user also bonds 1,000 tokens // two months pass by @@ -476,5 +477,7 @@ mod tests { latest_stake_client.withdraw_rewards(&new_user); assert_eq!(reward_token_client.balance(&new_user), 5_000_000); + + latest_stake_client.unbond(&new_user, &10_000_000_000, &time_of_bond); } } From fab4a5f16d7143d9bab7bb22b67df504cd87342b Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:59:03 +0200 Subject: [PATCH 33/34] adds finala assertion --- contracts/stake/src/tests/setup.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index f959d6ce6..898ae3427 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -479,5 +479,6 @@ mod tests { assert_eq!(reward_token_client.balance(&new_user), 5_000_000); latest_stake_client.unbond(&new_user, &10_000_000_000, &time_of_bond); + assert_eq!(lp_token_client.balance(&new_user), 10_000_000_000); } } From 62c48bc2c460f6a7e21054625de8e2ebd2b22a67 Mon Sep 17 00:00:00 2001 From: gangov <6922910+gangov@users.noreply.github.com> Date: Tue, 4 Feb 2025 21:27:45 +0200 Subject: [PATCH 34/34] adds missing 0 --- contracts/stake/src/tests/setup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/stake/src/tests/setup.rs b/contracts/stake/src/tests/setup.rs index 898ae3427..899174739 100644 --- a/contracts/stake/src/tests/setup.rs +++ b/contracts/stake/src/tests/setup.rs @@ -479,6 +479,6 @@ mod tests { assert_eq!(reward_token_client.balance(&new_user), 5_000_000); latest_stake_client.unbond(&new_user, &10_000_000_000, &time_of_bond); - assert_eq!(lp_token_client.balance(&new_user), 10_000_000_000); + assert_eq!(lp_token_client.balance(&new_user), 10_000_000_000_000); } }