Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions pallets/fee-processor/src/tests/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,66 @@ use frame_support::traits::fungibles::Mutate;
use frame_support::traits::Hooks;
use frame_support::{assert_noop, assert_ok};
use pallet_currencies::fungibles::FungibleCurrencies;
use sp_runtime::Permill;

#[test]
fn on_idle_retries_failed_conversion_on_next_block() {
ExtBuilder::default().build().execute_with(|| {
let pot = FeeProcessor::pot_account_id();

<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(DOT, &pot, 500 * ONE).unwrap();
PendingConversions::<Test>::insert(DOT, ());

// Conversion fails - asset stays pending
set_convert_result(None);
let weight = frame_support::weights::Weight::from_parts(200_000_000, 0);
FeeProcessor::on_idle(1u64, weight);

assert!(PendingConversions::<Test>::contains_key(DOT));

// Conversion succeeds - asset removed from pending
set_convert_result(Some(1000 * ONE));
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(HDX, &pot, 1000 * ONE).unwrap();
System::set_block_number(2);
FeeProcessor::on_idle(2u64, weight);

assert!(!PendingConversions::<Test>::contains_key(DOT));
System::assert_has_event(
Event::Converted {
asset_id: DOT,
amount_in: 500 * ONE,
hdx_out: 1000 * ONE,
}
.into(),
);
});
}

#[test]
fn convert_extrinsic_for_asset_not_in_pending_still_executes() {
ExtBuilder::default().build().execute_with(|| {
let pot = FeeProcessor::pot_account_id();

// Fund pot with DOT but do NOT insert into PendingConversions
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(DOT, &pot, 500 * ONE).unwrap();
assert!(!PendingConversions::<Test>::contains_key(DOT));

set_convert_result(Some(1000 * ONE));
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(HDX, &pot, 1000 * ONE).unwrap();

// do_convert has no guard - succeeds regardless of PendingConversions membership
assert_ok!(FeeProcessor::convert(RuntimeOrigin::signed(ALICE), DOT));

System::assert_has_event(
Event::Converted {
asset_id: DOT,
amount_in: 500 * ONE,
hdx_out: 1000 * ONE,
}
.into(),
);
});
}

#[test]
fn convert_extrinsic_works() {
Expand Down Expand Up @@ -139,3 +199,102 @@ fn on_idle_respects_max_conversions_per_block() {
);
});
}

#[test]
fn distribute_to_pots_uses_total_param_not_actual_pot_balance() {
ExtBuilder::default().build().execute_with(|| {
let pot = FeeProcessor::pot_account_id();

// Pot already has extra HDX beyond ED (simulates leftover from previous rounds)
let pre_existing_hdx = 500 * ONE;
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(HDX, &pot, pre_existing_hdx).unwrap();

// Set up DOT for conversion
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(DOT, &pot, 200 * ONE).unwrap();
PendingConversions::<Test>::insert(DOT, ());

// MockConvert returns 1000 ONE of HDX
let hdx_received = 1000 * ONE;
set_convert_result(Some(hdx_received));
// Fund pot with the HDX that "convert" will produce
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(HDX, &pot, hdx_received).unwrap();

let staking_before =
<FungibleCurrencies<Test> as frame_support::traits::fungibles::Inspect<AccountId>>::balance(
HDX,
&STAKING_POT,
);
let referrals_before =
<FungibleCurrencies<Test> as frame_support::traits::fungibles::Inspect<AccountId>>::balance(
HDX,
&REFERRALS_POT,
);

assert_ok!(FeeProcessor::convert(RuntimeOrigin::signed(ALICE), DOT));

let staking_after = <FungibleCurrencies<Test> as frame_support::traits::fungibles::Inspect<AccountId>>::balance(
HDX,
&STAKING_POT,
);
let referrals_after =
<FungibleCurrencies<Test> as frame_support::traits::fungibles::Inspect<AccountId>>::balance(
HDX,
&REFERRALS_POT,
);

let staking_received = staking_after - staking_before;
let referrals_received = referrals_after - referrals_before;

// FeeReceivers: StakingFeeReceiver=70%, ReferralsFeeReceiver=30%
// distribute_to_pots uses `hdx_received` (1000 ONE), NOT actual pot balance (1500+ ONE)
assert_eq!(staking_received, Permill::from_percent(70).mul_floor(hdx_received));
assert_eq!(referrals_received, Permill::from_percent(30).mul_floor(hdx_received));

// Pre-existing HDX (500 ONE) + ED (1 ONE) stays on pot - not distributed
let pot_balance_after =
<FungibleCurrencies<Test> as frame_support::traits::fungibles::Inspect<AccountId>>::balance(HDX, &pot);
assert_eq!(pot_balance_after, ONE + pre_existing_hdx); // only ED + leftover remains
});
}

#[test]
fn on_idle_returns_zero_when_weight_below_single_conversion() {
ExtBuilder::default().build().execute_with(|| {
let pot = FeeProcessor::pot_account_id();

<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(DOT, &pot, 500 * ONE).unwrap();
PendingConversions::<Test>::insert(DOT, ());

set_convert_result(Some(1000 * ONE));

// One conversion costs Weight::from_parts(100_000_000, 0); pass one unit less
let below_threshold = frame_support::weights::Weight::from_parts(99_999_999, 0);
let used = FeeProcessor::on_idle(1u64, below_threshold);

assert!(used.is_zero());
assert!(PendingConversions::<Test>::contains_key(DOT));
});
}

#[test]
fn on_idle_processes_only_one_when_weight_fits_exactly_one() {
ExtBuilder::default().build().execute_with(|| {
let pot = FeeProcessor::pot_account_id();

<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(DOT, &pot, 500 * ONE).unwrap();
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(DAI, &pot, 300 * ONE).unwrap();
PendingConversions::<Test>::insert(DOT, ());
PendingConversions::<Test>::insert(DAI, ());

set_convert_result(Some(1000 * ONE));
<FungibleCurrencies<Test> as Mutate<AccountId>>::mint_into(HDX, &pot, 2000 * ONE).unwrap();

// Weight for exactly one conversion
let one_conversion = frame_support::weights::Weight::from_parts(100_000_000, 0);
let used = FeeProcessor::on_idle(1u64, one_conversion);

assert_eq!(used, one_conversion);
// Exactly one asset removed, one still pending
assert_eq!(PendingConversions::<Test>::count(), 1);
});
}
93 changes: 75 additions & 18 deletions pallets/fee-processor/src/tests/mock.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate as pallet_fee_processor;
use crate::*;
use frame_support::{
parameter_types,
sp_runtime::{
Expand Down Expand Up @@ -42,9 +41,10 @@ pub const FEE_SOURCE: AccountId = 100;
pub const STAKING_POT: AccountId = 200;
pub const REFERRALS_POT: AccountId = 201;

// HDX path uses same destination accounts but different percentages
// HDX path pots
pub const HDX_STAKING_POT: AccountId = 200;
pub const HDX_REFERRALS_POT: AccountId = 201;
pub const HDX_GIGAPOT: AccountId = 202;
pub const HDX_REWARD_POT: AccountId = 203;

frame_support::construct_runtime!(
pub enum Test {
Expand Down Expand Up @@ -124,7 +124,6 @@ impl pallet_balances::Config for Test {
}

impl orml_tokens::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type Amount = Amount;
type CurrencyId = AssetId;
Expand All @@ -138,14 +137,14 @@ impl orml_tokens::Config for Test {
}

impl pallet_currencies::Config for Test {
type RuntimeEvent = RuntimeEvent;
type MultiCurrency = Tokens;
type NativeCurrency = BasicCurrencyAdapter<Test, Balances, Amount, u32>;
type Erc20Currency = MockErc20Currency<Test>;
type BoundErc20 = MockBoundErc20<Test>;
type ReserveAccount = TreasuryAccount;
type GetNativeCurrencyId = NativeAssetId;
type RegistryInspect = MockBoundErc20<Test>;
type EgressHandler = pallet_currencies::MockEgressHandler<Test>;
type WeightInfo = ();
}

Expand All @@ -157,6 +156,11 @@ thread_local! {
static DEPOSIT_CALLS: RefCell<Vec<Balance>> = RefCell::new(Vec::new());
static HDX_PRE_DEPOSIT_CALLS: RefCell<Vec<(AccountId, Balance)>> = RefCell::new(Vec::new());
static HDX_DEPOSIT_CALLS: RefCell<Vec<Balance>> = RefCell::new(Vec::new());
static HDX_GIGAPOT_PRE_DEPOSIT_CALLS: RefCell<Vec<(AccountId, Balance)>> = RefCell::new(Vec::new());
static HDX_GIGAPOT_DEPOSIT_CALLS: RefCell<Vec<Balance>> = RefCell::new(Vec::new());
static HDX_REWARD_POT_PRE_DEPOSIT_CALLS: RefCell<Vec<(AccountId, Balance)>> = RefCell::new(Vec::new());
static HDX_REWARD_POT_DEPOSIT_CALLS: RefCell<Vec<Balance>> = RefCell::new(Vec::new());
static PRE_DEPOSIT_FAIL: RefCell<bool> = RefCell::new(false);
}

pub struct MockConvert;
Expand Down Expand Up @@ -199,6 +203,26 @@ pub fn hdx_deposit_calls() -> Vec<Balance> {
HDX_DEPOSIT_CALLS.with(|c| c.borrow().clone())
}

pub fn hdx_gigapot_pre_deposit_calls() -> Vec<(AccountId, Balance)> {
HDX_GIGAPOT_PRE_DEPOSIT_CALLS.with(|c| c.borrow().clone())
}

pub fn hdx_gigapot_deposit_calls() -> Vec<Balance> {
HDX_GIGAPOT_DEPOSIT_CALLS.with(|c| c.borrow().clone())
}

pub fn hdx_reward_pot_pre_deposit_calls() -> Vec<(AccountId, Balance)> {
HDX_REWARD_POT_PRE_DEPOSIT_CALLS.with(|c| c.borrow().clone())
}

pub fn hdx_reward_pot_deposit_calls() -> Vec<Balance> {
HDX_REWARD_POT_DEPOSIT_CALLS.with(|c| c.borrow().clone())
}

pub fn set_pre_deposit_fail(fail: bool) {
PRE_DEPOSIT_FAIL.with(|f| *f.borrow_mut() = fail);
}

// --- Mock PriceProvider ---
thread_local! {
static MOCK_PRICE: RefCell<Option<EmaPrice>> = RefCell::new(Some(EmaPrice::new(2, 1)));
Expand Down Expand Up @@ -235,6 +259,9 @@ impl FeeReceiver<AccountId, Balance> for StakingFeeReceiver {

fn on_pre_fee_deposit(trader: AccountId, amount: Balance) -> Result<(), Self::Error> {
PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().push((trader, amount)));
if PRE_DEPOSIT_FAIL.with(|f| *f.borrow()) {
return Err(DispatchError::Other("pre_deposit_failed"));
}
Ok(())
}

Expand Down Expand Up @@ -268,43 +295,67 @@ impl FeeReceiver<AccountId, Balance> for ReferralsFeeReceiver {
}
}

// --- HDX-specific FeeReceivers (50/50 split) ---
// --- HDX-specific FeeReceivers (70/20/10 split, no referrals) ---

pub struct HdxStakingFeeReceiver;
pub struct HdxGigaHdxFeeReceiver;

impl FeeReceiver<AccountId, Balance> for HdxStakingFeeReceiver {
impl FeeReceiver<AccountId, Balance> for HdxGigaHdxFeeReceiver {
type Error = DispatchError;

fn destination() -> AccountId {
HDX_STAKING_POT
HDX_GIGAPOT
}

fn percentage() -> Permill {
Permill::from_percent(50)
Permill::from_percent(70)
}

fn on_pre_fee_deposit(trader: AccountId, amount: Balance) -> Result<(), Self::Error> {
HDX_PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().push((trader, amount)));
HDX_GIGAPOT_PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().push((trader, amount)));
Ok(())
}

fn on_fee_received(amount: Balance) -> Result<(), Self::Error> {
HDX_DEPOSIT_CALLS.with(|c| c.borrow_mut().push(amount));
HDX_GIGAPOT_DEPOSIT_CALLS.with(|c| c.borrow_mut().push(amount));
Ok(())
}
}

pub struct HdxReferralsFeeReceiver;
pub struct HdxGigaRewardFeeReceiver;

impl FeeReceiver<AccountId, Balance> for HdxReferralsFeeReceiver {
impl FeeReceiver<AccountId, Balance> for HdxGigaRewardFeeReceiver {
type Error = DispatchError;

fn destination() -> AccountId {
HDX_REFERRALS_POT
HDX_REWARD_POT
}

fn percentage() -> Permill {
Permill::from_percent(50)
Permill::from_percent(20)
}

fn on_pre_fee_deposit(trader: AccountId, amount: Balance) -> Result<(), Self::Error> {
HDX_REWARD_POT_PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().push((trader, amount)));
Ok(())
}

fn on_fee_received(amount: Balance) -> Result<(), Self::Error> {
HDX_REWARD_POT_DEPOSIT_CALLS.with(|c| c.borrow_mut().push(amount));
Ok(())
}
}

pub struct HdxStakingFeeReceiver;

impl FeeReceiver<AccountId, Balance> for HdxStakingFeeReceiver {
type Error = DispatchError;

fn destination() -> AccountId {
HDX_STAKING_POT
}

fn percentage() -> Permill {
Permill::from_percent(10)
}

fn on_pre_fee_deposit(trader: AccountId, amount: Balance) -> Result<(), Self::Error> {
Expand All @@ -319,7 +370,6 @@ impl FeeReceiver<AccountId, Balance> for HdxReferralsFeeReceiver {
}

impl pallet_fee_processor::Config for Test {
type RuntimeEvent = RuntimeEvent;
type AssetId = AssetId;
type Currency = FungibleCurrencies<Test>;
type Convert = MockConvert;
Expand All @@ -329,7 +379,7 @@ impl pallet_fee_processor::Config for Test {
type LrnaAssetId = LrnaAssetId;
type MaxConversionsPerBlock = MaxConversionsPerBlock;
type FeeReceivers = (StakingFeeReceiver, ReferralsFeeReceiver);
type HdxFeeReceivers = (HdxStakingFeeReceiver, HdxReferralsFeeReceiver);
type HdxFeeReceivers = (HdxGigaHdxFeeReceiver, HdxGigaRewardFeeReceiver, HdxStakingFeeReceiver);
type WeightInfo = ();
}

Expand All @@ -349,6 +399,8 @@ impl Default for ExtBuilder {
// ED for pots
(STAKING_POT, HDX, ONE),
(REFERRALS_POT, HDX, ONE),
(HDX_GIGAPOT, HDX, ONE),
(HDX_REWARD_POT, HDX, ONE),
// ED for fee processor pot
(FeeProcessor::pot_account_id(), HDX, ONE),
],
Expand Down Expand Up @@ -395,6 +447,11 @@ impl ExtBuilder {
DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
HDX_PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
HDX_DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
HDX_GIGAPOT_PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
HDX_GIGAPOT_DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
HDX_REWARD_POT_PRE_DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
HDX_REWARD_POT_DEPOSIT_CALLS.with(|c| c.borrow_mut().clear());
PRE_DEPOSIT_FAIL.with(|f| *f.borrow_mut() = false);
MOCK_PRICE.with(|p| *p.borrow_mut() = Some(EmaPrice::new(2, 1)));
});
ext
Expand Down
Loading
Loading