Skip to content

fix: dAppStaking - Ledger contract stake count invariant#1569

Merged
ipapandinas merged 12 commits intomasterfrom
fix/dapp-staking
Dec 16, 2025
Merged

fix: dAppStaking - Ledger contract stake count invariant#1569
ipapandinas merged 12 commits intomasterfrom
fix/dapp-staking

Conversation

@ipapandinas
Copy link
Contributor

@ipapandinas ipapandinas commented Dec 5, 2025

Pull Request Summary

When a staker claims rewards for a finished period, the ledger's claim_up_to_era function zeros out the staked and staked_future fields once the period end is reached. However, the corresponding StakerInfo entries remain in storage and the ledger's contract_stake_count is never decremented. The next time the user restakes on the same contract (or when the cleanup extrinsic tries to reconcile counts), the pallet treats it as a new entry and bumps contract_stake_count again. Over time, the counter grows out of sync with the actual number of StakerInfo entries and may eventually block further stakes because the ledger appears to have reached MaxNumberOfStakedContracts, even though storage contains far fewer entries.

This PR fixes this bug by syncing the contract_stake_count to the remaining valid entries during a cleanup_expired_entries. A new test scenario ensures the expected behavior.

Check list**

  • added or updated unit tests

@ipapandinas ipapandinas added shiden related to shiden runtime dapps-staking Dapps Staking astar Related to Astar shibuya related to shibuya runtime This PR/Issue is related to the topic “runtime”. labels Dec 5, 2025
@ipapandinas ipapandinas requested a review from Dinonard December 5, 2025 15:46
Copy link
Contributor

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work.

The approach with the cleanup during reward claiming is hacky and very inefficient. Also, what if user locks himself out from being able to stake anything - they won't be able to claim any rewards, and thus will remain locked out.

The fix should address claim calls where ledger isn't updated when contract stake entry is removed - or just identify if contract take entry should have been removed but wasn't.

The cleanup extrinsic can also be modified as suggested in the comments. Ideally, we can just use it to do the cleanup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you should change this to set the stake account to the number of entries that haven't been cleaned up. This should implicitly help cleanup the current mess.

@@ -1237,9 +1237,7 @@ pub mod pallet {
// This is bounded by max allowed number of stake entries per account.
let to_be_deleted: Vec<T::SmartContract> = StakerInfo::<T>::iter_prefix(&account)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remind me why doesn't this function properly cleanup faulty entries?

If it does, please check my next comment.

T::StakingRewardHandler::payout_reward(&account, reward_sum)
.map_err(|_| Error::<T>::RewardPayoutFailed)?;

let stale_contracts: Vec<T::SmartContract> = StakerInfo::<T>::iter_prefix(&account)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should reward claiming extrinsic be responsible for cleaning up everything?

@ipapandinas
Copy link
Contributor Author

@Dinonard I misunderstood the issue, now it should be properly fixed using the cleanup extrinsic as you suggested.

Copy link
Contributor

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for the cleanup looks good to me now.

The part that is missing though is the fix for why this error can even happen, no?

@ipapandinas
Copy link
Contributor Author

/bench astar,shibuya,shiden pallet_dapp_staking

@github-actions
Copy link

Benchmarks job is scheduled at https://github.com/AstarNetwork/Astar/actions/runs/20239643390.
Please wait for a while.
Branch: fix/dapp-staking
SHA: b7ac135

@github-actions
Copy link

Benchmarks have been finished.
You can download artifacts if exists https://github.com/AstarNetwork/Astar/actions/runs/20239643390.


Self::update_ledger(&account, ledger)?;

if ledger_was_cleared {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1569 (comment)

Same question again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes true, this is not claim responsibility, it doesn't create the inconsistency itself in first place. In that case I suppose the new check on staking to properly replace an existing key is enough. I can add a test for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, claim staker call should not care for it.

What needs to be done:

  • clean-up call for inconsistent ledgers - ✅
  • logic updates to ensure inconsistency doesn't happen - ✅ (from what I can see)
  • tests that reproduce the reported issue (should fail without your changes, and pass after the claim_staker is fixed)

This changes shouldn't impact performance/benchmarks in any way.

Comment on lines +1242 to 1258
// Bounded by max allowed number of stake entries per account.
for (smart_contract, stake_info) in StakerInfo::<T>::iter_prefix(&account) {
let stake_period = stake_info.period_number();

// Check if this entry should be kept
let should_keep = stake_period == current_period
|| (stake_period >= threshold_period
&& stake_period < current_period
&& stake_info.is_bonus_eligible());

if should_keep {
remaining = remaining.saturating_add(1);
} else {
to_be_deleted.push(smart_contract);
}
}
let entries_to_delete = to_be_deleted.len();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for this, and then it can be merged.

You shouldn't be able to create this scenario using regular calls (if you can, it means there is still a bug), so just adding values directly into the storage should do the trick.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, the new cleanup_expired_entries_correctly_with_ledger_counter_reconciliation test covers the proper cleanup and the counter alignment.

@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
pallets/astar-xcm-benchmarks/src/fungible 100% 0%
pallets/dapp-staking/rpc/runtime-api/src 0% 0%
pallets/unified-accounts/src 80% 0%
precompiles/xcm/src 69% 0%
pallets/collator-selection/src 82% 0%
pallets/dapp-staking/src/test 0% 0%
pallets/astar-xcm-benchmarks/src 86% 0%
pallets/democracy-mbm/src 30% 0%
primitives/src 53% 0%
pallets/vesting-mbm/src 87% 0%
chain-extensions/types/assets/src 0% 0%
precompiles/assets-erc20/src 77% 0%
pallets/collective-proxy/src 94% 0%
chain-extensions/pallet-assets/src 54% 0%
pallets/price-aggregator/src 75% 0%
pallets/static-price-provider/src 91% 0%
pallets/xc-asset-config/src 64% 0%
precompiles/substrate-ecdsa/src 67% 0%
pallets/inflation/src 58% 0%
chain-extensions/unified-accounts/src 0% 0%
pallets/dynamic-evm-base-fee/src 84% 0%
pallets/dapp-staking/src/benchmarking 95% 0%
precompiles/sr25519/src 56% 0%
pallets/dapp-staking/src 80% 0%
precompiles/unified-accounts/src 100% 0%
precompiles/dapp-staking/src/test 0% 0%
precompiles/dispatch-lockdrop/src 83% 0%
primitives/src/xcm 60% 0%
precompiles/dapp-staking/src 89% 0%
chain-extensions/types/unified-accounts/src 0% 0%
pallets/ethereum-checked/src 76% 0%
pallets/astar-xcm-benchmarks/src/generic 100% 0%
Summary 72% (3895 / 5387) 0% (0 / 0)

Minimum allowed line rate is 50%

@ipapandinas ipapandinas merged commit 944da78 into master Dec 16, 2025
8 checks passed
@ipapandinas ipapandinas deleted the fix/dapp-staking branch December 16, 2025 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

astar Related to Astar dapps-staking Dapps Staking runtime This PR/Issue is related to the topic “runtime”. shibuya related to shibuya shiden related to shiden runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants