diff --git a/amm/contracts/stable_pool/lib.rs b/amm/contracts/stable_pool/lib.rs index 38dbdb6..5ef3c61 100644 --- a/amm/contracts/stable_pool/lib.rs +++ b/amm/contracts/stable_pool/lib.rs @@ -1,6 +1,14 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] mod token_rate; - +/// Stabelswap implementation based on the CurveFi stableswap model. +/// +/// This pool contract supports PSP22 tokens which value increases at some +/// on-chain discoverable rate in terms of some other token, e.g. AZERO x sAZERO. +/// The rate oracle contract must implement [`RateProvider`](trait@traits::RateProvider). +/// +/// IMPORTANT: +/// This stableswap implementation is NOT meant for yield-bearing assets which adjusts +/// its total supply to try and maintain a stable price a.k.a. rebasing tokens. #[ink::contract] pub mod stable_pool { use crate::token_rate::TokenRate; @@ -287,7 +295,7 @@ pub mod stable_pool { let current_time = self.env().block_timestamp(); let mut rate_changed = false; for rate in self.pool.token_rates.iter_mut() { - rate_changed = rate_changed || rate.update_rate(current_time); + rate_changed = rate.update_rate(current_time) | rate_changed; } if rate_changed { Self::env().emit_event(RatesUpdated { @@ -303,6 +311,8 @@ pub mod stable_pool { /// Scaled rates are rates multiplied by precision. They are assumed to fit in u128. /// If TOKEN_TARGET_DECIMALS is 18 and RATE_DECIMALS is 12, then rates not exceeding ~340282366 should fit. /// That's because if precision <= 10^18 and rate <= 10^12 * 340282366, then rate * precision < 2^128. + /// + /// NOTE: Rates should be updated prior to calling this function fn get_scaled_rates(&self) -> Result, MathError> { self.pool .token_rates @@ -344,7 +354,9 @@ pub mod stable_pool { let token_out_id = self.token_id(token_out)?; Ok((token_in_id, token_out_id)) } - + /// Calculates lpt equivalent of the protocol fee and mints it to the `fee_to` if one is set. + /// + /// NOTE: Rates should be updated prior to calling this function fn mint_protocol_fee(&mut self, fee: u128, token_id: usize) -> Result<(), StablePoolError> { if let Some(fee_to) = self.fee_to() { let protocol_fee = self.pool.fees.protocol_trade_fee(fee)?; @@ -394,13 +406,6 @@ pub mod stable_pool { Ok(()) } - /// This method is for internal use only - /// - calculates token_out amount - /// - calculates swap fee - /// - mints protocol fee - /// - updates reserves - /// It assumes that rates have been updated. - /// Returns (token_out_amount, swap_fee) fn _swap_exact_in( &mut self, token_in: AccountId, @@ -460,13 +465,6 @@ pub mod stable_pool { Ok((token_out_amount, fee)) } - /// This method is for internal use only - /// - calculates token_in amount - /// - calculates swap fee - /// - mints protocol fee - /// - updates reserves - /// It assumes that rates have been updated. - /// Returns (token_in_amount, swap_fee) fn _swap_exact_out( &mut self, token_in: AccountId, @@ -533,6 +531,13 @@ pub mod stable_pool { Ok((token_in_amount, fee)) } + /// Handles PSP22 token transfer, + /// + /// If `amount` is `Some(amount)`, transfer this amount of `token_id` + /// from the caller to this contract. + /// + /// If `amount` of `None`, calculate the difference between + /// this contract balance and recorded reserve of `token_id`. fn _transfer_in( &self, token_id: usize, @@ -718,7 +723,7 @@ pub mod stable_pool { let current_time = self.env().block_timestamp(); let mut rate_changed = false; for rate in self.pool.token_rates.iter_mut() { - rate_changed = rate_changed || rate.update_rate_no_cache(current_time); + rate_changed = rate.update_rate_no_cache(current_time) | rate_changed; } if rate_changed { Self::env().emit_event(RatesUpdated { @@ -829,8 +834,6 @@ pub mod stable_pool { self.pool.tokens.clone() } - // This can output values lower than the actual balances of these tokens, which stems from roundings. - // However an invariant holds that each balance is at least the value returned by this function. #[ink(message)] fn reserves(&self) -> Vec { self.pool.reserves.clone() diff --git a/amm/contracts/stable_pool/token_rate.rs b/amm/contracts/stable_pool/token_rate.rs index 8b39cf3..ae2bc18 100644 --- a/amm/contracts/stable_pool/token_rate.rs +++ b/amm/contracts/stable_pool/token_rate.rs @@ -37,7 +37,9 @@ impl TokenRate { rate } - // To make sure the rate is up-to-date, the caller should call `update_rate` before calling this method. + /// Returns cached rate. + /// + /// NOTE: To make sure the rate is up-to-date, the caller should call `update_rate` before calling this method. pub fn get_rate(&self) -> u128 { match self { Self::Constant(rate) => *rate, @@ -45,17 +47,23 @@ impl TokenRate { } } + /// Update rate. + /// + /// Returns `true` if the rate was expired and value of the new rate is different than the previous. pub fn update_rate(&mut self, current_time: u64) -> bool { match self { Self::External(external) => external.update_rate(current_time), - _ => false, + Self::Constant(_) => false, } } + /// Update rate without expiry check. + /// + /// Returns `true` if value of the new rate is different than the previous. pub fn update_rate_no_cache(&mut self, current_time: u64) -> bool { match self { Self::External(external) => external.update_rate_no_cache(current_time), - _ => false, + Self::Constant(_) => false, } } } @@ -102,8 +110,9 @@ impl ExternalTokenRate { } fn update(&mut self, current_time: u64) -> bool { + let old_rate = self.cached_token_rate; self.cached_token_rate = Self::query_rate(self.token_rate_contract); self.last_token_rate_update_ts = current_time; - true + old_rate != self.cached_token_rate } } diff --git a/amm/traits/stable_pool.rs b/amm/traits/stable_pool.rs index 4bac840..12e4e3f 100644 --- a/amm/traits/stable_pool.rs +++ b/amm/traits/stable_pool.rs @@ -29,9 +29,9 @@ pub trait StablePoolView { fn token_rates(&mut self) -> Vec; /// Calculate swap amount of token_out - /// given token_in amount + /// given token_in amount. /// Returns (amount_out, fee) - /// fee is applied to token_out + /// NOTE: fee is applied to token_out #[ink(message)] fn get_swap_amount_out( &mut self, @@ -96,7 +96,7 @@ pub trait StablePool { /// for this contract. /// Returns an error if the minted LP tokens amount is less /// than `min_share_amount`. - /// Returns (minted_shares, fee_part) + /// Returns (minted_share_amount, fee_part) #[ink(message)] fn add_liquidity( &mut self, @@ -118,7 +118,7 @@ pub trait StablePool { /// Burns LP tokens and withdraws underlying tokens in balanced amounts to `to` account. /// Fails if any of the amounts received is less than in `min_amounts`. - /// Returns ([amounts_by_tokens], fee_part) + /// Returns (amounts_out, fee_part) #[ink(message)] fn remove_liquidity_by_shares( &mut self, @@ -150,6 +150,7 @@ pub trait StablePool { /// for this contract. /// Returns an error if to get token_out_amount of token_out it is required /// to spend more than `max_token_in_amount` of token_in. + /// NOTE: Fee is applied to `token_out`. /// Returns (token_in_amount, fee_amount) #[ink(message)] fn swap_exact_out( @@ -162,9 +163,8 @@ pub trait StablePool { ) -> Result<(u128, u128), StablePoolError>; /// Swaps excess reserve balance of `token_in` to `token_out`. - /// /// Swapped tokens are transferred to the `to` account. - /// Returns (amount_out, fees) + /// Returns (token_out_amount, fee_amount) #[ink(message)] fn swap_received( &mut self, diff --git a/helpers/math.rs b/helpers/math.rs index aad8abd..343c263 100644 --- a/helpers/math.rs +++ b/helpers/math.rs @@ -12,4 +12,5 @@ pub enum MathError { DivByZero(u8), MulOverflow(u8), SubUnderflow(u8), + Precision(u8), } diff --git a/helpers/stable_swap_math/mod.rs b/helpers/stable_swap_math/mod.rs index 767cfbf..fae4070 100644 --- a/helpers/stable_swap_math/mod.rs +++ b/helpers/stable_swap_math/mod.rs @@ -9,7 +9,7 @@ use primitive_types::U256; use fees::Fees; -/// Max number of iterations for curve computation using Newton–Raphson method +/// Max number of iterations performed in Newton–Raphson method const MAX_ITERATIONS: u8 = 255; fn amount_to_rated(amount: u128, scaled_rate: u128) -> Result { @@ -47,11 +47,10 @@ fn compute_d(amounts: &Vec, amp_coef: u128) -> Result { Ok(0.into()) } else { let n = amounts.len() as u32; + // n^n + let nn = n.checked_pow(n).ok_or(MathError::MulOverflow(1))?; // A * n^n - let ann: U256 = casted_mul( - amp_coef, - n.checked_pow(n).ok_or(MathError::MulOverflow(1))?.into(), - ); + let ann: U256 = casted_mul(amp_coef, nn.into()); // A * n^n * SUM{x_i} let ann_sum = ann .checked_mul(amount_sum) @@ -65,43 +64,37 @@ fn compute_d(amounts: &Vec, amp_coef: u128) -> Result { let mut d = amount_sum; // Computes next D unitl satisfying precision is reached for _ in 0..MAX_ITERATIONS { - let d_next = compute_d_next(d, n, amounts, ann_sum, ann_sub_one, n_add_one)?; - if d_next > d { - if d_next.checked_sub(d).ok_or(MathError::SubUnderflow(2))? <= 1.into() { - return Ok(d); - } - } else if d.checked_sub(d_next).ok_or(MathError::SubUnderflow(3))? <= 1.into() { + let d_next = compute_d_next(d, n, nn, amounts, ann_sum, ann_sub_one, n_add_one)?; + if d_next.abs_diff(d) <= 1.into() { return Ok(d); } d = d_next; } - Ok(d) + Err(MathError::Precision(1)) } } +/// Computes next step's approximation of D in Newton-Raphson method. +/// Returns d_next, or error if any math error occurred. fn compute_d_next( d_prev: U256, n: u32, + nn: u32, amounts: &Vec, ann_sum: U256, ann_sub_one: U256, n_add_one: u32, ) -> Result { let mut d_prod = d_prev; - // d_prod = ... * [d_prev / (x_(i) * n)] * ... - // where i in (0,n) - for amount in amounts { + // d_prod = d_prev^(n+1) / (n^n * Prod{amounts_i}) + for &amount in amounts { d_prod = d_prod .checked_mul(d_prev) .ok_or(MathError::MulOverflow(3))? - .checked_div( - amount - .checked_mul(n.into()) - .ok_or(MathError::MulOverflow(4))? - .into(), - ) + .checked_div(amount.into()) .ok_or(MathError::DivByZero(1))?; } + d_prod = d_prod.checked_div(nn.into()).unwrap(); let numerator = d_prev .checked_mul( d_prod @@ -128,7 +121,7 @@ fn compute_d_next( /// Returns new reserve of `y` tokens /// given new reserve of `x` tokens /// -/// NOTICE: it does not check if `token_x_id` != `token_y_id` and if tokens' `id`s are out of bounds +/// NOTE: it does not check if `token_x_id` != `token_y_id` and if tokens' `id`s are out of bounds fn compute_y( new_reserve_x: u128, reserves: &Vec, @@ -181,19 +174,14 @@ fn compute_y( .ok_or(MathError::AddOverflow(6))?; // d will be subtracted later let mut y_prev = d; - let mut y = y_prev; for _ in 0..MAX_ITERATIONS { - y = compute_y_next(y_prev, b, c, d)?; - if y > y_prev { - if y.checked_sub(y_prev).ok_or(MathError::SubUnderflow(4))? <= 1.into() { - return Ok(y.as_u128()); - } - } else if y_prev.checked_sub(y).ok_or(MathError::SubUnderflow(5))? <= 1.into() { - return Ok(y.as_u128()); + let y = compute_y_next(y_prev, b, c, d)?; + if y.abs_diff(y_prev) <= 1.into() { + return Ok(y.try_into().map_err(|_| MathError::CastOverflow(11))?); } y_prev = y; } - Ok(y.as_u128()) + Err(MathError::Precision(2)) } fn compute_y_next(y_prev: U256, b: U256, c: U256, d: U256) -> Result { @@ -382,15 +370,7 @@ fn compute_lp_amount_for_deposit( .ok_or(MathError::DivByZero(9))? .try_into() .map_err(|_| MathError::CastOverflow(2))?; - let difference = if ideal_reserve > new_reserves[i] { - ideal_reserve - .checked_sub(new_reserves[i]) - .ok_or(MathError::SubUnderflow(16))? - } else { - new_reserves[i] - .checked_sub(ideal_reserve) - .ok_or(MathError::SubUnderflow(17))? - }; + let difference = ideal_reserve.abs_diff(new_reserves[i]); let fee = _fees.normalized_trade_fee(n_coins, difference)?; new_reserves[i] = new_reserves[i] .checked_sub(fee) @@ -471,9 +451,7 @@ pub fn compute_amounts_given_lp( let mut amounts = Vec::with_capacity(reserves.len()); for &reserve in reserves { amounts.push( - U256::from(reserve) - .checked_mul(lp_amount.into()) - .ok_or(MathError::MulOverflow(21))? + casted_mul(reserve, lp_amount) .checked_div(pool_token_supply.into()) .ok_or(MathError::DivByZero(13))? .try_into() @@ -504,7 +482,7 @@ fn compute_lp_amount_for_withdraw( .map(|(reserve, &amount)| { reserve .checked_sub(amount) - .ok_or(MathError::AddOverflow(14)) + .ok_or(MathError::SubUnderflow(14)) }) .collect::, MathError>>()?; let d_1 = compute_d(&new_reserves, amp_coef)?; @@ -512,23 +490,16 @@ fn compute_lp_amount_for_withdraw( // Recalculate the invariant accounting for fees if let Some(_fees) = fees { for i in 0..new_reserves.len() { - let ideal_u128 = d_1 + let ideal_reserve: u128 = d_1 .checked_mul(old_reserves[i].into()) .ok_or(MathError::MulOverflow(22))? .checked_div(d_0) .ok_or(MathError::DivByZero(14))? - .as_u128(); - let difference = if ideal_u128 > new_reserves[i] { - ideal_u128 - .checked_sub(new_reserves[i]) - .ok_or(MathError::SubUnderflow(25))? - } else { - new_reserves[i] - .checked_sub(ideal_u128) - .ok_or(MathError::SubUnderflow(26))? - }; + .try_into() + .map_err(|_| MathError::CastOverflow(7))?; + let difference = ideal_reserve.abs_diff(new_reserves[i]); let fee = _fees.normalized_trade_fee(n_coins, difference)?; - // new_u128 is for calculation D2, the one with fee charged + // new_reserves is for calculation D2, the one with fee charged new_reserves[i] = new_reserves[i] .checked_sub(fee) .ok_or(MathError::SubUnderflow(27))?; @@ -545,14 +516,15 @@ fn compute_lp_amount_for_withdraw( .ok_or(MathError::MulOverflow(23))? .checked_div(d_0) .ok_or(MathError::DivByZero(15))? - .as_u128(); + .try_into() + .map_err(|_| MathError::CastOverflow(8))?; let diff_shares = U256::from(pool_token_supply) .checked_mul(d_0.checked_sub(d_1).ok_or(MathError::SubUnderflow(29))?) .ok_or(MathError::MulOverflow(24))? .checked_div(d_0) .ok_or(MathError::DivByZero(16))? - .as_u128(); - + .try_into() + .map_err(|_| MathError::CastOverflow(9))?; Ok(( burn_shares, burn_shares @@ -565,7 +537,8 @@ fn compute_lp_amount_for_withdraw( .ok_or(MathError::MulOverflow(25))? .checked_div(d_0) .ok_or(MathError::DivByZero(17))? - .as_u128(); + .try_into() + .map_err(|_| MathError::CastOverflow(10))?; Ok((burn_shares, 0)) } }