// 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. use super::*; use crate::{mock::*, Event}; use frame_support::{assert_err, assert_noop, assert_ok, assert_storage_noop}; use pallet_balances::Event as BEvent; use sp_runtime::{ bounded_btree_map, traits::{BadOrigin, Dispatchable}, FixedU128, }; macro_rules! unbonding_pools_with_era { ($($k:expr => $v:expr),* $(,)?) => {{ use ::core::iter::{Iterator, IntoIterator}; let not_bounded: BTreeMap<_, _> = Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*])); BoundedBTreeMap::, TotalUnbondingPools>::try_from(not_bounded).unwrap() }}; } macro_rules! member_unbonding_eras { ($( $any:tt )*) => {{ let x: BoundedBTreeMap = bounded_btree_map!($( $any )*); x }}; } pub const DEFAULT_ROLES: PoolRoles = PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), bouncer: Some(902) }; fn deposit_rewards(r: u128) { let b = Currency::free_balance(&default_reward_account()).checked_add(r).unwrap(); Currency::set_balance(&default_reward_account(), b); } fn remove_rewards(r: u128) { let b = Currency::free_balance(&default_reward_account()).checked_sub(r).unwrap(); Currency::set_balance(&default_reward_account(), b); } #[test] fn test_setup_works() { ExtBuilder::default().build_and_execute(|| { assert_eq!(BondedPools::::count(), 1); assert_eq!(RewardPools::::count(), 1); assert_eq!(SubPoolsStorage::::count(), 0); assert_eq!(PoolMembers::::count(), 1); assert_eq!(StakingMock::bonding_duration(), 3); assert!(Metadata::::contains_key(1)); // initial member. assert_eq!(TotalValueLocked::::get(), 10); let last_pool = LastPoolId::::get(); assert_eq!( BondedPool::::get(last_pool).unwrap(), BondedPool:: { id: last_pool, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 10, roles: DEFAULT_ROLES, state: PoolState::Open, }, } ); assert_eq!( RewardPools::::get(last_pool).unwrap(), RewardPool:: { last_recorded_reward_counter: Zero::zero(), last_recorded_total_payouts: 0, total_rewards_claimed: 0, total_commission_claimed: 0, total_commission_pending: 0, } ); assert_eq!( PoolMembers::::get(10).unwrap(), PoolMember:: { pool_id: last_pool, points: 10, ..Default::default() } ); let bonded_account = Pools::create_bonded_account(last_pool); let reward_account = Pools::create_reward_account(last_pool); // the bonded_account should be bonded by the depositor's funds. assert_eq!(StakingMock::active_stake(&bonded_account).unwrap(), 10); assert_eq!(StakingMock::total_stake(&bonded_account).unwrap(), 10); // but not nominating yet. assert!(Nominations::get().is_none()); // reward account should have an initial ED in it. assert_eq!(Currency::free_balance(&reward_account), Currency::minimum_balance()); }) } mod bonded_pool { use super::*; #[test] fn balance_to_point_works() { ExtBuilder::default().build_and_execute(|| { let mut bonded_pool = BondedPool:: { id: 123123, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 100, roles: DEFAULT_ROLES, state: PoolState::Open, }, }; // 1 points : 1 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); assert_eq!(bonded_pool.balance_to_point(10), 10); assert_eq!(bonded_pool.balance_to_point(0), 0); // 2 points : 1 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 50); assert_eq!(bonded_pool.balance_to_point(10), 20); // 1 points : 2 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); bonded_pool.points = 50; assert_eq!(bonded_pool.balance_to_point(10), 5); // 100 points : 0 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 0); bonded_pool.points = 100; assert_eq!(bonded_pool.balance_to_point(10), 100 * 10); // 0 points : 100 balance StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); bonded_pool.points = 0; assert_eq!(bonded_pool.balance_to_point(10), 10); // 10 points : 3 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 30); bonded_pool.points = 100; assert_eq!(bonded_pool.balance_to_point(10), 33); // 2 points : 3 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 300); bonded_pool.points = 200; assert_eq!(bonded_pool.balance_to_point(10), 6); // 4 points : 9 balance ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 900); bonded_pool.points = 400; assert_eq!(bonded_pool.balance_to_point(90), 40); }) } #[test] fn points_to_balance_works() { ExtBuilder::default().build_and_execute(|| { // 1 balance : 1 points ratio let mut bonded_pool = BondedPool:: { id: 123123, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 100, roles: DEFAULT_ROLES, state: PoolState::Open, }, }; StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); assert_eq!(bonded_pool.points_to_balance(10), 10); assert_eq!(bonded_pool.points_to_balance(0), 0); // 2 balance : 1 points ratio bonded_pool.points = 50; assert_eq!(bonded_pool.points_to_balance(10), 20); // 100 balance : 0 points ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); bonded_pool.points = 0; assert_eq!(bonded_pool.points_to_balance(10), 0); // 0 balance : 100 points ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 0); bonded_pool.points = 100; assert_eq!(bonded_pool.points_to_balance(10), 0); // 10 balance : 3 points ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 100); bonded_pool.points = 30; assert_eq!(bonded_pool.points_to_balance(10), 33); // 2 balance : 3 points ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 200); bonded_pool.points = 300; assert_eq!(bonded_pool.points_to_balance(10), 6); // 4 balance : 9 points ratio StakingMock::set_bonded_balance(bonded_pool.bonded_account(), 400); bonded_pool.points = 900; assert_eq!(bonded_pool.points_to_balance(90), 40); }) } #[test] fn api_points_to_balance_works() { ExtBuilder::default().build_and_execute(|| { assert!(BondedPool::::get(1).is_some()); assert_eq!(Pallet::::api_points_to_balance(1, 10), 10); // slash half of the pool's balance. expected result of `fn api_points_to_balance` // to be 1/2 of the pool's balance. StakingMock::slash_by(1, Pools::depositor_min_bond() / 2); assert_eq!(Pallet::::api_points_to_balance(1, 10), 5); // if pool does not exist, points to balance ratio is 0. assert_eq!(BondedPool::::get(2), None); assert_eq!(Pallet::::api_points_to_balance(2, 10), 0); }) } #[test] fn api_balance_to_points_works() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Pallet::::api_balance_to_points(1, 0), 0); assert_eq!(Pallet::::api_balance_to_points(1, 10), 10); // slash half of the pool's balance. expect result of `fn api_balance_to_points` // to be 2 * of the balance to add to the pool. StakingMock::slash_by(1, Pools::depositor_min_bond() / 2); assert_eq!(Pallet::::api_balance_to_points(1, 10), 20); // if pool does not exist, balance to points ratio is 0. assert_eq!(BondedPool::::get(2), None); assert_eq!(Pallet::::api_points_to_balance(2, 10), 0); }) } #[test] fn ok_to_join_with_works() { ExtBuilder::default().build_and_execute(|| { let pool = BondedPool:: { id: 123, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 100, roles: DEFAULT_ROLES, state: PoolState::Open, }, }; let max_points_to_balance: u128 = <::MaxPointsToBalance as Get>::get().into(); // Simulate a 100% slashed pool StakingMock::set_bonded_balance(pool.bonded_account(), 0); assert_noop!(pool.ok_to_join(), Error::::OverflowRisk); // Simulate a slashed pool at `MaxPointsToBalance` + 1 slashed pool StakingMock::set_bonded_balance( pool.bonded_account(), max_points_to_balance.saturating_add(1), ); assert_ok!(pool.ok_to_join()); // Simulate a slashed pool at `MaxPointsToBalance` StakingMock::set_bonded_balance(pool.bonded_account(), max_points_to_balance); assert_noop!(pool.ok_to_join(), Error::::OverflowRisk); StakingMock::set_bonded_balance( pool.bonded_account(), Balance::MAX / max_points_to_balance, ); // and a sanity check StakingMock::set_bonded_balance( pool.bonded_account(), Balance::MAX / max_points_to_balance - 1, ); assert_ok!(pool.ok_to_join()); }); } #[test] fn points_and_balance_conversions_are_safe() { ExtBuilder::default().build_and_execute(|| { let bonded_pool = BondedPool:: { id: 123123, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: u128::MAX, roles: DEFAULT_ROLES, state: PoolState::Open, }, }; StakingMock::set_bonded_balance(bonded_pool.bonded_account(), u128::MAX); // Max out the points and balance of the pool and make sure the conversion works as // expected and does not overflow. assert_eq!(bonded_pool.balance_to_point(u128::MAX), u128::MAX); assert_eq!(bonded_pool.points_to_balance(u128::MAX), u128::MAX); }) } } mod reward_pool { use super::*; use crate::mock::RewardImbalance::{Deficit, Surplus}; #[test] fn ed_change_causes_reward_deficit() { ExtBuilder::default().max_members_per_pool(Some(5)).build_and_execute(|| { // original ED ExistentialDeposit::set(5); // 11 joins the pool Currency::set_balance(&11, 500); assert_ok!(Pools::join(RuntimeOrigin::signed(11), 90, 1)); // new delegator does not have any pending rewards assert_eq!(pending_rewards_for_delegator(11), 0); // give the pool some rewards deposit_rewards(100); // all existing delegator has pending rewards assert_eq!(pending_rewards_for_delegator(11), 90); assert_eq!(pending_rewards_for_delegator(10), 10); assert_eq!(reward_imbalance(1), Surplus(0)); // 12 joins the pool. Currency::set_balance(&12, 500); assert_ok!(Pools::join(RuntimeOrigin::signed(12), 100, 1)); // Current reward balance is committed to last recorded reward counter of // the pool before the increase in ED. let bonded_pool = BondedPools::::get(1).unwrap(); let reward_pool = RewardPools::::get(1).unwrap(); assert_eq!( reward_pool.last_recorded_reward_counter, reward_pool .current_reward_counter(1, bonded_pool.points, Perbill::zero()) .unwrap() .0 ); // reward pool before ED increase and reward counter getting committed. let reward_pool_1 = RewardPools::::get(1).unwrap(); // increase ED from 5 to 50 ExistentialDeposit::set(50); // There is now an expected deficit of ed_diff assert_eq!(reward_imbalance(1), Deficit(45)); // 13 joins the pool which commits the reward counter to reward pool. Currency::set_balance(&13, 500); assert_ok!(Pools::join(RuntimeOrigin::signed(13), 100, 1)); // still a deficit assert_eq!(reward_imbalance(1), Deficit(45)); // reward pool after ED increase let reward_pool_2 = RewardPools::::get(1).unwrap(); // last recorded total payout does not decrease even as ED increases. assert_eq!( reward_pool_1.last_recorded_total_payouts, reward_pool_2.last_recorded_total_payouts ); // Topping up pool decreases deficit deposit_rewards(10); assert_eq!(reward_imbalance(1), Deficit(35)); // top up the pool to remove the deficit deposit_rewards(35); // No deficit anymore assert_eq!(reward_imbalance(1), Surplus(0)); // fix the ed deficit assert_ok!(Currency::mint_into(&10, 45)); assert_ok!(Pools::adjust_pool_deposit(RuntimeOrigin::signed(10), 1)); }); } #[test] fn ed_adjust_fixes_reward_deficit() { ExtBuilder::default().max_members_per_pool(Some(5)).build_and_execute(|| { // Given: pool has a reward deficit // original ED ExistentialDeposit::set(5); // 11 joins the pool Currency::set_balance(&11, 500); assert_ok!(Pools::join(RuntimeOrigin::signed(11), 90, 1)); // Pool some rewards deposit_rewards(100); // 12 joins the pool. Currency::set_balance(&12, 500); assert_ok!(Pools::join(RuntimeOrigin::signed(12), 10, 1)); // When: pool ends up in reward deficit // increase ED ExistentialDeposit::set(50); assert_eq!(reward_imbalance(1), Deficit(45)); // clear events pool_events_since_last_call(); // Then: Anyone can permissionlessly can adjust ED deposit. // make sure caller has enough funds.. assert_ok!(Currency::mint_into(&99, 100)); let pre_balance = Currency::free_balance(&99); // adjust ED assert_ok!(Pools::adjust_pool_deposit(RuntimeOrigin::signed(99), 1)); // depositor's balance should decrease by 45 assert_eq!(Currency::free_balance(&99), pre_balance - 45); assert_eq!(reward_imbalance(1), Surplus(0)); assert_eq!( pool_events_since_last_call(), vec![Event::MinBalanceDeficitAdjusted { pool_id: 1, amount: 45 },] ); // Trying to top up again does not work assert_err!( Pools::adjust_pool_deposit(RuntimeOrigin::signed(10), 1), Error::::NothingToAdjust ); // When: ED is decreased and reward account has excess ED frozen ExistentialDeposit::set(5); // And:: adjust ED deposit is called let pre_balance = Currency::free_balance(&100); assert_ok!(Pools::adjust_pool_deposit(RuntimeOrigin::signed(100), 1)); // Then: excess ED is claimed by the caller assert_eq!(Currency::free_balance(&100), pre_balance + 45); assert_eq!( pool_events_since_last_call(), vec![Event::MinBalanceExcessAdjusted { pool_id: 1, amount: 45 },] ); }); } #[test] fn topping_up_does_not_work_for_pools_with_no_deficit() { ExtBuilder::default().max_members_per_pool(Some(5)).build_and_execute(|| { // 11 joins the pool Currency::set_balance(&11, 500); assert_ok!(Pools::join(RuntimeOrigin::signed(11), 90, 1)); // Pool some rewards deposit_rewards(100); assert_eq!(reward_imbalance(1), Surplus(0)); // Topping up fails assert_err!( Pools::adjust_pool_deposit(RuntimeOrigin::signed(10), 1), Error::::NothingToAdjust ); }); } } mod unbond_pool { use super::*; #[test] fn points_to_issue_works() { ExtBuilder::default().build_and_execute(|| { // 1 points : 1 balance ratio let unbond_pool = UnbondPool:: { points: 100, balance: 100 }; assert_eq!(unbond_pool.balance_to_point(10), 10); assert_eq!(unbond_pool.balance_to_point(0), 0); // 2 points : 1 balance ratio let unbond_pool = UnbondPool:: { points: 100, balance: 50 }; assert_eq!(unbond_pool.balance_to_point(10), 20); // 1 points : 2 balance ratio let unbond_pool = UnbondPool:: { points: 50, balance: 100 }; assert_eq!(unbond_pool.balance_to_point(10), 5); // 100 points : 0 balance ratio let unbond_pool = UnbondPool:: { points: 100, balance: 0 }; assert_eq!(unbond_pool.balance_to_point(10), 100 * 10); // 0 points : 100 balance let unbond_pool = UnbondPool:: { points: 0, balance: 100 }; assert_eq!(unbond_pool.balance_to_point(10), 10); // 10 points : 3 balance ratio let unbond_pool = UnbondPool:: { points: 100, balance: 30 }; assert_eq!(unbond_pool.balance_to_point(10), 33); // 2 points : 3 balance ratio let unbond_pool = UnbondPool:: { points: 200, balance: 300 }; assert_eq!(unbond_pool.balance_to_point(10), 6); // 4 points : 9 balance ratio let unbond_pool = UnbondPool:: { points: 400, balance: 900 }; assert_eq!(unbond_pool.balance_to_point(90), 40); }) } #[test] fn balance_to_unbond_works() { // 1 balance : 1 points ratio let unbond_pool = UnbondPool:: { points: 100, balance: 100 }; assert_eq!(unbond_pool.point_to_balance(10), 10); assert_eq!(unbond_pool.point_to_balance(0), 0); // 1 balance : 2 points ratio let unbond_pool = UnbondPool:: { points: 100, balance: 50 }; assert_eq!(unbond_pool.point_to_balance(10), 5); // 2 balance : 1 points ratio let unbond_pool = UnbondPool:: { points: 50, balance: 100 }; assert_eq!(unbond_pool.point_to_balance(10), 20); // 100 balance : 0 points ratio let unbond_pool = UnbondPool:: { points: 0, balance: 100 }; assert_eq!(unbond_pool.point_to_balance(10), 0); // 0 balance : 100 points ratio let unbond_pool = UnbondPool:: { points: 100, balance: 0 }; assert_eq!(unbond_pool.point_to_balance(10), 0); // 10 balance : 3 points ratio let unbond_pool = UnbondPool:: { points: 30, balance: 100 }; assert_eq!(unbond_pool.point_to_balance(10), 33); // 2 balance : 3 points ratio let unbond_pool = UnbondPool:: { points: 300, balance: 200 }; assert_eq!(unbond_pool.point_to_balance(10), 6); // 4 balance : 9 points ratio let unbond_pool = UnbondPool:: { points: 900, balance: 400 }; assert_eq!(unbond_pool.point_to_balance(90), 40); } } mod sub_pools { use super::*; #[test] fn maybe_merge_pools_works() { ExtBuilder::default().build_and_execute(|| { assert_eq!(TotalUnbondingPools::::get(), 5); assert_eq!(BondingDuration::get(), 3); assert_eq!(PostUnbondingPoolsWindow::get(), 2); // Given let mut sub_pool_0 = SubPools:: { no_era: UnbondPool::::default(), with_era: unbonding_pools_with_era! { 0 => UnbondPool:: { points: 10, balance: 10 }, 1 => UnbondPool:: { points: 10, balance: 10 }, 2 => UnbondPool:: { points: 20, balance: 20 }, 3 => UnbondPool:: { points: 30, balance: 30 }, 4 => UnbondPool:: { points: 40, balance: 40 }, }, }; // When `current_era < TotalUnbondingPools`, let sub_pool_1 = sub_pool_0.clone().maybe_merge_pools(0); // Then it exits early without modifications assert_eq!(sub_pool_1, sub_pool_0); // When `current_era == TotalUnbondingPools`, let sub_pool_1 = sub_pool_1.maybe_merge_pools(1); // Then it exits early without modifications assert_eq!(sub_pool_1, sub_pool_0); // When `current_era - TotalUnbondingPools == 0`, let mut sub_pool_1 = sub_pool_1.maybe_merge_pools(2); // Then era 0 is merged into the `no_era` pool sub_pool_0.no_era = sub_pool_0.with_era.remove(&0).unwrap(); assert_eq!(sub_pool_1, sub_pool_0); // Given we have entries for era 1..=5 sub_pool_1 .with_era .try_insert(5, UnbondPool:: { points: 50, balance: 50 }) .unwrap(); sub_pool_0 .with_era .try_insert(5, UnbondPool:: { points: 50, balance: 50 }) .unwrap(); // When `current_era - TotalUnbondingPools == 1` let sub_pool_2 = sub_pool_1.maybe_merge_pools(3); let era_1_pool = sub_pool_0.with_era.remove(&1).unwrap(); // Then era 1 is merged into the `no_era` pool sub_pool_0.no_era.points += era_1_pool.points; sub_pool_0.no_era.balance += era_1_pool.balance; assert_eq!(sub_pool_2, sub_pool_0); // When `current_era - TotalUnbondingPools == 5`, so all pools with era <= 4 are removed let sub_pool_3 = sub_pool_2.maybe_merge_pools(7); // Then all eras <= 5 are merged into the `no_era` pool for era in 2..=5 { let to_merge = sub_pool_0.with_era.remove(&era).unwrap(); sub_pool_0.no_era.points += to_merge.points; sub_pool_0.no_era.balance += to_merge.balance; } assert_eq!(sub_pool_3, sub_pool_0); }); } } mod join { use sp_runtime::TokenError; use super::*; #[test] fn join_works() { let bonded = |points, member_counter| BondedPool:: { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter, points, roles: DEFAULT_ROLES, state: PoolState::Open, }, }; ExtBuilder::default().with_check(0).build_and_execute(|| { // Given Currency::set_balance(&11, ExistentialDeposit::get() + 2); assert!(!PoolMembers::::contains_key(11)); assert_eq!(TotalValueLocked::::get(), 10); // When assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1)); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, ] ); assert_eq!(TotalValueLocked::::get(), 12); assert_eq!( PoolMembers::::get(11).unwrap(), PoolMember:: { pool_id: 1, points: 2, ..Default::default() } ); assert_eq!(BondedPool::::get(1).unwrap(), bonded(12, 2)); // Given // The bonded balance is slashed in half StakingMock::slash_by(1, 6); // And Currency::set_balance(&12, ExistentialDeposit::get() + 12); assert!(!PoolMembers::::contains_key(12)); // When assert_ok!(Pools::join(RuntimeOrigin::signed(12), 12, 1)); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::PoolSlashed { pool_id: 1, balance: 6 }, Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true } ] ); assert_eq!(TotalValueLocked::::get(), 18); assert_eq!( PoolMembers::::get(12).unwrap(), PoolMember:: { pool_id: 1, points: 24, ..Default::default() } ); assert_eq!(BondedPool::::get(1).unwrap(), bonded(12 + 24, 3)); }); } #[test] fn join_errors_correctly() { ExtBuilder::default().with_check(0).build_and_execute(|| { // 10 is already part of the default pool created. assert_eq!(PoolMembers::::get(10).unwrap().pool_id, 1); assert_noop!( Pools::join(RuntimeOrigin::signed(10), 420, 123), Error::::AccountBelongsToOtherPool ); assert_noop!( Pools::join(RuntimeOrigin::signed(11), 420, 123), Error::::PoolNotFound ); // Force the pools bonded balance to 0, simulating a 100% slash StakingMock::set_bonded_balance(Pools::create_bonded_account(1), 0); assert_noop!( Pools::join(RuntimeOrigin::signed(11), 420, 1), Error::::OverflowRisk ); // Given a mocked bonded pool BondedPool:: { id: 123, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 100, roles: DEFAULT_ROLES, state: PoolState::Open, }, } .put(); // and reward pool RewardPools::::insert(123, RewardPool:: { ..Default::default() }); // Force the points:balance ratio to `MaxPointsToBalance` (100/10) let max_points_to_balance: u128 = <::MaxPointsToBalance as Get>::get().into(); StakingMock::set_bonded_balance( Pools::create_bonded_account(123), max_points_to_balance, ); assert_noop!( Pools::join(RuntimeOrigin::signed(11), 420, 123), Error::::OverflowRisk ); StakingMock::set_bonded_balance( Pools::create_bonded_account(123), Balance::MAX / max_points_to_balance, ); // Balance needs to be gt Balance::MAX / `MaxPointsToBalance` assert_noop!( Pools::join(RuntimeOrigin::signed(11), 5, 123), TokenError::FundsUnavailable, ); StakingMock::set_bonded_balance(Pools::create_bonded_account(1), max_points_to_balance); // Cannot join a pool that isn't open unsafe_set_state(123, PoolState::Blocked); assert_noop!( Pools::join(RuntimeOrigin::signed(11), max_points_to_balance, 123), Error::::NotOpen ); unsafe_set_state(123, PoolState::Destroying); assert_noop!( Pools::join(RuntimeOrigin::signed(11), max_points_to_balance, 123), Error::::NotOpen ); // Given MinJoinBond::::put(100); // Then assert_noop!( Pools::join(RuntimeOrigin::signed(11), 99, 123), Error::::MinimumBondNotMet ); }); } #[test] #[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] #[cfg_attr(not(debug_assertions), should_panic)] fn join_panics_when_reward_pool_not_found() { ExtBuilder::default().build_and_execute(|| { StakingMock::set_bonded_balance(Pools::create_bonded_account(123), 100); BondedPool:: { id: 123, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 100, roles: DEFAULT_ROLES, state: PoolState::Open, }, } .put(); let _ = Pools::join(RuntimeOrigin::signed(11), 420, 123); }); } #[test] fn join_max_member_limits_are_respected() { ExtBuilder::default().build_and_execute(|| { // Given assert_eq!(MaxPoolMembersPerPool::::get(), Some(3)); for i in 1..3 { let account = i + 100; Currency::set_balance(&account, 100 + Currency::minimum_balance()); assert_ok!(Pools::join(RuntimeOrigin::signed(account), 100, 1)); } Currency::set_balance(&103, 100 + Currency::minimum_balance()); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 101, pool_id: 1, bonded: 100, joined: true }, Event::Bonded { member: 102, pool_id: 1, bonded: 100, joined: true } ] ); assert_noop!( Pools::join(RuntimeOrigin::signed(103), 100, 1), Error::::MaxPoolMembers ); // Given assert_eq!(PoolMembers::::count(), 3); assert_eq!(MaxPoolMembers::::get(), Some(4)); Currency::set_balance(&104, 100 + Currency::minimum_balance()); assert_ok!(Pools::create(RuntimeOrigin::signed(104), 100, 104, 104, 104)); let pool_account = BondedPools::::iter() .find(|(_, bonded_pool)| bonded_pool.roles.depositor == 104) .map(|(pool_account, _)| pool_account) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 104, pool_id: 2 }, Event::Bonded { member: 104, pool_id: 2, bonded: 100, joined: true } ] ); assert_noop!( Pools::join(RuntimeOrigin::signed(103), 100, pool_account), Error::::MaxPoolMembers ); }); } } mod claim_payout { use super::*; fn del(points: Balance, last_recorded_reward_counter: u128) -> PoolMember { PoolMember { pool_id: 1, points, last_recorded_reward_counter: last_recorded_reward_counter.into(), unbonding_eras: Default::default(), } } fn del_float(points: Balance, last_recorded_reward_counter: f64) -> PoolMember { PoolMember { pool_id: 1, points, last_recorded_reward_counter: RewardCounter::from_float(last_recorded_reward_counter), unbonding_eras: Default::default(), } } fn rew( last_recorded_reward_counter: u128, last_recorded_total_payouts: Balance, total_rewards_claimed: Balance, ) -> RewardPool { RewardPool { last_recorded_reward_counter: last_recorded_reward_counter.into(), last_recorded_total_payouts, total_rewards_claimed, total_commission_claimed: 0, total_commission_pending: 0, } } #[test] fn claim_payout_works() { ExtBuilder::default() .add_members(vec![(40, 40), (50, 50)]) .build_and_execute(|| { // Given each member currently has a free balance of Currency::set_balance(&10, 0); Currency::set_balance(&40, 0); Currency::set_balance(&50, 0); let ed = Currency::minimum_balance(); // and the reward pool has earned 100 in rewards assert_eq!(Currency::free_balance(&default_reward_account()), ed); deposit_rewards(100); let _ = pool_events_since_last_call(); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 },] ); // last recorded reward counter at the time of this member's payout is 1 assert_eq!(PoolMembers::::get(10).unwrap(), del(10, 1)); // pool's 'last_recorded_reward_counter' and 'last_recorded_total_payouts' don't // really change unless if someone bonds/unbonds. assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 10)); assert_eq!(Currency::free_balance(&10), 10); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 90); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(40))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 40, pool_id: 1, payout: 40 }] ); assert_eq!(PoolMembers::::get(40).unwrap(), del(40, 1)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 50)); assert_eq!(Currency::free_balance(&40), 40); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 50); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(50))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] ); assert_eq!(PoolMembers::::get(50).unwrap(), del(50, 1)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 100)); assert_eq!(Currency::free_balance(&50), 50); assert_eq!(Currency::free_balance(&default_reward_account()), ed); // Given the reward pool has some new rewards deposit_rewards(50); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] ); assert_eq!(PoolMembers::::get(10).unwrap(), del_float(10, 1.5)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 105)); assert_eq!(Currency::free_balance(&10), 10 + 5); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 45); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(40))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 40, pool_id: 1, payout: 20 }] ); assert_eq!(PoolMembers::::get(40).unwrap(), del_float(40, 1.5)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 125)); assert_eq!(Currency::free_balance(&40), 40 + 20); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 25); // Given del 50 hasn't claimed and the reward pools has just earned 50 deposit_rewards(50); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 75); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(50))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] ); assert_eq!(PoolMembers::::get(50).unwrap(), del_float(50, 2.0)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 175)); assert_eq!(Currency::free_balance(&50), 50 + 50); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 25); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] ); assert_eq!(PoolMembers::::get(10).unwrap(), del(10, 2)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 180)); assert_eq!(Currency::free_balance(&10), 15 + 5); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 20); // Given del 40 hasn't claimed and the reward pool has just earned 400 deposit_rewards(400); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 420); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 40 }] ); // We expect a payout of 40 assert_eq!(PoolMembers::::get(10).unwrap(), del(10, 6)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 220)); assert_eq!(Currency::free_balance(&10), 20 + 40); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 380); // Given del 40 + del 50 haven't claimed and the reward pool has earned 20 deposit_rewards(20); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 400); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 2 }] ); assert_eq!(PoolMembers::::get(10).unwrap(), del_float(10, 6.2)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 222)); assert_eq!(Currency::free_balance(&10), 60 + 2); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 398); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(40))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 40, pool_id: 1, payout: 188 }] ); assert_eq!(PoolMembers::::get(40).unwrap(), del_float(40, 6.2)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 410)); assert_eq!(Currency::free_balance(&40), 60 + 188); assert_eq!(Currency::free_balance(&default_reward_account()), ed + 210); // When assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(50))); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 50, pool_id: 1, payout: 210 }] ); assert_eq!(PoolMembers::::get(50).unwrap(), del_float(50, 6.2)); assert_eq!(RewardPools::::get(1).unwrap(), rew(0, 0, 620)); assert_eq!(Currency::free_balance(&50), 100 + 210); assert_eq!(Currency::free_balance(&default_reward_account()), ed); }); } #[test] fn reward_payout_errors_if_a_member_is_fully_unbonding() { ExtBuilder::default().add_members(vec![(11, 11)]).build_and_execute(|| { // fully unbond the member. assert_ok!(fully_unbond_permissioned(11)); assert_noop!( Pools::claim_payout(RuntimeOrigin::signed(11)), Error::::FullyUnbonding ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 11, pool_id: 1, bonded: 11, joined: true }, Event::Unbonded { member: 11, pool_id: 1, points: 11, balance: 11, era: 3 } ] ); }); } #[test] fn claim_payout_bounds_commission_above_global() { ExtBuilder::default().build_and_execute(|| { let (mut member, bonded_pool, mut reward_pool) = Pools::get_member_with_pools(&10).unwrap(); // top up commission payee account to existential deposit let _ = Currency::set_balance(&2, 5); // Set a commission pool 1 to 75%, with a payee set to `2` assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), bonded_pool.id, Some((Perbill::from_percent(75), 2)), )); // re-introduce the global maximum to 50% - 25% lower than the current commission of the // pool. GlobalMaxCommission::::set(Some(Perbill::from_percent(50))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(75), 2)) } ] ); // The pool earns 10 points deposit_rewards(10); assert_ok!(Pools::do_reward_payout( &10, &mut member, &mut BondedPool::::get(1).unwrap(), &mut reward_pool )); // commission applied is 50%, not 75%. Has been bounded by `GlobalMaxCommission`. assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 },] ); }) } #[test] fn do_reward_payout_works_with_a_pool_of_1() { let del = |last_recorded_reward_counter| del_float(10, last_recorded_reward_counter); ExtBuilder::default().build_and_execute(|| { let (mut member, mut bonded_pool, mut reward_pool) = Pools::get_member_with_pools(&10).unwrap(); let ed = Currency::minimum_balance(); let payout = Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!(payout, 0); assert_eq!(member, del(0.0)); assert_eq!(reward_pool, rew(0, 0, 0)); Pools::put_member_with_pools( &10, member.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given the pool has earned some rewards for the first time deposit_rewards(5); // When let payout = Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 5 } ] ); assert_eq!(payout, 5); assert_eq!(reward_pool, rew(0, 0, 5)); assert_eq!(member, del(0.5)); Pools::put_member_with_pools( &10, member.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given the pool has earned rewards again deposit_rewards(10); // When let payout = Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 }] ); assert_eq!(payout, 10); assert_eq!(reward_pool, rew(0, 0, 15)); assert_eq!(member, del(1.5)); Pools::put_member_with_pools( &10, member.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given the pool has earned no new rewards Currency::set_balance(&default_reward_account(), ed); // When let payout = Pools::do_reward_payout(&10, &mut member, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!(pool_events_since_last_call(), vec![]); assert_eq!(payout, 0); assert_eq!(reward_pool, rew(0, 0, 15)); assert_eq!(member, del(1.5)); Pools::put_member_with_pools( &10, member.clone(), bonded_pool.clone(), reward_pool.clone(), ); }); } #[test] fn do_reward_payout_works_with_a_pool_of_3() { ExtBuilder::default() .add_members(vec![(40, 40), (50, 50)]) .build_and_execute(|| { let mut bonded_pool = BondedPool::::get(1).unwrap(); let mut reward_pool = RewardPools::::get(1).unwrap(); let mut del_10 = PoolMembers::::get(10).unwrap(); let mut del_40 = PoolMembers::::get(40).unwrap(); let mut del_50 = PoolMembers::::get(50).unwrap(); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, Event::Bonded { member: 50, pool_id: 1, bonded: 50, joined: true } ] ); // Given we have a total of 100 points split among the members assert_eq!(del_50.points + del_40.points + del_10.points, 100); assert_eq!(bonded_pool.points, 100); // and the reward pool has earned 100 in rewards deposit_rewards(100); // When let payout = Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 }] ); assert_eq!(payout, 10); assert_eq!(del_10, del(10, 1)); assert_eq!(reward_pool, rew(0, 0, 10)); Pools::put_member_with_pools( &10, del_10.clone(), bonded_pool.clone(), reward_pool.clone(), ); // When let payout = Pools::do_reward_payout(&40, &mut del_40, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 40, pool_id: 1, payout: 40 }] ); assert_eq!(payout, 40); assert_eq!(del_40, del(40, 1)); assert_eq!(reward_pool, rew(0, 0, 50)); Pools::put_member_with_pools( &40, del_40.clone(), bonded_pool.clone(), reward_pool.clone(), ); // When let payout = Pools::do_reward_payout(&50, &mut del_50, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] ); assert_eq!(payout, 50); assert_eq!(del_50, del(50, 1)); assert_eq!(reward_pool, rew(0, 0, 100)); Pools::put_member_with_pools( &50, del_50.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given the reward pool has some new rewards deposit_rewards(50); // When let payout = Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] ); assert_eq!(payout, 5); assert_eq!(del_10, del_float(10, 1.5)); assert_eq!(reward_pool, rew(0, 0, 105)); Pools::put_member_with_pools( &10, del_10.clone(), bonded_pool.clone(), reward_pool.clone(), ); // When let payout = Pools::do_reward_payout(&40, &mut del_40, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 40, pool_id: 1, payout: 20 }] ); assert_eq!(payout, 20); assert_eq!(del_40, del_float(40, 1.5)); assert_eq!(reward_pool, rew(0, 0, 125)); Pools::put_member_with_pools( &40, del_40.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given del_50 hasn't claimed and the reward pools has just earned 50 deposit_rewards(50); // When let payout = Pools::do_reward_payout(&50, &mut del_50, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 50, pool_id: 1, payout: 50 }] ); assert_eq!(payout, 50); assert_eq!(del_50, del_float(50, 2.0)); assert_eq!(reward_pool, rew(0, 0, 175)); Pools::put_member_with_pools( &50, del_50.clone(), bonded_pool.clone(), reward_pool.clone(), ); // When let payout = Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 5 }] ); assert_eq!(payout, 5); assert_eq!(del_10, del_float(10, 2.0)); assert_eq!(reward_pool, rew(0, 0, 180)); Pools::put_member_with_pools( &10, del_10.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given del_40 hasn't claimed and the reward pool has just earned 400 deposit_rewards(400); // When let payout = Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 40 }] ); assert_eq!(payout, 40); assert_eq!(del_10, del_float(10, 6.0)); assert_eq!(reward_pool, rew(0, 0, 220)); Pools::put_member_with_pools( &10, del_10.clone(), bonded_pool.clone(), reward_pool.clone(), ); // Given del_40 + del_50 haven't claimed and the reward pool has earned 20 deposit_rewards(20); // When let payout = Pools::do_reward_payout(&10, &mut del_10, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!(payout, 2); assert_eq!(del_10, del_float(10, 6.2)); assert_eq!(reward_pool, rew(0, 0, 222)); Pools::put_member_with_pools( &10, del_10.clone(), bonded_pool.clone(), reward_pool.clone(), ); // When let payout = Pools::do_reward_payout(&40, &mut del_40, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!(payout, 188); // 20 (from the 50) + 160 (from the 400) + 8 (from the 20) assert_eq!(del_40, del_float(40, 6.2)); assert_eq!(reward_pool, rew(0, 0, 410)); Pools::put_member_with_pools( &40, del_40.clone(), bonded_pool.clone(), reward_pool.clone(), ); // When let payout = Pools::do_reward_payout(&50, &mut del_50, &mut bonded_pool, &mut reward_pool) .unwrap(); // Then assert_eq!(payout, 210); // 200 (from the 400) + 10 (from the 20) assert_eq!(del_50, del_float(50, 6.2)); assert_eq!(reward_pool, rew(0, 0, 620)); Pools::put_member_with_pools( &50, del_50.clone(), bonded_pool.clone(), reward_pool.clone(), ); }); } #[test] fn rewards_distribution_is_fair_basic() { ExtBuilder::default().build_and_execute(|| { // reward pool by 10. deposit_rewards(10); // 20 joins afterwards. Currency::set_balance(&20, Currency::minimum_balance() + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); // reward by another 20 deposit_rewards(20); // 10 should claim 10 + 10, 20 should claim 20 / 2. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, ] ); // any upcoming rewards are shared equally. deposit_rewards(20); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, ] ); }); } #[test] fn rewards_distribution_is_fair_basic_with_fractions() { // basically checks the case where the amount of rewards is less than the pool shares. for // this, we have to rely on fixed point arithmetic. ExtBuilder::default().build_and_execute(|| { deposit_rewards(3); Currency::set_balance(&20, Currency::minimum_balance() + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); deposit_rewards(6); // 10 should claim 3, 20 should claim 3 + 3. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 3 + 3 }, Event::PaidOut { member: 20, pool_id: 1, payout: 3 }, ] ); // any upcoming rewards are shared equally. deposit_rewards(8); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 4 }, Event::PaidOut { member: 20, pool_id: 1, payout: 4 }, ] ); // uneven upcoming rewards are shared equally, rounded down. deposit_rewards(7); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 3 }, Event::PaidOut { member: 20, pool_id: 1, payout: 3 }, ] ); }); } #[test] fn rewards_distribution_is_fair_3() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); deposit_rewards(30); Currency::set_balance(&20, ed + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); deposit_rewards(100); Currency::set_balance(&30, ed + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); deposit_rewards(60); // 10 should claim 10, 20 should claim nothing. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 30 + 100 / 2 + 60 / 3 }, Event::PaidOut { member: 20, pool_id: 1, payout: 100 / 2 + 60 / 3 }, Event::PaidOut { member: 30, pool_id: 1, payout: 60 / 3 }, ] ); // any upcoming rewards are shared equally. deposit_rewards(30); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, Event::PaidOut { member: 30, pool_id: 1, payout: 10 }, ] ); }); } #[test] fn pending_rewards_per_member_works() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); assert_eq!(Pools::api_pending_rewards(10), Some(0)); deposit_rewards(30); assert_eq!(Pools::api_pending_rewards(10), Some(30)); assert_eq!(Pools::api_pending_rewards(20), None); Currency::set_balance(&20, ed + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1)); assert_eq!(Pools::api_pending_rewards(10), Some(30)); assert_eq!(Pools::api_pending_rewards(20), Some(0)); deposit_rewards(100); assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50)); assert_eq!(Pools::api_pending_rewards(20), Some(50)); assert_eq!(Pools::api_pending_rewards(30), None); Currency::set_balance(&30, ed + 10); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50)); assert_eq!(Pools::api_pending_rewards(20), Some(50)); assert_eq!(Pools::api_pending_rewards(30), Some(0)); deposit_rewards(60); assert_eq!(Pools::api_pending_rewards(10), Some(30 + 50 + 20)); assert_eq!(Pools::api_pending_rewards(20), Some(50 + 20)); assert_eq!(Pools::api_pending_rewards(30), Some(20)); // 10 should claim 10, 20 should claim nothing. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!(Pools::api_pending_rewards(10), Some(0)); assert_eq!(Pools::api_pending_rewards(20), Some(50 + 20)); assert_eq!(Pools::api_pending_rewards(30), Some(20)); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!(Pools::api_pending_rewards(10), Some(0)); assert_eq!(Pools::api_pending_rewards(20), Some(0)); assert_eq!(Pools::api_pending_rewards(30), Some(20)); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!(Pools::api_pending_rewards(10), Some(0)); assert_eq!(Pools::api_pending_rewards(20), Some(0)); assert_eq!(Pools::api_pending_rewards(30), Some(0)); }); } #[test] fn rewards_distribution_is_fair_bond_extra() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); Currency::set_balance(&20, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); Currency::set_balance(&30, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); deposit_rewards(40); // everyone claims. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, Event::PaidOut { member: 20, pool_id: 1, payout: 20 }, Event::PaidOut { member: 30, pool_id: 1, payout: 10 } ] ); // 30 now bumps itself to be like 20. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(30), BondExtra::FreeBalance(10))); // more rewards come in. deposit_rewards(100); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![ Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: false }, Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, Event::PaidOut { member: 20, pool_id: 1, payout: 40 }, Event::PaidOut { member: 30, pool_id: 1, payout: 40 } ] ); }); } #[test] fn rewards_distribution_is_fair_unbond() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); Currency::set_balance(&20, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); deposit_rewards(30); // everyone claims. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, Event::PaidOut { member: 20, pool_id: 1, payout: 20 } ] ); // 20 unbonds to be equal to 10 (10 points each). assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); // more rewards come in. deposit_rewards(100); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 3 }, Event::PaidOut { member: 10, pool_id: 1, payout: 50 }, Event::PaidOut { member: 20, pool_id: 1, payout: 50 }, ] ); }); } #[test] fn unclaimed_reward_is_safe() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); Currency::set_balance(&20, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); Currency::set_balance(&30, ed + 20); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10, 1)); // 10 gets 10, 20 gets 20, 30 gets 10 deposit_rewards(40); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, Event::PaidOut { member: 20, pool_id: 1, payout: 20 } ] ); // 10 gets 20, 20 gets 40, 30 gets 20 deposit_rewards(80); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, Event::PaidOut { member: 20, pool_id: 1, payout: 40 } ] ); // 10 gets 20, 20 gets 40, 30 gets 20 deposit_rewards(80); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 20 }, Event::PaidOut { member: 20, pool_id: 1, payout: 40 } ] ); // now 30 claims all at once assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 30, pool_id: 1, payout: 10 + 20 + 20 }] ); }); } #[test] fn bond_extra_and_delayed_claim() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); Currency::set_balance(&20, ed + 200); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1)); // 10 gets 10, 20 gets 20, 30 gets 10 deposit_rewards(30); // some claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 10 } ] ); // 20 has not claimed yet, more reward comes deposit_rewards(60); // and 20 bonds more -- they should not have more share of this reward. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(20), BondExtra::FreeBalance(10))); // everyone claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ // 20 + 40, which means the extra amount they bonded did not impact us. Event::PaidOut { member: 20, pool_id: 1, payout: 60 }, Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: false }, Event::PaidOut { member: 10, pool_id: 1, payout: 20 } ] ); // but in the next round of rewards, the extra10 they bonded has an impact. deposit_rewards(60); // everyone claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 15 }, Event::PaidOut { member: 20, pool_id: 1, payout: 45 } ] ); }); } #[test] fn create_sets_recorded_data() { ExtBuilder::default().build_and_execute(|| { MaxPools::::set(None); // pool 10 has already been created. let (member_10, _, reward_pool_10) = Pools::get_member_with_pools(&10).unwrap(); assert_eq!(reward_pool_10.last_recorded_total_payouts, 0); assert_eq!(reward_pool_10.total_rewards_claimed, 0); assert_eq!(reward_pool_10.last_recorded_reward_counter, 0.into()); assert_eq!(member_10.last_recorded_reward_counter, 0.into()); // transfer some reward to pool 1. deposit_rewards(60); // create pool 2 Currency::set_balance(&20, 100); assert_ok!(Pools::create(RuntimeOrigin::signed(20), 10, 20, 20, 20)); // has no impact -- initial let (member_20, _, reward_pool_20) = Pools::get_member_with_pools(&20).unwrap(); assert_eq!(reward_pool_20.last_recorded_total_payouts, 0); assert_eq!(reward_pool_20.total_rewards_claimed, 0); assert_eq!(reward_pool_20.last_recorded_reward_counter, 0.into()); assert_eq!(member_20.last_recorded_reward_counter, 0.into()); // pre-fund the reward account of pool id 3 with some funds. Currency::set_balance(&Pools::create_reward_account(3), 10); // create pool 3 Currency::set_balance(&30, 100); assert_ok!(Pools::create(RuntimeOrigin::signed(30), 10, 30, 30, 30)); // reward counter is still the same. let (member_30, _, reward_pool_30) = Pools::get_member_with_pools(&30).unwrap(); assert_eq!( Currency::free_balance(&Pools::create_reward_account(3)), 10 + Currency::minimum_balance() ); assert_eq!(reward_pool_30.last_recorded_total_payouts, 0); assert_eq!(reward_pool_30.total_rewards_claimed, 0); assert_eq!(reward_pool_30.last_recorded_reward_counter, 0.into()); assert_eq!(member_30.last_recorded_reward_counter, 0.into()); // and 30 can claim the reward now. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Created { depositor: 20, pool_id: 2 }, Event::Bonded { member: 20, pool_id: 2, bonded: 10, joined: true }, Event::Created { depositor: 30, pool_id: 3 }, Event::Bonded { member: 30, pool_id: 3, bonded: 10, joined: true }, Event::PaidOut { member: 30, pool_id: 3, payout: 10 } ] ); }) } #[test] fn join_updates_recorded_data() { ExtBuilder::default().build_and_execute(|| { MaxPoolMembers::::set(None); MaxPoolMembersPerPool::::set(None); let join = |x, y| { Currency::set_balance(&x, y + Currency::minimum_balance()); assert_ok!(Pools::join(RuntimeOrigin::signed(x), y, 1)); }; { let (member_10, _, reward_pool_10) = Pools::get_member_with_pools(&10).unwrap(); assert_eq!(reward_pool_10.last_recorded_total_payouts, 0); assert_eq!(reward_pool_10.total_rewards_claimed, 0); assert_eq!(reward_pool_10.last_recorded_reward_counter, 0.into()); assert_eq!(member_10.last_recorded_reward_counter, 0.into()); } // someone joins without any rewards being issued. { join(20, 10); let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); // reward counter is 0 both before.. assert_eq!(member.last_recorded_reward_counter, 0.into()); assert_eq!(reward_pool.last_recorded_total_payouts, 0); assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); } // transfer some reward to pool 1. deposit_rewards(60); { join(30, 10); let (member, _, reward_pool) = Pools::get_member_with_pools(&30).unwrap(); assert_eq!(reward_pool.last_recorded_total_payouts, 60); // explanation: we have a total of 20 points so far (excluding the 10 that just got // bonded), and 60 unclaimed rewards. each share is then roughly worth of 3 units of // rewards, thus reward counter is 3. member's reward counter is the same assert_eq!(member.last_recorded_reward_counter, 3.into()); assert_eq!(reward_pool.last_recorded_reward_counter, 3.into()); } // someone else joins { join(40, 10); let (member, _, reward_pool) = Pools::get_member_with_pools(&40).unwrap(); // reward counter does not change since no rewards have came in. assert_eq!(member.last_recorded_reward_counter, 3.into()); assert_eq!(reward_pool.last_recorded_reward_counter, 3.into()); assert_eq!(reward_pool.last_recorded_total_payouts, 60); } assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 30, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 40, pool_id: 1, bonded: 10, joined: true } ] ); }) } #[test] fn bond_extra_updates_recorded_data() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { MaxPoolMembers::::set(None); MaxPoolMembersPerPool::::set(None); // initial state of pool 1. { let (member_10, _, reward_pool_10) = Pools::get_member_with_pools(&10).unwrap(); assert_eq!(reward_pool_10.last_recorded_total_payouts, 0); assert_eq!(reward_pool_10.total_rewards_claimed, 0); assert_eq!(reward_pool_10.last_recorded_reward_counter, 0.into()); assert_eq!(member_10.last_recorded_reward_counter, 0.into()); } Currency::set_balance(&10, 100); Currency::set_balance(&20, 100); // 10 bonds extra without any rewards. { assert_ok!(Pools::bond_extra( RuntimeOrigin::signed(10), BondExtra::FreeBalance(10) )); let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); assert_eq!(member.last_recorded_reward_counter, 0.into()); assert_eq!(reward_pool.last_recorded_total_payouts, 0); assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); } // 10 bonds extra again with some rewards. This reward should be split equally between // 10 and 20, as they both have equal points now. deposit_rewards(30); { assert_ok!(Pools::bond_extra( RuntimeOrigin::signed(10), BondExtra::FreeBalance(10) )); let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); // explanation: before bond_extra takes place, there is 40 points and 30 balance in // the system, RewardCounter is therefore 7.5 assert_eq!(member.last_recorded_reward_counter, RewardCounter::from_float(0.75)); assert_eq!( reward_pool.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); assert_eq!(reward_pool.last_recorded_total_payouts, 30); } // 20 bonds extra again, without further rewards. { assert_ok!(Pools::bond_extra( RuntimeOrigin::signed(20), BondExtra::FreeBalance(10) )); let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); assert_eq!(member.last_recorded_reward_counter, RewardCounter::from_float(0.75)); assert_eq!( reward_pool.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); assert_eq!(reward_pool.last_recorded_total_payouts, 30); } assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, Event::PaidOut { member: 10, pool_id: 1, payout: 15 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, Event::PaidOut { member: 20, pool_id: 1, payout: 15 }, Event::Bonded { member: 20, pool_id: 1, bonded: 10, joined: false } ] ); }) } #[test] fn bond_extra_pending_rewards_works() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { MaxPoolMembers::::set(None); MaxPoolMembersPerPool::::set(None); // pool receives some rewards. deposit_rewards(30); System::reset_events(); // 10 cashes it out, and bonds it. { assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); // there is 30 points and 30 reward points in the system RC is 1. assert_eq!(member.last_recorded_reward_counter, 1.into()); assert_eq!(reward_pool.total_rewards_claimed, 10); // these two are not updated -- only updated when the points change. assert_eq!(reward_pool.last_recorded_total_payouts, 0); assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10 }] ); } // 20 re-bonds it. { assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(20), BondExtra::Rewards)); let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); assert_eq!(member.last_recorded_reward_counter, 1.into()); assert_eq!(reward_pool.total_rewards_claimed, 30); // since points change, these two are updated. assert_eq!(reward_pool.last_recorded_total_payouts, 30); assert_eq!(reward_pool.last_recorded_reward_counter, 1.into()); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 20, pool_id: 1, payout: 20 }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: false } ] ); } }) } #[test] fn unbond_updates_recorded_data() { ExtBuilder::default() .add_members(vec![(20, 20), (30, 20)]) .build_and_execute(|| { MaxPoolMembers::::set(None); MaxPoolMembersPerPool::::set(None); // initial state of pool 1. { let (member, _, reward_pool) = Pools::get_member_with_pools(&10).unwrap(); assert_eq!(reward_pool.last_recorded_total_payouts, 0); assert_eq!(reward_pool.total_rewards_claimed, 0); assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); assert_eq!(member.last_recorded_reward_counter, 0.into()); } // 20 unbonds without any rewards. { assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10)); let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); assert_eq!(member.last_recorded_reward_counter, 0.into()); assert_eq!(reward_pool.last_recorded_total_payouts, 0); assert_eq!(reward_pool.last_recorded_reward_counter, 0.into()); } // some rewards come in. deposit_rewards(30); // and 30 also unbonds half. { assert_ok!(Pools::unbond(RuntimeOrigin::signed(30), 30, 10)); let (member, _, reward_pool) = Pools::get_member_with_pools(&30).unwrap(); // 30 reward in the system, and 40 points before this unbond to collect it, // RewardCounter is 3/4. assert_eq!( member.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); assert_eq!(reward_pool.last_recorded_total_payouts, 30); assert_eq!( reward_pool.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); } // 30 unbonds again, not change this time. { assert_ok!(Pools::unbond(RuntimeOrigin::signed(30), 30, 5)); let (member, _, reward_pool) = Pools::get_member_with_pools(&30).unwrap(); assert_eq!( member.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); assert_eq!(reward_pool.last_recorded_total_payouts, 30); assert_eq!( reward_pool.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); } // 20 unbonds again, not change this time, just collecting their reward. { assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); let (member, _, reward_pool) = Pools::get_member_with_pools(&20).unwrap(); assert_eq!( member.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); assert_eq!(reward_pool.last_recorded_total_payouts, 30); assert_eq!( reward_pool.last_recorded_reward_counter, RewardCounter::from_float(0.75) ); } // trigger 10's reward as well to see all of the payouts. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::Bonded { member: 30, pool_id: 1, bonded: 20, joined: true }, Event::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 3 }, Event::PaidOut { member: 30, pool_id: 1, payout: 15 }, Event::Unbonded { member: 30, pool_id: 1, balance: 10, points: 10, era: 3 }, Event::Unbonded { member: 30, pool_id: 1, balance: 5, points: 5, era: 3 }, Event::PaidOut { member: 20, pool_id: 1, payout: 7 }, Event::Unbonded { member: 20, pool_id: 1, balance: 5, points: 5, era: 3 }, Event::PaidOut { member: 10, pool_id: 1, payout: 7 } ] ); }) } #[test] fn rewards_are_rounded_down_depositor_collects_them() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { // initial balance of 10. assert_eq!(Currency::free_balance(&10), 35); assert_eq!( Currency::free_balance(&default_reward_account()), Currency::minimum_balance() ); // some rewards come in. deposit_rewards(40); // everyone claims assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); // some dust (1) remains in the reward account. assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 13 }, Event::PaidOut { member: 20, pool_id: 1, payout: 26 } ] ); // start dismantling the pool. assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); assert_ok!(fully_unbond_permissioned(20)); CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); assert_ok!(fully_unbond_permissioned(10)); CurrentEra::set(6); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 3 }, Event::Withdrawn { member: 20, pool_id: 1, balance: 20, points: 20 }, Event::MemberRemoved { pool_id: 1, member: 20 }, Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 6 }, Event::Withdrawn { member: 10, pool_id: 1, balance: 10, points: 10 }, Event::MemberRemoved { pool_id: 1, member: 10 }, Event::Destroyed { pool_id: 1 } ] ); assert!(!Metadata::::contains_key(1)); // original ed + ed put into reward account + reward + bond + dust. assert_eq!(Currency::free_balance(&10), 35 + 5 + 13 + 10 + 1); }) } #[test] fn claim_payout_large_numbers() { let unit = 10u128.pow(12); // akin to KSM ExistentialDeposit::set(unit); StakingMinBond::set(unit * 1000); ExtBuilder::default() .max_members(Some(4)) .max_members_per_pool(Some(4)) .add_members(vec![(20, 1500 * unit), (21, 2500 * unit), (22, 5000 * unit)]) .build_and_execute(|| { // some rewards come in. assert_eq!(Currency::free_balance(&default_reward_account()), unit); deposit_rewards(unit / 1000); // everyone claims assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(21))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(22))); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 1000000000000000, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 1500000000000000, joined: true }, Event::Bonded { member: 21, pool_id: 1, bonded: 2500000000000000, joined: true }, Event::Bonded { member: 22, pool_id: 1, bonded: 5000000000000000, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 100000000 }, Event::PaidOut { member: 20, pool_id: 1, payout: 150000000 }, Event::PaidOut { member: 21, pool_id: 1, payout: 250000000 }, Event::PaidOut { member: 22, pool_id: 1, payout: 500000000 } ] ); }) } #[test] fn claim_payout_other_works() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { Currency::set_balance(&default_reward_account(), 8); // ... of which only 3 are claimable to make sure the reward account does not die. let claimable_reward = 8 - ExistentialDeposit::get(); // NOTE: easier to read if we use 3, so let's use the number instead of variable. assert_eq!(claimable_reward, 3, "test is correct if rewards are divisible by 3"); // given assert_eq!(Currency::free_balance(&10), 35); // when // NOTE: Claim permission of `PermissionlessWithdraw` allows payout claiming as default, // so a claim permission does not need to be set for non-pool members prior to claiming. assert_ok!(Pools::claim_payout_other(RuntimeOrigin::signed(80), 10)); // then assert_eq!(Currency::free_balance(&10), 36); assert_eq!(Currency::free_balance(&default_reward_account()), 7); }) } } mod unbond { use super::*; #[test] fn member_unbond_open() { // depositor in pool, pool state open // - member unbond above limit // - member unbonds to 0 // - member cannot unbond between within limit and 0 ExtBuilder::default() .min_join_bond(10) .add_members(vec![(20, 20)]) .build_and_execute(|| { assert_eq!(TotalValueLocked::::get(), 30); // can unbond to above limit assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 15); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 5); // tvl remains unchanged. assert_eq!(TotalValueLocked::::get(), 30); // cannot go to below 10: assert_noop!( Pools::unbond(RuntimeOrigin::signed(20), 20, 10), Error::::MinimumBondNotMet ); // Make permissionless assert_ok!(Pools::set_claim_permission( RuntimeOrigin::signed(20), ClaimPermission::PermissionlessAll )); // but can go to 0 assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 15)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); assert_eq!( ClaimPermissions::::get(20), ClaimPermission::PermissionlessAll ); }) } #[test] fn member_kicked() { // depositor in pool, pool state blocked // - member cannot be kicked to above limit // - member cannot be kicked between within limit and 0 // - member kicked to 0 ExtBuilder::default() .min_join_bond(10) .add_members(vec![(20, 20)]) .build_and_execute(|| { unsafe_set_state(1, PoolState::Blocked); let kicker = DEFAULT_ROLES.bouncer.unwrap(); // cannot be kicked to above the limit. assert_noop!( Pools::unbond(RuntimeOrigin::signed(kicker), 20, 5), Error::::PartialUnbondNotAllowedPermissionlessly ); // cannot go to below 10: assert_noop!( Pools::unbond(RuntimeOrigin::signed(kicker), 20, 15), Error::::PartialUnbondNotAllowedPermissionlessly ); // but they themselves can do an unbond assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 18); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 2); // can be kicked to 0. assert_ok!(Pools::unbond(RuntimeOrigin::signed(kicker), 20, 18)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); }) } #[test] fn member_unbond_destroying() { // depositor in pool, pool state destroying // - member cannot be permissionlessly unbonded to above limit // - member cannot be permissionlessly unbonded between within limit and 0 // - member permissionlessly unbonded to 0 ExtBuilder::default() .min_join_bond(10) .add_members(vec![(20, 20)]) .build_and_execute(|| { unsafe_set_state(1, PoolState::Destroying); let random = 123; // cannot be kicked to above the limit. assert_noop!( Pools::unbond(RuntimeOrigin::signed(random), 20, 5), Error::::PartialUnbondNotAllowedPermissionlessly ); // cannot go to below 10: assert_noop!( Pools::unbond(RuntimeOrigin::signed(random), 20, 15), Error::::PartialUnbondNotAllowedPermissionlessly ); // but they themselves can do an unbond assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 18); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 2); // but can go to 0 assert_ok!(Pools::unbond(RuntimeOrigin::signed(random), 20, 18)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); }) } #[test] fn member_unbond_destroying_with_pending_rewards() { ExtBuilder::default() .min_join_bond(10) .add_members(vec![(20, 20)]) .build_and_execute(|| { unsafe_set_state(1, PoolState::Destroying); let random = 123; // given the pool some pending rewards. assert_eq!(pending_rewards_for_delegator(20), 0); deposit_rewards(10); assert_eq!(pending_rewards_for_delegator(20), 6); // any random user can unbond 20 now. assert_ok!(Pools::unbond(RuntimeOrigin::signed(random), 20, 20)); assert_eq!(PoolMembers::::get(20).unwrap().active_points(), 0); assert_eq!(PoolMembers::::get(20).unwrap().unbonding_points(), 20); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::PaidOut { member: 20, pool_id: 1, payout: 6 }, Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 3 } ] ); }) } #[test] fn depositor_unbond_open() { // depositor in pool, pool state open // - depositor unbonds to above limit // - depositor cannot unbond to below limit or 0 ExtBuilder::default().min_join_bond(10).build_and_execute(|| { // give the depositor some extra funds. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); assert_eq!(PoolMembers::::get(10).unwrap().points, 20); // can unbond to above the limit. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 15); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 5); // cannot go to below 10: assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 10), Error::::MinimumBondNotMet ); // cannot go to 0 either. assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 15), Error::::MinimumBondNotMet ); }) } #[test] fn depositor_kick() { // depositor in pool, pool state blocked // - depositor can never be kicked. ExtBuilder::default().min_join_bond(10).build_and_execute(|| { // give the depositor some extra funds. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); assert_eq!(PoolMembers::::get(10).unwrap().points, 20); // set the stage unsafe_set_state(1, PoolState::Blocked); let kicker = DEFAULT_ROLES.bouncer.unwrap(); // cannot be kicked to above limit. assert_noop!( Pools::unbond(RuntimeOrigin::signed(kicker), 10, 5), Error::::PartialUnbondNotAllowedPermissionlessly ); // or below the limit assert_noop!( Pools::unbond(RuntimeOrigin::signed(kicker), 10, 15), Error::::PartialUnbondNotAllowedPermissionlessly ); // or 0. assert_noop!( Pools::unbond(RuntimeOrigin::signed(kicker), 10, 20), Error::::DoesNotHavePermission ); // they themselves cannot do it either assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 20), Error::::MinimumBondNotMet ); }) } #[test] fn depositor_unbond_destroying_permissionless() { // depositor can never be permissionlessly unbonded. ExtBuilder::default().min_join_bond(10).build_and_execute(|| { // give the depositor some extra funds. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); assert_eq!(PoolMembers::::get(10).unwrap().points, 20); // set the stage unsafe_set_state(1, PoolState::Destroying); let random = 123; // cannot be kicked to above limit. assert_noop!( Pools::unbond(RuntimeOrigin::signed(random), 10, 5), Error::::PartialUnbondNotAllowedPermissionlessly ); // or below the limit assert_noop!( Pools::unbond(RuntimeOrigin::signed(random), 10, 15), Error::::PartialUnbondNotAllowedPermissionlessly ); // or 0. assert_noop!( Pools::unbond(RuntimeOrigin::signed(random), 10, 20), Error::::DoesNotHavePermission ); // they themselves can do it in this case though. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 20)); }) } #[test] fn depositor_unbond_destroying_not_last_member() { // deposit in pool, pool state destroying // - depositor can never leave if there is another member in the pool. ExtBuilder::default() .min_join_bond(10) .add_members(vec![(20, 20)]) .build_and_execute(|| { // give the depositor some extra funds. assert_ok!(Pools::bond_extra( RuntimeOrigin::signed(10), BondExtra::FreeBalance(10) )); assert_eq!(PoolMembers::::get(10).unwrap().points, 20); // set the stage unsafe_set_state(1, PoolState::Destroying); // can go above the limit assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 15); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 5); // but not below the limit assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 10), Error::::MinimumBondNotMet ); // and certainly not zero assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 15), Error::::MinimumBondNotMet ); }) } #[test] fn depositor_unbond_destroying_last_member() { // deposit in pool, pool state destroying // - depositor can unbond to above limit always. // - depositor cannot unbond to below limit if last. // - depositor can unbond to 0 if last and destroying. ExtBuilder::default().min_join_bond(10).build_and_execute(|| { // give the depositor some extra funds. assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); assert_eq!(PoolMembers::::get(10).unwrap().points, 20); // set the stage unsafe_set_state(1, PoolState::Destroying); // can unbond to above the limit. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 15); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 5); // still cannot go to below limit assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 10), Error::::MinimumBondNotMet ); // can go to 0 too. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 15)); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 0); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 20); }) } #[test] fn unbond_of_1_works() { ExtBuilder::default().build_and_execute(|| { unsafe_set_state(1, PoolState::Destroying); assert_ok!(fully_unbond_permissioned(10)); assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 3 => UnbondPool:: { points: 10, balance: 10 }} ); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 0, roles: DEFAULT_ROLES, state: PoolState::Destroying, } } ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); }); } #[test] fn unbond_of_3_works() { ExtBuilder::default() .add_members(vec![(40, 40), (550, 550)]) .build_and_execute(|| { let ed = Currency::minimum_balance(); // Given a slash from 600 -> 500 StakingMock::slash_by(1, 500); // and unclaimed rewards of 600. Currency::set_balance(&default_reward_account(), ed + 600); // When assert_ok!(fully_unbond_permissioned(40)); // Then assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }} ); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 3, points: 560, roles: DEFAULT_ROLES, state: PoolState::Open, } } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, Event::PoolSlashed { pool_id: 1, balance: 100 }, Event::PaidOut { member: 40, pool_id: 1, payout: 40 }, Event::Unbonded { member: 40, pool_id: 1, balance: 6, points: 6, era: 3 } ] ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 94); assert_eq!( PoolMembers::::get(40).unwrap().unbonding_eras, member_unbonding_eras!(3 => 6) ); assert_eq!(Currency::free_balance(&40), 40 + 40); // We claim rewards when unbonding // When unsafe_set_state(1, PoolState::Destroying); assert_ok!(fully_unbond_permissioned(550)); // Then assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 3 => UnbondPool { points: 98, balance: 98 }} ); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 3, points: 10, roles: DEFAULT_ROLES, state: PoolState::Destroying, } } ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 2); assert_eq!( PoolMembers::::get(550).unwrap().unbonding_eras, member_unbonding_eras!(3 => 92) ); assert_eq!(Currency::free_balance(&550), 550 + 550); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 550, pool_id: 1, payout: 550 }, Event::Unbonded { member: 550, pool_id: 1, points: 92, balance: 92, era: 3 } ] ); // When CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 40, 0)); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 550, 0)); assert_ok!(fully_unbond_permissioned(10)); // Then assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 6 => UnbondPool { points: 2, balance: 2 }} ); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 0, roles: DEFAULT_ROLES, state: PoolState::Destroying, } } ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); assert_eq!(Currency::free_balance(&550), 550 + 550 + 92); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 40, pool_id: 1, points: 6, balance: 6 }, Event::MemberRemoved { pool_id: 1, member: 40 }, Event::Withdrawn { member: 550, pool_id: 1, points: 92, balance: 92 }, Event::MemberRemoved { pool_id: 1, member: 550 }, Event::PaidOut { member: 10, pool_id: 1, payout: 10 }, Event::Unbonded { member: 10, pool_id: 1, points: 2, balance: 2, era: 6 } ] ); }); } #[test] fn unbond_merges_older_pools() { ExtBuilder::default().with_check(1).build_and_execute(|| { // Given assert_eq!(StakingMock::bonding_duration(), 3); SubPoolsStorage::::insert( 1, SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { balance: 10, points: 100 }, 1 + 3 => UnbondPool { balance: 20, points: 20 }, 2 + 3 => UnbondPool { balance: 101, points: 101} }, }, ); unsafe_set_state(1, PoolState::Destroying); // When let current_era = 1 + TotalUnbondingPools::::get(); CurrentEra::set(current_era); assert_ok!(fully_unbond_permissioned(10)); // Then assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: UnbondPool { balance: 10 + 20, points: 100 + 20 }, with_era: unbonding_pools_with_era! { 2 + 3 => UnbondPool { balance: 101, points: 101}, current_era + 3 => UnbondPool { balance: 10, points: 10 }, }, }, ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 } ] ); }); } #[test] fn unbond_kick_works() { // Kick: the pool is blocked and the caller is either the root or bouncer. ExtBuilder::default() .add_members(vec![(100, 100), (200, 200)]) .build_and_execute(|| { // Given unsafe_set_state(1, PoolState::Blocked); let bonded_pool = BondedPool::::get(1).unwrap(); assert_eq!(bonded_pool.roles.root.unwrap(), 900); assert_eq!(bonded_pool.roles.nominator.unwrap(), 901); assert_eq!(bonded_pool.roles.bouncer.unwrap(), 902); // When the nominator tries to kick, then its a noop assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(901), 100), Error::::NotKickerOrDestroying ); // When the root kicks then its ok // Account with ID 100 is kicked. assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(900), 100)); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, Event::Bonded { member: 200, pool_id: 1, bonded: 200, joined: true }, Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, ] ); // When the bouncer kicks then its ok // Account with ID 200 is kicked. assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(902), 200)); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 200, pool_id: 1, points: 200, balance: 200, era: 3 }] ); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 3, points: 10, // Only 10 points because 200 + 100 was unbonded roles: DEFAULT_ROLES, state: PoolState::Blocked, } } ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 10); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 100 + 200, balance: 100 + 200 } }, } ); assert_eq!( *UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), vec![(3, 100), (3, 200)], ); }); } #[test] fn unbond_permissionless_works() { // Scenarios where non-admin accounts can unbond others ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { // Given the pool is blocked unsafe_set_state(1, PoolState::Blocked); // A permissionless unbond attempt errors assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(420), 100), Error::::NotKickerOrDestroying ); // permissionless unbond must be full assert_noop!( Pools::unbond(RuntimeOrigin::signed(420), 100, 80), Error::::PartialUnbondNotAllowedPermissionlessly, ); // Given the pool is destroying unsafe_set_state(1, PoolState::Destroying); // The depositor cannot be fully unbonded until they are the last member assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(10), 10), Error::::MinimumBondNotMet, ); // Any account can unbond a member that is not the depositor assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(420), 100)); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 } ] ); // still permissionless unbond must be full assert_noop!( Pools::unbond(RuntimeOrigin::signed(420), 100, 80), Error::::PartialUnbondNotAllowedPermissionlessly, ); // Given the pool is blocked unsafe_set_state(1, PoolState::Blocked); // The depositor cannot be unbonded assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(420), 10), Error::::DoesNotHavePermission ); // Given the pools is destroying unsafe_set_state(1, PoolState::Destroying); // The depositor cannot be unbonded yet. assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(420), 10), Error::::DoesNotHavePermission, ); // but when everyone is unbonded it can.. CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 100, 0)); // still permissionless unbond must be full. assert_noop!( Pools::unbond(RuntimeOrigin::signed(420), 10, 5), Error::::PartialUnbondNotAllowedPermissionlessly, ); // depositor can never be unbonded permissionlessly . assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(420), 10), Error::::DoesNotHavePermission ); // but depositor itself can do it. assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(10), 10)); assert_eq!(BondedPools::::get(1).unwrap().points, 0); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 + 3 => UnbondPool { points: 10, balance: 10 } } } ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 0); assert_eq!( *UnbondingBalanceMap::get().get(&default_bonded_account()).unwrap(), vec![(6, 10)] ); }); } #[test] #[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] #[cfg_attr(not(debug_assertions), should_panic)] fn unbond_errors_correctly() { ExtBuilder::default().build_and_execute(|| { assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(11), 11), Error::::PoolMemberNotFound ); // Add the member let member = PoolMember { pool_id: 2, points: 10, ..Default::default() }; PoolMembers::::insert(11, member); let _ = Pools::fully_unbond(RuntimeOrigin::signed(11), 11); }); } #[test] #[cfg_attr(debug_assertions, should_panic(expected = "Defensive failure has been triggered!"))] #[cfg_attr(not(debug_assertions), should_panic)] fn unbond_panics_when_reward_pool_not_found() { ExtBuilder::default().build_and_execute(|| { let member = PoolMember { pool_id: 2, points: 10, ..Default::default() }; PoolMembers::::insert(11, member); BondedPool:: { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 10, roles: DEFAULT_ROLES, state: PoolState::Open, }, } .put(); let _ = Pools::fully_unbond(RuntimeOrigin::signed(11), 11); }); } #[test] fn partial_unbond_era_tracking() { ExtBuilder::default().ed(1).build_and_execute(|| { // to make the depositor capable of withdrawing. StakingMinBond::set(1); MinCreateBond::::set(1); MinJoinBond::::set(1); assert_eq!(Pools::depositor_min_bond(), 1); // given assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 10); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 0); assert_eq!(PoolMembers::::get(10).unwrap().pool_id, 1); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!() ); assert_eq!(BondedPool::::get(1).unwrap().points, 10); assert!(SubPoolsStorage::::get(1).is_none()); assert_eq!(CurrentEra::get(), 0); assert_eq!(BondingDuration::get(), 3); // so the depositor can leave, just keeps the test simpler. unsafe_set_state(1, PoolState::Destroying); // when: casual unbond assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 1)); // then assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 9); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 1); assert_eq!(BondedPool::::get(1).unwrap().points, 9); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(3 => 1) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 1, balance: 1 } } } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 3 } ] ); // when: casual further unbond, same era. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); // then assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 4); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 6); assert_eq!(BondedPool::::get(1).unwrap().points, 4); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(3 => 6) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 3 }] ); // when: casual further unbond, next era. CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 1)); // then assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 3); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 7); assert_eq!(BondedPool::::get(1).unwrap().points, 3); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(3 => 6, 4 => 1) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }, 4 => UnbondPool { points: 1, balance: 1 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 }] ); // when: unbonding more than our active: error assert_noop!( frame_support::storage::with_storage_layer(|| Pools::unbond( RuntimeOrigin::signed(10), 10, 5 )), Error::::MinimumBondNotMet ); // instead: assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3)); // then assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 0); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 10); assert_eq!(BondedPool::::get(1).unwrap().points, 0); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(3 => 6, 4 => 4) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }, 4 => UnbondPool { points: 4, balance: 4 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 4 }] ); }); } #[test] fn partial_unbond_max_chunks() { ExtBuilder::default().add_members(vec![(20, 20)]).ed(1).build_and_execute(|| { MaxUnbonding::set(2); // given assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 3)); assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 2, 4 => 3) ); // when CurrentEra::set(2); assert_noop!( frame_support::storage::with_storage_layer(|| Pools::unbond( RuntimeOrigin::signed(20), 20, 4 )), Error::::MaxUnbondingLimit ); // when MaxUnbonding::set(3); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 1)); assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 2, 4 => 3, 5 => 1) ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::Unbonded { member: 20, pool_id: 1, points: 2, balance: 2, era: 3 }, Event::Unbonded { member: 20, pool_id: 1, points: 3, balance: 3, era: 4 }, Event::Unbonded { member: 20, pool_id: 1, points: 1, balance: 1, era: 5 } ] ); }) } // depositor can unbond only up to `MinCreateBond`. #[test] fn depositor_permissioned_partial_unbond() { ExtBuilder::default().ed(1).build_and_execute(|| { // given StakingMinBond::set(5); assert_eq!(Pools::depositor_min_bond(), 5); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 10); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 0); // can unbond a bit.. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3)); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 7); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 3); // but not less than 2 assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 6), Error::::MinimumBondNotMet ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 3 } ] ); }); } #[test] fn depositor_permissioned_partial_unbond_slashed() { ExtBuilder::default().ed(1).build_and_execute(|| { // given assert_eq!(MinCreateBond::::get(), 2); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 10); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 0); // slash the default pool StakingMock::slash_by(1, 5); // cannot unbond even 7, because the value of shares is now less. assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 7), Error::::MinimumBondNotMet ); }); } #[test] fn every_unbonding_triggers_payout() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { let initial_reward_account = Currency::free_balance(&default_reward_account()); assert_eq!(initial_reward_account, Currency::minimum_balance()); assert_eq!(initial_reward_account, 5); Currency::set_balance(&default_reward_account(), 4 * Currency::minimum_balance()); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2)); assert_eq!( pool_events_since_last_call(), vec![ // 2/3 of ed, which is 20's share. Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::PaidOut { member: 20, pool_id: 1, payout: 10 }, Event::Unbonded { member: 20, pool_id: 1, balance: 2, points: 2, era: 3 } ] ); CurrentEra::set(1); Currency::set_balance(&default_reward_account(), 4 * Currency::minimum_balance()); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 3)); assert_eq!( pool_events_since_last_call(), vec![ // 2/3 of ed, which is 20's share. Event::PaidOut { member: 20, pool_id: 1, payout: 6 }, Event::Unbonded { member: 20, pool_id: 1, points: 3, balance: 3, era: 4 } ] ); CurrentEra::set(2); Currency::set_balance(&default_reward_account(), 4 * Currency::minimum_balance()); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 20, pool_id: 1, payout: 3 }, Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 } ] ); assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 2, 4 => 3, 5 => 5) ); }); } } mod pool_withdraw_unbonded { use super::*; #[test] fn pool_withdraw_unbonded_works() { ExtBuilder::default().add_members(vec![(20, 10)]).build_and_execute(|| { // Given 10 unbond'ed directly against the pool account assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15)); assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(20)); assert_eq!(Balances::free_balance(&default_bonded_account()), 20); // When CurrentEra::set(StakingMock::current_era() + StakingMock::bonding_duration() + 1); assert_ok!(Pools::pool_withdraw_unbonded(RuntimeOrigin::signed(10), 1, 0)); // Then their unbonding balance is no longer locked assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15)); assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(15)); assert_eq!(Balances::free_balance(&default_bonded_account()), 20); }); } #[test] fn pool_withdraw_unbonded_creates_tvl_diff() { ExtBuilder::default().add_members(vec![(20, 10)]).build_and_execute(|| { // Given 10 unbond'ed directly against the pool account assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15)); assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(20)); assert_eq!(Balances::free_balance(&default_bonded_account()), 20); assert_eq!(TotalValueLocked::::get(), 20); // When CurrentEra::set(StakingMock::current_era() + StakingMock::bonding_duration() + 1); assert_ok!(Pools::pool_withdraw_unbonded(RuntimeOrigin::signed(10), 1, 0)); assert_eq!(TotalValueLocked::::get(), 15); let member_balance = PoolMembers::::iter() .map(|(_, member)| member.total_balance()) .reduce(|acc, total_balance| acc + total_balance) .unwrap_or_default(); // Then their unbonding balance is no longer locked assert_eq!(StakingMock::active_stake(&default_bonded_account()), Ok(15)); assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(15)); assert_eq!(Currency::free_balance(&default_bonded_account()), 20); // The difference between TVL and member_balance is exactly the difference between // `total_stake` and the `free_balance`. // This relation is not guaranteed in the wild as arbitrary transfers towards // `free_balance` can be made to the pool that are not accounted for. let non_locked_balance = Balances::free_balance(&default_bonded_account()) - StakingMock::total_stake(&default_bonded_account()).unwrap(); assert_eq!(member_balance, TotalValueLocked::::get() + non_locked_balance); }); } } mod withdraw_unbonded { use super::*; use sp_runtime::bounded_btree_map; #[test] fn withdraw_unbonded_works_against_slashed_no_era_sub_pool() { ExtBuilder::default() .add_members(vec![(40, 40), (550, 550)]) .build_and_execute(|| { // reduce the noise a bit. let _ = balances_events_since_last_call(); // Given assert_eq!(StakingMock::bonding_duration(), 3); assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(550), 550)); assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(40), 40)); assert_eq!(Currency::free_balance(&default_bonded_account()), 600); let mut current_era = 1; CurrentEra::set(current_era); let mut sub_pools = SubPoolsStorage::::get(1).unwrap(); let unbond_pool = sub_pools.with_era.get_mut(&3).unwrap(); // Sanity check assert_eq!(*unbond_pool, UnbondPool { points: 550 + 40, balance: 550 + 40 }); assert_eq!(TotalValueLocked::::get(), 600); // Simulate a slash to the pool with_era(current_era), decreasing the balance by // half { unbond_pool.balance /= 2; // 295 SubPoolsStorage::::insert(1, sub_pools); // Adjust the TVL for this non-api usage (direct sub-pool modification) TotalValueLocked::::mutate(|x| *x -= 295); // Update the equivalent of the unbonding chunks for the `StakingMock` let mut x = UnbondingBalanceMap::get(); x.get_mut(&default_bonded_account()) .unwrap() .get_mut(current_era as usize) .unwrap() .1 /= 2; UnbondingBalanceMap::set(&x); Currency::set_balance( &default_bonded_account(), Currency::free_balance(&default_bonded_account()) / 2, // 300 ); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 10); StakingMock::slash_by(1, 5); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 5); }; // Advance the current_era to ensure all `with_era` pools will be merged into // `no_era` pool current_era += TotalUnbondingPools::::get(); CurrentEra::set(current_era); // Simulate some other call to unbond that would merge `with_era` pools into // `no_era` let sub_pools = SubPoolsStorage::::get(1).unwrap().maybe_merge_pools(current_era); SubPoolsStorage::::insert(1, sub_pools); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: UnbondPool { points: 550 + 40, balance: 275 + 20 }, with_era: Default::default() } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, Event::Unbonded { member: 550, pool_id: 1, points: 550, balance: 550, era: 3 }, Event::Unbonded { member: 40, pool_id: 1, points: 40, balance: 40, era: 3 }, Event::PoolSlashed { pool_id: 1, balance: 5 } ] ); assert_eq!( balances_events_since_last_call(), vec![BEvent::Burned { who: default_bonded_account(), amount: 300 }] ); // When assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(550), 550, 0)); // Then assert_eq!( SubPoolsStorage::::get(1).unwrap().no_era, UnbondPool { points: 40, balance: 20 } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 550, pool_id: 1, balance: 275, points: 550 }, Event::MemberRemoved { pool_id: 1, member: 550 } ] ); assert_eq!( balances_events_since_last_call(), vec![BEvent::Transfer { from: default_bonded_account(), to: 550, amount: 275 }] ); // When assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(40), 40, 0)); // Then assert_eq!( SubPoolsStorage::::get(1).unwrap().no_era, UnbondPool { points: 0, balance: 0 } ); assert!(!PoolMembers::::contains_key(40)); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 40, pool_id: 1, balance: 20, points: 40 }, Event::MemberRemoved { pool_id: 1, member: 40 } ] ); assert_eq!( balances_events_since_last_call(), vec![BEvent::Transfer { from: default_bonded_account(), to: 40, amount: 20 }] ); // now, finally, the depositor can take out its share. unsafe_set_state(1, PoolState::Destroying); assert_ok!(fully_unbond_permissioned(10)); current_era += 3; CurrentEra::set(current_era); // when assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::Unbonded { member: 10, pool_id: 1, balance: 5, points: 5, era: 9 }, Event::Withdrawn { member: 10, pool_id: 1, balance: 5, points: 5 }, Event::MemberRemoved { pool_id: 1, member: 10 }, Event::Destroyed { pool_id: 1 } ] ); assert!(!Metadata::::contains_key(1)); assert_eq!( balances_events_since_last_call(), vec![ BEvent::Transfer { from: default_bonded_account(), to: 10, amount: 5 }, BEvent::Thawed { who: default_reward_account(), amount: 5 }, BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } ] ); }); } #[test] fn withdraw_unbonded_works_against_slashed_with_era_sub_pools() { ExtBuilder::default() .add_members(vec![(40, 40), (550, 550)]) .build_and_execute(|| { let _ = balances_events_since_last_call(); // Given // current bond is 600, we slash it all to 300. StakingMock::slash_by(1, 300); Currency::set_balance(&default_bonded_account(), 300); assert_eq!(StakingMock::total_stake(&default_bonded_account()), Ok(300)); assert_ok!(fully_unbond_permissioned(40)); assert_ok!(fully_unbond_permissioned(550)); assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 3 => UnbondPool { points: 550 / 2 + 40 / 2, balance: 550 / 2 + 40 / 2 }} ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 40, pool_id: 1, bonded: 40, joined: true }, Event::Bonded { member: 550, pool_id: 1, bonded: 550, joined: true }, Event::PoolSlashed { pool_id: 1, balance: 300 }, Event::Unbonded { member: 40, pool_id: 1, balance: 20, points: 20, era: 3 }, Event::Unbonded { member: 550, pool_id: 1, balance: 275, points: 275, era: 3, } ] ); assert_eq!( balances_events_since_last_call(), vec![BEvent::Burned { who: default_bonded_account(), amount: 300 },] ); CurrentEra::set(StakingMock::bonding_duration()); // When assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(40), 40, 0)); // Then assert_eq!( balances_events_since_last_call(), vec![BEvent::Transfer { from: default_bonded_account(), to: 40, amount: 20 },] ); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 40, pool_id: 1, balance: 20, points: 20 }, Event::MemberRemoved { pool_id: 1, member: 40 } ] ); assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 3 => UnbondPool { points: 550 / 2, balance: 550 / 2 }} ); // When assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(550), 550, 0)); // Then assert_eq!( balances_events_since_last_call(), vec![BEvent::Transfer { from: default_bonded_account(), to: 550, amount: 275 },] ); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 550, pool_id: 1, balance: 275, points: 275 }, Event::MemberRemoved { pool_id: 1, member: 550 } ] ); assert!(SubPoolsStorage::::get(1).unwrap().with_era.is_empty()); // now, finally, the depositor can take out its share. unsafe_set_state(1, PoolState::Destroying); assert_ok!(fully_unbond_permissioned(10)); // because everyone else has left, the points assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 6 => UnbondPool { points: 5, balance: 5 }} ); CurrentEra::set(CurrentEra::get() + 3); // set metadata to check that it's being removed on dissolve assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); assert!(Metadata::::contains_key(1)); // when assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); // then assert_eq!(Currency::free_balance(&10), 10 + 35); assert_eq!(Currency::free_balance(&default_bonded_account()), 0); // in this test 10 also gets a fair share of the slash, because the slash was // applied to the bonded account. assert_eq!( pool_events_since_last_call(), vec![ Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 6 }, Event::Withdrawn { member: 10, pool_id: 1, points: 5, balance: 5 }, Event::MemberRemoved { pool_id: 1, member: 10 }, Event::Destroyed { pool_id: 1 } ] ); assert!(!Metadata::::contains_key(1)); assert_eq!( balances_events_since_last_call(), vec![ BEvent::Transfer { from: default_bonded_account(), to: 10, amount: 5 }, BEvent::Thawed { who: default_reward_account(), amount: 5 }, BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } ] ); }); } #[test] fn withdraw_unbonded_handles_faulty_sub_pool_accounting() { ExtBuilder::default().build_and_execute(|| { // Given assert_eq!(Currency::minimum_balance(), 5); assert_eq!(Currency::free_balance(&10), 35); assert_eq!(Currency::free_balance(&default_bonded_account()), 10); unsafe_set_state(1, PoolState::Destroying); assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(10), 10)); // Simulate a slash that is not accounted for in the sub pools. Currency::set_balance(&default_bonded_account(), 5); assert_eq!( SubPoolsStorage::::get(1).unwrap().with_era, //------------------------------balance decrease is not account for unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 } } ); CurrentEra::set(3); // When assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); // Then assert_eq!(Currency::free_balance(&10), 10 + 35); assert_eq!(Currency::free_balance(&default_bonded_account()), 0); }); } #[test] fn withdraw_unbonded_errors_correctly() { ExtBuilder::default().with_check(0).build_and_execute(|| { // Insert the sub-pool let sub_pools = SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 }}, }; SubPoolsStorage::::insert(1, sub_pools.clone()); assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), Error::::PoolMemberNotFound ); let mut member = PoolMember { pool_id: 1, points: 10, ..Default::default() }; PoolMembers::::insert(11, member.clone()); // Simulate calling `unbond` member.unbonding_eras = member_unbonding_eras!(3 => 10); PoolMembers::::insert(11, member.clone()); // We are still in the bonding duration assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), Error::::CannotWithdrawAny ); // If we error the member does not get removed assert_eq!(PoolMembers::::get(11), Some(member)); // and the sub pools do not get updated. assert_eq!(SubPoolsStorage::::get(1).unwrap(), sub_pools) }); } #[test] fn withdraw_unbonded_kick() { ExtBuilder::default() .add_members(vec![(100, 100), (200, 200)]) .build_and_execute(|| { // Given assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(100), 100)); assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(200), 200)); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 3, points: 10, roles: DEFAULT_ROLES, state: PoolState::Open, } } ); CurrentEra::set(StakingMock::bonding_duration()); // Cannot kick when pool is open assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(902), 100, 0), Error::::NotKickerOrDestroying ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, Event::Bonded { member: 200, pool_id: 1, bonded: 200, joined: true }, Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, Event::Unbonded { member: 200, pool_id: 1, points: 200, balance: 200, era: 3 } ] ); // Given unsafe_set_state(1, PoolState::Blocked); // Cannot kick as a nominator assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(901), 100, 0), Error::::NotKickerOrDestroying ); // Can kick as root assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(900), 100, 0)); // Can kick as bouncer assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(900), 200, 0)); assert_eq!(Currency::free_balance(&100), 100 + 100); assert_eq!(Currency::free_balance(&200), 200 + 200); assert!(!PoolMembers::::contains_key(100)); assert!(!PoolMembers::::contains_key(200)); assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 100, pool_id: 1, points: 100, balance: 100 }, Event::MemberRemoved { pool_id: 1, member: 100 }, Event::Withdrawn { member: 200, pool_id: 1, points: 200, balance: 200 }, Event::MemberRemoved { pool_id: 1, member: 200 } ] ); }); } #[test] fn withdraw_unbonded_destroying_permissionless() { ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { // Given assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(100), 100)); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter: 2, points: 10, roles: DEFAULT_ROLES, state: PoolState::Open, } } ); CurrentEra::set(StakingMock::bonding_duration()); assert_eq!(Currency::free_balance(&100), 100); // Cannot permissionlessly withdraw assert_noop!( Pools::fully_unbond(RuntimeOrigin::signed(420), 100), Error::::NotKickerOrDestroying ); // Given unsafe_set_state(1, PoolState::Destroying); // Can permissionlessly withdraw a member that is not the depositor assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(420), 100, 0)); assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default(),); assert_eq!(Currency::free_balance(&100), 100 + 100); assert!(!PoolMembers::::contains_key(100)); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 }, Event::Withdrawn { member: 100, pool_id: 1, points: 100, balance: 100 }, Event::MemberRemoved { pool_id: 1, member: 100 } ] ); }); } #[test] fn partial_withdraw_unbonded_depositor() { ExtBuilder::default().ed(1).build_and_execute(|| { assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); unsafe_set_state(1, PoolState::Destroying); // given assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 6)); CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 1)); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(3 => 6, 4 => 1) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }, 4 => UnbondPool { points: 1, balance: 1 } } } ); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 13); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 7); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, Event::Unbonded { member: 10, pool_id: 1, points: 6, balance: 6, era: 3 }, Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 } ] ); // when CurrentEra::set(2); assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0), Error::::CannotWithdrawAny ); // when CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); // then assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(4 => 1) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 4 => UnbondPool { points: 1, balance: 1 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Withdrawn { member: 10, pool_id: 1, points: 6, balance: 6 }] ); // when CurrentEra::set(4); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); // then assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!() ); assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); assert_eq!( pool_events_since_last_call(), vec![Event::Withdrawn { member: 10, pool_id: 1, points: 1, balance: 1 },] ); // when repeating: assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0), Error::::CannotWithdrawAny ); }); } #[test] fn partial_withdraw_unbonded_non_depositor() { ExtBuilder::default().add_members(vec![(11, 10)]).build_and_execute(|| { // given assert_ok!(Pools::unbond(RuntimeOrigin::signed(11), 11, 6)); CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(11), 11, 1)); assert_eq!( PoolMembers::::get(11).unwrap().unbonding_eras, member_unbonding_eras!(3 => 6, 4 => 1) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }, 4 => UnbondPool { points: 1, balance: 1 } } } ); assert_eq!(PoolMembers::::get(11).unwrap().active_points(), 3); assert_eq!(PoolMembers::::get(11).unwrap().unbonding_points(), 7); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 11, pool_id: 1, bonded: 10, joined: true }, Event::Unbonded { member: 11, pool_id: 1, points: 6, balance: 6, era: 3 }, Event::Unbonded { member: 11, pool_id: 1, points: 1, balance: 1, era: 4 } ] ); // when CurrentEra::set(2); assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), Error::::CannotWithdrawAny ); // when CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0)); // then assert_eq!( PoolMembers::::get(11).unwrap().unbonding_eras, member_unbonding_eras!(4 => 1) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 4 => UnbondPool { points: 1, balance: 1 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Withdrawn { member: 11, pool_id: 1, points: 6, balance: 6 }] ); // when CurrentEra::set(4); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0)); // then assert_eq!( PoolMembers::::get(11).unwrap().unbonding_eras, member_unbonding_eras!() ); assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); assert_eq!( pool_events_since_last_call(), vec![Event::Withdrawn { member: 11, pool_id: 1, points: 1, balance: 1 }] ); // when repeating: assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0), Error::::CannotWithdrawAny ); }); } #[test] fn full_multi_step_withdrawing_non_depositor() { ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { assert_eq!(TotalValueLocked::::get(), 110); // given assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 75)); assert_eq!( PoolMembers::::get(100).unwrap().unbonding_eras, member_unbonding_eras!(3 => 75) ); // tvl unchanged. assert_eq!(TotalValueLocked::::get(), 110); // progress one era and unbond the leftover. CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 25)); assert_eq!( PoolMembers::::get(100).unwrap().unbonding_eras, member_unbonding_eras!(3 => 75, 4 => 25) ); assert_noop!( Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0), Error::::CannotWithdrawAny ); // tvl unchanged. assert_eq!(TotalValueLocked::::get(), 110); // now the 75 should be free. CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 100, pool_id: 1, bonded: 100, joined: true }, Event::Unbonded { member: 100, pool_id: 1, points: 75, balance: 75, era: 3 }, Event::Unbonded { member: 100, pool_id: 1, points: 25, balance: 25, era: 4 }, Event::Withdrawn { member: 100, pool_id: 1, points: 75, balance: 75 }, ] ); assert_eq!( PoolMembers::::get(100).unwrap().unbonding_eras, member_unbonding_eras!(4 => 25) ); // tvl updated assert_eq!(TotalValueLocked::::get(), 35); // the 25 should be free now, and the member removed. CurrentEra::set(4); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 100, pool_id: 1, points: 25, balance: 25 }, Event::MemberRemoved { pool_id: 1, member: 100 } ] ); }) } #[test] fn out_of_sync_unbonding_chunks() { // the unbonding_eras in pool member are always fixed to the era at which they are unlocked, // but the actual unbonding pools get pruned and might get combined in the no_era pool. // Pools are only merged when one unbonds, so we unbond a little bit on every era to // simulate this. ExtBuilder::default() .add_members(vec![(20, 100), (30, 100)]) .build_and_execute(|| { System::reset_events(); // when assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); assert_ok!(Pools::unbond(RuntimeOrigin::signed(30), 30, 5)); // then member-local unbonding is pretty much in sync with the global pools. assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 5) ); assert_eq!( PoolMembers::::get(30).unwrap().unbonding_eras, member_unbonding_eras!(3 => 5) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 } } } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 3 }, Event::Unbonded { member: 30, pool_id: 1, points: 5, balance: 5, era: 3 }, ] ); // when CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); // then still member-local unbonding is pretty much in sync with the global pools. assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 5, 4 => 5) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 }, 4 => UnbondPool { points: 5, balance: 5 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 4 }] ); // when CurrentEra::set(2); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); // then still member-local unbonding is pretty much in sync with the global pools. assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { no_era: Default::default(), with_era: unbonding_pools_with_era! { 3 => UnbondPool { points: 10, balance: 10 }, 4 => UnbondPool { points: 5, balance: 5 }, 5 => UnbondPool { points: 5, balance: 5 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 }] ); // when CurrentEra::set(5); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5)); // then assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5, 8 => 5) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { // era 3 is merged into no_era. no_era: UnbondPool { points: 10, balance: 10 }, with_era: unbonding_pools_with_era! { 4 => UnbondPool { points: 5, balance: 5 }, 5 => UnbondPool { points: 5, balance: 5 }, 8 => UnbondPool { points: 5, balance: 5 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 8 }] ); // now we start withdrawing unlocked bonds. // when assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); // then assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, member_unbonding_eras!(8 => 5) ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { // era 3 is merged into no_era. no_era: UnbondPool { points: 5, balance: 5 }, with_era: unbonding_pools_with_era! { 8 => UnbondPool { points: 5, balance: 5 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Withdrawn { member: 20, pool_id: 1, points: 15, balance: 15 }] ); // when assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(30), 30, 0)); // then assert_eq!( PoolMembers::::get(30).unwrap().unbonding_eras, member_unbonding_eras!() ); assert_eq!( SubPoolsStorage::::get(1).unwrap(), SubPools { // era 3 is merged into no_era. no_era: Default::default(), with_era: unbonding_pools_with_era! { 8 => UnbondPool { points: 5, balance: 5 } } } ); assert_eq!( pool_events_since_last_call(), vec![Event::Withdrawn { member: 30, pool_id: 1, points: 5, balance: 5 }] ); }) } #[test] fn full_multi_step_withdrawing_depositor() { ExtBuilder::default().ed(1).build_and_execute(|| { // depositor now has 20, they can unbond to 10. assert_eq!(Pools::depositor_min_bond(), 10); assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); // now they can. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 7)); // progress one era and unbond the leftover. CurrentEra::set(1); assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3)); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(3 => 7, 4 => 3) ); // they can't unbond to a value below 10 other than 0.. assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 5), Error::::MinimumBondNotMet ); // but not even full, because they pool is not yet destroying. assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 10), Error::::MinimumBondNotMet ); // but now they can. unsafe_set_state(1, PoolState::Destroying); assert_noop!( Pools::unbond(RuntimeOrigin::signed(10), 10, 5), Error::::MinimumBondNotMet ); assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10)); // now the 7 should be free. CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false }, Event::Unbonded { member: 10, pool_id: 1, balance: 7, points: 7, era: 3 }, Event::Unbonded { member: 10, pool_id: 1, balance: 3, points: 3, era: 4 }, Event::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 }, Event::Withdrawn { member: 10, pool_id: 1, balance: 7, points: 7 } ] ); assert_eq!( PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(4 => 13) ); // the 13 should be free now, and the member removed. CurrentEra::set(4); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 10, pool_id: 1, points: 13, balance: 13 }, Event::MemberRemoved { pool_id: 1, member: 10 }, Event::Destroyed { pool_id: 1 }, ] ); assert!(!Metadata::::contains_key(1)); }) } #[test] fn withdraw_unbonded_removes_claim_permissions_on_leave() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { // Given CurrentEra::set(1); assert_eq!(PoolMembers::::get(20).unwrap().points, 20); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 20)); assert_eq!( ClaimPermissions::::get(20), ClaimPermission::PermissionlessWithdraw ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 4 }, ] ); CurrentEra::set(5); // When assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0)); assert_eq!( pool_events_since_last_call(), vec![ Event::Withdrawn { member: 20, pool_id: 1, balance: 20, points: 20 }, Event::MemberRemoved { pool_id: 1, member: 20 } ] ); // Then assert_eq!(PoolMembers::::get(20), None); assert_eq!(ClaimPermissions::::contains_key(20), false); }); } } mod create { use super::*; use frame_support::traits::fungible::InspectFreeze; #[test] fn create_works() { ExtBuilder::default().build_and_execute(|| { // next pool id is 2. let next_pool_stash = Pools::create_bonded_account(2); let ed = Currency::minimum_balance(); assert_eq!(TotalValueLocked::::get(), 10); assert!(!BondedPools::::contains_key(2)); assert!(!RewardPools::::contains_key(2)); assert!(!PoolMembers::::contains_key(11)); assert_err!(StakingMock::active_stake(&next_pool_stash), "balance not found"); Currency::set_balance(&11, StakingMock::minimum_nominator_bond() + ed); assert_ok!(Pools::create( RuntimeOrigin::signed(11), StakingMock::minimum_nominator_bond(), 123, 456, 789 )); assert_eq!(TotalValueLocked::::get(), 10 + StakingMock::minimum_nominator_bond()); assert_eq!(Currency::free_balance(&11), 0); assert_eq!( PoolMembers::::get(11).unwrap(), PoolMember { pool_id: 2, points: StakingMock::minimum_nominator_bond(), ..Default::default() } ); assert_eq!( BondedPool::::get(2).unwrap(), BondedPool { id: 2, inner: BondedPoolInner { commission: Commission::default(), points: StakingMock::minimum_nominator_bond(), member_counter: 1, roles: PoolRoles { depositor: 11, root: Some(123), nominator: Some(456), bouncer: Some(789) }, state: PoolState::Open, } } ); assert_eq!( StakingMock::active_stake(&next_pool_stash).unwrap(), StakingMock::minimum_nominator_bond() ); assert_eq!( RewardPools::::get(2).unwrap(), RewardPool { ..Default::default() } ); // make sure ED is frozen on pool creation. assert_eq!( Currency::balance_frozen( &FreezeReason::PoolMinBalance.into(), &default_reward_account() ), Currency::minimum_balance() ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Created { depositor: 11, pool_id: 2 }, Event::Bonded { member: 11, pool_id: 2, bonded: 10, joined: true } ] ); }); } #[test] fn create_errors_correctly() { ExtBuilder::default().with_check(0).build_and_execute(|| { assert_noop!( Pools::create(RuntimeOrigin::signed(10), 420, 123, 456, 789), Error::::AccountBelongsToOtherPool ); // Given assert_eq!(MinCreateBond::::get(), 2); assert_eq!(StakingMock::minimum_nominator_bond(), 10); // Then assert_noop!( Pools::create(RuntimeOrigin::signed(11), 9, 123, 456, 789), Error::::MinimumBondNotMet ); // Given MinCreateBond::::put(20); // Then assert_noop!( Pools::create(RuntimeOrigin::signed(11), 19, 123, 456, 789), Error::::MinimumBondNotMet ); // Given BondedPool:: { id: 2, inner: BondedPoolInner { commission: Commission::default(), member_counter: 1, points: 10, roles: DEFAULT_ROLES, state: PoolState::Open, }, } .put(); assert_eq!(MaxPools::::get(), Some(2)); assert_eq!(BondedPools::::count(), 2); // Then assert_noop!( Pools::create(RuntimeOrigin::signed(11), 20, 123, 456, 789), Error::::MaxPools ); // Given assert_eq!(PoolMembers::::count(), 1); MaxPools::::put(3); MaxPoolMembers::::put(1); Currency::set_balance(&11, 5 + 20); // Then let create = RuntimeCall::Pools(Call::::create { amount: 20, root: 11, nominator: 11, bouncer: 11, }); assert_noop!( create.dispatch(RuntimeOrigin::signed(11)), Error::::MaxPoolMembers ); }); } #[test] fn create_with_pool_id_works() { ExtBuilder::default().build_and_execute(|| { let ed = Currency::minimum_balance(); Currency::set_balance(&11, StakingMock::minimum_nominator_bond() + ed); assert_ok!(Pools::create( RuntimeOrigin::signed(11), StakingMock::minimum_nominator_bond(), 123, 456, 789 )); assert_eq!(Currency::free_balance(&11), 0); // delete the initial pool created, then pool_Id `1` will be free assert_noop!( Pools::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 1), Error::::PoolIdInUse ); assert_noop!( Pools::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 3), Error::::InvalidPoolId ); // start dismantling the pool. assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); assert_ok!(fully_unbond_permissioned(10)); CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 10)); assert_ok!(Pools::create_with_pool_id(RuntimeOrigin::signed(10), 20, 234, 654, 783, 1)); }); } } #[test] fn set_claim_permission_works() { ExtBuilder::default().build_and_execute(|| { // Given Currency::set_balance(&11, ExistentialDeposit::get() + 2); assert!(!PoolMembers::::contains_key(11)); // When assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1)); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, ] ); // Make permissioned assert_eq!(ClaimPermissions::::get(11), ClaimPermission::PermissionlessWithdraw); assert_noop!( Pools::set_claim_permission(RuntimeOrigin::signed(12), ClaimPermission::Permissioned), Error::::PoolMemberNotFound ); assert_ok!(Pools::set_claim_permission( RuntimeOrigin::signed(11), ClaimPermission::Permissioned )); // then assert_eq!(ClaimPermissions::::get(11), ClaimPermission::Permissioned); }); } mod nominate { use super::*; #[test] fn nominate_works() { ExtBuilder::default().build_and_execute(|| { // Depositor can't nominate assert_noop!( Pools::nominate(RuntimeOrigin::signed(10), 1, vec![21]), Error::::NotNominator ); // bouncer can't nominate assert_noop!( Pools::nominate(RuntimeOrigin::signed(902), 1, vec![21]), Error::::NotNominator ); // if `depositor` stake is less than the `MinimumNominatorBond`, they can't nominate StakingMinBond::set(20); // Can't nominate if depositor's stake is less than the `MinimumNominatorBond` assert_noop!( Pools::nominate(RuntimeOrigin::signed(900), 1, vec![21]), Error::::MinimumBondNotMet ); // restore `MinimumNominatorBond` StakingMinBond::set(10); // Root can nominate assert_ok!(Pools::nominate(RuntimeOrigin::signed(900), 1, vec![21])); assert_eq!(Nominations::get().unwrap(), vec![21]); // Nominator can nominate assert_ok!(Pools::nominate(RuntimeOrigin::signed(901), 1, vec![31])); assert_eq!(Nominations::get().unwrap(), vec![31]); // Can't nominate for a pool that doesn't exist assert_noop!( Pools::nominate(RuntimeOrigin::signed(902), 123, vec![21]), Error::::PoolNotFound ); }); } } mod set_state { use super::*; #[test] fn set_state_works() { ExtBuilder::default().build_and_execute(|| { // Given assert_ok!(BondedPool::::get(1).unwrap().ok_to_be_open()); // Only the root and bouncer can change the state when the pool is ok to be open. assert_noop!( Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Blocked), Error::::CanNotChangeState ); assert_noop!( Pools::set_state(RuntimeOrigin::signed(901), 1, PoolState::Blocked), Error::::CanNotChangeState ); // Root can change state assert_ok!(Pools::set_state(RuntimeOrigin::signed(900), 1, PoolState::Blocked)); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::StateChanged { pool_id: 1, new_state: PoolState::Blocked } ] ); assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Blocked); // bouncer can change state assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying)); assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); // If the pool is destroying, then no one can set state assert_noop!( Pools::set_state(RuntimeOrigin::signed(900), 1, PoolState::Blocked), Error::::CanNotChangeState ); assert_noop!( Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Blocked), Error::::CanNotChangeState ); // If the pool is not ok to be open, then anyone can set it to destroying // Given unsafe_set_state(1, PoolState::Open); // slash the pool to the point that `max_points_to_balance` ratio is // surpassed. Making this pool destroyable by anyone. StakingMock::slash_by(1, 10); // When assert_ok!(Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying)); // Then assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); // Given Currency::set_balance(&default_bonded_account(), Balance::MAX / 10); unsafe_set_state(1, PoolState::Open); // When assert_ok!(Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Destroying)); // Then assert_eq!(BondedPool::::get(1).unwrap().state, PoolState::Destroying); // If the pool is not ok to be open, it cannot be permissionlessly set to a state that // isn't destroying unsafe_set_state(1, PoolState::Open); assert_noop!( Pools::set_state(RuntimeOrigin::signed(11), 1, PoolState::Blocked), Error::::CanNotChangeState ); assert_eq!( pool_events_since_last_call(), vec![ Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, Event::PoolSlashed { pool_id: 1, balance: 0 }, Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying }, Event::StateChanged { pool_id: 1, new_state: PoolState::Destroying } ] ); }); } } mod set_metadata { use super::*; #[test] fn set_metadata_works() { ExtBuilder::default().build_and_execute(|| { // Root can set metadata assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1])); assert_eq!(Metadata::::get(1), vec![1, 1]); // bouncer can set metadata assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(902), 1, vec![2, 2])); assert_eq!(Metadata::::get(1), vec![2, 2]); // Depositor can't set metadata assert_noop!( Pools::set_metadata(RuntimeOrigin::signed(10), 1, vec![3, 3]), Error::::DoesNotHavePermission ); // Nominator can't set metadata assert_noop!( Pools::set_metadata(RuntimeOrigin::signed(901), 1, vec![3, 3]), Error::::DoesNotHavePermission ); // Metadata cannot be longer than `MaxMetadataLen` assert_noop!( Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1, 1]), Error::::MetadataExceedsMaxLen ); }); } } mod set_configs { use super::*; #[test] fn set_configs_works() { ExtBuilder::default().build_and_execute(|| { // only admin origin can set configs assert_noop!( Pools::set_configs( RuntimeOrigin::signed(20), ConfigOp::Set(1 as Balance), ConfigOp::Set(2 as Balance), ConfigOp::Set(3u32), ConfigOp::Set(4u32), ConfigOp::Set(5u32), ConfigOp::Set(Perbill::from_percent(6)) ), BadOrigin ); // Setting works by Admin (42) assert_ok!(Pools::set_configs( RuntimeOrigin::signed(42), ConfigOp::Set(1 as Balance), ConfigOp::Set(2 as Balance), ConfigOp::Set(3u32), ConfigOp::Set(4u32), ConfigOp::Set(5u32), ConfigOp::Set(Perbill::from_percent(6)) )); assert_eq!(MinJoinBond::::get(), 1); assert_eq!(MinCreateBond::::get(), 2); assert_eq!(MaxPools::::get(), Some(3)); assert_eq!(MaxPoolMembers::::get(), Some(4)); assert_eq!(MaxPoolMembersPerPool::::get(), Some(5)); assert_eq!(GlobalMaxCommission::::get(), Some(Perbill::from_percent(6))); // Noop does nothing assert_storage_noop!(assert_ok!(Pools::set_configs( RuntimeOrigin::signed(42), ConfigOp::Noop, ConfigOp::Noop, ConfigOp::Noop, ConfigOp::Noop, ConfigOp::Noop, ConfigOp::Noop, ))); // Removing works assert_ok!(Pools::set_configs( RuntimeOrigin::signed(42), ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, )); assert_eq!(MinJoinBond::::get(), 0); assert_eq!(MinCreateBond::::get(), 0); assert_eq!(MaxPools::::get(), None); assert_eq!(MaxPoolMembers::::get(), None); assert_eq!(MaxPoolMembersPerPool::::get(), None); assert_eq!(GlobalMaxCommission::::get(), None); }); } } mod bond_extra { use super::*; use crate::Event; #[test] fn bond_extra_from_free_balance_creator() { ExtBuilder::default().build_and_execute(|| { // 10 is the owner and a member in pool 1, give them some more funds. Currency::set_balance(&10, 100); // given assert_eq!(PoolMembers::::get(10).unwrap().points, 10); assert_eq!(BondedPools::::get(1).unwrap().points, 10); assert_eq!(Currency::free_balance(&10), 100); // when assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10))); // then assert_eq!(Currency::free_balance(&10), 90); assert_eq!(PoolMembers::::get(10).unwrap().points, 20); assert_eq!(BondedPools::::get(1).unwrap().points, 20); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: false } ] ); // when assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(20))); // then assert_eq!(Currency::free_balance(&10), 70); assert_eq!(PoolMembers::::get(10).unwrap().points, 40); assert_eq!(BondedPools::::get(1).unwrap().points, 40); assert_eq!( pool_events_since_last_call(), vec![Event::Bonded { member: 10, pool_id: 1, bonded: 20, joined: false }] ); }) } #[test] fn bond_extra_from_rewards_creator() { ExtBuilder::default().build_and_execute(|| { // put some money in the reward account, all of which will belong to 10 as the only // member of the pool. Currency::set_balance(&default_reward_account(), 7); // ... if which only 2 is claimable to make sure the reward account does not die. let claimable_reward = 7 - ExistentialDeposit::get(); // given assert_eq!(PoolMembers::::get(10).unwrap().points, 10); assert_eq!(BondedPools::::get(1).unwrap().points, 10); assert_eq!(Currency::free_balance(&10), 35); // when assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::Rewards)); // then assert_eq!(Currency::free_balance(&10), 35); assert_eq!(PoolMembers::::get(10).unwrap().points, 10 + claimable_reward); assert_eq!(BondedPools::::get(1).unwrap().points, 10 + claimable_reward); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: claimable_reward }, Event::Bonded { member: 10, pool_id: 1, bonded: claimable_reward, joined: false } ] ); }) } #[test] fn bond_extra_from_rewards_joiner() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { // put some money in the reward account, all of which will belong to 10 as the only // member of the pool. Currency::set_balance(&default_reward_account(), 8); // ... if which only 3 is claimable to make sure the reward account does not die. let claimable_reward = 8 - ExistentialDeposit::get(); // NOTE: easier to read of we use 3, so let's use the number instead of variable. assert_eq!(claimable_reward, 3, "test is correct if rewards are divisible by 3"); // given assert_eq!(PoolMembers::::get(10).unwrap().points, 10); assert_eq!(PoolMembers::::get(20).unwrap().points, 20); assert_eq!(BondedPools::::get(1).unwrap().points, 30); assert_eq!(Currency::free_balance(&10), 35); assert_eq!(Currency::free_balance(&20), 20); assert_eq!(TotalValueLocked::::get(), 30); // when assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::Rewards)); assert_eq!(Currency::free_balance(&default_reward_account()), 7); // then assert_eq!(Currency::free_balance(&10), 35); assert_eq!(TotalValueLocked::::get(), 31); // 10's share of the reward is 1/3, since they gave 10/30 of the total shares. assert_eq!(PoolMembers::::get(10).unwrap().points, 10 + 1); assert_eq!(BondedPools::::get(1).unwrap().points, 30 + 1); // when assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(20), BondExtra::Rewards)); // then assert_eq!(Currency::free_balance(&20), 20); assert_eq!(TotalValueLocked::::get(), 33); // 20's share of the rewards is the other 2/3 of the rewards, since they have 20/30 of // the shares assert_eq!(PoolMembers::::get(20).unwrap().points, 20 + 2); assert_eq!(BondedPools::::get(1).unwrap().points, 30 + 3); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 1, joined: false }, Event::PaidOut { member: 20, pool_id: 1, payout: 2 }, Event::Bonded { member: 20, pool_id: 1, bonded: 2, joined: false } ] ); }) } #[test] fn bond_extra_other() { ExtBuilder::default().add_members(vec![(20, 20)]).build_and_execute(|| { Currency::set_balance(&default_reward_account(), 8); // ... of which only 3 are claimable to make sure the reward account does not die. let claimable_reward = 8 - ExistentialDeposit::get(); // NOTE: easier to read if we use 3, so let's use the number instead of variable. assert_eq!(claimable_reward, 3, "test is correct if rewards are divisible by 3"); // given assert_eq!(PoolMembers::::get(10).unwrap().points, 10); assert_eq!(PoolMembers::::get(20).unwrap().points, 20); assert_eq!(BondedPools::::get(1).unwrap().points, 30); assert_eq!(Currency::free_balance(&10), 35); assert_eq!(Currency::free_balance(&20), 20); // Permissioned by default assert_noop!( Pools::bond_extra_other(RuntimeOrigin::signed(80), 20, BondExtra::Rewards), Error::::DoesNotHavePermission ); assert_ok!(Pools::set_claim_permission( RuntimeOrigin::signed(10), ClaimPermission::PermissionlessCompound )); assert_ok!(Pools::bond_extra_other(RuntimeOrigin::signed(50), 10, BondExtra::Rewards)); assert_eq!(Currency::free_balance(&default_reward_account()), 7); // then assert_eq!(Currency::free_balance(&10), 35); assert_eq!(PoolMembers::::get(10).unwrap().points, 10 + 1); assert_eq!(BondedPools::::get(1).unwrap().points, 30 + 1); // when assert_noop!( Pools::bond_extra_other(RuntimeOrigin::signed(40), 40, BondExtra::Rewards), Error::::PoolMemberNotFound ); // when assert_ok!(Pools::bond_extra_other( RuntimeOrigin::signed(20), 20, BondExtra::FreeBalance(10) )); // then assert_eq!(Currency::free_balance(&20), 12); assert_eq!(Currency::free_balance(&default_reward_account()), 5); assert_eq!(PoolMembers::::get(20).unwrap().points, 30); assert_eq!(BondedPools::::get(1).unwrap().points, 41); }) } } mod update_roles { use super::*; #[test] fn update_roles_works() { ExtBuilder::default().build_and_execute(|| { assert_eq!( BondedPools::::get(1).unwrap().roles, PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), bouncer: Some(902) }, ); // non-existent pools assert_noop!( Pools::update_roles( RuntimeOrigin::signed(1), 2, ConfigOp::Set(5), ConfigOp::Set(6), ConfigOp::Set(7) ), Error::::PoolNotFound, ); // depositor cannot change roles. assert_noop!( Pools::update_roles( RuntimeOrigin::signed(1), 1, ConfigOp::Set(5), ConfigOp::Set(6), ConfigOp::Set(7) ), Error::::DoesNotHavePermission, ); // nominator cannot change roles. assert_noop!( Pools::update_roles( RuntimeOrigin::signed(901), 1, ConfigOp::Set(5), ConfigOp::Set(6), ConfigOp::Set(7) ), Error::::DoesNotHavePermission, ); // bouncer assert_noop!( Pools::update_roles( RuntimeOrigin::signed(902), 1, ConfigOp::Set(5), ConfigOp::Set(6), ConfigOp::Set(7) ), Error::::DoesNotHavePermission, ); // but root can assert_ok!(Pools::update_roles( RuntimeOrigin::signed(900), 1, ConfigOp::Set(5), ConfigOp::Set(6), ConfigOp::Set(7) )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::RolesUpdated { root: Some(5), bouncer: Some(7), nominator: Some(6) } ] ); assert_eq!( BondedPools::::get(1).unwrap().roles, PoolRoles { depositor: 10, root: Some(5), nominator: Some(6), bouncer: Some(7) }, ); // also root origin can assert_ok!(Pools::update_roles( RuntimeOrigin::root(), 1, ConfigOp::Set(1), ConfigOp::Set(2), ConfigOp::Set(3) )); assert_eq!( pool_events_since_last_call(), vec![Event::RolesUpdated { root: Some(1), bouncer: Some(3), nominator: Some(2) }] ); assert_eq!( BondedPools::::get(1).unwrap().roles, PoolRoles { depositor: 10, root: Some(1), nominator: Some(2), bouncer: Some(3) }, ); // Noop works assert_ok!(Pools::update_roles( RuntimeOrigin::root(), 1, ConfigOp::Set(11), ConfigOp::Noop, ConfigOp::Noop )); assert_eq!( pool_events_since_last_call(), vec![Event::RolesUpdated { root: Some(11), bouncer: Some(3), nominator: Some(2) }] ); assert_eq!( BondedPools::::get(1).unwrap().roles, PoolRoles { depositor: 10, root: Some(11), nominator: Some(2), bouncer: Some(3) }, ); // Remove works assert_ok!(Pools::update_roles( RuntimeOrigin::root(), 1, ConfigOp::Set(69), ConfigOp::Remove, ConfigOp::Remove )); assert_eq!( pool_events_since_last_call(), vec![Event::RolesUpdated { root: Some(69), bouncer: None, nominator: None }] ); assert_eq!( BondedPools::::get(1).unwrap().roles, PoolRoles { depositor: 10, root: Some(69), nominator: None, bouncer: None }, ); }) } } mod reward_counter_precision { use super::*; const DOT: Balance = 10u128.pow(10u32); const POLKADOT_TOTAL_ISSUANCE_GENESIS: Balance = DOT * 10u128.pow(9u32); const fn inflation(years: u128) -> u128 { let mut i = 0; let mut start = POLKADOT_TOTAL_ISSUANCE_GENESIS; while i < years { start = start + start / 10; i += 1 } start } fn default_pool_reward_counter() -> FixedU128 { let bonded_pool = BondedPools::::get(1).unwrap(); RewardPools::::get(1) .unwrap() .current_reward_counter(1, bonded_pool.points, bonded_pool.commission.current()) .unwrap() .0 } fn pending_rewards(of: AccountId) -> Option> { let member = PoolMembers::::get(of).unwrap(); assert_eq!(member.pool_id, 1); let rc = default_pool_reward_counter(); member.pending_rewards(rc).ok() } #[test] fn smallest_claimable_reward() { // create a pool that has all of the polkadot issuance in 50 years. let pool_bond = inflation(50); ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 1173908528796953165005, joined: true, } ] ); // the smallest reward that this pool can handle is let expected_smallest_reward = inflation(50) / 10u128.pow(18); // tad bit less. cannot be paid out. deposit_rewards(expected_smallest_reward - 1); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!(pool_events_since_last_call(), vec![]); // revert it. remove_rewards(expected_smallest_reward - 1); // tad bit more. can be claimed. deposit_rewards(expected_smallest_reward + 1); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 1173 }] ); }) } #[test] fn massive_reward_in_small_pool() { let tiny_bond = 1000 * DOT; ExtBuilder::default().ed(DOT).min_bond(tiny_bond).build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10000000000000, joined: true } ] ); Currency::set_balance(&20, tiny_bond); assert_ok!(Pools::join(RuntimeOrigin::signed(20), tiny_bond / 2, 1)); // Suddenly, add a shit ton of rewards. deposit_rewards(inflation(1)); // now claim. assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Bonded { member: 20, pool_id: 1, bonded: 5000000000000, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 7333333333333333333 }, Event::PaidOut { member: 20, pool_id: 1, payout: 3666666666666666666 } ] ); }) } #[test] fn reward_counter_calc_wont_fail_in_normal_polkadot_future() { // create a pool that has roughly half of the polkadot issuance in 10 years. let pool_bond = inflation(10) / 2; ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 12_968_712_300_500_000_000, joined: true, } ] ); // in 10 years, the total claimed rewards are large values as well. assuming that a pool // is earning all of the inflation per year (which is really unrealistic, but worse // case), that will be: let pool_total_earnings_10_years = inflation(10) - POLKADOT_TOTAL_ISSUANCE_GENESIS; deposit_rewards(pool_total_earnings_10_years); // some whale now joins with the other half ot the total issuance. This will bloat all // the calculation regarding current reward counter. Currency::set_balance(&20, pool_bond * 2); assert_ok!(Pools::join(RuntimeOrigin::signed(20), pool_bond, 1)); assert_eq!( pool_events_since_last_call(), vec![Event::Bonded { member: 20, pool_id: 1, bonded: 12_968_712_300_500_000_000, joined: true }] ); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 15937424600999999996 }] ); // now let a small member join with 10 DOTs. Currency::set_balance(&30, 20 * DOT); assert_ok!(Pools::join(RuntimeOrigin::signed(30), 10 * DOT, 1)); // and give a reasonably small reward to the pool. deposit_rewards(DOT); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(30))); assert_eq!( pool_events_since_last_call(), vec![ Event::Bonded { member: 30, pool_id: 1, bonded: 100000000000, joined: true }, // quite small, but working fine. Event::PaidOut { member: 30, pool_id: 1, payout: 38 } ] ); }) } #[test] fn reward_counter_update_can_fail_if_pool_is_highly_slashed() { // create a pool that has roughly half of the polkadot issuance in 10 years. let pool_bond = inflation(10) / 2; ExtBuilder::default().ed(DOT).min_bond(pool_bond).build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 12_968_712_300_500_000_000, joined: true, } ] ); // slash this pool by 99% of that. StakingMock::slash_by(1, pool_bond * 99 / 100); // some whale now joins with the other half ot the total issuance. This will trigger an // overflow. This test is actually a bit too lenient because all the reward counters are // set to zero. In other tests that we want to assert a scenario won't fail, we should // also set the reward counters to some large value. Currency::set_balance(&20, pool_bond * 2); assert_err!( Pools::join(RuntimeOrigin::signed(20), pool_bond, 1), Error::::OverflowRisk ); }) } #[test] fn if_small_member_waits_long_enough_they_will_earn_rewards() { // create a pool that has a quarter of the current polkadot issuance ExtBuilder::default() .ed(DOT) .min_bond(POLKADOT_TOTAL_ISSUANCE_GENESIS / 4) .build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 2500000000000000000, joined: true, } ] ); // and have a tiny fish join the pool as well.. Currency::set_balance(&20, 20 * DOT); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10 * DOT, 1)); // earn some small rewards deposit_rewards(DOT / 1000); // no point in claiming for 20 (nonetheless, it should be harmless) assert!(pending_rewards(20).unwrap().is_zero()); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( pool_events_since_last_call(), vec![ Event::Bonded { member: 20, pool_id: 1, bonded: 100000000000, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 9999997 } ] ); // earn some small more, still nothing can be claimed for 20, but 10 claims their // share. deposit_rewards(DOT / 1000); assert!(pending_rewards(20).unwrap().is_zero()); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 10000000 }] ); // earn some more rewards, this time 20 can also claim. deposit_rewards(DOT / 1000); assert_eq!(pending_rewards(20).unwrap(), 1); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 10000000 }, Event::PaidOut { member: 20, pool_id: 1, payout: 1 } ] ); }); } #[test] fn zero_reward_claim_does_not_update_reward_counter() { // create a pool that has a quarter of the current polkadot issuance ExtBuilder::default() .ed(DOT) .min_bond(POLKADOT_TOTAL_ISSUANCE_GENESIS / 4) .build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 2500000000000000000, joined: true, } ] ); // and have a tiny fish join the pool as well.. Currency::set_balance(&20, 20 * DOT); assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10 * DOT, 1)); // earn some small rewards deposit_rewards(DOT / 1000); // if 20 claims now, their reward counter should stay the same, so that they have a // chance of claiming this if they let it accumulate. Also see // `if_small_member_waits_long_enough_they_will_earn_rewards` assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(20))); assert_eq!( pool_events_since_last_call(), vec![ Event::Bonded { member: 20, pool_id: 1, bonded: 100000000000, joined: true }, Event::PaidOut { member: 10, pool_id: 1, payout: 9999997 } ] ); let current_reward_counter = default_pool_reward_counter(); // has been updated, because they actually claimed something. assert_eq!( PoolMembers::::get(10).unwrap().last_recorded_reward_counter, current_reward_counter ); // has not be updated, even though the claim transaction went through okay. assert_eq!( PoolMembers::::get(20).unwrap().last_recorded_reward_counter, Default::default() ); }); } } mod commission { use super::*; #[test] fn set_commission_works() { ExtBuilder::default().build_and_execute(|| { let pool_id = 1; let root = 900; // Commission can be set by the `root` role. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), pool_id, Some((Perbill::from_percent(50), root)) )); // Then: assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id }, Event::Bonded { member: 10, pool_id, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(50), root)) }, ] ); // Commission can be updated only, while keeping the same payee. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), 1, Some((Perbill::from_percent(25), root)) )); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(25), root)) },] ); // Payee can be updated only, while keeping the same commission. // Given: let payee = 901; // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), pool_id, Some((Perbill::from_percent(25), payee)) )); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(25), payee)) },] ); // Pool earns 80 points and a payout is triggered. // Given: deposit_rewards(80); assert_eq!( PoolMembers::::get(10).unwrap(), PoolMember:: { pool_id, points: 10, ..Default::default() } ); // When: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id, payout: 60 }] ); assert_eq!(RewardPool::::current_balance(pool_id), 20); // Pending pool commission can be claimed by the root role. // When: assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(root), pool_id)); // Then: assert_eq!(RewardPool::::current_balance(pool_id), 0); assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionClaimed { pool_id: 1, commission: 20 }] ); // Commission can be removed from the pool completely. // When: assert_ok!(Pools::set_commission(RuntimeOrigin::signed(root), pool_id, None)); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id, current: None },] ); // Given a pool now has a reward counter history, additional rewards and payouts can be // made while maintaining a correct ledger of the reward pool. Pool earns 100 points, // payout is triggered. // // Note that the `total_commission_pending` will not be updated until `update_records` // is next called, which is not done in this test segment.. // Given: deposit_rewards(100); // When: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id, payout: 100 },] ); assert_eq!( RewardPools::::get(pool_id).unwrap(), RewardPool { last_recorded_reward_counter: FixedU128::from_float(6.0), last_recorded_total_payouts: 80, total_rewards_claimed: 160, total_commission_pending: 0, total_commission_claimed: 20 } ); // When set commission is called again, update_records is called and // `total_commission_pending` is updated, based on the current reward counter and pool // balance. // // Note that commission is now 0%, so it should not come into play with subsequent // payouts. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), 1, Some((Perbill::from_percent(10), root)) )); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(10), root)) },] ); assert_eq!( RewardPools::::get(pool_id).unwrap(), RewardPool { last_recorded_reward_counter: FixedU128::from_float(16.0), last_recorded_total_payouts: 180, total_rewards_claimed: 160, total_commission_pending: 0, total_commission_claimed: 20 } ); // Supplying a 0% commission along with a payee results in a `None` current value. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), pool_id, Some((Perbill::from_percent(0), root)) )); // Then: assert_eq!( BondedPool::::get(1).unwrap().commission, Commission { current: None, max: None, change_rate: None, throttle_from: Some(1), claim_permission: None, } ); assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(0), root)) },] ); // The payee can be updated even when commission has reached maximum commission. Both // commission and max commission are set to 10% to test this. // Given: assert_ok!(Pools::set_commission_max( RuntimeOrigin::signed(root), pool_id, Perbill::from_percent(10) )); assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), pool_id, Some((Perbill::from_percent(10), root)) )); // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), pool_id, Some((Perbill::from_percent(10), payee)) )); // Then: assert_eq!( pool_events_since_last_call(), vec![ Event::PoolMaxCommissionUpdated { pool_id, max_commission: Perbill::from_percent(10) }, Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(10), root)) }, Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(10), payee)) } ] ); }); } #[test] fn commission_reward_counter_works_one_member() { ExtBuilder::default().build_and_execute(|| { let pool_id = 1; let root = 900; let member = 10; // Set the pool commission to 10% to test commission shares. Pool is topped up 40 points // and `member` immediately claims their pending rewards. Reward pool should still have // 10% share. // Given: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), 1, Some((Perbill::from_percent(10), root)), )); deposit_rewards(40); // When: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!(RewardPool::::current_balance(pool_id), 4); // Set pool commission to 20% and repeat the same process. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(root), 1, Some((Perbill::from_percent(20), root)), )); // Then: assert_eq!( RewardPools::::get(pool_id).unwrap(), RewardPool { last_recorded_reward_counter: FixedU128::from_float(3.6), last_recorded_total_payouts: 40, total_rewards_claimed: 36, total_commission_pending: 4, total_commission_claimed: 0 } ); // The current reward counter should yield the correct pending rewards of zero. // Given: let (current_reward_counter, _) = RewardPools::::get(pool_id) .unwrap() .current_reward_counter( pool_id, BondedPools::::get(pool_id).unwrap().points, Perbill::from_percent(20), ) .unwrap(); // Then: assert_eq!( PoolMembers::::get(member) .unwrap() .pending_rewards(current_reward_counter) .unwrap(), 0 ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(10), 900)) }, Event::PaidOut { member: 10, pool_id: 1, payout: 36 }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(20), 900)) } ] ); }) } #[test] fn set_commission_handles_errors() { ExtBuilder::default().build_and_execute(|| { assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, ] ); // Provided pool does not exist. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 9999, Some((Perbill::from_percent(1), 900)), ), Error::::PoolNotFound ); // Sender does not have permission to set commission. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(1), 1, Some((Perbill::from_percent(5), 900)), ), Error::::DoesNotHavePermission ); // Commission increases will be throttled if outside of change_rate allowance. // Commission is set to 5%. // Change rate is set to 1% max increase, 2 block delay. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(5), 900)), )); assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 2_u64 } )); assert_eq!( BondedPool::::get(1).unwrap().commission, Commission { current: Some((Perbill::from_percent(5), 900)), max: None, change_rate: Some(CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 2_u64 }), throttle_from: Some(1_u64), claim_permission: None, } ); assert_eq!( pool_events_since_last_call(), vec![ Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(5), 900)) }, Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 2 } } ] ); // Now try to increase commission to 10% (5% increase). This should be throttled. // Then: assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(10), 900)) ), Error::::CommissionChangeThrottled ); run_blocks(2); // Increase commission by 1% and provide an initial payee. This should succeed and set // the `throttle_from` field. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(6), 900)) )); assert_eq!( BondedPool::::get(1).unwrap().commission, Commission { current: Some((Perbill::from_percent(6), 900)), max: None, change_rate: Some(CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 2_u64 }), throttle_from: Some(3_u64), claim_permission: None, } ); assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(6), 900)) },] ); // Attempt to increase the commission an additional 1% (now 7%). This will fail as // `throttle_from` is now the current block. At least 2 blocks need to pass before we // can set commission again. // Then: assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(7), 900)) ), Error::::CommissionChangeThrottled ); run_blocks(2); // Can now successfully increase the commission again, to 7%. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(7), 900)), )); assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(7), 900)) },] ); run_blocks(2); // Now surpassed the `min_delay` threshold, but the `max_increase` threshold is // still at play. An attempted commission change now to 8% (+2% increase) should fail. // Then: assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(9), 900)), ), Error::::CommissionChangeThrottled ); // Now set a max commission to the current 5%. This will also update the current // commission to 5%. // When: assert_ok!(Pools::set_commission_max( RuntimeOrigin::signed(900), 1, Perbill::from_percent(5) )); assert_eq!( BondedPool::::get(1).unwrap().commission, Commission { current: Some((Perbill::from_percent(5), 900)), max: Some(Perbill::from_percent(5)), change_rate: Some(CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 2 }), throttle_from: Some(7), claim_permission: None, } ); assert_eq!( pool_events_since_last_call(), vec![ Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(5), 900)) }, Event::PoolMaxCommissionUpdated { pool_id: 1, max_commission: Perbill::from_percent(5) } ] ); // Run 2 blocks into the future so we are eligible to update commission again. run_blocks(2); // Now attempt again to increase the commission by 1%, to 6%. This is within the change // rate allowance, but `max_commission` will now prevent us from going any higher. // Then: assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(6), 900)), ), Error::::CommissionExceedsMaximum ); }); } #[test] fn set_commission_max_works_with_error_tests() { ExtBuilder::default().build_and_execute(|| { // Provided pool does not exist assert_noop!( Pools::set_commission_max( RuntimeOrigin::signed(900), 9999, Perbill::from_percent(1) ), Error::::PoolNotFound ); // Sender does not have permission to set commission assert_noop!( Pools::set_commission_max(RuntimeOrigin::signed(1), 1, Perbill::from_percent(5)), Error::::DoesNotHavePermission ); // Cannot set max commission above GlobalMaxCommission assert_noop!( Pools::set_commission_max( RuntimeOrigin::signed(900), 1, Perbill::from_percent(100) ), Error::::CommissionExceedsGlobalMaximum ); // Set a max commission commission pool 1 to 80% assert_ok!(Pools::set_commission_max( RuntimeOrigin::signed(900), 1, Perbill::from_percent(80) )); assert_eq!( BondedPools::::get(1).unwrap().commission.max, Some(Perbill::from_percent(80)) ); // We attempt to increase the max commission to 90%, but increasing is // disallowed due to pool's max commission. assert_noop!( Pools::set_commission_max(RuntimeOrigin::signed(900), 1, Perbill::from_percent(90)), Error::::MaxCommissionRestricted ); // We will now set a commission to 75% and then amend the max commission // to 50%. The max commission change should decrease the current // commission to 50%. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(75), 900)) )); assert_ok!(Pools::set_commission_max( RuntimeOrigin::signed(900), 1, Perbill::from_percent(50) )); assert_eq!( BondedPools::::get(1).unwrap().commission, Commission { current: Some((Perbill::from_percent(50), 900)), max: Some(Perbill::from_percent(50)), change_rate: None, throttle_from: Some(1), claim_permission: None, } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolMaxCommissionUpdated { pool_id: 1, max_commission: Perbill::from_percent(80) }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(75), 900)) }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(50), 900)) }, Event::PoolMaxCommissionUpdated { pool_id: 1, max_commission: Perbill::from_percent(50) } ] ); }); } #[test] fn set_commission_change_rate_works_with_errors() { ExtBuilder::default().build_and_execute(|| { // Provided pool does not exist assert_noop!( Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 9999, CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 1000_u64 } ), Error::::PoolNotFound ); // Sender does not have permission to set commission assert_noop!( Pools::set_commission_change_rate( RuntimeOrigin::signed(1), 1, CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 1000_u64 } ), Error::::DoesNotHavePermission ); // Set a commission change rate for pool 1 assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 10_u64 } )); assert_eq!( BondedPools::::get(1).unwrap().commission.change_rate, Some(CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 10_u64 }) ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 10 } }, ] ); // We now try to half the min_delay - this will be disallowed. A greater delay between // commission changes is seen as more restrictive. assert_noop!( Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 5_u64 } ), Error::::CommissionChangeRateNotAllowed ); // We now try to increase the allowed max_increase - this will fail. A smaller allowed // commission change is seen as more restrictive. assert_noop!( Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(10), min_delay: 10_u64 } ), Error::::CommissionChangeRateNotAllowed ); // Successful more restrictive change of min_delay with the current max_increase assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 20_u64 } )); // Successful more restrictive change of max_increase with the current min_delay assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(4), min_delay: 20_u64 } )); // Successful more restrictive change of both max_increase and min_delay assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(3), min_delay: 30_u64 } )); assert_eq!( pool_events_since_last_call(), vec![ Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(5), min_delay: 20 } }, Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(4), min_delay: 20 } }, Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(3), min_delay: 30 } } ] ); }); } #[test] fn change_rate_does_not_apply_to_decreasing_commission() { ExtBuilder::default().build_and_execute(|| { // set initial commission of the pool to 10%. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(10), 900)) )); // Set a commission change rate for pool 1, 1% every 10 blocks assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 10_u64 } )); assert_eq!( BondedPools::::get(1).unwrap().commission.change_rate, Some(CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 10_u64 }) ); // run `min_delay` blocks to allow a commission update. run_blocks(10_u64); // Test `max_increase`: attempt to decrease the commission by 5%. Should succeed. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(5), 900)) )); // Test `min_delay`: *immediately* attempt to decrease the commission by 2%. Should // succeed. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(3), 900)) )); // Attempt to *increase* the commission by 5%. Should fail. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(8), 900)) ), Error::::CommissionChangeThrottled ); // Sanity check: the resulting pool Commission state. assert_eq!( BondedPools::::get(1).unwrap().commission, Commission { current: Some((Perbill::from_percent(3), 900)), max: None, change_rate: Some(CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 10_u64 }), throttle_from: Some(11), claim_permission: None, } ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(10), 900)) }, Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 10 } }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(5), 900)) }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(3), 900)) } ] ); }); } #[test] fn set_commission_max_to_zero_works() { ExtBuilder::default().build_and_execute(|| { // 0% max commission test. // set commission max 0%. assert_ok!(Pools::set_commission_max(RuntimeOrigin::signed(900), 1, Zero::zero())); // a max commission of 0% essentially freezes the current commission, even when None. // All commission update attempts will fail. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(1), 900)) ), Error::::CommissionExceedsMaximum ); }) } #[test] fn set_commission_change_rate_zero_max_increase_works() { ExtBuilder::default().build_and_execute(|| { // set commission change rate to 0% per 10 blocks assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(0), min_delay: 10_u64 } )); // even though there is a min delay of 10 blocks, a max increase of 0% essentially // freezes the commission. All commission update attempts will fail. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(1), 900)) ), Error::::CommissionChangeThrottled ); }) } #[test] fn set_commission_change_rate_zero_min_delay_works() { ExtBuilder::default().build_and_execute(|| { // set commission change rate to 1% with a 0 block `min_delay`. assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 0_u64 } )); assert_eq!( BondedPools::::get(1).unwrap().commission, Commission { current: None, max: None, change_rate: Some(CommissionChangeRate { max_increase: Perbill::from_percent(1), min_delay: 0 }), throttle_from: Some(1), claim_permission: None, } ); // since there is no min delay, we should be able to immediately set the commission. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(1), 900)) )); // sanity check: increasing again to more than +1% will fail. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(3), 900)) ), Error::::CommissionChangeThrottled ); }) } #[test] fn set_commission_change_rate_zero_value_works() { ExtBuilder::default().build_and_execute(|| { // Check zero values play nice. 0 `min_delay` and 0% max_increase test. // set commission change rate to 0% per 0 blocks. assert_ok!(Pools::set_commission_change_rate( RuntimeOrigin::signed(900), 1, CommissionChangeRate { max_increase: Perbill::from_percent(0), min_delay: 0_u64 } )); // even though there is no min delay, a max increase of 0% essentially freezes the // commission. All commission update attempts will fail. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), 1, Some((Perbill::from_percent(1), 900)) ), Error::::CommissionChangeThrottled ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionChangeRateUpdated { pool_id: 1, change_rate: CommissionChangeRate { max_increase: Perbill::from_percent(0), min_delay: 0_u64 } } ] ); }) } #[test] fn do_reward_payout_with_various_commissions() { ExtBuilder::default().build_and_execute(|| { // turn off GlobalMaxCommission for this test. GlobalMaxCommission::::set(None); let pool_id = 1; // top up commission payee account to existential deposit let _ = Currency::set_balance(&2, 5); // Set a commission pool 1 to 33%, with a payee set to `2` assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), pool_id, Some((Perbill::from_percent(33), 2)), )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(33), 2)) }, ] ); // The pool earns 10 points deposit_rewards(10); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 7 },] ); // The pool earns 17 points deposit_rewards(17); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 11 },] ); // The pool earns 50 points deposit_rewards(50); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 34 },] ); // The pool earns 10439 points deposit_rewards(10439); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PaidOut { member: 10, pool_id: 1, payout: 6994 },] ); // Set the commission to 100% and ensure the following payout to the pool member will // not happen. // When: assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), pool_id, Some((Perbill::from_percent(100), 2)), )); // Given: deposit_rewards(200); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Then: assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(100), 2)) },] ); }) } #[test] fn commission_accumulates_on_multiple_rewards() { ExtBuilder::default().build_and_execute(|| { let pool_id = 1; // Given: // Set initial commission of pool 1 to 10%. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), pool_id, Some((Perbill::from_percent(10), 2)), )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(10), 2)) }, ] ); // When: // The pool earns 100 points deposit_rewards(100); // Change commission to 20% assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), pool_id, Some((Perbill::from_percent(20), 2)), )); assert_eq!( pool_events_since_last_call(), vec![Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(20), 2)) },] ); // The pool earns 100 points deposit_rewards(100); // Then: // Claim payout: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Claim commission: assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); // Then: assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 90 + 80 }, Event::PoolCommissionClaimed { pool_id: 1, commission: 30 } ] ); }) } #[test] fn last_recorded_total_payouts_needs_commission() { ExtBuilder::default().build_and_execute(|| { let pool_id = 1; // Given: // Set initial commission of pool 1 to 10%. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), pool_id, Some((Perbill::from_percent(10), 2)), )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(10), 2)) }, ] ); // When: // The pool earns 100 points deposit_rewards(100); // Claim payout: assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(10))); // Claim commission: assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); // Then: assert_eq!( RewardPools::::get(1).unwrap().last_recorded_total_payouts, 90 + 10 ); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id: 1, payout: 90 }, Event::PoolCommissionClaimed { pool_id: 1, commission: 10 } ] ); }) } #[test] fn do_reward_payout_with_100_percent_commission() { ExtBuilder::default().build_and_execute(|| { // turn off GlobalMaxCommission for this test. GlobalMaxCommission::::set(None); let (mut member, bonded_pool, mut reward_pool) = Pools::get_member_with_pools(&10).unwrap(); // top up commission payee account to existential deposit let _ = Currency::set_balance(&2, 5); // Set a commission pool 1 to 100%, with a payee set to `2` assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), bonded_pool.id, Some((Perbill::from_percent(100), 2)), )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(100), 2)) } ] ); // The pool earns 10 points deposit_rewards(10); // execute the payout assert_ok!(Pools::do_reward_payout( &10, &mut member, &mut BondedPool::::get(1).unwrap(), &mut reward_pool )); }) } #[test] fn global_max_caps_max_commission_payout() { ExtBuilder::default().build_and_execute(|| { // Note: GlobalMaxCommission is set at 90%. let (mut member, bonded_pool, mut reward_pool) = Pools::get_member_with_pools(&10).unwrap(); // top up the commission payee account to existential deposit let _ = Currency::set_balance(&2, 5); // Set a commission pool 1 to 100% fails. assert_noop!( Pools::set_commission( RuntimeOrigin::signed(900), bonded_pool.id, Some((Perbill::from_percent(100), 2)), ), Error::::CommissionExceedsGlobalMaximum ); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, ] ); // Set pool commission to 90% and then set global max commission to 80%. assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), bonded_pool.id, Some((Perbill::from_percent(90), 2)), )); GlobalMaxCommission::::set(Some(Perbill::from_percent(80))); // The pool earns 10 points deposit_rewards(10); // execute the payout assert_ok!(Pools::do_reward_payout( &10, &mut member, &mut BondedPool::::get(1).unwrap(), &mut reward_pool )); // Confirm the commission was only 8 points out of 10 points, and the payout was 2 out // of 10 points, reflecting the 80% global max commission. assert_eq!( pool_events_since_last_call(), vec![ Event::PoolCommissionUpdated { pool_id: 1, current: Some((Perbill::from_percent(90), 2)) }, Event::PaidOut { member: 10, pool_id: 1, payout: 2 }, ] ); }) } #[test] fn claim_commission_works() { ExtBuilder::default().build_and_execute(|| { /// Deposit rewards into the pool and claim payout. This will set up pending commission /// to be tested in various scenarios. fn deposit_rewards_and_claim_payout(caller: AccountId, points: u128) { deposit_rewards(points); assert_ok!(Pools::claim_payout(RuntimeOrigin::signed(caller))); } let pool_id = 1; let _ = Currency::set_balance(&900, 5); assert_ok!(Pools::set_commission( RuntimeOrigin::signed(900), pool_id, Some((Perbill::from_percent(50), 900)) )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id }, Event::Bonded { member: 10, pool_id, bonded: 10, joined: true }, Event::PoolCommissionUpdated { pool_id, current: Some((Perbill::from_percent(50), 900)) }, ] ); // Given: deposit_rewards_and_claim_payout(10, 100); assert_eq!(RewardPool::::current_balance(pool_id), 50); // Pool does not exist assert_noop!( Pools::claim_commission(RuntimeOrigin::signed(900), 9999,), Error::::PoolNotFound ); // Does not have permission. assert_noop!( Pools::claim_commission(RuntimeOrigin::signed(10), pool_id,), Error::::DoesNotHavePermission ); // When: assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); // Then: assert_eq!(RewardPool::::current_balance(pool_id), 0); // No more pending commission. assert_noop!( Pools::claim_commission(RuntimeOrigin::signed(900), pool_id,), Error::::NoPendingCommission ); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id, payout: 50 }, Event::PoolCommissionClaimed { pool_id: 1, commission: 50 } ] ); // The pool commission's claim_permission field is updated to `Permissionless` by the // root member, which means anyone can now claim commission for the pool. // Given: // Some random non-pool member to claim commission. let non_pool_member = 1001; let _ = Currency::set_balance(&non_pool_member, 5); // Set up pending commission. deposit_rewards_and_claim_payout(10, 100); assert_ok!(Pools::set_commission_claim_permission( RuntimeOrigin::signed(900), pool_id, Some(CommissionClaimPermission::Permissionless) )); // When: assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(non_pool_member), pool_id)); // Then: assert_eq!(RewardPool::::current_balance(pool_id), 0); assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id, payout: 50 }, Event::PoolCommissionClaimPermissionUpdated { pool_id: 1, permission: Some(CommissionClaimPermission::Permissionless) }, Event::PoolCommissionClaimed { pool_id: 1, commission: 50 }, ] ); // The pool commission's claim_permission is updated to an adhoc account by the root // member, which means now only that account (in addition to the root role) can claim // commission for the pool. // Given: // The account designated to claim commission. let designated_commission_claimer = 2001; let _ = Currency::set_balance(&designated_commission_claimer, 5); // Set up pending commission. deposit_rewards_and_claim_payout(10, 100); assert_ok!(Pools::set_commission_claim_permission( RuntimeOrigin::signed(900), pool_id, Some(CommissionClaimPermission::Account(designated_commission_claimer)) )); // When: // Previous claimer can no longer claim commission. assert_noop!( Pools::claim_commission(RuntimeOrigin::signed(1001), pool_id,), Error::::DoesNotHavePermission ); // Designated claimer can claim commission. assert_ok!(Pools::claim_commission( RuntimeOrigin::signed(designated_commission_claimer), pool_id )); // Then: assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id, payout: 50 }, Event::PoolCommissionClaimPermissionUpdated { pool_id: 1, permission: Some(CommissionClaimPermission::Account(2001)) }, Event::PoolCommissionClaimed { pool_id: 1, commission: 50 }, ] ); // Even with an Account claim permission set, the `root` role of the pool can still // claim commission. // Given: deposit_rewards_and_claim_payout(10, 100); // When: assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); // Then: assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id, payout: 50 }, Event::PoolCommissionClaimed { pool_id: 1, commission: 50 }, ] ); // The root role updates commission's claim_permission back to `None`, which results in // only the root member being able to claim commission for the pool. // Given: deposit_rewards_and_claim_payout(10, 100); // When: assert_ok!(Pools::set_commission_claim_permission( RuntimeOrigin::signed(900), pool_id, None )); // Previous claimer can no longer claim commission. assert_noop!( Pools::claim_commission( RuntimeOrigin::signed(designated_commission_claimer), pool_id, ), Error::::DoesNotHavePermission ); // Root can claim commission. assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id)); // Then: assert_eq!( pool_events_since_last_call(), vec![ Event::PaidOut { member: 10, pool_id, payout: 50 }, Event::PoolCommissionClaimPermissionUpdated { pool_id: 1, permission: None }, Event::PoolCommissionClaimed { pool_id: 1, commission: 50 }, ] ); }) } #[test] fn set_commission_claim_permission_handles_errors() { ExtBuilder::default().build_and_execute(|| { let pool_id = 1; let _ = Currency::set_balance(&900, 5); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id }, Event::Bonded { member: 10, pool_id, bonded: 10, joined: true }, ] ); // Cannot operate on a non-existing pool. assert_noop!( Pools::set_commission_claim_permission( RuntimeOrigin::signed(10), 90, Some(CommissionClaimPermission::Permissionless) ), Error::::PoolNotFound ); // Only the root role can change the commission claim permission. assert_noop!( Pools::set_commission_claim_permission( RuntimeOrigin::signed(10), pool_id, Some(CommissionClaimPermission::Permissionless) ), Error::::DoesNotHavePermission ); }) } } mod slash { use super::*; #[test] fn slash_no_subpool_is_tracked() { let bonded = |points, member_counter| BondedPool:: { id: 1, inner: BondedPoolInner { commission: Commission::default(), member_counter, points, roles: DEFAULT_ROLES, state: PoolState::Open, }, }; ExtBuilder::default().with_check(0).build_and_execute(|| { // Given Currency::set_balance(&11, ExistentialDeposit::get() + 2); assert!(!PoolMembers::::contains_key(11)); assert_eq!(TotalValueLocked::::get(), 10); // When assert_ok!(Pools::join(RuntimeOrigin::signed(11), 2, 1)); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, Event::Bonded { member: 11, pool_id: 1, bonded: 2, joined: true }, ] ); assert_eq!(TotalValueLocked::::get(), 12); assert_eq!( PoolMembers::::get(11).unwrap(), PoolMember:: { pool_id: 1, points: 2, ..Default::default() } ); assert_eq!(BondedPool::::get(1).unwrap(), bonded(12, 2)); // Given // The bonded balance is slashed in half StakingMock::slash_by(1, 6); // And Currency::set_balance(&12, ExistentialDeposit::get() + 12); assert!(!PoolMembers::::contains_key(12)); // When assert_ok!(Pools::join(RuntimeOrigin::signed(12), 12, 1)); // Then assert_eq!( pool_events_since_last_call(), vec![ Event::PoolSlashed { pool_id: 1, balance: 6 }, Event::Bonded { member: 12, pool_id: 1, bonded: 12, joined: true } ] ); assert_eq!(TotalValueLocked::::get(), 18); assert_eq!( PoolMembers::::get(12).unwrap(), PoolMember:: { pool_id: 1, points: 24, ..Default::default() } ); assert_eq!(BondedPool::::get(1).unwrap(), bonded(12 + 24, 3)); }); } } mod chill { use super::*; #[test] fn chill_works() { ExtBuilder::default().build_and_execute(|| { // only nominator or root can chill assert_noop!( Pools::chill(RuntimeOrigin::signed(10), 1), Error::::NotNominator ); // root can chill and re-nominate assert_ok!(Pools::chill(RuntimeOrigin::signed(900), 1)); assert_ok!(Pools::nominate(RuntimeOrigin::signed(900), 1, vec![31])); // nominator can chill and re-nominate assert_ok!(Pools::chill(RuntimeOrigin::signed(901), 1)); assert_ok!(Pools::nominate(RuntimeOrigin::signed(901), 1, vec![31])); // if `depositor` stake is less than the `MinimumNominatorBond`, then this call // becomes permissionless; StakingMinBond::set(20); // any account can chill assert_ok!(Pools::chill(RuntimeOrigin::signed(10), 1)); }) } }