From 3c643c8dd6f51559c643ceaa5961aa9c7b3efb78 Mon Sep 17 00:00:00 2001 From: Nikita Jerschow Date: Mon, 18 Sep 2023 09:37:50 -0400 Subject: [PATCH] Fix/cl-vault account for unused funds (#501) ## 1. Overview Changing the CL Vault contract to also use any funds that aren't being saved as rewards ## 2. Implementation details Added a function that gets the usable balance of the contract ## 3. How to test/use ## 4. Checklist - [ ] Does the Readme need to be updated? ## 5. Limitations (optional) ## 6. Future Work (optional) --------- Co-authored-by: LaurensKubat <32776056+LaurensKubat@users.noreply.github.com> Co-authored-by: magiodev.eth <31893902+magiodev@users.noreply.github.com> --- .../contracts/cl-vault/src/contract.rs | 6 +- .../contracts/cl-vault/src/helpers.rs | 187 ++++++++++- .../contracts/cl-vault/src/instantiate.rs | 4 +- .../contracts/cl-vault/src/math/tick.rs | 1 - .../contracts/cl-vault/src/query.rs | 6 +- .../cl-vault/src/rewards/distribution.rs | 14 +- .../contracts/cl-vault/src/rewards/helpers.rs | 65 ++-- .../contracts/cl-vault/src/state.rs | 9 +- .../contracts/cl-vault/src/test_helpers.rs | 297 +++++++++++++----- .../src/test_tube/deposit_withdraw.rs | 178 ++++++++++- .../cl-vault/src/test_tube/initialize.rs | 2 +- .../cl-vault/src/test_tube/proptest.rs | 6 +- .../contracts/cl-vault/src/test_tube/range.rs | 6 - .../cl-vault/src/test_tube/rewards.rs | 2 - .../contracts/cl-vault/src/vault/admin.rs | 11 +- .../contracts/cl-vault/src/vault/claim.rs | 17 +- .../src/vault/concentrated_liquidity.rs | 1 - .../contracts/cl-vault/src/vault/deposit.rs | 39 ++- .../contracts/cl-vault/src/vault/merge.rs | 1 - .../contracts/cl-vault/src/vault/range.rs | 192 ++++++----- .../contracts/cl-vault/src/vault/withdraw.rs | 233 ++++++++++++-- 21 files changed, 991 insertions(+), 286 deletions(-) diff --git a/smart-contracts/contracts/cl-vault/src/contract.rs b/smart-contracts/contracts/cl-vault/src/contract.rs index 2a2d54064..f65038df2 100644 --- a/smart-contracts/contracts/cl-vault/src/contract.rs +++ b/smart-contracts/contracts/cl-vault/src/contract.rs @@ -61,7 +61,7 @@ pub fn execute( execute_exact_deposit(deps, env, info, recipient) } cw_vault_multi_standard::VaultStandardExecuteMsg::Redeem { recipient, amount } => { - execute_withdraw(deps, env, info, recipient, amount) + execute_withdraw(deps, env, info, recipient, amount.into()) } cw_vault_multi_standard::VaultStandardExecuteMsg::VaultExtension(vault_msg) => { match vault_msg { @@ -86,7 +86,7 @@ pub fn execute( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { match msg { cw_vault_multi_standard::VaultStandardQueryMsg::VaultStandardInfo {} => todo!(), cw_vault_multi_standard::VaultStandardQueryMsg::Info {} => { @@ -96,7 +96,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { cw_vault_multi_standard::VaultStandardQueryMsg::DepositRatio => todo!(), cw_vault_multi_standard::VaultStandardQueryMsg::PreviewRedeem { amount: _ } => todo!(), cw_vault_multi_standard::VaultStandardQueryMsg::TotalAssets {} => { - Ok(to_binary(&query_total_assets(deps, env)?)?) + Ok(to_binary(&query_total_assets(deps)?)?) } cw_vault_multi_standard::VaultStandardQueryMsg::TotalVaultTokenSupply {} => { Ok(to_binary(&query_total_vault_token_supply(deps)?)?) diff --git a/smart-contracts/contracts/cl-vault/src/helpers.rs b/smart-contracts/contracts/cl-vault/src/helpers.rs index ba72df12f..399c4b688 100644 --- a/smart-contracts/contracts/cl-vault/src/helpers.rs +++ b/smart-contracts/contracts/cl-vault/src/helpers.rs @@ -1,12 +1,15 @@ use std::str::FromStr; use crate::math::tick::tick_to_price; -use crate::state::ADMIN_ADDRESS; +use crate::rewards::CoinList; +use crate::state::{ADMIN_ADDRESS, STRATEGIST_REWARDS, USER_REWARDS}; +use crate::vault::concentrated_liquidity::{get_cl_pool_info, get_position}; use crate::{error::ContractResult, state::POOL_CONFIG, ContractError}; use cosmwasm_std::{ - coin, Addr, Coin, Decimal, Decimal256, Deps, Fraction, MessageInfo, QuerierWrapper, Storage, - Uint128, Uint256, + coin, Addr, Coin, Decimal, Decimal256, Deps, DepsMut, Env, Fraction, MessageInfo, + QuerierWrapper, Storage, Uint128, Uint256, }; + use osmosis_std::types::osmosis::poolmanager::v1beta1::PoolmanagerQuerier; /// returns the Coin of the needed denoms in the order given in denoms @@ -244,6 +247,184 @@ pub fn sort_tokens(tokens: Vec) -> Vec { sorted_tokens } +/// this function subtracts out anything from the raw contract balance that isn't dedicated towards user or strategist rewards. +/// this function is expensive. +pub fn get_unused_balances( + storage: &dyn Storage, + querier: &QuerierWrapper, + env: &Env, +) -> Result { + let mut balances = + CoinList::from_coins(querier.query_all_balances(env.contract.address.to_string())?); + + // subtract out strategist rewards and all user rewards + let strategist_rewards = STRATEGIST_REWARDS.load(storage)?; + + balances.sub(&strategist_rewards)?; + + for user_reward in USER_REWARDS.range(storage, None, None, cosmwasm_std::Order::Ascending) { + balances.sub(&user_reward?.1)?; + } + + Ok(balances) +} + +pub fn get_max_utilization_for_ratio( + token0: Uint256, + token1: Uint256, + ratio: Decimal256, +) -> Result<(Uint256, Uint256), ContractError> { + // maxdep1 = T0 / R + let max_deposit1_from_0 = + token0.checked_multiply_ratio(ratio.denominator(), ratio.numerator())?; + // maxdep0 = T1 * R + let max_deposit0_from_1 = + token1.checked_multiply_ratio(ratio.numerator(), ratio.denominator())?; + + if max_deposit0_from_1 > token0 { + Ok((token0, max_deposit1_from_0)) + } else if max_deposit1_from_0 > token1 { + Ok((max_deposit0_from_1, token1)) + } else { + Ok((token0, token1)) + } +} + +pub fn get_liquidity_amount_for_unused_funds( + deps: DepsMut, + env: &Env, + additional_excluded_funds: (Uint128, Uint128), +) -> Result { + // first get the ratio of token0:token1 in the position. + let p = get_position(deps.storage, &deps.querier)?; + // if there is no position, then we can assume that there are 0 unused funds + if p.position.is_none() { + return Ok(Decimal256::zero()); + } + let position_unwrapped = p.position.unwrap(); + let token0: Coin = p.asset0.unwrap().try_into()?; + let token1: Coin = p.asset1.unwrap().try_into()?; + // if any of the values are 0, we fill 1 + let ratio = if token0.amount.is_zero() { + Decimal256::from_ratio(1_u128, token1.amount) + } else if token1.amount.is_zero() { + Decimal256::from_ratio(token0.amount, 1_u128) + } else { + Decimal256::from_ratio(token0.amount, token1.amount) + }; + let pool_config = POOL_CONFIG.load(deps.storage)?; + let pool_details = get_cl_pool_info(&deps.querier, pool_config.pool_id)?; + + // then figure out based on current unused balance, what the max initial deposit could be + // (with the ratio, what is the max tokens we can deposit) + let tokens = get_unused_balances(deps.storage, &deps.querier, env)?; + let unused_t0: Uint256 = tokens + .find_coin(token0.denom) + .amount + .checked_sub(additional_excluded_funds.0)? + .into(); + let unused_t1: Uint256 = tokens + .find_coin(token1.denom) + .amount + .checked_sub(additional_excluded_funds.1)? + .into(); + + let max_initial_deposit = get_max_utilization_for_ratio(unused_t0, unused_t1, ratio)?; + + // then figure out how much liquidity this would give us. + // Formula: current_position_liquidity * token0_initial_deposit_amount / token0_in_current_position + // EDGE CASE: what if it's a one-sided position with only token1? + // SOLUTION: take whichever token is greater than the other to plug into the formula 1 line above + let position_liquidity = Decimal256::from_str(&position_unwrapped.liquidity)?; + let max_initial_deposit_liquidity = if token0.amount > token1.amount { + position_liquidity + .checked_mul(Decimal256::new(max_initial_deposit.0))? + .checked_div(Decimal256::new(token0.amount.into()))? + } else { + position_liquidity + .checked_mul(Decimal256::new(max_initial_deposit.1))? + .checked_div(Decimal256::new(token1.amount.into()))? + }; + + // subtract out the max deposit from both tokens, which will leave us with only one token, lets call this leftover_balance0 or 1 + let leftover_balance0 = unused_t0.checked_sub(max_initial_deposit.0)?; + let leftover_balance1 = unused_t1.checked_sub(max_initial_deposit.1)?; + + // call get_single_sided_deposit_0_to_1_swap_amount or get_single_sided_deposit_1_to_0_swap_amount to see how much we would swap to enter with the rest of our funds + let post_swap_liquidity = if leftover_balance0 > leftover_balance1 { + let swap_amount = if pool_details.current_tick > position_unwrapped.upper_tick { + leftover_balance0.try_into().unwrap() + } else { + get_single_sided_deposit_0_to_1_swap_amount( + leftover_balance0.try_into().unwrap(), + position_unwrapped.lower_tick, + pool_details.current_tick, + position_unwrapped.upper_tick, + )? + }; + // let swap_amount = get_single_sided_deposit_0_to_1_swap_amount( + // leftover_balance0.try_into().unwrap(), + // position_unwrapped.lower_tick, + // pool_details.current_tick, + // position_unwrapped.upper_tick, + // )?; + + // subtract the resulting swap_amount from leftover_balance0 or 1, we can then use the same formula as above to get the correct liquidity amount. + // we are also mindful of the same edge case + let leftover_balance0 = leftover_balance0.checked_sub(swap_amount.into())?; + + if leftover_balance0.is_zero() { + // in this case we need to get the expected token1 from doing a full swap, meaning we need to multiply by the spot price + let token1_from_swap_amount = Decimal256::new(swap_amount.into()) + .checked_mul(tick_to_price(pool_details.current_tick)?)?; + position_liquidity + .checked_mul(token1_from_swap_amount)? + .checked_div(Decimal256::new(token1.amount.into()))? + } else { + position_liquidity + .checked_mul(Decimal256::new(leftover_balance0))? + .checked_div(Decimal256::new(token0.amount.into()))? + } + } else { + let swap_amount = if pool_details.current_tick < position_unwrapped.lower_tick { + leftover_balance1.try_into().unwrap() + } else { + get_single_sided_deposit_1_to_0_swap_amount( + leftover_balance1.try_into().unwrap(), + position_unwrapped.lower_tick, + pool_details.current_tick, + position_unwrapped.upper_tick, + )? + }; + // let swap_amount = get_single_sided_deposit_1_to_0_swap_amount( + // leftover_balance1.try_into().unwrap(), + // position_unwrapped.lower_tick, + // pool_details.current_tick, + // position_unwrapped.upper_tick, + // )?; + + // subtract the resulting swap_amount from leftover_balance0 or 1, we can then use the same formula as above to get the correct liquidity amount. + // we are also mindful of the same edge case + let leftover_balance1 = leftover_balance1.checked_sub(swap_amount.into())?; + + if leftover_balance1.is_zero() { + // in this case we need to get the expected token0 from doing a full swap, meaning we need to multiply by the spot price + let token0_from_swap_amount = Decimal256::new(swap_amount.into()) + .checked_div(tick_to_price(pool_details.current_tick)?)?; + position_liquidity + .checked_mul(token0_from_swap_amount)? + .checked_div(Decimal256::new(token0.amount.into()))? + } else { + position_liquidity + .checked_mul(Decimal256::new(leftover_balance1))? + .checked_div(Decimal256::new(token1.amount.into()))? + } + }; + + // add together the liquidity from the initial deposit and the swap deposit and return that + Ok(max_initial_deposit_liquidity.checked_add(post_swap_liquidity)?) +} + #[cfg(test)] mod tests { diff --git a/smart-contracts/contracts/cl-vault/src/instantiate.rs b/smart-contracts/contracts/cl-vault/src/instantiate.rs index c845662ec..cd0263f41 100644 --- a/smart-contracts/contracts/cl-vault/src/instantiate.rs +++ b/smart-contracts/contracts/cl-vault/src/instantiate.rs @@ -13,7 +13,7 @@ use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ use crate::helpers::must_pay_one_or_two; use crate::msg::InstantiateMsg; use crate::reply::Replies; -use crate::rewards::Rewards; +use crate::rewards::CoinList; use crate::state::{ Metadata, PoolConfig, Position, ADMIN_ADDRESS, METADATA, POOL_CONFIG, POSITION, RANGE_ADMIN, STRATEGIST_REWARDS, VAULT_CONFIG, VAULT_DENOM, @@ -54,7 +54,7 @@ pub fn handle_instantiate( }, )?; - STRATEGIST_REWARDS.save(deps.storage, &Rewards::new())?; + STRATEGIST_REWARDS.save(deps.storage, &CoinList::new())?; METADATA.save( deps.storage, diff --git a/smart-contracts/contracts/cl-vault/src/math/tick.rs b/smart-contracts/contracts/cl-vault/src/math/tick.rs index e194c7292..39d823bc5 100644 --- a/smart-contracts/contracts/cl-vault/src/math/tick.rs +++ b/smart-contracts/contracts/cl-vault/src/math/tick.rs @@ -229,7 +229,6 @@ mod tests { let tick_index = 27445000_i128; let _expected_price = Decimal256::from_str("30352").unwrap(); let price = tick_to_price(tick_index.try_into().unwrap()).unwrap(); - println!("{:?}", price.to_string()); // assert_eq!(price, expected_price); let tick = price_to_tick(deps.as_mut().storage, price).unwrap(); assert_eq!(tick_index, tick) diff --git a/smart-contracts/contracts/cl-vault/src/query.rs b/smart-contracts/contracts/cl-vault/src/query.rs index c582f6324..2f95d2d7c 100644 --- a/smart-contracts/contracts/cl-vault/src/query.rs +++ b/smart-contracts/contracts/cl-vault/src/query.rs @@ -7,7 +7,7 @@ use crate::{ }, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{coin, Coin, Deps, Env, Uint128}; +use cosmwasm_std::{coin, Coin, Deps, Uint128}; use cw_vault_multi_standard::VaultInfoResponse; use osmosis_std::types::cosmos::bank::v1beta1::BankQuerier; @@ -119,8 +119,8 @@ pub fn query_user_rewards(deps: Deps, user: String) -> ContractResult ContractResult { - let position = get_position(deps.storage, &deps.querier, &env)?; +pub fn query_total_assets(deps: Deps) -> ContractResult { + let position = get_position(deps.storage, &deps.querier)?; let pool = POOL_CONFIG.load(deps.storage)?; Ok(TotalAssetsResponse { token0: position diff --git a/smart-contracts/contracts/cl-vault/src/rewards/distribution.rs b/smart-contracts/contracts/cl-vault/src/rewards/distribution.rs index 75ed291bd..4ecae897c 100644 --- a/smart-contracts/contracts/cl-vault/src/rewards/distribution.rs +++ b/smart-contracts/contracts/cl-vault/src/rewards/distribution.rs @@ -20,11 +20,11 @@ use osmosis_std::types::{ }, }; -use super::helpers::Rewards; +use super::helpers::CoinList; /// claim_rewards claims rewards from Osmosis and update the rewards map to reflect each users rewards pub fn execute_distribute_rewards(deps: DepsMut, env: Env) -> Result { - CURRENT_REWARDS.save(deps.storage, &Rewards::new())?; + CURRENT_REWARDS.save(deps.storage, &CoinList::new())?; let msg = collect_incentives(deps.as_ref(), env)?; Ok(Response::new().add_submessage(SubMsg::reply_on_success( @@ -53,7 +53,7 @@ pub fn handle_collect_incentives_reply( let response: MsgCollectIncentivesResponse = data?; CURRENT_REWARDS.update( deps.storage, - |mut rewards| -> Result { + |mut rewards| -> Result { rewards.update_rewards(response.collected_incentives)?; Ok(rewards) }, @@ -96,7 +96,7 @@ pub fn handle_collect_spread_rewards_reply( fn distribute_rewards( mut deps: DepsMut, - mut rewards: Rewards, + mut rewards: CoinList, ) -> Result, ContractError> { if rewards.is_empty() { return Ok(vec![Attribute::new("total_rewards_amount", "0")]); @@ -121,9 +121,9 @@ fn distribute_rewards( // for each user with locked tokens, we distribute some part of the rewards to them // get all users and their current pre-distribution rewards - let user_rewards: Result, ContractError> = SHARES + let user_rewards: Result, ContractError> = SHARES .range(deps.branch().storage, None, None, Order::Ascending) - .map(|v| -> Result<(Addr, Rewards), ContractError> { + .map(|v| -> Result<(Addr, CoinList), ContractError> { let (address, user_shares) = v?; // calculate the amount of each asset the user should get in rewards // we need to always round down here, so we never expect more rewards than we have @@ -136,7 +136,7 @@ fn distribute_rewards( user_rewards? .into_iter() .try_for_each(|(addr, reward)| -> ContractResult<()> { - USER_REWARDS.update(deps.storage, addr, |old| -> ContractResult { + USER_REWARDS.update(deps.storage, addr, |old| -> ContractResult { if let Some(old_user_rewards) = old { Ok(reward.add(old_user_rewards)?) } else { diff --git a/smart-contracts/contracts/cl-vault/src/rewards/helpers.rs b/smart-contracts/contracts/cl-vault/src/rewards/helpers.rs index 6115ee483..c62470e4f 100644 --- a/smart-contracts/contracts/cl-vault/src/rewards/helpers.rs +++ b/smart-contracts/contracts/cl-vault/src/rewards/helpers.rs @@ -5,16 +5,16 @@ use crate::{error::ContractResult, helpers::sort_tokens}; use osmosis_std::types::cosmos::base::v1beta1::Coin as OsmoCoin; #[cw_serde] #[derive(Default)] -pub struct Rewards(Vec); +pub struct CoinList(Vec); -impl Rewards { - pub fn new() -> Rewards { - Rewards::default() +impl CoinList { + pub fn new() -> CoinList { + CoinList::default() } /// calculates the ratio of the current rewards - pub fn ratio(&self, ratio: Decimal) -> Rewards { - Rewards( + pub fn ratio(&self, ratio: Decimal) -> CoinList { + CoinList( self.0 .iter() .map(|c| { @@ -46,7 +46,7 @@ impl Rewards { } /// add rewards to self and mutate self - pub fn add(mut self, rewards: Rewards) -> ContractResult { + pub fn add(mut self, rewards: CoinList) -> ContractResult { self.merge(rewards.coins())?; Ok(self) } @@ -64,7 +64,7 @@ impl Rewards { } /// substract a percentage from self, mutate self and return the subtracted rewards - pub fn sub_ratio(&mut self, ratio: Decimal) -> ContractResult { + pub fn sub_ratio(&mut self, ratio: Decimal) -> ContractResult { let to_sub = self.ratio(ratio); // actually subtract the funds @@ -75,7 +75,7 @@ impl Rewards { /// subtract to_sub from self, ignores any coins in to_sub that don't exist in self and vice versa /// every item in self is expected to be greater or equal to the amount of the coin with the same denom /// in to_sub - pub fn sub(&mut self, to_sub: &Rewards) -> ContractResult<()> { + pub fn sub(&mut self, to_sub: &CoinList) -> ContractResult<()> { to_sub .0 .iter() @@ -114,7 +114,18 @@ impl Rewards { } pub fn from_coins(coins: Vec) -> Self { - Rewards(coins) + CoinList(coins) + } + + pub fn find_coin(&self, denom: String) -> Coin { + self.0 + .clone() + .into_iter() + .find(|c| c.denom == denom) + .unwrap_or(Coin { + denom, + amount: 0u128.into(), + }) } } @@ -126,7 +137,7 @@ mod tests { #[test] fn sub_works() { - let mut rewards = Rewards::new(); + let mut rewards = CoinList::new(); rewards .update_rewards(vec![ OsmoCoin { @@ -146,7 +157,7 @@ mod tests { assert_eq!( rewards, - Rewards(vec![ + CoinList(vec![ coin(1000, "uosmo"), coin(2000, "uatom"), coin(3000, "uqsr") @@ -154,12 +165,12 @@ mod tests { ); rewards - .sub(&Rewards::from_coins(vec![coin(1500, "uqsr")])) + .sub(&CoinList::from_coins(vec![coin(1500, "uqsr")])) .unwrap(); assert_eq!( rewards, - Rewards(vec![ + CoinList(vec![ coin(1000, "uosmo"), coin(2000, "uatom"), coin(1500, "uqsr") @@ -167,11 +178,11 @@ mod tests { ); rewards - .sub(&Rewards::from_coins(vec![coin(2000, "uqsr")])) + .sub(&CoinList::from_coins(vec![coin(2000, "uqsr")])) .unwrap_err(); rewards - .sub(&Rewards::from_coins(vec![ + .sub(&CoinList::from_coins(vec![ coin(999, "uqsr"), coin(999, "uosmo"), ])) @@ -179,7 +190,7 @@ mod tests { assert_eq!( rewards, - Rewards(vec![ + CoinList(vec![ coin(1, "uosmo"), coin(2000, "uatom"), coin(501, "uqsr") @@ -189,7 +200,7 @@ mod tests { #[test] fn percentage_works() { - let mut rewards = Rewards::new(); + let mut rewards = CoinList::new(); rewards .update_rewards(vec![ OsmoCoin { @@ -210,7 +221,7 @@ mod tests { let ratio = rewards.ratio(Decimal::from_ratio(Uint128::new(10), Uint128::new(100))); assert_eq!( ratio, - Rewards(vec![ + CoinList(vec![ coin(100, "uosmo"), coin(200, "uatom"), coin(300, "uqsr") @@ -220,7 +231,7 @@ mod tests { #[test] fn sub_percentage_works() { - let mut rewards = Rewards::new(); + let mut rewards = CoinList::new(); rewards .update_rewards(vec![ OsmoCoin { @@ -243,7 +254,7 @@ mod tests { .unwrap(); assert_eq!( ratio, - Rewards(vec![ + CoinList(vec![ coin(100, "uosmo"), coin(200, "uatom"), coin(300, "uqsr") @@ -251,7 +262,7 @@ mod tests { ); assert_eq!( rewards, - Rewards(vec![ + CoinList(vec![ coin(900, "uosmo"), coin(1800, "uatom"), coin(2700, "uqsr") @@ -264,7 +275,7 @@ mod tests { #[test] fn add_works() { - let mut rewards = Rewards::new(); + let mut rewards = CoinList::new(); rewards .update_rewards(vec![ OsmoCoin { @@ -282,7 +293,7 @@ mod tests { ]) .unwrap(); rewards = rewards - .add(Rewards::from_coins(vec![ + .add(CoinList::from_coins(vec![ coin(2000, "uosmo"), coin(2000, "uatom"), coin(6000, "uqsr"), @@ -291,7 +302,7 @@ mod tests { .unwrap(); assert_eq!( rewards, - Rewards::from_coins(vec![ + CoinList::from_coins(vec![ coin(3000, "uosmo"), coin(4000, "uatom"), coin(9000, "uqsr"), @@ -302,7 +313,7 @@ mod tests { #[test] fn update_rewards_works() { - let mut rewards = Rewards::new(); + let mut rewards = CoinList::new(); rewards .update_rewards(vec![ OsmoCoin { @@ -339,7 +350,7 @@ mod tests { assert_eq!( rewards, - Rewards::from_coins(vec![ + CoinList::from_coins(vec![ coin(2000, "uosmo"), coin(2000, "uatom"), coin(6000, "uqsr"), diff --git a/smart-contracts/contracts/cl-vault/src/state.rs b/smart-contracts/contracts/cl-vault/src/state.rs index 4a300d21a..8e00bbe33 100644 --- a/smart-contracts/contracts/cl-vault/src/state.rs +++ b/smart-contracts/contracts/cl-vault/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Decimal, Decimal256, Uint128}; use cw_storage_plus::{Deque, Item, Map}; -use crate::rewards::Rewards; +use crate::rewards::CoinList; use crate::vault::merge::CurrentMergeWithdraw; use crate::vault::range::SwapDirection; @@ -83,9 +83,9 @@ pub struct CurrentDeposit { pub const CURRENT_DEPOSIT: Item = Item::new("current_deposit"); /// REWARDS: Current rewards are the rewards being gathered, these can be both spread rewards as well as incentives -pub const CURRENT_REWARDS: Item = Item::new("current_rewards"); -pub const USER_REWARDS: Map = Map::new("user_rewards"); -pub const STRATEGIST_REWARDS: Item = Item::new("strategist_rewards"); +pub const CURRENT_REWARDS: Item = Item::new("current_rewards"); +pub const USER_REWARDS: Map = Map::new("user_rewards"); +pub const STRATEGIST_REWARDS: Item = Item::new("strategist_rewards"); /// CURRENT_REMAINDERS is a tuple of Uin128 containing the current remainder amount before performing a swap pub const CURRENT_REMAINDERS: Item<(Uint128, Uint128)> = Item::new("current_remainders"); @@ -125,6 +125,7 @@ pub struct TickExpIndexData { pub const TICK_EXP_CACHE: Map = Map::new("tick_exp_cache"); pub const CURRENT_WITHDRAWER: Item = Item::new("current_withdrawer"); +pub const CURRENT_WITHDRAWER_DUST: Item<(Uint128, Uint128)> = Item::new("current_withdrawer_dust"); #[cfg(test)] mod tests { diff --git a/smart-contracts/contracts/cl-vault/src/test_helpers.rs b/smart-contracts/contracts/cl-vault/src/test_helpers.rs index dd244da5a..8cdc82dbd 100644 --- a/smart-contracts/contracts/cl-vault/src/test_helpers.rs +++ b/smart-contracts/contracts/cl-vault/src/test_helpers.rs @@ -1,7 +1,9 @@ -use cosmwasm_std::testing::BankQuerier; +use std::marker::PhantomData; + +use cosmwasm_std::testing::{BankQuerier, MockApi, MockStorage, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ - from_binary, to_binary, Binary, Coin, ContractResult as CwContractResult, Empty, Querier, - QuerierResult, QueryRequest, + from_binary, to_binary, Addr, BankQuery, Binary, Coin, ContractResult as CwContractResult, + Decimal, Empty, MessageInfo, OwnedDeps, Querier, QuerierResult, QueryRequest, }; use osmosis_std::types::cosmos::bank::v1beta1::{QuerySupplyOfRequest, QuerySupplyOfResponse}; @@ -10,11 +12,12 @@ use osmosis_std::types::osmosis::poolmanager::v1beta1::{PoolResponse, SpotPriceR use osmosis_std::types::{ cosmos::base::v1beta1::Coin as OsmoCoin, osmosis::concentratedliquidity::v1beta1::{ - FullPositionBreakdown, PositionByIdRequest, PositionByIdResponse, + FullPositionBreakdown, Position as OsmoPosition, PositionByIdRequest, PositionByIdResponse, }, }; use crate::math::tick::tick_to_price; +use crate::state::{PoolConfig, VaultConfig, POOL_CONFIG, POSITION, RANGE_ADMIN, VAULT_CONFIG}; pub struct QuasarQuerier { position: FullPositionBreakdown, current_tick: i64, @@ -47,78 +50,84 @@ impl Querier for QuasarQuerier { fn raw_query(&self, bin_request: &[u8]) -> cosmwasm_std::QuerierResult { let request: QueryRequest = from_binary(&Binary::from(bin_request)).unwrap(); match request { - QueryRequest::Stargate { path, data } => { - println!("{}", path.as_str()); - match path.as_str() { - "/osmosis.concentratedliquidity.v1beta1.Query/PositionById" => { - let position_by_id_request: PositionByIdRequest = - prost::Message::decode(data.as_slice()).unwrap(); - let position_id = position_by_id_request.position_id; - if position_id == self.position.position.clone().unwrap().position_id { - QuerierResult::Ok(CwContractResult::Ok( - to_binary(&PositionByIdResponse { - position: Some(self.position.clone()), - }) - .unwrap(), - )) - } else { - QuerierResult::Err(cosmwasm_std::SystemError::UnsupportedRequest { - kind: format!("position id not found: {position_id:?}"), - }) - } - } - "/cosmos.bank.v1beta1.Query/SupplyOf" => { - let query_supply_of_request: QuerySupplyOfRequest = - prost::Message::decode(data.as_slice()).unwrap(); - let denom = query_supply_of_request.denom; - QuerierResult::Ok(CwContractResult::Ok( - to_binary(&QuerySupplyOfResponse { - amount: Some(OsmoCoin { - denom, - amount: 100.to_string(), - }), - }) - .unwrap(), - )) - } - "/osmosis.poolmanager.v1beta1.Query/Pool" => { + QueryRequest::Stargate { path, data } => match path.as_str() { + "/osmosis.concentratedliquidity.v1beta1.Query/PositionById" => { + let position_by_id_request: PositionByIdRequest = + prost::Message::decode(data.as_slice()).unwrap(); + let position_id = position_by_id_request.position_id; + if position_id == self.position.position.clone().unwrap().position_id { QuerierResult::Ok(CwContractResult::Ok( - to_binary(&PoolResponse { - pool: Some( - Pool { - address: "idc".to_string(), - incentives_address: "not being used".to_string(), - spread_rewards_address: "not being used".to_string(), - id: 1, - current_tick_liquidity: "100".to_string(), - token0: "uosmo".to_string(), - token1: "uion".to_string(), - current_sqrt_price: "not used".to_string(), - current_tick: self.current_tick, - tick_spacing: 100, - exponent_at_price_one: -6, - spread_factor: "not used".to_string(), - last_liquidity_update: None, - } - .to_any(), - ), + to_binary(&PositionByIdResponse { + position: Some(self.position.clone()), }) .unwrap(), )) + } else { + QuerierResult::Err(cosmwasm_std::SystemError::UnsupportedRequest { + kind: format!("position id not found: {position_id:?}"), + }) } - "/osmosis.poolmanager.v1beta1.Query/SpotPrice" => { - QuerierResult::Ok(CwContractResult::Ok( - to_binary(&SpotPriceResponse { - spot_price: tick_to_price(self.current_tick).unwrap().to_string(), - }) - .unwrap(), - )) - } - &_ => QuerierResult::Err(cosmwasm_std::SystemError::UnsupportedRequest { - kind: format!("Unmocked stargate query path: {path:?}"), - }), } - } + "/cosmos.bank.v1beta1.Query/SupplyOf" => { + let query_supply_of_request: QuerySupplyOfRequest = + prost::Message::decode(data.as_slice()).unwrap(); + let denom = query_supply_of_request.denom; + QuerierResult::Ok(CwContractResult::Ok( + to_binary(&QuerySupplyOfResponse { + amount: Some(OsmoCoin { + denom, + amount: 100000.to_string(), + }), + }) + .unwrap(), + )) + } + "/cosmos.bank.v1beta.Query/Balance" => { + let query: BankQuery = from_binary(&Binary::from(bin_request)).unwrap(); + self.bank.query(&query) + } + "/cosmos.bank.v1beta.Query/AllBalances" => { + let query: BankQuery = from_binary(&Binary::from(bin_request)).unwrap(); + self.bank.query(&query) + } + "/osmosis.poolmanager.v1beta1.Query/Pool" => { + QuerierResult::Ok(CwContractResult::Ok( + to_binary(&PoolResponse { + pool: Some( + Pool { + address: "idc".to_string(), + incentives_address: "not being used".to_string(), + spread_rewards_address: "not being used".to_string(), + id: 1, + current_tick_liquidity: "100".to_string(), + token0: "uosmo".to_string(), + token1: "uion".to_string(), + current_sqrt_price: "not used".to_string(), + current_tick: self.current_tick, + tick_spacing: 100, + exponent_at_price_one: -6, + spread_factor: "not used".to_string(), + last_liquidity_update: None, + } + .to_any(), + ), + }) + .unwrap(), + )) + } + "/osmosis.poolmanager.v1beta1.Query/SpotPrice" => { + QuerierResult::Ok(CwContractResult::Ok( + to_binary(&SpotPriceResponse { + spot_price: tick_to_price(self.current_tick).unwrap().to_string(), + }) + .unwrap(), + )) + } + &_ => QuerierResult::Err(cosmwasm_std::SystemError::UnsupportedRequest { + kind: format!("Unmocked stargate query path: {path:?}"), + }), + }, + QueryRequest::Bank(query) => self.bank.query(&query), _ => QuerierResult::Err(cosmwasm_std::SystemError::UnsupportedRequest { kind: format!("Unmocked query type: {request:?}"), }), @@ -126,3 +135,151 @@ impl Querier for QuasarQuerier { // QuerierResult::Ok(ContractResult::Ok(to_binary(&"hello").unwrap())) } } + +pub fn mock_deps_with_querier_with_balance( + info: &MessageInfo, + balances: &[(&str, &[Coin])], +) -> OwnedDeps { + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: QuasarQuerier::new_with_balances( + FullPositionBreakdown { + position: Some(OsmoPosition { + position_id: 1, + address: MOCK_CONTRACT_ADDR.to_string(), + pool_id: 1, + lower_tick: 100, + upper_tick: 1000, + join_time: None, + liquidity: "1000000.1".to_string(), + }), + asset0: Some(OsmoCoin { + denom: "token0".to_string(), + amount: "1000000".to_string(), + }), + asset1: Some(OsmoCoin { + denom: "token1".to_string(), + amount: "1000000".to_string(), + }), + claimable_spread_rewards: vec![ + OsmoCoin { + denom: "token0".to_string(), + amount: "100".to_string(), + }, + OsmoCoin { + denom: "token1".to_string(), + amount: "100".to_string(), + }, + ], + claimable_incentives: vec![], + forfeited_incentives: vec![], + }, + 500, + balances, + ), + custom_query_type: PhantomData, + }; + + let storage = &mut deps.storage; + + RANGE_ADMIN.save(storage, &info.sender).unwrap(); + POOL_CONFIG + .save( + storage, + &PoolConfig { + pool_id: 1, + token0: "token0".to_string(), + token1: "token1".to_string(), + }, + ) + .unwrap(); + VAULT_CONFIG + .save( + storage, + &VaultConfig { + performance_fee: Decimal::zero(), + treasury: Addr::unchecked("treasure"), + swap_max_slippage: Decimal::from_ratio(1u128, 20u128), + }, + ) + .unwrap(); + POSITION + .save(storage, &crate::state::Position { position_id: 1 }) + .unwrap(); + + deps +} + +pub fn mock_deps_with_querier( + info: &MessageInfo, +) -> OwnedDeps { + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: QuasarQuerier::new( + FullPositionBreakdown { + position: Some(OsmoPosition { + position_id: 1, + address: MOCK_CONTRACT_ADDR.to_string(), + pool_id: 1, + lower_tick: 100, + upper_tick: 1000, + join_time: None, + liquidity: "1000000.1".to_string(), + }), + asset0: Some(OsmoCoin { + denom: "token0".to_string(), + amount: "1000000".to_string(), + }), + asset1: Some(OsmoCoin { + denom: "token1".to_string(), + amount: "1000000".to_string(), + }), + claimable_spread_rewards: vec![ + OsmoCoin { + denom: "token0".to_string(), + amount: "100".to_string(), + }, + OsmoCoin { + denom: "token1".to_string(), + amount: "100".to_string(), + }, + ], + claimable_incentives: vec![], + forfeited_incentives: vec![], + }, + 500, + ), + custom_query_type: PhantomData, + }; + + let storage = &mut deps.storage; + + RANGE_ADMIN.save(storage, &info.sender).unwrap(); + POOL_CONFIG + .save( + storage, + &PoolConfig { + pool_id: 1, + token0: "token0".to_string(), + token1: "token1".to_string(), + }, + ) + .unwrap(); + VAULT_CONFIG + .save( + storage, + &VaultConfig { + performance_fee: Decimal::zero(), + treasury: Addr::unchecked("treasure"), + swap_max_slippage: Decimal::from_ratio(1u128, 20u128), + }, + ) + .unwrap(); + POSITION + .save(storage, &crate::state::Position { position_id: 1 }) + .unwrap(); + + deps +} diff --git a/smart-contracts/contracts/cl-vault/src/test_tube/deposit_withdraw.rs b/smart-contracts/contracts/cl-vault/src/test_tube/deposit_withdraw.rs index b867a7988..37163946e 100644 --- a/smart-contracts/contracts/cl-vault/src/test_tube/deposit_withdraw.rs +++ b/smart-contracts/contracts/cl-vault/src/test_tube/deposit_withdraw.rs @@ -1,15 +1,183 @@ #[cfg(test)] mod tests { - use cosmwasm_std::Coin; + use cosmwasm_std::{coin, Coin}; - use osmosis_test_tube::{Account, Module, Wasm}; + use osmosis_std::types::{ + cosmos::bank::v1beta1::{MsgSend, QueryAllBalancesRequest}, + osmosis::concentratedliquidity::v1beta1::PositionByIdRequest, + }; + use osmosis_test_tube::{Account, Bank, ConcentratedLiquidity, Module, Wasm}; use crate::{ msg::{ExecuteMsg, ExtensionQueryMsg, QueryMsg}, - query::UserBalanceResponse, + query::{PositionResponse, UserBalanceResponse}, test_tube::default_init, }; + #[test] + #[ignore] + fn multiple_deposit_withdraw_unused_funds_works() { + let (app, contract_address, _cl_pool_id, _admin) = default_init(); + let alice = app + .init_account(&[ + Coin::new(1_000_000_000_000, "uatom"), + Coin::new(1_000_000_000_000, "uosmo"), + ]) + .unwrap(); + let bob = app + .init_account(&[ + Coin::new(1_000_000_000_000, "uatom"), + Coin::new(1_000_000_000_000, "uosmo"), + ]) + .unwrap(); + + let bank = Bank::new(&app); + // our initial balance, 89874uosmo + let balances = bank + .query_all_balances(&QueryAllBalancesRequest { + address: contract_address.to_string(), + pagination: None, + }) + .unwrap(); + + let wasm = Wasm::new(&app); + + // depositing + let _res = wasm + .execute( + contract_address.as_str(), + &ExecuteMsg::ExactDeposit { recipient: None }, + &[Coin::new(5000, "uatom"), Coin::new(5000, "uosmo")], + &alice, + ) + .unwrap(); + + // The contract right now has 89874 free uosmo, if we send another 89874 free uosmo, we double the amount of free + // liquidity, but we want to double the amount of total liquidity, so we first query to contract to get how many + // assets we have in the position + let pos_id: PositionResponse = wasm + .query( + contract_address.as_str(), + &QueryMsg::VaultExtension(ExtensionQueryMsg::ConcentratedLiquidity( + crate::msg::ClQueryMsg::Position {}, + )), + ) + .unwrap(); + let position = ConcentratedLiquidity::new(&app) + .query_position_by_id(&PositionByIdRequest { + position_id: pos_id.position_ids[0], + }) + .unwrap(); + // This amount should decrease the amount of shares we get back + // "uatom", amount: "100000" }), asset1: Some(Coin { denom: "uosmo", amount: "10126" + // to dilute 50%, we need to send uatom100000, 10631uosmo + 89874+uosmo = 100000uosmo + // aka double the liquidty + + bank.send( + MsgSend { + from_address: alice.address(), + to_address: contract_address.to_string(), + amount: vec![coin(9995, "uatom").into(), coin(1012, "uosmo").into()], + }, + &alice, + ) + .unwrap(); + + let res = wasm + .execute( + contract_address.as_str(), + &ExecuteMsg::ExactDeposit { recipient: None }, + &[Coin::new(5_000, "uatom"), Coin::new(5_000, "uosmo")], + &bob, + ) + .unwrap(); + + // 2766182566501133149875859 before banksend, + // 1926137978194597565946694 after banksend + // does this make sense? + // when we withdraw 2766182566501133149875859 shares, we should get our original amount back + + // 2766182566501133149875859 / total_shares * 89874 back, remember we had original free osmo + // and sent free osmo + // the second share amount should only get it's original amount back + + // let _ = wasm + // .execute( + // contract_address.as_str(), + // &ExecuteMsg::ExactDeposit { recipient: None }, + // &[Coin::new(5_000, "uatom"), Coin::new(5_000, "uosmo")], + // &alice, + // ) + // .unwrap(); + + let alice_shares: UserBalanceResponse = wasm + .query( + contract_address.as_str(), + &QueryMsg::VaultExtension(ExtensionQueryMsg::Balances( + crate::msg::UserBalanceQueryMsg::UserSharesBalance { + user: alice.address(), + }, + )), + ) + .unwrap(); + let bob_shares: UserBalanceResponse = wasm + .query( + contract_address.as_str(), + &QueryMsg::VaultExtension(ExtensionQueryMsg::Balances( + crate::msg::UserBalanceQueryMsg::UserSharesBalance { + user: bob.address(), + }, + )), + ) + .unwrap(); + + let balances = bank + .query_all_balances(&QueryAllBalancesRequest { + address: contract_address.to_string(), + pagination: None, + }) + .unwrap(); + let pos_id: PositionResponse = wasm + .query( + contract_address.as_str(), + &QueryMsg::VaultExtension(ExtensionQueryMsg::ConcentratedLiquidity( + crate::msg::ClQueryMsg::Position {}, + )), + ) + .unwrap(); + let position = ConcentratedLiquidity::new(&app) + .query_position_by_id(&PositionByIdRequest { + position_id: pos_id.position_ids[0], + }) + .unwrap(); + // This amount should decrease the amount of shares we get back + + let withdraw = wasm + .execute( + contract_address.as_str(), + &ExecuteMsg::Redeem { + recipient: None, + amount: bob_shares.balance, + }, + &[], + &bob, + ) + .unwrap(); + + let withdraw = wasm + .execute( + contract_address.as_str(), + &ExecuteMsg::Redeem { + recipient: None, + amount: alice_shares.balance, + }, + &[], + &alice, + ) + .unwrap(); + // we receive "token0_amount", value: "2018" }, Attribute { key: "token1_amount", value: "3503 + // we used 5000uatom to deposit and 507 uosmo, thus we are down 3000 uatom and up 2996 uosmo + } + #[test] #[ignore] fn multiple_deposit_withdraw_works() { @@ -98,8 +266,6 @@ mod tests { ) .unwrap(); - let _mint = deposit.events.iter().find(|e| e.ty == "tf_mint").unwrap(); - let shares: UserBalanceResponse = wasm .query( contract_address.as_str(), @@ -112,7 +278,7 @@ mod tests { .unwrap(); assert!(!shares.balance.is_zero()); - let _withdraw = wasm + let withdraw = wasm .execute( contract_address.as_str(), &ExecuteMsg::Redeem { diff --git a/smart-contracts/contracts/cl-vault/src/test_tube/initialize.rs b/smart-contracts/contracts/cl-vault/src/test_tube/initialize.rs index cbbafa828..666d2db88 100644 --- a/smart-contracts/contracts/cl-vault/src/test_tube/initialize.rs +++ b/smart-contracts/contracts/cl-vault/src/test_tube/initialize.rs @@ -171,7 +171,7 @@ pub mod initialize { &instantiate_msg, Some(admin.address().as_str()), Some("cl-vault"), - sort_tokens(vec![coin(100000, pool.token0), coin(100000, pool.token1)]).as_ref(), + sort_tokens(vec![coin(5000, pool.token0), coin(507, pool.token1)]).as_ref(), &admin, ) .unwrap(); diff --git a/smart-contracts/contracts/cl-vault/src/test_tube/proptest.rs b/smart-contracts/contracts/cl-vault/src/test_tube/proptest.rs index c5c3a8687..81a7d9b49 100644 --- a/smart-contracts/contracts/cl-vault/src/test_tube/proptest.rs +++ b/smart-contracts/contracts/cl-vault/src/test_tube/proptest.rs @@ -109,7 +109,7 @@ mod tests { .unwrap(); // Find the event with "ty": "create_position" and collect the relevant attributes - let create_position_attrs = get_event_attributes_by_ty_and_key( + let _create_position_attrs = get_event_attributes_by_ty_and_key( &create_position, "create_position", vec!["liquidity", "amount0", "amount1"], @@ -190,7 +190,7 @@ mod tests { .unwrap(); // Find the event with "ty": "withdraw_position" and collect the relevant attributes - let withdraw_position_attrs = get_event_attributes_by_ty_and_key( + let _withdraw_position_attrs = get_event_attributes_by_ty_and_key( &withdraw_position, "withdraw_position", vec!["liquidity", "amount0", "amount1"], @@ -294,7 +294,7 @@ mod tests { ModifyRangeMsg { lower_price: Decimal::new(Uint128::new(new_lower_price)), upper_price: Decimal::new(Uint128::new(new_upper_price)), - max_slippage: Decimal::new(Uint128::new(5)), // optimize and check how this fits in the strategy as it could trigger organic errors we dont want to test + max_slippage: Decimal::bps(5), // optimize and check how this fits in the strategy as it could trigger organic errors we dont want to test }, )), &[], diff --git a/smart-contracts/contracts/cl-vault/src/test_tube/range.rs b/smart-contracts/contracts/cl-vault/src/test_tube/range.rs index 387cd258b..53529589a 100644 --- a/smart-contracts/contracts/cl-vault/src/test_tube/range.rs +++ b/smart-contracts/contracts/cl-vault/src/test_tube/range.rs @@ -183,8 +183,6 @@ mod test { let pools = cl.query_pools(&PoolsRequest { pagination: None }).unwrap(); let pool = Pool::decode(pools.pools[0].value.as_slice()).unwrap(); - println!("{:?}", pool); - let _result = wasm .execute( contract.as_str(), @@ -255,8 +253,6 @@ mod test { let pools = cl.query_pools(&PoolsRequest { pagination: None }).unwrap(); let pool: Pool = Pool::decode(pools.pools[0].value.as_slice()).unwrap(); - println!("pool: {:?}", pool); - // from the spreadsheet // create a basic position on the pool let initial_position = MsgCreatePosition { @@ -272,7 +268,5 @@ mod test { token_min_amount1: "0".to_string(), }; let position = cl.create_position(initial_position, &alice).unwrap(); - - println!("{:?}", position.events) } } diff --git a/smart-contracts/contracts/cl-vault/src/test_tube/rewards.rs b/smart-contracts/contracts/cl-vault/src/test_tube/rewards.rs index 30fc10d51..925d604ba 100644 --- a/smart-contracts/contracts/cl-vault/src/test_tube/rewards.rs +++ b/smart-contracts/contracts/cl-vault/src/test_tube/rewards.rs @@ -154,7 +154,5 @@ mod tests { &alice, ) .unwrap(); - - println!("{:?}", res.events) } } diff --git a/smart-contracts/contracts/cl-vault/src/vault/admin.rs b/smart-contracts/contracts/cl-vault/src/vault/admin.rs index 9c9430bba..827fa8b13 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/admin.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/admin.rs @@ -1,6 +1,6 @@ use crate::error::ContractResult; use crate::helpers::{assert_admin, sort_tokens}; -use crate::rewards::Rewards; +use crate::rewards::CoinList; use crate::state::{VaultConfig, ADMIN_ADDRESS, RANGE_ADMIN, STRATEGIST_REWARDS, VAULT_CONFIG}; use crate::{msg::AdminExtensionExecuteMsg, ContractError}; use cosmwasm_std::{BankMsg, DepsMut, MessageInfo, Response}; @@ -39,7 +39,7 @@ pub fn execute_claim_strategist_rewards( // get the currently attained rewards let rewards = STRATEGIST_REWARDS.load(deps.storage)?; // empty the saved rewards - STRATEGIST_REWARDS.save(deps.storage, &Rewards::new())?; + STRATEGIST_REWARDS.save(deps.storage, &CoinList::new())?; Ok(Response::new() .add_attribute("rewards", format!("{:?}", rewards.coins())) @@ -129,7 +129,10 @@ mod tests { let mut deps = mock_dependencies(); let rewards = vec![coin(12304151, "uosmo"), coin(5415123, "uatom")]; STRATEGIST_REWARDS - .save(deps.as_mut().storage, &Rewards::from_coins(rewards.clone())) + .save( + deps.as_mut().storage, + &CoinList::from_coins(rewards.clone()), + ) .unwrap(); RANGE_ADMIN @@ -154,7 +157,7 @@ mod tests { let mut deps = mock_dependencies(); let rewards = vec![coin(12304151, "uosmo"), coin(5415123, "uatom")]; STRATEGIST_REWARDS - .save(deps.as_mut().storage, &Rewards::from_coins(rewards)) + .save(deps.as_mut().storage, &CoinList::from_coins(rewards)) .unwrap(); RANGE_ADMIN diff --git a/smart-contracts/contracts/cl-vault/src/vault/claim.rs b/smart-contracts/contracts/cl-vault/src/vault/claim.rs index 42d11b92d..4d26955af 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/claim.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/claim.rs @@ -7,14 +7,15 @@ pub fn execute_claim_user_rewards( recipient: &str, ) -> Result { // addr unchecked is safe here because we will chekc addresses on save into this map - let mut user_rewards = match USER_REWARDS.may_load(deps.storage, Addr::unchecked(recipient))? { - Some(user_rewards) => user_rewards, - None => { - return Ok(Response::default() - .add_attribute("action", "claim_user_rewards") - .add_attribute("result", "no_rewards")) - } - }; + let mut user_rewards = + match USER_REWARDS.may_load(deps.storage, deps.api.addr_validate(recipient)?)? { + Some(user_rewards) => user_rewards, + None => { + return Ok(Response::default() + .add_attribute("action", "claim_user_rewards") + .add_attribute("result", "no_rewards")) + } + }; let send_rewards_msg = user_rewards.claim(recipient)?; diff --git a/smart-contracts/contracts/cl-vault/src/vault/concentrated_liquidity.rs b/smart-contracts/contracts/cl-vault/src/vault/concentrated_liquidity.rs index 576311198..f02caa266 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/concentrated_liquidity.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/concentrated_liquidity.rs @@ -66,7 +66,6 @@ pub fn withdraw_from_position( pub fn get_position( storage: &dyn Storage, querier: &QuerierWrapper, - _env: &Env, ) -> Result { let position = POSITION.load(storage)?; diff --git a/smart-contracts/contracts/cl-vault/src/vault/deposit.rs b/smart-contracts/contracts/cl-vault/src/vault/deposit.rs index dd24e0039..b89455c57 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/deposit.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/deposit.rs @@ -15,7 +15,7 @@ use osmosis_std::types::{ use crate::{ error::ContractResult, - helpers::{must_pay_one_or_two, sort_tokens}, + helpers::{get_liquidity_amount_for_unused_funds, must_pay_one_or_two, sort_tokens}, msg::{ExecuteMsg, MergePositionMsg}, reply::Replies, state::{CurrentDeposit, CURRENT_DEPOSIT, POOL_CONFIG, POSITION, SHARES, VAULT_DENOM}, @@ -100,7 +100,7 @@ pub(crate) fn execute_exact_deposit( /// handles the reply to creating a position for a user deposit /// and calculates the refund for the user pub fn handle_deposit_create_position_reply( - deps: DepsMut, + mut deps: DepsMut, env: Env, data: SubMsgResult, ) -> ContractResult { @@ -114,7 +114,7 @@ pub fn handle_deposit_create_position_reply( create_deposit_position_resp.liquidity_created.as_str(), )?); - let existing_position = get_position(deps.storage, &deps.querier, &env)? + let existing_position = get_position(deps.storage, &deps.querier)? .position .ok_or(ContractError::PositionNotFound)?; @@ -129,19 +129,30 @@ pub fn handle_deposit_create_position_reply( .parse::()? .into(); + let refunded = ( + current_deposit.token0_in.checked_sub(Uint128::new( + create_deposit_position_resp.amount0.parse::()?, + ))?, + current_deposit.token1_in.checked_sub(Uint128::new( + create_deposit_position_resp.amount1.parse::()?, + ))?, + ); + // total_vault_shares.is_zero() should never be zero. This should ideally always enter the else and we are just sanity checking. let user_shares: Uint128 = if total_vault_shares.is_zero() { existing_liquidity.to_uint_floor().try_into()? } else { + let liquidity_amount_of_unused_funds: Decimal256 = + get_liquidity_amount_for_unused_funds(deps.branch(), &env, refunded)?; + let total_liquidity = existing_liquidity.checked_add(liquidity_amount_of_unused_funds)?; + + // user_shares = total_vault_shares * user_liq / total_liq total_vault_shares .multiply_ratio( user_created_liquidity.numerator(), user_created_liquidity.denominator(), ) - .multiply_ratio( - existing_liquidity.denominator(), - existing_liquidity.numerator(), - ) + .multiply_ratio(total_liquidity.denominator(), total_liquidity.numerator()) .try_into()? }; @@ -291,7 +302,8 @@ mod tests { }; use crate::{ - state::{PoolConfig, Position}, + rewards::CoinList, + state::{PoolConfig, Position, STRATEGIST_REWARDS}, test_helpers::QuasarQuerier, }; @@ -309,6 +321,9 @@ mod tests { .save(deps.as_mut().storage, &Position { position_id: 1 }) .unwrap(); + STRATEGIST_REWARDS + .save(deps.as_mut().storage, &CoinList::new()) + .unwrap(); CURRENT_DEPOSIT .save( deps.as_mut().storage, @@ -370,7 +385,7 @@ mod tests { // the mint amount is dependent on the liquidity returned by MsgCreatePositionResponse, in this case 50% of current liquidty assert_eq!( SHARES.load(deps.as_ref().storage, sender).unwrap(), - Uint128::new(50) + Uint128::new(50000) ); assert_eq!( response.messages[1], @@ -378,7 +393,7 @@ mod tests { sender: env.contract.address.to_string(), amount: Some(OsmoCoin { denom: "money".to_string(), - amount: 50.to_string() + amount: 50000.to_string() }), mint_to_address: env.contract.address.to_string() }) @@ -391,7 +406,7 @@ mod tests { let total_liquidity = Decimal256::from_str("1000000000").unwrap(); let liquidity = Decimal256::from_str("5000000").unwrap(); - let user_shares: Uint128 = if total_shares.is_zero() && total_liquidity.is_zero() { + let _user_shares: Uint128 = if total_shares.is_zero() && total_liquidity.is_zero() { liquidity.to_uint_floor().try_into().unwrap() } else { let _ratio = liquidity.checked_div(total_liquidity).unwrap(); @@ -401,8 +416,6 @@ mod tests { .try_into() .unwrap() }; - - println!("{}", user_shares); } #[test] diff --git a/smart-contracts/contracts/cl-vault/src/vault/merge.rs b/smart-contracts/contracts/cl-vault/src/vault/merge.rs index 6d9eb0f85..52ebd792a 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/merge.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/merge.rs @@ -229,7 +229,6 @@ pub mod tests { let expected = MergeResponse { new_position_id: 5 }; let data = &to_binary(&expected).unwrap(); - println!("{:?}", data); let result = from_binary(data).unwrap(); assert_eq!(expected, result) diff --git a/smart-contracts/contracts/cl-vault/src/vault/range.rs b/smart-contracts/contracts/cl-vault/src/vault/range.rs index f3f9f3e09..3f9066c0f 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/range.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/range.rs @@ -13,17 +13,18 @@ use osmosis_std::types::osmosis::{ gamm::v1beta1::MsgSwapExactAmountInResponse, }; -use crate::msg::{ExecuteMsg, MergePositionMsg}; -use crate::state::CURRENT_SWAP; -use crate::vault::concentrated_liquidity::create_position; use crate::{ helpers::get_spot_price, + helpers::get_unused_balances, math::tick::price_to_tick, + msg::{ExecuteMsg, MergePositionMsg}, reply::Replies, + state::CURRENT_SWAP, state::{ ModifyRangeState, Position, SwapDepositMergeState, MODIFY_RANGE_STATE, POOL_CONFIG, POSITION, RANGE_ADMIN, SWAP_DEPOSIT_MERGE_STATE, }, + vault::concentrated_liquidity::create_position, vault::concentrated_liquidity::get_position, vault::merge::MergeResponse, vault::swap::swap, @@ -89,7 +90,7 @@ pub fn execute_update_range_ticks( // todo: prevent re-entrancy by checking if we have anything in MODIFY_RANGE_STATE (redundant check but whatever) // this will error if we dont have a position anyway - let position_breakdown = get_position(deps.storage, &deps.querier, &env)?; + let position_breakdown = get_position(deps.storage, &deps.querier)?; let position = position_breakdown.position.unwrap(); let withdraw_msg = MsgWithdrawPosition { @@ -134,10 +135,22 @@ pub fn handle_withdraw_position_reply( let modify_range_state = MODIFY_RANGE_STATE.load(deps.storage)?.unwrap(); let pool_config = POOL_CONFIG.load(deps.storage)?; - // what about funds sent to the vault via banksend, what about airdrops/other ways this would not be the total deposited balance - // todo: Test that one-sided withdraw wouldn't error here (it shouldn't) - let amount0: Uint128 = msg.amount0.parse()?; - let amount1: Uint128 = msg.amount1.parse()?; + + let mut amount0: Uint128 = msg.amount0.parse()?; + let mut amount1: Uint128 = msg.amount1.parse()?; + + let unused_balances = get_unused_balances(deps.storage, &deps.querier, &env)?; + let unused_balance0 = unused_balances + .find_coin(pool_config.token0.clone()) + .amount + .checked_sub(amount0)?; + let unused_balance1 = unused_balances + .find_coin(pool_config.token1.clone()) + .amount + .checked_sub(amount1)?; + + amount0 = amount0.checked_add(unused_balance0)?; + amount1 = amount1.checked_add(unused_balance1)?; CURRENT_BALANCE.save(deps.storage, &(amount0, amount1))?; @@ -199,8 +212,8 @@ pub fn handle_withdraw_position_reply( .add_attribute("method", "create_position") .add_attribute("lower_tick", format!("{:?}", modify_range_state.lower_tick)) .add_attribute("upper_tick", format!("{:?}", modify_range_state.upper_tick)) - .add_attribute("token0", format!("{:?}{:?}", amount0, pool_config.token0)) - .add_attribute("token1", format!("{:?}{:?}", amount1, pool_config.token1))) + .add_attribute("token0", format!("{}{}", amount0, pool_config.token0)) + .add_attribute("token1", format!("{}{}", amount1, pool_config.token1))) } } @@ -323,6 +336,7 @@ pub fn do_swap_deposit_merge( .add_attribute("method", "no_swap") .add_attribute("new_position", position_id.unwrap().to_string())); }; + // todo check that this math is right with spot price (numerators, denominators) if taken by legacy gamm module instead of poolmanager let spot_price = get_spot_price(deps.storage, &deps.querier)?; let (token_in_denom, token_out_ideal_amount, left_over_amount) = match swap_direction { @@ -356,8 +370,8 @@ pub fn do_swap_deposit_merge( .add_submessage(SubMsg::reply_on_success(swap_msg, Replies::Swap.into())) .add_attribute("action", "swap_deposit_merge") .add_attribute("method", "swap") - .add_attribute("token_in", format!("{:?}{:?}", swap_amount, token_in_denom)) - .add_attribute("token_out_min", format!("{:?}", token_out_min_amount))) + .add_attribute("token_in", format!("{}{}", swap_amount, token_in_denom)) + .add_attribute("token_out_min", format!("{}", token_out_min_amount))) } // do deposit @@ -513,102 +527,21 @@ pub enum SwapDirection { #[cfg(test)] mod tests { - use std::{marker::PhantomData, str::FromStr}; + use std::str::FromStr; use cosmwasm_std::{ - testing::{ - mock_dependencies, mock_env, mock_info, MockApi, MockStorage, MOCK_CONTRACT_ADDR, - }, - Addr, Decimal, Empty, MessageInfo, OwnedDeps, SubMsgResponse, SubMsgResult, - }; - use osmosis_std::types::{ - cosmos::base::v1beta1::Coin as OsmoCoin, - osmosis::concentratedliquidity::v1beta1::{ - FullPositionBreakdown, MsgWithdrawPositionResponse, Position as OsmoPosition, - }, + coin, + testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}, + Addr, Decimal, SubMsgResponse, SubMsgResult, }; + use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgWithdrawPositionResponse; use crate::{ - state::{ - PoolConfig, VaultConfig, MODIFY_RANGE_STATE, POOL_CONFIG, POSITION, RANGE_ADMIN, - VAULT_CONFIG, - }, - test_helpers::QuasarQuerier, + rewards::CoinList, + state::{MODIFY_RANGE_STATE, RANGE_ADMIN, STRATEGIST_REWARDS}, + test_helpers::{mock_deps_with_querier, mock_deps_with_querier_with_balance}, }; - fn mock_deps_with_querier( - info: &MessageInfo, - ) -> OwnedDeps { - let mut deps = OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: QuasarQuerier::new( - FullPositionBreakdown { - position: Some(OsmoPosition { - position_id: 1, - address: MOCK_CONTRACT_ADDR.to_string(), - pool_id: 1, - lower_tick: 100, - upper_tick: 1000, - join_time: None, - liquidity: "1000000.1".to_string(), - }), - asset0: Some(OsmoCoin { - denom: "token0".to_string(), - amount: "1000000".to_string(), - }), - asset1: Some(OsmoCoin { - denom: "token1".to_string(), - amount: "1000000".to_string(), - }), - claimable_spread_rewards: vec![ - OsmoCoin { - denom: "token0".to_string(), - amount: "100".to_string(), - }, - OsmoCoin { - denom: "token1".to_string(), - amount: "100".to_string(), - }, - ], - claimable_incentives: vec![], - forfeited_incentives: vec![], - }, - 500, - ), - custom_query_type: PhantomData, - }; - - let storage = &mut deps.storage; - - RANGE_ADMIN.save(storage, &info.sender).unwrap(); - POOL_CONFIG - .save( - storage, - &PoolConfig { - pool_id: 1, - token0: "token0".to_string(), - token1: "token1".to_string(), - }, - ) - .unwrap(); - VAULT_CONFIG - .save( - storage, - &VaultConfig { - performance_fee: Decimal::zero(), - treasury: Addr::unchecked("treasure"), - swap_max_slippage: Decimal::from_ratio(1u128, 20u128), - }, - ) - .unwrap(); - POSITION - .save(storage, &crate::state::Position { position_id: 1 }) - .unwrap(); - - deps - } - #[test] fn test_assert_range_admin() { let mut deps = mock_dependencies(); @@ -667,7 +600,17 @@ mod tests { #[test] fn test_handle_withdraw_position_reply_selects_correct_next_step_for_new_range() { let info = mock_info("addr0000", &[]); - let mut deps = mock_deps_with_querier(&info); + let mut deps = mock_deps_with_querier_with_balance( + &info, + &[(MOCK_CONTRACT_ADDR, &[coin(11234, "token1")])], + ); + + STRATEGIST_REWARDS + .save( + deps.as_mut().storage, + &CoinList::from_coins(vec![coin(1000, "token0"), coin(500, "token1")]), + ) + .unwrap(); // moving into a range MODIFY_RANGE_STATE @@ -703,6 +646,43 @@ mod tests { assert_eq!(res.messages.len(), 1); assert_eq!(res.attributes[0].value, "swap_deposit_merge"); assert_eq!(res.attributes[1].value, "swap"); + // check that our token1 attribute is incremented with the local balance - strategist rewards + assert_eq!( + res.attributes + .iter() + .find(|a| { a.key == "token_in" }) + .unwrap() + .value, + "5962token1" + ); + + let mut deps = mock_deps_with_querier_with_balance( + &info, + &[( + MOCK_CONTRACT_ADDR, + &[coin(11000, "token0"), coin(11234, "token1")], + )], + ); + + STRATEGIST_REWARDS + .save( + deps.as_mut().storage, + &CoinList::from_coins(vec![coin(1000, "token0"), coin(500, "token1")]), + ) + .unwrap(); + + // moving into a range + MODIFY_RANGE_STATE + .save( + deps.as_mut().storage, + &Some(crate::state::ModifyRangeState { + lower_tick: 100, + upper_tick: 1000, // since both times we are moving into range and in the quasarquerier we configured the current_tick as 500, this would mean we are trying to move into range + new_range_position_ids: vec![], + max_slippage: Decimal::zero(), + }), + ) + .unwrap(); // now test two-sided withdraw let data = SubMsgResult::Ok(SubMsgResponse { @@ -723,5 +703,13 @@ mod tests { assert_eq!(res.messages.len(), 1); assert_eq!(res.attributes[0].value, "modify_range"); assert_eq!(res.attributes[1].value, "create_position"); + assert_eq!( + res.attributes + .iter() + .find(|a| { a.key == "token1" }) + .unwrap() + .value, + "10734token1" + ); // 10000 withdrawn + 1234 local balance - 500 rewards } } diff --git a/smart-contracts/contracts/cl-vault/src/vault/withdraw.rs b/smart-contracts/contracts/cl-vault/src/vault/withdraw.rs index b89f02bf6..333918433 100644 --- a/smart-contracts/contracts/cl-vault/src/vault/withdraw.rs +++ b/smart-contracts/contracts/cl-vault/src/vault/withdraw.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ attr, coin, BankMsg, CosmosMsg, Decimal256, DepsMut, Env, MessageInfo, Response, SubMsg, - SubMsgResult, Uint128, + SubMsgResult, Uint128, Uint256, }; use osmosis_std::types::{ cosmos::bank::v1beta1::BankQuerier, @@ -11,9 +11,9 @@ use osmosis_std::types::{ }; use crate::{ - helpers::sort_tokens, + helpers::{get_unused_balances, sort_tokens}, reply::Replies, - state::{CURRENT_WITHDRAWER, POOL_CONFIG, SHARES, VAULT_DENOM}, + state::{CURRENT_WITHDRAWER, CURRENT_WITHDRAWER_DUST, POOL_CONFIG, SHARES, VAULT_DENOM}, vault::concentrated_liquidity::{get_position, withdraw_from_position}, ContractError, }; @@ -25,7 +25,7 @@ pub fn execute_withdraw( env: Env, info: MessageInfo, recipient: Option, - amount: Uint128, + shares_to_withdraw: Uint256, ) -> Result { let recipient = recipient.map_or(Ok(info.sender.clone()), |x| deps.api.addr_validate(&x))?; @@ -35,14 +35,52 @@ pub fn execute_withdraw( // let shares = must_pay(&info, vault_denom.as_str())?; // get the amount from SHARES state - let user_shares = SHARES.load(deps.storage, info.sender.clone())?; + let user_shares: Uint256 = SHARES + .load(deps.storage, info.sender.clone())? + .try_into() + .unwrap(); let left_over = user_shares - .checked_sub(amount) + .checked_sub(shares_to_withdraw) .map_err(|_| ContractError::InsufficientFunds)?; - SHARES.save(deps.storage, info.sender, &left_over)?; + SHARES.save(deps.storage, info.sender, &left_over.try_into().unwrap())?; + + let total_shares = BankQuerier::new(&deps.querier) + .supply_of(vault_denom.clone())? + .amount + .unwrap() + .amount + .parse()?; + + // get the dust amounts belonging to the user + let pool_config = POOL_CONFIG.load(deps.storage)?; + // TODO replace dust with queries for balance + let unused_balances = get_unused_balances(deps.storage, &deps.querier, &env)?; + let dust0: Uint256 = unused_balances + .find_coin(pool_config.token0.clone()) + .amount + .try_into() + .unwrap(); + let dust1: Uint256 = unused_balances + .find_coin(pool_config.token1) + .amount + .try_into() + .unwrap(); + + let user_dust0: Uint128 = dust0 + .checked_mul(shares_to_withdraw)? + .checked_div(total_shares)? + .try_into()?; + let user_dust1 = dust1 + .checked_mul(shares_to_withdraw)? + .checked_div(total_shares)? + .try_into()?; + // save the new total amount of dust available for other actions + + CURRENT_WITHDRAWER_DUST.save(deps.storage, &(user_dust0, user_dust1))?; + let shares_to_withdraw_u128: Uint128 = shares_to_withdraw.try_into().unwrap(); // burn the shares - let burn_coin = coin(amount.u128(), vault_denom); + let burn_coin = coin(shares_to_withdraw_u128.u128(), vault_denom); let burn_msg: CosmosMsg = MsgBurn { sender: env.contract.address.clone().into_string(), amount: Some(burn_coin.into()), @@ -53,13 +91,13 @@ pub fn execute_withdraw( CURRENT_WITHDRAWER.save(deps.storage, &recipient)?; // withdraw the user's funds from the position - let withdraw_msg = withdraw(deps, &env, amount)?; // TODOSN: Rename this function name to something more explicative + let withdraw_msg = withdraw(deps, &env, shares_to_withdraw_u128)?; // TODOSN: Rename this function name to something more explicative Ok(Response::new() .add_attribute("method", "withdraw") .add_attribute("action", "withdraw") .add_attribute("liquidity_amount", withdraw_msg.liquidity_amount.as_str()) - .add_attribute("share_amount", amount) + .add_attribute("share_amount", shares_to_withdraw) .add_message(burn_msg) .add_submessage(SubMsg::reply_on_success( withdraw_msg, @@ -70,9 +108,9 @@ pub fn execute_withdraw( fn withdraw( deps: DepsMut, env: &Env, - shares: Uint128, + user_shares: Uint128, ) -> Result { - let existing_position = get_position(deps.storage, &deps.querier, env)?; + let existing_position = get_position(deps.storage, &deps.querier)?; let existing_liquidity: Decimal256 = existing_position .position .ok_or(ContractError::PositionNotFound)? @@ -90,11 +128,11 @@ fn withdraw( .parse::()? .into(); - let user_shares = Decimal256::from_ratio(shares, 1_u128) + let user_liquidity = Decimal256::from_ratio(user_shares, 1_u128) .checked_mul(existing_liquidity)? .checked_div(Decimal256::from_ratio(total_vault_shares, 1_u128))?; - withdraw_from_position(deps.storage, env, user_shares) + withdraw_from_position(deps.storage, env, user_liquidity) } pub fn handle_withdraw_user_reply( @@ -106,8 +144,12 @@ pub fn handle_withdraw_user_reply( let user = CURRENT_WITHDRAWER.load(deps.storage)?; let pool_config = POOL_CONFIG.load(deps.storage)?; - let coin0 = coin(response.amount0.parse()?, pool_config.token0); - let coin1 = coin(response.amount1.parse()?, pool_config.token1); + let (user_dust0, user_dust1) = CURRENT_WITHDRAWER_DUST.load(deps.storage)?; + let amount0 = Uint128::new(response.amount0.parse()?).checked_add(user_dust0)?; + let amount1 = Uint128::new(response.amount1.parse()?).checked_add(user_dust1)?; + + let coin0 = coin(amount0.u128(), pool_config.token0); + let coin1 = coin(amount1.u128(), pool_config.token1); let withdraw_attrs = vec![ attr("token0_amount", coin0.amount), @@ -128,11 +170,157 @@ pub fn handle_withdraw_user_reply( #[cfg(test)] mod tests { - use crate::state::PoolConfig; - use cosmwasm_std::{testing::mock_dependencies, Addr, CosmosMsg, SubMsgResponse}; + use crate::{ + rewards::CoinList, + state::{PoolConfig, STRATEGIST_REWARDS, USER_REWARDS}, + test_helpers::mock_deps_with_querier_with_balance, + }; + use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}, + Addr, CosmosMsg, SubMsgResponse, + }; use super::*; + #[test] + fn execute_withdraw_works_no_rewards() { + let info = mock_info("bolice", &[]); + let mut deps = mock_deps_with_querier_with_balance( + &info, + &[( + MOCK_CONTRACT_ADDR, + &[coin(2000, "token0"), coin(3000, "token1")], + )], + ); + let env = mock_env(); + + STRATEGIST_REWARDS + .save(deps.as_mut().storage, &CoinList::new()) + .unwrap(); + VAULT_DENOM + .save(deps.as_mut().storage, &"share_token".to_string()) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + Addr::unchecked("bolice"), + &Uint128::new(1000), + ) + .unwrap(); + + let _res = + execute_withdraw(deps.as_mut(), env, info, None, Uint128::new(1000).into()).unwrap(); + // our querier returns a total supply of 100_000, this user unbonds 1000, or 1%. The Dust saved should be one lower + assert_eq!( + CURRENT_WITHDRAWER_DUST.load(deps.as_ref().storage).unwrap(), + (Uint128::new(20), Uint128::new(30)) + ) + } + + #[test] + fn execute_withdraw_works_user_rewards() { + let info = mock_info("bolice", &[]); + let mut deps = mock_deps_with_querier_with_balance( + &info, + &[( + MOCK_CONTRACT_ADDR, + &[coin(2000, "token0"), coin(3000, "token1")], + )], + ); + let env = mock_env(); + + STRATEGIST_REWARDS + .save(deps.as_mut().storage, &CoinList::new()) + .unwrap(); + VAULT_DENOM + .save(deps.as_mut().storage, &"share_token".to_string()) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + Addr::unchecked("bolice"), + &Uint128::new(1000), + ) + .unwrap(); + + USER_REWARDS + .save( + deps.as_mut().storage, + Addr::unchecked("alice"), + &CoinList::from_coins(vec![coin(100, "token0"), coin(175, "token1")]), + ) + .unwrap(); + USER_REWARDS + .save( + deps.as_mut().storage, + Addr::unchecked("bob"), + &CoinList::from_coins(vec![coin(50, "token0"), coin(125, "token1")]), + ) + .unwrap(); + + let _res = + execute_withdraw(deps.as_mut(), env, info, None, Uint128::new(1000).into()).unwrap(); + // our querier returns a total supply of 100_000, this user unbonds 1000, or 1%. The Dust saved should be one lower + assert_eq!( + CURRENT_WITHDRAWER_DUST.load(deps.as_ref().storage).unwrap(), + (Uint128::new(18), Uint128::new(27)) + ) + } + + #[test] + fn execute_withdraw_works_user_strategist_rewards() { + let info = mock_info("bolice", &[]); + let mut deps = mock_deps_with_querier_with_balance( + &info, + &[( + MOCK_CONTRACT_ADDR, + &[coin(200000, "token0"), coin(300000, "token1")], + )], + ); + let env = mock_env(); + + STRATEGIST_REWARDS + .save( + deps.as_mut().storage, + &CoinList::from_coins(vec![coin(50, "token0"), coin(50, "token1")]), + ) + .unwrap(); + VAULT_DENOM + .save(deps.as_mut().storage, &"share_token".to_string()) + .unwrap(); + SHARES + .save( + deps.as_mut().storage, + Addr::unchecked("bolice"), + &Uint128::new(1000), + ) + .unwrap(); + + USER_REWARDS + .save( + deps.as_mut().storage, + Addr::unchecked("alice"), + &CoinList::from_coins(vec![coin(200, "token0"), coin(300, "token1")]), + ) + .unwrap(); + USER_REWARDS + .save( + deps.as_mut().storage, + Addr::unchecked("bob"), + &CoinList::from_coins(vec![coin(400, "token0"), coin(100, "token1")]), + ) + .unwrap(); + + let _res = + execute_withdraw(deps.as_mut(), env, info, None, Uint128::new(1000).into()).unwrap(); + // our querier returns a total supply of 100_000, this user unbonds 1000, or 1%. The Dust saved should be one lower + // user dust should be 1% of 200000 - 650 (= 1993.5) and 1% of 300000 - 450 (= 2995.5) + assert_eq!( + CURRENT_WITHDRAWER_DUST.load(deps.as_ref().storage).unwrap(), + (Uint128::new(1993), Uint128::new(2995)) + ) + } + // the execute withdraw flow should be easiest to test in test-tube, since it requires quite a bit of Osmsosis specific information // we just test the handle withdraw implementation here #[test] @@ -142,6 +330,13 @@ mod tests { CURRENT_WITHDRAWER .save(deps.as_mut().storage, &to_address) .unwrap(); + CURRENT_WITHDRAWER_DUST + .save( + deps.as_mut().storage, + &(Uint128::new(123), Uint128::new(234)), + ) + .unwrap(); + POOL_CONFIG .save( deps.as_mut().storage, @@ -172,7 +367,7 @@ mod tests { response.messages[0].msg, CosmosMsg::Bank(BankMsg::Send { to_address: to_address.to_string(), - amount: sort_tokens(vec![coin(1000, "uosmo"), coin(1000, "uatom")]) + amount: sort_tokens(vec![coin(1123, "uosmo"), coin(1234, "uatom")]) }) ) }