Skip to content

Commit 3e75ffa

Browse files
review comments
1 parent edbbae0 commit 3e75ffa

2 files changed

Lines changed: 36 additions & 133 deletions

File tree

integration-tests/src/fee_processor.rs

Lines changed: 35 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::polkadot_test_net::*;
44
use frame_support::{assert_ok, traits::Hooks};
55
use frame_system::RawOrigin;
66
use hydradx_runtime::{
7-
Currencies, FeeProcessor, Omnipool, Referrals, Router, Runtime, RuntimeOrigin, Staking, System, Tokens, Treasury,
7+
Currencies, FeeProcessor, Omnipool, Referrals, Router, Runtime, RuntimeOrigin, Staking, System, Tokens,
88
};
99
use orml_traits::MultiCurrency;
1010
use pallet_fee_processor::WeightInfo;
@@ -103,7 +103,6 @@ fn hdx_fee_distributes_to_hdx_receivers() {
103103

104104
let staking_before = Currencies::free_balance(HDX, &staking_pot());
105105
let referrals_before = Currencies::free_balance(HDX, &referrals_pot());
106-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
107106

108107
// Sell DAI for HDX — the trade fee on the HDX side will be in HDX
109108
assert_ok!(Omnipool::sell(
@@ -116,22 +115,16 @@ fn hdx_fee_distributes_to_hdx_receivers() {
116115

117116
let staking_increase = Currencies::free_balance(HDX, &staking_pot()).saturating_sub(staking_before);
118117
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_before);
119-
let treasury_increase = Currencies::free_balance(HDX, &Treasury::account_id()).saturating_sub(treasury_before);
120118

121-
// Staking receives its HDX-path slice directly.
119+
// Referrals participates in the HDX fee path (ReferralsFeeReceiver is in HdxFeeReceivers),
120+
// so both pots receive their identical 5%/50% slice directly, without any conversion step.
122121
assert_eq!(
123122
staking_increase, 188932,
124123
"Staking pot should receive its HDX-path slice"
125124
);
126-
// No referral shares exist, so the referrals slice is routed to the treasury rather than
127-
// stranded in the (unclaimable) referrals pot.
128125
assert_eq!(
129-
referrals_increase, 0,
130-
"Referrals pot must not be funded while TotalShares == 0"
131-
);
132-
assert_eq!(
133-
treasury_increase, 188932,
134-
"Referrals slice should be routed to the treasury while TotalShares == 0"
126+
referrals_increase, 188932,
127+
"Referrals pot should receive its HDX-path slice"
135128
);
136129
});
137130
}
@@ -234,7 +227,6 @@ fn non_hdx_fee_conversion_distributes_to_all_receivers() {
234227

235228
let staking_before = Currencies::free_balance(HDX, &staking_pot());
236229
let referrals_before = Currencies::free_balance(HDX, &referrals_pot());
237-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
238230

239231
// Manually trigger conversion
240232
assert_ok!(FeeProcessor::convert(RuntimeOrigin::signed(ALICE.into()), DAI));
@@ -254,27 +246,21 @@ fn non_hdx_fee_conversion_distributes_to_all_receivers() {
254246
// HDX should be distributed to all configured receiver pots
255247
// FeeReceivers: StakingFeeReceiver 10%, ReferralsFeeReceiver 10%
256248
let staking_increase = Currencies::free_balance(HDX, &staking_pot()).saturating_sub(staking_before);
257-
let referrals_pot_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_before);
258-
// No referral shares exist, so the referrals slice is routed to the treasury, not the pot.
259-
let referrals_increase = Currencies::free_balance(HDX, &Treasury::account_id()).saturating_sub(treasury_before);
249+
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_before);
260250

261251
assert!(staking_increase > 0, "Staking pot should receive HDX from conversion");
262-
assert_eq!(
263-
referrals_pot_increase, 0,
264-
"Referrals pot must not be funded while TotalShares == 0"
265-
);
266252
assert!(
267253
referrals_increase > 0,
268-
"Referrals slice should reach the treasury from conversion"
254+
"Referrals pot should receive HDX from conversion"
269255
);
270256

271-
// Staking and referrals both get the same 5/50 slice, so the treasury-routed referrals
272-
// amount should roughly match the staking pot's increase.
257+
// Staking and referrals both get 10%, but referrals on_fee_received callback
258+
// redistributes some HDX to referrers, so the pot balance may be slightly less.
273259
let diff = staking_increase.abs_diff(referrals_increase);
274260
let tolerance = staking_increase / 10; // 10% tolerance
275261
assert!(
276262
diff <= tolerance,
277-
"Staking and referrals should receive roughly equal amounts: {staking_increase} vs {referrals_increase} (diff: {diff})",
263+
"Staking (10%) and referrals (10%) should receive roughly equal amounts: {staking_increase} vs {referrals_increase} (diff: {diff})",
278264
);
279265
});
280266
}
@@ -304,7 +290,6 @@ fn non_hdx_conversion_distributes_exact_proportional_amounts_to_each_receiver()
304290

305291
let staking_before = Currencies::free_balance(HDX, &staking_pot());
306292
let referrals_before = Currencies::free_balance(HDX, &referrals_pot());
307-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
308293
let gigahdx_before = Currencies::free_balance(HDX, &gigahdx_pot());
309294
let rewards_before = Currencies::free_balance(HDX, &gigahdx_rewards_pot());
310295

@@ -351,13 +336,7 @@ fn non_hdx_conversion_distributes_exact_proportional_amounts_to_each_receiver()
351336
let gigahdx_increase = Currencies::free_balance(HDX, &gigahdx_pot()) - gigahdx_before;
352337
let rewards_increase = Currencies::free_balance(HDX, &gigahdx_rewards_pot()) - rewards_before;
353338
let staking_increase = Currencies::free_balance(HDX, &staking_pot()) - staking_before;
354-
// No referral shares exist, so the referrals slice is routed to the treasury, not the pot.
355-
let referrals_pot_increase = Currencies::free_balance(HDX, &referrals_pot()) - referrals_before;
356-
let referrals_increase = Currencies::free_balance(HDX, &Treasury::account_id()) - treasury_before;
357-
assert_eq!(
358-
referrals_pot_increase, 0,
359-
"Referrals pot must not be funded while TotalShares == 0"
360-
);
339+
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()) - referrals_before;
361340

362341
// Every receiver gets at least its primary `hdx_out` share, plus a strictly positive
363342
// secondary slice — so each strictly exceeds the primary share.
@@ -535,7 +514,6 @@ fn multiple_hdx_trades_accumulate_in_all_hdx_receivers() {
535514

536515
let staking_initial = Currencies::free_balance(HDX, &staking_pot());
537516
let referrals_initial = Currencies::free_balance(HDX, &referrals_pot());
538-
let treasury_initial = Currencies::free_balance(HDX, &Treasury::account_id());
539517

540518
// Multiple HDX-generating trades
541519
for _ in 0..3 {
@@ -549,23 +527,17 @@ fn multiple_hdx_trades_accumulate_in_all_hdx_receivers() {
549527
}
550528

551529
let staking_total = Currencies::free_balance(HDX, &staking_pot()).saturating_sub(staking_initial);
552-
let referrals_pot_total = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_initial);
553-
// No referral shares exist, so the referrals slice is routed to the treasury, not the pot.
554-
let referrals_total = Currencies::free_balance(HDX, &Treasury::account_id()).saturating_sub(treasury_initial);
530+
let referrals_total = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_initial);
555531

556532
// Both staking and referrals are in HdxFeeReceivers, so both accumulate across the 3 trades
557533
// (3 × the single-trade slice of 188932).
558534
assert_eq!(
559535
staking_total, 566796,
560536
"Staking pot should accumulate from multiple HDX trades"
561537
);
562-
assert_eq!(
563-
referrals_pot_total, 0,
564-
"Referrals pot must not be funded while TotalShares == 0"
565-
);
566538
assert_eq!(
567539
referrals_total, 566796,
568-
"Referrals slice should accumulate in the treasury from multiple HDX trades"
540+
"Referrals pot should accumulate from multiple HDX trades"
569541
);
570542
});
571543
}
@@ -678,7 +650,6 @@ fn tiny_hdx_fee_distributes_to_configured_receivers() {
678650
let rewards_before = Currencies::free_balance(HDX, &gigahdx_rewards_pot());
679651
let staking_before = Currencies::free_balance(HDX, &staking_pot());
680652
let referrals_before = Currencies::free_balance(HDX, &referrals_pot());
681-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
682653

683654
//Act: HdxFeeReceivers = GigaHdx 15% + GigaHdxRewards 25% + HdxStaking 5% + Referrals 5%
684655
// (total 50%). fee = 100 → take = 50, distributed as 15 / 25 / 5 / 5.
@@ -691,12 +662,7 @@ fn tiny_hdx_fee_distributes_to_configured_receivers() {
691662
25
692663
);
693664
assert_eq!(Currencies::free_balance(HDX, &staking_pot()) - staking_before, 5);
694-
// No referral shares exist, so the referrals slice (5) is routed to the treasury, not the pot.
695-
assert_eq!(Currencies::free_balance(HDX, &referrals_pot()) - referrals_before, 0);
696-
assert_eq!(
697-
Currencies::free_balance(HDX, &Treasury::account_id()) - treasury_before,
698-
5
699-
);
665+
assert_eq!(Currencies::free_balance(HDX, &referrals_pot()) - referrals_before, 5);
700666
});
701667
}
702668

@@ -935,26 +901,19 @@ fn buy_trade_distributes_fees_same_as_sell() {
935901
// Convert and verify configured receiver pots get HDX
936902
let staking_pre = Currencies::free_balance(HDX, &staking_pot());
937903
let referrals_pre = Currencies::free_balance(HDX, &referrals_pot());
938-
let treasury_pre = Currencies::free_balance(HDX, &Treasury::account_id());
939904

940905
assert_ok!(FeeProcessor::convert(RuntimeOrigin::signed(ALICE.into()), DAI));
941906

942907
let staking_increase = Currencies::free_balance(HDX, &staking_pot()).saturating_sub(staking_pre);
943-
let referrals_pot_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_pre);
944-
// No referral shares exist, so the referrals slice is routed to the treasury, not the pot.
945-
let referrals_increase = Currencies::free_balance(HDX, &Treasury::account_id()).saturating_sub(treasury_pre);
908+
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_pre);
946909

947910
assert!(
948911
staking_increase > 0,
949912
"Staking should receive HDX from buy trade conversion"
950913
);
951-
assert_eq!(
952-
referrals_pot_increase, 0,
953-
"Referrals pot must not be funded while TotalShares == 0"
954-
);
955914
assert!(
956915
referrals_increase > 0,
957-
"Referrals slice should reach the treasury from buy trade conversion"
916+
"Referrals should receive HDX from buy trade conversion"
958917
);
959918
});
960919
}
@@ -1378,26 +1337,19 @@ fn router_trade_fee_distribution_matches_direct_trade() {
13781337
// Convert and verify configured receiver pots get HDX
13791338
let staking_pre = Currencies::free_balance(HDX, &staking_pot());
13801339
let referrals_pre = Currencies::free_balance(HDX, &referrals_pot());
1381-
let treasury_pre = Currencies::free_balance(HDX, &Treasury::account_id());
13821340

13831341
assert_ok!(FeeProcessor::convert(RuntimeOrigin::signed(ALICE.into()), DAI));
13841342

13851343
let staking_increase = Currencies::free_balance(HDX, &staking_pot()).saturating_sub(staking_pre);
1386-
let referrals_pot_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_pre);
1387-
// No referral shares exist, so the referrals slice is routed to the treasury, not the pot.
1388-
let referrals_increase = Currencies::free_balance(HDX, &Treasury::account_id()).saturating_sub(treasury_pre);
1344+
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_pre);
13891345

13901346
assert!(
13911347
staking_increase > 0,
13921348
"Staking should receive HDX from router trade conversion"
13931349
);
1394-
assert_eq!(
1395-
referrals_pot_increase, 0,
1396-
"Referrals pot must not be funded while TotalShares == 0"
1397-
);
13981350
assert!(
13991351
referrals_increase > 0,
1400-
"Referrals slice should reach the treasury from router trade conversion"
1352+
"Referrals should receive HDX from router trade conversion"
14011353
);
14021354
});
14031355
}
@@ -1418,7 +1370,6 @@ fn buying_hdx_from_omnipool_credits_all_four_hdx_fee_pots() {
14181370
let giga_rewards_before = Currencies::free_balance(HDX, &gigahdx_rewards_pot());
14191371
let staking_before = Currencies::free_balance(HDX, &staking_pot());
14201372
let referrals_before = Currencies::free_balance(HDX, &referrals_pot());
1421-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
14221373

14231374
// Buy 100 HDX with DAI — fee leg is on `asset_out` (HDX), so this hits
14241375
// the HDX path of process_trade_fee and distributes synchronously.
@@ -1467,13 +1418,7 @@ fn buying_hdx_from_omnipool_credits_all_four_hdx_fee_pots() {
14671418
let giga_increase = Currencies::free_balance(HDX, &gigahdx_pot()) - giga_before;
14681419
let giga_rewards_increase = Currencies::free_balance(HDX, &gigahdx_rewards_pot()) - giga_rewards_before;
14691420
let staking_increase = Currencies::free_balance(HDX, &staking_pot()) - staking_before;
1470-
// No referral shares exist, so the referrals slice is routed to the treasury, not the pot.
1471-
let referrals_pot_increase = Currencies::free_balance(HDX, &referrals_pot()) - referrals_before;
1472-
let referrals_increase = Currencies::free_balance(HDX, &Treasury::account_id()) - treasury_before;
1473-
assert_eq!(
1474-
referrals_pot_increase, 0,
1475-
"Referrals pot must not be funded while TotalShares == 0"
1476-
);
1421+
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()) - referrals_before;
14771422

14781423
assert_eq!(giga_increase, share(15), "gigaHDX pot must receive 15/50 share");
14791424
assert_eq!(
@@ -1482,11 +1427,7 @@ fn buying_hdx_from_omnipool_credits_all_four_hdx_fee_pots() {
14821427
"gigaHDX rewards pot must receive 25/50 share"
14831428
);
14841429
assert_eq!(staking_increase, share(5), "legacy staking pot must receive 5/50 share");
1485-
assert_eq!(
1486-
referrals_increase,
1487-
share(5),
1488-
"referrals 5/50 share must be routed to the treasury"
1489-
);
1430+
assert_eq!(referrals_increase, share(5), "referrals pot must receive 5/50 share");
14901431

14911432
// Conservation: the four receiver shares plus any rounding dust account
14921433
// for the full take. With three 5% slices and one 15%/25%, distinct
@@ -1535,9 +1476,6 @@ fn selling_for_dai_then_advancing_block_distributes_converted_hdx_to_all_four_po
15351476
// so we call it explicitly with generous weight. This is the path the chain
15361477
// will take in production once the next block fires.
15371478
hydradx_run_to_next_block();
1538-
// Snapshot the treasury right before conversion: with no referral shares the referrals
1539-
// slice is routed here, and this isolates it from any block-production effects above.
1540-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
15411479
let weight = frame_support::weights::Weight::from_parts(1_000_000_000_000, u64::MAX);
15421480
pallet_fee_processor::Pallet::<Runtime>::on_idle(System::block_number(), weight);
15431481

@@ -1600,14 +1538,7 @@ fn selling_for_dai_then_advancing_block_distributes_converted_hdx_to_all_four_po
16001538
let giga_increase = Currencies::free_balance(HDX, &gigahdx_pot()) - giga_before;
16011539
let giga_rewards_increase = Currencies::free_balance(HDX, &gigahdx_rewards_pot()) - giga_rewards_before;
16021540
let staking_increase = Currencies::free_balance(HDX, &staking_pot()) - staking_before;
1603-
// No referral shares exist, so the referrals slice (primary + nested HDX) is routed to the
1604-
// treasury, not the pot.
1605-
let referrals_pot_increase = Currencies::free_balance(HDX, &referrals_pot()) - referrals_before;
1606-
let referrals_increase = Currencies::free_balance(HDX, &Treasury::account_id()) - treasury_before;
1607-
assert_eq!(
1608-
referrals_pot_increase, 0,
1609-
"Referrals pot must not be funded while TotalShares == 0"
1610-
);
1541+
let referrals_increase = Currencies::free_balance(HDX, &referrals_pot()) - referrals_before;
16111542

16121543
assert_eq!(
16131544
giga_increase,
@@ -1627,7 +1558,7 @@ fn selling_for_dai_then_advancing_block_distributes_converted_hdx_to_all_four_po
16271558
assert_eq!(
16281559
referrals_increase,
16291560
expected(5),
1630-
"referrals 5/50 of both inflows must be routed to the treasury"
1561+
"referrals pot must receive 5/50 of both inflows"
16311562
);
16321563
});
16331564
}
@@ -1834,35 +1765,28 @@ fn referrer_may_claim_a_bounded_cut_during_the_conversion_gap() {
18341765
});
18351766
}
18361767

1837-
/// FINDING 5 — referral fee slice must not strand in the pot when `TotalShares == 0`.
1838-
///
1839-
/// The referrals accumulator (`RewardPerShare`) divides by `TotalShares`, so it cannot
1840-
/// record a deposit while no shares exist (genesis/pre-adoption, or after every holder has
1841-
/// claimed and burned). Previously the slice was still transferred into the referrals pot,
1842-
/// where — never reflected in `RewardPerShare` — it was permanently unclaimable.
1843-
///
1844-
/// Fix: while `TotalShares == 0` the slice is routed to the treasury instead. The referrals
1845-
/// pot is left untouched and the accumulator correctly stays zero (no shareholder to
1846-
/// attribute the deposit to); the value is preserved in the treasury.
1768+
/// FINDING 5 (accepted limitation) — while `TotalShares == 0` the referrals slice is still
1769+
/// deposited into the referrals pot, but the accumulator (`RewardPerShare`) divides by
1770+
/// `TotalShares` and so cannot record it yet. This is accepted for the genesis/pre-adoption
1771+
/// window; once any referral shares exist, deposits accrue normally. (A treasury-redirect fix
1772+
/// was reverted because it polluted the shared treasury fee accounting.)
18471773
#[test]
1848-
fn referral_fee_with_zero_shares_should_be_routed_to_treasury() {
1774+
fn referral_fee_with_zero_shares_is_deposited_to_pot_but_not_yet_accrued() {
18491775
TestNet::reset();
18501776

18511777
Hydra::execute_with(|| {
18521778
init_omnipool_with_oracle_for_block_24();
18531779

1854-
// No referral codes registered/linked → no referral shares exist.
18551780
assert_eq!(Referrals::total_shares(), 0, "no referral shares in the system");
18561781
assert!(
18571782
pallet_referrals::RewardPerShare::<Runtime>::get().is_zero(),
18581783
"accumulator starts at zero"
18591784
);
18601785

18611786
let referrals_pot_before = Currencies::free_balance(HDX, &referrals_pot());
1862-
let treasury_before = Currencies::free_balance(HDX, &Treasury::account_id());
18631787

1864-
// An unlinked trader generates a non-HDX (DAI) fee, then it is converted to HDX
1865-
// the referrals slice would normally be transferred into the referrals pot.
1788+
// An unlinked trader generates a non-HDX (DAI) fee that is converted to HDX; the
1789+
// referrals slice is transferred into the referrals pot.
18661790
assert_ok!(Omnipool::sell(
18671791
RuntimeOrigin::signed(BOB.into()),
18681792
HDX,
@@ -1872,24 +1796,15 @@ fn referral_fee_with_zero_shares_should_be_routed_to_treasury() {
18721796
));
18731797
assert_ok!(FeeProcessor::convert(RuntimeOrigin::signed(ALICE.into()), DAI));
18741798

1875-
let referrals_pot_increase =
1876-
Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_pot_before);
1877-
let treasury_increase = Currencies::free_balance(HDX, &Treasury::account_id()).saturating_sub(treasury_before);
1799+
let deposited = Currencies::free_balance(HDX, &referrals_pot()).saturating_sub(referrals_pot_before);
18781800

1879-
// The slice was generated and routed to the treasury, not stranded in the pot...
1880-
assert!(
1881-
treasury_increase > 0,
1882-
"the un-attributable referrals slice must be routed to the treasury"
1883-
);
1884-
assert_eq!(
1885-
referrals_pot_increase, 0,
1886-
"the referrals pot must not retain un-attributable HDX"
1887-
);
1888-
// ...and the accumulator correctly stays zero (no shares to attribute to).
1801+
// The slice lands in the referrals pot...
1802+
assert!(deposited > 0, "the referrals slice is deposited into the referrals pot");
1803+
// ...but with zero shares the accumulator cannot record it (accepted limitation).
18891804
assert_eq!(Referrals::total_shares(), 0, "still no referral shares");
18901805
assert!(
18911806
pallet_referrals::RewardPerShare::<Runtime>::get().is_zero(),
1892-
"RewardPerShare must stay zero — there is no shareholder to attribute the deposit to"
1807+
"with zero shares the deposit is not (yet) reflected in the accumulator"
18931808
);
18941809
});
18951810
}

runtime/hydradx/src/assets.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,14 +2342,7 @@ impl hydradx_traits::fee_processor::FeeReceiver<AccountId, Balance> for Referral
23422342
type Error = sp_runtime::DispatchError;
23432343

23442344
fn destination() -> AccountId {
2345-
// With no shareholders the accumulator can't attribute a deposit (it divides by
2346-
// `TotalShares`), so route the slice to the treasury instead of stranding it in the pot
2347-
// where nobody could ever claim it (audit 2026-06-02, finding 5).
2348-
if pallet_referrals::Pallet::<Runtime>::total_shares() == 0 {
2349-
TreasuryAccount::get()
2350-
} else {
2351-
pallet_referrals::Pallet::<Runtime>::pot_account_id()
2352-
}
2345+
pallet_referrals::Pallet::<Runtime>::pot_account_id()
23532346
}
23542347

23552348
fn percentage() -> Permill {
@@ -2361,11 +2354,6 @@ impl hydradx_traits::fee_processor::FeeReceiver<AccountId, Balance> for Referral
23612354
}
23622355

23632356
fn on_fee_received(amount: Balance) -> Result<(), Self::Error> {
2364-
// Mirror `destination()`: with no shareholders the slice went to the treasury, not the
2365-
// pot, so there is nothing to accrue into the accumulator.
2366-
if pallet_referrals::Pallet::<Runtime>::total_shares() == 0 {
2367-
return Ok(());
2368-
}
23692357
pallet_referrals::Pallet::<Runtime>::on_hdx_deposited(amount)
23702358
}
23712359
}

0 commit comments

Comments
 (0)