Files
pezkuwi-subxt/substrate/frame/nomination-pools/src/tests.rs
T
Ross Bulat b772cb576d Pools: Make PermissionlessWithdraw the default claim permission (#3438)
Related Issue https://github.com/paritytech/polkadot-sdk/issues/3398

This PR makes permissionless withdrawing the default option, giving any
network participant access to claim pool rewards on member's behalf. Of
course, members can still opt out of this by setting a `Permissioned`
claim permission.

Permissionless claiming has been a part of the nomination pool pallet
for around 9 months now, with very limited uptake (~4% of total pool
members). 1.6% of pool members are using `PermissionlessAll`, strongly
suggesting it is not wanted - it is too ambiguous and doesn't provide
guidance to claimers.

Stakers expect rewards to be claimed on their behalf by default - I have
expanded upon this in detail within the [accompanying issue's
discussion](https://github.com/paritytech/polkadot-sdk/issues/3398).
Other protocols have this behaviour, whereby staking rewards are
received without the staker having to take any action. From this
perspective, permissionless claiming is not intuitive for pool members.
As evidence of this, over 150,000 DOT is currently unclaimed on
Polkadot, and is growing at a non-linear rate.
2024-04-01 09:35:36 +00:00

7372 lines
220 KiB
Rust

// 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::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::<EraIndex, UnbondPool<T>, TotalUnbondingPools<T>>::try_from(not_bounded).unwrap()
}};
}
macro_rules! member_unbonding_eras {
($( $any:tt )*) => {{
let x: BoundedBTreeMap<EraIndex, Balance, MaxUnbonding> = bounded_btree_map!($( $any )*);
x
}};
}
pub const DEFAULT_ROLES: PoolRoles<AccountId> =
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::<Runtime>::count(), 1);
assert_eq!(RewardPools::<Runtime>::count(), 1);
assert_eq!(SubPoolsStorage::<Runtime>::count(), 0);
assert_eq!(PoolMembers::<Runtime>::count(), 1);
assert_eq!(StakingMock::bonding_duration(), 3);
assert!(Metadata::<T>::contains_key(1));
// initial member.
assert_eq!(TotalValueLocked::<T>::get(), 10);
let last_pool = LastPoolId::<Runtime>::get();
assert_eq!(
BondedPool::<Runtime>::get(last_pool).unwrap(),
BondedPool::<Runtime> {
id: last_pool,
inner: BondedPoolInner {
commission: Commission::default(),
member_counter: 1,
points: 10,
roles: DEFAULT_ROLES,
state: PoolState::Open,
},
}
);
assert_eq!(
RewardPools::<Runtime>::get(last_pool).unwrap(),
RewardPool::<Runtime> {
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::<Runtime>::get(10).unwrap(),
PoolMember::<Runtime> { 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::<Runtime> {
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::<Runtime> {
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::<Runtime>::get(1).is_some());
assert_eq!(Pallet::<Runtime>::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::<Runtime>::api_points_to_balance(1, 10), 5);
// if pool does not exist, points to balance ratio is 0.
assert_eq!(BondedPool::<Runtime>::get(2), None);
assert_eq!(Pallet::<Runtime>::api_points_to_balance(2, 10), 0);
})
}
#[test]
fn api_balance_to_points_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(Pallet::<Runtime>::api_balance_to_points(1, 0), 0);
assert_eq!(Pallet::<Runtime>::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::<Runtime>::api_balance_to_points(1, 10), 20);
// if pool does not exist, balance to points ratio is 0.
assert_eq!(BondedPool::<Runtime>::get(2), None);
assert_eq!(Pallet::<Runtime>::api_points_to_balance(2, 10), 0);
})
}
#[test]
fn ok_to_join_with_works() {
ExtBuilder::default().build_and_execute(|| {
let pool = BondedPool::<Runtime> {
id: 123,
inner: BondedPoolInner {
commission: Commission::default(),
member_counter: 1,
points: 100,
roles: DEFAULT_ROLES,
state: PoolState::Open,
},
};
let max_points_to_balance: u128 =
<<Runtime as Config>::MaxPointsToBalance as Get<u8>>::get().into();
// Simulate a 100% slashed pool
StakingMock::set_bonded_balance(pool.bonded_account(), 0);
assert_noop!(pool.ok_to_join(), Error::<Runtime>::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::<Runtime>::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::<Runtime> {
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::<Runtime>::get(1).unwrap();
let reward_pool = RewardPools::<Runtime>::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::<Runtime>::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::<Runtime>::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::<T>::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::<T>::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::<Runtime> { 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::<Runtime> { points: 100, balance: 50 };
assert_eq!(unbond_pool.balance_to_point(10), 20);
// 1 points : 2 balance ratio
let unbond_pool = UnbondPool::<Runtime> { points: 50, balance: 100 };
assert_eq!(unbond_pool.balance_to_point(10), 5);
// 100 points : 0 balance ratio
let unbond_pool = UnbondPool::<Runtime> { points: 100, balance: 0 };
assert_eq!(unbond_pool.balance_to_point(10), 100 * 10);
// 0 points : 100 balance
let unbond_pool = UnbondPool::<Runtime> { points: 0, balance: 100 };
assert_eq!(unbond_pool.balance_to_point(10), 10);
// 10 points : 3 balance ratio
let unbond_pool = UnbondPool::<Runtime> { points: 100, balance: 30 };
assert_eq!(unbond_pool.balance_to_point(10), 33);
// 2 points : 3 balance ratio
let unbond_pool = UnbondPool::<Runtime> { points: 200, balance: 300 };
assert_eq!(unbond_pool.balance_to_point(10), 6);
// 4 points : 9 balance ratio
let unbond_pool = UnbondPool::<Runtime> { 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::<Runtime> { 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::<Runtime> { points: 100, balance: 50 };
assert_eq!(unbond_pool.point_to_balance(10), 5);
// 2 balance : 1 points ratio
let unbond_pool = UnbondPool::<Runtime> { points: 50, balance: 100 };
assert_eq!(unbond_pool.point_to_balance(10), 20);
// 100 balance : 0 points ratio
let unbond_pool = UnbondPool::<Runtime> { points: 0, balance: 100 };
assert_eq!(unbond_pool.point_to_balance(10), 0);
// 0 balance : 100 points ratio
let unbond_pool = UnbondPool::<Runtime> { points: 100, balance: 0 };
assert_eq!(unbond_pool.point_to_balance(10), 0);
// 10 balance : 3 points ratio
let unbond_pool = UnbondPool::<Runtime> { points: 30, balance: 100 };
assert_eq!(unbond_pool.point_to_balance(10), 33);
// 2 balance : 3 points ratio
let unbond_pool = UnbondPool::<Runtime> { points: 300, balance: 200 };
assert_eq!(unbond_pool.point_to_balance(10), 6);
// 4 balance : 9 points ratio
let unbond_pool = UnbondPool::<Runtime> { 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::<Runtime>::get(), 5);
assert_eq!(BondingDuration::get(), 3);
assert_eq!(PostUnbondingPoolsWindow::get(), 2);
// Given
let mut sub_pool_0 = SubPools::<Runtime> {
no_era: UnbondPool::<Runtime>::default(),
with_era: unbonding_pools_with_era! {
0 => UnbondPool::<Runtime> { points: 10, balance: 10 },
1 => UnbondPool::<Runtime> { points: 10, balance: 10 },
2 => UnbondPool::<Runtime> { points: 20, balance: 20 },
3 => UnbondPool::<Runtime> { points: 30, balance: 30 },
4 => UnbondPool::<Runtime> { 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::<Runtime> { points: 50, balance: 50 })
.unwrap();
sub_pool_0
.with_era
.try_insert(5, UnbondPool::<Runtime> { 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::<Runtime> {
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::<Runtime>::contains_key(11));
assert_eq!(TotalValueLocked::<T>::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::<T>::get(), 12);
assert_eq!(
PoolMembers::<Runtime>::get(11).unwrap(),
PoolMember::<Runtime> { pool_id: 1, points: 2, ..Default::default() }
);
assert_eq!(BondedPool::<Runtime>::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::<Runtime>::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::<T>::get(), 18);
assert_eq!(
PoolMembers::<Runtime>::get(12).unwrap(),
PoolMember::<Runtime> { pool_id: 1, points: 24, ..Default::default() }
);
assert_eq!(BondedPool::<Runtime>::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::<Runtime>::get(10).unwrap().pool_id, 1);
assert_noop!(
Pools::join(RuntimeOrigin::signed(10), 420, 123),
Error::<Runtime>::AccountBelongsToOtherPool
);
assert_noop!(
Pools::join(RuntimeOrigin::signed(11), 420, 123),
Error::<Runtime>::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::<Runtime>::OverflowRisk
);
// Given a mocked bonded pool
BondedPool::<Runtime> {
id: 123,
inner: BondedPoolInner {
commission: Commission::default(),
member_counter: 1,
points: 100,
roles: DEFAULT_ROLES,
state: PoolState::Open,
},
}
.put();
// and reward pool
RewardPools::<Runtime>::insert(123, RewardPool::<Runtime> { ..Default::default() });
// Force the points:balance ratio to `MaxPointsToBalance` (100/10)
let max_points_to_balance: u128 =
<<Runtime as Config>::MaxPointsToBalance as Get<u8>>::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::<Runtime>::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::<Runtime>::NotOpen
);
unsafe_set_state(123, PoolState::Destroying);
assert_noop!(
Pools::join(RuntimeOrigin::signed(11), max_points_to_balance, 123),
Error::<Runtime>::NotOpen
);
// Given
MinJoinBond::<Runtime>::put(100);
// Then
assert_noop!(
Pools::join(RuntimeOrigin::signed(11), 99, 123),
Error::<Runtime>::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::<Runtime> {
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::<Runtime>::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::<Runtime>::MaxPoolMembers
);
// Given
assert_eq!(PoolMembers::<Runtime>::count(), 3);
assert_eq!(MaxPoolMembers::<Runtime>::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::<Runtime>::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::<Runtime>::MaxPoolMembers
);
});
}
}
mod claim_payout {
use super::*;
fn del(points: Balance, last_recorded_reward_counter: u128) -> PoolMember<Runtime> {
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<Runtime> {
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<Runtime> {
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::<Runtime>::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::<Runtime>::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::<Runtime>::get(40).unwrap(), del(40, 1));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(50).unwrap(), del(50, 1));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(10).unwrap(), del_float(10, 1.5));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(40).unwrap(), del_float(40, 1.5));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(50).unwrap(), del_float(50, 2.0));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(10).unwrap(), del(10, 2));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(10).unwrap(), del(10, 6));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(10).unwrap(), del_float(10, 6.2));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(40).unwrap(), del_float(40, 6.2));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::get(50).unwrap(), del_float(50, 6.2));
assert_eq!(RewardPools::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::get(1).unwrap();
let mut reward_pool = RewardPools::<Runtime>::get(1).unwrap();
let mut del_10 = PoolMembers::<Runtime>::get(10).unwrap();
let mut del_40 = PoolMembers::<Runtime>::get(40).unwrap();
let mut del_50 = PoolMembers::<Runtime>::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::<Runtime>::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::<Runtime>::set(None);
MaxPoolMembersPerPool::<Runtime>::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::<Runtime>::set(None);
MaxPoolMembersPerPool::<Runtime>::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::<Runtime>::set(None);
MaxPoolMembersPerPool::<Runtime>::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::<Runtime>::set(None);
MaxPoolMembersPerPool::<Runtime>::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::<T>::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::<T>::get(), 30);
// can unbond to above limit
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 5));
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().active_points(), 15);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_points(), 5);
// tvl remains unchanged.
assert_eq!(TotalValueLocked::<T>::get(), 30);
// cannot go to below 10:
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(20), 20, 10),
Error::<T>::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::<Runtime>::get(20).unwrap().active_points(), 0);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_points(), 20);
assert_eq!(
ClaimPermissions::<Runtime>::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::<T>::PartialUnbondNotAllowedPermissionlessly
);
// cannot go to below 10:
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(kicker), 20, 15),
Error::<T>::PartialUnbondNotAllowedPermissionlessly
);
// but they themselves can do an unbond
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2));
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().active_points(), 18);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_points(), 2);
// can be kicked to 0.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(kicker), 20, 18));
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().active_points(), 0);
assert_eq!(PoolMembers::<Runtime>::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::<T>::PartialUnbondNotAllowedPermissionlessly
);
// cannot go to below 10:
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(random), 20, 15),
Error::<T>::PartialUnbondNotAllowedPermissionlessly
);
// but they themselves can do an unbond
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 2));
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().active_points(), 18);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_points(), 2);
// but can go to 0
assert_ok!(Pools::unbond(RuntimeOrigin::signed(random), 20, 18));
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().active_points(), 0);
assert_eq!(PoolMembers::<Runtime>::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::<Runtime>::get(20).unwrap().active_points(), 0);
assert_eq!(PoolMembers::<Runtime>::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::<T>::get(10).unwrap().points, 20);
// can unbond to above the limit.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5));
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().active_points(), 15);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 5);
// cannot go to below 10:
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 10),
Error::<T>::MinimumBondNotMet
);
// cannot go to 0 either.
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 15),
Error::<T>::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::<T>::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::<T>::PartialUnbondNotAllowedPermissionlessly
);
// or below the limit
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(kicker), 10, 15),
Error::<T>::PartialUnbondNotAllowedPermissionlessly
);
// or 0.
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(kicker), 10, 20),
Error::<T>::DoesNotHavePermission
);
// they themselves cannot do it either
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 20),
Error::<T>::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::<T>::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::<T>::PartialUnbondNotAllowedPermissionlessly
);
// or below the limit
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(random), 10, 15),
Error::<T>::PartialUnbondNotAllowedPermissionlessly
);
// or 0.
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(random), 10, 20),
Error::<T>::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::<T>::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::<Runtime>::get(10).unwrap().active_points(), 15);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 5);
// but not below the limit
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 10),
Error::<T>::MinimumBondNotMet
);
// and certainly not zero
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 15),
Error::<T>::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::<T>::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::<Runtime>::get(10).unwrap().active_points(), 15);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 5);
// still cannot go to below limit
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 10),
Error::<T>::MinimumBondNotMet
);
// can go to 0 too.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 15));
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().active_points(), 0);
assert_eq!(PoolMembers::<Runtime>::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::<Runtime>::get(1).unwrap().with_era,
unbonding_pools_with_era! { 3 => UnbondPool::<Runtime> { points: 10, balance: 10 }}
);
assert_eq!(
BondedPool::<Runtime>::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::<Runtime>::get(1).unwrap().with_era,
unbonding_pools_with_era! { 3 => UnbondPool { points: 6, balance: 6 }}
);
assert_eq!(
BondedPool::<Runtime>::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::<Runtime>::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::<Runtime>::get(1).unwrap().with_era,
unbonding_pools_with_era! { 3 => UnbondPool { points: 98, balance: 98 }}
);
assert_eq!(
BondedPool::<Runtime>::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::<Runtime>::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::<Runtime>::get(1).unwrap().with_era,
unbonding_pools_with_era! { 6 => UnbondPool { points: 2, balance: 2 }}
);
assert_eq!(
BondedPool::<Runtime>::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::<Runtime>::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::<Runtime>::get();
CurrentEra::set(current_era);
assert_ok!(fully_unbond_permissioned(10));
// Then
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::NotKickerOrDestroying
);
// permissionless unbond must be full
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(420), 100, 80),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::PartialUnbondNotAllowedPermissionlessly,
);
// depositor can never be unbonded permissionlessly .
assert_noop!(
Pools::fully_unbond(RuntimeOrigin::signed(420), 10),
Error::<T>::DoesNotHavePermission
);
// but depositor itself can do it.
assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(10), 10));
assert_eq!(BondedPools::<Runtime>::get(1).unwrap().points, 0);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::PoolMemberNotFound
);
// Add the member
let member = PoolMember { pool_id: 2, points: 10, ..Default::default() };
PoolMembers::<Runtime>::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::<Runtime>::insert(11, member);
BondedPool::<Runtime> {
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::<T>::set(1);
MinJoinBond::<T>::set(1);
assert_eq!(Pools::depositor_min_bond(), 1);
// given
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().active_points(), 10);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 0);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().pool_id, 1);
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!()
);
assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 10);
assert!(SubPoolsStorage::<Runtime>::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::<Runtime>::get(10).unwrap().active_points(), 9);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 1);
assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 9);
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 1)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(10).unwrap().active_points(), 4);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 6);
assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 4);
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 6)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(10).unwrap().active_points(), 3);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 7);
assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 3);
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 6, 4 => 1)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::MinimumBondNotMet
);
// instead:
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3));
// then
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().active_points(), 0);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 10);
assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 0);
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 6, 4 => 4)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::MaxUnbondingLimit
);
// when
MaxUnbonding::set(3);
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 1));
assert_eq!(
PoolMembers::<Runtime>::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::<Runtime>::get(10).unwrap().active_points(), 10);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 0);
// can unbond a bit..
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 3));
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().active_points(), 7);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().unbonding_points(), 3);
// but not less than 2
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 6),
Error::<Runtime>::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::<Runtime>::get(), 2);
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().active_points(), 10);
assert_eq!(PoolMembers::<Runtime>::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::<Runtime>::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::<Runtime>::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::<T>::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::<T>::get(), 15);
let member_balance = PoolMembers::<T>::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::<T>::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::<Runtime>::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::<Runtime>::get(), 600);
// Simulate a slash to the pool with_era(current_era), decreasing the balance by
// half
{
unbond_pool.balance /= 2; // 295
SubPoolsStorage::<Runtime>::insert(1, sub_pools);
// Adjust the TVL for this non-api usage (direct sub-pool modification)
TotalValueLocked::<Runtime>::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::<Runtime>::get();
CurrentEra::set(current_era);
// Simulate some other call to unbond that would merge `with_era` pools into
// `no_era`
let sub_pools =
SubPoolsStorage::<Runtime>::get(1).unwrap().maybe_merge_pools(current_era);
SubPoolsStorage::<Runtime>::insert(1, sub_pools);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::get(1).unwrap().no_era,
UnbondPool { points: 0, balance: 0 }
);
assert!(!PoolMembers::<Runtime>::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::<T>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<T>::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::<T>::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::<Runtime>::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::<Runtime>::insert(1, sub_pools.clone());
assert_noop!(
Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0),
Error::<Runtime>::PoolMemberNotFound
);
let mut member = PoolMember { pool_id: 1, points: 10, ..Default::default() };
PoolMembers::<Runtime>::insert(11, member.clone());
// Simulate calling `unbond`
member.unbonding_eras = member_unbonding_eras!(3 => 10);
PoolMembers::<Runtime>::insert(11, member.clone());
// We are still in the bonding duration
assert_noop!(
Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0),
Error::<Runtime>::CannotWithdrawAny
);
// If we error the member does not get removed
assert_eq!(PoolMembers::<Runtime>::get(11), Some(member));
// and the sub pools do not get updated.
assert_eq!(SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::contains_key(100));
assert!(!PoolMembers::<Runtime>::contains_key(200));
assert_eq!(SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::get(1).unwrap(), Default::default(),);
assert_eq!(Currency::free_balance(&100), 100 + 100);
assert!(!PoolMembers::<Runtime>::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::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 6, 4 => 1)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(10).unwrap().active_points(), 13);
assert_eq!(PoolMembers::<Runtime>::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::<Runtime>::CannotWithdrawAny
);
// when
CurrentEra::set(3);
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0));
// then
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!(4 => 1)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(10).unwrap().unbonding_eras,
member_unbonding_eras!()
);
assert_eq!(SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::get(11).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 6, 4 => 1)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(11).unwrap().active_points(), 3);
assert_eq!(PoolMembers::<Runtime>::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::<Runtime>::CannotWithdrawAny
);
// when
CurrentEra::set(3);
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(11), 11, 0));
// then
assert_eq!(
PoolMembers::<Runtime>::get(11).unwrap().unbonding_eras,
member_unbonding_eras!(4 => 1)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(11).unwrap().unbonding_eras,
member_unbonding_eras!()
);
assert_eq!(SubPoolsStorage::<Runtime>::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::<Runtime>::CannotWithdrawAny
);
});
}
#[test]
fn full_multi_step_withdrawing_non_depositor() {
ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| {
assert_eq!(TotalValueLocked::<T>::get(), 110);
// given
assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 75));
assert_eq!(
PoolMembers::<Runtime>::get(100).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 75)
);
// tvl unchanged.
assert_eq!(TotalValueLocked::<T>::get(), 110);
// progress one era and unbond the leftover.
CurrentEra::set(1);
assert_ok!(Pools::unbond(RuntimeOrigin::signed(100), 100, 25));
assert_eq!(
PoolMembers::<Runtime>::get(100).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 75, 4 => 25)
);
assert_noop!(
Pools::withdraw_unbonded(RuntimeOrigin::signed(100), 100, 0),
Error::<Runtime>::CannotWithdrawAny
);
// tvl unchanged.
assert_eq!(TotalValueLocked::<T>::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::<Runtime>::get(100).unwrap().unbonding_eras,
member_unbonding_eras!(4 => 25)
);
// tvl updated
assert_eq!(TotalValueLocked::<T>::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::<Runtime>::get(20).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 5)
);
assert_eq!(
PoolMembers::<Runtime>::get(30).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 5)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(20).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 5, 4 => 5)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(20).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(20).unwrap().unbonding_eras,
member_unbonding_eras!(3 => 5, 4 => 5, 5 => 5, 8 => 5)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(20).unwrap().unbonding_eras,
member_unbonding_eras!(8 => 5)
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::get(30).unwrap().unbonding_eras,
member_unbonding_eras!()
);
assert_eq!(
SubPoolsStorage::<Runtime>::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::<Runtime>::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::<Runtime>::MinimumBondNotMet
);
// but not even full, because they pool is not yet destroying.
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 10),
Error::<Runtime>::MinimumBondNotMet
);
// but now they can.
unsafe_set_state(1, PoolState::Destroying);
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 5),
Error::<Runtime>::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::<Runtime>::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::<T>::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::<Runtime>::get(20).unwrap().points, 20);
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 20));
assert_eq!(
ClaimPermissions::<Runtime>::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::<Runtime>::get(20), None);
assert_eq!(ClaimPermissions::<Runtime>::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::<T>::get(), 10);
assert!(!BondedPools::<Runtime>::contains_key(2));
assert!(!RewardPools::<Runtime>::contains_key(2));
assert!(!PoolMembers::<Runtime>::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::<T>::get(), 10 + StakingMock::minimum_nominator_bond());
assert_eq!(Currency::free_balance(&11), 0);
assert_eq!(
PoolMembers::<Runtime>::get(11).unwrap(),
PoolMember {
pool_id: 2,
points: StakingMock::minimum_nominator_bond(),
..Default::default()
}
);
assert_eq!(
BondedPool::<Runtime>::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::<Runtime>::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::<Runtime>::AccountBelongsToOtherPool
);
// Given
assert_eq!(MinCreateBond::<Runtime>::get(), 2);
assert_eq!(StakingMock::minimum_nominator_bond(), 10);
// Then
assert_noop!(
Pools::create(RuntimeOrigin::signed(11), 9, 123, 456, 789),
Error::<Runtime>::MinimumBondNotMet
);
// Given
MinCreateBond::<Runtime>::put(20);
// Then
assert_noop!(
Pools::create(RuntimeOrigin::signed(11), 19, 123, 456, 789),
Error::<Runtime>::MinimumBondNotMet
);
// Given
BondedPool::<Runtime> {
id: 2,
inner: BondedPoolInner {
commission: Commission::default(),
member_counter: 1,
points: 10,
roles: DEFAULT_ROLES,
state: PoolState::Open,
},
}
.put();
assert_eq!(MaxPools::<Runtime>::get(), Some(2));
assert_eq!(BondedPools::<Runtime>::count(), 2);
// Then
assert_noop!(
Pools::create(RuntimeOrigin::signed(11), 20, 123, 456, 789),
Error::<Runtime>::MaxPools
);
// Given
assert_eq!(PoolMembers::<Runtime>::count(), 1);
MaxPools::<Runtime>::put(3);
MaxPoolMembers::<Runtime>::put(1);
Currency::set_balance(&11, 5 + 20);
// Then
let create = RuntimeCall::Pools(Call::<Runtime>::create {
amount: 20,
root: 11,
nominator: 11,
bouncer: 11,
});
assert_noop!(
create.dispatch(RuntimeOrigin::signed(11)),
Error::<Runtime>::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::<Runtime>::PoolIdInUse
);
assert_noop!(
Pools::create_with_pool_id(RuntimeOrigin::signed(12), 20, 234, 654, 783, 3),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::get(11), ClaimPermission::PermissionlessWithdraw);
assert_noop!(
Pools::set_claim_permission(RuntimeOrigin::signed(12), ClaimPermission::Permissioned),
Error::<T>::PoolMemberNotFound
);
assert_ok!(Pools::set_claim_permission(
RuntimeOrigin::signed(11),
ClaimPermission::Permissioned
));
// then
assert_eq!(ClaimPermissions::<Runtime>::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::<Runtime>::NotNominator
);
// bouncer can't nominate
assert_noop!(
Pools::nominate(RuntimeOrigin::signed(902), 1, vec![21]),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::PoolNotFound
);
});
}
}
mod set_state {
use super::*;
#[test]
fn set_state_works() {
ExtBuilder::default().build_and_execute(|| {
// Given
assert_ok!(BondedPool::<Runtime>::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::<Runtime>::CanNotChangeState
);
assert_noop!(
Pools::set_state(RuntimeOrigin::signed(901), 1, PoolState::Blocked),
Error::<Runtime>::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::<Runtime>::get(1).unwrap().state, PoolState::Blocked);
// bouncer can change state
assert_ok!(Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Destroying));
assert_eq!(BondedPool::<Runtime>::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::<Runtime>::CanNotChangeState
);
assert_noop!(
Pools::set_state(RuntimeOrigin::signed(902), 1, PoolState::Blocked),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::get(1), vec![1, 1]);
// bouncer can set metadata
assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(902), 1, vec![2, 2]));
assert_eq!(Metadata::<Runtime>::get(1), vec![2, 2]);
// Depositor can't set metadata
assert_noop!(
Pools::set_metadata(RuntimeOrigin::signed(10), 1, vec![3, 3]),
Error::<Runtime>::DoesNotHavePermission
);
// Nominator can't set metadata
assert_noop!(
Pools::set_metadata(RuntimeOrigin::signed(901), 1, vec![3, 3]),
Error::<Runtime>::DoesNotHavePermission
);
// Metadata cannot be longer than `MaxMetadataLen`
assert_noop!(
Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1, 1]),
Error::<Runtime>::MetadataExceedsMaxLen
);
});
}
}
mod set_configs {
use super::*;
#[test]
fn set_configs_works() {
ExtBuilder::default().build_and_execute(|| {
// Setting works
assert_ok!(Pools::set_configs(
RuntimeOrigin::root(),
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::<Runtime>::get(), 1);
assert_eq!(MinCreateBond::<Runtime>::get(), 2);
assert_eq!(MaxPools::<Runtime>::get(), Some(3));
assert_eq!(MaxPoolMembers::<Runtime>::get(), Some(4));
assert_eq!(MaxPoolMembersPerPool::<Runtime>::get(), Some(5));
assert_eq!(GlobalMaxCommission::<Runtime>::get(), Some(Perbill::from_percent(6)));
// Noop does nothing
assert_storage_noop!(assert_ok!(Pools::set_configs(
RuntimeOrigin::root(),
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
)));
// Removing works
assert_ok!(Pools::set_configs(
RuntimeOrigin::root(),
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
));
assert_eq!(MinJoinBond::<Runtime>::get(), 0);
assert_eq!(MinCreateBond::<Runtime>::get(), 0);
assert_eq!(MaxPools::<Runtime>::get(), None);
assert_eq!(MaxPoolMembers::<Runtime>::get(), None);
assert_eq!(MaxPoolMembersPerPool::<Runtime>::get(), None);
assert_eq!(GlobalMaxCommission::<Runtime>::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::<Runtime>::get(10).unwrap().points, 10);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::get(10).unwrap().points, 20);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::get(10).unwrap().points, 40);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::get(10).unwrap().points, 10);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::get(10).unwrap().points, 10 + claimable_reward);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::get(10).unwrap().points, 10);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().points, 20);
assert_eq!(BondedPools::<Runtime>::get(1).unwrap().points, 30);
assert_eq!(Currency::free_balance(&10), 35);
assert_eq!(Currency::free_balance(&20), 20);
assert_eq!(TotalValueLocked::<T>::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::<T>::get(), 31);
// 10's share of the reward is 1/3, since they gave 10/30 of the total shares.
assert_eq!(PoolMembers::<Runtime>::get(10).unwrap().points, 10 + 1);
assert_eq!(BondedPools::<Runtime>::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::<T>::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::<Runtime>::get(20).unwrap().points, 20 + 2);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::get(10).unwrap().points, 10);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().points, 20);
assert_eq!(BondedPools::<Runtime>::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::<Runtime>::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::<Runtime>::get(10).unwrap().points, 10 + 1);
assert_eq!(BondedPools::<Runtime>::get(1).unwrap().points, 30 + 1);
// when
assert_noop!(
Pools::bond_extra_other(RuntimeOrigin::signed(40), 40, BondExtra::Rewards),
Error::<Runtime>::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::<Runtime>::get(20).unwrap().points, 30);
assert_eq!(BondedPools::<Runtime>::get(1).unwrap().points, 41);
})
}
}
mod update_roles {
use super::*;
#[test]
fn update_roles_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(
BondedPools::<Runtime>::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::<Runtime>::PoolNotFound,
);
// depositor cannot change roles.
assert_noop!(
Pools::update_roles(
RuntimeOrigin::signed(1),
1,
ConfigOp::Set(5),
ConfigOp::Set(6),
ConfigOp::Set(7)
),
Error::<Runtime>::DoesNotHavePermission,
);
// nominator cannot change roles.
assert_noop!(
Pools::update_roles(
RuntimeOrigin::signed(901),
1,
ConfigOp::Set(5),
ConfigOp::Set(6),
ConfigOp::Set(7)
),
Error::<Runtime>::DoesNotHavePermission,
);
// bouncer
assert_noop!(
Pools::update_roles(
RuntimeOrigin::signed(902),
1,
ConfigOp::Set(5),
ConfigOp::Set(6),
ConfigOp::Set(7)
),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<T>::get(1).unwrap();
RewardPools::<Runtime>::get(1)
.unwrap()
.current_reward_counter(1, bonded_pool.points, bonded_pool.commission.current())
.unwrap()
.0
}
fn pending_rewards(of: AccountId) -> Option<BalanceOf<T>> {
let member = PoolMembers::<T>::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::<T>::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::<T>::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::<T>::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::<Runtime>::get(10).unwrap(),
PoolMember::<Runtime> { 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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::get(pool_id)
.unwrap()
.current_reward_counter(
pool_id,
BondedPools::<Runtime>::get(pool_id).unwrap().points,
Perbill::from_percent(20),
)
.unwrap();
// Then:
assert_eq!(
PoolMembers::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::PoolNotFound
);
// Sender does not have permission to set commission
assert_noop!(
Pools::set_commission_max(RuntimeOrigin::signed(1), 1, Perbill::from_percent(5)),
Error::<Runtime>::DoesNotHavePermission
);
// Cannot set max commission above GlobalMaxCommission
assert_noop!(
Pools::set_commission_max(
RuntimeOrigin::signed(900),
1,
Perbill::from_percent(100)
),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::CommissionChangeThrottled
);
// Sanity check: the resulting pool Commission state.
assert_eq!(
BondedPools::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::current_balance(pool_id), 50);
// Pool does not exist
assert_noop!(
Pools::claim_commission(RuntimeOrigin::signed(900), 9999,),
Error::<Runtime>::PoolNotFound
);
// Does not have permission.
assert_noop!(
Pools::claim_commission(RuntimeOrigin::signed(10), pool_id,),
Error::<Runtime>::DoesNotHavePermission
);
// When:
assert_ok!(Pools::claim_commission(RuntimeOrigin::signed(900), pool_id));
// Then:
assert_eq!(RewardPool::<Runtime>::current_balance(pool_id), 0);
// No more pending commission.
assert_noop!(
Pools::claim_commission(RuntimeOrigin::signed(900), pool_id,),
Error::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::DoesNotHavePermission
);
})
}
}
mod slash {
use super::*;
#[test]
fn slash_no_subpool_is_tracked() {
let bonded = |points, member_counter| BondedPool::<Runtime> {
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::<Runtime>::contains_key(11));
assert_eq!(TotalValueLocked::<T>::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::<T>::get(), 12);
assert_eq!(
PoolMembers::<Runtime>::get(11).unwrap(),
PoolMember::<Runtime> { pool_id: 1, points: 2, ..Default::default() }
);
assert_eq!(BondedPool::<Runtime>::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::<Runtime>::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::<T>::get(), 18);
assert_eq!(
PoolMembers::<Runtime>::get(12).unwrap(),
PoolMember::<Runtime> { pool_id: 1, points: 24, ..Default::default() }
);
assert_eq!(BondedPool::<Runtime>::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::<Runtime>::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));
})
}
}