diff --git a/amm/contracts/router_v2/lib.rs b/amm/contracts/router_v2/lib.rs index d1bfafc5..05428b3b 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; @@ -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 => { @@ -60,13 +60,24 @@ pub mod router_v2 { } } } + + /// Returns StablePool for `pool_id`. + /// Adds the StablePool to the cache. + #[inline] + 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), + } + } + /// 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 /// 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, @@ -76,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), } } @@ -91,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], @@ -99,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], @@ -120,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], @@ -149,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], @@ -284,9 +295,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 +324,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) } @@ -386,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, @@ -411,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, @@ -435,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, @@ -459,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, @@ -470,6 +477,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_and_cache_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_and_cache_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_and_cache_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 fa05e138..2656b742 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}; @@ -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 { @@ -185,8 +187,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)) @@ -240,7 +241,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 11fd8961..95b8e7c0 100644 --- a/amm/contracts/router_v2/stable_pool.rs +++ b/amm/contracts/router_v2/stable_pool.rs @@ -1,9 +1,18 @@ +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 crate::utils::{ + check_timestamp, psp22_approve, psp22_transfer, psp22_transfer_from, transfer_native, withdraw, + wrap, +}; + #[derive(scale::Decode, scale::Encode)] #[cfg_attr( feature = "std", @@ -30,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, @@ -40,6 +56,123 @@ 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)?; + let native_received = transferred_value::(); + let (wnative_idx, native_surplus) = match wnative { + Some(wnative) => { + let wnative_idx = self.wnative_idx(wnative)?; + let wnative_amount = amounts[wnative_idx]; + ensure!( + native_received >= wnative_amount, + RouterV2Error::InsufficientTransferredAmount + ); + wrap(wnative, wnative_amount)?; + (wnative_idx, native_received.saturating_sub(wnative_amount)) + } + 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], + 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)?; + 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::(), + )?; + self.transfer_tokens_back(&amounts, to, wnative_idx)?; + 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)?; + 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::(), + )?; + self.transfer_tokens_back(&amounts, to, wnative_idx)?; + Ok(amounts) + } + None => { + Ok(self + .contract_ref() + .remove_liquidity_by_shares(share_amount, min_amounts, to)?) + } + } + } + pub fn swap( &self, token_in: AccountId, @@ -75,4 +208,25 @@ impl StablePool { .get_swap_amount_out(token_in, token_out, amount_in) .map(|(amount_out, _)| amount_out)?) } + + fn wnative_idx(&self, wnative: AccountId) -> Result { + self.tokens + .iter() + .position(|&token| wnative == token) + .ok_or(RouterV2Error::InvalidToken) + } + + fn transfer_tokens_back( + &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(()) + } } diff --git a/amm/contracts/router_v2/utils.rs b/amm/contracts/router_v2/utils.rs index 518b447f..5f652de1 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,18 +34,20 @@ 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(); - 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] @@ -53,3 +55,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/drink-tests/src/router_v2_tests.rs b/amm/drink-tests/src/router_v2_tests.rs index 3b2d0317..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}; @@ -110,6 +111,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] @@ -151,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"); @@ -271,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, @@ -527,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, @@ -573,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()); @@ -603,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, @@ -701,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, @@ -775,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(), @@ -813,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 5ab1bd69..3f4488f8 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 { @@ -341,6 +343,99 @@ 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 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, diff --git a/amm/traits/router_v2.rs b/amm/traits/router_v2.rs index 91e5b42d..bac69f1d 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, }; @@ -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`. @@ -235,14 +289,13 @@ pub enum RouterV2Error { MathError(MathError), StablePoolError(StablePoolError), - CrossContractCallFailed(String), Expired, InvalidPoolAddress, InvalidToken, - PairNotFound, TransferError, ExcessiveInputAmount, + InsufficientTransferredAmount, InsufficientAmount, InsufficientOutputAmount, InsufficientAmountA,