Lines
100 %
Functions
Branches
// Copyright 2019-2025 PureStake Inc.
// This file is part of Moonbeam.
// Moonbeam 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.
// Moonbeam 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 Moonbeam. If not, see <http://www.gnu.org/licenses/>.
use crate::{
assert_event_emitted, hash, log_closed, log_executed, log_proposed, log_voted,
mock::{ExtBuilder, PCall, Precompiles, PrecompilesValue, Runtime, RuntimeOrigin},
};
use frame_support::{assert_ok, instances::Instance1};
use parity_scale_codec::Encode;
use precompile_utils::{solidity::codec::Address, testing::*};
use sp_core::{H160, H256};
use sp_runtime::DispatchError;
fn precompiles() -> Precompiles<Runtime> {
PrecompilesValue::get()
}
#[test]
fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() {
check_precompile_implements_solidity_interfaces(&["Collective.sol"], PCall::supports_selector)
fn selector_less_than_four_bytes() {
ExtBuilder::default().build().execute_with(|| {
// This selector is only three bytes long when four are required.
precompiles()
.prepare_test(Alice, Precompile1, vec![1u8, 2u8, 3u8])
.execute_reverts(|output| output == b"Tried to read selector out of bounds");
});
fn no_selector_exists_but_length_is_right() {
.prepare_test(Alice, Precompile1, vec![1u8, 2u8, 3u8, 4u8])
.execute_reverts(|output| output == b"Unknown selector");
fn selectors() {
assert!(PCall::execute_selectors().contains(&0x09c5eabe));
assert!(PCall::propose_selectors().contains(&0xc57f3260));
assert!(PCall::vote_selectors().contains(&0x73e37688));
assert!(PCall::close_selectors().contains(&0x638d9d47));
assert!(PCall::proposal_hash_selectors().contains(&0xfc379417));
assert!(PCall::proposals_selectors().contains(&0x55ef20e6));
assert!(PCall::members_selectors().contains(&0xbdd4d18d));
assert!(PCall::is_member_selectors().contains(&0xa230c524));
assert!(PCall::prime_selectors().contains(&0xc7ee005e));
fn modifiers() {
ExtBuilder::default()
.with_balances(vec![(Alice.into(), 1000)])
.build()
.execute_with(|| {
let mut tester = PrecompilesModifierTester::new(precompiles(), Alice, Precompile1);
tester.test_default_modifier(PCall::execute_selectors());
tester.test_default_modifier(PCall::propose_selectors());
tester.test_default_modifier(PCall::vote_selectors());
tester.test_default_modifier(PCall::close_selectors());
tester.test_view_modifier(PCall::proposal_hash_selectors());
tester.test_view_modifier(PCall::proposals_selectors());
tester.test_view_modifier(PCall::members_selectors());
tester.test_view_modifier(PCall::is_member_selectors());
tester.test_view_modifier(PCall::prime_selectors());
fn non_member_cannot_propose() {
let proposal = pallet_treasury::Call::<Runtime>::spend_local {
amount: 1,
beneficiary: Alice.into(),
let proposal: <Runtime as frame_system::Config>::RuntimeCall = proposal.into();
let proposal = proposal.encode();
.prepare_test(
Alice,
Precompile1,
PCall::propose {
threshold: 1,
proposal: proposal.into(),
},
)
.expect_no_logs()
.execute_reverts(|output| output.ends_with(b"NotMember\") })"));
fn non_member_cannot_vote() {
PCall::vote {
proposal_hash: H256::zero(),
proposal_index: 1,
approve: false,
fn non_member_cannot_execute() {
PCall::execute {
fn cannot_vote_for_unknown_proposal() {
Bob,
.execute_reverts(|output| output.ends_with(b"ProposalMissing\") })"));
fn cannot_close_unknown_proposal() {
PCall::close {
proposal_weight_bound: 0,
length_bound: 0,
fn member_can_make_instant_proposal() {
let proposal_hash: H256 = hash::<Runtime>(&proposal);
// Proposal is executed. The proposal call will itself fail but it
// still counts as a success according to pallet_collective.
.expect_log(log_executed(Precompile1, proposal_hash))
.execute_returns(0u32);
assert_event_emitted!(pallet_collective::Event::Executed {
proposal_hash,
result: Err(DispatchError::BadOrigin)
.into());
fn member_can_make_delayed_proposal() {
threshold: 2,
.expect_log(log_proposed(Precompile1, Bob, 0, proposal_hash, 2))
assert_event_emitted!(pallet_collective::Event::Proposed {
account: Bob.into(),
proposal_index: 0,
fn member_can_vote_on_proposal() {
Charlie,
approve: true,
.expect_log(log_voted(Precompile1, Charlie, proposal_hash, true))
.execute_returns(());
assert_event_emitted!(pallet_collective::Event::Voted {
account: Charlie.into(),
voted: true,
yes: 1,
no: 0,
fn cannot_close_if_not_enough_votes() {
let length_bound = proposal.len() as u32;
proposal_weight_bound: 10_000_000,
length_bound,
.execute_reverts(|output| output.ends_with(b"TooEarly\") })"));
fn can_close_execute_if_enough_votes() {
.expect_log(log_voted(Precompile1, Bob, proposal_hash, true))
proposal_weight_bound: 200_000_000,
.execute_returns(true);
assert_event_emitted!(pallet_collective::Event::Closed {
yes: 2,
assert_event_emitted!(pallet_collective::Event::Approved { proposal_hash }.into());
result: Ok(())
assert_event_emitted!(pallet_treasury::Event::SpendApproved {
fn can_close_refuse_if_enough_votes() {
.expect_log(log_voted(Precompile1, Bob, proposal_hash, false))
.expect_log(log_voted(Precompile1, Charlie, proposal_hash, false))
proposal_weight_bound: 100_000_000,
.expect_log(log_closed(Precompile1, proposal_hash))
.execute_returns(false);
yes: 0,
no: 2,
assert_event_emitted!(pallet_collective::Event::Disapproved { proposal_hash }.into());
fn multiple_propose_increase_index() {
amount: 2,
.expect_log(log_proposed(Precompile1, Bob, 1, proposal_hash, 2))
.execute_returns(1u32);
fn view_members() {
.prepare_test(Bob, Precompile1, PCall::members {})
.execute_returns(vec![Address(Bob.into()), Address(Charlie.into())]);
fn view_no_prime() {
.prepare_test(Bob, Precompile1, PCall::prime {})
.execute_returns(Address(H160::zero()));
fn view_some_prime() {
assert_ok!(pallet_collective::Pallet::<
Runtime,
pallet_collective::Instance1,
>::set_members(
RuntimeOrigin::root(),
vec![Alice.into(), Bob.into()],
Some(Alice.into()),
2
));
.execute_returns(Address(Alice.into()));
fn view_is_member() {
PCall::is_member {
account: Address(Bob.into()),
account: Address(Alice.into()),
mod bounded_proposal_decode {
use super::*;
use crate::GetProposalLimit;
use precompile_utils::prelude::BoundedBytes;
fn scenario<F>(nesting: usize, call: F)
where
F: FnOnce(BoundedBytes<GetProposalLimit>) -> PCall,
{
// Some random call.
let mut proposal = pallet_collective::Call::<Runtime, Instance1>::set_members {
new_members: Vec::new(),
prime: None,
old_count: 0,
// Nest it.
for _ in 0..nesting {
proposal = pallet_collective::Call::<Runtime, Instance1>::propose {
threshold: 10,
proposal: Box::new(proposal.into()),
length_bound: 1,
.prepare_test(Alice, Precompile1, call(proposal.into()))
.execute_reverts(|output| {
if nesting < 8 {
output.ends_with(b"NotMember\") })")
} else {
output == b"proposal: Failed to decode proposal"
fn proposal_above_bound() {
scenario(8, |proposal| PCall::propose {
proposal,
fn proposal_below_bound() {
scenario(7, |proposal| PCall::propose {
fn execute_above_bound() {
scenario(8, |proposal| PCall::execute { proposal });
fn execute_below_bound() {
scenario(7, |proposal| PCall::execute { proposal });