//! Safe-address regressions use real AccountId32 encodings; legacy upstream //! tests retain their original u64 runtime and golden behavior. #![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] use super::*; use crate as pallet_multisig; use frame::{deps::sp_runtime::AccountId32, prelude::*, runtime::prelude::*, testing_prelude::*}; type Block = frame_system::mocking::MockBlockU32; construct_runtime!( pub struct Test { System: frame_system, Balances: pallet_balances, Multisig: pallet_multisig, } ); #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { type Block = Block; type AccountId = AccountId32; type Lookup = frame::deps::sp_runtime::traits::IdentityLookup; type AccountData = pallet_balances::AccountData; } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for Test { type ReserveIdentifier = [u8; 8]; type AccountStore = System; } parameter_types! { pub static DepositBase: u64 = 1; pub static DepositFactor: u64 = 1; // A small nonzero test weight checks that refunds retain the derivation charge. pub static DerivationWeight: Weight = Weight::from_parts(123, 456); } impl Config for Test { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; type Currency = Balances; type DepositBase = DepositBase; type DepositFactor = DepositFactor; type MaxSignatories = ConstU32<4>; type WeightInfo = (); type SafeDerivationWeight = DerivationWeight; type BlockNumberProvider = System; } fn account(value: u8) -> AccountId32 { AccountId32::new([value; 32]) } fn members() -> Vec { vec![account(1), account(2), account(3)] } pub fn new_test_ext() -> TestState { let mut storage = frame_system::GenesisConfig::::default() .build_storage() .unwrap(); pallet_balances::GenesisConfig:: { balances: (1..=5).map(|n| (account(n), 100)).collect(), ..Default::default() } .assimilate_storage(&mut storage) .unwrap(); let mut ext = TestState::new(storage); ext.execute_with(|| { DepositBase::set(1); DepositFactor::set(1); DerivationWeight::set(Weight::from_parts(123, 456)); System::set_block_number(1); }); ext } fn transfer(amount: u64) -> Box { Box::new(RuntimeCall::Balances( pallet_balances::Call::transfer_allow_death { dest: account(9), value: amount, }, )) } #[test] fn safe_full_width_address_matches_shared_helper_and_legacy_is_unchanged() { let signatories = members(); let legacy = Multisig::multi_account_id(&signatories, 2); assert_eq!( legacy, AccountId32::new([ 63, 253, 32, 235, 151, 186, 253, 111, 90, 248, 208, 38, 205, 17, 16, 31, 21, 83, 250, 156, 180, 184, 163, 125, 23, 33, 49, 79, 187, 180, 143, 197 ]) ); assert_eq!( legacy, AccountId32::new(blake2_256( &(b"modlpy/utilisuba", &signatories, 2u16).encode() )) ); let safe = Multisig::safe_multi_account_id(&signatories, 2).unwrap(); assert_eq!( safe, AccountId32::new([ 172, 65, 87, 122, 63, 188, 134, 21, 202, 232, 106, 72, 162, 86, 241, 135, 139, 225, 234, 107, 76, 89, 48, 29, 190, 28, 238, 185, 3, 36, 96, 46 ]) ); assert_ne!(safe, legacy); let expected = subtensor_hashed::safe::safe_derived_account( subtensor_hashed::safe::MULTISIG_DOMAIN, &(signatories.as_slice(), 2u16), ) .unwrap(); assert_eq!(safe, AccountId32::new(expected)); assert!(subtensor_hashed::mldsa::rejects_legacy_public_keys( safe.as_ref() )); // Upstream's u64 mock must never silently truncate a supposedly safe account. assert_eq!( crate::Pallet::::safe_multi_account_id(&[1, 2, 3], 2), Err(crate::Error::::InvalidSafeAccountId.into()) ); } #[test] fn every_legacy_call_encoding_remains_the_original_index_and_fields() { let others = vec![account(2), account(3)]; let when = Timepoint { height: 7u32, index: 2, }; let hash = [9; 32]; let weight = Weight::from_parts(17, 23); let call = transfer(11); let pairs = [ ( Call::::as_multi_threshold_1 { other_signatories: others.clone(), call: call.clone(), } .encode(), (0u8, &others, &call).encode(), ), ( Call::::as_multi { threshold: 2, other_signatories: others.clone(), maybe_timepoint: Some(when), call: call.clone(), max_weight: weight, } .encode(), (1u8, 2u16, &others, Some(when), &call, weight).encode(), ), ( Call::::approve_as_multi { threshold: 2, other_signatories: others.clone(), maybe_timepoint: Some(when), call_hash: hash, max_weight: weight, } .encode(), (2u8, 2u16, &others, Some(when), hash, weight).encode(), ), ( Call::::cancel_as_multi { threshold: 2, other_signatories: others.clone(), timepoint: when, call_hash: hash, } .encode(), (3u8, 2u16, &others, when, hash).encode(), ), ( Call::::poke_deposit { threshold: 2, other_signatories: others.clone(), call_hash: hash, } .encode(), (4u8, 2u16, &others, hash).encode(), ), ]; for (actual, expected) in pairs { assert_eq!(actual, expected); } } #[test] fn safe_three_of_three_requires_full_threshold_and_refunds_only_after_execution() { new_test_ext().execute_with(|| { let safe = Multisig::safe_multi_account_id(&members(), 3).unwrap(); assert_ok!(Balances::transfer_allow_death( RuntimeOrigin::signed(account(4)), safe.clone(), 30 )); let call = transfer(30); let hash = blake2_256(&call.encode()); let when = Multisig::timepoint(); let opened = Multisig::as_multi_safe( RuntimeOrigin::signed(account(1)), 3, vec![account(2), account(3)], None, call.clone(), Weight::MAX, ) .unwrap(); assert_eq!( opened.actual_weight, Some( <() as WeightInfo>::as_multi_create(2, call.encoded_size() as u32) .saturating_add(DerivationWeight::get()) ) ); assert_eq!(Balances::reserved_balance(account(1)), 4); assert_ok!(Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(2)), 3, vec![account(1), account(3)], Some(when), hash, Weight::zero() )); assert_eq!(Balances::free_balance(account(9)), 0); assert_eq!( Multisigs::::get(&safe, hash).unwrap().approvals.len(), 2 ); assert_noop!( Multisig::as_multi_safe( RuntimeOrigin::signed(account(2)), 3, vec![account(1), account(3)], Some(when), call.clone(), Weight::MAX ), Error::::AlreadyApproved ); assert_ok!(Multisig::as_multi_safe( RuntimeOrigin::signed(account(3)), 3, vec![account(1), account(2)], Some(when), call, Weight::MAX )); assert_eq!(Balances::free_balance(account(9)), 30); assert_eq!(Balances::reserved_balance(account(1)), 0); assert!(!Multisigs::::contains_key(safe, hash)); }); } #[test] fn legacy_approvals_cancel_and_poke_cannot_touch_safe_operation() { new_test_ext().execute_with(|| { let safe = Multisig::safe_multi_account_id(&members(), 2).unwrap(); let legacy = Multisig::multi_account_id(&members(), 2); let call = transfer(10); let hash = blake2_256(&call.encode()); let when = Multisig::timepoint(); assert_ok!(Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], None, hash, Weight::zero() )); assert_noop!( Multisig::cancel_as_multi( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], when, hash ), Error::::NotFound ); assert_noop!( Multisig::poke_deposit( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], hash ), Error::::NotFound ); assert_ok!(Multisig::approve_as_multi( RuntimeOrigin::signed(account(2)), 2, vec![account(1), account(3)], None, hash, Weight::zero() )); assert_eq!( Multisigs::::get(&safe, hash) .unwrap() .approvals .as_slice(), &[account(1)] ); assert_eq!( Multisigs::::get(&legacy, hash) .unwrap() .approvals .as_slice(), &[account(2)] ); assert_noop!( Multisig::cancel_as_multi_safe( RuntimeOrigin::signed(account(2)), 2, vec![account(1), account(3)], when, hash ), Error::::NotOwner ); assert_ok!(Multisig::cancel_as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], when, hash )); assert_eq!(Balances::reserved_balance(account(1)), 0); assert!(Multisigs::::contains_key(legacy, hash)); }); } #[test] fn safe_deposit_adjustments_and_cancellation_preserve_depositor_rules() { new_test_ext().execute_with(|| { let safe = Multisig::safe_multi_account_id(&members(), 2).unwrap(); let hash = [1; 32]; assert_ok!(Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], None, hash, Weight::zero() )); DepositBase::set(5); assert_noop!( Multisig::poke_deposit_safe( RuntimeOrigin::signed(account(2)), 2, vec![account(1), account(3)], hash ), Error::::NotOwner ); assert_ok!(Multisig::poke_deposit_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], hash )); assert_eq!(Balances::reserved_balance(account(1)), 7); assert_eq!(Multisigs::::get(&safe, hash).unwrap().deposit, 7); DepositBase::set(1); assert_ok!(Multisig::poke_deposit_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], hash )); assert_eq!(Balances::reserved_balance(account(1)), 3); assert_ok!(Multisig::cancel_as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], Multisig::timepoint(), hash )); assert_eq!(Balances::reserved_balance(account(1)), 0); }); } #[test] fn safe_threshold_one_uses_safe_origin_and_reports_inner_failure() { new_test_ext().execute_with(|| { let safe = Multisig::safe_multi_account_id(&members(), 1).unwrap(); assert_ok!(Balances::transfer_allow_death( RuntimeOrigin::signed(account(4)), safe.clone(), 20 )); assert_ok!(Multisig::as_multi_threshold_1_safe( RuntimeOrigin::signed(account(1)), vec![account(2), account(3)], transfer(20) )); assert_eq!(Balances::free_balance(account(9)), 20); assert!(Multisig::as_multi_threshold_1_safe( RuntimeOrigin::signed(account(2)), vec![account(1), account(3)], transfer(1) ) .is_err()); assert_eq!(Balances::reserved_balance(account(1)), 0); assert_eq!(Multisigs::::iter().count(), 0); }); } #[test] fn safe_completion_checks_inner_weight_and_reports_failed_dispatch_before_refund() { new_test_ext().execute_with(|| { let safe = Multisig::safe_multi_account_id(&members(), 2).unwrap(); let call = transfer(10); let hash = blake2_256(&call.encode()); let when = Multisig::timepoint(); assert_ok!(Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], None, hash, Weight::zero(), )); assert_noop!( Multisig::as_multi_safe( RuntimeOrigin::signed(account(2)), 2, vec![account(1), account(3)], Some(when), call.clone(), Weight::zero(), ), Error::::MaxWeightTooLow ); assert_eq!(Balances::reserved_balance(account(1)), 3); assert_eq!( Multisigs::::get(&safe, hash) .unwrap() .approvals .as_slice(), &[account(1)] ); assert_ok!(Multisig::as_multi_safe( RuntimeOrigin::signed(account(2)), 2, vec![account(1), account(3)], Some(when), call, Weight::MAX, )); assert!(matches!( System::events().last().unwrap().event, RuntimeEvent::Multisig(Event::MultisigExecuted { result: Err(_), .. }) )); assert_eq!(Balances::free_balance(account(9)), 0); assert_eq!(Balances::reserved_balance(account(1)), 0); assert!(!Multisigs::::contains_key(safe, hash)); }); } #[test] fn malformed_manifests_and_wrong_timepoint_leave_state_unchanged() { new_test_ext().execute_with(|| { for (threshold, others, expected) in [ ( 4, vec![account(2), account(3)], Error::::ThresholdTooHigh, ), ( 2, vec![account(3), account(2)], Error::::SignatoriesOutOfOrder, ), ( 2, vec![account(2), account(2)], Error::::SignatoriesOutOfOrder, ), ( 2, vec![account(1), account(2)], Error::::SenderInSignatories, ), (2, vec![], Error::::TooFewSignatories), ] { assert_noop!( Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(1)), threshold, others, None, [0; 32], Weight::zero() ), expected ); } assert_eq!(Balances::reserved_balance(account(1)), 0); assert_eq!(Multisigs::::iter().count(), 0); assert_ok!(Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], None, [0; 32], Weight::zero() )); assert_noop!( Multisig::approve_as_multi_safe( RuntimeOrigin::signed(account(2)), 2, vec![account(1), account(3)], Some(Timepoint { height: 2, index: 0 }), [0; 32], Weight::zero() ), Error::::WrongTimepoint ); }); } #[test] fn maximum_derivation_weight_cannot_be_refunded_away() { new_test_ext().execute_with(|| { DerivationWeight::set(Weight::MAX); let call = Call::::as_multi_safe { threshold: 2, other_signatories: vec![account(2), account(3)], maybe_timepoint: None, call: transfer(1), max_weight: Weight::zero(), }; assert_eq!(call.get_dispatch_info().call_weight, Weight::MAX); let result = Multisig::as_multi_safe( RuntimeOrigin::signed(account(1)), 2, vec![account(2), account(3)], None, transfer(1), Weight::zero(), ) .unwrap(); assert_eq!(result.actual_weight, Some(Weight::MAX)); }); }