From 1b7b11ef0e02938ff9102f205254c71b39ab18ea Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Fri, 19 May 2023 13:37:44 +0100 Subject: [PATCH 1/2] feat: add support for rebasing tokens Signed-off-by: Gregory Hill --- crates/dex-stable/src/base_pool.rs | 163 ++++++++++------- crates/dex-stable/src/base_pool_tests.rs | 1 + crates/dex-stable/src/lib.rs | 39 +++- crates/dex-stable/src/meta_pool.rs | 218 +++++++++++++++-------- crates/dex-stable/src/meta_pool_tests.rs | 1 + crates/dex-stable/src/mock.rs | 15 +- crates/dex-stable/src/primitives.rs | 2 +- crates/dex-stable/src/rebase.rs | 154 ++++++++++++++++ crates/dex-stable/src/utils.rs | 29 ++- 9 files changed, 478 insertions(+), 144 deletions(-) create mode 100644 crates/dex-stable/src/rebase.rs diff --git a/crates/dex-stable/src/base_pool.rs b/crates/dex-stable/src/base_pool.rs index d855ee6669..a177ee65d9 100644 --- a/crates/dex-stable/src/base_pool.rs +++ b/crates/dex-stable/src/base_pool.rs @@ -64,13 +64,16 @@ impl Pallet { .try_into() .map_err(|_| Error::::BadPoolCurrencySymbol)?; + let balances = + BoundedVec::try_from(vec![Zero::zero(); currency_ids.len()]).map_err(|_| Error::::TooManyCurrencies)?; + Ok(( BasePool { currency_ids: BoundedVec::try_from(currency_ids.to_vec()).map_err(|_| Error::::TooManyCurrencies)?, lp_currency_id, token_multipliers: BoundedVec::try_from(rate).map_err(|_| Error::::TooManyCurrencies)?, - balances: BoundedVec::try_from(vec![Zero::zero(); currency_ids.len()]) - .map_err(|_| Error::::TooManyCurrencies)?, + rebased_balances: balances.clone(), + balances, fee, admin_fee, initial_a: a_with_precision, @@ -105,30 +108,28 @@ impl Pallet { let amp = Self::get_a_precise(pool).ok_or(Error::::Arithmetic)?; if lp_total_supply > Zero::zero() { d0 = Self::get_d( - &Self::xp(&pool.balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?, + &Self::xp(&pool.rebased_balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?, amp, ) .ok_or(Error::::Arithmetic)?; } - let mut new_balances = pool.balances.clone().to_vec(); + let mut new_rebased_balances = pool.rebased_balances.clone().to_vec(); for i in 0..n_currencies { if lp_total_supply == Zero::zero() { ensure!(!amounts[i].is_zero(), Error::::RequireAllCurrencies); } - new_balances[i] = new_balances[i] - .checked_add(Self::do_transfer_in( - pool.currency_ids[i], - who, - &pool.account, - amounts[i], - )?) + let (amount, rebased_amount) = + Self::do_transfer_in_and_convert(pool.currency_ids[i], who, &pool.account, amounts[i])?; + pool.balances[i] = pool.balances[i].checked_add(amount).ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_add(rebased_amount) .ok_or(Error::::Arithmetic)?; } let mut d1 = Self::get_d( - &Self::xp(&new_balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?, + &Self::xp(&new_rebased_balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?, amp, ) .ok_or(Error::::Arithmetic)?; @@ -137,12 +138,13 @@ impl Pallet { let mint_amount: Balance; if lp_total_supply.is_zero() { - pool.balances = BoundedVec::try_from(new_balances).map_err(|_| Error::::TooManyCurrencies)?; + pool.rebased_balances = + BoundedVec::try_from(new_rebased_balances).map_err(|_| Error::::TooManyCurrencies)?; mint_amount = d1; } else { (mint_amount, fees) = Self::calculate_base_mint_amount( pool, - &mut new_balances, + &mut new_rebased_balances, d0, &mut d1, fee_per_token, @@ -153,7 +155,6 @@ impl Pallet { } ensure!(min_mint_amount <= mint_amount, Error::::AmountSlippage); - T::MultiCurrency::deposit(pool.lp_currency_id, to, mint_amount)?; Self::deposit_event(Event::AddLiquidity { @@ -181,11 +182,13 @@ impl Pallet { let n_currencies = pool.currency_ids.len(); ensure!(i < n_currencies && j < n_currencies, Error::::CurrencyIndexOutRange); - let in_amount = Self::do_transfer_in(pool.currency_ids[i], who, &pool.account, in_amount)?; + let (in_amount, rebased_in_amount) = + Self::do_transfer_in_and_convert(pool.currency_ids[i], who, &pool.account, in_amount)?; - let normalized_balances = Self::xp(&pool.balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?; + let normalized_balances = + Self::xp(&pool.rebased_balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?; - let x = in_amount + let x = rebased_in_amount .checked_mul(pool.token_multipliers[i]) .and_then(|n| n.checked_add(normalized_balances[i])) .ok_or(Error::::Arithmetic)?; @@ -197,6 +200,7 @@ impl Pallet { .and_then(|n| n.checked_sub(One::one())) .ok_or(Error::::Arithmetic)?; + // https://github.com/curvefi/curve-stablecoin/blob/a29fc2e1c395793d4d3966e10cf2431fa179bfe6/contracts/Stableswap.vy#L804 let dy_fee = U256::from(dy) .checked_mul(U256::from(pool.fee)) .and_then(|n| n.checked_div(U256::from(FEE_DENOMINATOR))) @@ -217,15 +221,26 @@ impl Pallet { .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; - // update pool balance + // update pool balances + pool.rebased_balances[i] = pool.rebased_balances[i] + .checked_add(rebased_in_amount) + .ok_or(Error::::Arithmetic)?; pool.balances[i] = pool.balances[i].checked_add(in_amount).ok_or(Error::::Arithmetic)?; - pool.balances[j] = pool.balances[j] + + // swap fee remains in pool + pool.rebased_balances[j] = pool.rebased_balances[j] .checked_sub(dy) .and_then(|n| n.checked_sub(admin_fee)) .ok_or(Error::::Arithmetic)?; + pool.balances[j] = pool.balances[j] + .checked_sub(T::RebaseConvert::try_convert_balance_back( + admin_fee, + pool.currency_ids[j], + )?) + .ok_or(Error::::Arithmetic)?; - T::MultiCurrency::transfer(pool.currency_ids[j], &pool.account, to, dy) - .map_err(|_| Error::::InsufficientReserve)?; + // transfer excluding admin and exchange fees + Self::do_convert_back_and_transfer_out(pool, j, to, dy)?; Self::deposit_event(Event::CurrencyExchange { pool_id, @@ -266,16 +281,26 @@ impl Pallet { ensure!(dy >= min_amount, Error::::AmountSlippage); let fee_denominator = U256::from(FEE_DENOMINATOR); - pool.balances[index as usize] = U256::from(dy_fee) + let admin_fee = U256::from(dy_fee) .checked_mul(U256::from(pool.admin_fee)) .and_then(|n| n.checked_div(fee_denominator)) - .and_then(|n| n.checked_add(U256::from(dy))) .and_then(|n| TryInto::::try_into(n).ok()) - .and_then(|n| pool.balances[index as usize].checked_sub(n)) + .ok_or(Error::::Arithmetic)?; + + pool.rebased_balances[index as usize] = pool.rebased_balances[index as usize] + .checked_sub(dy) + .and_then(|n| n.checked_sub(admin_fee)) + .ok_or(Error::::Arithmetic)?; + + pool.balances[index as usize] = pool.balances[index as usize] + .checked_sub(T::RebaseConvert::try_convert_balance_back( + admin_fee, + pool.currency_ids[index as usize], + )?) .ok_or(Error::::Arithmetic)?; T::MultiCurrency::withdraw(pool.lp_currency_id, who, lp_amount)?; - T::MultiCurrency::transfer(pool.currency_ids[index as usize], &pool.account, to, dy)?; + Self::do_convert_back_and_transfer_out(pool, index as usize, to, dy)?; Self::deposit_event(Event::RemoveLiquidityOneCurrency { pool_id, @@ -304,7 +329,6 @@ impl Pallet { let (mut burn_amount, fees, d1) = Self::calculate_base_remove_liquidity_imbalance(pool, amounts, total_supply) .ok_or(Error::::Arithmetic)?; ensure!(burn_amount > Zero::zero(), Error::::AmountSlippage); - burn_amount = burn_amount.checked_add(One::one()).ok_or(Error::::Arithmetic)?; ensure!(burn_amount <= max_burn_amount, Error::::AmountSlippage); @@ -313,7 +337,7 @@ impl Pallet { for (i, balance) in amounts.iter().enumerate() { if *balance > Zero::zero() { - T::MultiCurrency::transfer(pool.currency_ids[i], &pool.account, to, *balance)?; + Self::do_convert_back_and_transfer_out(pool, i, to, *balance)?; } } @@ -334,7 +358,7 @@ impl Pallet { pool: &BasePool, ) -> Option { let d = Self::get_d( - &Self::xp(&pool.balances, &pool.token_multipliers)?, + &Self::xp(&pool.rebased_balances, &pool.token_multipliers)?, Self::get_a_precise(pool)?, )?; @@ -359,37 +383,44 @@ impl Pallet { let fee_per_token = U256::from(Self::calculate_fee_per_token(pool)?); let amp = Self::get_a_precise(pool)?; - let mut new_balances = pool.balances.clone(); - let d0 = U256::from(Self::get_d(&Self::xp(&pool.balances, &pool.token_multipliers)?, amp)?); + let mut new_rebased_balances = pool.rebased_balances.clone(); + let d0 = U256::from(Self::get_d( + &Self::xp(&pool.rebased_balances, &pool.token_multipliers)?, + amp, + )?); for (i, x) in amounts.iter().enumerate() { - new_balances[i] = new_balances[i].checked_sub(*x)?; + new_rebased_balances[i] = new_rebased_balances[i].checked_sub(*x)?; } - let d1 = U256::from(Self::get_d(&Self::xp(&new_balances, &pool.token_multipliers)?, amp)?); + let d1 = U256::from(Self::get_d( + &Self::xp(&new_rebased_balances, &pool.token_multipliers)?, + amp, + )?); let mut fees = vec![Balance::default(); currencies_len]; let fee_denominator = U256::from(FEE_DENOMINATOR); - for (i, balance) in pool.balances.iter_mut().enumerate() { - let ideal_balance = d1.checked_mul(U256::from(*balance))?.checked_div(d0)?; - let diff = Self::distance(U256::from(new_balances[i]), ideal_balance); + for (i, rebased_balance) in pool.rebased_balances.iter_mut().enumerate() { + let ideal_balance = d1.checked_mul(U256::from(*rebased_balance))?.checked_div(d0)?; + let diff = Self::distance(U256::from(new_rebased_balances[i]), ideal_balance); fees[i] = fee_per_token .checked_mul(diff)? .checked_div(fee_denominator) .and_then(|n| TryInto::::try_into(n).ok())?; - *balance = U256::from(new_balances[i]) - .checked_sub( - U256::from(fees[i]) - .checked_mul(U256::from(pool.admin_fee))? - .checked_div(fee_denominator)?, - ) + let admin_fee = U256::from(fees[i]) + .checked_mul(U256::from(pool.admin_fee)) + .and_then(|n| n.checked_div(fee_denominator)) .and_then(|n| TryInto::::try_into(n).ok())?; - new_balances[i] = new_balances[i].checked_sub(fees[i])?; + *rebased_balance = new_rebased_balances[i].checked_sub(admin_fee)?; + new_rebased_balances[i] = new_rebased_balances[i].checked_sub(fees[i])?; + + pool.balances[i] = pool.balances[i] + .checked_sub(T::RebaseConvert::try_convert_balance_back(admin_fee, pool.currency_ids[i]).ok()?)?; } - let d1 = Self::get_d(&Self::xp(&new_balances, &pool.token_multipliers)?, amp)?; + let d1 = Self::get_d(&Self::xp(&new_rebased_balances, &pool.token_multipliers)?, amp)?; let burn_amount = d0 .checked_sub(U256::from(d1))? .checked_mul(U256::from(total_supply))? @@ -410,7 +441,7 @@ impl Pallet { let total_supply = T::MultiCurrency::total_issuance(pool.lp_currency_id); let amp = Self::get_a_precise(pool)?; - let xp = Self::xp(&pool.balances, &pool.token_multipliers)?; + let xp = Self::xp(&pool.rebased_balances, &pool.token_multipliers)?; let d0 = Self::get_d(&xp, amp)?; let d1 = U256::from(d0) @@ -472,7 +503,7 @@ impl Pallet { return None; } - let normalized_balances = Self::xp(&pool.balances, &pool.token_multipliers)?; + let normalized_balances = Self::xp(&pool.rebased_balances, &pool.token_multipliers)?; let new_in_balance = normalized_balances[i].checked_add(in_balance.checked_mul(pool.token_multipliers[i])?)?; let out_balance = Self::get_y(pool, i, j, new_in_balance, &normalized_balances)?; @@ -493,7 +524,7 @@ impl Pallet { pub(crate) fn calculate_base_mint_amount( pool: &mut BasePool, - new_balances: &mut [Balance], + new_rebased_balances: &mut [Balance], d0: Balance, d1: &mut Balance, fee: Balance, @@ -508,9 +539,9 @@ impl Pallet { for i in 0..n_currencies { diff = Self::distance( U256::from(*d1) - .checked_mul(U256::from(pool.balances[i])) + .checked_mul(U256::from(pool.rebased_balances[i])) .and_then(|n| n.checked_div(U256::from(d0)))?, - U256::from(new_balances[i]), + U256::from(new_rebased_balances[i]), ); fees[i] = U256::from(fee) @@ -518,16 +549,18 @@ impl Pallet { .and_then(|n| n.checked_div(fee_denominator)) .and_then(|n| TryInto::::try_into(n).ok())?; - pool.balances[i] = new_balances[i].checked_sub( - U256::from(fees[i]) - .checked_mul(U256::from(pool.admin_fee)) - .and_then(|n| n.checked_div(fee_denominator)) - .and_then(|n| TryInto::::try_into(n).ok())?, - )?; + let admin_fee = U256::from(fees[i]) + .checked_mul(U256::from(pool.admin_fee)) + .and_then(|n| n.checked_div(fee_denominator)) + .and_then(|n| TryInto::::try_into(n).ok())?; + + pool.rebased_balances[i] = new_rebased_balances[i].checked_sub(admin_fee)?; + new_rebased_balances[i] = new_rebased_balances[i].checked_sub(fees[i])?; - new_balances[i] = new_balances[i].checked_sub(fees[i])?; + pool.balances[i] = pool.balances[i] + .checked_sub(T::RebaseConvert::try_convert_balance_back(admin_fee, pool.currency_ids[i]).ok()?)?; } - *d1 = Self::get_d(&Self::xp(new_balances, &pool.token_multipliers)?, amp)?; + *d1 = Self::get_d(&Self::xp(new_rebased_balances, &pool.token_multipliers)?, amp)?; let mint_amount = U256::from(total_supply) .checked_mul(U256::from(*d1).checked_sub(U256::from(d0))?)? @@ -546,7 +579,7 @@ impl Pallet { return None; } let mut amounts = Vec::new(); - for b in pool.balances.iter() { + for b in pool.rebased_balances.iter() { amounts.push( U256::from(*b) .checked_mul(U256::from(amount))? @@ -565,20 +598,24 @@ impl Pallet { ensure!(pool.currency_ids.len() == amounts.len(), Error::::MismatchParameter); let amp = Self::get_a_precise(pool).ok_or(Error::::Arithmetic)?; - let d0 = Self::xp(&pool.balances, &pool.token_multipliers) + let d0 = Self::xp(&pool.rebased_balances, &pool.token_multipliers) .and_then(|xp| Self::get_d(&xp, amp)) .ok_or(Error::::Arithmetic)?; - let mut new_balances = pool.balances.clone(); + let mut new_rebased_balances = pool.rebased_balances.clone(); for (i, balance) in amounts.iter().enumerate() { if deposit { - new_balances[i] = new_balances[i].checked_add(*balance).ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_add(*balance) + .ok_or(Error::::Arithmetic)?; } else { - new_balances[i] = new_balances[i].checked_sub(*balance).ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_sub(*balance) + .ok_or(Error::::Arithmetic)?; } } - let d1 = Self::xp(&new_balances, &pool.token_multipliers) + let d1 = Self::xp(&new_rebased_balances, &pool.token_multipliers) .and_then(|xp| Self::get_d(&xp, amp)) .ok_or(Error::::Arithmetic)?; diff --git a/crates/dex-stable/src/base_pool_tests.rs b/crates/dex-stable/src/base_pool_tests.rs index 1888ceea75..bc1bedfbca 100644 --- a/crates/dex-stable/src/base_pool_tests.rs +++ b/crates/dex-stable/src/base_pool_tests.rs @@ -235,6 +235,7 @@ fn create_pool_should_work() { checked_pow(10, (POOL_TOKEN_COMMON_DECIMALS - TOKEN4_DECIMAL) as usize).unwrap(), ]), balances: BoundedVec::truncate_from(vec![Zero::zero(); 4]), + rebased_balances: BoundedVec::truncate_from(vec![Zero::zero(); 4]), fee: SWAP_FEE, admin_fee: ADMIN_FEE, initial_a: INITIAL_A_VALUE * (A_PRECISION as Balance), diff --git a/crates/dex-stable/src/lib.rs b/crates/dex-stable/src/lib.rs index 6bc0ae2e7b..f15bae42a3 100644 --- a/crates/dex-stable/src/lib.rs +++ b/crates/dex-stable/src/lib.rs @@ -47,6 +47,7 @@ mod base_pool; mod default_weights; mod meta_pool; mod primitives; +mod rebase; mod utils; use frame_support::{ @@ -64,6 +65,7 @@ use sp_std::{ops::Sub, vec, vec::Vec}; pub use default_weights::WeightInfo; pub use pallet::*; use primitives::*; +pub use rebase::TryConvertBalance; use traits::{StablePoolLpCurrencyIdGenerate, ValidateCurrency}; #[allow(type_alias_bounds)] @@ -108,6 +110,9 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + /// Convert supported currencies to target asset. + type RebaseConvert: TryConvertBalance; } #[pallet::pallet] @@ -134,6 +139,9 @@ pub mod pallet { #[pallet::getter(fn lp_currencies)] pub type LpCurrencies = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::PoolId>; + #[pallet::storage] + pub type RebaseTokens = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::CurrencyId, OptionQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -1105,6 +1113,23 @@ pub mod pallet { Ok(()) }) } + + /// Add rebasing token with dynamically adjusted price. + /// + /// Only callable by admin. + /// + /// # Argument + /// + /// - `from`: The asset to rebase (e.g. LDOT). + /// - `to`: The target asset (e.g. DOT). + #[pallet::call_index(19)] + #[pallet::weight(1_000_000)] + #[transactional] + pub fn insert_rebase_token(origin: OriginFor, from: T::CurrencyId, to: T::CurrencyId) -> DispatchResult { + ensure_root(origin)?; + RebaseTokens::::insert(from, to); + Ok(()) + } } } @@ -1124,6 +1149,7 @@ impl Pallet { ) -> Result { Pools::::try_mutate_exists(pool_id, |optioned_pool| -> Result { let pool = optioned_pool.as_mut().ok_or(Error::::InvalidPoolId)?; + Self::inner_collect_yield(pool)?; match pool { Pool::Base(bp) => Self::base_pool_add_liquidity(who, pool_id, bp, amounts, min_mint_amount, to), Pool::Meta(mp) => Self::meta_pool_add_liquidity(who, pool_id, mp, amounts, min_mint_amount, to), @@ -1144,6 +1170,7 @@ impl Pallet { Pools::::try_mutate_exists(pool_id, |optioned_pool| -> Result { let pool = optioned_pool.as_mut().ok_or(Error::::InvalidPoolId)?; + Self::inner_collect_yield(pool)?; match pool { Pool::Base(bp) => Self::base_pool_swap(who, pool_id, bp, i, j, in_amount, out_min_amount, to), Pool::Meta(mp) => Self::meta_pool_swap(who, pool_id, mp, i, j, in_amount, out_min_amount, to), @@ -1161,6 +1188,7 @@ impl Pallet { Pools::::try_mutate_exists(pool_id, |optioned_pool| -> DispatchResult { ensure!(!lp_amount.is_zero(), Error::::InvalidTransaction); let global_pool = optioned_pool.as_mut().ok_or(Error::::InvalidPoolId)?; + Self::inner_collect_yield(global_pool)?; let pool = match global_pool { Pool::Base(bp) => bp, Pool::Meta(mp) => &mut mp.info, @@ -1174,13 +1202,17 @@ impl Pallet { let min_amounts_length = min_amounts.len(); ensure!(currencies_length == min_amounts_length, Error::::MismatchParameter); + // fees are not applied on withdrawal as this method + // does not change the imbalance of the pool in any way let fees: Vec = vec![Zero::zero(); currencies_length]; let amounts = Self::calculate_base_remove_liquidity(pool, lp_amount).ok_or(Error::::Arithmetic)?; for (i, amount) in amounts.iter().enumerate() { ensure!(*amount >= min_amounts[i], Error::::AmountSlippage); - pool.balances[i] = pool.balances[i].checked_sub(*amount).ok_or(Error::::Arithmetic)?; - T::MultiCurrency::transfer(pool.currency_ids[i], &pool.account, to, *amount)?; + pool.rebased_balances[i] = pool.rebased_balances[i] + .checked_sub(*amount) + .ok_or(Error::::Arithmetic)?; + Self::do_convert_back_and_transfer_out(pool, i, &to, *amount)?; } T::MultiCurrency::withdraw(pool.lp_currency_id, who, lp_amount)?; @@ -1207,6 +1239,7 @@ impl Pallet { Pools::::try_mutate_exists(pool_id, |optioned_pool| -> Result { ensure!(!lp_amount.is_zero(), Error::::InvalidTransaction); let pool = optioned_pool.as_mut().ok_or(Error::::InvalidPoolId)?; + Self::inner_collect_yield(pool)?; match pool { Pool::Base(bp) => { Self::base_pool_remove_liquidity_one_currency(pool_id, bp, who, lp_amount, index, min_amount, to) @@ -1227,6 +1260,7 @@ impl Pallet { ) -> DispatchResult { Pools::::try_mutate_exists(pool_id, |optioned_pool| -> DispatchResult { let pool = optioned_pool.as_mut().ok_or(Error::::InvalidPoolId)?; + Self::inner_collect_yield(pool)?; match pool { Pool::Base(bp) => { Self::base_pool_remove_liquidity_imbalance(who, pool_id, bp, amounts, max_burn_amount, to) @@ -1486,7 +1520,6 @@ impl Pallet { } let balance = T::MultiCurrency::free_balance(pool.currency_ids[currency_index], &pool.account); - balance.checked_sub(pool.balances[currency_index]) } else { None diff --git a/crates/dex-stable/src/meta_pool.rs b/crates/dex-stable/src/meta_pool.rs index 4ccaa4f3dd..fab8c24e2c 100644 --- a/crates/dex-stable/src/meta_pool.rs +++ b/crates/dex-stable/src/meta_pool.rs @@ -29,7 +29,7 @@ impl Pallet { if !lp_total_supply.is_zero() { let normalized_balances = Self::meta_pool_xp( - &meta_pool.info.balances, + &meta_pool.info.rebased_balances, &meta_pool.info.token_multipliers, base_virtual_price, ) @@ -38,27 +38,34 @@ impl Pallet { d0 = Self::get_d(&normalized_balances, amp).ok_or(Error::::Arithmetic)?; } - let mut new_balances = meta_pool.info.balances.clone(); - for (i, currency) in meta_pool.info.currency_ids.iter().enumerate() { + let mut new_rebased_balances = meta_pool.info.rebased_balances.clone(); + for i in 0..meta_pool.info.currency_ids.len() { ensure!( !lp_total_supply.is_zero() || amounts[i] > Zero::zero(), Error::::RequireAllCurrencies ); if !amounts[i].is_zero() { - new_balances[i] = new_balances[i] - .checked_add(Self::do_transfer_in( - *currency, - who, - &meta_pool.info.account, - amounts[i], - )?) + let (amount, rebased_amount) = Self::do_transfer_in_and_convert( + meta_pool.info.currency_ids[i], + who, + &meta_pool.info.account, + amounts[i], + )?; + meta_pool.info.balances[i] = meta_pool.info.balances[i] + .checked_add(amount) + .ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_add(rebased_amount) .ok_or(Error::::Arithmetic)?; } } - let normalized_balances = - Self::meta_pool_xp(&new_balances, &meta_pool.info.token_multipliers, base_virtual_price) - .ok_or(Error::::Arithmetic)?; + let normalized_balances = Self::meta_pool_xp( + &new_rebased_balances, + &meta_pool.info.token_multipliers, + base_virtual_price, + ) + .ok_or(Error::::Arithmetic)?; let d1 = Self::get_d(&normalized_balances, amp).ok_or(Error::::Arithmetic)?; ensure!(d1 > d0, Error::::CheckDFailed); @@ -71,29 +78,44 @@ impl Pallet { let fee_per_token = Self::calculate_fee_per_token(&meta_pool.info).ok_or(Error::::Arithmetic)?; for i in 0..meta_pool.info.currency_ids.len() { let ideal_balance = U256::from(d1) - .checked_mul(U256::from(meta_pool.info.balances[i])) + .checked_mul(U256::from(meta_pool.info.rebased_balances[i])) .and_then(|n| n.checked_div(U256::from(d0))) .ok_or(Error::::Arithmetic)?; fees[i] = U256::from(fee_per_token) - .checked_mul(Self::distance(ideal_balance, U256::from(new_balances[i]))) + .checked_mul(Self::distance(ideal_balance, U256::from(new_rebased_balances[i]))) .and_then(|n| n.checked_div(U256::from(FEE_DENOMINATOR))) .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; - meta_pool.info.balances[i] = U256::from(fees[i]) + let admin_fee = U256::from(fees[i]) .checked_mul(U256::from(meta_pool.info.admin_fee)) .and_then(|n| n.checked_div(U256::from(FEE_DENOMINATOR))) - .and_then(|n| U256::from(new_balances[i]).checked_sub(n)) .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; - new_balances[i] = new_balances[i].checked_sub(fees[i]).ok_or(Error::::Arithmetic)?; + meta_pool.info.rebased_balances[i] = new_rebased_balances[i] + .checked_sub(admin_fee) + .ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_sub(fees[i]) + .ok_or(Error::::Arithmetic)?; + + meta_pool.info.balances[i] = meta_pool.info.balances[i] + .checked_sub(T::RebaseConvert::try_convert_balance_back( + admin_fee, + meta_pool.info.currency_ids[i], + )?) + .ok_or(Error::::Arithmetic)?; } d2 = Self::get_d( - &Self::meta_pool_xp(&new_balances, &meta_pool.info.token_multipliers, base_virtual_price) - .ok_or(Error::::Arithmetic)?, + &Self::meta_pool_xp( + &new_rebased_balances, + &meta_pool.info.token_multipliers, + base_virtual_price, + ) + .ok_or(Error::::Arithmetic)?, amp, ) .ok_or(Error::::Arithmetic)?; @@ -105,7 +127,7 @@ impl Pallet { .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; } else { - meta_pool.info.balances = new_balances; + meta_pool.info.rebased_balances = new_rebased_balances; mint_amount = d1; } @@ -144,10 +166,11 @@ impl Pallet { let n_currencies = meta_pool.info.currency_ids.len(); ensure!(i < n_currencies && j < n_currencies, Error::::CurrencyIndexOutRange); - let in_amount = Self::do_transfer_in(meta_pool.info.currency_ids[i], who, &meta_pool.info.account, in_amount)?; + let (in_amount, rebased_in_amount) = + Self::do_transfer_in_and_convert(meta_pool.info.currency_ids[i], who, &meta_pool.info.account, in_amount)?; let virtual_price = Self::meta_pool_update_virtual_price(meta_pool).ok_or(Error::::Arithmetic)?; - let (dy, dy_fee) = Self::calculate_meta_swap_amount(meta_pool, i, j, in_amount, virtual_price) + let (dy, dy_fee) = Self::calculate_meta_swap_amount(meta_pool, i, j, rebased_in_amount, virtual_price) .ok_or(Error::::Arithmetic)?; ensure!(dy >= out_min_amount, Error::::AmountSlippage); @@ -159,17 +182,27 @@ impl Pallet { .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; - //update pool balance + // update pool balances + meta_pool.info.rebased_balances[i] = meta_pool.info.rebased_balances[i] + .checked_add(rebased_in_amount) + .ok_or(Error::::Arithmetic)?; meta_pool.info.balances[i] = meta_pool.info.balances[i] .checked_add(in_amount) .ok_or(Error::::Arithmetic)?; - meta_pool.info.balances[j] = meta_pool.info.balances[j] + + meta_pool.info.rebased_balances[j] = meta_pool.info.rebased_balances[j] .checked_sub(dy) .and_then(|n| n.checked_sub(admin_fee)) .ok_or(Error::::Arithmetic)?; + meta_pool.info.balances[j] = meta_pool.info.balances[j] + .checked_sub(T::RebaseConvert::try_convert_balance_back( + admin_fee, + meta_pool.info.currency_ids[j], + )?) + .ok_or(Error::::Arithmetic)?; - T::MultiCurrency::transfer(meta_pool.info.currency_ids[j], &meta_pool.info.account, to, dy) - .map_err(|_| Error::::InsufficientReserve)?; + // transfer excluding admin and exchange fees + Self::do_convert_back_and_transfer_out(&mut meta_pool.info, j, to, dy)?; Self::deposit_event(Event::CurrencyExchange { pool_id, @@ -218,21 +251,26 @@ impl Pallet { ensure!(dy >= min_amount, Error::::AmountSlippage); let fee_denominator = U256::from(FEE_DENOMINATOR); - meta_pool.info.balances[index as usize] = U256::from(dy_fee) + let admin_fee = U256::from(dy_fee) .checked_mul(U256::from(meta_pool.info.admin_fee)) .and_then(|n| n.checked_div(fee_denominator)) - .and_then(|n| n.checked_add(U256::from(dy))) .and_then(|n| TryInto::::try_into(n).ok()) - .and_then(|n| meta_pool.info.balances[index as usize].checked_sub(n)) + .ok_or(Error::::Arithmetic)?; + + meta_pool.info.rebased_balances[index as usize] = meta_pool.info.rebased_balances[index as usize] + .checked_sub(dy) + .and_then(|n| n.checked_sub(admin_fee)) + .ok_or(Error::::Arithmetic)?; + + meta_pool.info.balances[index as usize] = meta_pool.info.balances[index as usize] + .checked_sub(T::RebaseConvert::try_convert_balance_back( + admin_fee, + meta_pool.info.currency_ids[index as usize], + )?) .ok_or(Error::::Arithmetic)?; T::MultiCurrency::withdraw(meta_pool.info.lp_currency_id, who, lp_amount)?; - T::MultiCurrency::transfer( - meta_pool.info.currency_ids[index as usize], - &meta_pool.info.account, - to, - dy, - )?; + Self::do_convert_back_and_transfer_out(&mut meta_pool.info, index as usize, to, dy)?; Self::deposit_event(Event::RemoveLiquidityOneCurrency { pool_id, @@ -282,7 +320,7 @@ impl Pallet { for (i, balance) in amounts.iter().enumerate() { if *balance > Zero::zero() { - T::MultiCurrency::transfer(meta_pool.info.currency_ids[i], &meta_pool.info.account, to, *balance)?; + Self::do_convert_back_and_transfer_out(&mut meta_pool.info, i, to, *balance)?; } } @@ -354,13 +392,18 @@ impl Pallet { meta_index_to = base_lp_currency_index; } - let mut dx = Self::do_transfer_in(currency_from, who, &meta_pool.info.account, in_amount)?; + let (amount_in, mut dx) = + Self::do_transfer_in_and_convert(currency_from, who, &meta_pool.info.account, in_amount)?; let mut dy: Balance; if currency_index_from < base_lp_currency_index || currency_index_to < base_lp_currency_index { - let old_balances = meta_pool.info.balances.clone(); + let old_rebased_balances = meta_pool.info.rebased_balances.clone(); - let xp = Self::meta_pool_xp(&old_balances, &meta_pool.info.token_multipliers, base_virtual_price) - .ok_or(Error::::Arithmetic)?; + let xp = Self::meta_pool_xp( + &old_rebased_balances, + &meta_pool.info.token_multipliers, + base_virtual_price, + ) + .ok_or(Error::::Arithmetic)?; let x: Balance; if currency_index_from < base_lp_currency_index { x = dx @@ -369,7 +412,7 @@ impl Pallet { .ok_or(Error::::Arithmetic)?; } else { let mut base_amounts = vec![Balance::default(); meta_pool.base_currencies.len()]; - base_amounts[currency_index_from - base_lp_currency_index] = dx; + base_amounts[currency_index_from - base_lp_currency_index] = amount_in; dx = Self::inner_add_liquidity( &meta_pool.info.account, meta_pool.base_pool_id, @@ -401,6 +444,7 @@ impl Pallet { .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; } + let dy_fee = U256::from(dy) .checked_mul(U256::from(meta_pool.info.fee)) .and_then(|n| n.checked_div(U256::from(FEE_DENOMINATOR))) @@ -412,22 +456,30 @@ impl Pallet { .and_then(|n| n.checked_div(meta_pool.info.token_multipliers[meta_index_to])) .ok_or(Error::::Arithmetic)?; - let mut dy_admin_fee = U256::from(dy_fee) + let mut admin_fee = U256::from(dy_fee) .checked_mul(U256::from(meta_pool.info.admin_fee)) .and_then(|n| n.checked_div(U256::from(FEE_DENOMINATOR))) .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; - dy_admin_fee = dy_admin_fee + admin_fee = admin_fee .checked_div(meta_pool.info.token_multipliers[meta_index_to]) .ok_or(Error::::Arithmetic)?; - meta_pool.info.balances[meta_index_from] = old_balances[meta_index_from] + meta_pool.info.rebased_balances[meta_index_from] = old_rebased_balances[meta_index_from] .checked_add(dx) .ok_or(Error::::Arithmetic)?; - meta_pool.info.balances[meta_index_to] = old_balances[meta_index_to] + + meta_pool.info.rebased_balances[meta_index_to] = old_rebased_balances[meta_index_to] .checked_sub(dy) - .and_then(|n| n.checked_sub(dy_admin_fee)) + .and_then(|n| n.checked_sub(admin_fee)) + .ok_or(Error::::Arithmetic)?; + + meta_pool.info.balances[meta_index_to] = meta_pool.info.balances[meta_index_to] + .checked_sub(T::RebaseConvert::try_convert_balance_back( + admin_fee, + meta_pool.info.currency_ids[meta_index_to], + )?) .ok_or(Error::::Arithmetic)?; if currency_index_to >= base_lp_currency_index { @@ -505,7 +557,7 @@ impl Pallet { ) -> Option { let base_virtual_price = Self::meta_pool_base_virtual_price(meta_pool)?; let normalized_balances = Self::meta_pool_xp( - &meta_pool.info.balances, + &meta_pool.info.rebased_balances, &meta_pool.info.token_multipliers, base_virtual_price, )?; @@ -559,7 +611,7 @@ impl Pallet { let base_virtual_price = Self::meta_pool_base_virtual_price(meta_pool)?; let mut xp = Self::meta_pool_xp( - &meta_pool.info.balances, + &meta_pool.info.rebased_balances, &meta_pool.info.token_multipliers, base_virtual_price, )?; @@ -637,24 +689,28 @@ impl Pallet { deposit: bool, ) -> Result { let base_virtual_price = Self::meta_pool_base_virtual_price(meta_pool).ok_or(Error::::Arithmetic)?; - let mut new_balances = meta_pool.info.balances.clone(); + let mut new_rebased_balances = meta_pool.info.rebased_balances.clone(); let token_multipliers = meta_pool.info.token_multipliers.clone(); - let xp = - Self::meta_pool_xp(&new_balances, &token_multipliers, base_virtual_price).ok_or(Error::::Arithmetic)?; + let xp = Self::meta_pool_xp(&new_rebased_balances, &token_multipliers, base_virtual_price) + .ok_or(Error::::Arithmetic)?; let a = Self::get_a_precise(&meta_pool.info).ok_or(Error::::Arithmetic)?; let d0 = Self::get_d(&xp, a).ok_or(Error::::Arithmetic)?; for (i, balance) in amounts.iter().enumerate() { if deposit { - new_balances[i] = new_balances[i].checked_add(*balance).ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_add(*balance) + .ok_or(Error::::Arithmetic)?; } else { - new_balances[i] = new_balances[i].checked_sub(*balance).ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] + .checked_sub(*balance) + .ok_or(Error::::Arithmetic)?; } } - let xp1 = - Self::meta_pool_xp(&new_balances, &token_multipliers, base_virtual_price).ok_or(Error::::Arithmetic)?; + let xp1 = Self::meta_pool_xp(&new_rebased_balances, &token_multipliers, base_virtual_price) + .ok_or(Error::::Arithmetic)?; let d1 = Self::get_d(&xp1, a).ok_or(Error::::Arithmetic)?; let total_supply = T::MultiCurrency::total_issuance(meta_pool.info.lp_currency_id); @@ -694,40 +750,54 @@ impl Pallet { let fee_per_token = U256::from(Self::calculate_fee_per_token(&meta_pool.info)?); let amp = Self::get_a_precise(&meta_pool.info)?; let mut fees = vec![Balance::default(); currencies_len]; - let mut new_balances = meta_pool.info.balances.clone(); + let mut new_rebased_balances = meta_pool.info.rebased_balances.clone(); let fee_denominator = U256::from(FEE_DENOMINATOR); - let xp = Self::meta_pool_xp(&new_balances, &meta_pool.info.token_multipliers, base_virtual_price)?; + let xp = Self::meta_pool_xp( + &new_rebased_balances, + &meta_pool.info.token_multipliers, + base_virtual_price, + )?; let d0 = U256::from(Self::get_d(&xp, amp)?); for (i, x) in amounts.iter().enumerate() { - new_balances[i] = new_balances[i].checked_sub(*x)?; + new_rebased_balances[i] = new_rebased_balances[i].checked_sub(*x)?; } - let new_xp = Self::meta_pool_xp(&new_balances, &meta_pool.info.token_multipliers, base_virtual_price)?; + let new_xp = Self::meta_pool_xp( + &new_rebased_balances, + &meta_pool.info.token_multipliers, + base_virtual_price, + )?; let d1 = U256::from(Self::get_d(&new_xp, amp)?); - for (i, balance) in meta_pool.info.balances.iter_mut().enumerate() { - let ideal_balance = d1.checked_mul(U256::from(*balance))?.checked_div(d0)?; - let diff = Self::distance(U256::from(new_balances[i]), ideal_balance); + for (i, rebased_balance) in meta_pool.info.rebased_balances.iter_mut().enumerate() { + let ideal_balance = d1.checked_mul(U256::from(*rebased_balance))?.checked_div(d0)?; + let diff = Self::distance(U256::from(new_rebased_balances[i]), ideal_balance); fees[i] = fee_per_token .checked_mul(diff)? .checked_div(fee_denominator) .and_then(|n| TryInto::::try_into(n).ok())?; - *balance = U256::from(new_balances[i]) - .checked_sub( - U256::from(fees[i]) - .checked_mul(U256::from(meta_pool.info.admin_fee))? - .checked_div(fee_denominator)?, - ) + let admin_fee = U256::from(fees[i]) + .checked_mul(U256::from(meta_pool.info.admin_fee)) + .and_then(|n| n.checked_div(fee_denominator)) .and_then(|n| TryInto::::try_into(n).ok())?; - new_balances[i] = new_balances[i].checked_sub(fees[i])?; + *rebased_balance = new_rebased_balances[i].checked_sub(admin_fee)?; + new_rebased_balances[i] = new_rebased_balances[i].checked_sub(fees[i])?; + + meta_pool.info.balances[i] = meta_pool.info.balances[i].checked_sub( + T::RebaseConvert::try_convert_balance_back(admin_fee, meta_pool.info.currency_ids[i]).ok()?, + )?; } let d1 = Self::get_d( - &Self::meta_pool_xp(&new_balances, &meta_pool.info.token_multipliers, base_virtual_price)?, + &Self::meta_pool_xp( + &new_rebased_balances, + &meta_pool.info.token_multipliers, + base_virtual_price, + )?, amp, )?; @@ -752,7 +822,7 @@ impl Pallet { return None; } let xp = Self::meta_pool_xp( - &meta_pool.info.balances, + &meta_pool.info.rebased_balances, &meta_pool.info.token_multipliers, base_virtual_price, )?; @@ -828,7 +898,7 @@ impl Pallet { ); let xp = Self::meta_pool_xp( - &meta_pool.info.balances, + &meta_pool.info.rebased_balances, &meta_pool.info.token_multipliers, base_virtual_price, ) @@ -857,7 +927,7 @@ impl Pallet { .and_then(|n| TryInto::::try_into(n).ok()) .ok_or(Error::::Arithmetic)?; - // when adding to the base pool,you pay approx 50% of the swap fee + // when adding to the base pool, you pay approx 50% of the swap fee let base_pool = Self::pools(meta_pool.base_pool_id).ok_or(Error::::InvalidBasePool)?; let base_pool_fee = base_pool.get_fee(); diff --git a/crates/dex-stable/src/meta_pool_tests.rs b/crates/dex-stable/src/meta_pool_tests.rs index 446c068479..9dbe334372 100644 --- a/crates/dex-stable/src/meta_pool_tests.rs +++ b/crates/dex-stable/src/meta_pool_tests.rs @@ -329,6 +329,7 @@ fn create_meta_pool_should_work() { checked_pow(10, (POOL_TOKEN_COMMON_DECIMALS - STABLE_LP_DECIMAL) as usize).unwrap(), ]), balances: BoundedVec::truncate_from(vec![Zero::zero(); 4]), + rebased_balances: BoundedVec::truncate_from(vec![Zero::zero(); 4]), fee: SWAP_FEE, admin_fee: ADMIN_FEE, initial_a: INITIAL_A_VALUE * (A_PRECISION as Balance), diff --git a/crates/dex-stable/src/mock.rs b/crates/dex-stable/src/mock.rs index 37a97f95c5..d7fc41cf1b 100644 --- a/crates/dex-stable/src/mock.rs +++ b/crates/dex-stable/src/mock.rs @@ -31,7 +31,6 @@ type Block = frame_system::mocking::MockBlock; parameter_types! { pub const ExistentialDeposit: u64 = 1; - pub const BlockHashCount: u64 = 250; pub const StableAmmPalletId: PalletId = PalletId(*b"dex/stbl"); pub const MaxReserves: u32 = 50; @@ -65,6 +64,7 @@ pub enum CurrencyId { Token(TokenSymbol), StableLP(PoolType), StableLPV2(PoolId), + Rebase(TokenSymbol), } impl From for CurrencyId { @@ -159,6 +159,17 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } +pub struct CurrencyConvert; +impl crate::rebase::CurrencyConversion for CurrencyConvert { + fn convert(amount: Balance, from: CurrencyId, to: CurrencyId) -> Result { + Ok(match (from, to) { + (CurrencyId::Rebase(_), _) => amount / 2, + (_, CurrencyId::Rebase(_)) => amount * 2, + (_, _) => amount, + }) + } +} + impl Config for Test { type RuntimeEvent = RuntimeEvent; type CurrencyId = CurrencyId; @@ -171,6 +182,7 @@ impl Config for Test { type PoolCurrencySymbolLimit = PoolCurrencySymbolLimit; type PalletId = StableAmmPalletId; type WeightInfo = (); + type RebaseConvert = crate::rebase::RebaseAdapter; } pub struct ExtBuilder; @@ -271,6 +283,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (ALICE, CurrencyId::Token(TOKEN2_SYMBOL), TOKEN2_UNIT * 1_00_000_000), (ALICE, CurrencyId::Token(TOKEN3_SYMBOL), TOKEN3_UNIT * 1_00_000_000), (ALICE, CurrencyId::Token(TOKEN4_SYMBOL), TOKEN4_UNIT * 1_00_000_000), + (ALICE, CurrencyId::Rebase(TOKEN1_SYMBOL), TOKEN1_UNIT * 1_00_000_000), (BOB, CurrencyId::Token(TOKEN1_SYMBOL), TOKEN1_UNIT * 1_00), (BOB, CurrencyId::Token(TOKEN2_SYMBOL), TOKEN2_UNIT * 1_00), (BOB, CurrencyId::Token(TOKEN3_SYMBOL), TOKEN3_UNIT * 1_00), diff --git a/crates/dex-stable/src/primitives.rs b/crates/dex-stable/src/primitives.rs index aaad83f34c..4288367b2b 100644 --- a/crates/dex-stable/src/primitives.rs +++ b/crates/dex-stable/src/primitives.rs @@ -35,7 +35,6 @@ pub const MAX_SWAP_FEE: Number = 100_000_000; // 1% #[derive(CloneNoBound, PartialEqNoBound, EqNoBound, RuntimeDebugNoBound, TypeInfo, Encode, Decode, MaxEncodedLen)] #[codec(mel_bound(skip_type_params(PoolCurrencyLimit, PoolCurrencySymbolLimit)))] #[scale_info(skip_type_params(PoolCurrencyLimit, PoolCurrencySymbolLimit))] - pub struct BasePool, PoolCurrencySymbolLimit: Get> where AccountId: Clone + Debug + Eq + PartialEq, @@ -48,6 +47,7 @@ where // effective balance which might different from token balance of the pool account because it // hold admin fee as well pub balances: BoundedVec, + pub rebased_balances: BoundedVec, // swap fee ratio. Change on any action which move balance state far from the ideal state pub fee: Number, // admin fee in ratio of swap fee. diff --git a/crates/dex-stable/src/rebase.rs b/crates/dex-stable/src/rebase.rs new file mode 100644 index 0000000000..4a9e068b1f --- /dev/null +++ b/crates/dex-stable/src/rebase.rs @@ -0,0 +1,154 @@ +use super::*; +use orml_traits::MultiCurrency; + +pub trait TryConvertBalance { + type AssetId; + fn try_convert_balance(amount: A, asset_id: Self::AssetId) -> Result; + fn try_convert_balance_back(amount: B, asset_id: Self::AssetId) -> Result; +} + +pub trait CurrencyConversion { + fn convert(amount: Balance, from: CurrencyId, to: CurrencyId) -> Result; +} + +/// Used to convert supported liquid currencies to their corresponding staking currencies +/// and vice versa, e.g. LDOT/DOT +/// NOTE: oracle already handles converting via wrapped currency +pub struct RebaseAdapter(PhantomData<(T, C)>); + +impl TryConvertBalance for RebaseAdapter +where + T: Config, + C: CurrencyConversion, +{ + type AssetId = T::CurrencyId; + + // e.g. LKSM -> KSM + fn try_convert_balance(amount: Balance, from_asset_id: T::CurrencyId) -> Result { + if let Some(to_asset_id) = RebaseTokens::::get(&from_asset_id) { + C::convert(amount, from_asset_id, to_asset_id) + } else { + Ok(amount) + } + } + + // e.g. KSM -> LKSM + fn try_convert_balance_back(amount: Balance, from_asset_id: T::CurrencyId) -> Result { + if let Some(to_asset_id) = RebaseTokens::::get(&from_asset_id) { + C::convert(amount, to_asset_id, from_asset_id) + } else { + Ok(amount) + } + } +} + +impl Pallet { + fn update_balances( + pool: &mut BasePool, + ) -> Result<(), DispatchError> { + for (i, balance) in pool.balances.iter().enumerate() { + pool.rebased_balances[i] = T::RebaseConvert::try_convert_balance(*balance, pool.currency_ids[i])?; + } + Ok(()) + } + + fn get_yield_amount( + pool: &BasePool, + ) -> Result { + let amp = Self::get_a_precise(pool).ok_or(Error::::Arithmetic)?; + let new_d = Self::get_d( + &Self::xp(&pool.rebased_balances, &pool.token_multipliers).ok_or(Error::::Arithmetic)?, + amp, + ) + .ok_or(Error::::Arithmetic)?; + Ok(new_d) + } + + fn collect_yield( + pool: &mut BasePool, + ) -> DispatchResult { + let old_d = Self::get_yield_amount(pool)?; + Self::update_balances(pool)?; + let new_d = Self::get_yield_amount(pool)?; + ensure!(new_d >= old_d, Error::::CheckDFailed); + if new_d > old_d { + let yield_amount = new_d - old_d; + T::MultiCurrency::deposit(pool.lp_currency_id, &pool.admin_fee_receiver, yield_amount)?; + } + Ok(()) + } + + pub(crate) fn inner_collect_yield( + general_pool: &mut Pool< + T::PoolId, + T::CurrencyId, + T::AccountId, + T::PoolCurrencyLimit, + T::PoolCurrencySymbolLimit, + >, + ) -> DispatchResult { + let pool = match general_pool { + Pool::Base(bp) => bp, + Pool::Meta(mp) => &mut mp.info, + }; + Self::collect_yield(pool) + } +} + +#[cfg(test)] +mod tests { + use super::{ + mock::{CurrencyId::*, *}, + *, + }; + use frame_support::assert_ok; + use frame_system::RawOrigin; + + const INITIAL_A_VALUE: Balance = 50; + const SWAP_FEE: Balance = 1e7 as Balance; + const ADMIN_FEE: Balance = 5_000_000_000; + + #[test] + fn create_pool_with_rebasing_asset() { + new_test_ext().execute_with(|| { + // staking (rebase) token is worth 2:1 of liquid token + RebaseTokens::::insert(Rebase(TOKEN1_SYMBOL), Token(TOKEN1_SYMBOL)); + + assert_ok!(StableAmm::create_base_pool( + RawOrigin::Root.into(), + vec![Token(TOKEN1_SYMBOL), Rebase(TOKEN1_SYMBOL)], + vec![TOKEN1_DECIMAL, TOKEN1_DECIMAL], + INITIAL_A_VALUE, + SWAP_FEE, + ADMIN_FEE, + ALICE, + Vec::from("stable_pool_lp"), + )); + + let pool_id = StableAmm::next_pool_id() - 1; + assert_ok!(StableAmm::add_liquidity( + RawOrigin::Signed(ALICE).into(), + pool_id, + vec![1e18 as Balance, 1e18 as Balance * 2], + 0, + ALICE, + u64::MAX, + )); + + let pool = StableAmm::pools(pool_id).unwrap().get_pool_info(); + let calculated_swap_return = StableAmm::calculate_base_swap_amount(&pool, 0, 1, 1e17 as Balance).unwrap(); + assert_eq!(calculated_swap_return, 99702611562565288); + + assert_ok!(StableAmm::swap( + RawOrigin::Signed(BOB).into(), + pool_id, + 0, + 1, + 1e17 as Balance, + calculated_swap_return, + CHARLIE, + u64::MAX + )); + }); + } +} diff --git a/crates/dex-stable/src/utils.rs b/crates/dex-stable/src/utils.rs index f932d5c241..23f024f94f 100644 --- a/crates/dex-stable/src/utils.rs +++ b/crates/dex-stable/src/utils.rs @@ -177,14 +177,39 @@ impl Pallet { from: &T::AccountId, to: &T::AccountId, amount: Balance, - ) -> Result> { + ) -> Result { let to_prior_balance = T::MultiCurrency::free_balance(currency_id, to); T::MultiCurrency::transfer(currency_id, from, to, amount).map_err(|_| Error::::InsufficientReserve)?; let to_new_balance = T::MultiCurrency::free_balance(currency_id, to); to_new_balance .checked_sub(to_prior_balance) - .ok_or(Error::::Arithmetic) + .ok_or(Error::::Arithmetic.into()) + } + + pub(crate) fn do_transfer_in_and_convert( + currency_id: T::CurrencyId, + from: &T::AccountId, + to: &T::AccountId, + amount: Balance, + ) -> Result<(Balance, Balance), DispatchError> { + let amount = Self::do_transfer_in(currency_id, from, to, amount)?; + let rebased_amount = T::RebaseConvert::try_convert_balance(amount, currency_id)?; + Ok((amount, rebased_amount)) + } + + pub(crate) fn do_convert_back_and_transfer_out( + pool: &mut BasePool, + index: usize, + to: &T::AccountId, + rebased_amount: Balance, + ) -> Result { + let currency_id = pool.currency_ids[index]; + let amount = T::RebaseConvert::try_convert_balance_back(rebased_amount, currency_id)?; + T::MultiCurrency::transfer(currency_id, &pool.account, to, amount) + .map_err(|_| Error::::InsufficientReserve)?; + pool.balances[index] = pool.balances[index].checked_sub(amount).ok_or(Error::::Arithmetic)?; + Ok(amount) } pub(crate) fn get_a_precise( From f6807108b6567bf11ae0db37350eba923fecf5b2 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Fri, 19 May 2023 16:07:24 +0100 Subject: [PATCH 2/2] chore: test collect yield Signed-off-by: Gregory Hill --- crates/dex-stable/src/base_pool.rs | 3 +- crates/dex-stable/src/lib.rs | 2 ++ crates/dex-stable/src/meta_pool.rs | 2 ++ crates/dex-stable/src/mock.rs | 40 ++++++++++++++++----- crates/dex-stable/src/rebase.rs | 57 ++++++++++++++++++++++++------ 5 files changed, 83 insertions(+), 21 deletions(-) diff --git a/crates/dex-stable/src/base_pool.rs b/crates/dex-stable/src/base_pool.rs index a177ee65d9..1f65ce7716 100644 --- a/crates/dex-stable/src/base_pool.rs +++ b/crates/dex-stable/src/base_pool.rs @@ -232,6 +232,7 @@ impl Pallet { .checked_sub(dy) .and_then(|n| n.checked_sub(admin_fee)) .ok_or(Error::::Arithmetic)?; + // dy subtracted from balances after transfer pool.balances[j] = pool.balances[j] .checked_sub(T::RebaseConvert::try_convert_balance_back( admin_fee, @@ -291,7 +292,7 @@ impl Pallet { .checked_sub(dy) .and_then(|n| n.checked_sub(admin_fee)) .ok_or(Error::::Arithmetic)?; - + // dy subtracted from balances after transfer pool.balances[index as usize] = pool.balances[index as usize] .checked_sub(T::RebaseConvert::try_convert_balance_back( admin_fee, diff --git a/crates/dex-stable/src/lib.rs b/crates/dex-stable/src/lib.rs index f15bae42a3..438696af35 100644 --- a/crates/dex-stable/src/lib.rs +++ b/crates/dex-stable/src/lib.rs @@ -112,6 +112,7 @@ pub mod pallet { type WeightInfo: WeightInfo; /// Convert supported currencies to target asset. + /// NOTE: the price should only ever increase type RebaseConvert: TryConvertBalance; } @@ -139,6 +140,7 @@ pub mod pallet { #[pallet::getter(fn lp_currencies)] pub type LpCurrencies = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::PoolId>; + // quote -> base #[pallet::storage] pub type RebaseTokens = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::CurrencyId, OptionQuery>; diff --git a/crates/dex-stable/src/meta_pool.rs b/crates/dex-stable/src/meta_pool.rs index fab8c24e2c..e47b7310f8 100644 --- a/crates/dex-stable/src/meta_pool.rs +++ b/crates/dex-stable/src/meta_pool.rs @@ -97,6 +97,7 @@ impl Pallet { meta_pool.info.rebased_balances[i] = new_rebased_balances[i] .checked_sub(admin_fee) .ok_or(Error::::Arithmetic)?; + new_rebased_balances[i] = new_rebased_balances[i] .checked_sub(fees[i]) .ok_or(Error::::Arithmetic)?; @@ -194,6 +195,7 @@ impl Pallet { .checked_sub(dy) .and_then(|n| n.checked_sub(admin_fee)) .ok_or(Error::::Arithmetic)?; + // dy subtracted from balances after transfer meta_pool.info.balances[j] = meta_pool.info.balances[j] .checked_sub(T::RebaseConvert::try_convert_balance_back( admin_fee, diff --git a/crates/dex-stable/src/mock.rs b/crates/dex-stable/src/mock.rs index d7fc41cf1b..73b8ffe9f9 100644 --- a/crates/dex-stable/src/mock.rs +++ b/crates/dex-stable/src/mock.rs @@ -159,17 +159,37 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } -pub struct CurrencyConvert; -impl crate::rebase::CurrencyConversion for CurrencyConvert { - fn convert(amount: Balance, from: CurrencyId, to: CurrencyId) -> Result { - Ok(match (from, to) { - (CurrencyId::Rebase(_), _) => amount / 2, - (_, CurrencyId::Rebase(_)) => amount * 2, - (_, _) => amount, - }) +#[frame_support::pallet] +pub mod oracle { + use super::{Balance, CurrencyId}; + use frame_support::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + // base / quote + #[pallet::storage] + pub type Price = StorageMap<_, Blake2_128Concat, (CurrencyId, CurrencyId), Balance, OptionQuery>; + + impl crate::rebase::CurrencyConversion for Pallet { + fn convert(amount: Balance, from: CurrencyId, to: CurrencyId) -> Result { + Ok(match Price::::get((from, to)) { + Some(price) => amount * price, + None => match Price::::get((to, from)) { + Some(price) => amount / price, + None => amount, + }, + }) + } } } +impl oracle::Config for Test {} + impl Config for Test { type RuntimeEvent = RuntimeEvent; type CurrencyId = CurrencyId; @@ -182,7 +202,7 @@ impl Config for Test { type PoolCurrencySymbolLimit = PoolCurrencySymbolLimit; type PalletId = StableAmmPalletId; type WeightInfo = (); - type RebaseConvert = crate::rebase::RebaseAdapter; + type RebaseConvert = crate::rebase::RebaseAdapter; } pub struct ExtBuilder; @@ -242,6 +262,7 @@ frame_support::construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config, Event} = 8, StableAMM: dex_stable::{Pallet, Call, Storage, Event} = 9, Tokens: orml_tokens::{Pallet, Storage, Event, Config} = 11, + Oracle: oracle::{Pallet, Storage} = 12, } ); @@ -288,6 +309,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (BOB, CurrencyId::Token(TOKEN2_SYMBOL), TOKEN2_UNIT * 1_00), (BOB, CurrencyId::Token(TOKEN3_SYMBOL), TOKEN3_UNIT * 1_00), (BOB, CurrencyId::Token(TOKEN4_SYMBOL), TOKEN4_UNIT * 1_00), + (BOB, CurrencyId::Rebase(TOKEN1_SYMBOL), TOKEN1_UNIT * 1_00_000_000), (CHARLIE, CurrencyId::Token(TOKEN1_SYMBOL), TOKEN1_UNIT * 1_00_000_000), (CHARLIE, CurrencyId::Token(TOKEN2_SYMBOL), TOKEN2_UNIT * 1_00_000_000), (CHARLIE, CurrencyId::Token(TOKEN3_SYMBOL), TOKEN3_UNIT * 1_00_000_000), diff --git a/crates/dex-stable/src/rebase.rs b/crates/dex-stable/src/rebase.rs index 4a9e068b1f..884299aacf 100644 --- a/crates/dex-stable/src/rebase.rs +++ b/crates/dex-stable/src/rebase.rs @@ -113,11 +113,12 @@ mod tests { new_test_ext().execute_with(|| { // staking (rebase) token is worth 2:1 of liquid token RebaseTokens::::insert(Rebase(TOKEN1_SYMBOL), Token(TOKEN1_SYMBOL)); + mock::oracle::Price::::insert((Rebase(TOKEN1_SYMBOL), Token(TOKEN1_SYMBOL)), 1); assert_ok!(StableAmm::create_base_pool( RawOrigin::Root.into(), - vec![Token(TOKEN1_SYMBOL), Rebase(TOKEN1_SYMBOL)], - vec![TOKEN1_DECIMAL, TOKEN1_DECIMAL], + vec![Token(TOKEN1_SYMBOL), Rebase(TOKEN1_SYMBOL), Token(TOKEN2_SYMBOL)], + vec![TOKEN1_DECIMAL, TOKEN1_DECIMAL, TOKEN2_DECIMAL], INITIAL_A_VALUE, SWAP_FEE, ADMIN_FEE, @@ -127,11 +128,11 @@ mod tests { let pool_id = StableAmm::next_pool_id() - 1; assert_ok!(StableAmm::add_liquidity( - RawOrigin::Signed(ALICE).into(), + RawOrigin::Signed(BOB).into(), pool_id, - vec![1e18 as Balance, 1e18 as Balance * 2], + vec![1e18 as Balance, 1e18 as Balance, 1e18 as Balance], 0, - ALICE, + BOB, u64::MAX, )); @@ -139,16 +140,50 @@ mod tests { let calculated_swap_return = StableAmm::calculate_base_swap_amount(&pool, 0, 1, 1e17 as Balance).unwrap(); assert_eq!(calculated_swap_return, 99702611562565288); - assert_ok!(StableAmm::swap( + // rebase tokens are now worth twice as much + mock::oracle::Price::::insert((Rebase(TOKEN1_SYMBOL), Token(TOKEN1_SYMBOL)), 2); + assert_ok!(StableAmm::add_liquidity( RawOrigin::Signed(BOB).into(), pool_id, + vec![1e18 as Balance, 0, 1e18 as Balance], 0, - 1, - 1e17 as Balance, - calculated_swap_return, - CHARLIE, - u64::MAX + BOB, + u64::MAX, )); + + // price stays the same + let calculated_swap_return = StableAmm::calculate_base_swap_amount(&pool, 0, 1, 1e17 as Balance).unwrap(); + assert_eq!(calculated_swap_return, 99702611562565288); + + let admin_lp_balance = ::MultiCurrency::free_balance(pool.lp_currency_id, &ALICE); + assert_eq!(admin_lp_balance, 995181615631638418); + + fn calculate_remove_all_liquidity(pool_id: PoolId, account: AccountId) -> Vec { + let pool = StableAmm::pools(pool_id).unwrap().get_pool_info(); + let amounts = StableAmm::calculate_base_remove_liquidity( + &pool, + ::MultiCurrency::free_balance(pool.lp_currency_id, &account), + ) + .unwrap(); + + amounts + .iter() + .zip(pool.currency_ids) + .map(|(amount, currency_id)| { + ::RebaseConvert::try_convert_balance_back(*amount, currency_id.clone()) + }) + .collect::, _>>() + .unwrap() + } + + assert_eq!( + calculate_remove_all_liquidity(pool_id, ALICE), + [331753180913049698, 165868730225911397, 331753180913049698] + ); + assert_eq!( + calculate_remove_all_liquidity(pool_id, BOB), + [1668153408288201704, 834037180572837200, 1668153408288201704] + ); }); } }