From 106e2063a57d77c194f19907d21c314f7cb67e22 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Tue, 24 Sep 2024 12:56:42 +0200 Subject: [PATCH 1/9] stable pool liquidity methods --- amm/contracts/router_v2/lib.rs | 82 +++++++++++-- amm/contracts/router_v2/pair.rs | 7 +- amm/contracts/router_v2/stable_pool.rs | 163 ++++++++++++++++++++++++- amm/contracts/router_v2/utils.rs | 13 +- amm/traits/router_v2.rs | 56 +++++++++ 5 files changed, 307 insertions(+), 14 deletions(-) diff --git a/amm/contracts/router_v2/lib.rs b/amm/contracts/router_v2/lib.rs index 1bbbee8a..7d904248 100755 --- a/amm/contracts/router_v2/lib.rs +++ b/amm/contracts/router_v2/lib.rs @@ -12,7 +12,7 @@ pub struct CallerIsNotOwner; #[ink::contract] pub mod router_v2 { use crate::{ - pool::{Pair, Pool}, + pool::{Pair, Pool, StablePool}, utils::*, }; use amm_helpers::ensure; @@ -60,6 +60,16 @@ pub mod router_v2 { } } } + + /// Returns StablePool for `pool_id`. + #[inline] + fn get_stable_pool(&mut self, pool_id: AccountId) -> Result { + match self.get_pool(pool_id) { + Ok(Pool::StablePool(pool)) => Ok(pool), + _ => Err(RouterV2Error::PoolNotFound), + } + } + /// Returns Pair for `pool_id`. /// If `pool_id` is `None`, it creates a new Pair for /// `(token_0, token_1)` tokens if the Pair does not @@ -284,9 +294,7 @@ pub mod router_v2 { self.swap(&amounts, &path, wnative, self.env().account_id())?; let native_out = amounts[amounts.len() - 1]; withdraw(wnative, native_out)?; - self.env() - .transfer(to, native_out) - .map_err(|_| RouterV2Error::TransferError)?; + transfer_native(to, native_out)?; Ok(amounts) } @@ -315,9 +323,7 @@ pub mod router_v2 { )?; self.swap(&amounts, &path, wnative, self.env().account_id())?; withdraw(wnative, native_out)?; - self.env() - .transfer(to, native_out) - .map_err(|_| RouterV2Error::TransferError)?; + transfer_native(to, native_out)?; Ok(amounts) } @@ -470,6 +476,68 @@ pub mod router_v2 { deadline, ) } + + // ----------- STABLE POOL LIQUIDITY METHODS ----------- // + + #[ink(message, payable)] + fn add_stable_pool_liquidity( + &mut self, + pool: AccountId, + min_share_amount: u128, + amounts: Vec, + to: AccountId, + deadline: u64, + native: bool, + ) -> Result<(u128, u128), RouterV2Error> { + let wnative = if native { Some(self.wnative) } else { None }; + self.get_stable_pool(pool)?.add_liquidity( + min_share_amount, + amounts, + to, + deadline, + wnative, + ) + } + + #[ink(message)] + fn remove_stable_pool_liquidity( + &mut self, + pool: AccountId, + max_share_amount: u128, + amounts: Vec, + to: AccountId, + deadline: u64, + native: bool, + ) -> Result<(u128, u128), RouterV2Error> { + let wnative = if native { Some(self.wnative) } else { None }; + self.get_stable_pool(pool)?.remove_liquidity( + max_share_amount, + amounts, + to, + deadline, + wnative, + ) + } + + #[ink(message)] + fn remove_stable_pool_liquidity_by_share( + &mut self, + pool: AccountId, + share_amount: u128, + min_amounts: Vec, + to: AccountId, + deadline: u64, + native: bool, + ) -> Result, RouterV2Error> { + let wnative = if native { Some(self.wnative) } else { None }; + self.get_stable_pool(pool)?.remove_liquidity_by_share( + share_amount, + min_amounts, + to, + deadline, + wnative, + ) + } } #[cfg(test)] diff --git a/amm/contracts/router_v2/pair.rs b/amm/contracts/router_v2/pair.rs index 25a689b5..24274544 100644 --- a/amm/contracts/router_v2/pair.rs +++ b/amm/contracts/router_v2/pair.rs @@ -3,7 +3,7 @@ use amm_helpers::{ensure, math::casted_mul}; use ink::{ codegen::TraitCallBuilder, contract_ref, - env::{account_id, caller, transfer, transferred_value, DefaultEnvironment as Env}, + env::{account_id, caller, transferred_value, DefaultEnvironment as Env}, primitives::AccountId, }; use traits::{Balance, MathError, Pair as PairTrait, RouterV2Error}; @@ -189,8 +189,7 @@ impl Pair { let liquidity = self.contract_ref().mint(to)?; if received_value > amount_native { - transfer::(caller, received_value - amount_native) - .map_err(|_| RouterV2Error::TransferError)?; + transfer_native(caller, received_value - amount_native)?; } Ok((amount_0, amount_native, liquidity)) @@ -244,7 +243,7 @@ impl Pair { )?; psp22_transfer(token, to, amount_token)?; withdraw(wnative, amount_native)?; - transfer::(to, amount_native).map_err(|_| RouterV2Error::TransferError)?; + transfer_native(to, amount_native)?; Ok((amount_token, amount_native)) } diff --git a/amm/contracts/router_v2/stable_pool.rs b/amm/contracts/router_v2/stable_pool.rs index 417a2844..9d49f63e 100644 --- a/amm/contracts/router_v2/stable_pool.rs +++ b/amm/contracts/router_v2/stable_pool.rs @@ -1,9 +1,17 @@ use amm_helpers::ensure; use ink::{ - codegen::TraitCallBuilder, contract_ref, env::DefaultEnvironment as Env, prelude::vec::Vec, + codegen::TraitCallBuilder, + contract_ref, + env::{account_id, caller, transferred_value, DefaultEnvironment as Env}, + prelude::vec::Vec, primitives::AccountId, }; -use traits::{RouterV2Error, StablePool as StablePoolTrait}; +use traits::{RouterV2Error, StablePool as StablePoolTrait, StablePoolError}; + +use crate::utils::{ + check_timestamp, psp22_approve, psp22_transfer, psp22_transfer_from, transfer_native, withdraw, + wrap, +}; #[derive(scale::Decode, scale::Encode)] #[cfg_attr( @@ -31,6 +39,13 @@ impl StablePool { }, Err(_) => return None, }; + // set spending allowance of each token for the pool to `u128::MAX` + // required for adding liquidity + for &token in &tokens { + if psp22_approve(token, pool_id, u128::MAX).is_err() { + return None; + }; + } Some(Self { id: pool_id, tokens, @@ -41,6 +56,143 @@ impl StablePool { self.id.into() } + /// Adds liquidity to the pool. + /// + /// If `wnative` is specified, it attemps to wrap the transferred native token + /// and use it instead of transferring the wrapped version. + pub fn add_liquidity( + &self, + min_share_amount: u128, + amounts: Vec, + to: AccountId, + deadline: u64, + wnative: Option, + ) -> Result<(u128, u128), RouterV2Error> { + check_timestamp(deadline)?; + ensure!( + self.tokens.len() == amounts.len(), + RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) + ); + let wnative_idx = match wnative { + Some(wnative) => { + let wnative_idx = self.wnative_idx(wnative)?; + let native_received = transferred_value::(); + let wnative_amount = amounts[wnative_idx]; + ensure!( + native_received >= wnative_amount, + RouterV2Error::InsufficientTransferredAmount + ); + wrap(wnative, wnative_amount)?; + if native_received > wnative_amount { + transfer_native(caller::(), native_received - wnative_amount)?; + } + wnative_idx + } + None => self.tokens.len(), + }; + for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { + psp22_transfer_from( + self.tokens[i], + caller::(), + account_id::(), + amounts[i], + )?; + } + Ok(self + .contract_ref() + .add_liquidity(min_share_amount, amounts, to)?) + } + + /// Withdraws liquidity from the pool by the specified amounts. + /// + /// If `wnative` is specified, it attemps to unwrap the wrapped native token + /// and withdraw it to the `to` account. + pub fn remove_liquidity( + &self, + max_share_amount: u128, + amounts: Vec, + to: AccountId, + deadline: u64, + wnative: Option, + ) -> Result<(u128, u128), RouterV2Error> { + check_timestamp(deadline)?; + ensure!( + self.tokens.len() == amounts.len(), + RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) + ); + psp22_transfer_from( + self.id, + caller::(), + account_id::(), + max_share_amount, + )?; + let (lp_burned, fee_part) = match wnative { + Some(wnative) => { + let wnative_idx = self.wnative_idx(wnative)?; + let res = self.contract_ref().remove_liquidity_by_amounts( + max_share_amount, + amounts.clone(), + account_id::(), + )?; + withdraw(wnative, amounts[wnative_idx])?; + transfer_native(to, amounts[wnative_idx])?; + for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { + psp22_transfer(self.tokens[i], to, amounts[i])?; + } + res + } + None => { + self.contract_ref() + .remove_liquidity_by_amounts(max_share_amount, amounts, to)? + } + }; + if max_share_amount > lp_burned { + psp22_transfer(self.id, caller::(), max_share_amount - lp_burned)?; + } + Ok((lp_burned, fee_part)) + } + + /// Withdraws liquidity from the pool in balanced propotions. + /// + /// If `wnative` is specified, it attemps to unwrap the wrapped native token + /// and withdraw it to the `to` account. + pub fn remove_liquidity_by_share( + &self, + share_amount: u128, + min_amounts: Vec, + to: AccountId, + deadline: u64, + wnative: Option, + ) -> Result, RouterV2Error> { + check_timestamp(deadline)?; + ensure!( + self.tokens.len() == min_amounts.len(), + RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) + ); + psp22_transfer_from(self.id, caller::(), account_id::(), share_amount)?; + match wnative { + Some(wnative) => { + let wnative_idx = self.wnative_idx(wnative)?; + let amounts = self.contract_ref().remove_liquidity_by_shares( + share_amount, + min_amounts, + account_id::(), + )?; + withdraw(wnative, amounts[wnative_idx])?; + transfer_native(to, amounts[wnative_idx])?; + for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { + psp22_transfer(self.tokens[i], to, amounts[i])?; + } + Ok(amounts) + } + None => { + Ok(self + .contract_ref() + .remove_liquidity_by_shares(share_amount, min_amounts, to)?) + } + } + } + pub fn swap( &self, token_in: AccountId, @@ -94,4 +246,11 @@ impl StablePool { Err(err) => Err(err.into()), } } + + fn wnative_idx(&self, wnative: AccountId) -> Result { + self.tokens + .iter() + .position(|&token| wnative == token) + .ok_or(RouterV2Error::InvalidToken) + } } diff --git a/amm/contracts/router_v2/utils.rs b/amm/contracts/router_v2/utils.rs index 518b447f..5be358e4 100644 --- a/amm/contracts/router_v2/utils.rs +++ b/amm/contracts/router_v2/utils.rs @@ -2,7 +2,7 @@ use amm_helpers::ensure; use ink::{ codegen::TraitCallBuilder, contract_ref, - env::{block_timestamp, DefaultEnvironment as Env}, + env::{block_timestamp, transfer, DefaultEnvironment as Env}, prelude::{string::String, vec::Vec}, primitives::AccountId, }; @@ -34,6 +34,12 @@ pub fn psp22_transfer_from( token.transfer_from(from, to, value, Vec::new()) } +#[inline] +pub fn psp22_approve(token: AccountId, spender: AccountId, value: u128) -> Result<(), PSP22Error> { + let mut token: contract_ref!(PSP22, Env) = token.into(); + token.approve(spender, value) +} + #[inline] pub fn wrap(wnative: AccountId, value: Balance) -> Result<(), RouterV2Error> { let mut wnative_ref: contract_ref!(WrappedAZERO, Env) = wnative.into(); @@ -53,3 +59,8 @@ pub fn withdraw(wnative: AccountId, value: Balance) -> Result<(), RouterV2Error> let mut wnative_ref: contract_ref!(WrappedAZERO, Env) = wnative.into(); Ok(wnative_ref.withdraw(value)?) } + +#[inline] +pub fn transfer_native(to: AccountId, amount: u128) -> Result<(), RouterV2Error> { + transfer::(to, amount).map_err(|_| RouterV2Error::TransferError) +} diff --git a/amm/traits/router_v2.rs b/amm/traits/router_v2.rs index 0315f742..3dbba490 100644 --- a/amm/traits/router_v2.rs +++ b/amm/traits/router_v2.rs @@ -24,6 +24,8 @@ pub trait RouterV2 { #[ink(message)] fn wnative(&self) -> AccountId; + // ----------- PAIR LIQUIDITY METHODS ----------- // + /// Adds liquidity to the `pair`. /// /// If `pair` is `None` then it creates a new pair for @@ -108,6 +110,58 @@ pub trait RouterV2 { deadline: u64, ) -> Result<(u128, Balance), RouterV2Error>; + // ----------- STABLE POOL LIQUIDITY METHODS ----------- // + + /// Adds liquidity to the stable pool. + /// + /// If `native` is true, it attemps to wrap the transferred native token + /// and use it instead of transferring the wrapped version. + /// Fails if `native` is true but the pool does not have wrapped native token. + #[ink(message, payable)] + fn add_stable_pool_liquidity( + &mut self, + pool: AccountId, + min_share_amount: u128, + amounts: Vec, + to: AccountId, + deadline: u64, + native: bool, + ) -> Result<(u128, u128), RouterV2Error>; + + /// Withdraws liquidity from the stable pool by the specified amounts. + /// + /// If `native` is true, it attemps to unwrap the wrapped native token + /// and withdraw it to the `to` account. + /// Fails if `native` is true but the pool does not have wrapped native token. + #[ink(message)] + fn remove_stable_pool_liquidity( + &mut self, + pool: AccountId, + max_share_amount: u128, + amounts: Vec, + to: AccountId, + deadline: u64, + native: bool, + ) -> Result<(u128, u128), RouterV2Error>; + + /// Withdraws liquidity from the stable pool in balanced propotions. + /// + /// If `native` is true, it attemps to unwrap the wrapped native token + /// and withdraw it to the `to` account. + /// Fails if `native` is true but the pool does not have wrapped native token. + #[ink(message)] + fn remove_stable_pool_liquidity_by_share( + &mut self, + pool: AccountId, + share_amount: u128, + min_amounts: Vec, + to: AccountId, + deadline: u64, + native: bool, + ) -> Result, RouterV2Error>; + + // ----------- SWAP METHODS ----------- // + /// Exchanges tokens along the `path` to `token_out`. /// /// Starts with `amount_in` and token address under `path[0].token_in`. @@ -240,9 +294,11 @@ pub enum RouterV2Error { InvalidPath, InvalidToken, PairNotFound, + PoolNotFound, TransferError, ExcessiveInputAmount, + InsufficientTransferredAmount, InsufficientAmount, InsufficientOutputAmount, InsufficientAmountA, From ba7557f8b307a61b692281dd06b60c6b3531e5c0 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Tue, 24 Sep 2024 12:57:39 +0200 Subject: [PATCH 2/9] test cache stable --- amm/drink-tests/src/router_v2_tests.rs | 65 ++++++++++++++++++++++++++ amm/drink-tests/src/utils.rs | 33 +++++++++++++ 2 files changed, 98 insertions(+) diff --git a/amm/drink-tests/src/router_v2_tests.rs b/amm/drink-tests/src/router_v2_tests.rs index 3b2d0317..e9b0401f 100644 --- a/amm/drink-tests/src/router_v2_tests.rs +++ b/amm/drink-tests/src/router_v2_tests.rs @@ -110,6 +110,71 @@ fn test_cache_stable_pool(mut session: Session) { ); } +/// Tests that a StablePool is cached in the Router +/// with the first liquidity deposit. +#[drink::test] +fn test_cache_stable_pool_with_add_liquidity(mut session: Session) { + upload_all(&mut session); + + // Fix timestamp. Otherwise underlying UNIX clock is used. + let now = get_timestamp(&mut session); + set_timestamp(&mut session, now); + + let (router, _, _, _) = setup_router(&mut session); + + let initial_reserves = vec![U100K * ONE_USDT, U100K * ONE_USDC]; + let initial_supply = initial_reserves + .iter() + .map(|amount| amount * U1M) + .collect::>(); + + let (usdt_usdc_pool, tokens) = setup_stable_swap_with_tokens( + &mut session, + vec![6, 6], + initial_supply, + 10_000, + 2_500_000, + 200_000_000, + BOB, + vec![], + ); + + let (usdt, usdc) = (tokens[0], tokens[1]); + + psp22_utils::increase_allowance(&mut session, usdt, router.into(), u128::MAX, BOB) + .expect("Should increase allowance"); + psp22_utils::increase_allowance(&mut session, usdc, router.into(), u128::MAX, BOB) + .expect("Should increase allowance"); + + // ensure that the pool is not cached before the swap + let res = router_v2::get_cached_pool(&mut session, router.into(), usdt_usdc_pool); + assert_eq!(res, None, "StablePool should not be in the cache"); + + router_v2::add_stable_swap_liquidity( + &mut session, + router.into(), + usdt_usdc_pool, + 1, + initial_reserves.clone(), + bob(), + false, + 0, + BOB, + ) + .expect("Should add liquidity"); + + let res = router_v2::get_cached_pool(&mut session, router.into(), usdt_usdc_pool) + .expect("Should return cached StablePool"); + assert_eq!( + res, + Pool::StablePool(StablePool { + id: usdt_usdc_pool, + tokens + }), + "StablePool cache mismatch" + ); +} + /// Tests that a Pair is cached in the Router /// with the first swap. #[drink::test] diff --git a/amm/drink-tests/src/utils.rs b/amm/drink-tests/src/utils.rs index 68f5139e..f316ed7f 100644 --- a/amm/drink-tests/src/utils.rs +++ b/amm/drink-tests/src/utils.rs @@ -342,6 +342,39 @@ pub mod router_v2 { .unwrap() } + pub fn add_stable_swap_liquidity( + session: &mut Session, + router: AccountId, + pool: AccountId, + min_share_amount: u128, + amounts: Vec, + to: AccountId, + native: bool, + native_amount: u128, + caller: drink::AccountId32, + ) -> Result<(u128, u128), RouterV2Error> { + let now = get_timestamp(session); + let deadline = now + 10; + let _ = session.set_actor(caller); + + session + .execute( + router_v2_contract::Instance::from(router) + .add_stable_pool_liquidity( + pool, + min_share_amount, + amounts, + to, + deadline, + native, + ) + .with_value(native_amount), + ) + .unwrap() + .result + .unwrap() + } + pub fn get_cached_pool( session: &mut Session, router: AccountId, From ea7cc445455232525410160c01fbc22235873230 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Tue, 24 Sep 2024 15:14:33 +0200 Subject: [PATCH 3/9] stable liquidity tests --- amm/drink-tests/src/router_v2_tests.rs | 238 ++++++++++++++++++++++++- amm/drink-tests/src/utils.rs | 62 +++++++ 2 files changed, 293 insertions(+), 7 deletions(-) diff --git a/amm/drink-tests/src/router_v2_tests.rs b/amm/drink-tests/src/router_v2_tests.rs index e9b0401f..65159a3f 100644 --- a/amm/drink-tests/src/router_v2_tests.rs +++ b/amm/drink-tests/src/router_v2_tests.rs @@ -5,6 +5,7 @@ use crate::utils::*; use crate::{factory_contract, pair_contract, router_v2_contract, wrapped_azero}; use drink::{runtime::MinimalRuntime, Weight}; +use ink_primitives::AccountId; use ink_wrapper_types::ToAccountId; use pair_contract::Pair as _; use router_v2_contract::{Pair, Pool, StablePool, Step}; @@ -216,7 +217,7 @@ fn test_cache_pair(mut session: Session) { .result .unwrap() .expect("Should mint"); - + // ensure that the pair is not cached before the swap let res = router_v2::get_cached_pool(&mut session, router.into(), ice_wood_pair.into()); assert_eq!(res, None, "Pair should not be in the cache"); @@ -336,9 +337,9 @@ fn test_create_and_cache_pair_with_add_liqudity(mut session: Session) { let wood = psp22_utils::setup(&mut session, WOOD.to_string(), BOB); psp22_utils::increase_allowance(&mut session, ice.into(), router.into(), u128::MAX, BOB) - .unwrap(); + .expect("Should increase allowance"); psp22_utils::increase_allowance(&mut session, wood.into(), router.into(), u128::MAX, BOB) - .unwrap(); + .expect("Should increase allowance"); router_v2::add_pair_liquidity( &mut session, @@ -592,7 +593,7 @@ fn test_native_in_swap(mut session: Session) { .expect("Should increase allowance"); let stable_amount = U100K * ONE_USDC; - let native_amount = U100K * TOKEN; + let native_amount = U100K * ONE_AZERO; router_v2::add_pair_liquidity_native( &mut session, @@ -638,7 +639,7 @@ fn test_native_in_swap(mut session: Session) { 10 * gas_limit.proof_size(), )); - let native_amount = 100 * TOKEN; + let native_amount = 100 * ONE_AZERO; let router_native_balance = native_balance_of(&mut session, router.into()); @@ -668,7 +669,7 @@ fn test_native_in_swap(mut session: Session) { .expect("Should swap"); let swap_amount = 100 * TOKEN; - let native_amount = 120 * TOKEN; + let native_amount = 120 * ONE_AZERO; router_v2::swap_native_for_exact_tokens( &mut session, @@ -766,7 +767,7 @@ fn test_native_out_swap(mut session: Session) { .expect("Should increase allowance"); let stable_amount = U100K * ONE_USDC; - let native_amount = U100K * TOKEN; + let native_amount = U100K * ONE_AZERO; router_v2::add_pair_liquidity_native( &mut session, @@ -840,6 +841,8 @@ fn test_native_out_swap(mut session: Session) { ) .expect("Should swap"); + let swap_amount = 100 * ONE_AZERO; + router_v2::swap_tokens_for_exact_native( &mut session, router.into(), @@ -878,3 +881,224 @@ fn test_native_out_swap(mut session: Session) { "Router native balance should not change" ); } + +/// Tests StablePool add liquidity PSP22 tokens +#[drink::test] +fn test_stable_pool_liqudity(mut session: Session) { + upload_all(&mut session); + + // seed test accounts with some native token + seed_account(&mut session, BOB); + + // Fix timestamp. Otherwise underlying UNIX clock is used. + let now = get_timestamp(&mut session); + set_timestamp(&mut session, now); + + let (router, _, _, _) = setup_router(&mut session); + + // setup stable pool + let initial_reserves = vec![U100K * ONE_USDT, U100K * ONE_USDC]; + let initial_supply = initial_reserves + .iter() + .map(|amount| amount * U1M) + .collect::>(); + + let (usdt_usdc_pool, tokens) = setup_stable_swap_with_tokens( + &mut session, + vec![6, 6], + initial_supply.clone(), + A, + TRADE_FEE, + PROTOCOL_FEE, + BOB, + vec![], + ); + + let (usdt, usdc) = (tokens[0], tokens[1]); + + // add liquidity via Router + psp22_utils::increase_allowance(&mut session, usdt, router.into(), u128::MAX, BOB) + .expect("Should increase allowance"); + psp22_utils::increase_allowance(&mut session, usdc, router.into(), u128::MAX, BOB) + .expect("Should increase allowance"); + router_v2::add_stable_swap_liquidity( + &mut session, + router.into(), + usdt_usdc_pool, + 1, + initial_reserves.clone(), + bob(), + false, + 0, + BOB, + ) + .expect("Should successfully add liquidity"); + + psp22_utils::increase_allowance(&mut session, usdt_usdc_pool, router.into(), u128::MAX, BOB) + .expect("Should increase allowance"); + + let to_withdraw = vec![100 * ONE_USDT, 100 * ONE_USDC]; + let (max_share, _) = stable_swap::get_burn_liquidity_for_amounts( + &mut session, + usdt_usdc_pool, + to_withdraw.clone(), + ) + .expect("Should estimate burn liquidity"); + + router_v2::remove_stable_pool_liquidity( + &mut session, + router.into(), + usdt_usdc_pool, + max_share, + to_withdraw, + bob(), + false, + BOB, + ) + .expect("Should successfully add liquidity"); + + let share_amount = psp22_utils::balance_of(&mut session, usdt_usdc_pool, bob()) / 100; // ~1% + let min_amounts = + stable_swap::get_amounts_for_liquidity_burn(&mut session, usdt_usdc_pool, share_amount) + .expect("Should estimate amounts"); + + router_v2::remove_stable_pool_liquidity_by_share( + &mut session, + router.into(), + usdt_usdc_pool, + share_amount, + min_amounts, + bob(), + false, + BOB, + ) + .expect("Should successfully add liquidity"); + + for token in [usdt, usdc] { + assert_eq!( + psp22_utils::balance_of(&mut session, token, router.into()), + 0, + "Router should not hold any tokens" + ); + } +} + +/// Tests StablePool add liquidity with native token +#[drink::test] +fn test_stable_pool_liqudity_native(mut session: Session) { + upload_all(&mut session); + + // seed test accounts with some native token + seed_account(&mut session, BOB); + + // Fix timestamp. Otherwise underlying UNIX clock is used. + let now = get_timestamp(&mut session); + set_timestamp(&mut session, now); + + // setup router and tokens + let (router, _, wnative, _) = setup_router(&mut session); + let sazero = session + .instantiate(crate::psp22::Instance::new( + 1_000_000u128 * ONE_AZERO, + Some("SAZERO".to_string()), + Some("SAZERO".to_string()), + 12, + )) + .unwrap() + .result + .to_account_id() + .into(); + + // setup stable pool + let initial_reserves = vec![U100K * ONE_AZERO, U100K * ONE_AZERO]; + + let wazero_sazero_pool: AccountId = stable_swap::setup( + &mut session, + vec![wnative.into(), sazero], + vec![12, 12], + A, + BOB, + TRADE_FEE, + PROTOCOL_FEE, + Some(fee_receiver()), + ) + .into(); + + let router_native_balance = native_balance_of(&mut session, router.into()); + + // add liquidity via Router + psp22_utils::increase_allowance(&mut session, sazero, router.into(), u128::MAX, BOB) + .expect("Should increase allowance"); + router_v2::add_stable_swap_liquidity( + &mut session, + router.into(), + wazero_sazero_pool, + 1, + initial_reserves.clone(), + bob(), + true, + initial_reserves[0], + BOB, + ) + .expect("Should successfully add liquidity"); + + psp22_utils::increase_allowance( + &mut session, + wazero_sazero_pool, + router.into(), + u128::MAX, + BOB, + ) + .expect("Should increase allowance"); + + let to_withdraw = vec![100 * ONE_AZERO, 100 * ONE_AZERO]; + let (max_share, _) = stable_swap::get_burn_liquidity_for_amounts( + &mut session, + wazero_sazero_pool, + to_withdraw.clone(), + ) + .expect("Should estimate burn liquidity"); + + router_v2::remove_stable_pool_liquidity( + &mut session, + router.into(), + wazero_sazero_pool, + max_share, + to_withdraw, + bob(), + true, + BOB, + ) + .expect("Should successfully add liquidity"); + + let share_amount = psp22_utils::balance_of(&mut session, wazero_sazero_pool, bob()) / 100; // ~1% + let min_amounts = + stable_swap::get_amounts_for_liquidity_burn(&mut session, wazero_sazero_pool, share_amount) + .expect("Should estimate amounts"); + + router_v2::remove_stable_pool_liquidity_by_share( + &mut session, + router.into(), + wazero_sazero_pool, + share_amount, + min_amounts, + bob(), + true, + BOB, + ) + .expect("Should successfully add liquidity"); + + for token in [wnative.into(), sazero] { + assert_eq!( + psp22_utils::balance_of(&mut session, token, router.into()), + 0, + "Router should not hold any tokens" + ); + } + + assert_eq!( + router_native_balance, + native_balance_of(&mut session, router.into()), + "Router native balance should not change" + ); +} diff --git a/amm/drink-tests/src/utils.rs b/amm/drink-tests/src/utils.rs index f316ed7f..80dd1504 100644 --- a/amm/drink-tests/src/utils.rs +++ b/amm/drink-tests/src/utils.rs @@ -15,6 +15,8 @@ pub const EVA: drink::AccountId32 = AccountId32::new([5u8; 32]); pub const TOKEN: u128 = 10u128.pow(18); +pub const ONE_AZERO: u128 = 10u128.pow(12); + pub const FEE_RECEIVER: AccountId32 = AccountId32::new([42u8; 32]); pub fn fee_receiver() -> ink_primitives::AccountId { @@ -375,6 +377,66 @@ pub mod router_v2 { .unwrap() } + pub fn remove_stable_pool_liquidity( + session: &mut Session, + router: AccountId, + pool: AccountId, + max_share_amount: u128, + amounts: Vec, + to: AccountId, + native: bool, + caller: drink::AccountId32, + ) -> Result<(u128, u128), RouterV2Error> { + let now = get_timestamp(session); + let deadline = now + 10; + let _ = session.set_actor(caller); + + session + .execute( + router_v2_contract::Instance::from(router).remove_stable_pool_liquidity( + pool, + max_share_amount, + amounts, + to, + deadline, + native, + ), + ) + .unwrap() + .result + .unwrap() + } + + pub fn remove_stable_pool_liquidity_by_share( + session: &mut Session, + router: AccountId, + pool: AccountId, + share_amount: u128, + min_amounts: Vec, + to: AccountId, + native: bool, + caller: drink::AccountId32, + ) -> Result, RouterV2Error> { + let now = get_timestamp(session); + let deadline = now + 10; + let _ = session.set_actor(caller); + + session + .execute( + router_v2_contract::Instance::from(router).remove_stable_pool_liquidity_by_share( + pool, + share_amount, + min_amounts, + to, + deadline, + native, + ), + ) + .unwrap() + .result + .unwrap() + } + pub fn get_cached_pool( session: &mut Session, router: AccountId, From f8a20ed0247fec21de5df08deaffd640c4fad85a Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Tue, 24 Sep 2024 16:18:42 +0200 Subject: [PATCH 4/9] return surplus of native token on liquidity stable mint --- amm/contracts/router_v2/pair.rs | 2 ++ amm/contracts/router_v2/stable_pool.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/amm/contracts/router_v2/pair.rs b/amm/contracts/router_v2/pair.rs index 24274544..fedbe401 100644 --- a/amm/contracts/router_v2/pair.rs +++ b/amm/contracts/router_v2/pair.rs @@ -63,6 +63,8 @@ impl Pair { /// Makes a cross-contract call to fetch the Pair reserves. /// Returns reserves `(reserve_0, reserve_1)` in order of `token_0` and `token_1` + /// NOTE: before calling this method ensure that `token_0` and `token_1` belong to + /// this `Pair` pool fn get_reserves(&self, token_0: &AccountId, token_1: &AccountId) -> (u128, u128) { let (reserve_0, reserve_1, _) = self.contract_ref().get_reserves(); if token_0 < token_1 { diff --git a/amm/contracts/router_v2/stable_pool.rs b/amm/contracts/router_v2/stable_pool.rs index 9d49f63e..002ac04b 100644 --- a/amm/contracts/router_v2/stable_pool.rs +++ b/amm/contracts/router_v2/stable_pool.rs @@ -73,23 +73,23 @@ impl StablePool { self.tokens.len() == amounts.len(), RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) ); - let wnative_idx = match wnative { + let native_received = transferred_value::(); + let (wnative_idx, native_surplus) = match wnative { Some(wnative) => { let wnative_idx = self.wnative_idx(wnative)?; - let native_received = transferred_value::(); let wnative_amount = amounts[wnative_idx]; ensure!( native_received >= wnative_amount, RouterV2Error::InsufficientTransferredAmount ); wrap(wnative, wnative_amount)?; - if native_received > wnative_amount { - transfer_native(caller::(), native_received - wnative_amount)?; - } - wnative_idx + (wnative_idx, native_received.saturating_sub(wnative_amount)) } - None => self.tokens.len(), + None => (self.tokens.len(), native_received), }; + if native_surplus > 0 { + transfer_native(caller::(), native_surplus)?; + } for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { psp22_transfer_from( self.tokens[i], From eff301ac3ccb349d46e4a84fa96758df2f7fb1d9 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Wed, 25 Sep 2024 17:06:49 +0200 Subject: [PATCH 5/9] extract code to fn --- amm/contracts/router_v2/stable_pool.rs | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/amm/contracts/router_v2/stable_pool.rs b/amm/contracts/router_v2/stable_pool.rs index 7f6cc160..3981644d 100644 --- a/amm/contracts/router_v2/stable_pool.rs +++ b/amm/contracts/router_v2/stable_pool.rs @@ -1,3 +1,4 @@ +use amm_helpers::ensure; use ink::{ codegen::TraitCallBuilder, contract_ref, @@ -133,11 +134,7 @@ impl StablePool { amounts.clone(), account_id::(), )?; - withdraw(wnative, amounts[wnative_idx])?; - transfer_native(to, amounts[wnative_idx])?; - for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { - psp22_transfer(self.tokens[i], to, amounts[i])?; - } + self.handle_token_transfers(&amounts, to, wnative_idx)?; res } None => { @@ -177,11 +174,7 @@ impl StablePool { min_amounts, account_id::(), )?; - withdraw(wnative, amounts[wnative_idx])?; - transfer_native(to, amounts[wnative_idx])?; - for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { - psp22_transfer(self.tokens[i], to, amounts[i])?; - } + self.handle_token_transfers(&amounts, to, wnative_idx)?; Ok(amounts) } None => { @@ -234,4 +227,18 @@ impl StablePool { .position(|&token| wnative == token) .ok_or(RouterV2Error::InvalidToken) } + + fn handle_token_transfers( + &self, + amounts: &[u128], + to: AccountId, + wnative_idx: usize, + ) -> Result<(), RouterV2Error> { + withdraw(self.tokens[wnative_idx], amounts[wnative_idx])?; + transfer_native(to, amounts[wnative_idx])?; + for i in (0..self.tokens.len()).filter(|&idx| idx != wnative_idx) { + psp22_transfer(self.tokens[i], to, amounts[i])?; + } + Ok(()) + } } From b02cb14878808bc878038f7ee455b3c1437d5016 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Wed, 25 Sep 2024 17:07:59 +0200 Subject: [PATCH 6/9] remove unecessary checks --- amm/contracts/router_v2/stable_pool.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/amm/contracts/router_v2/stable_pool.rs b/amm/contracts/router_v2/stable_pool.rs index 3981644d..a80f44c6 100644 --- a/amm/contracts/router_v2/stable_pool.rs +++ b/amm/contracts/router_v2/stable_pool.rs @@ -6,7 +6,7 @@ use ink::{ prelude::vec::Vec, primitives::AccountId, }; -use traits::{RouterV2Error, StablePool as StablePoolTrait, StablePoolError}; +use traits::{RouterV2Error, StablePool as StablePoolTrait}; use crate::utils::{ check_timestamp, psp22_approve, psp22_transfer, psp22_transfer_from, transfer_native, withdraw, @@ -69,10 +69,6 @@ impl StablePool { wnative: Option, ) -> Result<(u128, u128), RouterV2Error> { check_timestamp(deadline)?; - ensure!( - self.tokens.len() == amounts.len(), - RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) - ); let native_received = transferred_value::(); let (wnative_idx, native_surplus) = match wnative { Some(wnative) => { @@ -116,10 +112,6 @@ impl StablePool { wnative: Option, ) -> Result<(u128, u128), RouterV2Error> { check_timestamp(deadline)?; - ensure!( - self.tokens.len() == amounts.len(), - RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) - ); psp22_transfer_from( self.id, caller::(), @@ -161,10 +153,6 @@ impl StablePool { wnative: Option, ) -> Result, RouterV2Error> { check_timestamp(deadline)?; - ensure!( - self.tokens.len() == min_amounts.len(), - RouterV2Error::StablePoolError(StablePoolError::IncorrectAmountsCount) - ); psp22_transfer_from(self.id, caller::(), account_id::(), share_amount)?; match wnative { Some(wnative) => { From 779f4b42b2de8ad8e84cbfad248e3653be4f10d4 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Wed, 25 Sep 2024 17:44:57 +0200 Subject: [PATCH 7/9] refactor withdraw function --- amm/contracts/router_v2/utils.rs | 8 ++------ amm/traits/router_v2.rs | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/amm/contracts/router_v2/utils.rs b/amm/contracts/router_v2/utils.rs index 5be358e4..5f652de1 100644 --- a/amm/contracts/router_v2/utils.rs +++ b/amm/contracts/router_v2/utils.rs @@ -43,15 +43,11 @@ pub fn psp22_approve(token: AccountId, spender: AccountId, value: u128) -> Resul #[inline] pub fn wrap(wnative: AccountId, value: Balance) -> Result<(), RouterV2Error> { let mut wnative_ref: contract_ref!(WrappedAZERO, Env) = wnative.into(); - wnative_ref + Ok(wnative_ref .call_mut() .deposit() .transferred_value(value) - .try_invoke() - .map_err(|_| { - RouterV2Error::CrossContractCallFailed(String::from("Wrapped AZERO: deposit")) - })???; - Ok(()) + .invoke()?) } #[inline] diff --git a/amm/traits/router_v2.rs b/amm/traits/router_v2.rs index 14876fcb..416974c6 100644 --- a/amm/traits/router_v2.rs +++ b/amm/traits/router_v2.rs @@ -1,6 +1,6 @@ use crate::{Balance, FactoryError, MathError, PairError, StablePoolError}; use ink::{ - prelude::{string::String, vec::Vec}, + prelude::vec::Vec, primitives::AccountId, LangError, }; @@ -289,7 +289,6 @@ pub enum RouterV2Error { MathError(MathError), StablePoolError(StablePoolError), - CrossContractCallFailed(String), Expired, InvalidPoolAddress, InvalidToken, From 836d435c41719bef5397c44bfa1044885651a9f7 Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Wed, 25 Sep 2024 17:46:26 +0200 Subject: [PATCH 8/9] rename functions --- amm/contracts/router_v2/lib.rs | 43 +++++++++++++++++----------------- amm/traits/router_v2.rs | 2 -- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/amm/contracts/router_v2/lib.rs b/amm/contracts/router_v2/lib.rs index dbb98c96..05428b3b 100755 --- a/amm/contracts/router_v2/lib.rs +++ b/amm/contracts/router_v2/lib.rs @@ -50,7 +50,7 @@ pub mod router_v2 { /// Returns Pool for `pool_id` if it exists. /// Adds the Pool to the cache. #[inline] - fn get_pool(&mut self, pool_id: AccountId) -> Result { + fn get_and_cache_pool(&mut self, pool_id: AccountId) -> Result { match self.cached_pools.get(pool_id) { Some(pool) => Ok(pool), None => { @@ -62,11 +62,12 @@ pub mod router_v2 { } /// Returns StablePool for `pool_id`. + /// Adds the StablePool to the cache. #[inline] - fn get_stable_pool(&mut self, pool_id: AccountId) -> Result { - match self.get_pool(pool_id) { - Ok(Pool::StablePool(pool)) => Ok(pool), - _ => Err(RouterV2Error::PoolNotFound), + fn get_and_cache_stable_pool(&mut self, pool_id: AccountId) -> Result { + match self.get_and_cache_pool(pool_id)? { + Pool::StablePool(pool) => Ok(pool), + Pool::Pair(_) => Err(RouterV2Error::InvalidPoolAddress), } } @@ -76,7 +77,7 @@ pub mod router_v2 { /// exist in the pair Factory. /// Adds the Pair to the cache. #[inline] - fn get_pair( + fn get_and_cache_pair( &mut self, pool_id: Option, token_0: AccountId, @@ -86,9 +87,9 @@ pub mod router_v2 { Some(pool_id) => pool_id, None => self.pair_factory_ref().create_pair(token_0, token_1)?, }; - match self.get_pool(pool_id)? { + match self.get_and_cache_pool(pool_id)? { Pool::Pair(pair) => Ok(pair), - Pool::StablePool(_) => Err(RouterV2Error::PairNotFound), + Pool::StablePool(_) => Err(RouterV2Error::InvalidPoolAddress), } } @@ -101,7 +102,7 @@ pub mod router_v2 { ) -> Result<(), RouterV2Error> { let n_pools = path.len(); for i in 0..n_pools - 1 { - self.get_pool(path[i].pool_id)?.swap( + self.get_and_cache_pool(path[i].pool_id)?.swap( path[i].token_in, path[i + 1].token_in, amounts[i + 1], @@ -109,7 +110,7 @@ pub mod router_v2 { )?; } // If last pool in the path, transfer tokens to the `to` recipient. - self.get_pool(path[n_pools - 1].pool_id)?.swap( + self.get_and_cache_pool(path[n_pools - 1].pool_id)?.swap( path[n_pools - 1].token_in, token_out, amounts[n_pools], @@ -130,13 +131,13 @@ pub mod router_v2 { let n_pools = path.len(); let mut amounts = vec![0; n_pools + 1]; amounts[n_pools] = amount_out; - amounts[n_pools - 1] = self.get_pool(path[n_pools - 1].pool_id)?.get_amount_in( + amounts[n_pools - 1] = self.get_and_cache_pool(path[n_pools - 1].pool_id)?.get_amount_in( path[n_pools - 1].token_in, token_out, amount_out, )?; for i in (0..n_pools - 1).rev() { - amounts[i] = self.get_pool(path[i].pool_id)?.get_amount_in( + amounts[i] = self.get_and_cache_pool(path[i].pool_id)?.get_amount_in( path[i].token_in, path[i + 1].token_in, amounts[i + 1], @@ -159,13 +160,13 @@ pub mod router_v2 { let mut amounts = vec![0; n_pools + 1]; amounts[0] = amount_in; for i in 0..n_pools - 1 { - amounts[i + 1] = self.get_pool(path[i].pool_id)?.get_amount_out( + amounts[i + 1] = self.get_and_cache_pool(path[i].pool_id)?.get_amount_out( path[i].token_in, path[i + 1].token_in, amounts[i], )?; } - amounts[n_pools] = self.get_pool(path[n_pools - 1].pool_id)?.get_amount_out( + amounts[n_pools] = self.get_and_cache_pool(path[n_pools - 1].pool_id)?.get_amount_out( path[n_pools - 1].token_in, token_out, amounts[n_pools - 1], @@ -392,7 +393,7 @@ pub mod router_v2 { to: AccountId, deadline: u64, ) -> Result<(u128, u128, u128), RouterV2Error> { - let pair = self.get_pair(pair, token_0, token_1)?; + let pair = self.get_and_cache_pair(pair, token_0, token_1)?; pair.add_liquidity( token_0, token_1, @@ -417,7 +418,7 @@ pub mod router_v2 { deadline: u64, ) -> Result<(u128, Balance, u128), RouterV2Error> { let wnative = self.wnative; - let pair = self.get_pair(pair, token, wnative)?; + let pair = self.get_and_cache_pair(pair, token, wnative)?; pair.add_liquidity_native( token, wnative, @@ -441,7 +442,7 @@ pub mod router_v2 { to: AccountId, deadline: u64, ) -> Result<(u128, u128), RouterV2Error> { - let pair = self.get_pair(Some(pair), token_0, token_1)?; + let pair = self.get_and_cache_pair(Some(pair), token_0, token_1)?; pair.remove_liquidity( token_0, token_1, @@ -465,7 +466,7 @@ pub mod router_v2 { deadline: u64, ) -> Result<(u128, Balance), RouterV2Error> { let wnative = self.wnative; - let pair = self.get_pair(Some(pair), token, wnative)?; + let pair = self.get_and_cache_pair(Some(pair), token, wnative)?; pair.remove_liquidity_native( token, wnative, @@ -490,7 +491,7 @@ pub mod router_v2 { native: bool, ) -> Result<(u128, u128), RouterV2Error> { let wnative = if native { Some(self.wnative) } else { None }; - self.get_stable_pool(pool)?.add_liquidity( + self.get_and_cache_stable_pool(pool)?.add_liquidity( min_share_amount, amounts, to, @@ -510,7 +511,7 @@ pub mod router_v2 { native: bool, ) -> Result<(u128, u128), RouterV2Error> { let wnative = if native { Some(self.wnative) } else { None }; - self.get_stable_pool(pool)?.remove_liquidity( + self.get_and_cache_stable_pool(pool)?.remove_liquidity( max_share_amount, amounts, to, @@ -530,7 +531,7 @@ pub mod router_v2 { native: bool, ) -> Result, RouterV2Error> { let wnative = if native { Some(self.wnative) } else { None }; - self.get_stable_pool(pool)?.remove_liquidity_by_share( + self.get_and_cache_stable_pool(pool)?.remove_liquidity_by_share( share_amount, min_amounts, to, diff --git a/amm/traits/router_v2.rs b/amm/traits/router_v2.rs index 416974c6..bac69f1d 100644 --- a/amm/traits/router_v2.rs +++ b/amm/traits/router_v2.rs @@ -292,8 +292,6 @@ pub enum RouterV2Error { Expired, InvalidPoolAddress, InvalidToken, - PairNotFound, - PoolNotFound, TransferError, ExcessiveInputAmount, From 3222cff52dae2e98f98c882e23293ffb4d072a4b Mon Sep 17 00:00:00 2001 From: JanKuczma Date: Thu, 26 Sep 2024 11:32:31 +0200 Subject: [PATCH 9/9] rename fn --- amm/contracts/router_v2/stable_pool.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amm/contracts/router_v2/stable_pool.rs b/amm/contracts/router_v2/stable_pool.rs index a80f44c6..95b8e7c0 100644 --- a/amm/contracts/router_v2/stable_pool.rs +++ b/amm/contracts/router_v2/stable_pool.rs @@ -126,7 +126,7 @@ impl StablePool { amounts.clone(), account_id::(), )?; - self.handle_token_transfers(&amounts, to, wnative_idx)?; + self.transfer_tokens_back(&amounts, to, wnative_idx)?; res } None => { @@ -162,7 +162,7 @@ impl StablePool { min_amounts, account_id::(), )?; - self.handle_token_transfers(&amounts, to, wnative_idx)?; + self.transfer_tokens_back(&amounts, to, wnative_idx)?; Ok(amounts) } None => { @@ -216,7 +216,7 @@ impl StablePool { .ok_or(RouterV2Error::InvalidToken) } - fn handle_token_transfers( + fn transfer_tokens_back( &self, amounts: &[u128], to: AccountId,