Skip to content

Redeems Reserve Push-based approach#1756

Draft
bulbozaur wants to merge 6 commits intofeat/staking-router-3.0from
feat/redeems-reserve-over-sr-3.0-push-approach
Draft

Redeems Reserve Push-based approach#1756
bulbozaur wants to merge 6 commits intofeat/staking-router-3.0from
feat/redeems-reserve-over-sr-3.0-push-approach

Conversation

@bulbozaur
Copy link
Copy Markdown
Member

Summary

Redeems reserve — governance-configurable portion of buffered ether protected from CL deposits and WQ finalization,
enabling instant stETH→ETH redemptions. Full spec.

In the push-based approach, reserve ETH is physically held in a dedicated RedeemsBuffer contract. Shares are forwarded
to Burner's isolated redeem track immediately during redeem() and burned outside the rebase limiter on the next oracle
report.

RedeemsBuffer

  • Deployed behind OssifiableProxy with initialize() + Versioned
  • redeem() — transfers shares to Burner via requestBurnSharesForRedeem, sends ETH to recipient, tracks
    _redeemedEther
  • withdrawUnredeemed() — returns unredeemed ETH to Lido, resets counters. Gated by Lido
  • fundReserve() — accepts ETH from Lido during report push
  • PausableUntil for Gate Seal compatibility
  • setRedeemsBuffer(address(0)) requires current.balance == 0

Lido

  • collectRewardsAndProcessWithdrawals gains _redeemedEther parameter, handles full ETH round-trip internally: withdraw
    unredeemed → reconcile → standard flow → grow reserve → push
  • setRedeemsReserveTargetRatio stores ratio only, no immediate reduction — deferred to next report
  • _growRedeemsReserve() replenishes reserve toward target on each report
  • receiveFromRedeemsBuffer() accepts returned ETH

Burner: isolated redeem track

  • requestBurnSharesForRedeem — gated by REQUEST_BURN_SHARES_ROLE
  • commitRedeemSharesToBurn — burns all pending redeem shares unconditionally
  • _getExcessStETHShares includes redeemSharesBurnRequested
  • getSharesRequestedToBurn returns (cover, nonCover, redeem)

Accounting

  • Reads burner.getRedeemSharesRequestedToBurn() and buffer.getRedeemedEther()
  • Subtracts redeemedEther from smoothenTokenRebase base
  • smoothedSharesToBurn (limiter-constrained) and redeemSharesToBurn (unconditional) tracked separately
  • Passes redeemedEther to collectRewardsAndProcessWithdrawals

Comment on lines +99 to +122
function redeem(uint256 _stETHAmount, address _ethRecipient) external onlyRole(REDEEMER_ROLE) whenResumed {
if (_stETHAmount == 0) revert ZeroAmount();
if (_ethRecipient == address(0)) revert ZeroRecipient();
if (LIDO.isStopped()) revert LidoStopped();
if (WITHDRAWAL_QUEUE.isBunkerModeActive()) revert BunkerMode();
if (WITHDRAWAL_QUEUE.isPaused()) revert WQPaused();

uint256 sharesAmount = LIDO.getSharesByPooledEth(_stETHAmount);
uint256 etherAmount = LIDO.getPooledEthByShares(sharesAmount);

uint256 available = _reserveBalance - _redeemedEther;
if (etherAmount > available) {
revert InsufficientReserve(etherAmount, available);
}

LIDO.transferSharesFrom(msg.sender, address(this), sharesAmount);
BURNER.requestBurnSharesForRedeem(address(this), sharesAmount);
_redeemedEther += etherAmount;

(bool success,) = _ethRecipient.call{value: etherAmount}("");
if (!success) revert ETHTransferFailed(_ethRecipient, etherAmount);

emit Redeemed(msg.sender, _ethRecipient, _stETHAmount, sharesAmount, etherAmount);
}

Check failure

Code scanning / Slither

Functions that send Ether to arbitrary destinations High

Comment on lines +158 to +161
function recoverERC20(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (_token == address(LIDO)) revert StETHRecoveryNotAllowed();
IERC20(_token).transfer(msg.sender, _amount);
}

Check failure

Code scanning / Slither

Unchecked transfer High

Comment on lines +86 to +90
function initialize(address _admin) external {
_initializeContractVersionTo(1);
LIDO.approve(address(BURNER), type(uint256).max);
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +99 to +122
function redeem(uint256 _stETHAmount, address _ethRecipient) external onlyRole(REDEEMER_ROLE) whenResumed {
if (_stETHAmount == 0) revert ZeroAmount();
if (_ethRecipient == address(0)) revert ZeroRecipient();
if (LIDO.isStopped()) revert LidoStopped();
if (WITHDRAWAL_QUEUE.isBunkerModeActive()) revert BunkerMode();
if (WITHDRAWAL_QUEUE.isPaused()) revert WQPaused();

uint256 sharesAmount = LIDO.getSharesByPooledEth(_stETHAmount);
uint256 etherAmount = LIDO.getPooledEthByShares(sharesAmount);

uint256 available = _reserveBalance - _redeemedEther;
if (etherAmount > available) {
revert InsufficientReserve(etherAmount, available);
}

LIDO.transferSharesFrom(msg.sender, address(this), sharesAmount);
BURNER.requestBurnSharesForRedeem(address(this), sharesAmount);
_redeemedEther += etherAmount;

(bool success,) = _ethRecipient.call{value: etherAmount}("");
if (!success) revert ETHTransferFailed(_ethRecipient, etherAmount);

emit Redeemed(msg.sender, _ethRecipient, _stETHAmount, sharesAmount, etherAmount);
}
Comment on lines +99 to +122
function redeem(uint256 _stETHAmount, address _ethRecipient) external onlyRole(REDEEMER_ROLE) whenResumed {
if (_stETHAmount == 0) revert ZeroAmount();
if (_ethRecipient == address(0)) revert ZeroRecipient();
if (LIDO.isStopped()) revert LidoStopped();
if (WITHDRAWAL_QUEUE.isBunkerModeActive()) revert BunkerMode();
if (WITHDRAWAL_QUEUE.isPaused()) revert WQPaused();

uint256 sharesAmount = LIDO.getSharesByPooledEth(_stETHAmount);
uint256 etherAmount = LIDO.getPooledEthByShares(sharesAmount);

uint256 available = _reserveBalance - _redeemedEther;
if (etherAmount > available) {
revert InsufficientReserve(etherAmount, available);
}

LIDO.transferSharesFrom(msg.sender, address(this), sharesAmount);
BURNER.requestBurnSharesForRedeem(address(this), sharesAmount);
_redeemedEther += etherAmount;

(bool success,) = _ethRecipient.call{value: etherAmount}("");
if (!success) revert ETHTransferFailed(_ethRecipient, etherAmount);

emit Redeemed(msg.sender, _ethRecipient, _stETHAmount, sharesAmount, etherAmount);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +165 to +170
function recoverStETHShares() external onlyRole(DEFAULT_ADMIN_ROLE) {
uint256 shares = LIDO.sharesOf(address(this));
if (shares > 0) {
LIDO.transferShares(msg.sender, shares);
}
}

Check warning

Code scanning / Slither

Unused return Medium

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

badge

Hardhat Unit Tests Coverage Summary

Details
Filename                                                                Stmts    Miss  Cover    Missing
--------------------------------------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------
contracts/0.4.24/Lido.sol                                                 347      38  89.05%   679, 744-784, 799-822, 1177-1196, 1230-1232, 1273-1275, 1311-1358
contracts/0.4.24/StETH.sol                                                 80       0  100.00%
contracts/0.4.24/StETHPermit.sol                                           15       0  100.00%
contracts/0.4.24/lib/Packed64x4.sol                                         5       0  100.00%
contracts/0.4.24/lib/SigningKeys.sol                                       36       0  100.00%
contracts/0.4.24/lib/StakeLimitUtils.sol                                   41       0  100.00%
contracts/0.4.24/nos/NodeOperatorsRegistry.sol                            435       0  100.00%
contracts/0.4.24/utils/Pausable.sol                                         9       0  100.00%
contracts/0.4.24/utils/UnstructuredStorageExt.sol                          14       0  100.00%
contracts/0.4.24/utils/Versioned.sol                                        5       0  100.00%
contracts/0.6.12/WstETH.sol                                                17       0  100.00%
contracts/0.8.25/CLValidatorVerifier.sol                                   34       1  97.06%   92
contracts/0.8.25/TopUpGateway.sol                                          98       2  97.96%   233, 281
contracts/0.8.25/ValidatorExitDelayVerifier.sol                            75       0  100.00%
contracts/0.8.25/consolidation/ConsolidationBus.sol                        75       0  100.00%
contracts/0.8.25/consolidation/ConsolidationGateway.sol                    75       0  100.00%
contracts/0.8.25/consolidation/ConsolidationMigrator.sol                   65       0  100.00%
contracts/0.8.25/lib/BeaconChainDepositor.sol                              40       4  90.00%   44, 47, 82, 97
contracts/0.8.25/sr/ISRBase.sol                                             0       0  100.00%
contracts/0.8.25/sr/SRLib.sol                                             289      16  94.46%   57, 95-144, 312
contracts/0.8.25/sr/SRStorage.sol                                          13       0  100.00%
contracts/0.8.25/sr/SRTypes.sol                                             0       0  100.00%
contracts/0.8.25/sr/SRUtils.sol                                            13       1  92.31%   87
contracts/0.8.25/sr/StakingRouter.sol                                     263      14  94.68%   70, 374-383, 628-629, 651, 707, 711, 759, 857-862, 1133
contracts/0.8.25/utils/AccessControlConfirmable.sol                         2       0  100.00%
contracts/0.8.25/utils/Confirmable2Addresses.sol                            5       0  100.00%
contracts/0.8.25/utils/Confirmations.sol                                   37       0  100.00%
contracts/0.8.25/utils/PausableUntilWithRoles.sol                           3       0  100.00%
contracts/0.8.25/vaults/LazyOracle.sol                                    134      18  86.57%   203-209, 248, 276-279, 436, 449, 467, 515, 556-558, 650, 658
contracts/0.8.25/vaults/OperatorGrid.sol                                  196       1  99.49%   203
contracts/0.8.25/vaults/PinnedBeaconProxy.sol                               6       0  100.00%
contracts/0.8.25/vaults/StakingVault.sol                                  111      14  87.39%   307-341
contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol                 48       3  93.75%   183, 187, 199
contracts/0.8.25/vaults/VaultFactory.sol                                   34       0  100.00%
contracts/0.8.25/vaults/VaultHub.sol                                      425      76  82.12%   257-266, 281-287, 342-366, 383, 552-553, 595-688, 997-999, 1087-1091, 1147, 1202-1209, 1495-1496, 1511-1521
contracts/0.8.25/vaults/dashboard/Dashboard.sol                           137       8  94.16%   183-201, 327, 636-649
contracts/0.8.25/vaults/dashboard/NodeOperatorFee.sol                      70       0  100.00%
contracts/0.8.25/vaults/dashboard/Permissions.sol                          47       2  95.74%   321-330
contracts/0.8.25/vaults/interfaces/IPinnedBeaconProxy.sol                   0       0  100.00%
contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol                 0       0  100.00%
contracts/0.8.25/vaults/interfaces/IStakingVault.sol                        0       0  100.00%
contracts/0.8.25/vaults/interfaces/IVaultFactory.sol                        0       0  100.00%
contracts/0.8.25/vaults/lib/PinnedBeaconUtils.sol                           5       0  100.00%
contracts/0.8.25/vaults/lib/RecoverTokens.sol                               5       0  100.00%
contracts/0.8.25/vaults/lib/RefSlotCache.sol                               36       0  100.00%
contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol           16       0  100.00%
contracts/0.8.25/vaults/predeposit_guarantee/MeIfNobodyElse.sol             3       0  100.00%
contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol      213      12  94.37%   483-503, 532, 671, 678, 700
contracts/0.8.9/Accounting.sol                                            103       5  95.15%   388-389, 395, 450-451
contracts/0.8.9/Burner.sol                                                109      16  85.32%   310-316, 442-457, 494-501
contracts/0.8.9/DepositSecurityModule.sol                                 126       0  100.00%
contracts/0.8.9/EIP712StETH.sol                                            16       0  100.00%
contracts/0.8.9/LidoExecutionLayerRewardsVault.sol                         16       0  100.00%
contracts/0.8.9/LidoLocator.sol                                            28       0  100.00%
contracts/0.8.9/OracleDaemonConfig.sol                                     28       0  100.00%
contracts/0.8.9/RedeemsBuffer.sol                                          42      42  0.00%    79-188
contracts/0.8.9/TokenRateNotifier.sol                                      36      36  0.00%    35-130
contracts/0.8.9/TriggerableWithdrawalsGateway.sol                          54       1  98.15%   271
contracts/0.8.9/WithdrawalQueue.sol                                        88       0  100.00%
contracts/0.8.9/WithdrawalQueueBase.sol                                   146       0  100.00%
contracts/0.8.9/WithdrawalQueueERC721.sol                                  89       0  100.00%
contracts/0.8.9/WithdrawalVault.sol                                        37       0  100.00%
contracts/0.8.9/WithdrawalVaultEIP7685.sol                                 45       0  100.00%
contracts/0.8.9/lib/ExitLimitUtils.sol                                     35       0  100.00%
contracts/0.8.9/lib/Math.sol                                                4       0  100.00%
contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol                         22       0  100.00%
contracts/0.8.9/lib/UnstructuredRefStorage.sol                              2       0  100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                               197       3  98.48%   441-442, 616
contracts/0.8.9/oracle/BaseOracle.sol                                      89       1  98.88%   401
contracts/0.8.9/oracle/HashConsensus.sol                                  263       1  99.62%   1005
contracts/0.8.9/oracle/ValidatorsExitBus.sol                              240       0  100.00%
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol                         57       1  98.25%   217
contracts/0.8.9/proxy/OssifiableProxy.sol                                  17       0  100.00%
contracts/0.8.9/proxy/WithdrawalsManagerProxy.sol                          60       0  100.00%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol               377       2  99.47%   1292, 1304
contracts/0.8.9/utils/DummyEmptyContract.sol                                0       0  100.00%
contracts/0.8.9/utils/PausableUntil.sol                                    31       0  100.00%
contracts/0.8.9/utils/Versioned.sol                                        11       0  100.00%
contracts/0.8.9/utils/access/AccessControl.sol                             23       0  100.00%
contracts/0.8.9/utils/access/AccessControlEnumerable.sol                    9       0  100.00%
contracts/common/utils/PausableUntil.sol                                   29       0  100.00%
contracts/tooling/AlertingHarness.sol                                      54       1  98.15%   97
contracts/tooling/sepolia/SepoliaDepositAdapter.sol                        21      21  0.00%    55-106
TOTAL                                                                    6081     340  94.41%

Diff against master

Filename                                                            Stmts    Miss  Cover
----------------------------------------------------------------  -------  ------  --------
contracts/0.4.24/Lido.sol                                             +66     +27  -7.04%
contracts/0.8.25/CLValidatorVerifier.sol                              +34      +1  +97.06%
contracts/0.8.25/TopUpGateway.sol                                     +98      +2  +97.96%
contracts/0.8.25/consolidation/ConsolidationBus.sol                   +75       0  +100.00%
contracts/0.8.25/consolidation/ConsolidationGateway.sol               +75       0  +100.00%
contracts/0.8.25/consolidation/ConsolidationMigrator.sol              +65       0  +100.00%
contracts/0.8.25/lib/BeaconChainDepositor.sol                         +40      +4  +90.00%
contracts/0.8.25/sr/ISRBase.sol                                         0       0  +100.00%
contracts/0.8.25/sr/SRLib.sol                                        +289     +16  +94.46%
contracts/0.8.25/sr/SRStorage.sol                                     +13       0  +100.00%
contracts/0.8.25/sr/SRTypes.sol                                         0       0  +100.00%
contracts/0.8.25/sr/SRUtils.sol                                       +13      +1  +92.31%
contracts/0.8.25/sr/StakingRouter.sol                                +263     +14  +94.68%
contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol        0      -1  +6.25%
contracts/0.8.9/Accounting.sol                                         +7      +3  -2.77%
contracts/0.8.9/Burner.sol                                            +17     +16  -14.68%
contracts/0.8.9/DepositSecurityModule.sol                              -2       0  +100.00%
contracts/0.8.9/LidoLocator.sol                                        +2       0  +100.00%
contracts/0.8.9/RedeemsBuffer.sol                                     +42     +42  +100.00%
contracts/0.8.9/WithdrawalVault.sol                                    +5       0  +100.00%
contracts/0.8.9/WithdrawalVaultEIP7685.sol                            +45       0  +100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                           +23      +3  -1.52%
contracts/0.8.9/oracle/ValidatorsExitBus.sol                         +102     -10  +7.25%
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol                     +5       0  +0.17%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol          +145     -10  +4.64%
TOTAL                                                               +1422    +108  -0.92%

Results for commit: 13ae3e5

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants