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
Flows that cause the bug
Validator gets slashed
ValidatorFacet.voteToSlashValidator(vId, expiry)setsslashedAtTimestamp = Tslash.Later, admin calls
ValidatorFacet.forceSettleValidatorCommission(vId)(orRewardsFacet.removeRewardToken(token)/RewardsFacet.setRewardRates(tokens, rates)).Those call
PlumeRewardLogic.updateRewardPerTokenForValidator($, token, vId)→ slashed branch writesvalidatorLastUpdateTimes[vId][token] = now.Subsequent
RewardsFacet.claim(token, vId)→calculateRewardsWithCheckpointscomputeseffectiveEndTime = Tslash <= lastUpdateTime→ final pre-slash accrual skipped.
Validator set inactive
Admin calls
ValidatorFacet.setValidatorStatus(vId, false)→ recordsslashedAtTimestamp = Tinactiveand pushes 0-rate checkpoints.Any later admin op calls
updateRewardPerTokenForValidator($, token, vId), which (inactive branch) writesvalidatorLastUpdateTimes[vId][token] = now.Subsequent
RewardsFacet.claim(token, vId)→ final pre-inactive accrual skipped.
Token removal during/after slash/inactive
Admin calls
RewardsFacet.removeRewardToken(token).Loop calls
PlumeRewardLogic.updateRewardPerTokenForValidator($, token, vId)thencreateRewardRateCheckpoint($, token, vId, 0).For slashed/inactive validators, updater writes
validatorLastUpdateTimes[vId][token] = now(> cap).Later claims skip the pre-cap delta.
Set reward rates after slash/inactive
Admin calls
RewardsFacet.setRewardRates(tokens, rates)(orsetMaxRewardRate(token, newMax)).createRewardRateCheckpointfirst callsupdateRewardPerTokenForValidator.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.
Recommended remediation
In
updateRewardPerTokenForValidator, do not advancevalidatorLastUpdateTimesin slashed/inactive branches.Alternatively, when transitioning a validator to slashed/inactive, immediately settle rewards up to the cap and set
validatorLastUpdateTimesto 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
lastUpdateTimesbeyond it.
References / Relevant code excerpts
The slashed and inactive branches currently set
validatorLastUpdateTimes[validatorId][token] = block.timestampand return without settling to the cap time — the root cause.In
calculateRewardsWithCheckpoints, the slashed path setseffectiveEndTime = validator.slashedAtTimestampand only accrues a final segment ifeffectiveEndTime > validatorLastUpdateTime. AdvancingvalidatorLastUpdateTimesto 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?