Skip to content

Commit 7b40622

Browse files
committed
fix: contract stake count invariant
1 parent 642068f commit 7b40622

File tree

2 files changed

+91
-18
lines changed

2 files changed

+91
-18
lines changed

pallets/dapp-staking/src/lib.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,9 +1237,7 @@ pub mod pallet {
12371237
// This is bounded by max allowed number of stake entries per account.
12381238
let to_be_deleted: Vec<T::SmartContract> = StakerInfo::<T>::iter_prefix(&account)
12391239
.filter_map(|(smart_contract, stake_info)| {
1240-
if stake_info.period_number() < current_period
1241-
&& !stake_info.is_bonus_eligible()
1242-
|| stake_info.period_number() < threshold_period
1240+
if Self::is_expired_staker_entry(&stake_info, current_period, threshold_period)
12431241
{
12441242
Some(smart_contract)
12451243
} else {
@@ -1788,6 +1786,16 @@ pub mod pallet {
17881786
current_period.saturating_sub(T::RewardRetentionInPeriods::get())
17891787
}
17901788

1789+
/// Returns `true` when the staking entry can be removed, meaning it is past the claim window and with no claimable bonus reward.
1790+
fn is_expired_staker_entry(
1791+
info: &SingularStakingInfo,
1792+
current_period: PeriodNumber,
1793+
threshold_period: PeriodNumber,
1794+
) -> bool {
1795+
(info.period_number() < current_period && !info.is_bonus_eligible())
1796+
|| info.period_number() < threshold_period
1797+
}
1798+
17911799
/// Unlocking period expressed in the number of blocks.
17921800
pub fn unlocking_period() -> BlockNumber {
17931801
T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into())
@@ -2283,12 +2291,12 @@ pub mod pallet {
22832291
.staked_period()
22842292
.ok_or(Error::<T>::NoClaimableRewards)?;
22852293

2286-
// Check if the rewards have expired
22872294
let protocol_state = ActiveProtocolState::<T>::get();
2288-
ensure!(
2289-
staked_period >= Self::oldest_claimable_period(protocol_state.period_number()),
2290-
Error::<T>::RewardExpired
2291-
);
2295+
let current_period = protocol_state.period_number();
2296+
let threshold_period = Self::oldest_claimable_period(current_period);
2297+
2298+
// Check if the rewards have expired
2299+
ensure!(staked_period >= threshold_period, Error::<T>::RewardExpired);
22922300

22932301
// Calculate the reward claim span
22942302
let earliest_staked_era = ledger
@@ -2300,7 +2308,7 @@ pub mod pallet {
23002308

23012309
// The last era for which we can theoretically claim rewards.
23022310
// And indicator if we know the period's ending era.
2303-
let (last_period_era, period_end) = if staked_period == protocol_state.period_number() {
2311+
let (last_period_era, period_end) = if staked_period == current_period {
23042312
(protocol_state.era.saturating_sub(1), None)
23052313
} else {
23062314
PeriodEnd::<T>::get(&staked_period)
@@ -2343,6 +2351,25 @@ pub mod pallet {
23432351
T::StakingRewardHandler::payout_reward(&account, reward_sum)
23442352
.map_err(|_| Error::<T>::RewardPayoutFailed)?;
23452353

2354+
let stale_contracts: Vec<T::SmartContract> = StakerInfo::<T>::iter_prefix(&account)
2355+
.filter_map(|(smart_contract, stake_info)| {
2356+
if Self::is_expired_staker_entry(&stake_info, current_period, threshold_period)
2357+
{
2358+
Some(smart_contract)
2359+
} else {
2360+
None
2361+
}
2362+
})
2363+
.collect();
2364+
2365+
if !stale_contracts.is_empty() {
2366+
let removed: u32 = stale_contracts.len().unique_saturated_into();
2367+
for contract in stale_contracts {
2368+
StakerInfo::<T>::remove(&account, &contract);
2369+
}
2370+
ledger.contract_stake_count.saturating_reduce(removed);
2371+
}
2372+
23462373
Self::update_ledger(&account, ledger)?;
23472374

23482375
rewards.into_iter().for_each(|(era, reward)| {

pallets/dapp-staking/src/test/tests.rs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,19 +1322,12 @@ fn stake_fails_due_to_too_many_staked_contracts() {
13221322
Error::<Test>::TooManyStakedContracts
13231323
);
13241324

1325-
// Advance into next period, error should still happen
1325+
// Advance into next period, staking should work after stale entries are removed during the rewards claim
13261326
advance_to_next_period();
13271327
for _ in 0..required_number_of_reward_claims(account) {
13281328
assert_claim_staker_rewards(account);
13291329
}
1330-
assert_noop!(
1331-
DappStaking::stake(
1332-
RuntimeOrigin::signed(account),
1333-
excess_smart_contract.clone(),
1334-
10
1335-
),
1336-
Error::<Test>::TooManyStakedContracts
1337-
);
1330+
assert_stake(account, &excess_smart_contract, 10);
13381331
})
13391332
}
13401333

@@ -4515,3 +4508,56 @@ fn unstake_from_unregistered_use_correct_stake_amount() {
45154508
assert_unstake_from_unregistered(staker_1, &smart_contract_1);
45164509
})
45174510
}
4511+
4512+
// Tests a previous bug where the contract_stake_count kept increasing for the same contract entry
4513+
// when a staker continued staking across periods.
4514+
// This occurred because stale entries were not properly cleaned up when claiming staker rewards.
4515+
#[test]
4516+
fn restaking_across_periods_does_not_inflate_contract_count() {
4517+
ExtBuilder::default().build_and_execute(|| {
4518+
let owner = 1;
4519+
let staker = 2;
4520+
let smart_contract = MockSmartContract::wasm(1 as AccountId);
4521+
assert_register(owner, &smart_contract);
4522+
assert_lock(staker, 1_000);
4523+
4524+
let stake_amount = 10;
4525+
advance_to_next_subperiod();
4526+
assert_ok!(DappStaking::stake(
4527+
RuntimeOrigin::signed(staker),
4528+
smart_contract.clone(),
4529+
stake_amount,
4530+
));
4531+
4532+
let mut ledger = Ledger::<Test>::get(&staker);
4533+
assert_eq!(ledger.contract_stake_count, 1);
4534+
assert_eq!(
4535+
StakerInfo::<Test>::iter_prefix(&staker).count(),
4536+
1,
4537+
"Only one staker entry should exist for the contract."
4538+
);
4539+
4540+
let max_contracts: u32 = <Test as Config>::MaxNumberOfStakedContracts::get();
4541+
4542+
for _ in 2..=(max_contracts + 1) {
4543+
advance_to_next_period();
4544+
for _ in 0..required_number_of_reward_claims(staker) {
4545+
assert_claim_staker_rewards(staker);
4546+
}
4547+
advance_to_next_subperiod();
4548+
assert_ok!(DappStaking::stake(
4549+
RuntimeOrigin::signed(staker),
4550+
smart_contract.clone(),
4551+
stake_amount,
4552+
));
4553+
4554+
ledger = Ledger::<Test>::get(&staker);
4555+
assert_eq!(ledger.contract_stake_count, 1);
4556+
assert_eq!(
4557+
StakerInfo::<Test>::iter_prefix(&staker).count(),
4558+
1,
4559+
"Only one staker entry should still exist for the contract."
4560+
);
4561+
}
4562+
});
4563+
}

0 commit comments

Comments
 (0)