diff --git a/Cargo.lock b/Cargo.lock index 82dcef669..ee0d79889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "426.0.0" +version = "427.0.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "4.10.0" +version = "4.11.0" dependencies = [ "frame-support", "frame-system", @@ -9682,7 +9682,7 @@ dependencies = [ [[package]] name = "pallet-fee-processor" -version = "1.0.0" +version = "1.1.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -13893,7 +13893,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.99.0" +version = "1.100.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 5f6fa0504..932e70b20 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.99.0" +version = "1.100.0" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/integration-tests/src/fee_processor.rs b/integration-tests/src/fee_processor.rs index 35d228e8b..267c9e267 100644 --- a/integration-tests/src/fee_processor.rs +++ b/integration-tests/src/fee_processor.rs @@ -1793,3 +1793,72 @@ fn hdx_referral_slice_is_raw_hdx_and_immediately_claimable() { ); }); } + +// --------------------------------------------------------------------------- +// hold_until_ed: the gigapot eventually receives every buffered HDX fee +// --------------------------------------------------------------------------- + +#[test] +fn gigapot_eventually_receives_all_buffered_hdx_fees_when_starting_below_ed() { + TestNet::reset(); + + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_24(); + + let giga = gigahdx_pot(); + let ed = primitives::constants::currency::NATIVE_EXISTENTIAL_DEPOSIT; + let held = || pallet_fee_processor::HeldFees::::get(&giga); + let delivered = || Currencies::free_balance(HDX, &giga); + + // Production cannot seed the gigapot; drain the test-genesis seed so it starts + // below ED and its 15% HDX-path slice must be buffered, not delivered. + let seeded = delivered(); + assert_ok!(Currencies::update_balance( + RawOrigin::Root.into(), + giga.clone(), + HDX, + -(seeded as i128), + )); + assert_eq!(delivered(), 0, "gigapot starts below ED"); + + // Several small DAI->HDX trades. Each giga slice is far below ED, so it accrues into + // the buffer (physically held in the seeded fee-processor pot) instead of being + // delivered to the gigapot. + for _ in 0..4 { + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + DAI, + HDX, + 10 * UNITS, + 0 + )); + assert_eq!(delivered(), 0, "below ED: buffered, nothing delivered to the gigapot"); + go_to_block(System::block_number() + 1); + } + + // The four sub-ED slices are all buffered (none delivered), and the buffer is below ED. + let buffered = held(); + assert_eq!(buffered, 2_371_450, "four sub-ED slices accrued into the buffer"); + assert!(buffered < ed, "buffer is still below ED"); + assert_eq!(delivered(), 0, "nothing delivered before the buffer crosses ED"); + + // One large trade makes buffer + new slice >= ED, flushing the whole buffer at once. + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + DAI, + HDX, + 50_000_000 * UNITS, + 0 + )); + + // The buffer drains fully into the gigapot: HeldFees back to 0 and the gigapot now holds + // every accrued giga fee — the four previously buffered slices plus the flushing slice. + assert_eq!(held(), 0, "buffer fully flushed once the gigapot crosses ED"); + assert_eq!( + delivered(), + 3_140_987_039_253, + "gigapot receives all buffered fees plus the flushing slice" + ); + assert!(delivered() > buffered, "delivery includes the previously buffered fees"); + }); +} diff --git a/integration-tests/src/gigahdx.rs b/integration-tests/src/gigahdx.rs index fd35217c0..4a9c7b0d0 100644 --- a/integration-tests/src/gigahdx.rs +++ b/integration-tests/src/gigahdx.rs @@ -2404,6 +2404,62 @@ fn crash_st_hdx_price(oracle: EvmAddress, st_hdx_evm: EvmAddress) { set_oracle_price_source(oracle, st_hdx_evm, mock); } +/// Attempt `Pool.borrow(asset, amount)` as `user` without asserting success, +/// returning the raw EVM exit reason so the caller can assert on a revert. +fn try_aave_borrow(pool: EvmAddress, user: EvmAddress, asset: EvmAddress, amount: Balance) -> fp_evm::ExitReason { + let data = EvmDataWriter::new_with_selector(liquidation_worker_support::Function::Borrow) + .write(asset) + .write(amount) + .write(2u32) // variable interest rate mode + .write(0u32) // referral code + .write(user) // onBehalfOf + .build(); + Executor::::call(CallContext::new_call(pool, user), data, U256::zero(), 50_000_000).exit_reason +} + +#[test] +fn aave_borrow_should_revert_when_asset_is_sthdx() { + // stHDX must be non-borrowable on the GIGAHDX AAVE pool (zero borrow cap / + // IRM returning 0). If it ever became borrowable, a drifting liquidityIndex + // would break the `aToken : stHDX = 1 : 1` invariant and leak unlocked + // aTokens past the lock-manager (see the stHDX invariants in `assets.rs`). + // Enforcement lives in the AAVE reserve config, not in runtime code, so this + // pins the deploy/snapshot setup against silent regression. + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + use crate::liquidation::get_user_account_data; + + let (alice, _bob, alice_evm, pool, _oracle, _hollar) = liquidation_test_setup(); + let st_hdx_evm = HydraErc20Mapping::asset_address(ST_HDX); + + // Large stake → ample GIGAHDX collateral, so a stHDX borrow would clear + // the collateral check if the asset were borrowable. This makes the + // revert attributable to the disabled reserve, not thin collateral. + assert_ok!(GigaHdx::giga_stake( + RuntimeOrigin::signed(alice.clone()), + 10_000 * UNITS + )); + + let data = get_user_account_data(pool, alice_evm).unwrap(); + assert!( + data.available_borrows_base > U256::zero(), + "precondition: Alice must have borrowing power so the revert is attributable to stHDX being non-borrowable", + ); + + let st_hdx_before = Currencies::free_balance(ST_HDX, &alice); + + let exit = try_aave_borrow(pool, alice_evm, st_hdx_evm, UNITS); + assert!( + matches!(exit, fp_evm::ExitReason::Revert(_)), + "borrowing stHDX must revert (reserve must be non-borrowable); got {:?}", + exit, + ); + + // No stHDX may reach the user. + assert_eq!(Currencies::free_balance(ST_HDX, &alice), st_hdx_before); + }); +} + /// Executes the same flow as `pallet_liquidation::liquidate_gigahdx` step by /// step from the test, so we exercise the real building blocks (AAVE pool, /// LockableAToken precompile, Seize trait, lock refresh) end-to-end against diff --git a/pallets/fee-processor/Cargo.toml b/pallets/fee-processor/Cargo.toml index 6cc620f51..836464186 100644 --- a/pallets/fee-processor/Cargo.toml +++ b/pallets/fee-processor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-fee-processor" -version = "1.0.0" +version = "1.1.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache-2.0" diff --git a/pallets/fee-processor/src/lib.rs b/pallets/fee-processor/src/lib.rs index e6a8f08a1..820cfe940 100644 --- a/pallets/fee-processor/src/lib.rs +++ b/pallets/fee-processor/src/lib.rs @@ -18,7 +18,7 @@ pub mod pallet { use frame_support::traits::tokens::Preservation; use frame_support::PalletId; use frame_system::pallet_prelude::*; - use hydradx_traits::fee_processor::{Convert, FeeReceiver}; + use hydradx_traits::fee_processor::{Convert, FeeDestination, FeeReceiver}; use sp_runtime::helpers_128bit::multiply_by_rational_with_rounding; use sp_runtime::traits::AccountIdConversion; use sp_runtime::{Permill, Rounding, Saturating}; @@ -71,6 +71,14 @@ pub mod pallet { #[pallet::getter(fn pending_conversions)] pub type PendingConversions = CountedStorageMap<_, Blake2_128Concat, T::AssetId, (), OptionQuery>; + /// HDX held in the pot for a `hold_until_ed` receiver whose account is still + /// below the existential deposit. The HDX physically lives in + /// `pot_account_id()`; this map only earmarks how much of it belongs to each + /// destination. Flushed to the destination once `balance + held + slice ≥ ED`. + #[pallet::storage] + #[pallet::getter(fn held_fees)] + pub type HeldFees = StorageMap<_, Blake2_128Concat, T::AccountId, Balance, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -277,35 +285,65 @@ pub mod pallet { } /// Sum of percentages of the HDX-target (non-raw) receivers. - fn convert_percentage(destinations: &[(T::AccountId, Permill, bool)]) -> Permill { + fn convert_percentage(destinations: &[FeeDestination]) -> Permill { destinations .iter() - .filter(|(_, _, accepts_raw)| !accepts_raw) - .fold(Permill::zero(), |acc, (_, pct, _)| acc.saturating_add(*pct)) + .filter(|d| !d.accepts_raw) + .fold(Permill::zero(), |acc, d| acc.saturating_add(d.percentage)) } /// Distribute `total` HDX among the HDX-target `destinations` proportionally to /// each destination's percentage relative to `total_pct`. Raw-asset receivers are /// skipped — they were already paid in the original asset. + /// + /// `total` is already sitting in `source` (the pot). For a `hold_until_ed` + /// destination whose account would still be below ED after receiving its + /// slice, the slice is left in the pot and tracked in `HeldFees` instead of + /// transferred — avoiding a `Token::BelowMinimum` revert. The buffer is + /// flushed (held + new slice) the moment `balance + held + slice ≥ ED`. fn distribute_proportionally( source: &T::AccountId, total: Balance, - destinations: Vec<(T::AccountId, Permill, bool)>, + destinations: Vec>, total_pct: Permill, ) -> DispatchResult { if total == 0 || total_pct.is_zero() { return Ok(()); } + let hdx = T::HdxAssetId::get(); + let ed = T::Currency::minimum_balance(hdx); let denom = total_pct.deconstruct() as u128; - for (dest, pct, accepts_raw) in destinations { + for FeeDestination { + account, + percentage, + accepts_raw, + hold_until_ed, + } in destinations + { if accepts_raw { continue; } - let numer = pct.deconstruct() as u128; - let amount = multiply_by_rational_with_rounding(total, numer, denom, Rounding::Down) + let numer = percentage.deconstruct() as u128; + let slice = multiply_by_rational_with_rounding(total, numer, denom, Rounding::Down) .ok_or(Error::::Arithmetic)?; - if amount > 0 { - T::Currency::transfer(T::HdxAssetId::get(), source, &dest, amount, Preservation::Expendable)?; + if slice == 0 { + continue; + } + + if hold_until_ed { + // `slice` is already in the pot; flush the accumulated buffer + // only if the account would then reach ED, else keep holding. + let held = HeldFees::::get(&account); + let pending = held.saturating_add(slice); + let balance = T::Currency::balance(hdx, &account); + if balance.saturating_add(pending) >= ed { + T::Currency::transfer(hdx, source, &account, pending, Preservation::Expendable)?; + HeldFees::::remove(&account); + } else { + HeldFees::::insert(&account, pending); + } + } else { + T::Currency::transfer(hdx, source, &account, slice, Preservation::Expendable)?; } } Ok(()) diff --git a/pallets/fee-processor/src/tests/hold_until_ed.rs b/pallets/fee-processor/src/tests/hold_until_ed.rs new file mode 100644 index 000000000..ea75215cb --- /dev/null +++ b/pallets/fee-processor/src/tests/hold_until_ed.rs @@ -0,0 +1,112 @@ +use super::mock::*; +use crate::*; +use frame_support::assert_ok; +use frame_support::traits::fungibles::{Inspect, Mutate}; +use frame_support::traits::tokens::Preservation; +use pallet_currencies::fungibles::FungibleCurrencies; + +fn balance(asset: AssetId, who: &AccountId) -> u128 { + as Inspect>::balance(asset, who) +} + +// Empty an account (reaped) so it starts below ED. +fn drain(asset: AssetId, who: &AccountId) { + let bal = balance(asset, who); + if bal > 0 { + assert_ok!( as Mutate>::transfer( + asset, + who, + &BOB, + bal, + Preservation::Expendable + )); + } +} + +// HDX path: HdxStakingFeeReceiver (50%, HDX-target, hold_until_ed) -> HDX_STAKING_POT, +// HdxReferralsFeeReceiver (50%, raw) -> HDX_REFERRALS_POT. With one non-raw receiver +// the pot `take` equals that receiver's slice, so the numbers stay clean. Genesis +// funds HDX_STAKING_POT / HDX_REFERRALS_POT / pot with ONE each. + +#[test] +fn hdx_fee_should_hold_staking_slice_when_pot_below_ed() { + ExtBuilder::default().build().execute_with(|| { + set_hdx_existential_deposit(ONE); + drain(HDX, &HDX_STAKING_POT); // receiver below ED + + let pot = FeeProcessor::pot_account_id(); + let pot_before = balance(HDX, &pot); + + // Staking slice = ONE/2 < ED -> held, not delivered. + assert_ok!(Pallet::::process_trade_fee(FEE_SOURCE, ALICE, HDX, ONE)); + + assert_eq!(balance(HDX, &HDX_STAKING_POT), 0, "nothing delivered to the receiver"); + assert_eq!(HeldFees::::get(HDX_STAKING_POT), ONE / 2, "slice earmarked"); + // The held HDX physically sits in the pot. + assert_eq!(balance(HDX, &pot), pot_before + ONE / 2); + }); +} + +#[test] +fn hdx_fee_should_flush_staking_buffer_when_accumulation_reaches_ed() { + ExtBuilder::default().build().execute_with(|| { + set_hdx_existential_deposit(ONE); + drain(HDX, &HDX_STAKING_POT); + + // First trade: ONE/2 held. + assert_ok!(Pallet::::process_trade_fee(FEE_SOURCE, ALICE, HDX, ONE)); + assert_eq!(balance(HDX, &HDX_STAKING_POT), 0); + assert_eq!(HeldFees::::get(HDX_STAKING_POT), ONE / 2); + + // Second trade: held(ONE/2) + slice(ONE/2) = ONE >= ED -> flush. + assert_ok!(Pallet::::process_trade_fee(FEE_SOURCE, ALICE, HDX, ONE)); + assert_eq!(balance(HDX, &HDX_STAKING_POT), ONE, "buffer flushed to the receiver"); + assert_eq!(HeldFees::::get(HDX_STAKING_POT), 0, "buffer cleared"); + }); +} + +#[test] +fn hdx_fee_should_deliver_immediately_when_pot_already_above_ed() { + ExtBuilder::default().build().execute_with(|| { + set_hdx_existential_deposit(ONE); + // Receiver already at/above ED (genesis ONE) -> even a sub-ED slice flows straight through. + let before = balance(HDX, &HDX_STAKING_POT); + + // amount = 2 -> staking slice = 1 (< ED). + assert_ok!(Pallet::::process_trade_fee(FEE_SOURCE, ALICE, HDX, 2)); + + assert_eq!(balance(HDX, &HDX_STAKING_POT), before + 1); + assert_eq!(HeldFees::::get(HDX_STAKING_POT), 0); + }); +} + +#[test] +fn hdx_fee_should_revert_below_ed_when_hold_until_ed_disabled() { + ExtBuilder::default().build().execute_with(|| { + set_hdx_existential_deposit(ONE); + set_hdx_staking_hold(false); + drain(HDX, &HDX_STAKING_POT); + + // Without buffering, the sub-ED slice is transferred straight into the empty + // receiver and reverts — the original failure mode this feature removes. + let err = Pallet::::process_trade_fee(FEE_SOURCE, ALICE, HDX, ONE).unwrap_err(); + assert_eq!(err, sp_runtime::TokenError::BelowMinimum.into()); + }); +} + +#[test] +fn hdx_fee_should_not_revert_when_pot_account_starts_empty() { + ExtBuilder::default().build().execute_with(|| { + set_hdx_existential_deposit(ONE); + let pot = FeeProcessor::pot_account_id(); + drain(HDX, &pot); + assert_eq!(balance(HDX, &pot), 0); + + // A take >= ED creates the pot on deposit and the slice is delivered without + // reverting. In production the pot is seeded >= ED before deployment, so it is + // never actually empty; this pins the create-on-deposit edge regardless. + let before = balance(HDX, &HDX_STAKING_POT); + assert_ok!(Pallet::::process_trade_fee(FEE_SOURCE, ALICE, HDX, 2 * ONE)); + assert_eq!(balance(HDX, &HDX_STAKING_POT), before + ONE, "slice delivered"); + }); +} diff --git a/pallets/fee-processor/src/tests/mock.rs b/pallets/fee-processor/src/tests/mock.rs index 4f326dcbd..11bfd34b1 100644 --- a/pallets/fee-processor/src/tests/mock.rs +++ b/pallets/fee-processor/src/tests/mock.rs @@ -59,8 +59,24 @@ parameter_type_with_key! { }; } +thread_local! { + /// Configurable native (HDX) existential deposit, so tests can exercise the + /// `hold_until_ed` buffering path. Reset to 1 by `ExtBuilder::build`. + pub static HDX_EXISTENTIAL_DEPOSIT: RefCell = const { RefCell::new(1) }; +} + +pub fn set_hdx_existential_deposit(value: Balance) { + HDX_EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = value); +} + +pub struct ExistentialDeposit; +impl frame_support::traits::Get for ExistentialDeposit { + fn get() -> Balance { + HDX_EXISTENTIAL_DEPOSIT.with(|v| *v.borrow()) + } +} + parameter_types! { - pub const ExistentialDeposit: u128 = 1; pub const MaxReserves: u32 = 50; pub const NativeAssetId: AssetId = HDX; pub const LrnaAssetId: AssetId = LRNA; @@ -155,12 +171,18 @@ thread_local! { static RAW_FEE_FAILS: RefCell = const { RefCell::new(false) }; // When set, the raw receiver consumes this fixed amount instead of the whole offered slice. static RAW_FEE_USED: RefCell> = const { RefCell::new(None) }; + // `hold_until_ed` flag for the HDX-path staking receiver, toggled per test. + static HDX_STAKING_HOLD: RefCell = const { RefCell::new(true) }; } pub fn set_raw_fee_should_fail(fail: bool) { RAW_FEE_FAILS.with(|f| *f.borrow_mut() = fail); } +pub fn set_hdx_staking_hold(value: bool) { + HDX_STAKING_HOLD.with(|v| *v.borrow_mut() = value); +} + pub fn set_raw_fee_used(used: Option) { RAW_FEE_USED.with(|u| *u.borrow_mut() = used); } @@ -260,6 +282,10 @@ impl FeeReceiver for HdxStakingFeeReceiver { fn percentage() -> Permill { Permill::from_percent(50) } + + fn hold_until_ed() -> bool { + HDX_STAKING_HOLD.with(|v| *v.borrow()) + } } pub struct HdxReferralsFeeReceiver; @@ -328,6 +354,8 @@ impl Default for ExtBuilder { impl ExtBuilder { pub fn build(self) -> sp_io::TestExternalities { + // Reset before genesis so endowments are assimilated against the default ED. + set_hdx_existential_deposit(1); let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); let native_endowed: Vec<(AccountId, Balance)> = self @@ -365,6 +393,7 @@ impl ExtBuilder { HDX_RAW_FEE_CALLS.with(|c| c.borrow_mut().clear()); RAW_FEE_FAILS.with(|f| *f.borrow_mut() = false); RAW_FEE_USED.with(|u| *u.borrow_mut() = None); + HDX_STAKING_HOLD.with(|v| *v.borrow_mut() = true); }); ext } diff --git a/pallets/fee-processor/src/tests/mod.rs b/pallets/fee-processor/src/tests/mod.rs index f13067bd9..e385e6c9f 100644 --- a/pallets/fee-processor/src/tests/mod.rs +++ b/pallets/fee-processor/src/tests/mod.rs @@ -1,3 +1,4 @@ mod convert; +mod hold_until_ed; mod mock; mod process_fee; diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 42cb4ede0..bc8a98828 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "426.0.0" +version = "427.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 3da81766b..a1046eebe 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -129,7 +129,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: Cow::Borrowed("hydradx"), impl_name: Cow::Borrowed("hydradx"), authoring_version: 1, - spec_version: 426, + spec_version: 427, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/hydradx/src/weights/pallet_omnipool.rs b/runtime/hydradx/src/weights/pallet_omnipool.rs index 825992446..b994095ba 100644 --- a/runtime/hydradx/src/weights/pallet_omnipool.rs +++ b/runtime/hydradx/src/weights/pallet_omnipool.rs @@ -19,7 +19,7 @@ //! Autogenerated weights for `pallet_omnipool` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-06-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-09, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `bench-bot`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -100,8 +100,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `3391` // Estimated: `6190` - // Minimum execution time: 202_604_000 picoseconds. - Weight::from_parts(203_917_000, 6190) + // Minimum execution time: 206_839_000 picoseconds. + Weight::from_parts(208_219_000, 6190) .saturating_add(T::DbWeight::get().reads(18_u64)) .saturating_add(T::DbWeight::get().writes(10_u64)) } @@ -163,8 +163,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `5577` // Estimated: `8739` - // Minimum execution time: 345_238_000 picoseconds. - Weight::from_parts(348_195_000, 8739) + // Minimum execution time: 355_206_000 picoseconds. + Weight::from_parts(356_353_000, 8739) .saturating_add(T::DbWeight::get().reads(32_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)) } @@ -224,8 +224,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `9117` // Estimated: `11322` - // Minimum execution time: 367_679_000 picoseconds. - Weight::from_parts(369_636_000, 11322) + // Minimum execution time: 372_720_000 picoseconds. + Weight::from_parts(374_189_000, 11322) .saturating_add(T::DbWeight::get().reads(33_u64)) .saturating_add(T::DbWeight::get().writes(14_u64)) } @@ -289,8 +289,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `5577` // Estimated: `8739` - // Minimum execution time: 353_282_000 picoseconds. - Weight::from_parts(355_852_000, 8739) + // Minimum execution time: 361_631_000 picoseconds. + Weight::from_parts(363_479_000, 8739) .saturating_add(T::DbWeight::get().reads(32_u64)) .saturating_add(T::DbWeight::get().writes(17_u64)) } @@ -350,8 +350,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `9117` // Estimated: `11322` - // Minimum execution time: 368_249_000 picoseconds. - Weight::from_parts(370_067_000, 11322) + // Minimum execution time: 374_863_000 picoseconds. + Weight::from_parts(376_226_000, 11322) .saturating_add(T::DbWeight::get().reads(33_u64)) .saturating_add(T::DbWeight::get().writes(14_u64)) } @@ -415,8 +415,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `9759` // Estimated: `19071` - // Minimum execution time: 593_084_000 picoseconds. - Weight::from_parts(595_540_000, 19071) + // Minimum execution time: 610_767_000 picoseconds. + Weight::from_parts(614_192_000, 19071) .saturating_add(T::DbWeight::get().reads(58_u64)) .saturating_add(T::DbWeight::get().writes(28_u64)) } @@ -480,8 +480,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `9759` // Estimated: `19071` - // Minimum execution time: 593_474_000 picoseconds. - Weight::from_parts(597_079_000, 19071) + // Minimum execution time: 608_159_000 picoseconds. + Weight::from_parts(611_244_000, 19071) .saturating_add(T::DbWeight::get().reads(58_u64)) .saturating_add(T::DbWeight::get().writes(28_u64)) } @@ -491,8 +491,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `1338` // Estimated: `3550` - // Minimum execution time: 34_846_000 picoseconds. - Weight::from_parts(35_356_000, 3550) + // Minimum execution time: 35_242_000 picoseconds. + Weight::from_parts(35_498_000, 3550) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -524,8 +524,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `2958` // Estimated: `6196` - // Minimum execution time: 142_423_000 picoseconds. - Weight::from_parts(143_785_000, 6196) + // Minimum execution time: 144_767_000 picoseconds. + Weight::from_parts(145_847_000, 6196) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -545,8 +545,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `3826` // Estimated: `3655` - // Minimum execution time: 79_044_000 picoseconds. - Weight::from_parts(79_681_000, 3655) + // Minimum execution time: 80_097_000 picoseconds. + Weight::from_parts(80_768_000, 3655) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -556,8 +556,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `1338` // Estimated: `3550` - // Minimum execution time: 34_929_000 picoseconds. - Weight::from_parts(35_421_000, 3550) + // Minimum execution time: 35_450_000 picoseconds. + Weight::from_parts(35_984_000, 3550) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -567,8 +567,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `1012` // Estimated: `0` - // Minimum execution time: 18_171_000 picoseconds. - Weight::from_parts(18_920_000, 0) + // Minimum execution time: 18_732_000 picoseconds. + Weight::from_parts(18_943_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: `Omnipool::Assets` (r:1 w:1) @@ -601,8 +601,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `5379` // Estimated: `8739` - // Minimum execution time: 194_334_000 picoseconds. - Weight::from_parts(195_787_000, 8739) + // Minimum execution time: 196_394_000 picoseconds. + Weight::from_parts(197_340_000, 8739) .saturating_add(T::DbWeight::get().reads(19_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -636,8 +636,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `4603` // Estimated: `11322` - // Minimum execution time: 195_830_000 picoseconds. - Weight::from_parts(196_997_000, 11322) + // Minimum execution time: 197_758_000 picoseconds. + Weight::from_parts(199_084_000, 11322) .saturating_add(T::DbWeight::get().reads(22_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)) } @@ -689,16 +689,14 @@ impl pallet_omnipool::WeightInfo for HydraWeight { /// Proof: `Broadcast::IncrementalId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// The range of component `c` is `[1, 2]`. /// The range of component `e` is `[0, 1]`. - fn router_execution_sell(c: u32, e: u32, ) -> Weight { + fn router_execution_sell(_c: u32, e: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `2455 + e * (6586 ±0)` - // Estimated: `6190 + e * (10332 ±2_239_151_241_671_078)` - // Minimum execution time: 72_006_000 picoseconds. - Weight::from_parts(60_109_597, 6190) - // Standard Error: 289_799 - .saturating_add(Weight::from_parts(6_689_980, 0).saturating_mul(c.into())) - // Standard Error: 289_799 - .saturating_add(Weight::from_parts(474_235_490, 0).saturating_mul(e.into())) + // Estimated: `6190 + e * (10332 ±3_165_345_618_907_560)` + // Minimum execution time: 72_624_000 picoseconds. + Weight::from_parts(79_760_189, 6190) + // Standard Error: 184_284 + .saturating_add(Weight::from_parts(474_589_461, 0).saturating_mul(e.into())) .saturating_add(T::DbWeight::get().reads(11_u64)) .saturating_add(T::DbWeight::get().reads((38_u64).saturating_mul(e.into()))) .saturating_add(T::DbWeight::get().writes((21_u64).saturating_mul(e.into()))) @@ -756,12 +754,12 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `9041` // Estimated: `16488` - // Minimum execution time: 512_198_000 picoseconds. - Weight::from_parts(494_684_910, 16488) - // Standard Error: 267_554 - .saturating_add(Weight::from_parts(22_931_912, 0).saturating_mul(c.into())) - // Standard Error: 267_554 - .saturating_add(Weight::from_parts(556_565, 0).saturating_mul(e.into())) + // Minimum execution time: 524_729_000 picoseconds. + Weight::from_parts(494_475_238, 16488) + // Standard Error: 582_753 + .saturating_add(Weight::from_parts(31_097_085, 0).saturating_mul(c.into())) + // Standard Error: 582_753 + .saturating_add(Weight::from_parts(5_219_089, 0).saturating_mul(e.into())) .saturating_add(T::DbWeight::get().reads(49_u64)) .saturating_add(T::DbWeight::get().writes(21_u64)) } @@ -779,8 +777,8 @@ impl pallet_omnipool::WeightInfo for HydraWeight { // Proof Size summary in bytes: // Measured: `2455` // Estimated: `6190` - // Minimum execution time: 71_697_000 picoseconds. - Weight::from_parts(72_487_000, 6190) + // Minimum execution time: 73_767_000 picoseconds. + Weight::from_parts(74_429_000, 6190) .saturating_add(T::DbWeight::get().reads(10_u64)) } } \ No newline at end of file diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 7de8fccfc..bca6d7f61 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "4.10.0" +version = "4.11.0" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" diff --git a/traits/src/fee_processor.rs b/traits/src/fee_processor.rs index e0152e60d..dd09f4104 100644 --- a/traits/src/fee_processor.rs +++ b/traits/src/fee_processor.rs @@ -7,6 +7,23 @@ use sp_std::vec::Vec; // Used by pallet-fee-processor to distribute trading fees. // --------------------------------------------------------------------------- +/// A resolved fee destination: the account a receiver's slice is paid to, its +/// share, and the two flags the processor needs to route the slice correctly. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct FeeDestination { + /// Account that receives the slice. + pub account: AccountId, + /// Receiver's share of the total fee. + pub percentage: Permill, + /// Receiver takes its slice in the raw (unconverted) trade-fee asset + /// instead of HDX (handled via `on_raw_fee_received`). + pub accepts_raw: bool, + /// When the slice is paid in HDX, hold it in the pot while `account` is + /// below the existential deposit and flush only once the accumulated amount + /// would lift it to/above ED. Ignored for raw receivers. + pub hold_until_ed: bool, +} + /// Trait for fee distribution recipients. /// Implemented by each fee receiver (staking, referrals, etc.). /// @@ -36,11 +53,27 @@ pub trait FeeReceiver { false } - /// Returns all `(destination, percentage, accepts_raw_asset)` triples. - /// Individual receiver: returns a single triple. + /// Whether the processor should buffer this receiver's HDX slices in the pot + /// while its account sits below the existential deposit, flushing only once + /// the accumulated amount would lift it to/above ED. Defaults to `true` so a + /// receiver whose pot may be uninitialized never reverts a trade with + /// `Token::BelowMinimum`. Receivers paid in a raw asset, or whose account is + /// always provider-backed, can override to `false` to be paid every slice + /// immediately. + fn hold_until_ed() -> bool { + true + } + + /// Returns all resolved `FeeDestination`s. + /// Individual receiver: returns a single entry. /// Tuple: returns the combined list from all receivers. - fn destinations() -> Vec<(AccountId, Permill, bool)> { - sp_std::vec![(Self::destination(), Self::percentage(), Self::accepts_raw_asset())] + fn destinations() -> Vec> { + sp_std::vec![FeeDestination { + account: Self::destination(), + percentage: Self::percentage(), + accepts_raw: Self::accepts_raw_asset(), + hold_until_ed: Self::hold_until_ed(), + }] } /// Offer a raw-asset receiver a slice of `amount` in `asset` for `trader`. @@ -69,7 +102,7 @@ impl FeeReceiver Vec<(AccountId, Permill, bool)> { + fn destinations() -> Vec> { Vec::new() } } @@ -107,7 +140,7 @@ impl< total } - fn destinations() -> Vec<(AccountId, Permill, bool)> { + fn destinations() -> Vec> { let mut result = Vec::new(); for_tuples!( #( result.extend(Tuple::destinations()); )* ); result