Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NPoS] Fix for Reward Deficit in the pool #1255

Merged
merged 116 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
7429f0b
make total reward payout counter never decrease
Ank4n Aug 29, 2023
fc135d7
improve comments
Ank4n Aug 29, 2023
a580bc1
new call to top up the pool
Ank4n Aug 29, 2023
aace388
format
Ank4n Aug 29, 2023
66b593d
topped up funds are not rewardeable to delegators
Ank4n Aug 29, 2023
56cafc4
test for top up function
Ank4n Aug 29, 2023
767cc3a
check user has enough balance and limit max transfer to their free ba…
Ank4n Aug 29, 2023
626f544
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 2, 2023
c4c7e96
contracts: Update to wasmi 0.31 (#1350)
athei Sep 2, 2023
a1a6d3e
substrate: chain-spec paths corrected in zombienet tests (#1362)
michalkucharczyk Sep 2, 2023
bda10a8
Ensure cumulus/bridges is ignored by formatter and run it (#1369)
gavofyork Sep 3, 2023
60895d0
feat: add futures api to `TransactionPool` (#1348)
yjhmelody Sep 3, 2023
86f0cc1
Bump proc-macro-warning from 0.4.1 to 0.4.2 (#1376)
dependabot[bot] Sep 4, 2023
0423b0d
Bump the known_good_semver group with 1 update (#1375)
dependabot[bot] Sep 4, 2023
2b89df6
Update `fmt` file and some authors (#1379)
ggwpez Sep 4, 2023
d3ba197
Markdown linter (#1309)
chevdor Sep 4, 2023
b14a908
[xcm-emulator] Redo Parachain init (#1356)
NachoPal Sep 4, 2023
5d37b81
add Treasurer to SchedulerOrigin (#1325)
xlc Sep 4, 2023
3d4089c
Cleanup repo (a tiny bit) (#1382)
ggwpez Sep 4, 2023
79edd91
approval-voting: use proper hash when querying session info (#1387)
ordian Sep 4, 2023
daf6141
[ci] Remove runtime-benchmarks from tests (#1335)
alvicsam Sep 4, 2023
c08d14d
Extract block announce validation from `ChainSync` (#1170)
dmitry-markin Sep 4, 2023
8e484a8
Contracts: Update read_sandbox (#1390)
pgherveou Sep 4, 2023
b5433ff
rust docs: add simple analytics (#1377)
liamaharon Sep 5, 2023
b1a3223
Move Relay-Specific Shared Code to One Place (#1193)
joepetrowski Sep 5, 2023
5bec497
Bump thiserror from 1.0.47 to 1.0.48 (#1396)
dependabot[bot] Sep 5, 2023
25f30b8
Bump actions/checkout from 3 to 4 (#1398)
dependabot[bot] Sep 5, 2023
d5565d3
Get rid of polling in `WarpSync` (#1265)
dmitry-markin Sep 5, 2023
23926b7
Remove redundant calls to `borrow()` (#1393)
mrcnski Sep 5, 2023
c292021
Remove dynamic dispatch using `Ext` (#1399)
athei Sep 5, 2023
d129363
Enforce a decoding limit in MultiAssets (#1395)
franciscoaguirre Sep 5, 2023
3e4576f
fmt fixes (#1413)
chevdor Sep 6, 2023
37161ee
Add PRdoc check (#1408)
chevdor Sep 6, 2023
a27e24e
RFC 14: Improve locking mechanism for parachains (#1290)
xlc Sep 6, 2023
425a72d
Bump enumn from 0.1.11 to 0.1.12 (#1412)
dependabot[bot] Sep 6, 2023
7520e04
Fix the wasm runtime substitute caching bug (#1416)
Sep 6, 2023
a317ef9
Fix PRdoc check (#1419)
chevdor Sep 6, 2023
c725bbe
Remove deprecated `pallet_balances`'s `set_balance_deprecated` and `t…
juangirini Sep 6, 2023
5e56b1a
pallet asset-conversion additional quote tests (#1371)
gilescope Sep 6, 2023
1be427e
GHW for building and publishing docker images (#1391)
EgorPopelyaev Sep 6, 2023
2f31a4c
Fix nothing scheduled on session boundary (#1403)
eskimor Sep 6, 2023
43d7a8c
Prevent a fail prdoc check to block (#1433)
chevdor Sep 6, 2023
433bc9d
zombienet: use another collator image for the slashing test (#1386)
ordian Sep 6, 2023
c6f1b9f
Adds base benchmark for do_tick in broker pallet (#1235)
gupnik Sep 7, 2023
7f2ae6e
Forgotten `polkadot-core-primitives/std` (#1440)
bkontur Sep 7, 2023
024210b
remove unused `keystore_uri` (#1421)
yjhmelody Sep 7, 2023
3ff561f
[ci] Enable flacky collector for tests (#1439)
alvicsam Sep 7, 2023
329df04
polkadot: pin one block per session (#1220)
ordian Sep 7, 2023
5fb02e7
[ci] Return publish-rustdoc (#1402)
alvicsam Sep 7, 2023
301c438
[ci] change image for prdoc gha (#1452)
alvicsam Sep 7, 2023
cbc1d0d
Update Chrono to 0.4.30 (#1451)
chevdor Sep 8, 2023
1089fe8
Bump actions/cache from 3.3.1 to 3.3.2 (#1456)
dependabot[bot] Sep 8, 2023
15898c3
Bump hex-literal from 0.3.4 to 0.4.1 (#1438)
dependabot[bot] Sep 8, 2023
eae53d5
Update ed25519-dalek to v2 (#1446)
chevdor Sep 8, 2023
f5d911a
Switch prdoc to the latest image (#1460)
chevdor Sep 8, 2023
675e7da
does not compile as balance is not serializable and used in genesis c…
Ank4n Sep 8, 2023
f3e8a4f
remove balance dependency in genesis config altogether
Ank4n Sep 8, 2023
8a3891e
compiles
Ank4n Sep 13, 2023
5848428
allow 1 holds
Ank4n Sep 13, 2023
84b6246
freezing currency is more appropriate here
Ank4n Sep 13, 2023
4d34b59
all tests pass
Ank4n Sep 13, 2023
c5fc61d
use Currency trait to read free balance
Ank4n Sep 13, 2023
22fd5de
verify ed is frozen
Ank4n Sep 13, 2023
b535954
format
Ank4n Sep 13, 2023
4a3387b
impl for adjusting ED
Ank4n Sep 13, 2023
36a87a4
use Runtime Freeze Reason
Ank4n Sep 14, 2023
de12c8f
add tests for adjusting ED
Ank4n Sep 14, 2023
138ecb2
fmt
Ank4n Sep 14, 2023
6a3bfc3
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 14, 2023
a1c4970
added by mistake while merging, reverting
Ank4n Sep 14, 2023
29465b1
basic migration
Ank4n Sep 14, 2023
05592c7
migrations
Ank4n Sep 15, 2023
b147cfe
add weights to new call
Ank4n Sep 15, 2023
8bfef28
use fungible traits in np bench
Ank4n Sep 15, 2023
e705898
bench for new instruction
Ank4n Sep 15, 2023
b0202e2
allow fn with runtime benchmarks
Ank4n Sep 15, 2023
05f4bd8
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 15, 2023
b0c00fe
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 15, 2023
d432fba
pending rewards for pools
Ank4n Sep 16, 2023
d960582
add missing features
Ank4n Sep 16, 2023
d3d8ef4
add a note about using Currency::balance()
Ank4n Sep 16, 2023
bb00d8d
rename misspelling in the function
Ank4n Sep 16, 2023
d4cd942
fix staking np integration test
Ank4n Sep 16, 2023
51a44c7
fix unused warning
Ank4n Sep 16, 2023
9b1042e
fix kitchensink runtime
Ank4n Sep 16, 2023
d54f029
use default config for node cli np chain spec
Ank4n Sep 16, 2023
14d4530
fmt
Ank4n Sep 16, 2023
ca17f83
remove unused imports
Ank4n Sep 16, 2023
88c8959
fix feature scopes
Ank4n Sep 16, 2023
3af90e9
minor
Ank4n Sep 16, 2023
4303bb5
make balances serde for genesis config
Ank4n Sep 16, 2023
809236e
add migration for pools to all runtimes
Ank4n Sep 16, 2023
e538a0f
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 16, 2023
ba9e53c
use balance everywhere in benchmark
Ank4n Sep 16, 2023
caac4e1
allow freezes in the kitchensink runtime
Ank4n Sep 16, 2023
d939a82
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 17, 2023
c138e17
add docs and minor refactor
Ank4n Sep 18, 2023
34541b2
comment
Ank4n Sep 18, 2023
5628881
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Sep 19, 2023
d20138a
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Sep 19, 2023
7de37d3
PR comments
Ank4n Sep 21, 2023
7969fdf
defensive assert while dissolving pool
Ank4n Sep 21, 2023
1e993eb
make unversioned migration private
Ank4n Sep 21, 2023
a15af2b
fix migrations in runtime
Ank4n Sep 21, 2023
58612c3
migration fix and name the new call better
Ank4n Sep 21, 2023
04f89b7
rename freeze unfreeze functions
Ank4n Sep 21, 2023
be4a86c
fmt
Ank4n Sep 21, 2023
f1425c3
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 21, 2023
dd1ad02
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 22, 2023
0395a4c
".git/.scripts/commands/bench/bench.sh" --subcommand=runtime --runtim…
Sep 22, 2023
7b40492
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 22, 2023
9eee4f6
add pr doc
Ank4n Sep 22, 2023
7ea6b34
make adjust ed permissionless
Ank4n Sep 26, 2023
6bb9425
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 26, 2023
8d4361a
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 27, 2023
5f072c5
Merge branch 'master' into ankan/fix-np-reward-deficit
Ank4n Sep 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions substrate/frame/nomination-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,13 +1300,22 @@ impl<T: Config> RewardPool<T> {
self.total_commission_pending =
self.total_commission_pending.saturating_add(new_pending_commission);

// Store the total payouts at the time of this update. Total payouts are essentially the
// entire historical balance of the reward pool, equating to the current balance + the total
// rewards that have left the pool + the total commission that has left the pool.
self.last_recorded_total_payouts = balance
// Total payouts are essentially the entire historical balance of the reward pool, equating
// to the current balance + the total rewards that have left the pool + the total commission
// that has left the pool.
let last_recorded_total_payouts = balance
.checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed))
.ok_or(Error::<T>::OverflowRisk)?;

// Store the total payouts at the time of this update.
//
// An increase in ED could cause `last_recorded_total_payouts` to decrease but we should not
// allow that to happen since an already paid out reward cannot decrease. The reward account
// might go in deficit temporarily in this exceptional case but it will be corrected once
// new rewards are added to the pool.
self.last_recorded_total_payouts =
self.last_recorded_total_payouts.max(last_recorded_total_payouts);

Ok(())
}

Expand Down Expand Up @@ -1487,6 +1496,7 @@ impl<T: Config> SubPools<T> {
/// `no_era` pool. This is guaranteed to at least be equal to the staking `UnbondingDuration`. For
/// improved UX [`Config::PostUnbondingPoolsWindow`] should be configured to a non-zero value.
pub struct TotalUnbondingPools<T: Config>(PhantomData<T>);

impl<T: Config> Get<u32> for TotalUnbondingPools<T> {
fn get() -> u32 {
// NOTE: this may be dangerous in the scenario bonding_duration gets decreased because
Expand Down Expand Up @@ -1770,6 +1780,8 @@ pub mod pallet {
},
/// Pool commission has been claimed.
PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf<T> },
/// Pool topped up in case of a reward deficit.
PoolToppedUp { pool_id: PoolId, top_up_value: BalanceOf<T>, deficit: BalanceOf<T> },
}

#[pallet::error]
Expand Down Expand Up @@ -1845,6 +1857,8 @@ pub mod pallet {
InvalidPoolId,
/// Bonding extra is restricted to the exact pending reward amount.
BondExtraRestricted,
/// No reward deficit to top up.
NoRewardDeficit,
}

#[derive(Encode, Decode, PartialEq, TypeInfo, PalletError, RuntimeDebug)]
Expand Down Expand Up @@ -2631,6 +2645,24 @@ pub mod pallet {
let who = ensure_signed(origin)?;
Self::do_claim_commission(who, pool_id)
}

/// Top up the reward deficit of a pool permissionlessly.
///
/// This can happen in situations where ED has increased from the time the pool was created.
/// The increased ED eats up from the available rewards of the pool, and the pool can end up
/// with a net deficit.
#[pallet::call_index(21)]
// FIXME(ank4n): bench + tests
#[pallet::weight(T::WeightInfo::claim_commission())]
pub fn top_up_reward_deficit(
origin: OriginFor<T>,
pool_id: PoolId,
#[pallet::compact] max_transfer: BalanceOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let caller_balance = T::Currency::free_balance(&who);
Self::do_top_up_reward_deficit(who, pool_id, max_transfer.min(caller_balance))
}
}

#[pallet::hooks]
Expand Down Expand Up @@ -3029,6 +3061,62 @@ impl<T: Config> Pallet<T> {
Ok(())
}

fn pool_pending_rewards(pool: PoolId) -> Result<BalanceOf<T>, sp_runtime::DispatchError> {
let bonded_pool = BondedPools::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
let reward_pool = RewardPools::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;

let current_rc = if !bonded_pool.points.is_zero() {
let commission = bonded_pool.commission.current();
reward_pool.current_reward_counter(pool, bonded_pool.points, commission)?.0
} else {
Default::default()
};

Ok(PoolMembers::<T>::iter()
Ank4n marked this conversation as resolved.
Show resolved Hide resolved
.filter(|(_, d)| d.pool_id == pool)
.map(|(_, d)| d.pending_rewards(current_rc).unwrap_or_default())
.fold(0u32.into(), |acc: BalanceOf<T>, x| acc.saturating_add(x)))
}

fn do_top_up_reward_deficit(
who: T::AccountId,
pool: PoolId,
max_transfer: BalanceOf<T>,
) -> DispatchResult {
let pool_pending_rewards = Self::pool_pending_rewards(pool)?;
let reward_balance = RewardPool::<T>::current_balance(pool);
let deficit = pool_pending_rewards.saturating_sub(reward_balance);

ensure!(!deficit.is_zero(), Error::<T>::NoRewardDeficit);

// do not top up beyond the max transfer amount desired by the caller.
let top_up_amount = deficit.min(max_transfer);
T::Currency::transfer(
&who,
&Self::create_reward_account(pool),
top_up_amount,
ExistenceRequirement::KeepAlive,
)?;

// The topped up amount should not be claimable by delegators.
RewardPools::<T>::mutate(pool, |maybe_reward_pool| {
if let Some(pool) = maybe_reward_pool {
pool.last_recorded_total_payouts.saturating_accrue(top_up_amount);
Ok(())
} else {
Err(Error::<T>::PoolNotFound)
}
})?;

Self::deposit_event(Event::<T>::PoolToppedUp {
pool_id: pool,
top_up_value: top_up_amount,
deficit: deficit.saturating_sub(top_up_amount),
});

Ok(())
}

/// Ensure the correctness of the state of this pallet.
///
/// This should be valid before or after each state transition of this pallet.
Expand Down
39 changes: 39 additions & 0 deletions substrate/frame/nomination-pools/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,45 @@ pub fn fully_unbond_permissioned(member: AccountId) -> DispatchResult {
Pools::unbond(RuntimeOrigin::signed(member), member, points)
}

pub fn pending_rewards_for_pool(pool: PoolId) -> Balance {
Pools::pool_pending_rewards(pool).expect("pool should exist")
}

pub fn pending_rewards_for_delegator(delegator: AccountId) -> Balance {
let member = PoolMembers::<T>::get(delegator).unwrap();
let bonded_pool = BondedPools::<T>::get(member.pool_id).unwrap();
let reward_pool = RewardPools::<T>::get(member.pool_id).unwrap();

assert!(!bonded_pool.points.is_zero());

let commission = bonded_pool.commission.current();
let current_rc = reward_pool
.current_reward_counter(member.pool_id, bonded_pool.points, commission)
.unwrap()
.0;

member.pending_rewards(current_rc).unwrap_or_default()
}

#[derive(PartialEq, Debug)]
pub enum RewardImbalance {
// There is no reward deficit.
Surplus(Balance),
// There is a reward deficit.
Deficit(Balance),
}

pub fn reward_imbalance(pool: PoolId) -> RewardImbalance {
let pending_rewards = pending_rewards_for_pool(pool);
let current_balance = RewardPool::<Runtime>::current_balance(pool);

if pending_rewards > current_balance {
RewardImbalance::Deficit(pending_rewards - current_balance)
} else {
RewardImbalance::Surplus(current_balance - pending_rewards)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
157 changes: 155 additions & 2 deletions substrate/frame/nomination-pools/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,11 @@ mod bonded_pool {
}

mod reward_pool {
use super::*;
use crate::mock::RewardImbalance::{Deficit, Surplus};

#[test]
fn current_balance_only_counts_balance_over_existential_deposit() {
use super::*;

ExtBuilder::default().build_and_execute(|| {
let reward_account = Pools::create_reward_account(2);

Expand All @@ -324,6 +325,158 @@ mod reward_pool {
assert_eq!(RewardPool::<Runtime>::current_balance(2), 1);
});
}

#[test]
fn ed_change_causes_reward_deficit() {
Ank4n marked this conversation as resolved.
Show resolved Hide resolved
ExtBuilder::default().max_members_per_pool(Some(5)).build_and_execute(|| {
// original ED
ExistentialDeposit::set(5);

// 11 joins the pool
Balances::make_free_balance_be(&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.
Balances::make_free_balance_be(&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.
Balances::make_free_balance_be(&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));
});
}

#[test]
fn top_up_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
Balances::make_free_balance_be(&11, 500);
assert_ok!(Pools::join(RuntimeOrigin::signed(11), 90, 1));

// Pool some rewards
deposit_rewards(100);

// 12 joins the pool.
Balances::make_free_balance_be(&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: top up reduces the deficit
Balances::make_free_balance_be(&99, 1000);
// caller can set safe ceiling for top up.
let max_top_up: Balance = 20;
assert_ok!(Pools::top_up_reward_deficit(RuntimeOrigin::signed(99), 1, max_top_up));
// only upto max_transfer is topped up.
assert_eq!(
pool_events_since_last_call(),
vec![Event::PoolToppedUp {
pool_id: 1,
top_up_value: max_top_up,
deficit: 45 - max_top_up
},]
);
assert_eq!(reward_imbalance(1), Deficit(25));

// Top up the remaining deficit
assert_ok!(Pools::top_up_reward_deficit(RuntimeOrigin::signed(99), 1, 1000));
assert_eq!(
pool_events_since_last_call(),
vec![Event::PoolToppedUp { pool_id: 1, top_up_value: 25, deficit: 0 },]
);
assert_eq!(reward_imbalance(1), Surplus(0));

// Trying to top up again does not work
assert_err!(
Pools::top_up_reward_deficit(RuntimeOrigin::signed(99), 1, 1000),
Error::<T>::NoRewardDeficit
);
});
}

#[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
Balances::make_free_balance_be(&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::top_up_reward_deficit(RuntimeOrigin::signed(11), 1, 100),
Error::<T>::NoRewardDeficit
);
});
}
}

mod unbond_pool {
Expand Down
Loading