diff --git a/runtime/moonbase/src/lib.rs b/runtime/moonbase/src/lib.rs index ab9a1db6c8..9774af7bca 100644 --- a/runtime/moonbase/src/lib.rs +++ b/runtime/moonbase/src/lib.rs @@ -558,9 +558,10 @@ parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const TreasuryId: PalletId = PalletId(*b"pc/trsry"); pub TreasuryAccount: AccountId = Treasury::account_id(); + pub const MaxSpendBalance: crate::Balance = crate::Balance::max_value(); } -type TreasuryRejectOrigin = EitherOfDiverse< +type RootOrTreasuryCouncilOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionMoreThan, >; @@ -569,7 +570,7 @@ impl pallet_treasury::Config for Runtime { type PalletId = TreasuryId; type Currency = Balances; // More than half of the council is required (or root) to reject a proposal - type RejectOrigin = TreasuryRejectOrigin; + type RejectOrigin = RootOrTreasuryCouncilOrigin; type RuntimeEvent = RuntimeEvent; type SpendPeriod = ConstU32<{ 6 * DAYS }>; type Burn = (); @@ -577,11 +578,8 @@ impl pallet_treasury::Config for Runtime { type MaxApprovals = ConstU32<100>; type WeightInfo = moonbase_weights::pallet_treasury::WeightInfo; type SpendFunds = (); - #[cfg(not(feature = "runtime-benchmarks"))] - type SpendOrigin = frame_support::traits::NeverEnsureOrigin; // Disabled, no spending - #[cfg(feature = "runtime-benchmarks")] type SpendOrigin = - frame_system::EnsureWithSuccess, AccountId, benches::MaxBalance>; + frame_system::EnsureWithSuccess; type AssetKind = (); type Beneficiary = AccountId; type BeneficiaryLookup = IdentityLookup; @@ -1190,12 +1188,6 @@ impl Contains for NormalFilter { // Note: It is also assumed that EVM calls are only allowed through `Origin::Root` so // this can be seen as an additional security RuntimeCall::EVM(_) => false, - RuntimeCall::Treasury( - pallet_treasury::Call::spend { .. } - | pallet_treasury::Call::payout { .. } - | pallet_treasury::Call::check_status { .. } - | pallet_treasury::Call::void_spend { .. }, - ) => false, _ => true, } } diff --git a/runtime/moonbase/tests/integration_test.rs b/runtime/moonbase/tests/integration_test.rs index 20f1200381..b7f6e3fb16 100644 --- a/runtime/moonbase/tests/integration_test.rs +++ b/runtime/moonbase/tests/integration_test.rs @@ -30,8 +30,8 @@ use frame_support::{ assert_noop, assert_ok, dispatch::DispatchClass, traits::{ - fungible::Inspect, Currency as CurrencyT, EnsureOrigin, PalletInfo, StorageInfo, - StorageInfoTrait, + fungible::Inspect, Currency as CurrencyT, EnsureOrigin, OnInitialize, PalletInfo, + StorageInfo, StorageInfoTrait, }, weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight}, StorageHasher, Twox128, @@ -56,6 +56,7 @@ use moonbase_runtime::{ System, TransactionPayment, TransactionPaymentAsGasPrice, + Treasury, TreasuryCouncilCollective, XcmTransactor, FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX, @@ -2973,6 +2974,173 @@ fn validate_transaction_fails_on_filtered_call() { }); } +#[cfg(test)] +mod treasury_tests { + use super::*; + use sp_runtime::traits::Hash; + + fn expect_events(events: Vec) { + let block_events: Vec = + 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()); + Treasury::on_initialize(System::block_number()); + } + + #[test] + fn test_treasury_spend_local_with_root_origin() { + let initial_treasury_balance = 1_000 * UNIT; + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * UNIT), + (Treasury::account_id(), initial_treasury_balance), + ]) + .build() + .execute_with(|| { + let spend_amount = 100u128 * UNIT; + let spend_beneficiary = AccountId::from(BOB); + + next_block(); + + // Perform treasury spending + + assert_ok!(moonbase_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(5u32), + })) + )); + + let payout_period = + <::PayoutPeriod as Get>::get(); + let expected_events = [RuntimeEvent::Treasury( + pallet_treasury::Event::AssetSpendApproved { + index: 0, + asset_kind: (), + amount: spend_amount, + beneficiary: spend_beneficiary, + valid_from: 5u32, + expire_at: payout_period + 5u32, + }, + )] + .to_vec(); + expect_events(expected_events); + + while System::block_number() < 5u32 { + 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 * UNIT; + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * UNIT), + (Treasury::account_id(), initial_treasury_balance), + ]) + .build() + .execute_with(|| { + let spend_amount = 100u128 * UNIT; + let spend_beneficiary = AccountId::from(BOB); + + next_block(); + + // TreasuryCouncilCollective + assert_ok!(TreasuryCouncilCollective::set_members( + root_origin(), + vec![AccountId::from(ALICE)], + Some(AccountId::from(ALICE)), + 1 + )); + + next_block(); + + // Perform treasury spending + let proposal = RuntimeCall::Treasury(pallet_treasury::Call::spend { + amount: spend_amount, + asset_kind: Box::new(()), + beneficiary: Box::new(AccountId::from(BOB)), + valid_from: Some(5u32), + }); + assert_ok!(TreasuryCouncilCollective::propose( + origin_of(AccountId::from(ALICE)), + 1, + Box::new(proposal.clone()), + 1_000 + )); + + let payout_period = + <::PayoutPeriod as Get>::get(); + let expected_events = [ + RuntimeEvent::Treasury(pallet_treasury::Event::AssetSpendApproved { + index: 0, + asset_kind: (), + amount: spend_amount, + beneficiary: spend_beneficiary, + valid_from: 5u32, + expire_at: payout_period + 5u32, + }), + RuntimeEvent::TreasuryCouncilCollective(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() < 5u32 { + 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); + }); + } +} + #[cfg(test)] mod fee_tests { use super::*; diff --git a/runtime/moonbeam/src/lib.rs b/runtime/moonbeam/src/lib.rs index 74be2eeeec..a4892e32d0 100644 --- a/runtime/moonbeam/src/lib.rs +++ b/runtime/moonbeam/src/lib.rs @@ -546,9 +546,10 @@ parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const TreasuryId: PalletId = PalletId(*b"py/trsry"); pub TreasuryAccount: AccountId = Treasury::account_id(); + pub const MaxSpendBalance: crate::Balance = crate::Balance::max_value(); } -type TreasuryRejectOrigin = EitherOfDiverse< +type RootOrTreasuryCouncilOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionMoreThan, >; @@ -557,7 +558,7 @@ impl pallet_treasury::Config for Runtime { type PalletId = TreasuryId; type Currency = Balances; // More than half of the council is required (or root) to reject a proposal - type RejectOrigin = TreasuryRejectOrigin; + type RejectOrigin = RootOrTreasuryCouncilOrigin; type RuntimeEvent = RuntimeEvent; type SpendPeriod = ConstU32<{ 6 * DAYS }>; type Burn = (); @@ -565,11 +566,8 @@ impl pallet_treasury::Config for Runtime { type MaxApprovals = ConstU32<100>; type WeightInfo = moonbeam_weights::pallet_treasury::WeightInfo; type SpendFunds = (); - #[cfg(not(feature = "runtime-benchmarks"))] - type SpendOrigin = frame_support::traits::NeverEnsureOrigin; // Disabled, no spending - #[cfg(feature = "runtime-benchmarks")] type SpendOrigin = - frame_system::EnsureWithSuccess, AccountId, benches::MaxBalance>; + frame_system::EnsureWithSuccess; type AssetKind = (); type Beneficiary = AccountId; type BeneficiaryLookup = IdentityLookup; @@ -1193,12 +1191,6 @@ impl Contains for NormalFilter { // Note: It is also assumed that EVM calls are only allowed through `Origin::Root` so // this can be seen as an additional security RuntimeCall::EVM(_) => false, - RuntimeCall::Treasury( - pallet_treasury::Call::spend { .. } - | pallet_treasury::Call::payout { .. } - | pallet_treasury::Call::check_status { .. } - | pallet_treasury::Call::void_spend { .. }, - ) => false, _ => true, } } diff --git a/runtime/moonbeam/tests/integration_test.rs b/runtime/moonbeam/tests/integration_test.rs index 1e9bc5c73a..d910eb50ea 100644 --- a/runtime/moonbeam/tests/integration_test.rs +++ b/runtime/moonbeam/tests/integration_test.rs @@ -26,8 +26,8 @@ use frame_support::{ assert_noop, assert_ok, dispatch::DispatchClass, traits::{ - fungible::Inspect, Currency as CurrencyT, EnsureOrigin, PalletInfo, StorageInfo, - StorageInfoTrait, + fungible::Inspect, Currency as CurrencyT, EnsureOrigin, OnInitialize, PalletInfo, + StorageInfo, StorageInfoTrait, }, weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight}, StorageHasher, Twox128, @@ -40,7 +40,7 @@ use moonbeam_runtime::{ xcm_config::{CurrencyId, SelfReserve}, AccountId, Balances, CrowdloanRewards, Executive, OpenTechCommitteeCollective, ParachainStaking, PolkadotXcm, Precompiles, Runtime, RuntimeBlockWeights, RuntimeCall, - RuntimeEvent, System, TransactionPayment, TransactionPaymentAsGasPrice, + RuntimeEvent, System, TransactionPayment, TransactionPaymentAsGasPrice, Treasury, TreasuryCouncilCollective, XcmTransactor, FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX, WEEKS, }; use moonbeam_xcm_benchmarks::weights::XcmWeight; @@ -2765,6 +2765,106 @@ fn evm_success_keeps_substrate_events() { }); } +#[cfg(test)] +mod treasury_tests { + use super::*; + use sp_runtime::traits::Hash; + + fn expect_events(events: Vec) { + let block_events: Vec = + System::events().into_iter().map(|r| r.event).collect(); + + 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()); + Treasury::on_initialize(System::block_number()); + } + + #[test] + fn test_treasury_spend_local_with_council_origin() { + let initial_treasury_balance = 1_000 * GLMR; + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * GLMR), + (Treasury::account_id(), initial_treasury_balance), + ]) + .build() + .execute_with(|| { + let spend_amount = 100u128 * GLMR; + let spend_beneficiary = AccountId::from(BOB); + + next_block(); + + // TreasuryCouncilCollective + assert_ok!(TreasuryCouncilCollective::set_members( + root_origin(), + vec![AccountId::from(ALICE)], + Some(AccountId::from(ALICE)), + 1 + )); + + next_block(); + + // Perform treasury spending + let proposal = RuntimeCall::Treasury(pallet_treasury::Call::spend { + amount: spend_amount, + asset_kind: Box::new(()), + beneficiary: Box::new(AccountId::from(BOB)), + valid_from: Some(5u32), + }); + assert_ok!(TreasuryCouncilCollective::propose( + origin_of(AccountId::from(ALICE)), + 1, + Box::new(proposal.clone()), + 1_000 + )); + + let payout_period = + <::PayoutPeriod as Get>::get(); + let expected_events = [ + RuntimeEvent::Treasury(pallet_treasury::Event::AssetSpendApproved { + index: 0, + asset_kind: (), + amount: spend_amount, + beneficiary: spend_beneficiary, + valid_from: 5u32, + expire_at: payout_period + 5u32, + }), + RuntimeEvent::TreasuryCouncilCollective(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() < 5u32 { + 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); + }); + } +} + #[cfg(test)] mod fee_tests { use super::*; diff --git a/runtime/moonriver/src/lib.rs b/runtime/moonriver/src/lib.rs index f0e5062258..3c30809b75 100644 --- a/runtime/moonriver/src/lib.rs +++ b/runtime/moonriver/src/lib.rs @@ -551,9 +551,10 @@ parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const TreasuryId: PalletId = PalletId(*b"py/trsry"); pub TreasuryAccount: AccountId = Treasury::account_id(); + pub const MaxSpendBalance: crate::Balance = crate::Balance::max_value(); } -type TreasuryRejectOrigin = EitherOfDiverse< +type RootOrTreasuryCouncilOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionMoreThan, >; @@ -562,7 +563,7 @@ impl pallet_treasury::Config for Runtime { type PalletId = TreasuryId; type Currency = Balances; // More than half of the council is required (or root) to reject a proposal - type RejectOrigin = TreasuryRejectOrigin; + type RejectOrigin = RootOrTreasuryCouncilOrigin; type RuntimeEvent = RuntimeEvent; type SpendPeriod = ConstU32<{ 6 * DAYS }>; type Burn = (); @@ -570,11 +571,8 @@ impl pallet_treasury::Config for Runtime { type MaxApprovals = ConstU32<100>; type WeightInfo = moonriver_weights::pallet_treasury::WeightInfo; type SpendFunds = (); - #[cfg(not(feature = "runtime-benchmarks"))] - type SpendOrigin = frame_support::traits::NeverEnsureOrigin; // Disabled, no spending - #[cfg(feature = "runtime-benchmarks")] type SpendOrigin = - frame_system::EnsureWithSuccess, AccountId, benches::MaxBalance>; + frame_system::EnsureWithSuccess; type AssetKind = (); type Beneficiary = AccountId; type BeneficiaryLookup = IdentityLookup; @@ -1201,12 +1199,6 @@ impl Contains for NormalFilter { // Note: It is also assumed that EVM calls are only allowed through `Origin::Root` so // this can be seen as an additional security RuntimeCall::EVM(_) => false, - RuntimeCall::Treasury( - pallet_treasury::Call::spend { .. } - | pallet_treasury::Call::payout { .. } - | pallet_treasury::Call::check_status { .. } - | pallet_treasury::Call::void_spend { .. }, - ) => false, _ => true, } } diff --git a/runtime/moonriver/tests/integration_test.rs b/runtime/moonriver/tests/integration_test.rs index 0109796f46..26a1cd61f6 100644 --- a/runtime/moonriver/tests/integration_test.rs +++ b/runtime/moonriver/tests/integration_test.rs @@ -26,7 +26,10 @@ use frame_support::traits::fungible::Inspect; use frame_support::{ assert_noop, assert_ok, dispatch::DispatchClass, - traits::{Currency as CurrencyT, EnsureOrigin, PalletInfo, StorageInfo, StorageInfoTrait}, + traits::{ + Currency as CurrencyT, EnsureOrigin, OnInitialize, PalletInfo, StorageInfo, + StorageInfoTrait, + }, weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight}, StorageHasher, Twox128, }; @@ -38,7 +41,7 @@ use moonriver_runtime::{ asset_config::ForeignAssetInstance, xcm_config::{CurrencyId, SelfReserve}, AssetId, Balances, CrowdloanRewards, Executive, OpenTechCommitteeCollective, PolkadotXcm, - Precompiles, RuntimeBlockWeights, TransactionPayment, TransactionPaymentAsGasPrice, + Precompiles, RuntimeBlockWeights, TransactionPayment, TransactionPaymentAsGasPrice, Treasury, TreasuryCouncilCollective, XcmTransactor, FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX, WEEKS, }; use nimbus_primitives::NimbusId; @@ -2663,6 +2666,106 @@ fn evm_success_keeps_substrate_events() { }); } +#[cfg(test)] +mod treasury_tests { + use super::*; + use sp_runtime::traits::Hash; + + fn expect_events(events: Vec) { + let block_events: Vec = + System::events().into_iter().map(|r| r.event).collect(); + + 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()); + Treasury::on_initialize(System::block_number()); + } + + #[test] + fn test_treasury_spend_local_with_council_origin() { + let initial_treasury_balance = 1_000 * MOVR; + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MOVR), + (Treasury::account_id(), initial_treasury_balance), + ]) + .build() + .execute_with(|| { + let spend_amount = 100u128 * MOVR; + let spend_beneficiary = AccountId::from(BOB); + + next_block(); + + // TreasuryCouncilCollective + assert_ok!(TreasuryCouncilCollective::set_members( + root_origin(), + vec![AccountId::from(ALICE)], + Some(AccountId::from(ALICE)), + 1 + )); + + next_block(); + + // Perform treasury spending + let proposal = RuntimeCall::Treasury(pallet_treasury::Call::spend { + amount: spend_amount, + asset_kind: Box::new(()), + beneficiary: Box::new(AccountId::from(BOB)), + valid_from: Some(5u32), + }); + assert_ok!(TreasuryCouncilCollective::propose( + origin_of(AccountId::from(ALICE)), + 1, + Box::new(proposal.clone()), + 1_000 + )); + + let payout_period = + <::PayoutPeriod as Get>::get(); + let expected_events = [ + RuntimeEvent::Treasury(pallet_treasury::Event::AssetSpendApproved { + index: 0, + asset_kind: (), + amount: spend_amount, + beneficiary: spend_beneficiary, + valid_from: 5u32, + expire_at: payout_period + 5u32, + }), + RuntimeEvent::TreasuryCouncilCollective(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() < 5u32 { + 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); + }); + } +} + #[cfg(test)] mod fee_tests { use super::*; diff --git a/test/suites/dev/moonbase/test-treasury/test-treasury-council-origin.ts b/test/suites/dev/moonbase/test-treasury/test-treasury-council-origin.ts new file mode 100644 index 0000000000..e5105bb583 --- /dev/null +++ b/test/suites/dev/moonbase/test-treasury/test-treasury-council-origin.ts @@ -0,0 +1,108 @@ +import "@moonbeam-network/api-augment"; +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import { alith, baltathar, ethan } from "@moonwall/util"; +import type { FrameSupportPalletId } from "@polkadot/types/lookup"; + +describeSuite({ + id: "D013802", + title: "Treasury pallet spend_local call (Council Origin)", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let treasuryPalletId: FrameSupportPalletId; + let treasuryAddress: string; + let api: ApiPromise; + + beforeAll(async function () { + api = context.polkadotJs(); + treasuryPalletId = api.consts.treasury.palletId; + treasuryAddress = `0x6d6f646C${treasuryPalletId.toString().slice(2)}0000000000000000`; + }); + + it({ + id: "T03", + title: "Members of the treasury council can spend treasury funds", + test: async function () { + // Set Alith as member of the treasury council + const setMembersTX = api.tx.treasuryCouncilCollective.setMembers( + [alith.address, baltathar.address], + alith.address, + 3 + ); + await context.createBlock(await api.tx.sudo.sudo(setMembersTX).signAsync(alith), { + allowFailures: false, + }); + + // Fund treasury + const treasuryPot = 2_000_000_000_000_000n; + await context.createBlock( + await api.tx.balances.transferAllowDeath(treasuryAddress, treasuryPot), + { allowFailures: false } + ); + await context.createBlock(); + expect((await api.query.treasury.deactivated()).toBigInt()).toBeGreaterThan(treasuryPot); + + // Pre-checks + expect((await api.query.treasury.proposalCount()).toNumber()).to.equal(0); + expect((await api.query.treasury.approvals()).length).to.equal(0); + + // Approve treasury spend to Ethan + const proposal_value = 1_000_000_000_000_000n; + const tx = api.tx.treasury.spend(null, proposal_value, ethan.address, null); + const signedTx = api.tx.treasuryCouncilCollective.propose(2, tx, 1_000).signAsync(alith); + const blockResult = await context.createBlock(signedTx, { + allowFailures: false, + expectEvents: [api.events.treasuryCouncilCollective.Proposed], + }); + + const councilProposalHash = blockResult + .result!.events.find(({ event: { method } }) => method.toString() === "Proposed")! + .event.data[2].toHex(); + + await context.createBlock( + api.tx.treasuryCouncilCollective.vote(councilProposalHash, 0, true).signAsync(alith), + { + allowFailures: false, + expectEvents: [api.events.treasuryCouncilCollective.Voted], + } + ); + + await context.createBlock( + api.tx.treasuryCouncilCollective.vote(councilProposalHash, 0, true).signAsync(baltathar), + { + allowFailures: false, + expectEvents: [api.events.treasuryCouncilCollective.Voted], + } + ); + + await context.createBlock( + api.tx.treasuryCouncilCollective + .close( + councilProposalHash, + 0, + { + refTime: 50_000_000_000, + proofSize: 100_000, + }, + 1_000 + ) + .signAsync(alith), + { + expectEvents: [ + api.events.treasuryCouncilCollective.Closed, + api.events.treasury.AssetSpendApproved, + ], + } + ); + + // Spending was successfully submitted + expect((await api.query.treasury.spendCount()).toNumber()).to.equal(1); + + await context.createBlock(await api.tx.treasury.payout(0).signAsync(ethan), { + allowFailures: false, + expectEvents: [api.events.treasury.Paid], + }); + }, + }); + }, +}); diff --git a/test/suites/dev/moonbase/test-treasury/test-treasury-pallet.ts b/test/suites/dev/moonbase/test-treasury/test-treasury-pallet.ts deleted file mode 100644 index 8cf3725692..0000000000 --- a/test/suites/dev/moonbase/test-treasury/test-treasury-pallet.ts +++ /dev/null @@ -1,51 +0,0 @@ -import "@moonbeam-network/api-augment"; -import { beforeAll, describeSuite, expect } from "@moonwall/cli"; -import type { ApiPromise } from "@polkadot/api"; -import { alith, baltathar, ethan } from "@moonwall/util"; - -describeSuite({ - id: "D013801", - title: "Treasury pallet tests", - foundationMethods: "dev", - testCases: ({ context, log, it }) => { - let api: ApiPromise; - - beforeAll(async function () { - api = context.polkadotJs(); - }); - - it({ - id: "T01", - title: "Non root cannot spend (local)", - test: async function () { - expect((await api.query.treasury.spendCount()).toNumber()).to.equal(0); - - // Creates a proposal - const proposal_value = 1000000000n; - const tx = api.tx.treasury.spendLocal(proposal_value, ethan.address); - const signedTx = await tx.signAsync(baltathar); - await context.createBlock([signedTx]); - - expect((await api.query.treasury.spendCount()).toNumber()).to.equal(0); - }, - }); - - it({ - id: "T02", - title: "Root should be able to spend (local) and approve a proposal", - test: async function () { - expect((await api.query.treasury.spendCount()).toNumber()).to.equal(0); - // Creates a proposal - // Value needs to be higher than the transaction fee paid by ethan, - // but lower than the total treasury pot - const proposal_value = 1000000000n; - const tx = api.tx.treasury.spendLocal(proposal_value, ethan.address); - const signedTx = await api.tx.sudo.sudo(tx).signAsync(alith); - await context.createBlock([signedTx]); - - // Local spends dont upadte the spend count - expect((await api.query.treasury.spendCount()).toNumber()).to.equal(0); - }, - }); - }, -}); diff --git a/test/suites/dev/moonbase/test-treasury/test-treasury-root-origin.ts b/test/suites/dev/moonbase/test-treasury/test-treasury-root-origin.ts new file mode 100644 index 0000000000..c0eafa4c01 --- /dev/null +++ b/test/suites/dev/moonbase/test-treasury/test-treasury-root-origin.ts @@ -0,0 +1,72 @@ +import "@moonbeam-network/api-augment"; +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import { alith, baltathar, ethan } from "@moonwall/util"; +import type { FrameSupportPalletId } from "@polkadot/types/lookup"; + +describeSuite({ + id: "D013801", + title: "Treasury pallet spend_local call (Root Origin)", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let treasuryPalletId: FrameSupportPalletId; + let treasuryAddress: string; + let api: ApiPromise; + + beforeAll(async function () { + api = context.polkadotJs(); + treasuryPalletId = api.consts.treasury.palletId; + treasuryAddress = `0x6d6f646C${treasuryPalletId.toString().slice(2)}0000000000000000`; + }); + + it({ + id: "T01", + title: "Origins that are not Root or members of treasury council cannot spend treasury funds", + test: async function () { + const proposal_value = 1000000000n; + const tx = api.tx.treasury.spend(null, proposal_value, ethan.address, null); + const signedTx = await tx.signAsync(baltathar); + await context.createBlock(signedTx, { + expectEvents: [api.events.system.ExtrinsicFailed], + }); + + expect((await api.query.treasury.spendCount()).toNumber()).to.equal(0); + }, + }); + + it({ + id: "T02", + title: "Root can spend treasury funds", + test: async function () { + // Fund treasury + const treasuryPot = 2_000_000_000_000_000n; + await context.createBlock( + await api.tx.balances.transferAllowDeath(treasuryAddress, treasuryPot), + { allowFailures: false } + ); + await context.createBlock(); + expect((await api.query.treasury.deactivated()).toBigInt()).toBeGreaterThan(treasuryPot); + + // Pre-checks + expect((await api.query.treasury.spendCount()).toNumber()).to.equal(0); + + // Approve treasury spend to Ethan + const proposal_value = 1_000_000_000_000_000n; + const tx = api.tx.treasury.spend(null, proposal_value, ethan.address, null); + const signedTx = await api.tx.sudo.sudo(tx).signAsync(alith); + await context.createBlock(signedTx, { + allowFailures: false, + expectEvents: [api.events.treasury.AssetSpendApproved], + }); + + // Spending was successfully submitted + expect((await api.query.treasury.spendCount()).toNumber()).to.equal(1); + + await context.createBlock(await api.tx.treasury.payout(0).signAsync(ethan), { + allowFailures: false, + expectEvents: [api.events.treasury.Paid], + }); + }, + }); + }, +});