Conversation
MC1823315
approved these changes
Aug 30, 2026
nadir-akhtar
approved these changes
Sep 1, 2026
antojoseph
approved these changes
Sep 1, 2026
antojoseph
left a comment
Contributor
There was a problem hiding this comment.
This looks good to me
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation:
An operator with queued withdrawals could be over-slashed: the protocol burns or redistributes more shares than the withdrawal queue actually backs. The over-count is wei-scale (rounding slack), but it grows with the number of queued stakers.
The root cause is a rounding mismatch between how queued-slashable shares are recorded and how they are later consumed:
_removeSharesAndQueueWithdrawalrecorded the raw scaled valuescaleForQueueWithdrawal = floor(depositShares · scalingFactor / WAD)— a single floor._getSlashableSharesInQueuere-multiplies the cumulative recorded value by the operator's magnitude to derive slashable shares.Because the recorded value skipped the magnitude round-trip, two rounding effects stack. First, per-staker slack: shares that don't divide cleanly against a fractional magnitude round up relative to the backing actually removed from the operator. Second, sum-then-floor vs. floor-then-sum: the queued figure floors once over the cumulative total, while the backing removed is a sum of per-staker floors, and
floor(Σ xᵢ) ≥ Σ floor(xᵢ). The recorded amount therefore drifts above the backing removed, and that inflated figure flows straight intototalDepositSharesToSlash→increaseBurnOrRedistributableShares.Modifications:
DelegationManager._removeSharesAndQueueWithdrawal: record the magnitude-round-tripped valuewithdrawableShares.divWad(slashingFactor)(withslashingFactor == 0 → 0) instead of the raw scaled shares. This ties the recorded amount to the backing actually removed from the operator via_decreaseDelegation, so re-multiplying by magnitude at slash time cannot exceed that backing by construction.src/test/integration/tests/QueueSlashAccounting.t.sol— two fuzz tests covering deposit→delegate→slash→queue→slash and slash→deposit→queue→redelegate→slash→complete-as-shares, assertingqueueSlashable ≤ removedFromOperator(LST).DelegationUnit.t.sol(DelegationManagerUnitTests_slashingShares), includingtest_slashOperatorShares_BeaconQueueSharesAreBackedAfterCombinedRounding, which exercises the beacon-chain path with a realbeaconChainSlashingFactordecrease.Result:
Recorded queued-slashable shares can no longer exceed the backing removed from the operator. The queue contribution to a slash is bounded by what the queue actually holds, eliminating the over-burn.
For reviewers / auditors:
assertLefailures; beacon-chain unit:queue slashable 1 > 0 removed), and reintroducing it turns them green. The assertions are exact wei bounds — they are the load-bearing guard, so any loosening would let the bug back through._cumulativeScaledSharesHistoryhas exactly one writer (_addQueuedSlashableShares, one call site), so no sibling path records with the old pattern. The fix is complete, not local.completeQueuedWithdrawal— worth pinning a concrete loss figure.beaconChainSlashingFactorcanceling between the queue-time divide (by fullslashingFactor = maxMagnitude · beaconChainSlashingFactor) and the slash-time multiply (bymaxMagnitudealone). It cancels today — the beacon test proves it — but the cancellation is implicit, and a future change to how_getSlashableSharesInQueuere-derives operator shares could silently break it. A code comment at that site would help.