diff --git a/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs b/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs index 9dcf83f44e1..7660391d59b 100644 --- a/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs +++ b/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs @@ -276,13 +276,13 @@ mod benchmarks { )); } - let boost_id = PrewitnessedDepositIdCounter::::get(); + let prewitnessed_deposit_id = PrewitnessedDepositIdCounter::::get(); #[block] { BoostPools::::mutate(asset, FEE_TIER, |pool| { // This depends on the number of boosters who contributed to it: - pool.as_mut().unwrap().on_lost_deposit(boost_id); + pool.as_mut().unwrap().on_lost_deposit(prewitnessed_deposit_id); }); } } diff --git a/state-chain/pallets/cf-ingress-egress/src/boost_pool.rs b/state-chain/pallets/cf-ingress-egress/src/boost_pool.rs index d9c5112accf..f9d78b087ea 100644 --- a/state-chain/pallets/cf-ingress-egress/src/boost_pool.rs +++ b/state-chain/pallets/cf-ingress-egress/src/boost_pool.rs @@ -91,10 +91,10 @@ pub struct BoostPool { // Mapping from booster to the available amount they own in `available_amount` amounts: BTreeMap>, // Boosted deposits awaiting finalisation and how much of them is owed to which booster - pending_boosts: BTreeMap>>, + pending_boosts: BTreeMap>>, // Stores boosters who have indicated that they want to stop boosting along with // the pending deposits that they have to wait to be finalised - pending_withdrawals: BTreeMap>, + pending_withdrawals: BTreeMap>, } impl BoostPool @@ -122,17 +122,17 @@ where } } - fn add_funds_inner(&mut self, account_id: AccountId, added_amount: ScaledAmount) { + fn add_funds_inner(&mut self, booster_id: AccountId, added_amount: ScaledAmount) { // To keep things simple, we assume that the booster no longer wants to withdraw // if they add more funds: - self.pending_withdrawals.remove(&account_id); + self.pending_withdrawals.remove(&booster_id); - self.amounts.entry(account_id).or_default().saturating_accrue(added_amount); + self.amounts.entry(booster_id).or_default().saturating_accrue(added_amount); self.available_amount.saturating_accrue(added_amount); } - pub(crate) fn add_funds(&mut self, account_id: AccountId, added_amount: C::ChainAmount) { - self.add_funds_inner(account_id, ScaledAmount::from_chain_amount(added_amount)); + pub(crate) fn add_funds(&mut self, booster_id: AccountId, added_amount: C::ChainAmount) { + self.add_funds_inner(booster_id, ScaledAmount::from_chain_amount(added_amount)); } pub(crate) fn get_available_amount(&self) -> C::ChainAmount { @@ -141,7 +141,7 @@ where pub(crate) fn provide_funds_for_boosting( &mut self, - boost_id: BoostId, + prewitnessed_deposit_id: PrewitnessedDepositId, amount_to_boost: C::ChainAmount, ) -> Result<(C::ChainAmount, C::ChainAmount), &'static str> { let amount_to_boost = ScaledAmount::::from_chain_amount(amount_to_boost); @@ -158,7 +158,7 @@ where (provided_amount, fee) }; - self.use_funds_for_boosting(boost_id, provided_amount, fee_amount)?; + self.use_funds_for_boosting(prewitnessed_deposit_id, provided_amount, fee_amount)?; Ok(( provided_amount.saturating_add(fee_amount).into_chain_amount(), @@ -170,7 +170,7 @@ where /// among current boosters (along with the fee) upon finalisation fn use_funds_for_boosting( &mut self, - boost_id: BoostId, + prewitnessed_deposit_id: PrewitnessedDepositId, required_amount: ScaledAmount, boost_fee: ScaledAmount, ) -> Result<(), &'static str> { @@ -237,7 +237,8 @@ where // ensure that we correctly account for every single atomic unit even in presence // of rounding errors: use nanorand::{Rng, WyRand}; - let lucky_index = WyRand::new_seed(boost_id).generate_range(0..self.amounts.len()); + let lucky_index = + WyRand::new_seed(prewitnessed_deposit_id).generate_range(0..self.amounts.len()); if let Some((lucky_id, amount)) = self.amounts.iter_mut().nth(lucky_index) { amount.saturating_accrue(excess_contributed); @@ -249,7 +250,7 @@ where // For every active booster, record how much of this particular deposit they are owed, // (which is their pool share at the time of boosting): self.pending_boosts - .try_insert(boost_id, boosters_to_receive) + .try_insert(prewitnessed_deposit_id, boosters_to_receive) .map_err(|_| "Pending boost id already exists")?; Ok(()) @@ -257,9 +258,9 @@ where pub(crate) fn on_finalised_deposit( &mut self, - boost_id: BoostId, + prewitnessed_deposit_id: PrewitnessedDepositId, ) -> Vec<(AccountId, C::ChainAmount)> { - let Some(boost_contributions) = self.pending_boosts.remove(&boost_id) else { + let Some(boost_contributions) = self.pending_boosts.remove(&prewitnessed_deposit_id) else { // The deposit hadn't been boosted return vec![]; }; @@ -270,8 +271,8 @@ where // Depending on whether the booster is withdrawing, add deposits to // their free balance or back to the available boost pool: if let Some(pending_deposits) = self.pending_withdrawals.get_mut(&booster_id) { - if !pending_deposits.remove(&boost_id) { - log::warn!("Withdrawing booster contributed to boost {boost_id}, but it is not in pending withdrawals"); + if !pending_deposits.remove(&prewitnessed_deposit_id) { + log::warn!("Withdrawing booster contributed to boost {prewitnessed_deposit_id}, but it is not in pending withdrawals"); } if pending_deposits.is_empty() { @@ -288,16 +289,19 @@ where } // Returns the number of boosters affected - pub fn on_lost_deposit(&mut self, boost_id: BoostId) -> usize { - let Some(booster_contributions) = self.pending_boosts.remove(&boost_id) else { - log_or_panic!("Failed to find boost record for a lost deposit: {boost_id}"); + pub fn on_lost_deposit(&mut self, prewitnessed_deposit_id: PrewitnessedDepositId) -> usize { + let Some(booster_contributions) = self.pending_boosts.remove(&prewitnessed_deposit_id) + else { + log_or_panic!( + "Failed to find boost record for a lost deposit: {prewitnessed_deposit_id}" + ); return 0; }; for booster_id in booster_contributions.keys() { if let Some(pending_deposits) = self.pending_withdrawals.get_mut(booster_id) { - if !pending_deposits.remove(&boost_id) { - log::warn!("Withdrawing booster contributed to boost {boost_id}, but it is not in pending withdrawals"); + if !pending_deposits.remove(&prewitnessed_deposit_id) { + log::warn!("Withdrawing booster contributed to boost {prewitnessed_deposit_id}, but it is not in pending withdrawals"); } if pending_deposits.is_empty() { @@ -309,8 +313,12 @@ where booster_contributions.len() } - // Return the amount immediately available for booster - pub fn stop_boosting(&mut self, booster_id: AccountId) -> Result { + // Return the amount immediately unlocked for the booster and a list of all pending boosts that + // the booster is still a part of. + pub fn stop_boosting( + &mut self, + booster_id: AccountId, + ) -> Result<(C::ChainAmount, BTreeSet), &'static str> { let Some(booster_active_amount) = self.amounts.remove(&booster_id) else { return Err("Account not found in boost pool") }; @@ -321,18 +329,25 @@ where .pending_boosts .iter() .filter(|(_, owed_amounts)| owed_amounts.contains_key(&booster_id)) - .map(|(boost_id, _)| *boost_id) + .map(|(prewitnessed_deposit_id, _)| *prewitnessed_deposit_id) .collect(); if !pending_deposits.is_empty() { - self.pending_withdrawals.insert(booster_id, pending_deposits); + self.pending_withdrawals.insert(booster_id, pending_deposits.clone()); } - Ok(booster_active_amount.into_chain_amount()) + Ok((booster_active_amount.into_chain_amount(), pending_deposits)) } #[cfg(test)] - pub fn get_pending_boosts(&self) -> Vec { + pub fn get_pending_boosts(&self) -> Vec { self.pending_boosts.keys().copied().collect() } + #[cfg(test)] + pub fn get_available_amount_for_account( + &self, + booster_id: &AccountId, + ) -> Option { + self.amounts.get(booster_id).copied().map(|a| a.into_chain_amount()) + } } diff --git a/state-chain/pallets/cf-ingress-egress/src/boost_pool/tests.rs b/state-chain/pallets/cf-ingress-egress/src/boost_pool/tests.rs index d26950cbdaf..faeece8bb91 100644 --- a/state-chain/pallets/cf-ingress-egress/src/boost_pool/tests.rs +++ b/state-chain/pallets/cf-ingress-egress/src/boost_pool/tests.rs @@ -12,8 +12,8 @@ const BOOSTER_1: AccountId = 1; const BOOSTER_2: AccountId = 2; const BOOSTER_3: AccountId = 3; -const BOOST_1: BoostId = 1; -const BOOST_2: BoostId = 2; +const BOOST_1: PrewitnessedDepositId = 1; +const BOOST_2: PrewitnessedDepositId = 2; #[track_caller] pub fn check_pool(pool: &TestPool, amounts: impl IntoIterator) { @@ -34,7 +34,7 @@ pub fn check_pool(pool: &TestPool, amounts: impl IntoIterator)>, + boosts: impl IntoIterator)>, ) { let expected_boosts: BTreeMap<_, _> = boosts.into_iter().collect(); @@ -44,8 +44,8 @@ fn check_pending_boosts( "mismatch in pending boosts ids" ); - for (boost_id, boost_amounts) in &pool.pending_boosts { - let expected_amounts = &expected_boosts[boost_id]; + for (prewitnessed_deposit_id, boost_amounts) in &pool.pending_boosts { + let expected_amounts = &expected_boosts[prewitnessed_deposit_id]; assert_eq!( BTreeMap::from_iter(expected_amounts.iter().copied()), @@ -59,11 +59,13 @@ fn check_pending_boosts( #[track_caller] fn check_pending_withdrawals( pool: &TestPool, - withdrawals: impl IntoIterator)>, + withdrawals: impl IntoIterator)>, ) { let expected_withdrawals: BTreeMap<_, BTreeSet<_>> = withdrawals .into_iter() - .map(|(account_id, boost_ids)| (account_id, boost_ids.into_iter().collect())) + .map(|(account_id, prewitnessed_deposit_ids)| { + (account_id, prewitnessed_deposit_ids.into_iter().collect()) + }) .collect(); assert_eq!(pool.pending_withdrawals, expected_withdrawals, "mismatch in pending withdrawals"); @@ -105,14 +107,14 @@ fn withdrawing_funds() { check_pool(&pool, [(BOOSTER_1, 1000), (BOOSTER_2, 900), (BOOSTER_3, 800)]); // No pending to receive, should be able to withdraw in full - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(1000)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((1000, Default::default()))); check_pool(&pool, [(BOOSTER_2, 900), (BOOSTER_3, 800)]); check_pending_withdrawals(&pool, []); - assert_eq!(pool.stop_boosting(BOOSTER_2), Ok(900)); + assert_eq!(pool.stop_boosting(BOOSTER_2), Ok((900, Default::default()))); check_pool(&pool, [(BOOSTER_3, 800)]); - assert_eq!(pool.stop_boosting(BOOSTER_3), Ok(800)); + assert_eq!(pool.stop_boosting(BOOSTER_3), Ok((800, Default::default()))); check_pool(&pool, []); } @@ -125,7 +127,7 @@ fn withdrawing_twice_is_no_op() { pool.add_funds(BOOSTER_1, AMOUNT_1); pool.add_funds(BOOSTER_2, AMOUNT_2); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(AMOUNT_1)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((AMOUNT_1, Default::default()))); check_pool(&pool, [(BOOSTER_2, AMOUNT_2)]); @@ -170,7 +172,7 @@ fn adding_funds_during_pending_withdrawal_from_same_booster() { check_pending_boosts(&pool, [(BOOST_1, vec![(BOOSTER_1, 500), (BOOSTER_2, 1500)])]); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(500)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((500, BTreeSet::from_iter([BOOST_1])))); check_pool(&pool, [(BOOSTER_2, 1500)]); check_pending_boosts(&pool, [(BOOST_1, vec![(BOOSTER_1, 500), (BOOSTER_2, 1500)])]); @@ -197,7 +199,7 @@ fn withdrawing_funds_before_finalisation() { check_pool(&pool, [(BOOSTER_1, 500), (BOOSTER_2, 500)]); // Only some of the funds are available immediately, and some are in pending withdrawals: - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(500)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((500, BTreeSet::from_iter([BOOST_1])))); check_pool(&pool, [(BOOSTER_2, 500)]); assert_eq!(pool.on_finalised_deposit(BOOST_1), vec![(BOOSTER_1, 500)]); @@ -215,7 +217,7 @@ fn adding_funds_with_pending_withdrawals() { check_pool(&pool, [(BOOSTER_1, 500), (BOOSTER_2, 500)]); // Only some of the funds are available immediately, and some are in pending withdrawals: - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(500)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((500, BTreeSet::from_iter([BOOST_1])))); check_pool(&pool, [(BOOSTER_2, 500)]); pool.add_funds(BOOSTER_3, 1000); @@ -243,7 +245,7 @@ fn deposit_is_lost_while_withdrawing() { pool.add_funds(BOOSTER_1, 1000); pool.add_funds(BOOSTER_2, 1000); assert_eq!(pool.provide_funds_for_boosting(BOOST_1, 1000), Ok((1000, 0))); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(500)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((500, BTreeSet::from_iter([BOOST_1])))); check_pool(&pool, [(BOOSTER_2, 500)]); check_pending_boosts(&pool, [(BOOST_1, vec![(BOOSTER_1, 500), (BOOSTER_2, 500)])]); @@ -268,7 +270,7 @@ fn partially_losing_pending_withdrawals() { check_pool(&pool, [(BOOSTER_1, 250), (BOOSTER_2, 250)]); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(250)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((250, BTreeSet::from_iter([BOOST_1, BOOST_2])))); check_pending_withdrawals(&pool, [(BOOSTER_1, vec![BOOST_1, BOOST_2])]); @@ -312,7 +314,7 @@ fn booster_joins_then_funds_lost() { assert_eq!(pool.provide_funds_for_boosting(BOOST_1, 500), Ok((500, 0))); assert_eq!(pool.provide_funds_for_boosting(BOOST_2, 1000), Ok((1000, 0))); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(250)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((250, BTreeSet::from_iter([BOOST_1, BOOST_2])))); check_pool(&pool, [(BOOSTER_2, 250)]); // New booster joins while we have a pending withdrawal: @@ -340,7 +342,7 @@ fn booster_joins_between_boosts() { check_pool(&pool, [(BOOSTER_1, 755), (BOOSTER_2, 755)]); check_pending_boosts(&pool, [(BOOST_1, vec![(BOOSTER_1, 250), (BOOSTER_2, 250)])]); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(755)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((755, BTreeSet::from_iter([BOOST_1])))); check_pool(&pool, [(BOOSTER_2, 755)]); // New booster joins while we have a pending withdrawal: @@ -404,12 +406,12 @@ fn small_rewards_accumulate() { check_pool(&pool, [(BOOSTER_1, 1004), (BOOSTER_2, 50)]); // 4 more boost like that and BOOSTER 2 should have withdrawable fees: - for boost_id in 1..=4 { + for prewitnessed_deposit_id in 1..=4 { assert_eq!( - pool.provide_funds_for_boosting(boost_id, SMALL_DEPOSIT), + pool.provide_funds_for_boosting(prewitnessed_deposit_id, SMALL_DEPOSIT), Ok((SMALL_DEPOSIT, 5)) ); - assert_eq!(pool.on_finalised_deposit(boost_id), vec![]); + assert_eq!(pool.on_finalised_deposit(prewitnessed_deposit_id), vec![]); } // Note the increase in Booster 2's balance: @@ -425,7 +427,7 @@ fn use_max_available_amount() { check_pool(&pool, [(BOOSTER_1, 0)]); - assert_eq!(pool.stop_boosting(BOOSTER_1), Ok(0)); + assert_eq!(pool.stop_boosting(BOOSTER_1), Ok((0, BTreeSet::from_iter([BOOST_1])))); pool.add_funds(BOOSTER_1, 200); diff --git a/state-chain/pallets/cf-ingress-egress/src/lib.rs b/state-chain/pallets/cf-ingress-egress/src/lib.rs index f93a9c84df8..8088fb43c4c 100644 --- a/state-chain/pallets/cf-ingress-egress/src/lib.rs +++ b/state-chain/pallets/cf-ingress-egress/src/lib.rs @@ -48,13 +48,17 @@ use frame_support::{ use frame_system::pallet_prelude::*; pub use pallet::*; use sp_runtime::traits::UniqueSaturatedInto; -use sp_std::{vec, vec::Vec}; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + vec, + vec::Vec, +}; use strum_macros::EnumIter; pub use weights::WeightInfo; #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] pub enum BoostStatus { - Boosted { boost_id: BoostId, pools: Vec }, + Boosted { prewitnessed_deposit_id: PrewitnessedDepositId, pools: Vec }, NotBoosted, } @@ -68,7 +72,9 @@ pub struct PrewitnessedDeposit { } // TODO: use u16 directly so we can dynamically add/remove pools? -#[derive(Copy, Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, EnumIter)] +#[derive( + Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo, EnumIter, +)] #[repr(u16)] pub enum BoostPoolTier { FiveBps = 5, @@ -76,7 +82,15 @@ pub enum BoostPoolTier { ThirtyBps = 30, } -type BoostId = u64; +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub struct BoostPoolId { + asset: C::ChainAsset, + tier: BoostPoolTier, +} +pub struct BoostOutput { + used_pools: BTreeMap, + total_fee: C::ChainAmount, +} const SORTED_BOOST_TIERS: [BoostPoolTier; 3] = [BoostPoolTier::FiveBps, BoostPoolTier::TenBps, BoostPoolTier::ThirtyBps]; @@ -183,7 +197,7 @@ pub mod pallet { }; use frame_system::WeightInfo as SystemWeightInfo; use sp_runtime::SaturatedConversion; - use sp_std::vec::Vec; + use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; pub(crate) type ChannelRecycleQueue = Vec<(TargetChainBlockNumber, TargetChainAccount)>; @@ -259,7 +273,7 @@ pub mod pallet { LiquidityProvision { lp_account: AccountId }, CcmTransfer { principal_swap_id: Option, gas_swap_id: Option }, NoAction, - BoostersCredited, + BoostersCredited { prewitnessed_deposit_id: PrewitnessedDepositId }, } /// Tracks funds that are owned by the vault and available for egress. @@ -553,6 +567,7 @@ pub mod pallet { // a non-gas asset. ingress_fee: TargetChainAmount, action: DepositAction, + channel_id: ChannelId, }, AssetEgressStatusChanged { asset: TargetChainAsset, @@ -626,14 +641,38 @@ pub mod pallet { DepositBoosted { deposit_address: TargetChainAccount, asset: TargetChainAsset, - amount: TargetChainAmount, + amounts: BTreeMap>, deposit_details: ::DepositDetails, + prewitnessed_deposit_id: PrewitnessedDepositId, + channel_id: ChannelId, // Ingress fee in the deposit asset. i.e. *NOT* the gas asset, if the deposit asset is - // a non-gas asset. + // a non-gas asset. The ingress fee is taken *after* the boost fee. ingress_fee: TargetChainAmount, + // Total fee the user paid for their deposit to be boosted. boost_fee: TargetChainAmount, action: DepositAction, }, + BoostFundsAdded { + booster_id: T::AccountId, + boost_pool: BoostPoolId, + amount: TargetChainAmount, + }, + StoppedBoosting { + booster_id: T::AccountId, + boost_pool: BoostPoolId, + // When we stop boosting, the amount in the pool that isn't currently pending + // finalisation can be returned immediately. + unlocked_amount: TargetChainAmount, + // The ids of the boosts that are pending finalisation, such that the funds can then be + // returned to the user's free balance when the finalisation occurs. + pending_boosts: BTreeSet, + }, + InsufficientBoostLiquidity { + prewitnessed_deposit_id: PrewitnessedDepositId, + asset: TargetChainAsset, + amount_attempted: TargetChainAmount, + channel_id: ChannelId, + }, } #[derive(CloneNoBound, PartialEqNoBound, EqNoBound)] @@ -709,11 +748,12 @@ pub mod pallet { let removed_deposits = Self::clear_prewitnessed_deposits(deposit_channel.channel_id); - if let BoostStatus::Boosted { boost_id, pools } = boost_status { + if let BoostStatus::Boosted { prewitnessed_deposit_id, pools } = boost_status { for pool_tier in pools { BoostPools::::mutate(deposit_channel.asset, pool_tier, |pool| { if let Some(pool) = pool { - let affected_boosters_count = pool.on_lost_deposit(boost_id); + let affected_boosters_count = + pool.on_lost_deposit(prewitnessed_deposit_id); used_weight.saturating_accrue(T::WeightInfo::on_lost_deposit( affected_boosters_count as u32, )); @@ -995,18 +1035,24 @@ pub mod pallet { amount: TargetChainAmount, pool_tier: BoostPoolTier, ) -> DispatchResult { - let booster = T::AccountRoleRegistry::ensure_liquidity_provider(origin)?; + let booster_id = T::AccountRoleRegistry::ensure_liquidity_provider(origin)?; - T::LpBalance::try_debit_account(&booster, asset.into(), amount.into())?; + T::LpBalance::try_debit_account(&booster_id, asset.into(), amount.into())?; BoostPools::::mutate(asset, pool_tier, |pool| { let pool = pool.as_mut().ok_or_else(|| DispatchError::from("Boost pool must exist"))?; - pool.add_funds(booster, amount); + pool.add_funds(booster_id.clone(), amount); Ok::<(), DispatchError>(()) })?; + Self::deposit_event(Event::::BoostFundsAdded { + booster_id, + boost_pool: BoostPoolId { asset, tier: pool_tier }, + amount, + }); + Ok(()) } @@ -1019,15 +1065,24 @@ pub mod pallet { ) -> DispatchResult { let booster = T::AccountRoleRegistry::ensure_liquidity_provider(origin)?; - let unlocked_amount = BoostPools::::mutate(asset, pool_tier, |pool| { - let pool = - pool.as_mut().ok_or_else(|| DispatchError::from("Boost pool must exist"))?; + let (unlocked_amount, pending_boosts) = + BoostPools::::mutate(asset, pool_tier, |pool| { + let pool = pool + .as_mut() + .ok_or_else(|| DispatchError::from("Boost pool must exist"))?; - pool.stop_boosting(booster.clone()) - })?; + pool.stop_boosting(booster.clone()) + })?; T::LpBalance::try_credit_account(&booster, asset.into(), unlocked_amount.into())?; + Self::deposit_event(Event::StoppedBoosting { + booster_id: booster, + boost_pool: BoostPoolId { asset, tier: pool_tier }, + unlocked_amount, + pending_boosts, + }); + Ok(()) } } @@ -1235,19 +1290,19 @@ impl, I: 'static> Pallet { Ok(()) } - /// Returns participating pools and the total boost fee if successful + /// Returns a list of contributions from the used pools and the total boost fee. #[transactional] fn try_boosting( asset: TargetChainAsset, required_amount: TargetChainAmount, max_boost_fee_bps: BasisPoints, - boost_id: u64, - ) -> Result<(Vec, TargetChainAmount), DispatchError> { + prewitnessed_deposit_id: PrewitnessedDepositId, + ) -> Result, DispatchError> { let mut remaining_amount = required_amount; let mut total_fee_amount: TargetChainAmount = 0u32.into(); - let mut used_pools = vec![]; + let mut used_pools = BTreeMap::new(); for boost_tier in SORTED_BOOST_TIERS { if boost_tier as u16 > max_boost_fee_bps { @@ -1267,20 +1322,23 @@ impl, I: 'static> Pallet { Some(pool) => pool, }; - used_pools.push(boost_tier); - - pool.provide_funds_for_boosting(boost_id, remaining_amount).map_err(Into::into) + pool.provide_funds_for_boosting(prewitnessed_deposit_id, remaining_amount) + .map_err(Into::into) })?; + if !boosted_amount.is_zero() { + used_pools.insert(boost_tier, boosted_amount); + } + remaining_amount.saturating_reduce(boosted_amount); total_fee_amount.saturating_accrue(fee); if remaining_amount == 0u32.into() { - return Ok((used_pools, total_fee_amount)); + return Ok(BoostOutput { used_pools, total_fee: total_fee_amount }); } } - Err("insufficient boost funds".into()) + Err("Insufficient boost funds".into()) } fn add_prewitnessed_deposits( @@ -1295,10 +1353,11 @@ impl, I: 'static> Pallet { continue; } - let id = PrewitnessedDepositIdCounter::::mutate(|id| -> u64 { - *id = id.saturating_add(1); - *id - }); + let prewitnessed_deposit_id = + PrewitnessedDepositIdCounter::::mutate(|id| -> u64 { + *id = id.saturating_add(1); + *id + }); let DepositChannelDetails { deposit_channel, action, boost_fee, boost_status, .. } = DepositChannelLookup::::get(&deposit_address) @@ -1308,7 +1367,7 @@ impl, I: 'static> Pallet { PrewitnessedDeposits::::insert( channel_id, - id, + prewitnessed_deposit_id, PrewitnessedDeposit { asset, amount, @@ -1320,12 +1379,14 @@ impl, I: 'static> Pallet { // Only boost on non-zero fee and if the channel isn't already boosted: if boost_fee > 0 && !matches!(boost_status, BoostStatus::Boosted { .. }) { - match Self::try_boosting(asset, amount, boost_fee, id) { - Ok((used_pools, boost_fee_amount)) => { + match Self::try_boosting(asset, amount, boost_fee, prewitnessed_deposit_id) { + Ok(BoostOutput { used_pools, total_fee: boost_fee_amount }) => { DepositChannelLookup::::mutate(&deposit_address, |details| { if let Some(details) = details { - details.boost_status = - BoostStatus::Boosted { boost_id: id, pools: used_pools }; + details.boost_status = BoostStatus::Boosted { + prewitnessed_deposit_id, + pools: used_pools.keys().cloned().collect(), + }; } }); @@ -1351,7 +1412,9 @@ impl, I: 'static> Pallet { Self::deposit_event(Event::DepositBoosted { deposit_address: deposit_address.clone(), asset, - amount, + amounts: used_pools, + prewitnessed_deposit_id, + channel_id, deposit_details: deposit_details.clone(), ingress_fee, boost_fee: boost_fee_amount, @@ -1359,8 +1422,14 @@ impl, I: 'static> Pallet { }); }, Err(err) => { + Self::deposit_event(Event::InsufficientBoostLiquidity { + prewitnessed_deposit_id, + asset, + amount_attempted: amount, + channel_id, + }); log::debug!( - "Deposit (id: {id}) of {amount:?} {asset:?} and boost fee {boost_fee} could not be boosted: {err:?}" + "Deposit (id: {prewitnessed_deposit_id}) of {amount:?} {asset:?} and boost fee {boost_fee} could not be boosted: {err:?}" ); }, } @@ -1501,35 +1570,36 @@ impl, I: 'static> Pallet { &deposit_channel_details.deposit_channel, ); - let maybe_boost_to_process = if let BoostStatus::Boosted { boost_id, pools } = - deposit_channel_details.boost_status - { - // We are expecting a boost, but check if the amount is matching - match PrewitnessedDeposits::::get(channel_id, boost_id) { - Some(boosted_deposit) if boosted_deposit.amount == deposit_amount => { - // Deposit matches boosted deposit, process as boosted - Some((boost_id, pools)) - }, - Some(_) => { - // Boosted deposit is found but the amounts didn't match, the deposit - // should be processed as not boosted. - None - }, - None => { - log_or_panic!("Could not find deposit by boost id: {boost_id}"); - // This is unexpected since we always add a prewitnessed deposit at the - // same time as boosting it! Because we won't be able to confirm if the - // amount is correct, we fallback to processing the deposit as not boosted. - None - }, - } - } else { - // The channel is not even boosted, so we process the deposit as not boosted - None - }; + let maybe_boost_to_process = + if let BoostStatus::Boosted { prewitnessed_deposit_id, pools } = + deposit_channel_details.boost_status + { + // We are expecting a boost, but check if the amount is matching + match PrewitnessedDeposits::::get(channel_id, prewitnessed_deposit_id) { + Some(boosted_deposit) if boosted_deposit.amount == deposit_amount => { + // Deposit matches boosted deposit, process as boosted + Some((prewitnessed_deposit_id, pools)) + }, + Some(_) => { + // Boosted deposit is found but the amounts didn't match, the deposit + // should be processed as not boosted. + None + }, + None => { + log_or_panic!("Could not find deposit by prewitness deposit id: {prewitnessed_deposit_id}"); + // This is unexpected since we always add a prewitnessed deposit at the + // same time as boosting it! Because we won't be able to confirm if the + // amount is correct, we fallback to processing the deposit as not boosted. + None + }, + } + } else { + // The channel is not even boosted, so we process the deposit as not boosted + None + }; - if let Some((boost_id, used_pools)) = maybe_boost_to_process { - PrewitnessedDeposits::::remove(channel_id, boost_id); + if let Some((prewitnessed_deposit_id, used_pools)) = maybe_boost_to_process { + PrewitnessedDeposits::::remove(channel_id, prewitnessed_deposit_id); // Note that ingress fee is not payed here, as it has already been payed at the time // of boosting @@ -1541,7 +1611,7 @@ impl, I: 'static> Pallet { BoostPools::::mutate(asset, boost_tier, |maybe_pool| { if let Some(pool) = maybe_pool { for (booster_id, finalised_withdrawn_amount) in - pool.on_finalised_deposit(boost_id) + pool.on_finalised_deposit(prewitnessed_deposit_id) { if let Err(err) = T::LpBalance::try_credit_account( &booster_id, @@ -1571,7 +1641,8 @@ impl, I: 'static> Pallet { amount: deposit_amount, deposit_details, ingress_fee: 0u32.into(), - action: DepositAction::BoostersCredited, + action: DepositAction::BoostersCredited { prewitnessed_deposit_id }, + channel_id, }); } else { // If the deposit isn't boosted, we don't care which prewitness deposit we remove @@ -1617,6 +1688,7 @@ impl, I: 'static> Pallet { deposit_details, ingress_fee: fees_withheld, action: deposit_action, + channel_id, }); } } diff --git a/state-chain/pallets/cf-ingress-egress/src/tests.rs b/state-chain/pallets/cf-ingress-egress/src/tests.rs index 1154eb68fd4..f61ea2be345 100644 --- a/state-chain/pallets/cf-ingress-egress/src/tests.rs +++ b/state-chain/pallets/cf-ingress-egress/src/tests.rs @@ -900,7 +900,7 @@ fn deposits_below_minimum_are_rejected() { const LP_ACCOUNT: u64 = 0; // Flip deposit should succeed. - let (_, deposit_address) = request_address_and_deposit(LP_ACCOUNT, flip); + let (channel_id, deposit_address) = request_address_and_deposit(LP_ACCOUNT, flip); System::assert_last_event(RuntimeEvent::IngressEgress( crate::Event::::DepositFinalised { deposit_address, @@ -909,6 +909,7 @@ fn deposits_below_minimum_are_rejected() { deposit_details: Default::default(), ingress_fee: 0, action: DepositAction::LiquidityProvision { lp_account: LP_ACCOUNT }, + channel_id, }, )); }); diff --git a/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs b/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs index 4110a4837fd..c2cb634bbd3 100644 --- a/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs +++ b/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs @@ -1,15 +1,16 @@ use super::*; use cf_chains::FeeEstimationApi; -use cf_primitives::{AssetAmount, BasisPoints}; +use cf_primitives::{AssetAmount, BasisPoints, PrewitnessedDepositId}; use cf_traits::{ mocks::{ account_role_registry::MockAccountRoleRegistry, tracked_data_provider::TrackedDataProvider, }, AccountRoleRegistry, LpBalanceApi, }; +use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}; -use crate::{BoostId, BoostPoolTier, BoostPools, DepositTracker, Event}; +use crate::{BoostPoolId, BoostPoolTier, BoostPools, DepositTracker, Event}; type AccountId = u64; type DepositBalances = crate::DepositBalances; @@ -169,7 +170,7 @@ fn basic_passive_boosting() { // ==== LP sends funds to liquidity deposit address, which gets pre-witnessed ==== assert_eq!(get_lp_eth_balance(&LP_ACCOUNT), INIT_LP_BALANCE); let (channel_id, deposit_address) = request_deposit_address_eth(LP_ACCOUNT, 30); - let deposit_id = prewitness_deposit(deposit_address, ASSET, DEPOSIT_AMOUNT); + let prewitnessed_deposit_id = prewitness_deposit(deposit_address, ASSET, DEPOSIT_AMOUNT); // All of BOOSTER_AMOUNT_1 should be used: const POOL_1_FEE: AssetAmount = BOOSTER_AMOUNT_1 * BoostPoolTier::FiveBps as u128 / 10_000; // Only part of BOOSTER_AMOUNT_2 should be used: @@ -179,10 +180,18 @@ fn basic_passive_boosting() { const LP_BALANCE_AFTER_BOOST: AssetAmount = INIT_LP_BALANCE + DEPOSIT_AMOUNT - POOL_1_FEE - POOL_2_FEE - INGRESS_FEE; { + const POOL_1_CONTRIBUTION: AssetAmount = BOOSTER_AMOUNT_1 + POOL_1_FEE; + const POOL_2_CONTRIBUTION: AssetAmount = DEPOSIT_AMOUNT - POOL_1_CONTRIBUTION; + System::assert_last_event(RuntimeEvent::IngressEgress(Event::DepositBoosted { deposit_address, asset: ASSET, - amount: DEPOSIT_AMOUNT, + amounts: BTreeMap::from_iter(vec![ + (BoostPoolTier::FiveBps, POOL_1_CONTRIBUTION), + (BoostPoolTier::TenBps, POOL_2_CONTRIBUTION), + ]), + channel_id, + prewitnessed_deposit_id, deposit_details: (), ingress_fee: INGRESS_FEE, boost_fee: POOL_1_FEE + POOL_2_FEE, @@ -191,13 +200,10 @@ fn basic_passive_boosting() { assert_boosted( deposit_address, - deposit_id, + prewitnessed_deposit_id, [BoostPoolTier::FiveBps, BoostPoolTier::TenBps], ); - const POOL_1_CONTRIBUTION: AssetAmount = BOOSTER_AMOUNT_1 + POOL_1_FEE; - const POOL_2_CONTRIBUTION: AssetAmount = DEPOSIT_AMOUNT - POOL_1_CONTRIBUTION; - // Channel action is immediately executed (LP gets credited in this case): assert_eq!(get_lp_eth_balance(&LP_ACCOUNT), LP_BALANCE_AFTER_BOOST); @@ -226,10 +232,14 @@ fn basic_passive_boosting() { amount: DEPOSIT_AMOUNT, deposit_details: (), ingress_fee: 0, - action: DepositAction::BoostersCredited, + action: DepositAction::BoostersCredited { prewitnessed_deposit_id }, + channel_id, })); - assert_eq!(PrewitnessedDeposits::::get(channel_id, deposit_id), None); + assert_eq!( + PrewitnessedDeposits::::get(channel_id, prewitnessed_deposit_id), + None + ); assert_eq!( get_available_amount(ASSET, BoostPoolTier::FiveBps), @@ -370,7 +380,7 @@ fn stop_boosting() { )); let (_channel_id, deposit_address) = request_deposit_address_eth(LP_ACCOUNT, 30); - let _deposit_id = prewitness_deposit(deposit_address, eth::Asset::Eth, DEPOSIT_AMOUNT); + let deposit_id = prewitness_deposit(deposit_address, eth::Asset::Eth, DEPOSIT_AMOUNT); assert_eq!(get_lp_eth_balance(&BOOSTER_1), INIT_BOOSTER_ETH_BALANCE - BOOSTER_AMOUNT_1); @@ -388,6 +398,13 @@ fn stop_boosting() { INIT_BOOSTER_ETH_BALANCE - BOOSTER_AMOUNT_1 + AVAILABLE_BOOST_AMOUNT ); + System::assert_last_event(RuntimeEvent::IngressEgress(Event::StoppedBoosting { + booster_id: BOOSTER_1, + boost_pool: BoostPoolId { asset: eth::Asset::Eth, tier: BoostPoolTier::TenBps }, + unlocked_amount: AVAILABLE_BOOST_AMOUNT, + pending_boosts: BTreeSet::from_iter(vec![deposit_id]), + })); + // Deposit is finalised, the booster gets their remaining funds from the pool: witness_deposit(deposit_address, eth::Asset::Eth, DEPOSIT_AMOUNT); assert_eq!(get_lp_eth_balance(&BOOSTER_1), INIT_BOOSTER_ETH_BALANCE + BOOST_FEE); @@ -397,12 +414,12 @@ fn stop_boosting() { #[track_caller] fn assert_boosted( deposit_address: H160, - boost_id: BoostId, + prewitnessed_deposit_id: PrewitnessedDepositId, pools: impl IntoIterator, ) { assert_eq!( DepositChannelLookup::::get(deposit_address).unwrap().boost_status, - BoostStatus::Boosted { boost_id, pools: Vec::from_iter(pools.into_iter()) } + BoostStatus::Boosted { prewitnessed_deposit_id, pools: Vec::from_iter(pools.into_iter()) } ); } @@ -651,8 +668,9 @@ fn insufficient_funds_for_boost() { BoostPoolTier::FiveBps )); - let (_channel_id, deposit_address) = request_deposit_address_eth(LP_ACCOUNT, 10); - let _deposit_id = prewitness_deposit(deposit_address, eth::Asset::Eth, DEPOSIT_AMOUNT); + let (channel_id, deposit_address) = request_deposit_address_eth(LP_ACCOUNT, 10); + System::reset_events(); + let deposit_id = prewitness_deposit(deposit_address, eth::Asset::Eth, DEPOSIT_AMOUNT); // The deposit is pre-witnessed, but no channel action took place: { @@ -660,6 +678,13 @@ fn insufficient_funds_for_boost() { assert_eq!(get_lp_eth_balance(&LP_ACCOUNT), INIT_LP_BALANCE); } + System::assert_last_event(RuntimeEvent::IngressEgress(Event::InsufficientBoostLiquidity { + prewitnessed_deposit_id: deposit_id, + asset: eth::Asset::Eth, + amount_attempted: DEPOSIT_AMOUNT, + channel_id, + })); + // When the deposit is finalised, it is processed as normal: { witness_deposit(deposit_address, eth::Asset::Eth, DEPOSIT_AMOUNT); @@ -691,7 +716,10 @@ fn lost_funds_are_acknowledged_by_boost_pool() { assert_eq!( DepositChannelLookup::::get(deposit_address).unwrap().boost_status, - BoostStatus::Boosted { boost_id: deposit_id, pools: vec![BoostPoolTier::FiveBps] } + BoostStatus::Boosted { + prewitnessed_deposit_id: deposit_id, + pools: vec![BoostPoolTier::FiveBps] + } ); assert_eq!(get_lp_eth_balance(&LP_ACCOUNT), DEPOSIT_AMOUNT - BOOST_FEE - INGRESS_FEE); @@ -711,12 +739,51 @@ fn lost_funds_are_acknowledged_by_boost_pool() { IngressEgress::on_idle(recycle_block, Weight::MAX); assert_eq!(PrewitnessedDeposits::::get(channel_id, deposit_id), None); - assert_eq!( - BoostPools::::get(eth::Asset::Eth, BoostPoolTier::FiveBps) - .unwrap() - .get_pending_boosts(), - Vec::::new() - ); + assert!(BoostPools::::get(eth::Asset::Eth, BoostPoolTier::FiveBps) + .unwrap() + .get_pending_boosts() + .is_empty()); } }); } + +#[test] +fn test_add_boost_funds() { + new_test_ext().execute_with(|| { + const BOOST_FUNDS: AssetAmount = 500_000_000; + + setup(); + + // Should have all funds in the lp account and non in the pool yet. + assert_eq!( + BoostPools::::get(eth::Asset::Eth, BoostPoolTier::FiveBps) + .unwrap() + .get_available_amount_for_account(&BOOSTER_1), + None + ); + assert_eq!(get_lp_eth_balance(&BOOSTER_1), INIT_BOOSTER_ETH_BALANCE); + + // Add some of the LP funds to the boost pool + assert_ok!(IngressEgress::add_boost_funds( + RuntimeOrigin::signed(BOOSTER_1), + eth::Asset::Eth, + BOOST_FUNDS, + BoostPoolTier::FiveBps + )); + + // Should see some of the funds in the pool now and some funds missing from the LP account + assert_eq!( + BoostPools::::get(eth::Asset::Eth, BoostPoolTier::FiveBps) + .unwrap() + .get_available_amount_for_account(&BOOSTER_1), + Some(BOOST_FUNDS) + ); + assert_eq!(get_lp_eth_balance(&BOOSTER_1), INIT_BOOSTER_ETH_BALANCE - BOOST_FUNDS); + + System::assert_last_event(RuntimeEvent::IngressEgress(Event::BoostFundsAdded { + booster_id: BOOSTER_1, + boost_pool: BoostPoolId { asset: eth::Asset::Eth, tier: BoostPoolTier::FiveBps }, + amount: BOOST_FUNDS, + })); + }); +}