fix: don't flag the NO fee cap on rounding alone - #321
Merged
Merged
Conversation
rawDelta truncates 4 divisions against the cap's 1, so it lands up to 4 wei above cap on periods where nothing happened — 5 of 19 on mainnet 0xd402...f711. Compare past a 4 wei slack and keep rawDelta inside it, so untouched periods stay bit-exact. Also corrects the mid-period feeRate note (bounded over-statement, not under-), the cap's provenance, and adds _stopFeeAccrual to the list of settledGrowth-raising paths.
ev-d
approved these changes
Sep 16, 2026
This branch was successfully deployed
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.
Description
Follow-up to the gross-rewards cap:
cappedfires on integer rounding alone, so thewarning names periods where nothing happened.
rawDeltais a difference of twocalcNoEarnings, each asettledGrowthterm plus acalcAccruedFeeOffChainterm — four truncating divisions against the cap's one. Eachloses under 1 wei, so a period that respects the bound exactly can still read up to
4 wei above it:
These differ by 1 whenever the dropped fractional parts carry — roughly half the time.
Measured on mainnet vault
0xd402937b3Ff3c187f727C1146a9E846275E9F711(3800 ETH, noexemptions in the window),
metrics read statistic-by-reports 20:developon 5 of 19 periods, by exactly 1 wei(28.08, 02.09, 04.09, 07.09, 11.09)
deposit raised settledGrowth above the vault growth" — none of which occurred
A quarter of the rows flagged as exemptions buries the one row that is one.
Fix
Why 4 wei. Five truncations, each losing strictly under 1 wei, so a period honouring
the bound can overshoot by at most 4. It's a proven ceiling, not a number fitted to the
observation (which was 1). Going higher starts swallowing real hits.
Why
rawDeltawins inside the slack. Tightening only the flag would leavemin(rawDelta, cap)silently dropping that 1 wei, sonodeOperatorRewardswould stilldiverge from
developon periods where nothing happened —vaults-apistores thesevalues and compares them exactly. Now a normal period is bit-identical to the pre-cap
result and the economic bound applies in full only once it actually binds.
Why the ternary replaces the branch-free form. The flag and the clamp need different
thresholds: the flag needs
cap + slack, the clamp needs a cleancap— otherwise theregression case would report 4 wei instead of 0.
capin the returned breakdown staysthe pure economic bound, unslacked, so callers and tests still see a meaningful number.
Description corrections
Three statements in #320 don't survive a check against the contracts; corrected in the
block comment, the docs and the test comment:
Mid-period
feeRatechange was described as "a bounded under-statement ratherthan an unbounded over-statement". Direction is wrong.
setFeeRatecallsdisburseFee()before applying the new rate (and requires a fresh report), sosettledGrowthbecomes the growth of the last fresh report and the entire periodaccrues at the new rate. Above the watermark the cap makes the result exact; below
it — the case pinned by the existing test, where the true fee is 0 and 2 ETH is
reported — it is a bounded over-statement.
"The cap is derived from the report leaves — a source independent of the two
Dashboard snapshots."
feeRatein the cap comes from those same snapshots; onlygrossis leaf-derived. Worth noting too thatinOutDeltaisn't in the Merkle leaf(
vault, totalValue, cumulativeLidoFees, liabilityShares, maxLiabilityShares, slashingReserve) — the contract derives it fromRefSlotCache, and the CLI reads itfrom the IPFS file's
extraValues, covered by the CID hash only.The list of
settledGrowth-raising paths was missing one._stopFeeAccrual()parks
settledGrowthattype(int104).max(~1.01e13 ETH) and is reached fromDashboard.voluntaryDisconnectandDashboard.transferVaultOwnership— ordinaryowner operations. Uncapped, a disconnecting vault would report a node operator fee
around 1e12 ETH and a net APR near −1e17%. No occurrences across all 24 mainnet
vaults in the last 55 days, but both call sites are live.
Also documented: a normal disbursement cannot breach the cap, because
_disburseFeesettles at the growth of a reported value and those reports are the same grid the metrics
sample. That's what makes the bound safe rather than merely conservative.
Testing notes
yarn test— 508 passed / 48 files.yarn lint,yarn buildclean.New case,
does not flag a healthy period whose raw delta only rounds past the cap:builds a period whose fractional parts carry and pins
rawDelta === cap + 1,capped === false,fee === rawDelta. Fails without the slack.End-to-end against mainnet, archive RPC:
0xd402…f711(healthy)developdevelop0x2773…cfcb3(real exemption)28.08→ 0 ETH / −0.0246%