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..d096115683f 100644 --- a/frame/pablo/src/lib.rs +++ b/frame/pablo/src/lib.rs @@ -37,19 +37,22 @@ 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::{stable_swap::StableSwap, uniswap::Uniswap, PoolConfiguration::ConstantProduct}; use codec::{Codec, FullCodec}; use composable_traits::{ currency::CurrencyFactory, defi::CurrencyPair, - dex::{Amm, StableSwapPoolInfo}, + dex::{Amm, ConstantProductPoolInfo, StableSwapPoolInfo}, math::{SafeAdd, SafeSub}, }; use core::fmt::Debug; @@ -65,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 { @@ -75,11 +76,17 @@ 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)] pub enum PoolConfiguration { StableSwap(StableSwapPoolInfo), + ConstantProduct(ConstantProductPoolInfo), } type AssetIdOf = ::AssetId; @@ -90,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 { @@ -109,6 +117,7 @@ pub mod pallet { /// Amount of quote asset repatriated. quote_amount: T::Balance, }, + /// Liquidity added into the pool `T::PoolId`. LiquidityAdded { /// Account id who added liquidity. @@ -119,8 +128,8 @@ pub mod pallet { base_amount: T::Balance, /// Amount of quote asset deposited. quote_amount: T::Balance, - /// Amount of minted lp tokens. - mint_amount: T::Balance, + /// Amount of minted lp. + minted_lp: T::Balance, }, /// Liquidity removed from pool `T::PoolId` by `T::AccountId` in balanced way. LiquidityRemoved { @@ -167,6 +176,9 @@ pub mod pallet { InvalidPair, InvalidFees, AmpFactorMustBeGreaterThanZero, + + // ConstantProduct Specific: Possibly rename + MissingAmount, } #[pallet::config] @@ -388,6 +400,11 @@ pub mod pallet { Ok(pool_id) }, + 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) + }, } } @@ -445,6 +462,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), } } @@ -463,6 +481,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, + ), } } @@ -493,10 +517,29 @@ pub mod pallet { pool_id, base_amount, quote_amount, - mint_amount, + minted_lp: 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::::LiquidityAdded { + who: who.clone(), + pool_id, + base_amount, + quote_amount, + minted_lp: mint_amount, }); }, } + // TODO refactor event publishing with cu-23v2y3n Ok(()) } @@ -529,7 +572,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(()) } @@ -592,7 +653,54 @@ pub mod pallet { Ok(base_amount_excluding_fees) }, + ConstantProduct(constant_product_pool_info) => { + 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_excluding_fees.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: quote_amount_excluding_fees, + fee: total_fees, + }); + + Ok(base_amount) + }, } + + // TODO refactor event publishing with cu-23v2y3n } #[transactional] @@ -616,6 +724,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) + }, } } @@ -632,15 +749,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 new file mode 100644 index 00000000000..1f72b79413c --- /dev/null +++ b/frame/pablo/src/uniswap.rs @@ -0,0 +1,198 @@ +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, +}; +use composable_traits::{ + currency::{CurrencyFactory, RangeId}, + defi::CurrencyPair, + dex::ConstantProductPoolInfo, + math::{safe_multiply_by_rational, SafeAdd, SafeSub}, +}; +use frame_support::{ + pallet_prelude::*, + traits::fungibles::{Inspect, Mutate, Transfer}, + transactional, +}; +use sp_runtime::{ + traits::{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) + } + + 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)); + 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] + 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 { + ensure!(base_amount > T::Balance::zero(), Error::::InvalidAmount); + 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_token_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_token_to_mint = T::Convert::convert(lp_token_to_mint); + + ensure!(quote_amount > T::Balance::zero(), Error::::InvalidAmount); + 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_token_to_mint)?; + Ok(lp_token_to_mint) + } + + #[transactional] + 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)); + 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)?; + + 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 new file mode 100644 index 00000000000..188f6bac624 --- /dev/null +++ b/frame/pablo/src/uniswap_tests.rs @@ -0,0 +1,410 @@ +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, 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_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); + + 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 substantial 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(()) + })?; +} +}