Skip to content

Commit

Permalink
feature/PRO-1038/pool-fee-rpc (#4459)
Browse files Browse the repository at this point in the history
* feat: ✨ tracking historical earned LP fees
* feat: ✨ extended RPC to return historical fees and total_swap_inputs
  • Loading branch information
Janislav authored Feb 21, 2024
1 parent 6812aed commit 38f4443
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 37 deletions.
16 changes: 16 additions & 0 deletions state-chain/amm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,22 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
self.range_orders.fee_hundredth_pips
}

pub fn range_order_total_fees_earned(&self) -> PoolPairsMap<Amount> {
self.range_orders.total_fees_earned
}

pub fn limit_order_total_fees_earned(&self) -> PoolPairsMap<Amount> {
self.limit_orders.total_fees_earned
}

pub fn range_order_swap_inputs(&self) -> PoolPairsMap<Amount> {
self.range_orders.total_swap_inputs
}

pub fn limit_order_swap_inputs(&self) -> PoolPairsMap<Amount> {
self.limit_orders.total_swap_inputs
}

pub fn limit_order_liquidity(&self, order: Side) -> Vec<(Tick, Amount)> {
match order {
Side::Sell => self.limit_orders.liquidity::<QuoteToBase>(),
Expand Down
4 changes: 2 additions & 2 deletions state-chain/amm/src/limit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,9 @@ pub(super) struct PoolState<LiquidityProvider: Ord> {
/// liquidity.
positions: PoolPairsMap<BTreeMap<(SqrtPriceQ64F96, LiquidityProvider), Position>>,
/// Total fees earned over all time
total_fees_earned: PoolPairsMap<Amount>,
pub(super) total_fees_earned: PoolPairsMap<Amount>,
/// Total of all swap inputs over all time (not including fees)
total_swap_inputs: PoolPairsMap<Amount>,
pub(super) total_swap_inputs: PoolPairsMap<Amount>,
/// Total of all swap outputs over all time
total_swap_outputs: PoolPairsMap<Amount>,
}
Expand Down
4 changes: 2 additions & 2 deletions state-chain/amm/src/range_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ pub struct PoolState<LiquidityProvider: Ord> {
liquidity_map: BTreeMap<Tick, TickDelta>,
positions: BTreeMap<(LiquidityProvider, Tick, Tick), Position>,
/// Total fees earned over all time
total_fees_earned: PoolPairsMap<Amount>,
pub(super) total_fees_earned: PoolPairsMap<Amount>,
/// Total of all swap inputs over all time (not including fees)
total_swap_inputs: PoolPairsMap<Amount>,
pub(super) total_swap_inputs: PoolPairsMap<Amount>,
/// Total of all swap outputs over all time
total_swap_outputs: PoolPairsMap<Amount>,
}
Expand Down
19 changes: 5 additions & 14 deletions state-chain/cf-integration-tests/src/swapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use pallet_cf_broadcast::{
ThresholdSignatureData,
};
use pallet_cf_ingress_egress::{DepositWitness, FailedForeignChainCall};
use pallet_cf_lp::HistoricalEarnedFees;
use pallet_cf_pools::{OrderId, RangeOrderSize};
use pallet_cf_swapping::{CcmIdCounter, SWAP_DELAY_BLOCKS};
use sp_core::U256;
Expand Down Expand Up @@ -193,14 +194,6 @@ fn setup_pool_and_accounts(assets: Vec<Asset>) {
}
}

fn get_asset_balance(who: &AccountId, asset: Asset) -> u128 {
LiquidityProvider::asset_balances(who)
.iter()
.filter(|asset_balance| asset_balance.0 == asset)
.map(|asset_balance| asset_balance.1)
.sum()
}

#[test]
fn basic_pool_setup_provision_and_swap() {
super::genesis::with_test_defaults()
Expand All @@ -210,22 +203,21 @@ fn basic_pool_setup_provision_and_swap() {
])
.build()
.execute_with(|| {
new_pool(Asset::Eth, 0u32, price_at_tick(0).unwrap());
new_pool(Asset::Flip, 0u32, price_at_tick(0).unwrap());
new_pool(Asset::Eth, 0, price_at_tick(0).unwrap());
new_pool(Asset::Flip, 0, price_at_tick(0).unwrap());
register_refund_addresses(&DORIS);

credit_account(&DORIS, Asset::Eth, 1_000_000);
credit_account(&DORIS, Asset::Flip, 1_000_000);
credit_account(&DORIS, Asset::Usdc, 1_000_000);
assert!(!HistoricalEarnedFees::<Runtime>::contains_key(&DORIS));

set_limit_order(&DORIS, Asset::Eth, Asset::Usdc, 0, Some(0), 500_000);
set_range_order(&DORIS, Asset::Eth, Asset::Usdc, 0, Some(-10..10), 1_000_000);

set_limit_order(&DORIS, Asset::Flip, Asset::Usdc, 0, Some(0), 500_000);
set_range_order(&DORIS, Asset::Flip, Asset::Usdc, 0, Some(-10..10), 1_000_000);

let usdc_balance_before = get_asset_balance(&DORIS, Asset::Usdc);

assert_ok!(Swapping::request_swap_deposit_address(
RuntimeOrigin::signed(ZION.clone()),
Asset::Eth,
Expand Down Expand Up @@ -310,8 +302,7 @@ fn basic_pool_setup_provision_and_swap() {
) if egress_ids.contains(&egress_id) => ()
);

let usdc_balance_after = get_asset_balance(&DORIS, Asset::Usdc);
assert!(usdc_balance_after > usdc_balance_before, "Fees should be collected");
assert!(HistoricalEarnedFees::<Runtime>::contains_key(&DORIS));
});
}

Expand Down
16 changes: 16 additions & 0 deletions state-chain/custom-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub struct AssetWithAmount {
pub amount: AssetAmount,
}

#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "snake_case")]
pub enum RpcAccountInfo {
Expand All @@ -64,6 +65,7 @@ pub enum RpcAccountInfo {
balances: any::AssetMap<NumberOrHex>,
refund_addresses: HashMap<ForeignChain, Option<ForeignChainAddressHumanreadable>>,
flip_balance: NumberOrHex,
earned_fees: any::AssetMap<AssetAmount>,
},
Validator {
flip_balance: NumberOrHex,
Expand Down Expand Up @@ -103,6 +105,7 @@ impl RpcAccountInfo {
.into_iter()
.map(|(chain, address)| (chain, address.map(|a| a.to_humanreadable(network))))
.collect(),
earned_fees: info.earned_fees,
}
}

Expand Down Expand Up @@ -1331,6 +1334,15 @@ mod test {
(Asset::Usdc, 0),
(Asset::Dot, 0),
],
earned_fees: any::AssetMap {
eth: eth::AssetMap {
eth: 0u32.into(),
flip: u64::MAX.into(),
usdc: (u64::MAX / 2 - 1).into(),
},
btc: btc::AssetMap { btc: 0u32.into() },
dot: dot::AssetMap { dot: 0u32.into() },
},
},
cf_primitives::NetworkEnvironment::Mainnet,
0,
Expand Down Expand Up @@ -1439,6 +1451,10 @@ mod test {
PoolInfo {
limit_order_fee_hundredth_pips: 0,
range_order_fee_hundredth_pips: 100,
range_order_total_fees_earned: Default::default(),
limit_order_total_fees_earned: Default::default(),
range_total_swap_inputs: Default::default(),
limit_total_swap_inputs: Default::default(),
}
.into(),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: state-chain/custom-rpc/src/lib.rs
assertion_line: 1448
assertion_line: 1466
expression: "serde_json::to_value(env).unwrap()"
---
{"funding":{"minimum_funding_amount":0,"redemption_tax":0},"ingress_egress":{"channel_opening_fees":{"Bitcoin":0,"Ethereum":1000,"Polkadot":1000},"egress_dust_limits":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffffffffffffffffffff","USDC":"0x7ffffffffffffffe"},"Polkadot":{"DOT":0}},"egress_fees":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffffffffffffffffffff","USDC":null},"Polkadot":{"DOT":"0x7ffffffffffffffe"}},"ingress_fees":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffffffffffffffffffff","USDC":null},"Polkadot":{"DOT":"0x7ffffffffffffffe"}},"minimum_deposit_amounts":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffff","USDC":"0x7ffffffffffffffe"},"Polkadot":{"DOT":0}},"witness_safety_margins":{"Bitcoin":3,"Ethereum":3,"Polkadot":null}},"pools":{"fees":{"Ethereum":{"FLIP":{"limit_order_fee_hundredth_pips":0,"quote_asset":{"asset":"USDC","chain":"Ethereum"},"range_order_fee_hundredth_pips":100}}}},"swapping":{"maximum_swap_amounts":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":null,"USDC":"0x7ffffffffffffffe"},"Polkadot":{"DOT":null}},"network_fee_hundredth_pips":1000000}}
{"funding":{"minimum_funding_amount":0,"redemption_tax":0},"ingress_egress":{"channel_opening_fees":{"Bitcoin":0,"Ethereum":1000,"Polkadot":1000},"egress_dust_limits":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffffffffffffffffffff","USDC":"0x7ffffffffffffffe"},"Polkadot":{"DOT":0}},"egress_fees":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffffffffffffffffffff","USDC":null},"Polkadot":{"DOT":"0x7ffffffffffffffe"}},"ingress_fees":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffffffffffffffffffff","USDC":null},"Polkadot":{"DOT":"0x7ffffffffffffffe"}},"minimum_deposit_amounts":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":"0xffffffffffffffff","USDC":"0x7ffffffffffffffe"},"Polkadot":{"DOT":0}},"witness_safety_margins":{"Bitcoin":3,"Ethereum":3,"Polkadot":null}},"pools":{"fees":{"Ethereum":{"FLIP":{"limit_order_fee_hundredth_pips":0,"limit_order_total_fees_earned":{"base":"0x0","quote":"0x0"},"limit_total_swap_inputs":{"base":"0x0","quote":"0x0"},"quote_asset":{"asset":"USDC","chain":"Ethereum"},"range_order_fee_hundredth_pips":100,"range_order_total_fees_earned":{"base":"0x0","quote":"0x0"},"range_total_swap_inputs":{"base":"0x0","quote":"0x0"}}}}},"swapping":{"maximum_swap_amounts":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":null,"USDC":"0x7ffffffffffffffe"},"Polkadot":{"DOT":null}},"network_fee_hundredth_pips":1000000}}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: state-chain/custom-rpc/src/lib.rs
assertion_line: 1344
assertion_line: 1352
expression: "serde_json::to_value(lp).unwrap()"
---
{"balances":{"Bitcoin":{"BTC":"0x0"},"Ethereum":{"ETH":"0xffffffffffffffffffffffffffffffff","FLIP":"0x7fffffffffffffffffffffffffffffff","USDC":"0x0"},"Polkadot":{"DOT":"0x0"}},"flip_balance":"0x0","refund_addresses":{"Bitcoin":null,"Ethereum":"0x0101010101010101010101010101010101010101","Polkadot":"111111111111111111111111111111111HC1"},"role":"liquidity_provider"}
{"balances":{"Bitcoin":{"BTC":"0x0"},"Ethereum":{"ETH":"0xffffffffffffffffffffffffffffffff","FLIP":"0x7fffffffffffffffffffffffffffffff","USDC":"0x0"},"Polkadot":{"DOT":"0x0"}},"earned_fees":{"Bitcoin":{"BTC":0},"Ethereum":{"ETH":0,"FLIP":18446744073709551615,"USDC":9223372036854775806},"Polkadot":{"DOT":0}},"flip_balance":"0x0","refund_addresses":{"Bitcoin":null,"Ethereum":"0x0101010101010101010101010101010101010101","Polkadot":"111111111111111111111111111111111HC1"},"role":"liquidity_provider"}
12 changes: 12 additions & 0 deletions state-chain/pallets/cf-lp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use cf_traits::{

use sp_std::vec;

use cf_chains::assets::any::AssetMap;
use frame_support::{pallet_prelude::*, sp_runtime::DispatchResult};
use frame_system::pallet_prelude::*;
pub use pallet::*;
Expand Down Expand Up @@ -145,6 +146,11 @@ pub mod pallet {
pub type FreeBalances<T: Config> =
StorageDoubleMap<_, Twox64Concat, T::AccountId, Identity, Asset, AssetAmount>;

#[pallet::storage]
/// Historical earned fees for an account. Map: AccountId => AssetAmount
pub type HistoricalEarnedFees<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, AssetMap<AssetAmount>, ValueQuery>;

/// Stores the registered energency withdrawal address for an Account
#[pallet::storage]
pub type LiquidityRefundAddress<T: Config> = StorageDoubleMap<
Expand Down Expand Up @@ -381,6 +387,12 @@ impl<T: Config> LpBalanceApi for Pallet<T> {
Ok(())
}

fn record_fees(account_id: &Self::AccountId, amount: AssetAmount, asset: Asset) {
HistoricalEarnedFees::<T>::mutate(account_id, |fees| {
fees[asset] = fees[asset].saturating_add(amount);
});
}

fn asset_balances(who: &Self::AccountId) -> Vec<(Asset, AssetAmount)> {
let mut balances: Vec<(Asset, AssetAmount)> = vec![];
T::PoolApi::sweep(who).unwrap();
Expand Down
14 changes: 7 additions & 7 deletions state-chain/pallets/cf-pools/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,13 @@ mod benchmarks {
);
}

assert_eq!(
Pallet::<T>::pool_info(Asset::Eth, STABLE_ASSET),
Ok(PoolInfo {
limit_order_fee_hundredth_pips: fee,
range_order_fee_hundredth_pips: fee,
})
);
match Pallet::<T>::pool_info(Asset::Eth, STABLE_ASSET) {
Ok(pool_info) => {
assert_eq!(pool_info.limit_order_fee_hundredth_pips, fee);
assert_eq!(pool_info.range_order_fee_hundredth_pips, fee);
},
Err(_) => panic!("Pool not found"),
}
}

#[benchmark]
Expand Down
21 changes: 16 additions & 5 deletions state-chain/pallets/cf-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,14 @@ pub struct PoolInfo {
/// The fee taken, when range orders are used, from swap inputs that contributes to liquidity
/// provider earnings
pub range_order_fee_hundredth_pips: u32,
/// The total fees earned in this pool by range orders.
pub range_order_total_fees_earned: PoolPairsMap<Amount>,
/// The total fees earned in this pool by limit orders.
pub limit_order_total_fees_earned: PoolPairsMap<Amount>,
/// The total amount of assets that have been bought by range orders in this pool.
pub range_total_swap_inputs: PoolPairsMap<Amount>,
/// The total amount of assets that have been bought by limit orders in this pool.
pub limit_total_swap_inputs: PoolPairsMap<Amount>,
}

#[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -1387,6 +1395,7 @@ impl<T: Config> Pallet<T> {
asset_pair.assets().zip(collected.fees).try_map(|(asset, collected_fees)| {
AssetAmount::try_from(collected_fees).map_err(Into::into).and_then(
|collected_fees| {
T::LpBalance::record_fees(lp, collected_fees, asset);
T::LpBalance::try_credit_account(lp, asset, collected_fees)
.map(|()| collected_fees)
},
Expand Down Expand Up @@ -1667,6 +1676,10 @@ impl<T: Config> Pallet<T> {
Ok(PoolInfo {
limit_order_fee_hundredth_pips: pool.pool_state.limit_order_fee(),
range_order_fee_hundredth_pips: pool.pool_state.range_order_fee(),
range_order_total_fees_earned: pool.pool_state.range_order_total_fees_earned(),
limit_order_total_fees_earned: pool.pool_state.limit_order_total_fees_earned(),
range_total_swap_inputs: pool.pool_state.range_order_swap_inputs(),
limit_total_swap_inputs: pool.pool_state.limit_order_swap_inputs(),
})
}

Expand Down Expand Up @@ -1816,11 +1829,9 @@ impl<T: Config> Pallet<T> {
amount_change: IncreaseOrDecrease<AssetAmount>,
) -> DispatchResult {
let collected_fees: AssetAmount = collected.fees.try_into()?;
T::LpBalance::try_credit_account(
lp,
asset_pair.assets()[!order.to_sold_pair()],
collected_fees,
)?;
let asset = asset_pair.assets()[!order.to_sold_pair()];
T::LpBalance::record_fees(lp, collected_fees, asset);
T::LpBalance::try_credit_account(lp, asset, collected_fees)?;

let bought_amount: AssetAmount = collected.bought_amount.try_into()?;
T::LpBalance::try_credit_account(
Expand Down
14 changes: 14 additions & 0 deletions state-chain/pallets/cf-pools/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
DispatchResult, Permill,
};
use sp_std::collections::btree_map::BTreeMap;

type AccountId = u64;

Expand Down Expand Up @@ -71,6 +72,7 @@ parameter_types! {
pub static AliceDebitedUsdc: AssetAmount = Default::default();
pub static BobDebitedEth: AssetAmount = Default::default();
pub static BobDebitedUsdc: AssetAmount = Default::default();
pub static RecordedFees: BTreeMap<AccountId, (Asset, AssetAmount)> = BTreeMap::new();
}
pub struct MockBalance;
impl LpBalanceApi for MockBalance {
Expand Down Expand Up @@ -121,11 +123,23 @@ impl LpBalanceApi for MockBalance {
Ok(())
}

fn record_fees(who: &Self::AccountId, amount: AssetAmount, asset: Asset) {
RecordedFees::mutate(|recorded_fees| {
recorded_fees.insert(*who, (asset, amount));
});
}

fn asset_balances(_who: &Self::AccountId) -> Vec<(Asset, AssetAmount)> {
unreachable!()
}
}

impl MockBalance {
pub fn assert_fees_recorded(who: &AccountId) {
assert!(RecordedFees::get().contains_key(who), "Fees not recorded for {:?}", who);
}
}

impl_mock_runtime_safe_mode!(pools: PalletSafeMode);
impl pallet_cf_pools::Config for Test {
type RuntimeEvent = RuntimeEvent;
Expand Down
Loading

0 comments on commit 38f4443

Please sign in to comment.