From 270acaa84df861bfe3b946ac607bcf3ec1b96d9b Mon Sep 17 00:00:00 2001 From: Jan Kuczma <63134918+JanKuczma@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:29:02 +0200 Subject: [PATCH] Gradual amplification coefficient change (#33) * gradual a change * fix tests --- amm/contracts/stable_pool/amp_coef.rs | 231 ++++++++++++++++++ amm/contracts/stable_pool/lib.rs | 64 ++--- .../src/stable_swap_tests/tests_getters.rs | 2 +- amm/drink-tests/src/utils.rs | 2 +- amm/traits/stable_pool.rs | 18 +- helpers/constants.rs | 14 +- 6 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 amm/contracts/stable_pool/amp_coef.rs diff --git a/amm/contracts/stable_pool/amp_coef.rs b/amm/contracts/stable_pool/amp_coef.rs new file mode 100644 index 0000000..6301007 --- /dev/null +++ b/amm/contracts/stable_pool/amp_coef.rs @@ -0,0 +1,231 @@ +use amm_helpers::{ + constants::stable_pool::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP, MIN_RAMP_DURATION}, + ensure, +}; +use ink::env::DefaultEnvironment; +use traits::{MathError, StablePoolError}; + +#[derive(Default, Debug, scale::Encode, scale::Decode, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +pub struct AmpCoef { + /// Initial amplification coefficient. + init_amp_coef: u128, + /// Target for ramping up amplification coefficient. + target_amp_coef: u128, + /// Initial amplification time. + init_amp_time: u64, + /// Stop ramp up amplification time. + stop_amp_time: u64, +} + +impl AmpCoef { + pub fn new(init_amp_coef: u128) -> Result { + ensure!(init_amp_coef >= MIN_AMP, StablePoolError::AmpCoefTooLow); + ensure!(init_amp_coef <= MAX_AMP, StablePoolError::AmpCoefTooHigh); + Ok(Self { + init_amp_coef, + target_amp_coef: init_amp_coef, + init_amp_time: 0, + stop_amp_time: 0, + }) + } + + pub fn compute_amp_coef(&self) -> Result { + let current_time = ink::env::block_timestamp::(); + if current_time < self.stop_amp_time { + let time_range = self + .stop_amp_time + .checked_sub(self.init_amp_time) + .ok_or(MathError::SubUnderflow(51))?; + let time_delta = current_time + .checked_sub(self.init_amp_time) + .ok_or(MathError::SubUnderflow(52))?; + + // Compute amp factor based on ramp time + let amp_range = self.target_amp_coef.abs_diff(self.init_amp_coef); + let amp_delta = amp_range + .checked_mul(time_delta as u128) + .ok_or(MathError::MulOverflow(51))? + .checked_div(time_range as u128) + .ok_or(MathError::DivByZero(51))?; + if self.target_amp_coef >= self.init_amp_coef { + // Ramp up + self.init_amp_coef + .checked_add(amp_delta) + .ok_or(MathError::AddOverflow(1)) + } else { + // Ramp down + self.init_amp_coef + .checked_sub(amp_delta) + .ok_or(MathError::SubUnderflow(55)) + } + } else { + Ok(self.target_amp_coef) + } + } + + pub fn ramp_amp_coef( + &mut self, + future_amp_coef: u128, + future_time_ts: u64, + ) -> Result<(), StablePoolError> { + ensure!(future_amp_coef >= MIN_AMP, StablePoolError::AmpCoefTooLow); + ensure!(future_amp_coef <= MAX_AMP, StablePoolError::AmpCoefTooHigh); + let current_time = ink::env::block_timestamp::(); + let ramp_duration = future_time_ts.checked_sub(current_time); + ensure!( + ramp_duration.is_some() && ramp_duration.unwrap() >= MIN_RAMP_DURATION, + StablePoolError::AmpCoefRampDurationTooShort + ); + let current_amp_coef = self.compute_amp_coef()?; + ensure!( + (future_amp_coef >= current_amp_coef + && future_amp_coef <= current_amp_coef * MAX_AMP_CHANGE) + || (future_amp_coef < current_amp_coef + && future_amp_coef * MAX_AMP_CHANGE >= current_amp_coef), + StablePoolError::AmpCoefChangeTooLarge + ); + self.init_amp_coef = current_amp_coef; + self.init_amp_time = current_time; + self.target_amp_coef = future_amp_coef; + self.stop_amp_time = future_time_ts; + Ok(()) + } + + /// Stop ramping A. If ramping is not in progress, it does not influence the A. + pub fn stop_ramp_amp_coef(&mut self) -> Result<(), StablePoolError> { + let current_amp_coef = self.compute_amp_coef()?; + let current_time = ink::env::block_timestamp::(); + self.init_amp_coef = current_amp_coef; + self.target_amp_coef = current_amp_coef; + self.init_amp_time = current_time; + self.stop_amp_time = current_time; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn set_block_timestamp(ts: u64) { + ink::env::test::set_block_timestamp::(ts); + } + + #[test] + fn amp_coef_up() { + let amp_coef = AmpCoef { + init_amp_coef: 100, + target_amp_coef: 1000, + init_amp_time: 100, + stop_amp_time: 1600, + }; + set_block_timestamp(100); + assert_eq!(amp_coef.compute_amp_coef(), Ok(100)); + set_block_timestamp(850); + assert_eq!(amp_coef.compute_amp_coef(), Ok(550)); + set_block_timestamp(1600); + assert_eq!(amp_coef.compute_amp_coef(), Ok(1000)); + } + + #[test] + fn amp_coef_down() { + let amp_coef = AmpCoef { + init_amp_coef: 1000, + target_amp_coef: 100, + init_amp_time: 100, + stop_amp_time: 1600, + }; + set_block_timestamp(100); + assert_eq!(amp_coef.compute_amp_coef(), Ok(1000)); + set_block_timestamp(850); + assert_eq!(amp_coef.compute_amp_coef(), Ok(550)); + set_block_timestamp(1600); + assert_eq!(amp_coef.compute_amp_coef(), Ok(100)); + } + + #[test] + fn amp_coef_change_duration() { + set_block_timestamp(1000); + let mut amp_coef = AmpCoef { + init_amp_coef: 1000, + target_amp_coef: 100, + init_amp_time: 100, + stop_amp_time: 1600, + }; + assert_eq!( + amp_coef.ramp_amp_coef(1000, 999), + Err(StablePoolError::AmpCoefRampDurationTooShort) + ); + assert_eq!( + amp_coef.ramp_amp_coef(1000, 1000 + MIN_RAMP_DURATION - 1), + Err(StablePoolError::AmpCoefRampDurationTooShort) + ); + assert_eq!( + amp_coef.ramp_amp_coef(1000, 1000 + MIN_RAMP_DURATION), + Ok(()) + ); + } + + #[test] + fn amp_coef_change_too_large() { + set_block_timestamp(100); + let mut amp_coef = AmpCoef { + init_amp_coef: 100, + target_amp_coef: 100, + init_amp_time: 100, + stop_amp_time: 1600, + }; + assert_eq!( + amp_coef.ramp_amp_coef(1001, 100 + MIN_RAMP_DURATION), + Err(StablePoolError::AmpCoefChangeTooLarge) + ); + assert_eq!( + amp_coef.ramp_amp_coef(1000, 100 + MIN_RAMP_DURATION), + Ok(()) + ); + } + + #[test] + fn amp_coef_stop_ramp() { + set_block_timestamp(100); + let mut amp_coef = AmpCoef { + init_amp_coef: 100, + target_amp_coef: 100, + init_amp_time: 100, + stop_amp_time: 1600, + }; + assert_eq!(amp_coef.compute_amp_coef(), Ok(100)); + assert_eq!( + amp_coef.ramp_amp_coef(1000, 100 + MIN_RAMP_DURATION), + Ok(()) + ); + set_block_timestamp(100 + MIN_RAMP_DURATION / 2); + assert!(amp_coef.stop_ramp_amp_coef().is_ok()); + assert_eq!(amp_coef.compute_amp_coef(), Ok(550)); + } + + #[test] + fn amp_coef_stop_ramp_no_change() { + set_block_timestamp(100); + let mut amp_coef = AmpCoef { + init_amp_coef: 100, + target_amp_coef: 100, + init_amp_time: 100, + stop_amp_time: 1600, + }; + assert_eq!(amp_coef.compute_amp_coef(), Ok(100)); + assert_eq!( + amp_coef.ramp_amp_coef(1000, 100 + MIN_RAMP_DURATION), + Ok(()) + ); + set_block_timestamp(100 + MIN_RAMP_DURATION); + assert_eq!(amp_coef.compute_amp_coef(), Ok(1000)); + set_block_timestamp(100 + MIN_RAMP_DURATION * 2); + assert!(amp_coef.stop_ramp_amp_coef().is_ok()); + assert_eq!(amp_coef.compute_amp_coef(), Ok(1000)); + } +} diff --git a/amm/contracts/stable_pool/lib.rs b/amm/contracts/stable_pool/lib.rs index 3291630..ba820d2 100644 --- a/amm/contracts/stable_pool/lib.rs +++ b/amm/contracts/stable_pool/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] +mod amp_coef; mod token_rate; /// Stabelswap implementation based on the CurveFi stableswap model. /// @@ -13,11 +14,9 @@ mod token_rate; /// 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; + use crate::{amp_coef::AmpCoef, token_rate::TokenRate}; use amm_helpers::{ - constants::stable_pool::{ - MAX_AMP, MAX_COINS, MIN_AMP, RATE_PRECISION, TOKEN_TARGET_DECIMALS, - }, + constants::stable_pool::{MAX_COINS, RATE_PRECISION, TOKEN_TARGET_DECIMALS}, ensure, stable_swap_math::{self as math, fees::Fees}, }; @@ -137,7 +136,7 @@ pub mod stable_pool { /// Means of getting token rates, either constant or external contract call. token_rates: Vec, /// Amplification coefficient. - amp_coef: u128, + amp_coef: AmpCoef, /// Fees fees: Fees, /// Who receives protocol fees (if any). @@ -151,14 +150,6 @@ pub mod stable_pool { psp22: PSP22Data, } - fn validate_amp_coef(amp_coef: u128) -> Result<(), StablePoolError> { - ensure!( - (MIN_AMP..=MAX_AMP).contains(&_coef), - StablePoolError::InvalidAmpCoef - ); - Ok(()) - } - impl StablePoolContract { pub fn new_pool( tokens: Vec, @@ -169,7 +160,6 @@ pub mod stable_pool { fees: Option, fee_receiver: Option, ) -> Result { - validate_amp_coef(amp_coef)?; let mut unique_tokens = tokens.clone(); unique_tokens.sort(); unique_tokens.dedup(); @@ -203,7 +193,7 @@ pub mod stable_pool { reserves: vec![0; token_count], precisions, token_rates, - amp_coef, + amp_coef: AmpCoef::new(amp_coef)?, fees: fees.ok_or(StablePoolError::InvalidFee)?, fee_receiver, }, @@ -349,7 +339,7 @@ pub mod stable_pool { &reserves, self.psp22.total_supply(), None, // no fees - self.amp_coef(), + self.amp_coef()?, )?; // mint fee (shares) to protocol let events = self.psp22.mint(fee_to, protocol_fee_lp)?; @@ -406,7 +396,7 @@ pub mod stable_pool { token_out_id, &self.reserves(), &self.pool.fees, - self.amp_coef(), + self.amp_coef()?, )?; // Check if swapped amount is not less than min_token_out_amount @@ -466,7 +456,7 @@ pub mod stable_pool { token_out_id, &self.reserves(), &self.pool.fees, - self.amp_coef(), + self.amp_coef()?, )?; // Check if in token_in_amount is as constrained by the user @@ -558,7 +548,7 @@ pub mod stable_pool { &self.reserves(), self.psp22.total_supply(), Some(&self.pool.fees), - self.amp_coef(), + self.amp_coef()?, )?; // Check min shares @@ -678,7 +668,7 @@ pub mod stable_pool { &self.reserves(), self.psp22.total_supply(), Some(&self.pool.fees), - self.amp_coef(), + self.amp_coef()?, )?; // check max shares @@ -791,13 +781,23 @@ pub mod stable_pool { } #[ink(message)] - fn set_amp_coef(&mut self, amp_coef: u128) -> Result<(), StablePoolError> { + fn ramp_amp_coef( + &mut self, + future_amp_coef: u128, + future_time_ts: u64, + ) -> Result<(), StablePoolError> { self.ensure_owner()?; - validate_amp_coef(amp_coef)?; - self.pool.amp_coef = amp_coef; - self.env().emit_event(AmpCoefChanged { - new_amp_coef: amp_coef, - }); + self.pool + .amp_coef + .ramp_amp_coef(future_amp_coef, future_time_ts)?; + + Ok(()) + } + + #[ink(message)] + fn stop_ramp_amp_coef(&mut self) -> Result<(), StablePoolError> { + self.ensure_owner()?; + self.pool.amp_coef.stop_ramp_amp_coef()?; Ok(()) } @@ -812,8 +812,8 @@ pub mod stable_pool { } #[ink(message)] - fn amp_coef(&self) -> u128 { - self.pool.amp_coef + fn amp_coef(&self) -> Result { + Ok(self.pool.amp_coef.compute_amp_coef()?) } #[ink(message)] @@ -860,7 +860,7 @@ pub mod stable_pool { token_out_id, &self.reserves(), &self.pool.fees, - self.amp_coef(), + self.amp_coef()?, )?) } @@ -880,7 +880,7 @@ pub mod stable_pool { token_out_id, &self.reserves(), &self.pool.fees, - self.amp_coef(), + self.amp_coef()?, )?) } @@ -900,7 +900,7 @@ pub mod stable_pool { &self.reserves(), self.psp22.total_supply(), Some(&self.pool.fees), - self.amp_coef(), + self.amp_coef()?, )?) } @@ -932,7 +932,7 @@ pub mod stable_pool { &self.reserves(), self.psp22.total_supply(), Some(&self.pool.fees), - self.amp_coef(), + self.amp_coef()?, ) .map_err(StablePoolError::MathError) } diff --git a/amm/drink-tests/src/stable_swap_tests/tests_getters.rs b/amm/drink-tests/src/stable_swap_tests/tests_getters.rs index 3ac7cfd..e6aab2e 100644 --- a/amm/drink-tests/src/stable_swap_tests/tests_getters.rs +++ b/amm/drink-tests/src/stable_swap_tests/tests_getters.rs @@ -41,7 +41,7 @@ fn test_01(mut session: Session) { ); assert_eq!( stable_swap::amp_coef(&mut session, stable_swap), - amp_coef, + Ok(amp_coef), "Incorrect A" ); assert_eq!( diff --git a/amm/drink-tests/src/utils.rs b/amm/drink-tests/src/utils.rs index b017ca7..3b6fa1e 100644 --- a/amm/drink-tests/src/utils.rs +++ b/amm/drink-tests/src/utils.rs @@ -231,7 +231,7 @@ pub mod stable_swap { ) } - pub fn amp_coef(session: &mut Session, stable_pool: AccountId) -> u128 { + pub fn amp_coef(session: &mut Session, stable_pool: AccountId) -> Result { handle_ink_error( session .query(stable_pool_contract::Instance::from(stable_pool).amp_coef()) diff --git a/amm/traits/stable_pool.rs b/amm/traits/stable_pool.rs index 95f98c0..0f49f58 100644 --- a/amm/traits/stable_pool.rs +++ b/amm/traits/stable_pool.rs @@ -17,7 +17,7 @@ pub trait StablePool { /// Returns current value of amplification coefficient. #[ink(message)] - fn amp_coef(&self) -> u128; + fn amp_coef(&self) -> Result; /// Returns current trade and protocol fees in 1e9 precision. #[ink(message)] @@ -211,8 +211,18 @@ pub trait StablePool { #[ink(message)] fn set_fees(&mut self, trade_fee: u32, protocol_fee: u32) -> Result<(), StablePoolError>; + /// Ramp amplification coeficient to `future_amp_coef`. The ramping should finish at `future_time_ts` #[ink(message)] - fn set_amp_coef(&mut self, amp_coef: u128) -> Result<(), StablePoolError>; + fn ramp_amp_coef( + &mut self, + future_amp_coef: u128, + future_time_ts: u64, + ) -> Result<(), StablePoolError>; + + /// Stop ramping amplification coefficient. + /// If ramping is not in progress, it does not influence the A. + #[ink(message)] + fn stop_ramp_amp_coef(&mut self) -> Result<(), StablePoolError>; } #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] @@ -234,6 +244,10 @@ pub enum StablePoolError { IncorrectTokenCount, TooLargeTokenDecimal, InvalidFee, + AmpCoefTooLow, + AmpCoefTooHigh, + AmpCoefRampDurationTooShort, + AmpCoefChangeTooLarge, } impl From for StablePoolError { diff --git a/helpers/constants.rs b/helpers/constants.rs index 2486b39..b6c6ff6 100644 --- a/helpers/constants.rs +++ b/helpers/constants.rs @@ -7,11 +7,6 @@ pub mod stable_pool { pub const RATE_DECIMALS: u8 = 12; pub const RATE_PRECISION: u128 = 10u128.pow(RATE_DECIMALS as u32); - /// Min amplification coefficient. - pub const MIN_AMP: u128 = 1; - /// Max amplification coefficient. - pub const MAX_AMP: u128 = 1_000_000; - /// Given as an integer with 1e9 precision (1%) pub const MAX_TRADE_FEE: u32 = 10_000_000; /// Given as an integer with 1e9 precision (50%) @@ -24,4 +19,13 @@ pub mod stable_pool { /// Maximum number coins (PSP22 token contracts) in the pool. pub const MAX_COINS: usize = 8; + + /// Minimum ramp duration, in milisec (24h). + pub const MIN_RAMP_DURATION: u64 = 86400000; + /// Min amplification coefficient. + pub const MIN_AMP: u128 = 1; + /// Max amplification coefficient. + pub const MAX_AMP: u128 = 1_000_000; + /// Max amplification change (how many times it can increase/decrease compared to current value). + pub const MAX_AMP_CHANGE: u128 = 10; }