@@ -34,9 +34,14 @@ interface IStakingRouter {
3434interface IWithdrawalQueue {
3535 function unfinalizedStETH () external view returns (uint256 );
3636 function isBunkerModeActive () external view returns (bool );
37+ function isPaused () external view returns (bool );
3738 function finalize (uint256 _lastIdToFinalize , uint256 _maxShareRate ) external payable ;
3839}
3940
41+ interface IRedeemsReserveVault {
42+ function withdrawToLido (uint256 _amount ) external ;
43+ }
44+
4045interface ILidoExecutionLayerRewardsVault {
4146 function withdrawRewards (uint256 _maxAmount ) external returns (uint256 amount );
4247}
@@ -170,6 +175,34 @@ contract Lido is Versioned, StETHPermit, AragonApp {
170175 bytes32 internal constant DEPOSITS_RESERVE_TARGET_POSITION =
171176 0x3d3e9bd6e90e5d1f1c6839835bcbe5746a47c9a013d1eae6e80c248264c06a81 ;
172177
178+ /// @dev Storage slot for redeems reserve target ratio.
179+ /// Stores governance-configured ratio (in basis points) of internal ether.
180+ /// Set via `setRedeemsReserveTargetRatio()`, gated by `BUFFER_RESERVE_MANAGER_ROLE`
181+ /// keccak256("lido.Lido.redeemsReserveTargetRatio")
182+ bytes32 internal constant REDEEMS_RESERVE_TARGET_RATIO_POSITION =
183+ 0xa3ab8c45cc56567e890b52bdd4f310aacdc4a7b9a4384808e34fb3b77524a729 ;
184+
185+ /// @dev Storage slot for the RedeemsReserveVault contract address.
186+ /// The vault physically holds reserve ETH. Set via `setRedeemsReserveVault()`.
187+ /// keccak256("lido.Lido.redeemsReserveVault")
188+ bytes32 internal constant REDEEMS_RESERVE_VAULT_POSITION =
189+ 0x8f3a06db2c1a07e3a5e3b5e3bfc1e2b4c7a8f9d0e1c2b3a4f5e6d7c8b9a0f1e2 ;
190+
191+ /// @dev Storage slot for tracked ETH balance of the RedeemsReserveVault.
192+ /// Updated exclusively during oracle reports via `reconcileRedeemsReserveVault()`.
193+ /// Reading vault.balance directly would expose a donation attack vector.
194+ /// keccak256("lido.Lido.redeemsReserveVaultEth")
195+ bytes32 internal constant REDEEMS_RESERVE_VAULT_ETH_POSITION =
196+ 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2 ;
197+
198+ /// @dev Storage slot for redeems reserve replenishment share.
199+ /// Basis points determining how shared allocation (withdrawalsReserve + unreserved) is split
200+ /// between reserve growth and WQ finalization when surplus is insufficient.
201+ /// 0 (default) = reserve grows only from surplus. 8000 = 80% to reserve, 20% to WQ.
202+ /// keccak256("lido.Lido.redeemsReserveReplenishmentShare")
203+ bytes32 internal constant REDEEMS_RESERVE_REPLENISHMENT_SHARE_POSITION =
204+ 0xf0700d4e4a89999085a7b5d94e2e6697101ac2553a2f01512a848b9140605dfb ;
205+
173206 // Staking was paused (don't accept user's ether submits)
174207 event StakingPaused ();
175208 // Staking was resumed (accept user's ether submits)
@@ -258,6 +291,24 @@ contract Lido is Versioned, StETHPermit, AragonApp {
258291 // Emitted even if the new value equals the previous one
259292 event DepositsReserveTargetSet (uint256 depositsReserveTarget );
260293
294+ // Emitted when redeems reserve target ratio is set via `setRedeemsReserveTargetRatio()`
295+ event RedeemsReserveTargetRatioSet (uint256 ratioBP );
296+
297+ // Emitted when redeems reserve replenishment share is set
298+ event RedeemsReserveReplenishmentShareSet (uint256 shareBP );
299+
300+ // Emitted when the RedeemsReserveVault address is set
301+ event RedeemsReserveVaultSet (address vault );
302+
303+ // Emitted when tracked RedeemsReserveVault ETH is reconciled to actual balance on oracle report
304+ event RedeemsReserveVaultReconciled (uint256 actualBalance );
305+
306+ // Emitted when ETH is pushed from buffer to RedeemsReserveVault
307+ event RedeemsReserveVaultFunded (uint256 amount );
308+
309+ // Emitted when ETH is returned from RedeemsReserveVault to buffer
310+ event RedeemsReserveVaultDrained (uint256 amount );
311+
261312 /**
262313 * @notice Initializer function for scratch deploy of Lido contract
263314 *
@@ -558,6 +609,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
558609 */
559610 struct BufferedEtherAllocation {
560611 uint256 total;
612+ uint256 redeemsReserve;
561613 uint256 unreserved;
562614 uint256 depositsReserve;
563615 uint256 withdrawalsReserve;
@@ -571,6 +623,9 @@ contract Lido is Versioned, StETHPermit, AragonApp {
571623 * 2. withdrawalsReserve - covers unfinalized withdrawal requests
572624 * 3. unreserved - excess, available for additional CL deposits
573625 *
626+ * redeemsReserve is a view-only field: tracked RedeemsReserveVault ETH (physically outside buffer).
627+ * total = bufferedEther + tracked RedeemsReserveVault ETH.
628+ *
574629 * ┌─────────── Total Buffered Ether ───────────┐
575630 * ├────────────────────┬───────────────────────┼─────┬──────────────┐
576631 * │●●●●●●●●●●●●●●●●●●●●│●●●●●●●●●●●●●●●●●●●●●●●●○○○○○│○○○○○○○○○○○○○○│
@@ -586,8 +641,12 @@ contract Lido is Versioned, StETHPermit, AragonApp {
586641 * unreserved = total - depositsReserve - withdrawalsReserve
587642 */
588643 function _getBufferedEtherAllocation () internal view returns (BufferedEtherAllocation allocation ) {
589- uint256 remaining = _getBufferedEther ();
590- allocation.total = remaining;
644+ uint256 buffered = _getBufferedEther ();
645+ allocation.redeemsReserve = REDEEMS_RESERVE_VAULT_ETH_POSITION.getStorageUint256 ();
646+ allocation.total = buffered.add (allocation.redeemsReserve);
647+
648+ // Remaining layers allocated from bufferedEther only (RedeemsReserveVault ETH is physically separate)
649+ uint256 remaining = buffered;
591650
592651 allocation.depositsReserve = Math256.min (remaining, DEPOSITS_RESERVE_POSITION.getStorageUint256 ());
593652 remaining -= allocation.depositsReserve;
@@ -598,6 +657,14 @@ contract Lido is Versioned, StETHPermit, AragonApp {
598657 allocation.unreserved = remaining;
599658 }
600659
660+ /**
661+ * @notice Returns the current redeems reserve — tracked ETH balance of the RedeemsReserveVault
662+ * @dev RedeemsReserveVault ETH is physically outside the buffer. May be stale between reports.
663+ */
664+ function getRedeemsReserve () external view returns (uint256 ) {
665+ return _getBufferedEtherAllocation ().redeemsReserve;
666+ }
667+
601668 /**
602669 * @notice Returns the currently effective deposits reserve — buffer portion available for CL deposits, protected
603670 * from withdrawals demand
@@ -654,6 +721,139 @@ contract Lido is Versioned, StETHPermit, AragonApp {
654721 }
655722 }
656723
724+ /**
725+ * @notice Sets redeems reserve target ratio as basis points of internal ether.
726+ * The vault is replenished to this target on each oracle report.
727+ * @param _ratioBP Target ratio in basis points [0-10000]
728+ */
729+ function setRedeemsReserveTargetRatio (uint256 _ratioBP ) external {
730+ _auth (BUFFER_RESERVE_MANAGER_ROLE);
731+ require (_ratioBP <= TOTAL_BASIS_POINTS, "INVALID_RATIO " );
732+ REDEEMS_RESERVE_TARGET_RATIO_POSITION.setStorageUint256 (_ratioBP);
733+ emit RedeemsReserveTargetRatioSet (_ratioBP);
734+ }
735+
736+ /**
737+ * @return the redeems reserve target ratio in basis points
738+ */
739+ function getRedeemsReserveTargetRatio () external view returns (uint256 ) {
740+ return REDEEMS_RESERVE_TARGET_RATIO_POSITION.getStorageUint256 ();
741+ }
742+
743+ /**
744+ * @notice Sets the replenishment share for redeems reserve.
745+ * When unreserved surplus is insufficient to fill the reserve, this share (in BP)
746+ * determines what fraction of the shared allocation (withdrawalsReserve + unreserved)
747+ * goes to reserve growth vs WQ finalization.
748+ * 0 (default) = reserve grows only from genuine surplus. 10000 = 100% to reserve.
749+ * @param _shareBP Replenishment share in basis points [0-10000]
750+ */
751+ function setRedeemsReserveReplenishmentShare (uint256 _shareBP ) external {
752+ _auth (BUFFER_RESERVE_MANAGER_ROLE);
753+ require (_shareBP <= TOTAL_BASIS_POINTS, "INVALID_SHARE " );
754+ REDEEMS_RESERVE_REPLENISHMENT_SHARE_POSITION.setStorageUint256 (_shareBP);
755+ emit RedeemsReserveReplenishmentShareSet (_shareBP);
756+ }
757+
758+ /**
759+ * @return the redeems reserve replenishment share in basis points
760+ */
761+ function getRedeemsReserveReplenishmentShare () external view returns (uint256 ) {
762+ return REDEEMS_RESERVE_REPLENISHMENT_SHARE_POSITION.getStorageUint256 ();
763+ }
764+
765+ /**
766+ * @notice Returns the current redeems reserve target in absolute ETH,
767+ * computed from the ratio and current internal ether.
768+ */
769+ function getRedeemsReserveTarget () external view returns (uint256 ) {
770+ return _getRedeemsReserveTarget ();
771+ }
772+
773+ function _getRedeemsReserveTarget () internal view returns (uint256 ) {
774+ return _getInternalEther ()
775+ * REDEEMS_RESERVE_TARGET_RATIO_POSITION.getStorageUint256 ()
776+ / TOTAL_BASIS_POINTS;
777+ }
778+
779+ /**
780+ * @notice Sets the RedeemsReserveVault contract address
781+ * @dev Setting to address(0) disables reserve; vault won't receive ETH on reports
782+ * @param _vault Address of the RedeemsReserveVault contract
783+ */
784+ function setRedeemsReserveVault (address _vault ) external {
785+ _auth (BUFFER_RESERVE_MANAGER_ROLE);
786+ REDEEMS_RESERVE_VAULT_POSITION.setStorageAddress (_vault);
787+ emit RedeemsReserveVaultSet (_vault);
788+ }
789+
790+ /**
791+ * @return the RedeemsReserveVault address
792+ */
793+ function getRedeemsReserveVault () external view returns (address ) {
794+ return REDEEMS_RESERVE_VAULT_POSITION.getStorageAddress ();
795+ }
796+
797+ /**
798+ * @return the tracked ETH balance of the RedeemsReserveVault (snapshot, may be stale between reports)
799+ */
800+ function getRedeemsReserveVaultEth () external view returns (uint256 ) {
801+ return REDEEMS_RESERVE_VAULT_ETH_POSITION.getStorageUint256 ();
802+ }
803+
804+ /**
805+ * @notice Updates tracked RedeemsReserveVault ETH to actual balance. Called by Accounting on report.
806+ * @param _actualBalance The actual ETH balance of the RedeemsReserveVault
807+ */
808+ function reconcileRedeemsReserveVault (uint256 _actualBalance ) external {
809+ _auth (_accounting ());
810+ REDEEMS_RESERVE_VAULT_ETH_POSITION.setStorageUint256 (_actualBalance);
811+ emit RedeemsReserveVaultReconciled (_actualBalance);
812+ }
813+
814+ /**
815+ * @notice Pushes ETH from buffer to RedeemsReserveVault. Called by Accounting on report.
816+ * @param _amount Amount of ETH to push
817+ */
818+ function pushToRedeemsReserveVault (uint256 _amount ) external {
819+ _auth (_accounting ());
820+ address vault = REDEEMS_RESERVE_VAULT_POSITION.getStorageAddress ();
821+ require (vault != address (0 ), "VAULT_NOT_SET " );
822+
823+ _setBufferedEther (_getBufferedEther ().sub (_amount));
824+ REDEEMS_RESERVE_VAULT_ETH_POSITION.setStorageUint256 (
825+ REDEEMS_RESERVE_VAULT_ETH_POSITION.getStorageUint256 ().add (_amount)
826+ );
827+
828+ vault.transfer (_amount);
829+ emit RedeemsReserveVaultFunded (_amount);
830+ }
831+
832+ /**
833+ * @notice Pulls excess ETH from RedeemsReserveVault back to the buffer. Called by Accounting on report.
834+ * @param _amount Amount of ETH to pull from the vault
835+ */
836+ function pullFromRedeemsReserveVault (uint256 _amount ) external {
837+ _auth (_accounting ());
838+ address vault = REDEEMS_RESERVE_VAULT_POSITION.getStorageAddress ();
839+ require (vault != address (0 ), "VAULT_NOT_SET " );
840+ IRedeemsReserveVault (vault).withdrawToLido (_amount);
841+ }
842+
843+ /**
844+ * @notice Receives ETH back from RedeemsReserveVault (excess return on report).
845+ */
846+ function receiveRedeemsReserve () external payable {
847+ _auth (REDEEMS_RESERVE_VAULT_POSITION.getStorageAddress ());
848+
849+ _setBufferedEther (_getBufferedEther ().add (msg .value ));
850+ REDEEMS_RESERVE_VAULT_ETH_POSITION.setStorageUint256 (
851+ REDEEMS_RESERVE_VAULT_ETH_POSITION.getStorageUint256 ().sub (msg .value )
852+ );
853+
854+ emit RedeemsReserveVaultDrained (msg .value );
855+ }
856+
657857 /**
658858 * @return the amount of ether held by external sources to back external shares
659859 */
@@ -1048,12 +1248,12 @@ contract Lido is Versioned, StETHPermit, AragonApp {
10481248 }
10491249
10501250 /**
1051- * @dev Syncs stored deposits reserve to configured target after oracle report processing
1251+ * @dev Syncs stored deposits reserve to configured target after oracle report processing.
1252+ * Redeems reserve is managed externally via RedeemsReserveVault push/pull in Accounting.
10521253 */
10531254 function _updateBufferedEtherAllocation () internal {
10541255 uint256 depositsReserveTarget = getDepositsReserveTarget ();
10551256 uint256 depositsReserve = DEPOSITS_RESERVE_POSITION.getStorageUint256 ();
1056-
10571257 if (depositsReserve != depositsReserveTarget) {
10581258 _setDepositsReserve (depositsReserveTarget);
10591259 }
@@ -1195,14 +1395,18 @@ contract Lido is Versioned, StETHPermit, AragonApp {
11951395 }
11961396
11971397 /// @dev Get the total amount of ether controlled by the protocol internally
1198- /// (buffered ether + CL validators balance + CL pending balance + deposited since last report)
1398+ /// (buffered ether + CL validators balance + CL pending balance + deposited since last report + redeems reserve vault )
11991399 function _getInternalEther () internal view returns (uint256 ) {
12001400 (uint256 bufferedEther , uint256 depositedPostReport ) = _getBufferedEtherAndDepositedPostReport ();
12011401 (uint256 clValidatorsBalance , uint256 clPendingBalance ) = _getClValidatorsBalanceAndClPendingBalance ();
12021402
12031403 // With balance-based accounting, we don't need to calculate transientEther
12041404 // as pending deposits are already included in clPendingBalance
1205- return bufferedEther.add (clValidatorsBalance).add (clPendingBalance).add (depositedPostReport);
1405+ return bufferedEther
1406+ .add (clValidatorsBalance)
1407+ .add (clPendingBalance)
1408+ .add (depositedPostReport)
1409+ .add (REDEEMS_RESERVE_VAULT_ETH_POSITION.getStorageUint256 ());
12061410 }
12071411
12081412 /// @dev Calculate the amount of ether controlled by external entities
0 commit comments