Lines
99.17 %
Functions
100 %
Branches
// Copyright (C) Moondance Labs Ltd.
// This file is part of Tanssi.
// Tanssi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Tanssi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Tanssi. If not, see <http://www.gnu.org/licenses/>
use {
super::*,
crate::{
assert_eq_last_events,
pools::{ActivePoolKind, AutoCompounding, ManualRewards},
Pallet,
},
frame_support::assert_err,
sp_runtime::DispatchError,
tp_traits::DistributeRewards,
};
struct Delegation {
candidate: AccountId,
delegator: AccountId,
pool: ActivePoolKind,
stake: Balance,
}
struct RewardRequest {
collator: AccountId,
rewards: Balance,
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DelegatorState {
auto_stake: Balance,
auto_shares: Balance,
manual_stake: Balance,
manual_shares: Balance,
pending_rewards: Balance,
struct Distribution {
collator_auto: Balance,
collator_manual: Balance,
delegators_auto: Balance,
delegators_manual: Balance,
fn setup_delegations(delegations: &[Delegation]) {
use crate::traits::Timer;
let block_number = <Runtime as crate::Config>::JoiningRequestTimer::now();
// Request all delegations
for d in delegations {
assert_ok!(Staking::request_delegate(
RuntimeOrigin::signed(d.delegator),
d.candidate,
d.pool,
d.stake,
));
// Wait for delegation to be executable
for _ in 0..BLOCKS_TO_WAIT {
run_block();
// Execute delegations
assert_ok!(Staking::execute_pending_operations(
vec![PendingOperationQuery {
delegator: d.delegator,
operation: match d.pool {
ActivePoolKind::AutoCompounding =>
PendingOperationKey::JoiningAutoCompounding {
candidate: d.candidate,
at: block_number
ActivePoolKind::ManualRewards => PendingOperationKey::JoiningManualRewards {
}]
fn test_distribution(
delegations: &[Delegation],
reward: RewardRequest,
stakes: &[DelegatorState],
distribution: Distribution,
) {
use frame_support::traits::Imbalance;
// Create new supply for rewards
let new_supply = currency_issue(reward.rewards);
let new_supply_amount = new_supply.peek();
// Setup delegations before rewarding.
setup_delegations(delegations);
// Distribute rewards
let candidate_balance_before = total_balance(&ACCOUNT_CANDIDATE_1);
assert_ok!(crate::pools::accumulate_rewards::<Runtime>(
reward.collator,
new_supply
let _ = crate::pools::distribute_accumulated_rewards_immediately::<Runtime>();
let candidate_balance_after = total_balance(&ACCOUNT_CANDIDATE_1);
// Check events matches the expected distribution.
assert_eq_last_events!(vec![Event::<Runtime>::RewardsDistributed {
collator: reward.collator,
collator_ac_rewards: distribution.collator_auto,
collator_mc_rewards: distribution.collator_manual,
delegators_ac_rewards: distribution.delegators_auto,
delegators_mc_rewards: distribution.delegators_manual,
},]);
// Check the state of each delegate match the expected values.
for expected in stakes {
let actual = DelegatorState {
candidate: expected.candidate,
delegator: expected.delegator,
auto_stake: AutoCompounding::<Runtime>::computed_stake(
&expected.candidate,
&expected.delegator,
)
.expect("to have stake")
.0,
auto_shares: AutoCompounding::<Runtime>::shares(
manual_stake: ManualRewards::<Runtime>::computed_stake(
manual_shares: ManualRewards::<Runtime>::shares(
pending_rewards: ManualRewards::<Runtime>::pending_rewards(
.expect("no overflow")
similar_asserts::assert_eq!(&actual, expected);
// Additional checks.
assert_eq!(
distribution.collator_auto
+ distribution.collator_manual
+ distribution.delegators_auto
+ distribution.delegators_manual,
new_supply_amount,
"Distribution total doesn't match requested reward"
);
candidate_balance_before + distribution.collator_manual,
candidate_balance_after,
"candidate balance should be increased by collator_manual"
let sum_manual: Balance = stakes.iter().map(|s| s.pending_rewards).sum();
sum_manual, distribution.delegators_manual,
"sum of pending rewards should match distributed delegators manual rewards"
let sum_auto_stake_before: Balance = delegations
.iter()
.filter_map(|d| match d.pool {
ActivePoolKind::AutoCompounding => Some(d.stake),
_ => None,
})
.sum();
let sum_auto_stake_after = AutoCompounding::<Runtime>::total_staked(&reward.collator).0;
sum_auto_stake_after - sum_auto_stake_before,
distribution.collator_auto + distribution.delegators_auto,
"diff between sum of auto stake before and after distribution should match distributed auto rewards"
#[test]
fn candidate_only_manual_only() {
ExtBuilder::default().build().execute_with(|| {
test_distribution(
&[Delegation {
candidate: ACCOUNT_CANDIDATE_1,
delegator: ACCOUNT_CANDIDATE_1,
pool: ActivePoolKind::ManualRewards,
stake: 1_000_000_000,
}],
RewardRequest {
collator: ACCOUNT_CANDIDATE_1,
rewards: 1_000_000,
&[DelegatorState {
auto_shares: 0,
auto_stake: 0,
manual_shares: 1_000,
manual_stake: 1_000_000_000,
pending_rewards: 800_000,
Distribution {
collator_auto: 0,
collator_manual: 200_000, // 20% of rewards
delegators_auto: 0,
delegators_manual: 800_000, // 80% of rewards
});
fn candidate_only_auto_only() {
pool: ActivePoolKind::AutoCompounding,
rewards: 10_000_000,
auto_shares: 1_001,
// initial auto stake is 1_000_000_000 for
// 8_000_000 is shared between all delegators, so 1 share
// is now worth 1_008_000_000 / 1000 = 1_008_000 now
// collator is should be rewarded 2_000_000 in auto shares,
// which allows to get 1 more share, so the collator now
// have 1_001 shares worth
// 1_008_000_000 + 1_008_000 = 1_009_008_000
auto_stake: 1_009_008_000,
manual_shares: 0,
manual_stake: 0,
pending_rewards: 0,
// 20% of rewards, rounded down to closest amount of Auto shares
// AFTER delegators rewards has been rewarded
collator_auto: 1_008_000,
// dust from collator_auto
collator_manual: 992_000,
delegators_auto: 8_000_000, // 80% of rewards
delegators_manual: 0,
fn candidate_only_mixed() {
&[
Delegation {
stake: 250_000_000,
],
auto_stake: 1_007_406_400,
manual_shares: 250,
manual_stake: 250_000_000,
pending_rewards: 1_600_000,
collator_auto: 1_006_400,
collator_manual: 993_600,
delegators_auto: 6_400_000,
delegators_manual: 1_600_000,
fn delegators_manual_only() {
delegator: ACCOUNT_DELEGATOR_1,
DelegatorState {
pending_rewards: 6_400_000,
collator_manual: 2_000_000,
delegators_manual: 8_000_000,
fn delegators_auto_only() {
auto_shares: 250,
auto_stake: 251_600_000,
delegators_auto: 8_000_000,
fn delegators_mixed() {
stake: 500_000_000,
auto_stake: 1_004_559_388,
manual_shares: 500,
manual_stake: 500_000_000,
pending_rewards: 1_777_500,
auto_shares: 500,
auto_stake: 501_777_916,
pending_rewards: 888_750,
collator_auto: 1_003_555,
collator_manual: 996_445,
// Total stake: 2_250_000_000
// Auto stake: 1_500_000_000
// Manual stake: 750_000_000
// Manual shares: 750
// Rewards towards delegators: 80% of 10_000_000 = 8_000_000
// Rewards towards manual deleg
// = 8_000_000 * 750_000_000 / 2_250_000_000
// = 2_666_666
// => 2_666_250 (rounding down to closest multiple of 750)
// gives dust of 2_666_666 - 2_666_250 = 416
delegators_manual: 2_666_250,
// Rewards towards auto deleg
// = Rewards deleg - Rewards manual deleg
// = 8_000_000 - 2_666_250
// = 5_333_750
delegators_auto: 5_333_750,
fn candidate_only_no_stake() {
// Rewarding a candidate that does not have any stake works
&[],
collator_manual: 1_000_000, // 100% of rewards
delegators_manual: 0, // 0% of rewards
fn delegator_only_candidate_zero() {
fn delegator_only_candidate_no_stake_auto_compounding() {
// Rewarding a candidate that does not have any stake, while some delegator
// has stake for that candidate
delegators_auto: 800_000, // 80% of rewards
fn reward_distribution_is_transactional() {
let request_time = <Runtime as crate::Config>::JoiningRequestTimer::now();
RuntimeOrigin::signed(ACCOUNT_CANDIDATE_1),
ACCOUNT_CANDIDATE_1,
ActivePoolKind::AutoCompounding,
1_000_000_000,
operation: PendingOperationKey::JoiningAutoCompounding {
at: request_time
let total_staked_before =
pools::AutoCompounding::<Runtime>::total_staked(&ACCOUNT_CANDIDATE_1);
// Increase ED to make reward destribution fail when resolving
// credit to Staking account.
MockExistentialDeposit::set(u128::MAX);
let rewards = Balances::issue(1_000_000_000);
assert_err!(
Staking::distribute_rewards(ACCOUNT_CANDIDATE_1, rewards),
DispatchError::NoProviders
let total_staked_after =
total_staked_before, total_staked_after,
"distribution should be reverted"
fn rewards_are_aggregated_then_distributed() {
// Pending rewards storage is empty at genesis
assert!(!crate::PendingRewards::<Runtime>::exists());
setup_delegations(&[
candidate: ACCOUNT_CANDIDATE_2,
delegator: ACCOUNT_CANDIDATE_2,
]);
// First time calling it will never reward immediatly since it write the current instant for
// the first time. We'll reward multiple times and ensure no distribution occurs.
let block_number = <Runtime as crate::Config>::RewardsDistributionTimer::now();
let candidates_balance_before = [
total_balance(&ACCOUNT_CANDIDATE_1),
total_balance(&ACCOUNT_CANDIDATE_2),
];
// - 1st reward
let rewards = currency_issue(1_000_000);
assert_ok!(Pallet::<Runtime>::distribute_rewards(
rewards
crate::PendingRewards::<Runtime>::get(),
Some(crate::pools::PendingRewards {
last_distribution: block_number,
rewards: [(ACCOUNT_CANDIDATE_1, 1_000_000)].into(),
// - 2nd reward
let rewards = currency_issue(1_100_000);
ACCOUNT_CANDIDATE_2,
rewards: [
(ACCOUNT_CANDIDATE_1, 1_000_000),
(ACCOUNT_CANDIDATE_2, 1_100_000)
]
.into(),
// - 3rd reward
let rewards = currency_issue(1_200_000);
(ACCOUNT_CANDIDATE_1, 2_200_000),
// Rewards have only be aggregated, not actually distributed.
let candidates_balance_after = [
assert_eq!(candidates_balance_before, candidates_balance_after);
// Let's move to next block, rewards should be distributed in on_initialize.
<Runtime as crate::Config>::RewardsDistributionTimer::now(),
block_number + 1
last_distribution: block_number + 1,
rewards: Default::default(), // distribution occured, no rewards pending
assert_ne!(candidates_balance_before, candidates_balance_after);
assert_eq_last_events!(vec![
Event::<Runtime>::RewardsDistributed {
collator_ac_rewards: 0,
collator_mc_rewards: 440_000,
delegators_ac_rewards: 1_173_500,
delegators_mc_rewards: 586_500,
Event::RewardsDistributed {
collator: ACCOUNT_CANDIDATE_2,
collator_mc_rewards: 220_000,
delegators_ac_rewards: 587_000,
delegators_mc_rewards: 293_000