@@ -17,6 +17,10 @@ import {WithdrawalQueue} from "./WithdrawalQueue.sol";
1717
1818interface IRedeemsReserveVault {
1919 function withdrawToLido (uint256 _amount ) external ;
20+ function getRedeemedShares () external view returns (uint256 );
21+ function getRedeemedEther () external view returns (uint256 );
22+ function flushSharesToBurner () external ;
23+ function resetRedeemedEther () external ;
2024}
2125
2226interface IStakingRouter {
@@ -145,9 +149,9 @@ contract Accounting {
145149 Contracts memory contracts = _loadOracleReportContracts ();
146150 if (msg .sender != contracts.accountingOracle) revert NotAuthorized ("handleOracleReport " , msg .sender );
147151
148- // Do NOT reconcile vault before snapshot. The stale tracked vault ETH keeps
149- // preInternalEther overcounted, and the vault delta is passed to smoothenTokenRebase
150- // as an explicit ether decrease — creating headroom for the paired burn .
152+ // Vault redemption counters are read on-chain during _simulateOracleReport.
153+ // redeemedEther is subtracted from the smoothenTokenRebase base,
154+ // and redeemedShares are added outside the rebase limiter .
151155
152156 PreReportState memory pre = _snapshotPreReportState (contracts, false );
153157 CalculatedValues memory update = _simulateOracleReport (contracts, pre, _report);
@@ -189,41 +193,41 @@ contract Accounting {
189193 // Principal CL balance is sum of previous balances and new deposits
190194 update.principalClBalance = _pre.clValidatorsBalance + _pre.clPendingBalance + _pre.depositedBalance;
191195
192- // Limit the rebase to avoid oracle frontrunning
193- // by leaving some ether to sit in EL rewards vault or withdrawals vault
194- // and/or leaving some shares unburnt on Burner to be processed on future reports
195- //
196- // NOTE: vaultDelta is merged with etherToFinalizeWQ into a single _etherToDecrease param
197- // due to stack-too-deep in smoothenTokenRebase (0.8.9 without via-IR).
198- // Spec describes them as separate decreaseEther calls; mathematically equivalent.
199- uint256 vaultDelta = _getRedeemsReserveVaultDelta ();
196+ // Read redemption counters from vault (on-chain, includes all redemptions)
197+ (uint256 redeemedShares , uint256 redeemedEther ) = _getRedeemedCounters ();
198+
199+ // Limit the rebase to avoid oracle frontrunning.
200+ // The base is reduced by redeemedEther so the limiter sees the actual protocol size.
200201 (
201202 update.withdrawalsVaultTransfer,
202203 update.elRewardsVaultTransfer,
203204 update.sharesToBurnForWithdrawals,
204- update.totalSharesToBurn // shares to burn from Burner balance
205+ update.totalSharesToBurn // shares to burn from Burner balance (WQ + cover)
205206 ) = _contracts.oracleReportSanityChecker.smoothenTokenRebase (
206- _pre.totalPooledEther - _pre.externalEther,
207+ _pre.totalPooledEther - _pre.externalEther - redeemedEther ,
207208 _pre.totalShares - _pre.externalShares,
208209 update.principalClBalance,
209210 _report.clValidatorsBalance + _report.clPendingBalance,
210211 _report.withdrawalVaultBalance,
211212 _report.elRewardsVaultBalance,
212213 _report.sharesRequestedToBurn,
213- update.etherToFinalizeWQ + vaultDelta, // paired decreases: WQ finalization + vault redemptions
214+ update.etherToFinalizeWQ,
214215 update.sharesToFinalizeWQ
215216 );
216217
218+ // Add redemption shares outside the limiter — rate-neutral, must all burn on this report
219+ update.totalSharesToBurn += redeemedShares;
220+
217221 uint256 postInternalSharesBeforeFees = _pre.totalShares -
218222 _pre.externalShares - // internal shares before
219- update.totalSharesToBurn; // shares to be burned for withdrawals and cover
223+ update.totalSharesToBurn; // shares to be burned (WQ + cover + redemptions)
220224
221225 update.postInternalEther =
222- _pre.totalPooledEther - _pre.externalEther // internal ether before (includes stale vault ETH)
226+ _pre.totalPooledEther - _pre.externalEther
223227 + _report.clValidatorsBalance + _report.clPendingBalance + update.withdrawalsVaultTransfer - update.principalClBalance
224228 + update.elRewardsVaultTransfer
225229 - update.etherToFinalizeWQ
226- - vaultDelta; // subtract vault delta: stale vault ETH was overcounted in preInternalEther
230+ - redeemedEther;
227231
228232 // Pre-calculate total amount of protocol fees as the amount of shares that will be minted to pay it
229233 (update.sharesToMintAsFees, update.feeDistribution) = _calculateProtocolFees (
@@ -382,12 +386,14 @@ contract Accounting {
382386 LIDO.internalizeExternalBadDebt (_pre.badDebtToInternalize);
383387 }
384388
389+ // Flush accumulated redemption shares from vault to Burner before commit
390+ _flushVaultSharesToBurner ();
391+
385392 if (_update.totalSharesToBurn > 0 ) {
386393 _contracts.burner.commitSharesToBurn (_update.totalSharesToBurn);
387394 }
388395
389- // Reconcile RedeemsReserveVault AFTER burns — tracked vault ETH updated to actual.
390- // Must happen after commitSharesToBurn so the paired burn + reconcile are both applied.
396+ // Reconcile RedeemsReserveVault — tracked vault ETH updated to actual.
391397 _reconcileRedeemsReserveVault ();
392398
393399 LIDO.collectRewardsAndProcessWithdrawals (
@@ -431,31 +437,38 @@ contract Accounting {
431437 );
432438 }
433439
434- /// @dev Computes how much ETH left the RedeemsReserveVault since the last report.
435- /// Returns 0 if no vault is configured or vault balance increased (e.g. donation).
436- /// Used to pass the delta to smoothenTokenRebase as an ether decrease.
437- function _getRedeemsReserveVaultDelta () internal view returns (uint256 ) {
440+ /// @dev Reads redemption counters from the vault. Returns (0, 0) if no vault configured.
441+ function _getRedeemedCounters () internal view returns (uint256 redeemedShares , uint256 redeemedEther ) {
438442 address vault = LIDO.getRedeemsReserveVault ();
439- if (vault == address (0 )) return 0 ;
443+ if (vault == address (0 )) return ( 0 , 0 ) ;
440444
441- uint256 tracked = LIDO.getRedeemsReserveVaultEth ();
442- uint256 actual = vault.balance;
443- return tracked > actual ? tracked - actual : 0 ;
445+ redeemedShares = IRedeemsReserveVault (vault).getRedeemedShares ();
446+ redeemedEther = IRedeemsReserveVault (vault).getRedeemedEther ();
447+ }
448+
449+ /// @dev Flushes accumulated redemption shares from vault to Burner.
450+ /// Called before commitSharesToBurn so redeemed shares are included in the burn.
451+ function _flushVaultSharesToBurner () internal {
452+ address vault = LIDO.getRedeemsReserveVault ();
453+ if (vault == address (0 )) return ;
454+
455+ IRedeemsReserveVault (vault).flushSharesToBurner ();
444456 }
445457
446- /// @dev Reconciles the RedeemsReserveVault tracked ETH to the actual vault balance.
447- /// Called AFTER commitSharesToBurn so the paired burn + reconcile are applied together .
458+ /// @dev Reconciles the RedeemsReserveVault tracked ETH to the actual vault balance
459+ /// and resets the redeemed ether counter .
448460 function _reconcileRedeemsReserveVault () internal {
449461 address vault = LIDO.getRedeemsReserveVault ();
450462 if (vault == address (0 )) return ;
451463
452464 LIDO.reconcileRedeemsReserveVault (vault.balance);
465+ IRedeemsReserveVault (vault).resetRedeemedEther ();
453466 }
454467
455468 /// @dev Replenishes or drains the RedeemsReserveVault to match the reserve target.
456469 /// Called after collectRewardsAndProcessWithdrawals when the buffer is finalized.
457470 /// Fills to target from unreserved surplus first. When surplus is insufficient,
458- /// splits the shared allocation (withdrawalsReserve + unreserved) by replenishmentShareBP .
471+ /// splits the shared allocation (withdrawalsReserve + unreserved) by growthShareBP .
459472 function _replenishRedeemsReserveVault () internal {
460473 address vault = LIDO.getRedeemsReserveVault ();
461474 if (vault == address (0 )) return ;
@@ -465,23 +478,20 @@ contract Accounting {
465478
466479 if (target > actual) {
467480 uint256 deficit = target - actual;
468- uint256 unreserved = LIDO.getDepositableEther ();
481+ uint256 depositableEther = LIDO.getDepositableEther ();
469482 uint256 toPush;
470483
471- if (unreserved >= deficit) {
472- // Surplus fully covers the deficit — no impact on WQ
484+ if (depositableEther >= deficit) {
473485 toPush = deficit;
474486 } else {
475- // Surplus insufficient — split shared allocation by replenishment share
476- uint256 shareBP = LIDO.getRedeemsReserveReplenishmentShare ();
487+ uint256 shareBP = LIDO.getRedeemsReserveGrowthShare ();
477488 if (shareBP == 0 ) {
478- // No forced growth — reserve grows only from genuine surplus
479- toPush = unreserved;
489+ toPush = depositableEther;
480490 } else {
481491 uint256 withdrawalsReserve = LIDO.getWithdrawalsReserve ();
482- uint256 sharedAllocation = withdrawalsReserve + unreserved ;
492+ uint256 sharedAllocation = withdrawalsReserve + depositableEther ;
483493 uint256 reserveShare = sharedAllocation * shareBP / TOTAL_BASIS_POINTS;
484- toPush = reserveShare > unreserved ? reserveShare : unreserved ;
494+ toPush = reserveShare > depositableEther ? reserveShare : depositableEther ;
485495 if (toPush > deficit) toPush = deficit;
486496 }
487497 }
0 commit comments