Repository navigation
fix(ValidatorFacet): seed initial commission checkpoint in addValidator - #249
0xrlawrence wants to merge 1 commit into
Conversation
addValidator sets `validator.commission` but never creates a commission-rate checkpoint, so `validatorCommissionCheckpoints[validatorId]` is empty until the first `setValidatorCommission`. While it is empty, `PlumeRewardLogic.getEffectiveCommissionRateAt` falls back to the *mutable live* `validator.commission` for ALL historical timestamps. As a result the first commission change re-prices the pre-checkpoint window: the validator-accrual leg was settled at the old rate (setValidatorCommission settles before overwriting), but a staker claiming afterwards reconstructs the same window at the new rate via the fallback. On a commission decrease the two legs disagree and the shared reward treasury is left owing more than was funded for that window. Seed a commission checkpoint at genesis so historical lookups are always well-defined and never read the mutable live value. Hardens the same getEffectiveCommissionRateAt path as the previously-reported duplicate-timestamp checkpoint issue. Verified against the existing diamond test suite (no regressions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary by OctaneAdds a call to PlumeRewardLogic.createCommissionRateCheckpoint in ValidatorFacet.addValidator, writing an initial commission checkpoint at validator creation time. Changes
Worth a look
🔗 Commit Hash: 626dedc |
|
| // so the first commission change re-prices the pre-checkpoint window for stakers at the NEW | ||
| // rate (via the fallback) while the validator-accrual leg already settled it at the OLD rate — | ||
| // leaving the two legs inconsistent. Seeding here keeps every historical window well-defined. | ||
| PlumeRewardLogic.createCommissionRateCheckpoint($, validatorId, commission); |
There was a problem hiding this comment.
The new checkpoint in addValidator has no committed regression test. testCommissionRateChange_NonRetroactive calls setValidatorCommission before time advances, so it already creates the history this change is meant to supply. That test can pass even if this fix is removed.
Add a test that creates a validator at 5%, stakes without first calling setValidatorCommission, advances time, lowers commission to 0%, then claims. Check that earlier rewards keep the 5% charge and that staker rewards plus validator commission equal the gross rewards, allowing for rounding. The test should fail without this call and pass with it.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Overview
Detailed findings
|
Summary
ValidatorFacet.addValidatorsetsvalidator.commissionand seeds reward-rate checkpoints, but never creates an initial commission-rate checkpoint. SovalidatorCommissionCheckpoints[validatorId]stays empty until the firstsetValidatorCommission.While that array is empty,
PlumeRewardLogic.getEffectiveCommissionRateAt(validatorId, ts)falls back to the mutable livevalidators[validatorId].commissionfor every historical timestamp:Why that's a problem
setValidatorCommissionsettles the validator's accrued commission at the old rate (via_settleCommissionForValidatorUpToNow) before overwritingvalidator.commissionand pushing the first checkpoint. But a staker who claims later reconstructs that same pre-checkpoint window through the fallback, which now returns the new rate.So the first commission change makes the two legs disagree over the already-elapsed window:
C0C1On a decrease (
C0 > C1), the staker is charged less than the validator was credited for the same gross, and the sharedPlumeStakingRewardTreasuryis left owing~ (C0 − C1) × grossmore than was funded for that window. (This is the samegetEffectiveCommissionRateAtconsistency surface as the previously-reported duplicate-timestamp checkpoint issue — a different trigger reaching the same inconsistency.)Fix
Seed a commission-rate checkpoint at genesis inside
addValidator, so historical commission lookups are always well-defined and never read the mutable live value:PlumeRewardLogic.createCommissionRateCheckpoint($, validatorId, commission);createCommissionRateCheckpointalready de-dupes same-block entries and respectsmaxCommissionCheckpoints, and a freshly-added validator has no stakers/accrual yet, so seeding is safe.Testing
forge test --via-ir— fullPlumeStakingDiamondsuite passes with no regressions.earned(staker) + getAccruedCommission(validator)exceed the gross funded for the window by ~the full commission slice; with this change the pre-checkpoint window is no longer re-priced and the two legs sum back to the gross.🤖 Generated with Claude Code