For the complete documentation index, see llms.txt. This page is also available as Markdown.

52849 sc high claimers who claim after slash inactive updaterewardpertokenforvalidator which advances validatorlastupdatetimes to be more than slashtimestamp will lose rewards for a segment

  • Submitted on: Aug 13th 2025 at 16:07:25 UTC by @IronsideSec for Attackathon | Plume Network

  • Report ID: #52849

  • Report Type: Smart Contract

  • Severity: High

  • Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/lib/PlumeRewardLogic.sol

  • Impact summary: Theft of unclaimed yield — underpayment of users and validator commission; unaccrued tokens remain in treasury.

Brief / Intro

When a validator becomes slashed or inactive, later admin operations can advance validatorLastUpdateTimes to a time greater than or equal to the effective cap (the slash/inactive timestamp). Because updateRewardPerTokenForValidator's slashed/inactive branches write validatorLastUpdateTimes[validatorId][token] = block.timestamp and return without first settling rewards up to the cap, subsequent calls to claim skip the final pre-cap reward segment. Result: some users (and validator commission) are underpaid; the under-accrued tokens remain in the treasury.

Root cause: in updateRewardPerTokenForValidator, the slashed and inactive branches advance validatorLastUpdateTimes to block.timestamp without settling to the cap time.

Vulnerability details

PoC attack flow

1

Step: Setup

  1. Set pUSD rate to 1e18.

  2. user1 stakes 10 ETH to validator v0; user3 stakes 10 ETH to v0.

  3. Advance time vm.warp(+1000).

2

Step: Slash validator

  1. voteToSlashValidator(v0, block.timestamp + 1 days) → validator is auto-slashed at Tslash.

  2. user1 immediately claim(pUSD, v0) → receives pre-slash rewards (e.g. ~9500e18).

3

Step: Advance last update past cap

  1. vm.warp(+10). Anyone calls forceSettleValidatorCommission(v0) → this invokes updateRewardPerTokenForValidator which (in slashed branch) writes validatorLastUpdateTimes[v0][pUSD] = now (post-cap).

4

Step: Victim claim

  1. user3 calls claim(pUSD, v0) → returns 0 because the pre-slash segment is skipped.

  2. Outcome: user3 receives 0 rewards while user1 already received their payout; under-accrued tokens remain.

Flows that cause the bug

1

Validator gets slashed

  • ValidatorFacet.voteToSlashValidator(vId, expiry) sets slashedAtTimestamp = Tslash.

  • Later, admin calls ValidatorFacet.forceSettleValidatorCommission(vId) (or RewardsFacet.removeRewardToken(token) / RewardsFacet.setRewardRates(tokens, rates)).

  • Those call PlumeRewardLogic.updateRewardPerTokenForValidator($, token, vId) → slashed branch writes validatorLastUpdateTimes[vId][token] = now.

  • Subsequent RewardsFacet.claim(token, vId)calculateRewardsWithCheckpoints computes effectiveEndTime = Tslash <= lastUpdateTime → final pre-slash accrual skipped.

2

Validator set inactive

  • Admin calls ValidatorFacet.setValidatorStatus(vId, false) → records slashedAtTimestamp = Tinactive and pushes 0-rate checkpoints.

  • Any later admin op calls updateRewardPerTokenForValidator($, token, vId), which (inactive branch) writes validatorLastUpdateTimes[vId][token] = now.

  • Subsequent RewardsFacet.claim(token, vId) → final pre-inactive accrual skipped.

3

Token removal during/after slash/inactive

  • Admin calls RewardsFacet.removeRewardToken(token).

  • Loop calls PlumeRewardLogic.updateRewardPerTokenForValidator($, token, vId) then createRewardRateCheckpoint($, token, vId, 0).

  • For slashed/inactive validators, updater writes validatorLastUpdateTimes[vId][token] = now (> cap).

  • Later claims skip the pre-cap delta.

4

Set reward rates after slash/inactive

  • Admin calls RewardsFacet.setRewardRates(tokens, rates) (or setMaxRewardRate(token, newMax)).

  • createRewardRateCheckpoint first calls updateRewardPerTokenForValidator.

  • Updater on slashed/inactive writes validatorLastUpdateTimes[vId][token] = now.

  • Later claims skip the last pre-cap segment.

Impact details

  • Users are underpaid for the final pre-cap time segment.

  • Validator commission for that segment is under-accrued.

  • Treasury retains more tokens than intended.

  • Magnitude: missed_time_segment × applicable_rate × stake / precision, aggregated over affected users/tokens.

  • Likelihood and impact are high because common admin actions (settlement, rate updates, token removal) occur often and call the updater.

  • In updateRewardPerTokenForValidator, do not advance validatorLastUpdateTimes in slashed/inactive branches.

  • Alternatively, when transitioning a validator to slashed/inactive, immediately settle rewards up to the cap and set validatorLastUpdateTimes to exactly that cap time; thereafter prevent any advancement past the cap while the validator remains slashed/inactive.

  • Ensure checkpoint creation paths that invoke the updater either skip calling it when slashed/inactive, or use updater logic that respects the cap and never moves lastUpdateTimes beyond it.

References / Relevant code excerpts

  • The slashed and inactive branches currently set validatorLastUpdateTimes[validatorId][token] = block.timestamp and return without settling to the cap time — the root cause.

  • In calculateRewardsWithCheckpoints, the slashed path sets effectiveEndTime = validator.slashedAtTimestamp and only accrues a final segment if effectiveEndTime > validatorLastUpdateTime. Advancing validatorLastUpdateTimes to now (≥ cap) causes that condition to be false and the pre-cap segment to be skipped.

  • Common admin flows that call the updater after slash/inactive will systematically cause underpayment.

Code excerpts (lines referenced in original report):

Proof of Concept

Follow the steps in the provided gist: https://gist.github.com/IronsideSec/8b8eb5d9c902d7fd91ef9190f2bbecce

(Original report PoC steps included above.)


If you want, I can prepare a suggested patch diff that implements the recommended fix (e.g., settling to the cap and avoiding advancing validatorLastUpdateTimes in slashed/inactive branches) based strictly on the code excerpts.

Was this helpful?