// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Treasury pallet tests. #![cfg(test)] use sp_core::H256; use sp_runtime::{ traits::{BadOrigin, BlakeTwo256, IdentityLookup}, BuildStorage, Perbill, Permill, }; use sp_storage::Storage; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, storage::StoragePrefixedMap, traits::{ tokens::{PayFromAccount, UnityAssetBalanceConversion}, ConstU32, ConstU64, IntegrityTest, SortedMembers, StorageVersion, }, PalletId, }; use super::*; use crate::{self as pallet_tips, Event as TipEvent}; type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test { System: frame_system, Balances: pallet_balances, Treasury: pallet_treasury, Treasury1: pallet_treasury::, Tips: pallet_tips, Tips1: pallet_tips::, } ); parameter_types! { pub const AvailableBlockRatio: Perbill = Perbill::one(); } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { type AccountId = u128; // u64 is not enough to hold bytes used to generate bounty account type Lookup = IdentityLookup; type Block = Block; type AccountData = pallet_balances::AccountData; } impl pallet_balances::Config for Test { type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type Balance = u64; type RuntimeEvent = RuntimeEvent; type DustRemoval = (); type ExistentialDeposit = ConstU64<1>; type AccountStore = System; type WeightInfo = (); type FreezeIdentifier = (); type MaxFreezes = (); type RuntimeHoldReason = (); type RuntimeFreezeReason = (); } parameter_types! { static TenToFourteenTestValue: Vec = vec![10,11,12,13,14]; } pub struct TenToFourteen; impl SortedMembers for TenToFourteen { fn sorted_members() -> Vec { TenToFourteenTestValue::get().clone() } #[cfg(feature = "runtime-benchmarks")] fn add(new: &u128) { TenToFourteenTestValue::mutate(|members| { members.push(*new); members.sort(); }) } } impl ContainsLengthBound for TenToFourteen { fn max_len() -> usize { TenToFourteenTestValue::get().len() } fn min_len() -> usize { 0 } } parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const TreasuryPalletId2: PalletId = PalletId(*b"py/trsr2"); pub TreasuryAccount: u128 = Treasury::account_id(); pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet; type ApproveOrigin = frame_system::EnsureRoot; type RejectOrigin = frame_system::EnsureRoot; type RuntimeEvent = RuntimeEvent; type OnSlash = (); type ProposalBond = ProposalBond; type ProposalBondMinimum = ConstU64<1>; type ProposalBondMaximum = (); type SpendPeriod = ConstU64<2>; type Burn = Burn; type BurnDestination = (); // Just gets burned. type WeightInfo = (); type SpendFunds = (); type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_support::traits::NeverEnsureOrigin; type AssetKind = (); type Beneficiary = Self::AccountId; type BeneficiaryLookup = IdentityLookup; type Paymaster = PayFromAccount; type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId2; type Currency = pallet_balances::Pallet; type ApproveOrigin = frame_system::EnsureRoot; type RejectOrigin = frame_system::EnsureRoot; type RuntimeEvent = RuntimeEvent; type OnSlash = (); type ProposalBond = ProposalBond; type ProposalBondMinimum = ConstU64<1>; type ProposalBondMaximum = (); type SpendPeriod = ConstU64<2>; type Burn = Burn; type BurnDestination = (); // Just gets burned. type WeightInfo = (); type SpendFunds = (); type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_support::traits::NeverEnsureOrigin; type AssetKind = (); type Beneficiary = Self::AccountId; type BeneficiaryLookup = IdentityLookup; type Paymaster = PayFromAccount; type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } parameter_types! { pub const TipFindersFee: Percent = Percent::from_percent(20); pub static TipReportDepositBase: u64 = 1; } impl Config for Test { type MaximumReasonLength = ConstU32<16384>; type Tippers = TenToFourteen; type TipCountdown = ConstU64<1>; type TipFindersFee = TipFindersFee; type TipReportDepositBase = TipReportDepositBase; type DataDepositPerByte = ConstU64<1>; type MaxTipAmount = ConstU64<10_000_000>; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); } impl Config for Test { type MaximumReasonLength = ConstU32<16384>; type Tippers = TenToFourteen; type TipCountdown = ConstU64<1>; type TipFindersFee = TipFindersFee; type TipReportDepositBase = TipReportDepositBase; type DataDepositPerByte = ConstU64<1>; type MaxTipAmount = ConstU64<10_000_000>; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); } pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { system: frame_system::GenesisConfig::default(), balances: pallet_balances::GenesisConfig { balances: vec![(0, 100), (1, 98), (2, 1)] }, treasury: Default::default(), treasury_1: Default::default(), } .build_storage() .unwrap() .into(); ext.execute_with(|| System::set_block_number(1)); ext } /// Run the function pointer inside externalities and asserts the try_state hook at the end. pub fn build_and_execute(test: impl FnOnce() -> ()) { new_test_ext().execute_with(|| { test(); Tips::do_try_state().expect("All invariants must hold after a test"); }); } fn last_event() -> TipEvent { System::events() .into_iter() .map(|r| r.event) .filter_map(|e| if let RuntimeEvent::Tips(inner) = e { Some(inner) } else { None }) .last() .unwrap() } #[test] fn genesis_config_works() { build_and_execute(|| { assert_eq!(Treasury::pot(), 0); assert_eq!(Treasury::proposal_count(), 0); }); } fn tip_hash() -> H256 { BlakeTwo256::hash_of(&(BlakeTwo256::hash(b"awesome.dot"), 3u128)) } #[test] fn tip_new_cannot_be_used_twice() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::tip_new(RuntimeOrigin::signed(10), b"awesome.dot".to_vec(), 3, 10)); assert_noop!( Tips::tip_new(RuntimeOrigin::signed(11), b"awesome.dot".to_vec(), 3, 10), Error::::AlreadyKnown ); }); } #[test] fn report_awesome_and_tip_works() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 3)); assert_eq!(Balances::reserved_balance(0), 12); assert_eq!(Balances::free_balance(0), 88); // other reports don't count. assert_noop!( Tips::report_awesome(RuntimeOrigin::signed(1), b"awesome.dot".to_vec(), 3), Error::::AlreadyKnown ); let h = tip_hash(); assert_ok!(Tips::tip(RuntimeOrigin::signed(10), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 10)); assert_noop!(Tips::tip(RuntimeOrigin::signed(9), h, 10), BadOrigin); System::set_block_number(2); assert_ok!(Tips::close_tip(RuntimeOrigin::signed(100), h.into())); assert_eq!(Balances::reserved_balance(0), 0); assert_eq!(Balances::free_balance(0), 102); assert_eq!(Balances::free_balance(3), 8); }); } #[test] fn report_awesome_from_beneficiary_and_tip_works() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 0)); assert_eq!(Balances::reserved_balance(0), 12); assert_eq!(Balances::free_balance(0), 88); let h = BlakeTwo256::hash_of(&(BlakeTwo256::hash(b"awesome.dot"), 0u128)); assert_ok!(Tips::tip(RuntimeOrigin::signed(10), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 10)); System::set_block_number(2); assert_ok!(Tips::close_tip(RuntimeOrigin::signed(100), h.into())); assert_eq!(Balances::reserved_balance(0), 0); assert_eq!(Balances::free_balance(0), 110); }); } #[test] fn close_tip_works() { build_and_execute(|| { System::set_block_number(1); Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Treasury::pot(), 100); assert_ok!(Tips::tip_new(RuntimeOrigin::signed(10), b"awesome.dot".to_vec(), 3, 10)); let h = tip_hash(); assert_eq!(last_event(), TipEvent::NewTip { tip_hash: h }); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10)); assert_noop!(Tips::close_tip(RuntimeOrigin::signed(0), h.into()), Error::::StillOpen); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 10)); assert_eq!(last_event(), TipEvent::TipClosing { tip_hash: h }); assert_noop!(Tips::close_tip(RuntimeOrigin::signed(0), h.into()), Error::::Premature); System::set_block_number(2); assert_noop!(Tips::close_tip(RuntimeOrigin::none(), h.into()), BadOrigin); assert_ok!(Tips::close_tip(RuntimeOrigin::signed(0), h.into())); assert_eq!(Balances::free_balance(3), 10); assert_eq!(last_event(), TipEvent::TipClosed { tip_hash: h, who: 3, payout: 10 }); assert_noop!( Tips::close_tip(RuntimeOrigin::signed(100), h.into()), Error::::UnknownTip ); }); } #[test] fn slash_tip_works() { build_and_execute(|| { System::set_block_number(1); Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Treasury::pot(), 100); assert_eq!(Balances::reserved_balance(0), 0); assert_eq!(Balances::free_balance(0), 100); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 3)); assert_eq!(Balances::reserved_balance(0), 12); assert_eq!(Balances::free_balance(0), 88); let h = tip_hash(); assert_eq!(last_event(), TipEvent::NewTip { tip_hash: h }); // can't remove from any origin assert_noop!(Tips::slash_tip(RuntimeOrigin::signed(0), h), BadOrigin); // can remove from root. assert_ok!(Tips::slash_tip(RuntimeOrigin::root(), h)); assert_eq!(last_event(), TipEvent::TipSlashed { tip_hash: h, finder: 0, deposit: 12 }); // tipper slashed assert_eq!(Balances::reserved_balance(0), 0); assert_eq!(Balances::free_balance(0), 88); }); } #[test] fn retract_tip_works() { build_and_execute(|| { // with report awesome Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 3)); let h = tip_hash(); assert_ok!(Tips::tip(RuntimeOrigin::signed(10), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 10)); assert_noop!(Tips::retract_tip(RuntimeOrigin::signed(10), h), Error::::NotFinder); assert_ok!(Tips::retract_tip(RuntimeOrigin::signed(0), h)); System::set_block_number(2); assert_noop!( Tips::close_tip(RuntimeOrigin::signed(0), h.into()), Error::::UnknownTip ); // with tip new Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::tip_new(RuntimeOrigin::signed(10), b"awesome.dot".to_vec(), 3, 10)); let h = tip_hash(); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 10)); assert_noop!(Tips::retract_tip(RuntimeOrigin::signed(0), h), Error::::NotFinder); assert_ok!(Tips::retract_tip(RuntimeOrigin::signed(10), h)); System::set_block_number(2); assert_noop!( Tips::close_tip(RuntimeOrigin::signed(10), h.into()), Error::::UnknownTip ); }); } #[test] fn tip_median_calculation_works() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::tip_new(RuntimeOrigin::signed(10), b"awesome.dot".to_vec(), 3, 0)); let h = tip_hash(); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 1000000)); System::set_block_number(2); assert_ok!(Tips::close_tip(RuntimeOrigin::signed(0), h.into())); assert_eq!(Balances::free_balance(3), 10); }); } #[test] fn tip_large_should_fail() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::tip_new(RuntimeOrigin::signed(10), b"awesome.dot".to_vec(), 3, 0)); let h = tip_hash(); assert_noop!( Tips::tip( RuntimeOrigin::signed(12), h, <::MaxTipAmount as Get>::get() + 1 ), Error::::MaxTipAmountExceeded ); }); } #[test] fn tip_changing_works() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::tip_new(RuntimeOrigin::signed(10), b"awesome.dot".to_vec(), 3, 10000)); let h = tip_hash(); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 10000)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 10000)); assert_ok!(Tips::tip(RuntimeOrigin::signed(13), h, 0)); assert_ok!(Tips::tip(RuntimeOrigin::signed(14), h, 0)); assert_ok!(Tips::tip(RuntimeOrigin::signed(12), h, 1000)); assert_ok!(Tips::tip(RuntimeOrigin::signed(11), h, 100)); assert_ok!(Tips::tip(RuntimeOrigin::signed(10), h, 10)); System::set_block_number(2); assert_ok!(Tips::close_tip(RuntimeOrigin::signed(0), h.into())); assert_eq!(Balances::free_balance(3), 10); }); } #[test] fn test_last_reward_migration() { let mut s = Storage::default(); #[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug)] pub struct OldOpenTip< AccountId: Parameter, Balance: Parameter, BlockNumber: Parameter, Hash: Parameter, > { /// The hash of the reason for the tip. The reason should be a human-readable UTF-8 encoded /// string. A URL would be sensible. reason: Hash, /// The account to be tipped. who: AccountId, /// The account who began this tip and the amount held on deposit. finder: Option<(AccountId, Balance)>, /// The block number at which this tip will close if `Some`. If `None`, then no closing is /// scheduled. closes: Option, /// The members who have voted for this tip. Sorted by AccountId. tips: Vec<(AccountId, Balance)>, } let reason1 = BlakeTwo256::hash(b"reason1"); let hash1 = BlakeTwo256::hash_of(&(reason1, 10u64)); let old_tip_finder = OldOpenTip:: { reason: reason1, who: 10, finder: Some((20, 30)), closes: Some(13), tips: vec![(40, 50), (60, 70)], }; let reason2 = BlakeTwo256::hash(b"reason2"); let hash2 = BlakeTwo256::hash_of(&(reason2, 20u64)); let old_tip_no_finder = OldOpenTip:: { reason: reason2, who: 20, finder: None, closes: Some(13), tips: vec![(40, 50), (60, 70)], }; let data = vec![ (pallet_tips::Tips::::hashed_key_for(hash1), old_tip_finder.encode().to_vec()), (pallet_tips::Tips::::hashed_key_for(hash2), old_tip_no_finder.encode().to_vec()), ]; s.top = data.into_iter().collect(); sp_io::TestExternalities::new(s).execute_with(|| { let module = pallet_tips::Tips::::pallet_prefix(); let item = pallet_tips::Tips::::storage_prefix(); Tips::migrate_retract_tip_for_tip_new(module, item); // Test w/ finder assert_eq!( pallet_tips::Tips::::get(hash1), Some(OpenTip { reason: reason1, who: 10, finder: 20, deposit: 30, closes: Some(13), tips: vec![(40, 50), (60, 70)], finders_fee: true, }) ); // Test w/o finder assert_eq!( pallet_tips::Tips::::get(hash2), Some(OpenTip { reason: reason2, who: 20, finder: Default::default(), deposit: 0, closes: Some(13), tips: vec![(40, 50), (60, 70)], finders_fee: false, }) ); }); } #[test] fn test_migration_v4() { let reason1 = BlakeTwo256::hash(b"reason1"); let hash1 = BlakeTwo256::hash_of(&(reason1, 10u64)); let tip = OpenTip:: { reason: reason1, who: 10, finder: 20, deposit: 30, closes: Some(13), tips: vec![(40, 50), (60, 70)], finders_fee: true, }; let data = vec![ (pallet_tips::Reasons::::hashed_key_for(hash1), reason1.encode().to_vec()), (pallet_tips::Tips::::hashed_key_for(hash1), tip.encode().to_vec()), ]; let mut s = Storage::default(); s.top = data.into_iter().collect(); sp_io::TestExternalities::new(s).execute_with(|| { use frame_support::traits::PalletInfoAccess; let old_pallet = "Treasury"; let new_pallet = ::name(); frame_support::storage::migration::move_pallet( new_pallet.as_bytes(), old_pallet.as_bytes(), ); StorageVersion::new(0).put::(); crate::migrations::v4::pre_migrate::(old_pallet); crate::migrations::v4::migrate::(old_pallet); crate::migrations::v4::post_migrate::(old_pallet); }); sp_io::TestExternalities::new(Storage::default()).execute_with(|| { use frame_support::traits::PalletInfoAccess; let old_pallet = "Treasury"; let new_pallet = ::name(); frame_support::storage::migration::move_pallet( new_pallet.as_bytes(), old_pallet.as_bytes(), ); StorageVersion::new(0).put::(); crate::migrations::v4::pre_migrate::(old_pallet); crate::migrations::v4::migrate::(old_pallet); crate::migrations::v4::post_migrate::(old_pallet); }); } #[test] fn genesis_funding_works() { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); let initial_funding = 100; pallet_balances::GenesisConfig:: { // Total issuance will be 200 with treasury account initialized with 100. balances: vec![(0, 100), (Treasury::account_id(), initial_funding)], } .assimilate_storage(&mut t) .unwrap(); pallet_treasury::GenesisConfig::::default() .assimilate_storage(&mut t) .unwrap(); let mut t: sp_io::TestExternalities = t.into(); t.execute_with(|| { assert_eq!(Balances::free_balance(Treasury::account_id()), initial_funding); assert_eq!(Treasury::pot(), initial_funding - Balances::minimum_balance()); }); } #[test] fn report_awesome_and_tip_works_second_instance() { build_and_execute(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); Balances::make_free_balance_be(&Treasury1::account_id(), 201); assert_eq!(Balances::free_balance(&Treasury::account_id()), 101); assert_eq!(Balances::free_balance(&Treasury1::account_id()), 201); assert_ok!(Tips1::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 3)); // duplicate report in tips1 reports don't count. assert_noop!( Tips1::report_awesome(RuntimeOrigin::signed(1), b"awesome.dot".to_vec(), 3), Error::::AlreadyKnown ); // but tips is separate assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 3)); let h = tip_hash(); assert_ok!(Tips1::tip(RuntimeOrigin::signed(10), h, 10)); assert_ok!(Tips1::tip(RuntimeOrigin::signed(11), h, 10)); assert_ok!(Tips1::tip(RuntimeOrigin::signed(12), h, 10)); assert_noop!(Tips1::tip(RuntimeOrigin::signed(9), h, 10), BadOrigin); System::set_block_number(2); assert_ok!(Tips1::close_tip(RuntimeOrigin::signed(100), h.into())); // Treasury 1 unchanged assert_eq!(Balances::free_balance(&Treasury::account_id()), 101); // Treasury 2 gave the funds assert_eq!(Balances::free_balance(&Treasury1::account_id()), 191); }); } #[test] fn equal_entries_invariant() { new_test_ext().execute_with(|| { use frame_support::pallet_prelude::DispatchError::Other; Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 3)); let reason1 = BlakeTwo256::hash(b"reason1"); let hash1 = BlakeTwo256::hash_of(&(reason1, 10u64)); let tip = OpenTip:: { reason: reason1, who: 10, finder: 20, deposit: 30, closes: Some(13), tips: vec![(40, 50), (60, 70)], finders_fee: true, }; // Breaks invariant by adding an entry to only `Tips` Storage. pallet_tips::Tips::::insert(hash1, tip); // Invariant violated assert_eq!( Tips::do_try_state(), Err(Other("Equal length of entries in `Tips` and `Reasons` Storage")) ); }) } #[test] fn finders_fee_invariant() { new_test_ext().execute_with(|| { use frame_support::pallet_prelude::DispatchError::Other; // Breaks invariant by having a zero deposit. TipReportDepositBase::set(0); Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"".to_vec(), 3)); // Invariant violated assert_eq!( Tips::do_try_state(), Err(Other("Tips with `finders_fee` should have non-zero `deposit`.")) ); }) } #[test] fn reasons_invariant() { new_test_ext().execute_with(|| { use frame_support::pallet_prelude::DispatchError::Other; Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!(Tips::report_awesome(RuntimeOrigin::signed(0), b"awesome.dot".to_vec(), 0)); let hash: Vec<_> = pallet_tips::Tips::::iter_keys().collect(); let mut open_tip = pallet_tips::Tips::::take(hash[0]).unwrap(); // Breaks invariant by changing value `open_tip.reason` in `Tips` Storage. open_tip.reason = ::Hashing::hash(&b"".to_vec()); pallet_tips::Tips::::insert(hash[0], open_tip); // Invariant violated assert_eq!(Tips::do_try_state(), Err(Other("no reason for this tip"))); }) } #[test] #[should_panic = "`TipReportDepositBase` should not be zero"] fn zero_base_deposit_prohibited() { new_test_ext().execute_with(|| { TipReportDepositBase::set(0); Tips::integrity_test(); }); }