datahaven/operator/runtime/testnet/tests/treasury.rs
undercover-cactus b8cc9eee4b
feat: upgrade to polkadot SDK 2503 (#444)
## Polkadot upgrade 2503

In this PR, we upgrade form Polkadot SDK 2412 to Polkadot SDK 2503. In
order to upgrade the SDK we need to upgrade some dependencies :
StorageHub and frontier simultaneously.


## What changes 

### Trivial changes

* https://github.com/paritytech/polkadot-sdk/pull/7634 -> The new trait
is required in all the pallets using scale encoding.

* https://github.com/paritytech/polkadot-sdk/pull/7043 -> Remove
deprecated `sp-std` and replace with `alloc` or `core`.

* https://github.com/paritytech/polkadot-sdk/pull/6140 -> Accurate
weight reclaim with frame_system::WeightReclaim


### Breaking changes

* https://github.com/paritytech/polkadot-sdk/pull/2072 -> There is a
change in `pallet-referenda`. Now, the tracks are retrieved as a list of
`Track`s. Also, the names of the tracks might have some trailing null
values (`\0`). This means display representation of the tracks' names
must be sanitized.

* https://github.com/paritytech/polkadot-sdk/pull/5723 -> adds the
ability for these pallets to specify their source of the block number.
This is useful when these pallets are migrated from the relay chain to a
parachain and vice versa. (Not entirely sure it is breaking as it is
being marked as backward compatible).

* https://github.com/paritytech/polkadot-sdk/pull/6338 -> Update
Referenda to Support Block Number Provider

### Notable changes

* https://github.com/paritytech/polkadot-sdk/pull/5703 -> Not changes
required in the codebase but impact fastSync mode. Should improve
testing.

* https://github.com/paritytech/polkadot-sdk/pull/5842 -> Removes
`libp2p` types in authority-discovery, and replace them with network
backend agnostic types from `sc-network-types`. The `sc-network`
interface is therefore updated accordingly.

## What changes in Frontier 

* https://github.com/polkadot-evm/frontier/pull/1693 -> Add support for
EIP 7702 which has been enable with Pectra. This EIP add a new field
`AuthorizationList` in Ethereum transaction.

Changes example :

```rust
#[test]
fn validate_transaction_fails_on_filtered_call() {
...
            pallet_evm::Call::<Runtime>::call {
                source: H160::default(),
                target: H160::default(),
                input: Vec::new(),
                value: sp_core::U256::zero(),
                gas_limit: 21000,
                max_fee_per_gas: sp_core::U256::zero(),
                max_priority_fee_per_gas: Some(sp_core::U256::zero()),
                nonce: None,
                access_list: Vec::new(),
                authorization_list: Vec::new(),
            }
            .into(),
```

## ⚠️ Breaking Changes ⚠️

* Upgrade to Stirage hub v0.5.1
* Support for Ethereum latest upgrade (txs now have the
`authoriation_list` for support EIP 7702)

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 10:04:57 +01:00

550 lines
22 KiB
Rust

// Copyright 2025 DataHaven
// This file is part of DataHaven.
// DataHaven is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// DataHaven is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with DataHaven. If not, see <http://www.gnu.org/licenses/>.
//! Moonbase Runtime Integration Tests
mod common;
use common::*;
use datahaven_runtime_common::Balance;
use datahaven_testnet_runtime::{
configs::{
runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion,
TransactionPaymentAsGasPrice,
},
currency::*,
AccountId, Balances, ExistentialDeposit, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin,
System, Treasury, TreasuryCouncil,
};
use fp_evm::FeeCalculator;
use frame_support::{
assert_ok,
traits::{Currency as CurrencyT, Get},
};
use sp_core::{H160, U256};
use sp_runtime::traits::{Dispatchable, Hash as HashT};
const BASE_FEE_GENESIS: u128 = 10 * MILLIHAVE / 4;
/// Helper function to get existential deposit (specific to this test file)
fn existential_deposit() -> Balance {
ExistentialDeposit::get()
}
#[test]
fn author_does_receive_priority_fee() {
ExtBuilder::default()
.with_balances(vec![(
AccountId::from(BOB),
(1 * HAVE) + (21_000 * (500 * MILLIHAVE)),
)])
.build()
.execute_with(|| {
// Set Charlie (validator index 0) as the block author
set_block_author_by_index(0);
let author = get_validator_by_index(0);
// Currently the default impl of the evm uses `deposit_into_existing`.
// If we were to use this implementation, and for an author to receive eventual tips,
// the account needs to be somehow initialized, otherwise the deposit would fail.
Balances::make_free_balance_be(&author, 100 * HAVE);
// EVM transfer.
assert_ok!(RuntimeCall::EVM(pallet_evm::Call::<Runtime>::call {
source: H160::from(BOB),
target: H160::from(ALICE),
input: Vec::new(),
value: (1 * HAVE).into(),
gas_limit: 21_000u64,
max_fee_per_gas: U256::from(300 * MILLIHAVE),
max_priority_fee_per_gas: Some(U256::from(200 * MILLIHAVE)),
nonce: Some(U256::from(0)),
access_list: Vec::new(),
authorization_list: Vec::new(),
})
.dispatch(<Runtime as frame_system::Config>::RuntimeOrigin::root()));
let priority_fee = 200 * MILLIHAVE * 21_000;
// Author free balance increased by priority fee.
assert_eq!(Balances::free_balance(author), 100 * HAVE + priority_fee,);
});
}
#[test]
fn total_issuance_after_evm_transaction_with_priority_fee() {
ExtBuilder::default()
.with_balances(vec![
(
AccountId::from(BOB),
(1 * HAVE) + (21_000 * (2 * BASE_FEE_GENESIS) + existential_deposit()),
),
(AccountId::from(ALICE), existential_deposit()),
(
<pallet_treasury::TreasuryAccountId<Runtime> as sp_core::TypedGet>::get(),
existential_deposit(),
),
])
.build()
.execute_with(|| {
let issuance_before = <Runtime as pallet_evm::Config>::Currency::total_issuance();
// Set Charlie (validator index 0) as the block author
set_block_author_by_index(0);
let _author = get_validator_by_index(0);
// EVM transfer.
assert_ok!(RuntimeCall::EVM(pallet_evm::Call::<Runtime>::call {
source: H160::from(BOB),
target: H160::from(ALICE),
input: Vec::new(),
value: (1 * HAVE).into(),
gas_limit: 21_000u64,
max_fee_per_gas: U256::from(2 * BASE_FEE_GENESIS),
max_priority_fee_per_gas: Some(U256::from(BASE_FEE_GENESIS)),
nonce: Some(U256::from(0)),
access_list: Vec::new(),
authorization_list: Vec::new(),
})
.dispatch(<Runtime as frame_system::Config>::RuntimeOrigin::root()));
let issuance_after = <Runtime as pallet_evm::Config>::Currency::total_issuance();
// Get the actual base fee that was charged by querying the fee calculator
// This represents the real network base fee, not the genesis constant
let (actual_base_fee_u256, _) = TransactionPaymentAsGasPrice::min_gas_price();
let actual_base_fee: Balance = actual_base_fee_u256.as_u128();
// Calculate expected treasury and burn amounts based on the actual base fee
let gas_used = 21_000u128; // Standard transfer gas
let total_base_fee_charged = actual_base_fee * gas_used;
let treasury_proportion = FeesTreasuryProportion::get();
let expected_treasury_amount = treasury_proportion.mul_floor(total_base_fee_charged);
let expected_burnt_amount = total_base_fee_charged - expected_treasury_amount;
// Verify total issuance was reduced by the burnt amount
assert_eq!(
issuance_after,
issuance_before - expected_burnt_amount,
"Total issuance should decrease by burnt base fee amount"
);
// Verify treasury received the expected amount
// Treasury pot starts at 0 and should equal exactly the treasury fee amount
let treasury_final_balance = datahaven_testnet_runtime::Treasury::pot();
assert_eq!(
treasury_final_balance,
expected_treasury_amount,
"Treasury should receive {}% of base fee: expected {}, got {}",
treasury_proportion.deconstruct() as f64 / 10_000_000.0 * 100.0,
expected_treasury_amount,
treasury_final_balance
);
});
}
#[test]
fn total_issuance_after_evm_transaction_without_priority_fee() {
use fp_evm::FeeCalculator;
ExtBuilder::default()
.with_balances(vec![
(
AccountId::from(BOB),
(1 * HAVE) + (21_000 * (2 * BASE_FEE_GENESIS)),
),
(AccountId::from(ALICE), existential_deposit()),
(
<pallet_treasury::TreasuryAccountId<Runtime> as sp_core::TypedGet>::get(),
existential_deposit(),
),
])
.build()
.execute_with(|| {
// Set block author for fee allocation
set_block_author_by_index(0);
let issuance_before = <Runtime as pallet_evm::Config>::Currency::total_issuance();
// EVM transfer.
assert_ok!(RuntimeCall::EVM(pallet_evm::Call::<Runtime>::call {
source: H160::from(BOB),
target: H160::from(ALICE),
input: Vec::new(),
value: (1 * HAVE).into(),
gas_limit: 21_000u64,
max_fee_per_gas: U256::from(BASE_FEE_GENESIS),
max_priority_fee_per_gas: None,
nonce: Some(U256::from(0)),
access_list: Vec::new(),
authorization_list: Vec::new(),
})
.dispatch(<Runtime as frame_system::Config>::RuntimeOrigin::root()));
let issuance_after = <Runtime as pallet_evm::Config>::Currency::total_issuance();
// Get the actual base fee that was charged by querying the fee calculator
// This represents the real network base fee, not the genesis constant
let (actual_base_fee_u256, _) = TransactionPaymentAsGasPrice::min_gas_price();
let actual_base_fee: Balance = actual_base_fee_u256.as_u128();
// Calculate expected treasury and burn amounts based on the actual base fee
let gas_used = 21_000u128; // Standard transfer gas
let total_base_fee_charged = actual_base_fee * gas_used;
let treasury_proportion = FeesTreasuryProportion::get();
let expected_treasury_amount = treasury_proportion.mul_floor(total_base_fee_charged);
let expected_burnt_amount = total_base_fee_charged - expected_treasury_amount;
// Verify total issuance was reduced by the burnt amount
assert_eq!(
issuance_after,
issuance_before - expected_burnt_amount,
"Total issuance should decrease by burnt base fee amount"
);
// Verify treasury received the expected amount
// Treasury pot starts at 0 and should equal exactly the treasury fee amount
let treasury_final_balance = datahaven_testnet_runtime::Treasury::pot();
assert_eq!(
treasury_final_balance,
expected_treasury_amount,
"Treasury should receive {}% of base fee: expected {}, got {}",
treasury_proportion.deconstruct() as f64 / 10_000_000.0 * 100.0,
expected_treasury_amount,
treasury_final_balance
);
});
}
#[test]
fn deal_with_fees_handles_tip() {
use datahaven_runtime_common::deal_with_fees::DealWithSubstrateFeesAndTip;
use frame_support::traits::OnUnbalanced;
ExtBuilder::default()
.with_balances(vec![
// Set up minimal balances for treasury to avoid existential deposit issues
(
datahaven_testnet_runtime::Treasury::account_id(),
existential_deposit(),
),
(get_validator_by_index(0), existential_deposit()),
])
.build()
.execute_with(|| {
// Set block author for fee allocation
set_block_author_by_index(0);
// This test validates the functionality of the `DealWithSubstrateFeesAndTip` trait implementation
// in the DataHaven runtime. It verifies that:
// - The correct proportion of the fee is sent to the treasury.
// - The remaining fee is burned (removed from the total supply).
// - The entire tip is sent to the block author.
// The test details:
// 1. Simulate issuing a `fee` of 100 and a `tip` of 1000.
// 2. Confirm the initial total supply is 1,100 (equal to the sum of the issued fee and tip).
// 3. Confirm the treasury's balance is initially 0.
// 4. Execute the `DealWithSubstrateFeesAndTip::on_unbalanceds` function with the `fee` and `tip`.
// 5. Validate that the treasury's balance has increased by 20% of the fee (based on FeesTreasuryProportion).
// 6. Validate that 80% of the fee is burned, and the total supply decreases accordingly.
// 7. Validate that the entire tip (100%) is sent to the block author (collator).
// Step 1: Issue the fee and tip amounts.
let fee =
<pallet_balances::Pallet<Runtime> as frame_support::traits::fungible::Balanced<
AccountId,
>>::issue(100);
let tip =
<pallet_balances::Pallet<Runtime> as frame_support::traits::fungible::Balanced<
AccountId,
>>::issue(1000);
// Step 2: Validate the initial supply and balances.
let total_supply_before = Balances::total_issuance();
let block_author = get_validator_by_index(0);
let block_author_balance_before = Balances::free_balance(&block_author);
// Total supply should be: issued fee (100) + issued tip (1000) + 2 existential deposits
let expected_supply = 1_100 + (2 * existential_deposit());
assert_eq!(total_supply_before, expected_supply);
assert_eq!(
Balances::free_balance(&datahaven_testnet_runtime::Treasury::account_id()),
existential_deposit()
);
// Step 3: Execute the fees handling logic.
DealWithSubstrateFeesAndTip::<Runtime, FeesTreasuryProportion>::on_unbalanceds(
vec![fee, tip].into_iter(),
);
// Step 4: Compute the split between treasury and burned fees based on FeesTreasuryProportion (20%).
let treasury_proportion = FeesTreasuryProportion::get();
let treasury_fee_part: Balance = treasury_proportion.mul_floor(100);
let burnt_fee_part: Balance = 100 - treasury_fee_part;
// Step 5: Validate the treasury received 20% of the fee.
assert_eq!(
Balances::free_balance(&datahaven_testnet_runtime::Treasury::account_id()),
existential_deposit() + treasury_fee_part,
);
// Step 6: Verify that 80% of the fee was burned (removed from the total supply).
let total_supply_after = Balances::total_issuance();
assert_eq!(total_supply_before - total_supply_after, burnt_fee_part,);
// Step 7: Validate that the block author (collator) received 100% of the tip.
let block_author_balance_after = Balances::free_balance(&block_author);
assert_eq!(
block_author_balance_after - block_author_balance_before,
1000,
);
});
}
#[test]
fn fees_burned_when_block_author_not_set() {
use datahaven_runtime_common::deal_with_fees::DealWithSubstrateFeesAndTip;
use frame_support::traits::OnUnbalanced;
ExtBuilder::default()
.with_balances(vec![(
datahaven_testnet_runtime::Treasury::account_id(),
existential_deposit(),
)])
.build()
.execute_with(|| {
// Explicitly do NOT set block author - this is the key difference from the test above
let fee =
<pallet_balances::Pallet<Runtime> as frame_support::traits::fungible::Balanced<
AccountId,
>>::issue(100);
let tip =
<pallet_balances::Pallet<Runtime> as frame_support::traits::fungible::Balanced<
AccountId,
>>::issue(1000);
let total_supply_before = Balances::total_issuance();
// Expected supply: issued fee (100) + issued tip (1000) + existential deposit
let expected_supply = 1_100 + existential_deposit();
assert_eq!(total_supply_before, expected_supply);
DealWithSubstrateFeesAndTip::<Runtime, FeesTreasuryProportion>::on_unbalanceds(
vec![fee, tip].into_iter(),
);
let treasury_proportion = FeesTreasuryProportion::get();
let treasury_fee_part: Balance = treasury_proportion.mul_floor(100);
let burnt_fee_part: Balance = 100 - treasury_fee_part;
// Verify treasury received its portion of the fee
assert_eq!(
Balances::free_balance(&datahaven_testnet_runtime::Treasury::account_id()),
existential_deposit() + treasury_fee_part,
);
// When block author is not set and ExistentialDeposit is 0,
// the tip goes to the default account (zero account) instead of being burned
let total_supply_after = Balances::total_issuance();
let expected_burned = burnt_fee_part; // Only the fee portion is burned
assert_eq!(
total_supply_before - total_supply_after,
expected_burned,
"Only fee portion should be burned when block author is not set"
);
// With ExistentialDeposit = 0, the default account can now hold the tip
let default_account: AccountId = Default::default();
assert_eq!(
Balances::free_balance(&default_account),
1000,
"Default account should receive the tip when ExistentialDeposit is 0"
);
});
}
#[cfg(test)]
mod treasury_tests {
use super::*;
use frame_support::traits::Hooks;
/// Helper function to create an origin for an account
fn origin_of(account_id: AccountId) -> RuntimeOrigin {
RuntimeOrigin::signed(account_id)
}
fn expect_events(events: Vec<RuntimeEvent>) {
let block_events: Vec<RuntimeEvent> =
System::events().into_iter().map(|r| r.event).collect();
dbg!(events.clone());
dbg!(block_events.clone());
assert!(events.iter().all(|evt| block_events.contains(evt)))
}
fn next_block() {
System::reset_events();
System::set_block_number(System::block_number() + 1u32);
System::on_initialize(System::block_number());
datahaven_testnet_runtime::Treasury::on_initialize(System::block_number());
}
#[test]
fn test_treasury_spend_local_with_root_origin() {
let initial_treasury_balance = 1_000 * HAVE;
ExtBuilder::default()
.with_balances(vec![
(AccountId::from(ALICE), 2_000 * HAVE),
(Treasury::account_id(), initial_treasury_balance),
])
.build()
.execute_with(|| {
let spend_amount = 100u128 * HAVE;
let spend_beneficiary = AccountId::from(BOB);
next_block();
// Perform treasury spending
let valid_from = System::block_number() + 5u32;
assert_ok!(datahaven_testnet_runtime::Sudo::sudo(
root_origin(),
Box::new(RuntimeCall::Treasury(pallet_treasury::Call::spend {
amount: spend_amount,
asset_kind: Box::new(()),
beneficiary: Box::new(AccountId::from(BOB)),
valid_from: Some(valid_from),
}))
));
let payout_period =
<<Runtime as pallet_treasury::Config>::PayoutPeriod as Get<u32>>::get();
let expected_events = [RuntimeEvent::Treasury(
pallet_treasury::Event::AssetSpendApproved {
index: 0,
asset_kind: (),
amount: spend_amount,
beneficiary: spend_beneficiary,
valid_from,
expire_at: payout_period + valid_from,
},
)]
.to_vec();
expect_events(expected_events);
while System::block_number() < valid_from {
next_block();
}
assert_ok!(Treasury::payout(origin_of(spend_beneficiary), 0));
let expected_events = [
RuntimeEvent::Treasury(pallet_treasury::Event::Paid {
index: 0,
payment_id: (),
}),
RuntimeEvent::Balances(pallet_balances::Event::Transfer {
from: Treasury::account_id(),
to: spend_beneficiary,
amount: spend_amount,
}),
]
.to_vec();
expect_events(expected_events);
});
}
#[test]
fn test_treasury_spend_local_with_council_origin() {
let initial_treasury_balance = 1_000 * HAVE;
ExtBuilder::default()
.with_balances(vec![
(AccountId::from(ALICE), 2_000 * HAVE),
(Treasury::account_id(), initial_treasury_balance),
])
.build()
.execute_with(|| {
let spend_amount = 100u128 * HAVE;
let spend_beneficiary = AccountId::from(BOB);
next_block();
// TreasuryCouncilCollective
assert_ok!(TreasuryCouncil::set_members(
root_origin(),
vec![AccountId::from(ALICE)],
Some(AccountId::from(ALICE)),
1
));
next_block();
// Perform treasury spending
let valid_from = System::block_number() + 5u32;
let proposal = RuntimeCall::Treasury(pallet_treasury::Call::spend {
amount: spend_amount,
asset_kind: Box::new(()),
beneficiary: Box::new(AccountId::from(BOB)),
valid_from: Some(valid_from),
});
assert_ok!(TreasuryCouncil::propose(
origin_of(AccountId::from(ALICE)),
1,
Box::new(proposal.clone()),
1_000
));
let payout_period =
<<Runtime as pallet_treasury::Config>::PayoutPeriod as Get<u32>>::get();
let expected_events = [
RuntimeEvent::Treasury(pallet_treasury::Event::AssetSpendApproved {
index: 0,
asset_kind: (),
amount: spend_amount,
beneficiary: spend_beneficiary,
valid_from,
expire_at: payout_period + valid_from,
}),
RuntimeEvent::TreasuryCouncil(pallet_collective::Event::Executed {
proposal_hash: sp_runtime::traits::BlakeTwo256::hash_of(&proposal),
result: Ok(()),
}),
]
.to_vec();
expect_events(expected_events);
while System::block_number() < valid_from {
next_block();
}
assert_ok!(Treasury::payout(origin_of(spend_beneficiary), 0));
let expected_events = [
RuntimeEvent::Treasury(pallet_treasury::Event::Paid {
index: 0,
payment_id: (),
}),
RuntimeEvent::Balances(pallet_balances::Event::Transfer {
from: Treasury::account_id(),
to: spend_beneficiary,
amount: spend_amount,
}),
]
.to_vec();
expect_events(expected_events);
});
}
}