diff --git a/Cargo.lock b/Cargo.lock index c28a52e5b..c93306162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3685,7 +3685,7 @@ dependencies = [ [[package]] name = "hydra-dx-math" -version = "7.2.0" +version = "7.3.0" dependencies = [ "approx", "criterion", @@ -3842,6 +3842,7 @@ dependencies = [ "pallet-dca", "pallet-democracy", "pallet-duster", + "pallet-dynamic-fees", "pallet-elections-phragmen", "pallet-ema-oracle", "pallet-genesis-history", @@ -6394,7 +6395,7 @@ dependencies = [ [[package]] name = "pallet-circuit-breaker" -version = "1.1.13" +version = "1.1.14" dependencies = [ "frame-benchmarking", "frame-support", @@ -6543,7 +6544,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.1.5" +version = "1.1.6" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -6626,7 +6627,7 @@ dependencies = [ [[package]] name = "pallet-dynamic-fees" -version = "1.0.0" +version = "1.0.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -7034,7 +7035,7 @@ dependencies = [ [[package]] name = "pallet-omnipool" -version = "2.1.1" +version = "3.1.0" dependencies = [ "bitflags", "frame-benchmarking", @@ -7061,7 +7062,7 @@ dependencies = [ [[package]] name = "pallet-omnipool-liquidity-mining" -version = "2.0.9" +version = "2.0.10" dependencies = [ "bitflags", "frame-benchmarking", @@ -10094,6 +10095,7 @@ dependencies = [ "pallet-dca", "pallet-democracy", "pallet-duster", + "pallet-dynamic-fees", "pallet-elections-phragmen", "pallet-ema-oracle", "pallet-liquidity-mining", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index b4b923c67..9a4d94a94 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -30,6 +30,7 @@ pallet-otc = { workspace = true } pallet-relaychain-info = { workspace = true } pallet-route-executor = { workspace = true} pallet-dca = { workspace = true} +pallet-dynamic-fees = { workspace = true } pallet-treasury = { workspace = true } pallet-democracy = { workspace = true } diff --git a/integration-tests/src/dynamic_fees.rs b/integration-tests/src/dynamic_fees.rs new file mode 100644 index 000000000..98da46493 --- /dev/null +++ b/integration-tests/src/dynamic_fees.rs @@ -0,0 +1,426 @@ +#![cfg(test)] + +use crate::{oracle::hydradx_run_to_block, polkadot_test_net::*}; +use frame_support::assert_ok; +use pallet_dynamic_fees::types::FeeEntry; +use primitives::AssetId; +use sp_runtime::{FixedU128, Permill}; +use xcm_emulator::TestExt; + +const DOT_UNITS: u128 = 10_000_000_000; +const BTC_UNITS: u128 = 10_000_000; +const ETH_UNITS: u128 = 1_000_000_000_000_000_000; + +#[test] +fn fees_should_work_when_oracle_not_initialized() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + + let trader = DAVE; + + set_balance(trader.into(), DOT, 1_000 * DOT_UNITS as i128); + + assert!(hydradx_runtime::DynamicFees::current_fees(HDX).is_none()); + + //Act + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + // Fees are not recalculated because nothing has been provided by oracle ( it did not go through on init yet) + assert!(hydradx_runtime::DynamicFees::current_fees(HDX).is_none()); + assert!(hydradx_runtime::DynamicFees::current_fees(DOT).is_none()); + }); +} + +#[test] +fn fees_should_initialize_lazily_to_min_value_when_first_trade_happens() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + init_oracle(); + hydradx_run_to_block(10); + + assert!(hydradx_runtime::DynamicFees::current_fees(HDX).is_none()); + + //Act + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + let asset_fee_params = ::AssetFeeParameters::get(); + + //Assert + assert_eq!( + hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(), + FeeEntry { + asset_fee: asset_fee_params.min_fee, + protocol_fee: Permill::from_float(0.000788_f64), + timestamp: 10_u32 + } + ); + }); +} + +#[test] +fn fees_should_initialize_lazily_to_min_value_when_first_buy_happens() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + init_oracle(); + hydradx_run_to_block(10); + + assert!(hydradx_runtime::DynamicFees::current_fees(HDX).is_none()); + + set_balance(DAVE.into(), HDX, 1_000 * UNITS as i128); + //Act + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX, + )); + + let asset_fee_params = ::AssetFeeParameters::get(); + + //Assert + assert_eq!( + hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(), + FeeEntry { + asset_fee: asset_fee_params.min_fee, + protocol_fee: Permill::from_float(0.000788_f64), + timestamp: 10_u32 + } + ); + }); +} + +#[test] +fn fees_should_change_when_buys_happen_in_different_blocks() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + init_oracle(); + hydradx_run_to_block(10); + + set_balance(DAVE.into(), HDX, 1_000 * UNITS as i128); + + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX, + )); + + let old_fees = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + + //Act + hydradx_run_to_block(11); + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX, + )); + + //Assert + let current_fee = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + assert_ne!(current_fee, old_fees); + assert_eq!( + current_fee, + FeeEntry { + asset_fee: Permill::from_float(0.0025_f64), + protocol_fee: Permill::from_float(0.001_f64), + timestamp: 11_u32 + } + ); + }); +} + +#[test] +fn fees_should_change_when_sells_happen_in_different_blocks() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + init_oracle(); + hydradx_run_to_block(10); + + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + let old_fees = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + + //Act + hydradx_run_to_block(11); + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + //Assert + let current_fee = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + assert_ne!(current_fee, old_fees); + assert_eq!( + current_fee, + FeeEntry { + asset_fee: Permill::from_float(0.0025_f64), + protocol_fee: Permill::from_float(0.000926_f64), + timestamp: 11_u32 + } + ); + }); +} + +#[test] +fn fees_should_change_when_trades_happen_in_different_blocks() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + init_oracle(); + hydradx_run_to_block(10); + + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + let old_fees = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + + //Act + hydradx_run_to_block(11); + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX, + )); + + //Assert + let current_fee = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + assert_ne!(current_fee, old_fees); + assert_eq!( + current_fee, + FeeEntry { + asset_fee: Permill::from_float(0.0025_f64), + protocol_fee: Permill::from_float(0.000926_f64), + timestamp: 11_u32 + } + ); + }); +} + +#[test] +fn fees_should_change_only_one_when_trades_happen_in_the_same_block() { + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + init_omnipool(); + init_oracle(); + hydradx_run_to_block(10); + + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + let old_fees = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + set_balance(DAVE.into(), HDX, 1_000 * UNITS as i128); + + //Act & assert + hydradx_run_to_block(11); + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX, + )); + + let current_fee = hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(); + assert_ne!(current_fee, old_fees); + assert_eq!( + current_fee, + FeeEntry { + asset_fee: Permill::from_float(0.0025_f64), + protocol_fee: Permill::from_float(0.000926_f64), + timestamp: 11_u32 + } + ); + + //NOTE: second trade in the same block should not change fees + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX, + )); + + assert_eq!(hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(), current_fee); + + //NOTE: second trade in the same block should not change fees + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + assert_eq!(hydradx_runtime::DynamicFees::current_fees(HDX).unwrap(), current_fee); + }); +} + +fn set_balance(who: hydradx_runtime::AccountId, currency: AssetId, amount: i128) { + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + who, + currency, + amount, + )); +} + +fn init_omnipool() { + let native_price = FixedU128::from_inner(1201500000000000); + let stable_price = FixedU128::from_inner(45_000_000_000); + + assert_ok!(hydradx_runtime::Omnipool::set_tvl_cap( + hydradx_runtime::RuntimeOrigin::root(), + 522_222_000_000_000_000_000_000, + )); + + assert_ok!(hydradx_runtime::Omnipool::initialize_pool( + hydradx_runtime::RuntimeOrigin::root(), + stable_price, + native_price, + Permill::from_percent(100), + Permill::from_percent(10) + )); + + let dot_price = FixedU128::from_inner(25_650_000_000_000_000_000); + assert_ok!(hydradx_runtime::Omnipool::add_token( + hydradx_runtime::RuntimeOrigin::root(), + DOT, + dot_price, + Permill::from_percent(100), + AccountId::from(BOB), + )); + + let eth_price = FixedU128::from_inner(71_145_071_145_071); + assert_ok!(hydradx_runtime::Omnipool::add_token( + hydradx_runtime::RuntimeOrigin::root(), + ETH, + eth_price, + Permill::from_percent(100), + AccountId::from(BOB), + )); + + let btc_price = FixedU128::from_inner(9_647_109_647_109_650_000_000_000); + assert_ok!(hydradx_runtime::Omnipool::add_token( + hydradx_runtime::RuntimeOrigin::root(), + BTC, + btc_price, + Permill::from_percent(100), + AccountId::from(BOB), + )); +} + +/// This function executes one sell and buy with HDX for all assets in the omnipool. This is necessary to +/// oracle have a prices for the assets. +/// NOTE: It's necessary to change parachain block to oracle have prices. +fn init_oracle() { + let trader = DAVE; + + set_balance(trader.into(), HDX, 1_000 * UNITS as i128); + set_balance(trader.into(), DOT, 1_000 * DOT_UNITS as i128); + set_balance(trader.into(), ETH, 1_000 * ETH_UNITS as i128); + set_balance(trader.into(), BTC, 1_000 * BTC_UNITS as i128); + + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + 0, + )); + + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + DOT, + HDX, + 2 * DOT_UNITS, + u128::MAX + )); + + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + ETH, + HDX, + 2 * ETH_UNITS, + 0, + )); + + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + ETH, + HDX, + 2 * ETH_UNITS, + u128::MAX + )); + + assert_ok!(hydradx_runtime::Omnipool::sell( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + BTC, + HDX, + 2 * BTC_UNITS, + 0, + )); + + assert_ok!(hydradx_runtime::Omnipool::buy( + hydradx_runtime::RuntimeOrigin::signed(DAVE.into()), + BTC, + HDX, + 2 * BTC_UNITS, + u128::MAX + )); +} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index f47137d32..0450b3afc 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -4,6 +4,7 @@ mod cross_chain_transfer; mod dca; mod dust; mod dust_removal_whitelist; +mod dynamic_fees; mod non_native_fee; mod omnipool_init; mod omnipool_liquidity_mining; diff --git a/math/Cargo.toml b/math/Cargo.toml index c0c270065..76c74d5ed 100644 --- a/math/Cargo.toml +++ b/math/Cargo.toml @@ -6,7 +6,7 @@ license = 'Apache-2.0' name = "hydra-dx-math" description = "A collection of utilities to make performing liquidity pool calculations more convenient." repository = 'https://github.com/galacticcouncil/hydradx-math' -version = "7.2.0" +version = "7.3.0" [dependencies] primitive-types = {default-features = false, version = '0.12.0'} diff --git a/math/src/omnipool/math.rs b/math/src/omnipool/math.rs index aabd0405b..d1a7c21ee 100644 --- a/math/src/omnipool/math.rs +++ b/math/src/omnipool/math.rs @@ -1,6 +1,6 @@ use crate::omnipool::types::BalanceUpdate::{Decrease, Increase}; use crate::omnipool::types::{ - AssetReserveState, AssetStateChange, BalanceUpdate, HubTradeStateChange, LiquidityStateChange, Position, + AssetReserveState, AssetStateChange, BalanceUpdate, HubTradeStateChange, LiquidityStateChange, Position, TradeFee, TradeStateChange, I129, }; use crate::types::Balance; @@ -49,7 +49,10 @@ pub fn calculate_sell_state_changes( .checked_mul(delta_hub_reserve_out_hp) .and_then(|v| v.checked_div(out_hub_reserve_hp.checked_add(delta_hub_reserve_out_hp)?))?; - let delta_reserve_out = amount_without_fee(to_balance!(delta_reserve_out).ok()?, asset_fee)?; + let amount_out = to_balance!(delta_reserve_out).ok()?; + let delta_reserve_out = amount_without_fee(amount_out, asset_fee)?; + + let asset_fee = amount_out.saturating_sub(delta_reserve_out); let delta_imbalance = min(protocol_fee_amount, imbalance); @@ -68,6 +71,10 @@ pub fn calculate_sell_state_changes( }, delta_imbalance: BalanceUpdate::Increase(delta_imbalance), hdx_hub_amount: hdx_fee_amount, + fee: TradeFee { + asset_fee, + protocol_fee: protocol_fee_amount, + }, }) } @@ -101,8 +108,9 @@ pub fn calculate_sell_hub_state_changes( .checked_mul(amount_hp) .and_then(|v| v.checked_div(hub_reserve_hp.checked_add(amount_hp)?))?; - let delta_reserve_out = to_balance!(delta_reserve_out_hp).ok()?; - let delta_reserve_out = amount_without_fee(delta_reserve_out, asset_fee)?; + let amount_out = to_balance!(delta_reserve_out_hp).ok()?; + let delta_reserve_out = amount_without_fee(amount_out, asset_fee)?; + let asset_fee = amount_out.saturating_sub(delta_reserve_out); let delta_imbalance = calculate_imbalance_in_hub_swap(total_hub_reserve, hub_asset_amount, imbalance)?; @@ -113,6 +121,45 @@ pub fn calculate_sell_hub_state_changes( ..Default::default() }, delta_imbalance: Decrease(delta_imbalance), + fee: TradeFee { + asset_fee, + ..Default::default() + }, + }) +} + +// only temporary helper function to calculate in amount with no fees +// will be removed when fee calculation in buy is simplified +fn calculate_hub_asset_in_for_given_out_no_fees( + asset_out_state: &AssetReserveState, + asset_out_amount: Balance, + imbalance: I129, + total_hub_reserve: Balance, +) -> Option> { + let asset_fee = Permill::zero(); + let reserve_no_fee = amount_without_fee(asset_out_state.reserve, asset_fee)?; + let hub_denominator = reserve_no_fee.checked_sub(asset_out_amount)?; + + let (hub_reserve_hp, amount_hp, hub_denominator_hp) = + to_u256!(asset_out_state.hub_reserve, asset_out_amount, hub_denominator); + + let delta_hub_reserve_hp = hub_reserve_hp.checked_mul(amount_hp).and_then(|v| { + v.checked_div(hub_denominator_hp) + .and_then(|v| v.checked_add(U256::one())) + })?; + + let delta_hub_reserve = to_balance!(delta_hub_reserve_hp).ok()?; + + let delta_imbalance = calculate_imbalance_in_hub_swap(total_hub_reserve, delta_hub_reserve, imbalance)?; + + Some(HubTradeStateChange { + asset: AssetStateChange { + delta_reserve: Decrease(asset_out_amount), + delta_hub_reserve: Increase(delta_hub_reserve), + ..Default::default() + }, + delta_imbalance: Decrease(delta_imbalance), + fee: TradeFee::default(), }) } @@ -124,10 +171,8 @@ pub fn calculate_buy_for_hub_asset_state_changes( imbalance: I129, total_hub_reserve: Balance, ) -> Option> { - let hub_denominator = Permill::from_percent(100) - .checked_sub(&asset_fee)? - .mul_floor(asset_out_state.reserve) - .checked_sub(asset_out_amount)?; + let reserve_no_fee = amount_without_fee(asset_out_state.reserve, asset_fee)?; + let hub_denominator = reserve_no_fee.checked_sub(asset_out_amount)?; let (hub_reserve_hp, amount_hp, hub_denominator_hp) = to_u256!(asset_out_state.hub_reserve, asset_out_amount, hub_denominator); @@ -141,6 +186,12 @@ pub fn calculate_buy_for_hub_asset_state_changes( let delta_imbalance = calculate_imbalance_in_hub_swap(total_hub_reserve, delta_hub_reserve, imbalance)?; + // TODO: consider rework the math here + // currently we just recalculate the state changes with zero fees and work out the difference to get fee amount + let no_fee_changes = + calculate_hub_asset_in_for_given_out_no_fees(asset_out_state, asset_out_amount, imbalance, total_hub_reserve)?; + let asset_fee = delta_hub_reserve.saturating_sub(*no_fee_changes.asset.delta_hub_reserve); + Some(HubTradeStateChange { asset: AssetStateChange { delta_reserve: Decrease(asset_out_amount), @@ -148,6 +199,73 @@ pub fn calculate_buy_for_hub_asset_state_changes( ..Default::default() }, delta_imbalance: Decrease(delta_imbalance), + fee: TradeFee { + asset_fee, + ..Default::default() + }, + }) +} + +// only temporary helper function to calculate in amount with no fees +// will be removed when fee calculation in buy is simplified +fn calculate_amount_in_no_fee( + asset_in_state: &AssetReserveState, + asset_out_state: &AssetReserveState, + amount: Balance, + imbalance: Balance, +) -> Option> { + let asset_fee = Permill::zero(); + let protocol_fee = Permill::zero(); + let reserve_no_fee = amount_without_fee(asset_out_state.reserve, asset_fee)?; + let (out_hub_reserve, out_reserve_no_fee, out_amount) = + to_u256!(asset_out_state.hub_reserve, reserve_no_fee, amount); + + let delta_hub_reserve_out = out_hub_reserve + .checked_mul(out_amount) + .and_then(|v| v.checked_div(out_reserve_no_fee.checked_sub(out_amount)?))?; + + let delta_hub_reserve_out = to_balance!(delta_hub_reserve_out).ok()?; + let delta_hub_reserve_out = delta_hub_reserve_out.checked_add(Balance::one())?; + + // Negative + let delta_hub_reserve_in: Balance = FixedU128::from_inner(delta_hub_reserve_out) + .checked_div(&Permill::from_percent(100).sub(protocol_fee).into())? + .into_inner(); + + if delta_hub_reserve_in >= asset_in_state.hub_reserve { + return None; + } + + let (delta_hub_reserve_in_hp, in_hub_reserve_hp, in_reserve_hp) = + to_u256!(delta_hub_reserve_in, asset_in_state.hub_reserve, asset_in_state.reserve); + + let delta_reserve_in = in_reserve_hp + .checked_mul(delta_hub_reserve_in_hp) + .and_then(|v| v.checked_div(in_hub_reserve_hp.checked_sub(delta_hub_reserve_in_hp)?))?; + + let delta_reserve_in = to_balance!(delta_reserve_in).ok()?; + let delta_reserve_in = delta_reserve_in.checked_add(Balance::one())?; + + // Fee accounting and imbalance + let protocol_fee_amount = protocol_fee.mul_floor(delta_hub_reserve_in); + let delta_imbalance = min(protocol_fee_amount, imbalance); + + let hdx_fee_amount = protocol_fee_amount.checked_sub(delta_imbalance)?; + + Some(TradeStateChange { + asset_in: AssetStateChange { + delta_reserve: Increase(delta_reserve_in), + delta_hub_reserve: Decrease(delta_hub_reserve_in), + ..Default::default() + }, + asset_out: AssetStateChange { + delta_reserve: Decrease(amount), + delta_hub_reserve: Increase(delta_hub_reserve_out), + ..Default::default() + }, + delta_imbalance: BalanceUpdate::Increase(delta_imbalance), + hdx_hub_amount: hdx_fee_amount, + fee: TradeFee::default(), }) } @@ -190,6 +308,12 @@ pub fn calculate_buy_state_changes( let delta_reserve_in = to_balance!(delta_reserve_in).ok()?; let delta_reserve_in = delta_reserve_in.checked_add(Balance::one())?; + // TODO: consider rework the math here + // currently we just recalculate the state changes with zero fees and work out the difference to get fee amount + // but it is unnecessary work + let no_fee_changes = calculate_amount_in_no_fee(asset_in_state, asset_out_state, amount, imbalance)?; + let asset_fee = delta_reserve_in.saturating_sub(*no_fee_changes.asset_in.delta_reserve); + // Fee accounting and imbalance let protocol_fee_amount = protocol_fee.mul_floor(delta_hub_reserve_in); let delta_imbalance = min(protocol_fee_amount, imbalance); @@ -209,6 +333,10 @@ pub fn calculate_buy_state_changes( }, delta_imbalance: BalanceUpdate::Increase(delta_imbalance), hdx_hub_amount: hdx_fee_amount, + fee: TradeFee { + asset_fee, + protocol_fee: protocol_fee_amount, + }, }) } diff --git a/math/src/omnipool/tests.rs b/math/src/omnipool/tests.rs index 0e3d1e516..85f74ed35 100644 --- a/math/src/omnipool/tests.rs +++ b/math/src/omnipool/tests.rs @@ -1,4 +1,4 @@ -use crate::omnipool::types::{AssetReserveState, BalanceUpdate, Position, I129}; +use crate::omnipool::types::{AssetReserveState, BalanceUpdate, Position, TradeFee, I129}; use crate::omnipool::{ calculate_add_liquidity_state_changes, calculate_buy_for_hub_asset_state_changes, calculate_buy_state_changes, calculate_cap_difference, calculate_delta_imbalance, calculate_remove_liquidity_state_changes, @@ -64,6 +64,92 @@ fn calculate_sell_should_work_when_correct_input_provided() { ); assert_eq!(state_changes.delta_imbalance, BalanceUpdate::Increase(0u128)); assert_eq!(state_changes.hdx_hub_amount, 0u128); + assert_eq!(state_changes.fee, TradeFee::default()); +} + +#[test] +fn calculate_sell_should_return_correct_when_protocol_fee_is_zero() { + let asset_in_state = AssetReserveState { + reserve: 10 * UNIT, + hub_reserve: 20 * UNIT, + shares: 10 * UNIT, + protocol_shares: 0u128, + }; + let asset_out_state = AssetReserveState { + reserve: 5 * UNIT, + hub_reserve: 5 * UNIT, + shares: 20 * UNIT, + protocol_shares: 0u128, + }; + + let amount_to_sell = 4 * UNIT; + let asset_fee = Permill::from_percent(1); + let protocol_fee = Permill::from_percent(0); + let imbalance = 2 * UNIT; + + let state_changes = calculate_sell_state_changes( + &asset_in_state, + &asset_out_state, + amount_to_sell, + asset_fee, + protocol_fee, + imbalance, + ); + + assert!(state_changes.is_some()); + + let state_changes = state_changes.unwrap(); + assert_eq!( + state_changes.asset_out.delta_reserve, + BalanceUpdate::Decrease(2666666666666u128 - state_changes.fee.asset_fee) + ); + assert_eq!( + state_changes.fee, + TradeFee { + asset_fee: 26666666667, + protocol_fee: 0, + } + ); +} + +#[test] +fn calculate_sell_should_return_correct_when_protocol_fee_is_not_zero() { + let asset_in_state = AssetReserveState { + reserve: 10 * UNIT, + hub_reserve: 20 * UNIT, + shares: 10 * UNIT, + protocol_shares: 0u128, + }; + let asset_out_state = AssetReserveState { + reserve: 5 * UNIT, + hub_reserve: 5 * UNIT, + shares: 20 * UNIT, + protocol_shares: 0u128, + }; + + let amount_to_sell = 4 * UNIT; + let asset_fee = Permill::from_percent(1); + let protocol_fee = Permill::from_percent(1); + let imbalance = 2 * UNIT; + + let state_changes = calculate_sell_state_changes( + &asset_in_state, + &asset_out_state, + amount_to_sell, + asset_fee, + protocol_fee, + imbalance, + ); + + assert!(state_changes.is_some()); + let state_changes = state_changes.unwrap(); + assert_eq!( + state_changes.fee, + TradeFee { + asset_fee: 26541554960, + protocol_fee: 57142857142, + } + ); } #[test] @@ -159,6 +245,7 @@ fn calculate_sell_hub_asset_should_work_when_correct_input_provided() { ); assert_eq!(state_changes.delta_imbalance, BalanceUpdate::Decrease(7454545454546)); + assert_eq!(state_changes.fee, TradeFee::default()); } #[test] @@ -195,6 +282,13 @@ fn calculate_sell_hub_asset_with_fee_should_work_when_correct_input_provided() { ); assert_eq!(state_changes.delta_imbalance, BalanceUpdate::Decrease(7454545454546)); + assert_eq!( + state_changes.fee, + TradeFee { + asset_fee: 16666666667, + protocol_fee: 0, + } + ); } #[test] @@ -251,6 +345,100 @@ fn calculate_buy_should_work_when_correct_input_provided() { assert_eq!(state_changes.hdx_hub_amount, 0u128); } +#[test] +fn calculate_buy_should_return_correct_fee_when_protocol_fee_is_zero() { + let asset_in_state = AssetReserveState { + reserve: 10 * UNIT, + hub_reserve: 20 * UNIT, + shares: 10 * UNIT, + protocol_shares: 0u128, + }; + let asset_out_state = AssetReserveState { + reserve: 5 * UNIT, + hub_reserve: 5 * UNIT, + shares: 20 * UNIT, + protocol_shares: 0u128, + }; + + let amount_to_buy = UNIT; + let asset_fee = Permill::from_percent(1); + let protocol_fee = Permill::from_percent(0); + let imbalance = 2 * UNIT; + + let state_changes = calculate_buy_state_changes( + &asset_in_state, + &asset_out_state, + amount_to_buy, + asset_fee, + protocol_fee, + imbalance, + ); + + assert!(state_changes.is_some()); + + let state_changes = state_changes.unwrap(); + + assert_eq!( + state_changes.asset_in.delta_reserve, + BalanceUpdate::Increase(666666666668u128 + state_changes.fee.asset_fee) + ); + + assert_eq!( + state_changes.fee, + TradeFee { + asset_fee: 9009009009, + protocol_fee: 0, + } + ) +} + +#[test] +fn calculate_buy_should_return_correct_fee_when_protocol_fee_is_non_zero() { + let asset_in_state = AssetReserveState { + reserve: 10 * UNIT, + hub_reserve: 20 * UNIT, + shares: 10 * UNIT, + protocol_shares: 0u128, + }; + let asset_out_state = AssetReserveState { + reserve: 5 * UNIT, + hub_reserve: 5 * UNIT, + shares: 20 * UNIT, + protocol_shares: 0u128, + }; + + let amount_to_buy = UNIT; + let asset_fee = Permill::from_percent(1); + let protocol_fee = Permill::from_percent(1); + let imbalance = 2 * UNIT; + + let state_changes = calculate_buy_state_changes( + &asset_in_state, + &asset_out_state, + amount_to_buy, + asset_fee, + protocol_fee, + imbalance, + ); + + assert!(state_changes.is_some()); + + let state_changes = state_changes.unwrap(); + + assert_eq!( + state_changes.asset_in.delta_reserve, + BalanceUpdate::Increase(666666666668u128 + state_changes.fee.asset_fee) + ); + + assert_eq!( + state_changes.fee, + TradeFee { + asset_fee: 16300141146, + protocol_fee: 12786088735, + } + ) +} + #[test] fn calculate_buy_with_fees_should_work_when_correct_input_provided() { let asset_in_state = AssetReserveState { @@ -344,6 +532,7 @@ fn calculate_buy_for_hub_asset_should_work_when_correct_input_provided() { ); assert_eq!(state_changes.delta_imbalance, BalanceUpdate::Decrease(9222222222224)); + assert_eq!(state_changes.fee, TradeFee::default()); } #[test] @@ -380,6 +569,18 @@ fn calculate_buy_for_hub_asset_with_fee_should_work_when_correct_input_provided( ); assert_eq!(state_changes.delta_imbalance, BalanceUpdate::Decrease(9332954060590)); + assert_eq!( + state_changes.asset.delta_hub_reserve, + BalanceUpdate::Increase(5000000000001u128 + state_changes.fee.asset_fee) + ); + + assert_eq!( + state_changes.fee, + TradeFee { + asset_fee: 63291139240, + protocol_fee: 0, + } + ); } #[test] diff --git a/math/src/omnipool/types.rs b/math/src/omnipool/types.rs index cb881a17e..f688393c7 100644 --- a/math/src/omnipool/types.rs +++ b/math/src/omnipool/types.rs @@ -155,6 +155,13 @@ where pub delta_protocol_shares: BalanceUpdate, } +/// Information about trade fee amounts +#[derive(Default, Debug, PartialEq, Eq)] +pub struct TradeFee { + pub asset_fee: Balance, + pub protocol_fee: Balance, +} + /// Delta changes after a trade is executed #[derive(Default, Debug, PartialEq, Eq)] pub struct TradeStateChange @@ -165,6 +172,7 @@ where pub asset_out: AssetStateChange, pub delta_imbalance: BalanceUpdate, pub hdx_hub_amount: Balance, + pub fee: TradeFee, } /// Delta changes after a trade with hub asset is executed. @@ -175,6 +183,7 @@ where { pub asset: AssetStateChange, pub delta_imbalance: BalanceUpdate, + pub fee: TradeFee, } /// Delta changes after add or remove liquidity. diff --git a/pallets/circuit-breaker/Cargo.toml b/pallets/circuit-breaker/Cargo.toml index 407e1dbb5..2f29a29e4 100644 --- a/pallets/circuit-breaker/Cargo.toml +++ b/pallets/circuit-breaker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-circuit-breaker" -version = "1.1.13" +version = "1.1.14" authors = ["GalacticCouncil "] edition = "2021" license = "Apache-2.0" diff --git a/pallets/circuit-breaker/src/tests/mock.rs b/pallets/circuit-breaker/src/tests/mock.rs index d66f38270..4be5dbde2 100644 --- a/pallets/circuit-breaker/src/tests/mock.rs +++ b/pallets/circuit-breaker/src/tests/mock.rs @@ -22,7 +22,7 @@ pub use frame_support::{assert_noop, assert_ok, parameter_types}; use frame_system::EnsureRoot; use hydra_dx_math::omnipool::types::BalanceUpdate; -use orml_traits::parameter_type_with_key; +use orml_traits::{parameter_type_with_key, GetByKey}; use sp_core::H256; use sp_runtime::traits::{ConstU128, ConstU32}; use sp_runtime::DispatchResult; @@ -210,8 +210,6 @@ impl pallet_omnipool::Config for Test { type Currency = Tokens; type AuthorityOrigin = EnsureRoot; type HubAssetId = LRNAAssetId; - type ProtocolFee = ProtocolFee; - type AssetFee = AssetFee; type StableCoinAssetId = DAIAssetId; type WeightInfo = (); type HdxAssetId = HDXAssetId; @@ -228,6 +226,7 @@ impl pallet_omnipool::Config for Test { type PriceBarrier = (); type MinWithdrawalFee = MinWithdrawFee; type ExternalPriceOracle = WithdrawFeePriceOracle; + type Fee = FeeProvider; } pub struct CircuitBreakerHooks(PhantomData); @@ -627,3 +626,11 @@ impl ExternalPriceProvider for WithdrawFeePriceOracle { todo!() } } + +pub struct FeeProvider; + +impl GetByKey for FeeProvider { + fn get(_: &AssetId) -> (Permill, Permill) { + (ASSET_FEE.with(|v| *v.borrow()), PROTOCOL_FEE.with(|v| *v.borrow())) + } +} diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index 5c79de4bf..4db1fa6c3 100644 --- a/pallets/dca/Cargo.toml +++ b/pallets/dca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-dca' -version = '1.1.5' +version = '1.1.6' description = 'A pallet to manage DCA scheduling' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/dca/src/tests/mock.rs b/pallets/dca/src/tests/mock.rs index 66f73052c..19cc89972 100644 --- a/pallets/dca/src/tests/mock.rs +++ b/pallets/dca/src/tests/mock.rs @@ -29,7 +29,7 @@ use frame_support::{assert_ok, parameter_types}; use frame_system as system; use frame_system::{ensure_signed, EnsureRoot}; use hydradx_traits::{OraclePeriod, PriceOracle, Registry}; -use orml_traits::parameter_type_with_key; +use orml_traits::{parameter_type_with_key, GetByKey}; use pallet_currencies::BasicCurrencyAdapter; use primitive_types::U128; use sp_core::H256; @@ -250,8 +250,6 @@ impl pallet_omnipool::Config for Test { type PositionItemId = u32; type Currency = Currencies; type HubAssetId = LRNAAssetId; - type ProtocolFee = ProtocolFee; - type AssetFee = AssetFee; type StableCoinAssetId = DAIAssetId; type WeightInfo = (); type HdxAssetId = HDXAssetId; @@ -269,6 +267,7 @@ impl pallet_omnipool::Config for Test { type PriceBarrier = (); type MinWithdrawalFee = (); type ExternalPriceOracle = WithdrawFeePriceOracle; + type Fee = FeeProvider; } pub struct WithdrawFeePriceOracle; @@ -1001,3 +1000,11 @@ pub(super) fn saturating_sub(l: EmaPrice, r: EmaPrice) -> EmaPrice { let d = l_d.full_mul(r_d); round_to_rational((n, d), Rounding::Nearest).into() } + +pub struct FeeProvider; + +impl GetByKey for FeeProvider { + fn get(_: &AssetId) -> (Permill, Permill) { + (ASSET_FEE.with(|v| *v.borrow()), PROTOCOL_FEE.with(|v| *v.borrow())) + } +} diff --git a/pallets/dynamic-fees/Cargo.toml b/pallets/dynamic-fees/Cargo.toml index ae2f0c11e..3a13dd7ac 100644 --- a/pallets/dynamic-fees/Cargo.toml +++ b/pallets/dynamic-fees/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-dynamic-fees' -version = '1.0.0' +version = '1.0.1' description = 'A pallet to provide support for dynamic fees' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/dynamic-fees/src/lib.rs b/pallets/dynamic-fees/src/lib.rs index d1cfdcfde..d9f9e20cd 100644 --- a/pallets/dynamic-fees/src/lib.rs +++ b/pallets/dynamic-fees/src/lib.rs @@ -164,7 +164,7 @@ where let current_fee_entry = Self::current_fees(asset_id).unwrap_or(FeeEntry { asset_fee: asset_fee_params.min_fee, protocol_fee: protocol_fee_params.min_fee, - timestamp: block_number, + timestamp: T::BlockNumber::default(), }); // Update only if it has not yet been updated this block diff --git a/pallets/dynamic-fees/src/tests/fees.rs b/pallets/dynamic-fees/src/tests/fees.rs index 8a0ab1cf9..cea2ea297 100644 --- a/pallets/dynamic-fees/src/tests/fees.rs +++ b/pallets/dynamic-fees/src/tests/fees.rs @@ -275,9 +275,9 @@ fn fees_should_not_change_when_already_update_within_same_block() { } #[test] -fn fees_should_be_minimum_when_nothing_in_storage() { +fn fees_should_be_recalculated_correctly_for_last_block_change_when_nothing_in_storage() { ExtBuilder::default() - .with_oracle(SingleValueOracle::new(ONE, 2 * ONE, 50 * ONE)) + .with_oracle(SingleValueOracle::new(ONE, 1_100_000_000_000, 50 * ONE)) .with_asset_fee_params( Fee::from_percent(1), Fee::from_percent(40), @@ -295,7 +295,7 @@ fn fees_should_be_minimum_when_nothing_in_storage() { System::set_block_number(1); let (asset_fee, protocol_fee) = retrieve_fee_entry(HDX); - assert_eq!(asset_fee, Fee::from_percent(1)); + assert_eq!(asset_fee, Fee::from_float(0.012)); assert_eq!(protocol_fee, Fee::from_percent(2)); }); } diff --git a/pallets/omnipool-liquidity-mining/Cargo.toml b/pallets/omnipool-liquidity-mining/Cargo.toml index 98a27ec91..dd8512d4e 100644 --- a/pallets/omnipool-liquidity-mining/Cargo.toml +++ b/pallets/omnipool-liquidity-mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool-liquidity-mining" -version = "2.0.9" +version = "2.0.10" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/omnipool-liquidity-mining/src/tests/mock.rs b/pallets/omnipool-liquidity-mining/src/tests/mock.rs index 79f9676d5..9db82a8e5 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/mock.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/mock.rs @@ -276,8 +276,6 @@ impl pallet_omnipool::Config for Test { type Currency = Tokens; type AuthorityOrigin = EnsureRoot; type HubAssetId = LRNAAssetId; - type ProtocolFee = ProtocolFee; - type AssetFee = AssetFee; type StableCoinAssetId = DAIAssetId; type WeightInfo = (); type HdxAssetId = HDXAssetId; @@ -294,6 +292,7 @@ impl pallet_omnipool::Config for Test { type PriceBarrier = (); type MinWithdrawalFee = MinWithdrawFee; type ExternalPriceOracle = WithdrawFeePriceOracle; + type Fee = FeeProvider; } pub struct ExtBuilder { @@ -776,3 +775,11 @@ impl ExternalPriceProvider for WithdrawFeePriceOracle { todo!() } } + +pub struct FeeProvider; + +impl GetByKey for FeeProvider { + fn get(_: &AssetId) -> (Permill, Permill) { + (ASSET_FEE.with(|v| *v.borrow()), PROTOCOL_FEE.with(|v| *v.borrow())) + } +} diff --git a/pallets/omnipool/Cargo.toml b/pallets/omnipool/Cargo.toml index 41373de99..f31536277 100644 --- a/pallets/omnipool/Cargo.toml +++ b/pallets/omnipool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool" -version = "2.1.1" +version = "3.1.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index 0be1c6520..aeec139ba 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -80,7 +80,7 @@ use sp_std::prelude::*; use frame_support::traits::tokens::nonfungibles::{Create, Inspect, Mutate}; use hydra_dx_math::omnipool::types::{AssetStateChange, BalanceUpdate, I129}; use hydradx_traits::Registry; -use orml_traits::MultiCurrency; +use orml_traits::{GetByKey, MultiCurrency}; use scale_info::TypeInfo; use sp_runtime::{ArithmeticError, DispatchError, FixedPointNumber, FixedU128, Permill}; @@ -113,6 +113,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; use hydra_dx_math::ema::EmaPrice; use hydra_dx_math::omnipool::types::{BalanceUpdate, I129}; + use orml_traits::GetByKey; use sp_runtime::ArithmeticError; #[pallet::pallet] @@ -158,13 +159,8 @@ pub mod pallet { #[pallet::constant] type StableCoinAssetId: Get; - /// Protocol fee - #[pallet::constant] - type ProtocolFee: Get; - - /// Asset fee - #[pallet::constant] - type AssetFee: Get; + /// Asset and Protocol Fee for given asset + type Fee: GetByKey; /// Minimum withdrawal fee #[pallet::constant] @@ -272,6 +268,8 @@ pub mod pallet { asset_out: T::AssetId, amount_in: Balance, amount_out: Balance, + asset_fee_amount: Balance, + protocol_fee_amount: Balance, }, /// Buy trade executed. BuyExecuted { @@ -280,6 +278,8 @@ pub mod pallet { asset_out: T::AssetId, amount_in: Balance, amount_out: Balance, + asset_fee_amount: Balance, + protocol_fee_amount: Balance, }, /// LP Position was created and NFT instance minted. PositionCreated { @@ -1064,12 +1064,14 @@ pub mod pallet { let current_imbalance = >::get(); + let (asset_fee, protocol_fee) = T::Fee::get(&asset_out); + let state_changes = hydra_dx_math::omnipool::calculate_sell_state_changes( &(&asset_in_state).into(), &(&asset_out_state).into(), amount, - T::AssetFee::get(), - T::ProtocolFee::get(), + asset_fee, + protocol_fee, current_imbalance.value, ) .ok_or(ArithmeticError::Overflow)?; @@ -1167,6 +1169,8 @@ pub mod pallet { asset_out, amount_in: amount, amount_out: *state_changes.asset_out.delta_reserve, + asset_fee_amount: state_changes.fee.asset_fee, + protocol_fee_amount: state_changes.fee.protocol_fee, }); Ok(()) @@ -1240,12 +1244,13 @@ pub mod pallet { let current_imbalance = >::get(); + let (asset_fee, protocol_fee) = T::Fee::get(&asset_in); let state_changes = hydra_dx_math::omnipool::calculate_buy_state_changes( &(&asset_in_state).into(), &(&asset_out_state).into(), amount, - T::AssetFee::get(), - T::ProtocolFee::get(), + asset_fee, + protocol_fee, current_imbalance.value, ) .ok_or(ArithmeticError::Overflow)?; @@ -1348,6 +1353,8 @@ pub mod pallet { asset_out, amount_in: *state_changes.asset_in.delta_reserve, amount_out: *state_changes.asset_out.delta_reserve, + asset_fee_amount: state_changes.fee.asset_fee, + protocol_fee_amount: state_changes.fee.protocol_fee, }); Ok(()) @@ -1648,10 +1655,12 @@ impl Pallet { let current_hub_asset_liquidity = Self::get_hub_asset_balance_of_protocol_account(); + let (asset_fee, _) = T::Fee::get(&asset_out); + let state_changes = hydra_dx_math::omnipool::calculate_sell_hub_state_changes( &(&asset_state).into(), amount, - T::AssetFee::get(), + asset_fee, I129 { value: current_imbalance.value, negative: current_imbalance.negative, @@ -1706,6 +1715,8 @@ impl Pallet { asset_out, amount_in: *state_changes.asset.delta_hub_reserve, amount_out: *state_changes.asset.delta_reserve, + asset_fee_amount: state_changes.fee.asset_fee, + protocol_fee_amount: state_changes.fee.protocol_fee, }); T::OmnipoolHooks::on_hub_asset_trade(origin, info)?; @@ -1744,10 +1755,12 @@ impl Pallet { let current_hub_asset_liquidity = Self::get_hub_asset_balance_of_protocol_account(); + let (asset_fee, _) = T::Fee::get(&asset_out); + let state_changes = hydra_dx_math::omnipool::calculate_buy_for_hub_asset_state_changes( &(&asset_state).into(), amount, - T::AssetFee::get(), + asset_fee, I129 { value: current_imbalance.value, negative: current_imbalance.negative, @@ -1801,6 +1814,8 @@ impl Pallet { asset_out, amount_in: *state_changes.asset.delta_hub_reserve, amount_out: *state_changes.asset.delta_reserve, + asset_fee_amount: state_changes.fee.asset_fee, + protocol_fee_amount: state_changes.fee.protocol_fee, }); T::OmnipoolHooks::on_hub_asset_trade(origin, info)?; diff --git a/pallets/omnipool/src/router_execution.rs b/pallets/omnipool/src/router_execution.rs index ac6ff6421..f275ddc89 100644 --- a/pallets/omnipool/src/router_execution.rs +++ b/pallets/omnipool/src/router_execution.rs @@ -4,7 +4,7 @@ use frame_system::pallet_prelude::OriginFor; use hydra_dx_math::omnipool::types::I129; use hydradx_traits::router::{ExecutorError, PoolType, TradeExecution}; -use orml_traits::MultiCurrency; +use orml_traits::{GetByKey, MultiCurrency}; use sp_runtime::traits::Get; use sp_runtime::{ArithmeticError, DispatchError}; @@ -32,10 +32,12 @@ impl TradeExecution, T::AccountId, T::AssetId, Balance> let current_hub_asset_liquidity = T::Currency::free_balance(T::HubAssetId::get(), &Self::protocol_account()); + let (asset_fee, _) = T::Fee::get(&asset_out); + let state_changes = hydra_dx_math::omnipool::calculate_sell_hub_state_changes( &(&asset_out_state).into(), amount_in, - T::AssetFee::get(), + asset_fee, I129 { value: current_imbalance.value, negative: current_imbalance.negative, @@ -47,13 +49,15 @@ impl TradeExecution, T::AccountId, T::AssetId, Balance> return Ok(*state_changes.asset.delta_reserve); } + let (asset_fee, protocol_fee) = T::Fee::get(&asset_out); + let asset_in_state = Self::load_asset_state(asset_in).map_err(ExecutorError::Error)?; let state_changes = hydra_dx_math::omnipool::calculate_sell_state_changes( &(&asset_in_state).into(), &(&asset_out_state).into(), amount_in, - T::AssetFee::get(), - T::ProtocolFee::get(), + asset_fee, + protocol_fee, current_imbalance.value, ) .ok_or_else(|| ExecutorError::Error(ArithmeticError::Overflow.into()))?; @@ -81,10 +85,12 @@ impl TradeExecution, T::AccountId, T::AssetId, Balance> let current_hub_asset_liquidity = T::Currency::free_balance(T::HubAssetId::get(), &Self::protocol_account()); + let (asset_fee, _) = T::Fee::get(&asset_out); + let state_changes = hydra_dx_math::omnipool::calculate_buy_for_hub_asset_state_changes( &(&asset_out_state).into(), amount_out, - T::AssetFee::get(), + asset_fee, I129 { value: current_imbalance.value, negative: current_imbalance.negative, @@ -98,12 +104,14 @@ impl TradeExecution, T::AccountId, T::AssetId, Balance> let asset_in_state = Self::load_asset_state(asset_in).map_err(ExecutorError::Error)?; + let (asset_fee, protocol_fee) = T::Fee::get(&asset_in); + let state_changes = hydra_dx_math::omnipool::calculate_buy_state_changes( &(&asset_in_state).into(), &(&asset_out_state).into(), amount_out, - T::AssetFee::get(), - T::ProtocolFee::get(), + asset_fee, + protocol_fee, current_imbalance.value, ) .ok_or_else(|| ExecutorError::Error(ArithmeticError::Overflow.into()))?; diff --git a/pallets/omnipool/src/tests/mock.rs b/pallets/omnipool/src/tests/mock.rs index 3c03fb6df..902be3e6f 100644 --- a/pallets/omnipool/src/tests/mock.rs +++ b/pallets/omnipool/src/tests/mock.rs @@ -177,8 +177,6 @@ impl Config for Test { type Currency = Tokens; type AuthorityOrigin = EnsureRoot; type HubAssetId = LRNAAssetId; - type ProtocolFee = ProtocolFee; - type AssetFee = AssetFee; type StableCoinAssetId = DAIAssetId; type WeightInfo = (); type HdxAssetId = HDXAssetId; @@ -198,6 +196,7 @@ impl Config for Test { ); type MinWithdrawalFee = MinWithdrawFee; type ExternalPriceOracle = WithdrawFeePriceOracle; + type Fee = FeeProvider; } pub struct ExtBuilder { @@ -639,3 +638,11 @@ pub(super) fn saturating_sub(l: EmaPrice, r: EmaPrice) -> EmaPrice { let d = l_d.full_mul(r_d); round_to_rational((n, d), Rounding::Nearest) } + +pub struct FeeProvider; + +impl GetByKey for FeeProvider { + fn get(_: &AssetId) -> (Permill, Permill) { + (ASSET_FEE.with(|v| *v.borrow()), PROTOCOL_FEE.with(|v| *v.borrow())) + } +} diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index f1b05ef01..33571e44e 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -29,6 +29,7 @@ pallet-circuit-breaker = { workspace = true } pallet-omnipool-liquidity-mining = { workspace = true } pallet-dca = { workspace = true } hydra-dx-math = { workspace = true } +pallet-dynamic-fees = { workspace = true } # pallets pallet-balances = { workspace = true } @@ -242,6 +243,7 @@ std = [ "pallet-duster/std", "warehouse-liquidity-mining/std", "pallet-omnipool-liquidity-mining/std", + "pallet-dynamic-fees/std", ] try-runtime= [ "frame-try-runtime", @@ -297,4 +299,5 @@ try-runtime= [ "pallet-ema-oracle/try-runtime", "pallet-otc/try-runtime", "pallet-route-executor/try-runtime", + "pallet-dynamic-fees/try-runtime", ] diff --git a/runtime/hydradx/src/adapters.rs b/runtime/hydradx/src/adapters.rs index 3632a3945..03f45a82d 100644 --- a/runtime/hydradx/src/adapters.rs +++ b/runtime/hydradx/src/adapters.rs @@ -16,8 +16,8 @@ use hydra_dx_math::{ support::rational::{round_to_rational, Rounding}, }; use hydradx_traits::{ - liquidity_mining::PriceAdjustment, AggregatedPriceOracle, OnLiquidityChangedHandler, OnTradeHandler, OraclePeriod, - PriceOracle, + liquidity_mining::PriceAdjustment, AggregatedOracle, AggregatedPriceOracle, OnLiquidityChangedHandler, + OnTradeHandler, OraclePeriod, PriceOracle, }; use orml_xcm_support::{OnDepositFail, UnknownAsset as UnknownAssetT}; use pallet_circuit_breaker::WeightInfo; @@ -398,3 +398,42 @@ impl< Ok(asset.clone().into()) } } + +// Dynamic fees volume adapter +pub struct OracleVolume(Balance, Balance); + +impl pallet_dynamic_fees::traits::Volume for OracleVolume { + fn amount_in(&self) -> Balance { + self.0 + } + + fn amount_out(&self) -> Balance { + self.1 + } +} + +pub struct OracleAssetVolumeProvider(PhantomData<(Runtime, Lrna, Period)>); + +impl pallet_dynamic_fees::traits::VolumeProvider + for OracleAssetVolumeProvider +where + Runtime: pallet_ema_oracle::Config, + Lrna: Get, + Period: Get, +{ + type Volume = OracleVolume; + + fn asset_volume(asset_id: AssetId) -> Option { + let entry = + pallet_ema_oracle::Pallet::::get_entry(asset_id, Lrna::get(), Period::get(), OMNIPOOL_SOURCE) + .ok()?; + Some(OracleVolume(entry.volume.a_in, entry.volume.a_out)) + } + + fn asset_liquidity(asset_id: AssetId) -> Option { + let entry = + pallet_ema_oracle::Pallet::::get_entry(asset_id, Lrna::get(), Period::get(), OMNIPOOL_SOURCE) + .ok()?; + Some(entry.liquidity.a) + } +} diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index 5dad1cf2a..2e30ee3ad 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -30,12 +30,14 @@ use primitives::constants::currency::{NATIVE_EXISTENTIAL_DEPOSIT, UNITS}; use frame_support::{ parameter_types, - sp_runtime::Permill, + sp_runtime::traits::One, + sp_runtime::{FixedU128, Permill}, traits::{AsEnsureOriginWithArg, ConstU32, Contains, EnsureOrigin, NeverEnsureOrigin}, BoundedVec, PalletId, }; use frame_system::{EnsureRoot, RawOrigin}; use orml_traits::currency::MutationHooks; +use pallet_dynamic_fees::types::FeeParams; parameter_types! { pub const NativeExistentialDeposit: u128 = NATIVE_EXISTENTIAL_DEPOSIT; @@ -195,8 +197,6 @@ pub const OMNIPOOL_SOURCE: [u8; 8] = *b"omnipool"; parameter_types! { pub const LRNA: AssetId = 1; pub const StableAssetId: AssetId = 2; - pub ProtocolFee: Permill = Permill::from_rational(5u32,10000u32); - pub AssetFee: Permill = Permill::from_rational(25u32,10000u32); pub const MinTradingLimit : Balance = 1_000u128; pub const MinPoolLiquidity: Balance = 1_000_000u128; pub const MaxInRatio: Balance = 3u128; @@ -218,8 +218,6 @@ impl pallet_omnipool::Config for Runtime { type HdxAssetId = NativeAssetId; type HubAssetId = LRNA; type StableCoinAssetId = StableAssetId; - type ProtocolFee = ProtocolFee; - type AssetFee = AssetFee; type MinWithdrawalFee = MinimumWithdrawalFee; type MinimumTradingLimit = MinTradingLimit; type MinimumPoolLiquidity = MinPoolLiquidity; @@ -248,6 +246,7 @@ impl pallet_omnipool::Config for Runtime { >, ); type ExternalPriceOracle = EmaOraclePriceAdapter; + type Fee = pallet_dynamic_fees::UpdateAndRetrieveFees; } pub struct CircuitBreakerWhitelist; @@ -450,3 +449,32 @@ impl pallet_otc::Config for Runtime { type ExistentialDepositMultiplier = ExistentialDepositMultiplier; type WeightInfo = weights::otc::HydraWeight; } + +// Dynamic fees +parameter_types! { + pub AssetFeeParams: FeeParams = FeeParams{ + min_fee: Permill::from_rational(25u32,10000u32), + max_fee: Permill::from_rational(4u32,1000u32), + decay: FixedU128::from_rational(5,1000000), + amplification: FixedU128::one(), + }; + + pub ProtocolFeeParams: FeeParams = FeeParams{ + min_fee: Permill::from_rational(5u32,10000u32), + max_fee: Permill::from_rational(1u32,1000u32), + decay: FixedU128::from_rational(5,1000000), + amplification: FixedU128::one(), + }; + + pub const DynamicFeesOraclePeriod: OraclePeriod = OraclePeriod::Short; +} + +impl pallet_dynamic_fees::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type BlockNumberProvider = System; + type Fee = Permill; + type AssetId = AssetId; + type Oracle = adapters::OracleAssetVolumeProvider; + type AssetFeeParameters = AssetFeeParams; + type ProtocolFeeParameters = ProtocolFeeParams; +} diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index ff6c2f9bc..a012dedf4 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -187,6 +187,7 @@ construct_runtime!( OTC: pallet_otc = 64, CircuitBreaker: pallet_circuit_breaker = 65, Router: pallet_route_executor = 67, + DynamicFees: pallet_dynamic_fees = 68, // ORML related modules Tokens: orml_tokens = 77,