From e088d6e824db702115c2d0ad9c60e24736f261af Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Tue, 15 Mar 2022 20:10:55 +0100 Subject: [PATCH 1/8] [Pablo] Minimal ConstantProductPool Integration --- frame/composable-traits/src/dex.rs | 2 +- frame/pablo/src/lib.rs | 43 +++- frame/pablo/src/uniswap.rs | 340 +++++++++++++++++++++++++++++ 3 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 frame/pablo/src/uniswap.rs diff --git a/frame/composable-traits/src/dex.rs b/frame/composable-traits/src/dex.rs index 235193ece6a..b276e24b793 100644 --- a/frame/composable-traits/src/dex.rs +++ b/frame/composable-traits/src/dex.rs @@ -130,7 +130,7 @@ pub trait SimpleExchange { ) -> Result; } -#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Default, PartialEq, RuntimeDebug)] +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Default, PartialEq, Eq, RuntimeDebug)] pub struct ConstantProductPoolInfo { /// Owner of pool pub owner: AccountId, diff --git a/frame/pablo/src/lib.rs b/frame/pablo/src/lib.rs index 67642d7f82a..42b7325e881 100644 --- a/frame/pablo/src/lib.rs +++ b/frame/pablo/src/lib.rs @@ -42,14 +42,16 @@ mod mock; mod stable_swap_tests; mod stable_swap; +mod uniswap; #[frame_support::pallet] pub mod pallet { + use crate::{uniswap::Uniswap, PoolConfiguration::ConstantProduct}; use codec::{Codec, FullCodec}; use composable_traits::{ - currency::CurrencyFactory, + currency::{CurrencyFactory, LocalAssets}, defi::CurrencyPair, - dex::{Amm, StableSwapPoolInfo}, + dex::{Amm, ConstantProductPoolInfo, StableSwapPoolInfo}, math::{SafeAdd, SafeSub}, }; use core::fmt::Debug; @@ -80,13 +82,14 @@ pub mod pallet { #[derive(RuntimeDebug, Encode, Decode, MaxEncodedLen, Clone, PartialEq, Eq, TypeInfo)] pub enum PoolConfiguration { StableSwap(StableSwapPoolInfo), + ConstantProduct(ConstantProductPoolInfo), } type AssetIdOf = ::AssetId; type BalanceOf = ::Balance; type AccountIdOf = ::AccountId; type PoolIdOf = ::PoolId; - type PoolConfigurationOf = + pub type PoolConfigurationOf = PoolConfiguration<::AccountId, ::AssetId>; type PoolInitConfigurationOf = PoolInitConfiguration<::AssetId>; @@ -110,7 +113,7 @@ pub mod pallet { quote_amount: T::Balance, }, /// Liquidity added into the pool `T::PoolId`. - LiquidityAdded { + LiquidityAddedToStableSwapPool { /// Account id who added liquidity. who: T::AccountId, /// Pool id to which liquidity added. @@ -122,6 +125,28 @@ pub mod pallet { /// Amount of minted lp tokens. mint_amount: T::Balance, }, + /// Liquidity added into the pool `T::PoolId`. + LiquidityAddedToLiquidityBootstrappingPool { + /// Pool id to which liquidity added. + pool_id: T::PoolId, + /// Amount of base asset deposited. + base_amount: T::Balance, + /// Amount of quote asset deposited. + quote_amount: T::Balance, + }, + /// Liquidity added into the pool `T::PoolId`. + LiquidityAddedToConstantProductPool { + /// Account id who added liquidity. + who: T::AccountId, + /// Pool id to which liquidity added. + pool_id: T::PoolId, + /// Amount of base asset deposited. + base_amount: T::Balance, + /// Amount of quote asset deposited. + quote_amount: T::Balance, + /// Amount of minted lp. + minted_lp: T::Balance, + }, /// Liquidity removed from pool `T::PoolId` by `T::AccountId` in balanced way. LiquidityRemoved { /// Account id who removed liquidity. @@ -167,6 +192,9 @@ pub mod pallet { InvalidPair, InvalidFees, AmpFactorMustBeGreaterThanZero, + + // ConstantProduct Specific: Possibly rename + MissingAmount, } #[pallet::config] @@ -388,6 +416,13 @@ pub mod pallet { Ok(pool_id) }, + PoolInitConfiguration::ConstantProduct(constant_product_pool_info) => + Uniswap::::do_create_pool( + &constant_product_pool_info.owner, + constant_product_pool_info.pair, + constant_product_pool_info.fee, + constant_product_pool_info.owner_fee, + ), } } diff --git a/frame/pablo/src/uniswap.rs b/frame/pablo/src/uniswap.rs new file mode 100644 index 00000000000..7a00770048a --- /dev/null +++ b/frame/pablo/src/uniswap.rs @@ -0,0 +1,340 @@ +use crate::{ + Config, Error, Event, Pallet, PoolConfiguration, PoolConfigurationOf, PoolCount, Pools, +}; +use composable_maths::dex::constant_product::{ + compute_deposit_lp, compute_in_given_out, compute_out_given_in, +}; +use composable_traits::{ + currency::{CurrencyFactory, RangeId}, + defi::CurrencyPair, + dex::{Amm, ConstantProductPoolInfo}, + math::{safe_multiply_by_rational, SafeAdd, SafeSub}, +}; +use frame_support::{ + pallet_prelude::*, + traits::fungibles::{Inspect, Mutate, Transfer}, + transactional, PalletId, +}; +use sp_runtime::{ + traits::{AccountIdConversion, CheckedAdd, Convert, One, Zero}, + ArithmeticError, Permill, +}; + +// Uniswap +pub(crate) struct Uniswap(PhantomData); + +impl Uniswap { + #[transactional] + pub(crate) fn do_create_pool( + who: &T::AccountId, + pair: CurrencyPair, + fee: Permill, + owner_fee: Permill, + ) -> Result { + // NOTE(hussein-aitlahcen): do we allow such pair? + ensure!(pair.base != pair.quote, Error::::InvalidPair); + + let total_fees = fee.checked_add(&owner_fee).ok_or(ArithmeticError::Overflow)?; + ensure!(total_fees < Permill::one(), Error::::InvalidFees); + + let lp_token = T::CurrencyFactory::create(RangeId::LP_TOKENS)?; + + // Add new pool + let pool_id = + PoolCount::::try_mutate(|pool_count| -> Result { + let pool_id = *pool_count; + Pools::::insert( + pool_id, + PoolConfiguration::ConstantProduct(ConstantProductPoolInfo { + owner: who.clone(), + pair, + lp_token, + fee, + owner_fee, + }), + ); + *pool_count = pool_id.safe_add(&T::PoolId::one())?; + Ok(pool_id) + })?; + + >::deposit_event(Event::PoolCreated { pool_id, owner: who.clone() }); + + Ok(pool_id) + } + + /// Assume that the pair is valid for the pool + pub(crate) fn do_compute_swap( + pool_id: T::PoolId, + pair: CurrencyPair, + quote_amount: T::Balance, + apply_fees: bool, + ) -> Result<(T::Balance, T::Balance, T::Balance, T::Balance), DispatchError> { + let pool = Self::get_pool(pool_id)?; + let pool_account = Self::account_id(&pool_id); + let pool_base_aum = T::Convert::convert(T::Assets::balance(pair.base, &pool_account)); + let pool_quote_aum = T::Convert::convert(T::Assets::balance(pair.quote, &pool_account)); + let quote_amount = T::Convert::convert(quote_amount); + + // https://uniswap.org/whitepaper.pdf + // 3.2.1 + // we do not inflate the lp for the owner fees + // cut is done before enforcing the invariant + let (lp_fee, owner_fee) = if apply_fees { + let lp_fee = pool.fee.mul_floor(quote_amount); + let owner_fee = pool.owner_fee.mul_floor(quote_amount); + (lp_fee, owner_fee) + } else { + (0, 0) + }; + + let quote_amount_excluding_fees = quote_amount.safe_sub(&lp_fee)?.safe_sub(&owner_fee)?; + + let half_weight = Permill::from_percent(50); + let base_amount = compute_out_given_in( + half_weight, + half_weight, + pool_quote_aum, + pool_base_aum, + quote_amount_excluding_fees, + )?; + + ensure!(base_amount > 0 && quote_amount_excluding_fees > 0, Error::::InvalidAmount); + + Ok(( + T::Convert::convert(base_amount), + T::Convert::convert(quote_amount_excluding_fees), + T::Convert::convert(lp_fee), + T::Convert::convert(owner_fee), + )) + } + fn get_pool( + pool_id: T::PoolId, + ) -> Result, DispatchError> { + let pool_config = Pools::::get(pool_id).expect("TODO FIX"); + // .ok_or_else(|| Error::::PoolNotFound.into())?; + match pool_config as PoolConfiguration { + PoolConfiguration::StableSwap(_) => Err(Error::::PoolNotFound.into()), + PoolConfiguration::ConstantProduct(pool) => Ok(pool), + } + } + + fn account_id(pool_id: &T::PoolId) -> T::AccountId { + T::PalletId::get().into_sub_account(pool_id) + } +} + +impl Amm for Uniswap { + type AssetId = T::AssetId; + type Balance = T::Balance; + type AccountId = T::AccountId; + type PoolId = T::PoolId; + + fn pool_exists(pool_id: Self::PoolId) -> bool { + Pools::::contains_key(pool_id) + } + + fn currency_pair(pool_id: Self::PoolId) -> Result, DispatchError> { + let pool = Self::get_pool(pool_id)?; + Ok(pool.pair) + } + + fn get_exchange_value( + pool_id: Self::PoolId, + asset_id: Self::AssetId, + amount: Self::Balance, + ) -> Result { + let pool = Self::get_pool(pool_id)?; + let pool_account = Self::account_id(&pool_id); + let amount = T::Convert::convert(amount); + let half_weight = Permill::from_percent(50); + let pool_base_aum = T::Convert::convert(T::Assets::balance(pool.pair.base, &pool_account)); + let pool_quote_aum = + T::Convert::convert(T::Assets::balance(pool.pair.quote, &pool_account)); + let exchange_amount = if asset_id == pool.pair.quote { + compute_out_given_in(half_weight, half_weight, pool_quote_aum, pool_base_aum, amount) + } else { + compute_in_given_out(half_weight, half_weight, pool_quote_aum, pool_base_aum, amount) + }?; + Ok(T::Convert::convert(exchange_amount)) + } + + #[transactional] + fn buy( + who: &Self::AccountId, + pool_id: Self::PoolId, + asset_id: Self::AssetId, + amount: Self::Balance, + keep_alive: bool, + ) -> Result { + let pool = Self::get_pool(pool_id)?; + let pair = if asset_id == pool.pair.base { pool.pair } else { pool.pair.swap() }; + let quote_amount = Self::get_exchange_value(pool_id, asset_id, amount)?; + ::exchange(who, pool_id, pair, quote_amount, T::Balance::zero(), keep_alive) + } + + #[transactional] + fn sell( + who: &Self::AccountId, + pool_id: Self::PoolId, + asset_id: Self::AssetId, + amount: Self::Balance, + keep_alive: bool, + ) -> Result { + let pool = Self::get_pool(pool_id)?; + let pair = if asset_id == pool.pair.base { pool.pair.swap() } else { pool.pair }; + ::exchange(who, pool_id, pair, amount, T::Balance::zero(), keep_alive) + } + + #[transactional] + fn add_liquidity( + who: &Self::AccountId, + pool_id: Self::PoolId, + base_amount: Self::Balance, + quote_amount: Self::Balance, + min_mint_amount: Self::Balance, + keep_alive: bool, + ) -> Result<(), DispatchError> { + ensure!(base_amount > T::Balance::zero(), Error::::InvalidAmount); + + let pool = Self::get_pool(pool_id)?; + let pool_account = Self::account_id(&pool_id); + let pool_base_aum = T::Convert::convert(T::Assets::balance(pool.pair.base, &pool_account)); + let pool_quote_aum = + T::Convert::convert(T::Assets::balance(pool.pair.quote, &pool_account)); + + let lp_total_issuance = T::Convert::convert(T::Assets::total_issuance(pool.lp_token)); + let (quote_amount, lp_to_mint) = compute_deposit_lp( + lp_total_issuance, + T::Convert::convert(base_amount), + T::Convert::convert(quote_amount), + pool_base_aum, + pool_quote_aum, + )?; + let quote_amount = T::Convert::convert(quote_amount); + let lp_to_mint = T::Convert::convert(lp_to_mint); + + ensure!(quote_amount > T::Balance::zero(), Error::::InvalidAmount); + ensure!(lp_to_mint >= min_mint_amount, Error::::CannotRespectMinimumRequested); + + T::Assets::transfer(pool.pair.base, who, &pool_account, base_amount, keep_alive)?; + T::Assets::transfer(pool.pair.quote, who, &pool_account, quote_amount, keep_alive)?; + T::Assets::mint_into(pool.lp_token, who, lp_to_mint)?; + + >::deposit_event(Event::::LiquidityAddedToConstantProductPool { + pool_id, + who: who.clone(), + base_amount, + quote_amount, + minted_lp: lp_to_mint, + }); + + Ok(()) + } + + #[transactional] + fn remove_liquidity( + who: &Self::AccountId, + pool_id: T::PoolId, + lp_amount: Self::Balance, + min_base_amount: Self::Balance, + min_quote_amount: Self::Balance, + ) -> Result<(), DispatchError> { + let pool = Self::get_pool(pool_id)?; + + let pool_account = Self::account_id(&pool_id); + let pool_base_aum = T::Convert::convert(T::Assets::balance(pool.pair.base, &pool_account)); + let pool_quote_aum = + T::Convert::convert(T::Assets::balance(pool.pair.quote, &pool_account)); + let lp_issued = T::Assets::total_issuance(pool.lp_token); + + let base_amount = T::Convert::convert(safe_multiply_by_rational( + T::Convert::convert(lp_amount), + pool_base_aum, + T::Convert::convert(lp_issued), + )?); + let quote_amount = T::Convert::convert(safe_multiply_by_rational( + T::Convert::convert(lp_amount), + pool_quote_aum, + T::Convert::convert(lp_issued), + )?); + + ensure!( + base_amount >= min_base_amount && quote_amount >= min_quote_amount, + Error::::CannotRespectMinimumRequested + ); + + // NOTE(hussein-aitlance): no need to keep alive the pool account + T::Assets::transfer(pool.pair.base, &pool_account, who, base_amount, false)?; + T::Assets::transfer(pool.pair.quote, &pool_account, who, quote_amount, false)?; + T::Assets::burn_from(pool.lp_token, who, lp_amount)?; + + >::deposit_event(Event::::LiquidityRemoved { + pool_id, + who: who.clone(), + base_amount, + quote_amount, + total_issuance: lp_issued.safe_sub(&lp_amount)?, + }); + + Ok(()) + } + + #[transactional] + fn exchange( + who: &Self::AccountId, + pool_id: T::PoolId, + pair: CurrencyPair, + quote_amount: Self::Balance, + min_receive: Self::Balance, + keep_alive: bool, + ) -> Result { + let pool = Self::get_pool(pool_id)?; + // /!\ NOTE(hussein-aitlahcen): after this check, do not use pool.pair as the provided + // pair might have been swapped + ensure!(pair == pool.pair, Error::::PairMismatch); + + let (base_amount, quote_amount, lp_fees, owner_fees) = + Self::do_compute_swap(pool_id, pair, quote_amount, true)?; + let total_fees = lp_fees.safe_add(&owner_fees)?; + let quote_amount_including_fees = quote_amount.safe_add(&total_fees)?; + + ensure!(base_amount >= min_receive, Error::::CannotRespectMinimumRequested); + + let pool_account = Self::account_id(&pool_id); + T::Assets::transfer( + pair.quote, + who, + &pool_account, + quote_amount_including_fees, + keep_alive, + )?; + // NOTE(hussein-aitlance): no need to keep alive the pool account + T::Assets::transfer(pair.quote, &pool_account, &pool.owner, owner_fees, false)?; + T::Assets::transfer(pair.base, &pool_account, who, base_amount, false)?; + + >::deposit_event(Event::::Swapped { + pool_id, + who: who.clone(), + base_asset: pair.base, + quote_asset: pair.quote, + base_amount, + quote_amount, + fee: total_fees, + }); + + Ok(base_amount) + } +} + +#[cfg(test)] +mod tests { + use crate::mock::{new_test_ext, ALICE, BOB, BTC, USDT}; + use composable_traits::{defi::CurrencyPair, dex::Amm}; + use frame_support::assert_ok; + use sp_arithmetic::Permill; + + #[test] + fn test() { + new_test_ext().execute_with(|| {}); + } +} From e2327746abf731b513e643aac1dc2a553a144994 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 16 Mar 2022 19:48:12 +0100 Subject: [PATCH 2/8] Updates with tests --- frame/pablo/src/lib.rs | 148 +++++++++-- frame/pablo/src/mock.rs | 1 + frame/pablo/src/uniswap.rs | 244 +++--------------- frame/pablo/src/uniswap_tests.rs | 414 +++++++++++++++++++++++++++++++ 4 files changed, 571 insertions(+), 236 deletions(-) create mode 100644 frame/pablo/src/uniswap_tests.rs diff --git a/frame/pablo/src/lib.rs b/frame/pablo/src/lib.rs index 42b7325e881..a8353055eb6 100644 --- a/frame/pablo/src/lib.rs +++ b/frame/pablo/src/lib.rs @@ -37,16 +37,17 @@ pub use pallet::*; #[cfg(test)] mod mock; - #[cfg(test)] mod stable_swap_tests; +#[cfg(test)] +mod uniswap_tests; mod stable_swap; mod uniswap; #[frame_support::pallet] pub mod pallet { - use crate::{uniswap::Uniswap, PoolConfiguration::ConstantProduct}; + use crate::{stable_swap::StableSwap, uniswap::Uniswap, PoolConfiguration::ConstantProduct}; use codec::{Codec, FullCodec}; use composable_traits::{ currency::{CurrencyFactory, LocalAssets}, @@ -67,8 +68,6 @@ pub mod pallet { Permill, }; - use crate::stable_swap::StableSwap; - #[derive(RuntimeDebug, Encode, Decode, MaxEncodedLen, Clone, PartialEq, Eq, TypeInfo)] pub enum PoolInitConfiguration { StableSwap { @@ -77,6 +76,11 @@ pub mod pallet { fee: Permill, protocol_fee: Permill, }, + ConstantProduct { + pair: CurrencyPair, + fee: Permill, + owner_fee: Permill, + }, } #[derive(RuntimeDebug, Encode, Decode, MaxEncodedLen, Clone, PartialEq, Eq, TypeInfo)] @@ -93,6 +97,7 @@ pub mod pallet { PoolConfiguration<::AccountId, ::AssetId>; type PoolInitConfigurationOf = PoolInitConfiguration<::AssetId>; + // TODO refactor event publishing with cu-23v2y3n #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -416,13 +421,11 @@ pub mod pallet { Ok(pool_id) }, - PoolInitConfiguration::ConstantProduct(constant_product_pool_info) => - Uniswap::::do_create_pool( - &constant_product_pool_info.owner, - constant_product_pool_info.pair, - constant_product_pool_info.fee, - constant_product_pool_info.owner_fee, - ), + PoolInitConfiguration::ConstantProduct { pair, fee, owner_fee } => { + let pool_id = Uniswap::::do_create_pool(&who, pair, fee, owner_fee)?; + Self::deposit_event(Event::PoolCreated { owner: who.clone(), pool_id }); + Ok(pool_id) + }, } } @@ -447,6 +450,10 @@ pub mod pallet { let base_amount = Self::get_exchange_value(pool_id, pair.base, quote_amount)?; let base_amount_u: u128 = T::Convert::convert(base_amount); + // https://uniswap.org/whitepaper.pdf + // 3.2.1 + // we do not inflate the lp for the owner fees + // cut is done before enforcing the invariant let (lp_fee, protocol_fee) = if apply_fees { let lp_fee = fee.mul_floor(base_amount_u); // protocol_fee is computed based on lp_fee @@ -480,6 +487,7 @@ pub mod pallet { match pool { PoolConfiguration::StableSwap(stable_swap_pool_info) => Ok(stable_swap_pool_info.pair), + ConstantProduct(constant_product_pool_info) => Ok(constant_product_pool_info.pair), } } @@ -498,6 +506,12 @@ pub mod pallet { asset_id, amount, ), + ConstantProduct(constant_product_pool_info) => Uniswap::::get_exchange_value( + constant_product_pool_info, + pool_account, + asset_id, + amount, + ), } } @@ -523,7 +537,7 @@ pub mod pallet { min_mint_amount, keep_alive, )?; - Self::deposit_event(Event::::LiquidityAdded { + Self::deposit_event(Event::::LiquidityAddedToStableSwapPool { who: who.clone(), pool_id, base_amount, @@ -531,7 +545,26 @@ pub mod pallet { mint_amount, }); }, + ConstantProduct(constant_product_pool_info) => { + let mint_amount = Uniswap::::add_liquidity( + who, + constant_product_pool_info, + pool_account, + base_amount, + quote_amount, + min_mint_amount, + keep_alive, + )?; + Self::deposit_event(Event::::LiquidityAddedToConstantProductPool { + who: who.clone(), + pool_id, + base_amount, + quote_amount, + minted_lp: mint_amount, + }); + }, } + // TODO refactor event publishing with cu-23v2y3n Ok(()) } @@ -564,7 +597,25 @@ pub mod pallet { total_issuance: updated_lp, }); }, + ConstantProduct(constant_product_pool_info) => { + let (base_amount, quote_amount, updated_lp) = Uniswap::::remove_liquidity( + who, + constant_product_pool_info, + pool_account, + lp_amount, + min_base_amount, + min_quote_amount, + )?; + Self::deposit_event(Event::::LiquidityRemoved { + pool_id, + who: who.clone(), + base_amount, + quote_amount, + total_issuance: updated_lp, + }); + }, } + // TODO refactor event publishing with cu-23v2y3n Ok(()) } @@ -627,7 +678,53 @@ pub mod pallet { Ok(base_amount_excluding_fees) }, + ConstantProduct(constant_product_pool_info) => { + let (base_amount, quote_amount, lp_fees, owner_fees) = Self::do_compute_swap( + pool_id, + pair, + quote_amount, + true, + constant_product_pool_info.fee, + constant_product_pool_info.owner_fee, + )?; + let total_fees = lp_fees.safe_add(&owner_fees)?; + let quote_amount_including_fees = quote_amount.safe_add(&total_fees)?; + + ensure!(base_amount >= min_receive, Error::::CannotRespectMinimumRequested); + + let pool_account = Self::account_id(&pool_id); + T::Assets::transfer( + pair.quote, + who, + &pool_account, + quote_amount_including_fees, + keep_alive, + )?; + // NOTE(hussein-aitlance): no need to keep alive the pool account + T::Assets::transfer( + pair.quote, + &pool_account, + &constant_product_pool_info.owner, + owner_fees, + false, + )?; + T::Assets::transfer(pair.base, &pool_account, who, base_amount, false)?; + + Self::deposit_event(Event::::Swapped { + pool_id, + who: who.clone(), + base_asset: pair.base, + quote_asset: pair.quote, + base_amount, + quote_amount, + fee: total_fees, + }); + + Ok(base_amount) + }, } + + // TODO refactor event publishing with cu-23v2y3n } #[transactional] @@ -651,6 +748,15 @@ pub mod pallet { Self::exchange(who, pool_id, pair, dx, T::Balance::zero(), keep_alive)?; Ok(amount) }, + ConstantProduct(constant_product_pool) => { + let pair = if asset_id == constant_product_pool.pair.base { + constant_product_pool.pair + } else { + constant_product_pool.pair.swap() + }; + let quote_amount = Self::get_exchange_value(pool_id, asset_id, amount)?; + Self::exchange(who, pool_id, pair, quote_amount, T::Balance::zero(), keep_alive) + }, } } @@ -667,15 +773,15 @@ pub mod pallet { PoolConfiguration::StableSwap(pool) => { let pair = if asset_id == pool.pair.base { pool.pair.swap() } else { pool.pair }; - let dy = Self::exchange( - who, - pool_id, - pair, - amount, - Self::Balance::zero(), - keep_alive, - )?; - Ok(dy) + Self::exchange(who, pool_id, pair, amount, Self::Balance::zero(), keep_alive) + }, + ConstantProduct(constant_product_pool) => { + let pair = if asset_id == constant_product_pool.pair.base { + constant_product_pool.pair.swap() + } else { + constant_product_pool.pair + }; + Self::exchange(who, pool_id, pair, amount, T::Balance::zero(), keep_alive) }, } } diff --git a/frame/pablo/src/mock.rs b/frame/pablo/src/mock.rs index 40a5487fab0..b40edbc52c8 100644 --- a/frame/pablo/src/mock.rs +++ b/frame/pablo/src/mock.rs @@ -13,6 +13,7 @@ use system::EnsureRoot; pub type CurrencyId = u128; +pub const BTC: AssetId = 0; pub const USDT: CurrencyId = 2; pub const USDC: CurrencyId = 4; diff --git a/frame/pablo/src/uniswap.rs b/frame/pablo/src/uniswap.rs index 7a00770048a..c415dc41553 100644 --- a/frame/pablo/src/uniswap.rs +++ b/frame/pablo/src/uniswap.rs @@ -1,6 +1,4 @@ -use crate::{ - Config, Error, Event, Pallet, PoolConfiguration, PoolConfigurationOf, PoolCount, Pools, -}; +use crate::{Config, Error, Event, Pallet, PoolConfiguration, PoolCount, Pools}; use composable_maths::dex::constant_product::{ compute_deposit_lp, compute_in_given_out, compute_out_given_in, }; @@ -62,89 +60,12 @@ impl Uniswap { Ok(pool_id) } - /// Assume that the pair is valid for the pool - pub(crate) fn do_compute_swap( - pool_id: T::PoolId, - pair: CurrencyPair, - quote_amount: T::Balance, - apply_fees: bool, - ) -> Result<(T::Balance, T::Balance, T::Balance, T::Balance), DispatchError> { - let pool = Self::get_pool(pool_id)?; - let pool_account = Self::account_id(&pool_id); - let pool_base_aum = T::Convert::convert(T::Assets::balance(pair.base, &pool_account)); - let pool_quote_aum = T::Convert::convert(T::Assets::balance(pair.quote, &pool_account)); - let quote_amount = T::Convert::convert(quote_amount); - - // https://uniswap.org/whitepaper.pdf - // 3.2.1 - // we do not inflate the lp for the owner fees - // cut is done before enforcing the invariant - let (lp_fee, owner_fee) = if apply_fees { - let lp_fee = pool.fee.mul_floor(quote_amount); - let owner_fee = pool.owner_fee.mul_floor(quote_amount); - (lp_fee, owner_fee) - } else { - (0, 0) - }; - - let quote_amount_excluding_fees = quote_amount.safe_sub(&lp_fee)?.safe_sub(&owner_fee)?; - - let half_weight = Permill::from_percent(50); - let base_amount = compute_out_given_in( - half_weight, - half_weight, - pool_quote_aum, - pool_base_aum, - quote_amount_excluding_fees, - )?; - - ensure!(base_amount > 0 && quote_amount_excluding_fees > 0, Error::::InvalidAmount); - - Ok(( - T::Convert::convert(base_amount), - T::Convert::convert(quote_amount_excluding_fees), - T::Convert::convert(lp_fee), - T::Convert::convert(owner_fee), - )) - } - fn get_pool( - pool_id: T::PoolId, - ) -> Result, DispatchError> { - let pool_config = Pools::::get(pool_id).expect("TODO FIX"); - // .ok_or_else(|| Error::::PoolNotFound.into())?; - match pool_config as PoolConfiguration { - PoolConfiguration::StableSwap(_) => Err(Error::::PoolNotFound.into()), - PoolConfiguration::ConstantProduct(pool) => Ok(pool), - } - } - - fn account_id(pool_id: &T::PoolId) -> T::AccountId { - T::PalletId::get().into_sub_account(pool_id) - } -} - -impl Amm for Uniswap { - type AssetId = T::AssetId; - type Balance = T::Balance; - type AccountId = T::AccountId; - type PoolId = T::PoolId; - - fn pool_exists(pool_id: Self::PoolId) -> bool { - Pools::::contains_key(pool_id) - } - - fn currency_pair(pool_id: Self::PoolId) -> Result, DispatchError> { - let pool = Self::get_pool(pool_id)?; - Ok(pool.pair) - } - - fn get_exchange_value( - pool_id: Self::PoolId, - asset_id: Self::AssetId, - amount: Self::Balance, - ) -> Result { - let pool = Self::get_pool(pool_id)?; - let pool_account = Self::account_id(&pool_id); + pub(crate) fn get_exchange_value( + pool: ConstantProductPoolInfo, + pool_account: T::AccountId, + asset_id: T::AssetId, + amount: T::Balance, + ) -> Result { let amount = T::Convert::convert(amount); let half_weight = Permill::from_percent(50); let pool_base_aum = T::Convert::convert(T::Assets::balance(pool.pair.base, &pool_account)); @@ -159,51 +80,22 @@ impl Amm for Uniswap { } #[transactional] - fn buy( - who: &Self::AccountId, - pool_id: Self::PoolId, - asset_id: Self::AssetId, - amount: Self::Balance, - keep_alive: bool, - ) -> Result { - let pool = Self::get_pool(pool_id)?; - let pair = if asset_id == pool.pair.base { pool.pair } else { pool.pair.swap() }; - let quote_amount = Self::get_exchange_value(pool_id, asset_id, amount)?; - ::exchange(who, pool_id, pair, quote_amount, T::Balance::zero(), keep_alive) - } - - #[transactional] - fn sell( - who: &Self::AccountId, - pool_id: Self::PoolId, - asset_id: Self::AssetId, - amount: Self::Balance, - keep_alive: bool, - ) -> Result { - let pool = Self::get_pool(pool_id)?; - let pair = if asset_id == pool.pair.base { pool.pair.swap() } else { pool.pair }; - ::exchange(who, pool_id, pair, amount, T::Balance::zero(), keep_alive) - } - - #[transactional] - fn add_liquidity( - who: &Self::AccountId, - pool_id: Self::PoolId, - base_amount: Self::Balance, - quote_amount: Self::Balance, - min_mint_amount: Self::Balance, + pub(crate) fn add_liquidity( + who: &T::AccountId, + pool: ConstantProductPoolInfo, + pool_account: T::AccountId, + base_amount: T::Balance, + quote_amount: T::Balance, + min_mint_amount: T::Balance, keep_alive: bool, - ) -> Result<(), DispatchError> { + ) -> Result { ensure!(base_amount > T::Balance::zero(), Error::::InvalidAmount); - - let pool = Self::get_pool(pool_id)?; - let pool_account = Self::account_id(&pool_id); let pool_base_aum = T::Convert::convert(T::Assets::balance(pool.pair.base, &pool_account)); let pool_quote_aum = T::Convert::convert(T::Assets::balance(pool.pair.quote, &pool_account)); let lp_total_issuance = T::Convert::convert(T::Assets::total_issuance(pool.lp_token)); - let (quote_amount, lp_to_mint) = compute_deposit_lp( + let (quote_amount, lp_token_to_mint) = compute_deposit_lp( lp_total_issuance, T::Convert::convert(base_amount), T::Convert::convert(quote_amount), @@ -211,37 +103,26 @@ impl Amm for Uniswap { pool_quote_aum, )?; let quote_amount = T::Convert::convert(quote_amount); - let lp_to_mint = T::Convert::convert(lp_to_mint); + let lp_token_to_mint = T::Convert::convert(lp_token_to_mint); ensure!(quote_amount > T::Balance::zero(), Error::::InvalidAmount); - ensure!(lp_to_mint >= min_mint_amount, Error::::CannotRespectMinimumRequested); + ensure!(lp_token_to_mint >= min_mint_amount, Error::::CannotRespectMinimumRequested); T::Assets::transfer(pool.pair.base, who, &pool_account, base_amount, keep_alive)?; T::Assets::transfer(pool.pair.quote, who, &pool_account, quote_amount, keep_alive)?; - T::Assets::mint_into(pool.lp_token, who, lp_to_mint)?; - - >::deposit_event(Event::::LiquidityAddedToConstantProductPool { - pool_id, - who: who.clone(), - base_amount, - quote_amount, - minted_lp: lp_to_mint, - }); - - Ok(()) + T::Assets::mint_into(pool.lp_token, who, lp_token_to_mint)?; + Ok(lp_token_to_mint) } #[transactional] - fn remove_liquidity( - who: &Self::AccountId, - pool_id: T::PoolId, - lp_amount: Self::Balance, - min_base_amount: Self::Balance, - min_quote_amount: Self::Balance, - ) -> Result<(), DispatchError> { - let pool = Self::get_pool(pool_id)?; - - let pool_account = Self::account_id(&pool_id); + pub(crate) fn remove_liquidity( + who: &T::AccountId, + pool: ConstantProductPoolInfo, + pool_account: T::AccountId, + lp_amount: T::Balance, + min_base_amount: T::Balance, + min_quote_amount: T::Balance, + ) -> Result<(T::Balance, T::Balance, T::Balance), DispatchError> { let pool_base_aum = T::Convert::convert(T::Assets::balance(pool.pair.base, &pool_account)); let pool_quote_aum = T::Convert::convert(T::Assets::balance(pool.pair.quote, &pool_account)); @@ -268,73 +149,6 @@ impl Amm for Uniswap { T::Assets::transfer(pool.pair.quote, &pool_account, who, quote_amount, false)?; T::Assets::burn_from(pool.lp_token, who, lp_amount)?; - >::deposit_event(Event::::LiquidityRemoved { - pool_id, - who: who.clone(), - base_amount, - quote_amount, - total_issuance: lp_issued.safe_sub(&lp_amount)?, - }); - - Ok(()) - } - - #[transactional] - fn exchange( - who: &Self::AccountId, - pool_id: T::PoolId, - pair: CurrencyPair, - quote_amount: Self::Balance, - min_receive: Self::Balance, - keep_alive: bool, - ) -> Result { - let pool = Self::get_pool(pool_id)?; - // /!\ NOTE(hussein-aitlahcen): after this check, do not use pool.pair as the provided - // pair might have been swapped - ensure!(pair == pool.pair, Error::::PairMismatch); - - let (base_amount, quote_amount, lp_fees, owner_fees) = - Self::do_compute_swap(pool_id, pair, quote_amount, true)?; - let total_fees = lp_fees.safe_add(&owner_fees)?; - let quote_amount_including_fees = quote_amount.safe_add(&total_fees)?; - - ensure!(base_amount >= min_receive, Error::::CannotRespectMinimumRequested); - - let pool_account = Self::account_id(&pool_id); - T::Assets::transfer( - pair.quote, - who, - &pool_account, - quote_amount_including_fees, - keep_alive, - )?; - // NOTE(hussein-aitlance): no need to keep alive the pool account - T::Assets::transfer(pair.quote, &pool_account, &pool.owner, owner_fees, false)?; - T::Assets::transfer(pair.base, &pool_account, who, base_amount, false)?; - - >::deposit_event(Event::::Swapped { - pool_id, - who: who.clone(), - base_asset: pair.base, - quote_asset: pair.quote, - base_amount, - quote_amount, - fee: total_fees, - }); - - Ok(base_amount) - } -} - -#[cfg(test)] -mod tests { - use crate::mock::{new_test_ext, ALICE, BOB, BTC, USDT}; - use composable_traits::{defi::CurrencyPair, dex::Amm}; - use frame_support::assert_ok; - use sp_arithmetic::Permill; - - #[test] - fn test() { - new_test_ext().execute_with(|| {}); + Ok((base_amount, quote_amount, lp_issued.safe_sub(&lp_amount)?)) } } diff --git a/frame/pablo/src/uniswap_tests.rs b/frame/pablo/src/uniswap_tests.rs new file mode 100644 index 00000000000..0023591df7e --- /dev/null +++ b/frame/pablo/src/uniswap_tests.rs @@ -0,0 +1,414 @@ +use crate::{ + mock::{Pablo, *}, + PoolConfiguration::ConstantProduct, + PoolInitConfiguration, +}; +use composable_tests_helpers::{ + prop_assert_ok, + test::helper::{acceptable_computation_error, default_acceptable_computation_error}, +}; +use composable_traits::{ + defi::CurrencyPair, + dex::{Amm, ConstantProductPoolInfo}, + math::safe_multiply_by_rational, +}; +use frame_support::{ + assert_err, assert_ok, + traits::fungibles::{Inspect, Mutate}, +}; +use proptest::prelude::*; +use sp_runtime::{ + traits::{IntegerSquareRoot, Zero}, + Permill, TokenError, +}; + +fn create_pool( + base_asset: AssetId, + quote_asset: AssetId, + base_amount: Balance, + quote_amount: Balance, + lp_fee: Permill, + protocol_fee: Permill, +) -> PoolId { + let pool_init_config = PoolInitConfiguration::ConstantProduct { + pair: CurrencyPair::new(base_asset, quote_asset), + fee: lp_fee, + owner_fee: protocol_fee, + }; + let pool_id = Pablo::do_create_pool(&ALICE, pool_init_config).expect("pool creation failed"); + + // Mint the tokens + assert_ok!(Tokens::mint_into(base_asset, &ALICE, base_amount)); + assert_ok!(Tokens::mint_into(quote_asset, &ALICE, quote_amount)); + + // Add the liquidity + assert_ok!(::add_liquidity(&ALICE, pool_id, base_amount, quote_amount, 0, false)); + pool_id +} + +fn get_pool(pool_id: PoolId) -> ConstantProductPoolInfo { + match Pablo::pools(pool_id).expect("pool not found") { + ConstantProduct(pool) => pool, + _ => panic!("expected stable_swap pool"), + } +} + +#[test] +fn test() { + new_test_ext().execute_with(|| { + let pool_id = create_pool( + BTC, + USDT, + Balance::zero(), + Balance::zero(), + Permill::zero(), + Permill::zero(), + ); + + let pool = get_pool(pool_id); + + let current_product = |a| { + let balance_btc = Tokens::balance(BTC, &a); + let balance_usdt = Tokens::balance(USDT, &a); + balance_btc * balance_usdt + }; + let current_pool_product = || current_product(Pablo::account_id(&pool_id)); + + let unit = 1_000_000_000_000; + + let btc_price = 45_000; + + let nb_of_btc = 100; + + // 100 BTC/4.5M USDT + let initial_btc = nb_of_btc * unit; + let initial_usdt = nb_of_btc * btc_price * unit; + + // Mint the tokens + assert_ok!(Tokens::mint_into(BTC, &ALICE, initial_btc)); + assert_ok!(Tokens::mint_into(USDT, &ALICE, initial_usdt)); + + let initial_user_invariant = current_product(ALICE); + + // Add the liquidity + assert_ok!(::add_liquidity( + &ALICE, + pool_id, + initial_btc, + initial_usdt, + 0, + false + )); + + // 1 unit of btc = 45k + some unit of usdt + let ratio = ::get_exchange_value(pool_id, BTC, unit) + .expect("get_exchange_value failed"); + assert!(ratio > (initial_usdt / initial_btc) * unit); + + let initial_pool_invariant = current_pool_product(); + + assert_eq!(initial_user_invariant, initial_pool_invariant); + + // swap a btc + let swap_btc = unit; + assert_ok!(Tokens::mint_into(BTC, &BOB, swap_btc)); + + ::sell(&BOB, pool_id, BTC, swap_btc, false).expect("sell failed"); + + let new_pool_invariant = current_pool_product(); + assert_ok!(default_acceptable_computation_error( + initial_pool_invariant, + new_pool_invariant + )); + + ::buy(&BOB, pool_id, BTC, swap_btc, false).expect("buy failed"); + + let precision = 100; + let epsilon = 5; + let bob_btc = Tokens::balance(BTC, &BOB); + assert_ok!(acceptable_computation_error(bob_btc, swap_btc, precision, epsilon)); + + let new_pool_invariant = current_pool_product(); + assert_ok!(default_acceptable_computation_error( + initial_pool_invariant, + new_pool_invariant + )); + + let lp = Tokens::balance(pool.lp_token, &ALICE); + assert_ok!(::remove_liquidity(&ALICE, pool_id, lp, 0, 0)); + + // Alice should get back a different amount of tokens. + let alice_btc = Tokens::balance(BTC, &ALICE); + let alice_usdt = Tokens::balance(USDT, &ALICE); + assert_ok!(default_acceptable_computation_error(alice_btc, initial_btc)); + assert_ok!(default_acceptable_computation_error(alice_usdt, initial_usdt)); + }); +} + +//- test lp mint/burn +#[test] +fn add_remove_lp() { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_00_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let pool_id = + create_pool(BTC, USDT, initial_btc, initial_usdt, Permill::zero(), Permill::zero()); + let pool = get_pool(pool_id); + let bob_btc = 10 * unit; + let bob_usdt = bob_btc * btc_price; + // Mint the tokens + assert_ok!(Tokens::mint_into(BTC, &BOB, bob_btc)); + assert_ok!(Tokens::mint_into(USDT, &BOB, bob_usdt)); + + let lp = Tokens::balance(pool.lp_token, &BOB); + assert_eq!(lp, 0_u128); + // Add the liquidity + assert_ok!(::add_liquidity(&BOB, pool_id, bob_btc, bob_usdt, 0, false)); + let lp = Tokens::balance(pool.lp_token, &BOB); + // must have received some lp tokens + assert!(lp > 0_u128); + assert_ok!(::remove_liquidity(&BOB, pool_id, lp, 0, 0)); + let lp = Tokens::balance(pool.lp_token, &BOB); + // all lp tokens must have been burnt + assert_eq!(lp, 0_u128); + }); +} + +// +// - test error if trying to remove > lp than we have +#[test] +fn remove_lp_failure() { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_00_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let pool_id = + create_pool(BTC, USDT, initial_btc, initial_usdt, Permill::zero(), Permill::zero()); + let pool = get_pool(pool_id); + let bob_btc = 10 * unit; + let bob_usdt = bob_btc * btc_price; + // Mint the tokens + assert_ok!(Tokens::mint_into(BTC, &BOB, bob_btc)); + assert_ok!(Tokens::mint_into(USDT, &BOB, bob_usdt)); + + // Add the liquidity + assert_ok!(::add_liquidity(&BOB, pool_id, bob_btc, bob_usdt, 0, false)); + let lp = Tokens::balance(pool.lp_token, &BOB); + assert_err!( + ::remove_liquidity(&BOB, pool_id, lp + 1, 0, 0), + TokenError::NoFunds + ); + let min_expected_btc = (bob_btc + 1) * unit; + let min_expected_usdt = (bob_usdt + 1) * unit; + assert_err!( + ::remove_liquidity( + &BOB, + pool_id, + lp, + min_expected_btc, + min_expected_usdt + ), + crate::Error::::CannotRespectMinimumRequested + ); + }); +} + +// +// - test exchange failure +#[test] +fn exchange_failure() { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_00_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let pool_id = + create_pool(BTC, USDT, initial_btc, initial_usdt, Permill::zero(), Permill::zero()); + let bob_btc = 10 * unit; + // Mint the tokens + assert_ok!(Tokens::mint_into(BTC, &BOB, bob_btc)); + + let exchange_btc = 100_u128 * unit; + assert_err!( + ::exchange( + &BOB, + pool_id, + CurrencyPair::new(USDT, BTC), + exchange_btc, + 0, + false + ), + orml_tokens::Error::::BalanceTooLow + ); + let exchange_value = 10 * unit; + let expected_value = exchange_value * btc_price + 1; + assert_err!( + ::exchange( + &BOB, + pool_id, + CurrencyPair::new(USDT, BTC), + exchange_value, + expected_value, + false + ), + crate::Error::::CannotRespectMinimumRequested + ); + }); +} + +// +// - test high slippage scenario +// trying to exchange a large value, will result in high_slippage scenario +// there should be substential difference between expected exchange value and received amount. +#[test] +fn high_slippage() { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_00_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let pool_id = + create_pool(BTC, USDT, initial_btc, initial_usdt, Permill::zero(), Permill::zero()); + let bob_btc = 99_u128 * unit; + // Mint the tokens + assert_ok!(Tokens::mint_into(BTC, &BOB, bob_btc)); + + assert_ok!(::sell(&BOB, pool_id, BTC, bob_btc, false)); + let usdt_balance = Tokens::balance(USDT, &BOB); + let idea_usdt_balance = bob_btc * btc_price; + assert!((idea_usdt_balance - usdt_balance) > 5_u128); + }); +} + +// +// - test protocol_fee and owner_fee +#[test] +fn fees() { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_00_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let fee = Permill::from_float(0.05); + let protocol_fee = Permill::from_float(0.01); + let total_fee = fee + protocol_fee; + let pool_id = create_pool(BTC, USDT, initial_btc, initial_usdt, fee, protocol_fee); + let bob_usdt = 45_000_u128 * unit; + let quote_usdt = bob_usdt - total_fee.mul_floor(bob_usdt); + let expected_btc_value = ::get_exchange_value(pool_id, USDT, quote_usdt) + .expect("get_exchange_value failed"); + // Mint the tokens + assert_ok!(Tokens::mint_into(USDT, &BOB, bob_usdt)); + + assert_ok!(::sell(&BOB, pool_id, USDT, bob_usdt, false)); + let btc_balance = Tokens::balance(BTC, &BOB); + assert_ok!(default_acceptable_computation_error(expected_btc_value, btc_balance)); + }); +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + #[test] + fn buy_sell_proptest( + btc_value in 1..u32::MAX, + ) { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_000_000_000_000_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let btc_value = btc_value as u128 * unit; + let usdt_value = btc_value * btc_price; + let pool_id = create_pool( + BTC, + USDT, + initial_btc, + initial_usdt, + Permill::zero(), + Permill::zero(), + ); + prop_assert_ok!(Tokens::mint_into(USDT, &BOB, usdt_value)); + prop_assert_ok!(Pablo::sell(Origin::signed(BOB), pool_id, USDT, usdt_value, false)); + let bob_btc = Tokens::balance(BTC, &BOB); + // mint extra BTC equal to slippage so that original amount of USDT can be buy back + prop_assert_ok!(Tokens::mint_into(BTC, &BOB, btc_value - bob_btc)); + prop_assert_ok!(Pablo::buy(Origin::signed(BOB), pool_id, USDT, usdt_value, false)); + let bob_usdt = Tokens::balance(USDT, &BOB); + let slippage = usdt_value - bob_usdt; + let slippage_percentage = slippage as f64 * 100.0_f64 / usdt_value as f64; + prop_assert!(slippage_percentage < 1.0_f64); + Ok(()) + })?; + } + + #[test] + fn add_remove_liquidity_proptest( + btc_value in 1..u32::MAX, + ) { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_000_000_000_000_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let btc_value = btc_value as u128 * unit; + let usdt_value = btc_value * btc_price; + let pool_id = create_pool( + BTC, + USDT, + initial_btc, + initial_usdt, + Permill::zero(), + Permill::zero(), + ); + let pool = get_pool(pool_id); + prop_assert_ok!(Tokens::mint_into(USDT, &BOB, usdt_value)); + prop_assert_ok!(Tokens::mint_into(BTC, &BOB, btc_value)); + prop_assert_ok!(Pablo::add_liquidity(Origin::signed(BOB), pool_id, btc_value, usdt_value, 0, false)); + let term1 = initial_usdt.integer_sqrt_checked().expect("integer_sqrt failed"); + let term2 = initial_btc.integer_sqrt_checked().expect("integer_sqrt failed"); + let expected_lp_tokens = safe_multiply_by_rational(term1, btc_value, term2).expect("multiply_by_rational failed"); + let lp_token = Tokens::balance(pool.lp_token, &BOB); + prop_assert_ok!(default_acceptable_computation_error(expected_lp_tokens, lp_token)); + prop_assert_ok!(Pablo::remove_liquidity(Origin::signed(BOB), pool_id, lp_token, 0, 0)); + let btc_value_redeemed = Tokens::balance(BTC, &BOB); + let usdt_value_redeemed = Tokens::balance(USDT, &BOB); + prop_assert_ok!(default_acceptable_computation_error(btc_value_redeemed, btc_value)); + prop_assert_ok!(default_acceptable_computation_error(usdt_value_redeemed, usdt_value)); + Ok(()) + })?; + } + + #[test] + fn swap_proptest( + usdt_value in 1..u32::MAX, + ) { + new_test_ext().execute_with(|| { + let unit = 1_000_000_000_000_u128; + let initial_btc = 1_000_000_000_000_u128 * unit; + let btc_price = 45_000_u128; + let initial_usdt = initial_btc * btc_price; + let usdt_value = usdt_value as u128 * unit; + let pool_id = create_pool( + BTC, + USDT, + initial_btc, + initial_usdt, + Permill::from_float(0.025), + Permill::zero(), + ); + let pool = get_pool(pool_id); + prop_assert_ok!(Tokens::mint_into(USDT, &BOB, usdt_value)); + prop_assert_ok!(Pablo::swap(Origin::signed(BOB), pool_id, CurrencyPair::new(BTC, USDT), usdt_value, 0, false)); + let usdt_value_after_fee = usdt_value - pool.fee.mul_floor(usdt_value); + let ratio = initial_btc as f64 / initial_usdt as f64; + let expected_btc_value = ratio * usdt_value_after_fee as f64; + let expected_btc_value = expected_btc_value as u128; + let bob_btc = Tokens::balance(BTC, &BOB); + prop_assert_ok!(default_acceptable_computation_error(bob_btc, expected_btc_value)); + Ok(()) + })?; +} +} From a06f6089ded69532880aa9cff9d4c80f81d40d4b Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Wed, 16 Mar 2022 19:51:08 +0100 Subject: [PATCH 3/8] Minor --- frame/pablo/src/uniswap_tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/frame/pablo/src/uniswap_tests.rs b/frame/pablo/src/uniswap_tests.rs index 0023591df7e..51d4bf84bcf 100644 --- a/frame/pablo/src/uniswap_tests.rs +++ b/frame/pablo/src/uniswap_tests.rs @@ -283,7 +283,6 @@ fn high_slippage() { }); } -// // - test protocol_fee and owner_fee #[test] fn fees() { From db727730438a72dd2814e8fa283cf98adb057e71 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 17 Mar 2022 11:13:55 +0100 Subject: [PATCH 4/8] [cu-23v2xkb] Tests fixed --- frame/pablo/src/lib.rs | 29 +++++++++--------- frame/pablo/src/uniswap.rs | 50 ++++++++++++++++++++++++++++++-- frame/pablo/src/uniswap_tests.rs | 20 +++++-------- 3 files changed, 68 insertions(+), 31 deletions(-) diff --git a/frame/pablo/src/lib.rs b/frame/pablo/src/lib.rs index a8353055eb6..c536daef8d5 100644 --- a/frame/pablo/src/lib.rs +++ b/frame/pablo/src/lib.rs @@ -50,7 +50,7 @@ pub mod pallet { use crate::{stable_swap::StableSwap, uniswap::Uniswap, PoolConfiguration::ConstantProduct}; use codec::{Codec, FullCodec}; use composable_traits::{ - currency::{CurrencyFactory, LocalAssets}, + currency::CurrencyFactory, defi::CurrencyPair, dex::{Amm, ConstantProductPoolInfo, StableSwapPoolInfo}, math::{SafeAdd, SafeSub}, @@ -93,7 +93,7 @@ pub mod pallet { type BalanceOf = ::Balance; type AccountIdOf = ::AccountId; type PoolIdOf = ::PoolId; - pub type PoolConfigurationOf = + type PoolConfigurationOf = PoolConfiguration<::AccountId, ::AssetId>; type PoolInitConfigurationOf = PoolInitConfiguration<::AssetId>; @@ -450,10 +450,6 @@ pub mod pallet { let base_amount = Self::get_exchange_value(pool_id, pair.base, quote_amount)?; let base_amount_u: u128 = T::Convert::convert(base_amount); - // https://uniswap.org/whitepaper.pdf - // 3.2.1 - // we do not inflate the lp for the owner fees - // cut is done before enforcing the invariant let (lp_fee, protocol_fee) = if apply_fees { let lp_fee = fee.mul_floor(base_amount_u); // protocol_fee is computed based on lp_fee @@ -679,16 +675,17 @@ pub mod pallet { Ok(base_amount_excluding_fees) }, ConstantProduct(constant_product_pool_info) => { - let (base_amount, quote_amount, lp_fees, owner_fees) = Self::do_compute_swap( - pool_id, - pair, - quote_amount, - true, - constant_product_pool_info.fee, - constant_product_pool_info.owner_fee, - )?; + let (base_amount, quote_amount_excluding_fees, lp_fees, owner_fees) = + Uniswap::::do_compute_swap( + &constant_product_pool_info, + pool_account, + pair, + quote_amount, + true, + )?; let total_fees = lp_fees.safe_add(&owner_fees)?; - let quote_amount_including_fees = quote_amount.safe_add(&total_fees)?; + let quote_amount_including_fees = + quote_amount_excluding_fees.safe_add(&total_fees)?; ensure!(base_amount >= min_receive, Error::::CannotRespectMinimumRequested); @@ -716,7 +713,7 @@ pub mod pallet { base_asset: pair.base, quote_asset: pair.quote, base_amount, - quote_amount, + quote_amount: quote_amount_excluding_fees, fee: total_fees, }); diff --git a/frame/pablo/src/uniswap.rs b/frame/pablo/src/uniswap.rs index c415dc41553..1f72b79413c 100644 --- a/frame/pablo/src/uniswap.rs +++ b/frame/pablo/src/uniswap.rs @@ -5,16 +5,16 @@ use composable_maths::dex::constant_product::{ use composable_traits::{ currency::{CurrencyFactory, RangeId}, defi::CurrencyPair, - dex::{Amm, ConstantProductPoolInfo}, + dex::ConstantProductPoolInfo, math::{safe_multiply_by_rational, SafeAdd, SafeSub}, }; use frame_support::{ pallet_prelude::*, traits::fungibles::{Inspect, Mutate, Transfer}, - transactional, PalletId, + transactional, }; use sp_runtime::{ - traits::{AccountIdConversion, CheckedAdd, Convert, One, Zero}, + traits::{CheckedAdd, Convert, One, Zero}, ArithmeticError, Permill, }; @@ -151,4 +151,48 @@ impl Uniswap { Ok((base_amount, quote_amount, lp_issued.safe_sub(&lp_amount)?)) } + + pub(crate) fn do_compute_swap( + pool: &ConstantProductPoolInfo, + pool_account: T::AccountId, + pair: CurrencyPair, + quote_amount: T::Balance, + apply_fees: bool, + ) -> Result<(T::Balance, T::Balance, T::Balance, T::Balance), DispatchError> { + let pool_base_aum = T::Convert::convert(T::Assets::balance(pair.base, &pool_account)); + let pool_quote_aum = T::Convert::convert(T::Assets::balance(pair.quote, &pool_account)); + let quote_amount = T::Convert::convert(quote_amount); + + // https://uniswap.org/whitepaper.pdf + // 3.2.1 + // we do not inflate the lp for the owner fees + // cut is done before enforcing the invariant + let (lp_fee, owner_fee) = if apply_fees { + let lp_fee = pool.fee.mul_floor(quote_amount); + let owner_fee = pool.owner_fee.mul_floor(quote_amount); + (lp_fee, owner_fee) + } else { + (0, 0) + }; + + let quote_amount_excluding_fees = quote_amount.safe_sub(&lp_fee)?.safe_sub(&owner_fee)?; + + let half_weight = Permill::from_percent(50); + let base_amount = compute_out_given_in( + half_weight, + half_weight, + pool_quote_aum, + pool_base_aum, + quote_amount_excluding_fees, + )?; + + ensure!(base_amount > 0 && quote_amount_excluding_fees > 0, Error::::InvalidAmount); + + Ok(( + T::Convert::convert(base_amount), + T::Convert::convert(quote_amount_excluding_fees), + T::Convert::convert(lp_fee), + T::Convert::convert(owner_fee), + )) + } } diff --git a/frame/pablo/src/uniswap_tests.rs b/frame/pablo/src/uniswap_tests.rs index 51d4bf84bcf..900bd714493 100644 --- a/frame/pablo/src/uniswap_tests.rs +++ b/frame/pablo/src/uniswap_tests.rs @@ -17,10 +17,7 @@ use frame_support::{ traits::fungibles::{Inspect, Mutate}, }; use proptest::prelude::*; -use sp_runtime::{ - traits::{IntegerSquareRoot, Zero}, - Permill, TokenError, -}; +use sp_runtime::{traits::IntegerSquareRoot, Permill, TokenError}; fn create_pool( base_asset: AssetId, @@ -56,14 +53,13 @@ fn get_pool(pool_id: PoolId) -> ConstantProductPoolInfo { #[test] fn test() { new_test_ext().execute_with(|| { - let pool_id = create_pool( - BTC, - USDT, - Balance::zero(), - Balance::zero(), - Permill::zero(), - Permill::zero(), - ); + let pool_init_config = PoolInitConfiguration::ConstantProduct { + pair: CurrencyPair::new(BTC, USDT), + fee: Permill::zero(), + owner_fee: Permill::zero(), + }; + let pool_id = + Pablo::do_create_pool(&ALICE, pool_init_config).expect("pool creation failed"); let pool = get_pool(pool_id); From e4e6bf446d2c942542120e4a7f2d6ab3b38c52f5 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 17 Mar 2022 11:19:03 +0100 Subject: [PATCH 5/8] Linter seems confused --- frame/pablo/src/uniswap_tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frame/pablo/src/uniswap_tests.rs b/frame/pablo/src/uniswap_tests.rs index 900bd714493..07f9b486e52 100644 --- a/frame/pablo/src/uniswap_tests.rs +++ b/frame/pablo/src/uniswap_tests.rs @@ -255,10 +255,9 @@ fn exchange_failure() { }); } -// // - test high slippage scenario // trying to exchange a large value, will result in high_slippage scenario -// there should be substential difference between expected exchange value and received amount. +// there should be substantial difference between expected exchange value and received amount. #[test] fn high_slippage() { new_test_ext().execute_with(|| { From c04b024970f424fe10859877ac0a517c62ac51a2 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 17 Mar 2022 11:33:24 +0100 Subject: [PATCH 6/8] Review and try lint --- frame/pablo/src/lib.rs | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/frame/pablo/src/lib.rs b/frame/pablo/src/lib.rs index c536daef8d5..d096115683f 100644 --- a/frame/pablo/src/lib.rs +++ b/frame/pablo/src/lib.rs @@ -117,30 +117,9 @@ pub mod pallet { /// Amount of quote asset repatriated. quote_amount: T::Balance, }, + /// Liquidity added into the pool `T::PoolId`. - LiquidityAddedToStableSwapPool { - /// Account id who added liquidity. - who: T::AccountId, - /// Pool id to which liquidity added. - pool_id: T::PoolId, - /// Amount of base asset deposited. - base_amount: T::Balance, - /// Amount of quote asset deposited. - quote_amount: T::Balance, - /// Amount of minted lp tokens. - mint_amount: T::Balance, - }, - /// Liquidity added into the pool `T::PoolId`. - LiquidityAddedToLiquidityBootstrappingPool { - /// Pool id to which liquidity added. - pool_id: T::PoolId, - /// Amount of base asset deposited. - base_amount: T::Balance, - /// Amount of quote asset deposited. - quote_amount: T::Balance, - }, - /// Liquidity added into the pool `T::PoolId`. - LiquidityAddedToConstantProductPool { + LiquidityAdded { /// Account id who added liquidity. who: T::AccountId, /// Pool id to which liquidity added. @@ -533,12 +512,12 @@ pub mod pallet { min_mint_amount, keep_alive, )?; - Self::deposit_event(Event::::LiquidityAddedToStableSwapPool { + Self::deposit_event(Event::::LiquidityAdded { who: who.clone(), pool_id, base_amount, quote_amount, - mint_amount, + minted_lp: mint_amount, }); }, ConstantProduct(constant_product_pool_info) => { @@ -551,7 +530,7 @@ pub mod pallet { min_mint_amount, keep_alive, )?; - Self::deposit_event(Event::::LiquidityAddedToConstantProductPool { + Self::deposit_event(Event::::LiquidityAdded { who: who.clone(), pool_id, base_amount, From b170370e341c5c525b617c58e88659e741c73529 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 17 Mar 2022 11:41:12 +0100 Subject: [PATCH 7/8] try lint --- frame/pablo/src/uniswap_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/frame/pablo/src/uniswap_tests.rs b/frame/pablo/src/uniswap_tests.rs index 07f9b486e52..c614858be32 100644 --- a/frame/pablo/src/uniswap_tests.rs +++ b/frame/pablo/src/uniswap_tests.rs @@ -278,6 +278,7 @@ fn high_slippage() { }); } +// // - test protocol_fee and owner_fee #[test] fn fees() { From 332cf33fac469eb8b565947db4b0502ba42f7e49 Mon Sep 17 00:00:00 2001 From: Vim Wickramasinghe Date: Thu, 17 Mar 2022 11:48:08 +0100 Subject: [PATCH 8/8] try lint --- frame/pablo/src/uniswap_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/frame/pablo/src/uniswap_tests.rs b/frame/pablo/src/uniswap_tests.rs index c614858be32..188f6bac624 100644 --- a/frame/pablo/src/uniswap_tests.rs +++ b/frame/pablo/src/uniswap_tests.rs @@ -255,6 +255,7 @@ fn exchange_failure() { }); } +// // - test high slippage scenario // trying to exchange a large value, will result in high_slippage scenario // there should be substantial difference between expected exchange value and received amount.