feat(redemption-rewards): phase 2 — raise the redeem payout cap over - #676
Draft
iamsahu wants to merge 3 commits into
Draft
Conversation
…stopped-earning demand Consumes the phase 1 report field. Redeemers now keep the yield their stake earned while it was still in the exit queue. Mechanism. Payout stays min(settlement value, cap) — the clamp against the withdrawal event's actual ETH at RedeemManager.1.sol:430 is untouched. What changes is the cap: it is the request-time value of the matched slice, RAISED over any sub-range whose backing principal has been reported as having crossed exit_epoch, to the pool rate of the report that observed it. Three PRD requirements fall out of that shape rather than needing separate code: - FR1/AC2: a fill involving no exit produces no mark, so the slice sits in a mark gap and is capped at rate_at_request exactly as today. No on-chain "was this an exit?" classification is needed — which matters, because exit ETH and buffer ETH are fungible in BalanceToRedeem and the distinction is not recoverable at fill time. - FR2/AC1+AC2: the cap is a ceiling, never a floor, so a redeemer still absorbs a loss between mark and settlement on the same terms as a holder who stayed. - Section 6 non-goals: the cap stops rising at exit_epoch, so nothing accrues through the ~27h withdrawability delay or the 0-8d sweep tail. RateMarkStack, not a loop over requests. Crediting each affected request during the report would be a protocol halt vector: _requestRedeem enforces only amount != 0, so dust requests let a later mark range span an unbounded loop inside the oracle report, and OOG there stops reporting, which stops _reportWithdrawToRedeemManager, which halts every redemption. One push per report is O(1) on the report path; the walk is deferred to claim time where the claimant pays for their own span and cannot be charged for anyone else's. The stack is deliberately NOT contiguous. Settlement can outrun marking, which advances the settled height past the mark cursor and leaves a permanent gap — and gaps are meaningful, they are exactly the LsETH paid at the request rate. So _performDichotomicResolution's contiguity assumption does not apply and _findRateMarkAtOrBefore is a separate predecessor search. RedeemRequestAnchor is a side-car mapping, not new fields on RedeemRequest: the queue is a raw-slot array with stride 5, so widening would shift every one of the 178 live mainnet elements and break the ABI of getRedeemRequestDetails. It stores the exact (lsETHAtRequest, ethAtRequest) pair rather than a rate because maxRedeemableEth is a decrementing budget whose implied rate drifts upward after a partial claim below the request rate (pinned in phase 0). A zero anchor means a pre-upgrade request and selects the original path, which is also the launch cutover. maxRedeemableEth's decrement is now saturating. It is checked arithmetic, and a marked payout may legitimately exceed the request-time budget, so an unguarded subtraction would revert the whole claimRedeemRequests call with Panic(0x11). Call ordering: reportStoppedEarning runs after the rebase and fee mint (so the rate is final) and BEFORE _reportWithdrawToRedeemManager (so demand settled in the same report is marked before settlement burns its shares). It is not gated on slashing containment — containment lengthens a queued redeemer's wait while leaving settlement priced at the depressed rate, so suspending accrual there would penalise them twice — and it must not be skipped at all, since the cumulative field is already persisted by then. Cost: RedeemManagerV1 9,167 -> 11,108 runtime bytes (13,468 free). RiverV1 unchanged at 23,068, since the new call sits in the delegatecalled library. Tests: 10 new, covering unmarked/fully-marked/partially-marked/blended, accrual capped at the mark rate, downside passthrough, marks accumulating across reports, clamping to markable demand, marks skipping settled demand, a slice positioned before the first mark, and the legacy no-anchor path. Full suite 1070 pass; heavy fuzzing (exact exceeding-buffer deltas and total-supply reconstruction) clean.
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
…port rate (#678) reportStoppedEarning derived the locked rate from live callbacks into River (sharesFromUnderlyingBalance / underlyingBalanceFromShares), and LibOracleReporting calls it after LastConsensusLayerReport.set and after the _onEarnings fee mint. The mark therefore captured the closing rate of the interval, crediting the redeemer with one interval of rewards more than intended — the very interval during which their backing principal stopped earning. River now values the delta at its pre-report snapshot, taken one line before _pullCLFunds, where the conversion views still return the previous interval's answer. Past that point the asset balance is transiently double counted: _pullCLFunds credits exited and skimmed eth to the buffers while the stored report still counts the same eth in validatorsBalance. The valuation travels to the redeem manager as data rather than being read there, so the anchor cannot silently drift if setConsensusLayerData is reordered. reportStoppedEarning takes the amount in both eth and LsETH; their ratio is the rate the mark locks. (eth, LsETH) rather than a scaled rate keeps the shape a RateMark already stores, matching how RedeemRequestAnchor holds (ethAtRequest, lsETHAtRequest), and avoids the extra rounding a 1e18-scaled scalar would compound. Marking the whole reported amount now needs no conversion at all; only the clamped case divides, scaling the eth leg in the same proportion so the locked rate survives the clamp. The call site keeps its position: running before _reportWithdrawToRedeemManager, so that demand settled in the same report is marked before its shares are burned, is unchanged and still load bearing. Tests: the existing redeem manager call sites are untouched — the mock helper derives the LsETH leg from its own rate, reproducing the previous math exactly. Four new tests pin the behaviour and all four fail against the old live-rate implementation: two at the redeem manager (the pair overrides the live rate; the clamp preserves the reported rate) and two end to end on River, with and without slashing containment.
… feat/redemption-rewards-p2-rate-marks
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.
…stopped-earning demand
Description
This pull request introduces a new "stopped earning" mechanism to the redeem manager, enabling the protocol to mark portions of redeem demand as having stopped accruing rewards at a fixed rate when their backing principal exits the consensus layer. This is achieved by tracking "rate marks" and "request anchors," which together allow for more accurate and fair payout calculations, especially in the presence of delayed withdrawals and protocol upgrades. The changes also ensure backward compatibility for requests made before this upgrade.
The most important changes are:
Stopped Earning and Rate Mark Mechanism:
reportStoppedEarningfunction toRedeemManagerV1, which marks redeem demand whose backing principal stopped earning, records a "rate mark" with the pre-report ETH/LsETH rate, and emits relevant events. [1] [2] [3]RateMarkStackandRedeemRequestAnchorimports, storage, and accessors, along with new getter functions for retrieving rate mark and anchor details. [1] [2] [3] [4]Payout Calculation Logic:
_rateMarkCursor,_settledHeight,_findRateMarkAtOrBefore,_sliceCap) to calculate the ETH payout cap for redeem requests, taking into account any rate marks that may raise the cap above the original request-time value.Oracle Reporting Integration:
LibOracleReportingto value the stopped-earning principal at the pre-report rate, pass this data to the redeem manager, and ensure the stopped-earning report is always called before withdrawal settlement. [1] [2] [3]These changes collectively add a robust mechanism for marking and handling stopped-earning principal, improving fairness and transparency for redeemers in the protocol.
Notice
Pull Request Type
Breaking changes (if applicable)
Testing
Manual tests (if applicable)
Additional comments