From 6e3b6474e71ff46d8b1c4a9f1aa3af3aac2c0f5a Mon Sep 17 00:00:00 2001 From: Evgeny Taktarov Date: Mon, 10 Feb 2025 22:50:38 +0700 Subject: [PATCH] feat: integrate predeposit guarantee into locator --- contracts/0.8.25/Accounting.sol | 42 ++- contracts/0.8.25/vaults/StakingVault.sol | 18 +- contracts/0.8.25/vaults/VaultHub.sol | 38 ++- .../vaults/interfaces/IStakingVault.sol | 2 + .../predeposit_guarantee/CLProofVerifier.sol | 2 +- .../PredepositGuarantee.sol | 301 ++++++++++++------ contracts/0.8.9/LidoLocator.sol | 30 +- contracts/common/interfaces/ILidoLocator.sol | 1 + .../StakingVault__HarnessForTestUpgrade.sol | 8 +- .../LidoLocator__MockForSanityChecker.sol | 3 + .../contracts/LidoLocator__MockMutable.sol | 3 + 11 files changed, 282 insertions(+), 166 deletions(-) diff --git a/contracts/0.8.25/Accounting.sol b/contracts/0.8.25/Accounting.sol index f2bffbdc0..0a8a3aca1 100644 --- a/contracts/0.8.25/Accounting.sol +++ b/contracts/0.8.25/Accounting.sol @@ -83,16 +83,10 @@ contract Accounting is VaultHub { /// @notice deposit size in wei (for pre-maxEB accounting) uint256 private constant DEPOSIT_SIZE = 32 ether; - /// @notice Lido Locator contract - ILidoLocator public immutable LIDO_LOCATOR; /// @notice Lido contract ILido public immutable LIDO; - constructor( - ILidoLocator _lidoLocator, - ILido _lido - ) VaultHub(_lido) { - LIDO_LOCATOR = _lidoLocator; + constructor(ILidoLocator _lidoLocator, ILido _lido) VaultHub(_lidoLocator, _lido) { LIDO = _lido; } @@ -217,22 +211,27 @@ contract Accounting is VaultHub { update.withdrawals - update.principalClBalance + // total cl rewards (or penalty) update.elRewards + // ELRewards - postExternalEther - _pre.externalEther // vaults rebase - - update.etherToFinalizeWQ; // withdrawals + postExternalEther - + _pre.externalEther - // vaults rebase + update.etherToFinalizeWQ; // withdrawals // Calculate the amount of ether locked in the vaults to back external balance of stETH // and the amount of shares to mint as fees to the treasury for each vaults - (update.vaultsLockedEther, update.vaultsTreasuryFeeShares, update.totalVaultsTreasuryFeeShares) = - _calculateVaultsRebase( - update.postTotalShares, - update.postTotalPooledEther, - _pre.totalShares, - _pre.totalPooledEther, - update.sharesToMintAsFees - ); + ( + update.vaultsLockedEther, + update.vaultsTreasuryFeeShares, + update.totalVaultsTreasuryFeeShares + ) = _calculateVaultsRebase( + update.postTotalShares, + update.postTotalPooledEther, + _pre.totalShares, + _pre.totalPooledEther, + update.sharesToMintAsFees + ); update.postTotalPooledEther += - update.totalVaultsTreasuryFeeShares * update.postTotalPooledEther / update.postTotalShares; + (update.totalVaultsTreasuryFeeShares * update.postTotalPooledEther) / + update.postTotalShares; update.postTotalShares += update.totalVaultsTreasuryFeeShares; } @@ -305,12 +304,7 @@ contract Accounting is VaultHub { ]; } - LIDO.processClStateUpdate( - _report.timestamp, - _pre.clValidators, - _report.clValidators, - _report.clBalance - ); + LIDO.processClStateUpdate(_report.timestamp, _pre.clValidators, _report.clValidators, _report.clBalance); if (_update.totalSharesToBurn > 0) { _contracts.burner.commitSharesToBurn(_update.totalSharesToBurn); diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 2044cd4de..253df0575 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -79,8 +79,6 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { */ uint64 private constant _VERSION = 1; - bytes32 public constant DEPOSIT_GUARDIAN_MESSAGE_PREFIX = keccak256("StakingVault.DepositGuardianMessagePrefix"); - /** * @notice Address of `VaultHub` * Set immutably in the constructor to avoid storage costs @@ -254,8 +252,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { /** * @notice Returns the address of the node operator * Node operator is the party responsible for managing the validators. - * In the context of this contract, the node operator performs deposits to the beacon chain - * and processes validator exit requests submitted by `owner` through `requestValidatorExit()`. + * In the context of this contract, the node operator runs vault validators on CL and + * processes validator exit requests submitted by `owner` through `requestValidatorExit()`. * Node operator address is set in the initialization and can never be changed. * @return Address of the node operator */ @@ -263,6 +261,18 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { return _getStorage().nodeOperator; } + /** + * @notice Returns the address of the deposit guardian + * Trusted party responsible for securely depositing validators to the beacon chain. + * In the context of this contract, the deposit guardian performs deposits through `depositToBeaconChain()`. + * DepositGuardian address is set in the initialization and can be changed by the owner with `setDepositGuardian` + * only on the condition that the vault is not connected to the VaultHub. + * @return Address of the deposit guardian + */ + function depositGuardian() external view returns (address) { + return _getStorage().depositGuardian; + } + /** * @notice Returns the 0x01-type withdrawal credentials for the validators deposited from this `StakingVault` * All CL rewards are sent to this contract. Only 0x01-type withdrawal credentials are supported for now. diff --git a/contracts/0.8.25/vaults/VaultHub.sol b/contracts/0.8.25/vaults/VaultHub.sol index 3c8d10b47..7d0ce758a 100644 --- a/contracts/0.8.25/vaults/VaultHub.sol +++ b/contracts/0.8.25/vaults/VaultHub.sol @@ -8,6 +8,7 @@ import {IBeacon} from "@openzeppelin/contracts-v5.2/proxy/beacon/IBeacon.sol"; import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {ILido as IStETH} from "../interfaces/ILido.sol"; import {PausableUntilWithRoles} from "../utils/PausableUntilWithRoles.sol"; @@ -72,11 +73,13 @@ abstract contract VaultHub is PausableUntilWithRoles { /// @notice Lido stETH contract IStETH public immutable STETH; + /// @notice Lido Locator contract + ILidoLocator public immutable LIDO_LOCATOR; /// @param _stETH Lido stETH contract - constructor(IStETH _stETH) { + constructor(ILidoLocator _locator, IStETH _stETH) { STETH = _stETH; - + LIDO_LOCATOR = _locator; _disableInitializers(); } @@ -140,9 +143,11 @@ abstract contract VaultHub is PausableUntilWithRoles { ) external onlyRole(VAULT_MASTER_ROLE) { if (_vault == address(0)) revert ZeroArgument("_vault"); if (_reserveRatioBP == 0) revert ZeroArgument("_reserveRatioBP"); - if (_reserveRatioBP > TOTAL_BASIS_POINTS) revert ReserveRatioTooHigh(_vault, _reserveRatioBP, TOTAL_BASIS_POINTS); + if (_reserveRatioBP > TOTAL_BASIS_POINTS) + revert ReserveRatioTooHigh(_vault, _reserveRatioBP, TOTAL_BASIS_POINTS); if (_reserveRatioThresholdBP == 0) revert ZeroArgument("_reserveRatioThresholdBP"); - if (_reserveRatioThresholdBP > _reserveRatioBP) revert ReserveRatioTooHigh(_vault, _reserveRatioThresholdBP, _reserveRatioBP); + if (_reserveRatioThresholdBP > _reserveRatioBP) + revert ReserveRatioTooHigh(_vault, _reserveRatioThresholdBP, _reserveRatioBP); if (_treasuryFeeBP > TOTAL_BASIS_POINTS) revert TreasuryFeeTooHigh(_vault, _treasuryFeeBP, TOTAL_BASIS_POINTS); if (vaultsCount() == MAX_VAULTS_COUNT) revert TooManyVaults(); _checkShareLimitUpperBound(_vault, _shareLimit); @@ -153,6 +158,9 @@ abstract contract VaultHub is PausableUntilWithRoles { bytes32 vaultProxyCodehash = address(_vault).codehash; if (!$.vaultProxyCodehash[vaultProxyCodehash]) revert VaultProxyNotAllowed(_vault); + if (IStakingVault(_vault).depositGuardian() != LIDO_LOCATOR.predepositGuarantee()) + revert VaultDepositGuardianNotAllowed(IStakingVault(_vault).depositGuardian()); + VaultSocket memory vr = VaultSocket( _vault, 0, // sharesMinted @@ -308,8 +316,10 @@ abstract contract VaultHub is PausableUntilWithRoles { // reserveRatio = BPS_BASE - maxMintableRatio // X = (mintedStETH * BPS_BASE - vault.valuation() * maxMintableRatio) / reserveRatio - uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS - - IStakingVault(_vault).valuation() * maxMintableRatio) / reserveRatioBP; + uint256 amountToRebalance = (mintedStETH * + TOTAL_BASIS_POINTS - + IStakingVault(_vault).valuation() * + maxMintableRatio) / reserveRatioBP; // TODO: add some gas compensation here IStakingVault(_vault).rebalance(amountToRebalance); @@ -356,7 +366,11 @@ abstract contract VaultHub is PausableUntilWithRoles { uint256 _preTotalShares, uint256 _preTotalPooledEther, uint256 _sharesToMintAsFees - ) internal view returns (uint256[] memory lockedEther, uint256[] memory treasuryFeeShares, uint256 totalTreasuryFeeShares) { + ) + internal + view + returns (uint256[] memory lockedEther, uint256[] memory treasuryFeeShares, uint256 totalTreasuryFeeShares) + { /// HERE WILL BE ACCOUNTING DRAGON // \||/ @@ -425,7 +439,8 @@ abstract contract VaultHub is PausableUntilWithRoles { // TODO: optimize potential rewards calculation uint256 potentialRewards = ((chargeableValue * (_postTotalPooledEther * _preTotalShares)) / - (_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue); + (_postTotalSharesNoFees * _preTotalPooledEther) - + chargeableValue); uint256 treasuryFee = (potentialRewards * _socket.treasuryFeeBP) / TOTAL_BASIS_POINTS; treasuryFeeShares = (treasuryFee * _preTotalShares) / _preTotalPooledEther; @@ -480,7 +495,11 @@ abstract contract VaultHub is PausableUntilWithRoles { /// @dev returns total number of stETH shares that is possible to mint on the provided vault with provided reserveRatio /// it does not count shares that is already minted, but does count shareLimit on the vault - function _maxMintableShares(address _vault, uint256 _reserveRatio, uint256 _shareLimit) internal view returns (uint256) { + function _maxMintableShares( + address _vault, + uint256 _reserveRatio, + uint256 _shareLimit + ) internal view returns (uint256) { uint256 maxStETHMinted = (IStakingVault(_vault).valuation() * (TOTAL_BASIS_POINTS - _reserveRatio)) / TOTAL_BASIS_POINTS; @@ -529,4 +548,5 @@ abstract contract VaultHub is PausableUntilWithRoles { error AlreadyExists(bytes32 codehash); error NoMintedSharesShouldBeLeft(address vault, uint256 sharesMinted); error VaultProxyNotAllowed(address beacon); + error VaultDepositGuardianNotAllowed(address depositGuardian); } diff --git a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol index 7e3d8e2df..6e320971d 100644 --- a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol +++ b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol @@ -39,6 +39,8 @@ interface IStakingVault { function nodeOperator() external view returns (address); + function depositGuardian() external view returns (address); + function locked() external view returns (uint256); function valuation() external view returns (uint256); diff --git a/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol b/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol index 4f0022632..63630795a 100644 --- a/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol +++ b/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol @@ -31,7 +31,7 @@ abstract contract CLProofVerifier { } // See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788. - address public immutable BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; // Index of parent node for (Pubkey,WC) in validator container GIndex public immutable GI_PUBKEY_WC_PARENT = pack(1 << 2, 2); diff --git a/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol b/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol index ac411cb47..bb1769d53 100644 --- a/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol +++ b/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol @@ -4,11 +4,19 @@ // See contracts/COMPILERS.md pragma solidity 0.8.25; -import {CLProofVerifier, GIndex} from "./CLProofVerifier.sol"; +import {GIndex} from "contracts/0.8.25/lib/GIndex.sol"; +import {PausableUntilWithRoles} from "contracts/0.8.25/utils/PausableUntilWithRoles.sol"; + +import {CLProofVerifier} from "./CLProofVerifier.sol"; import {IStakingVaultOwnable} from "../interfaces/IStakingVault.sol"; -contract PredepositGuarantee is CLProofVerifier { +/** + * @title PredepositGuarantee + * @author Lido + * @notice + */ +contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles { uint128 public constant PREDEPOSIT_AMOUNT = 1 ether; enum BondStatus { @@ -30,82 +38,111 @@ contract PredepositGuarantee is CLProofVerifier { address nodeOperator; } - // Events - event NodeOperatorBondToppedUp(address indexed nodeOperator, uint256 amount); - event NodeOperatorBondWithdrawn(address indexed nodeOperator, uint256 amount, address indexed recipient); - event NodeOperatorVoucherSet(address indexed nodeOperator, address indexed voucher); - event ValidatorPreDeposited(address indexed nodeOperator, address indexed stakingVault, uint256 numberOfDeposits, uint256 totalDepositAmount); - event ValidatorProven(address indexed nodeOperator, bytes indexed validatorPubkey, address indexed stakingVault, bytes32 withdrawalCredentials); - event ValidatorDisproven(address indexed nodeOperator, bytes indexed validatorPubkey, address indexed stakingVault, bytes32 withdrawalCredentials); - event ValidatorDisprovenWithdrawn(address indexed nodeOperator, bytes indexed validatorPubkey, address indexed stakingVault, address recipient); + /** + * @notice ERC-7201 storage namespace for the vault + * @dev ERC-7201 namespace is used to prevent upgrade collisions + * @custom: TODO + */ + struct ERC7201Storage { + mapping(address nodeOperator => NodeOperatorBond bond) nodeOperatorBonds; + mapping(address nodeOperator => address voucher) nodeOperatorVouchers; + mapping(bytes validatorPubkey => ValidatorStatus validatorStatus) validatorStatuses; + } + + /** + * @notice Storage offset slot for ERC-7201 namespace + * The storage namespace is used to prevent upgrade collisions + * `keccak256(abi.encode(uint256(keccak256("Lido.Vaults.PredepositGuarantee")) - 1)) & ~bytes32(uint256(0xff))` + */ + bytes32 private constant ERC721_STORAGE_LOCATION = + 0xf66b5a365356c5798cc70e3ea6a236b181a826a69f730fc07cc548244bee5200; + + constructor(GIndex _gIFirstValidator) CLProofVerifier(_gIFirstValidator) { + _disableInitializers(); + } - constructor(GIndex _gIFirstValidator) CLProofVerifier(_gIFirstValidator) {} + function initialize(address _admin) external initializer { + if (_admin == address(0)) revert ZeroArgument("_admin"); - mapping(address nodeOperator => NodeOperatorBond bond) public nodeOperatorBonds; - mapping(address nodeOperator => address voucher) public nodeOperatorVoucher; + __AccessControlEnumerable_init(); - mapping(bytes validatorPubkey => ValidatorStatus validatorStatus) public validatorStatuses; + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } - // View functions + // * * * * * * * * * * * * * * * * * * * * // + // * * * Node Operator Balance Logic * * * // + // * * * * * * * * * * * * * * * * * * * * // function nodeOperatorBond(address _nodeOperator) external view returns (NodeOperatorBond memory) { - return nodeOperatorBonds[_nodeOperator]; + return _getStorage().nodeOperatorBonds[_nodeOperator]; } - function nodeOperatorVoucherAddress(address _nodeOperator) external view returns (address) { - return nodeOperatorVoucher[_nodeOperator]; + function nodeOperatorVoucher(address _nodeOperator) external view returns (address) { + return _getStorage().nodeOperatorVouchers[_nodeOperator]; } function validatorStatus(bytes calldata _validatorPubkey) external view returns (ValidatorStatus memory) { - return validatorStatuses[_validatorPubkey]; + return _getStorage().validatorStatuses[_validatorPubkey]; } /// NO Balance operations function topUpNodeOperatorBond(address _nodeOperator) external payable { - _topUpNodeOperatorCollateral(_nodeOperator); + _topUpNodeOperatorBalance(_nodeOperator); } function withdrawNodeOperatorBond(address _nodeOperator, uint128 _amount, address _recipient) external { if (_amount == 0) revert ZeroArgument("amount"); if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); + ERC7201Storage storage $ = _getStorage(); + _validateNodeOperatorCaller(_nodeOperator); - uint256 unlockedCollateral = nodeOperatorBonds[_nodeOperator].total - nodeOperatorBonds[_nodeOperator].locked; + uint256 unlocked = $.nodeOperatorBonds[_nodeOperator].total - $.nodeOperatorBonds[_nodeOperator].locked; - if (unlockedCollateral < _amount) - revert NotEnoughUnlockedCollateralToWithdraw(unlockedCollateral, _amount); + if (unlocked < _amount) revert NotEnoughUnlockedBondToWithdraw(unlocked, _amount); - nodeOperatorBonds[_nodeOperator].total -= _amount; + $.nodeOperatorBonds[_nodeOperator].total -= _amount; (bool success, ) = _recipient.call{value: uint256(_amount)}(""); if (!success) revert WithdrawalFailed(); emit NodeOperatorBondWithdrawn(_nodeOperator, _amount, _recipient); } - function setNodeOperatorVoucher(address _voucher) external { - NodeOperatorBond storage bond = nodeOperatorBonds[msg.sender]; + function setNodeOperatorVoucher(address _voucher) external payable { + ERC7201Storage storage $ = _getStorage(); + + NodeOperatorBond storage bond = $.nodeOperatorBonds[msg.sender]; if (_voucher == msg.sender) revert CannotSetSelfAsVoucher(); - if (bond.locked != 0) revert BondMustBeFullyUnlocked(); + if (bond.locked != 0) revert BondMustBeFullyUnlocked(bond.locked); - if (bond.total > 0 && nodeOperatorVoucher[msg.sender] != address(0)) { - uint256 ejected = nodeOperatorBonds[msg.sender].total; - nodeOperatorBonds[msg.sender].total = 0; - (bool success, ) = nodeOperatorVoucher[msg.sender].call{value: ejected}(""); + if (bond.total > 0 && $.nodeOperatorVouchers[msg.sender] != address(0)) { + uint256 _ejected = $.nodeOperatorBonds[msg.sender].total; + $.nodeOperatorBonds[msg.sender].total = 0; + (bool success, ) = $.nodeOperatorVouchers[msg.sender].call{value: _ejected}(""); // voucher can block change? if (!success) revert WithdrawalFailed(); + + emit NodeOperatorBondWithdrawn(msg.sender, _ejected, _voucher); } - nodeOperatorVoucher[msg.sender] = _voucher; + // optional top up that will only work in NO sets voucher to zero address + if (msg.value != 0) { + _topUpNodeOperatorBalance(msg.sender); + } + + $.nodeOperatorVouchers[msg.sender] = _voucher; emit NodeOperatorVoucherSet(msg.sender, _voucher); } - /// Deposit operations + // * * * * * * * * * * * * * * * * * * * * // + // * * * * * Deposit Operations * * * * * // + // * * * * * * * * * * * * * * * * * * * * // function predeposit( IStakingVaultOwnable _stakingVault, @@ -118,69 +155,77 @@ contract PredepositGuarantee is CLProofVerifier { // optional top up if (msg.value != 0) { - _topUpNodeOperatorCollateral(_nodeOperator); + _topUpNodeOperatorBalance(_nodeOperator); } // ensures vault fair play if (address(_stakingVault) != _wcToAddress(_stakingVault.withdrawalCredentials())) { - revert stakingVaultWithdrawalCredentialsMismatch(); + revert StakingVaultWithdrawalCredentialsMismatch( + address(_stakingVault), + _wcToAddress(_stakingVault.withdrawalCredentials()) + ); } + ERC7201Storage storage $ = _getStorage(); + uint128 totalDepositAmount = PREDEPOSIT_AMOUNT * uint128(_deposits.length); - uint256 unlockedCollateral = nodeOperatorBonds[_nodeOperator].total - nodeOperatorBonds[_nodeOperator].locked; + uint256 unlocked = $.nodeOperatorBonds[_nodeOperator].total - $.nodeOperatorBonds[_nodeOperator].locked; - if (unlockedCollateral < totalDepositAmount) - revert NotEnoughUnlockedCollateralToPredeposit(unlockedCollateral, totalDepositAmount); + if (unlocked < totalDepositAmount) + revert NotEnoughUnlockedUnlockedBondToPredeposit(unlocked, totalDepositAmount); for (uint256 i = 0; i < _deposits.length; i++) { IStakingVaultOwnable.Deposit calldata _deposit = _deposits[i]; - if (validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.NO_RECORD) { - revert MustBeNewValidatorPubkey(_deposit.pubkey, validatorStatuses[_deposit.pubkey].bondStatus); + if ($.validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.NO_RECORD) { + revert MustBeNewValidatorPubkey(_deposit.pubkey, $.validatorStatuses[_deposit.pubkey].bondStatus); } // cannot predeposit a validator with a deposit amount that is not 1 ether - if (_deposit.amount != PREDEPOSIT_AMOUNT) revert PredepositDepositAmountInvalid(_deposit.pubkey, _deposit.amount); + if (_deposit.amount != PREDEPOSIT_AMOUNT) + revert PredepositDepositAmountInvalid(_deposit.pubkey, _deposit.amount); - validatorStatuses[_deposit.pubkey] = ValidatorStatus({ + $.validatorStatuses[_deposit.pubkey] = ValidatorStatus({ bondStatus: BondStatus.AWAITING_PROOF, stakingVault: _stakingVault, nodeOperator: _nodeOperator }); } - nodeOperatorBonds[_nodeOperator].locked += totalDepositAmount; + $.nodeOperatorBonds[_nodeOperator].locked += totalDepositAmount; _stakingVault.depositToBeaconChain(_deposits); - emit ValidatorPreDeposited(_nodeOperator, address(_stakingVault), _deposits.length, totalDepositAmount); + emit ValidatorPreDeposited(_nodeOperator, address(_stakingVault), _deposits.length); } - /* - * - * POSITIVE PROOF METHODS - * - */ + // * * * * * Positive Proof Flow * * * * * // function proveValidatorWC(ValidatorWitness calldata _witness) external { - _processWitnessProof(_witness); + _processWCProof(_witness); } - function depositToProvenValidators( + function depositToBeaconChain( IStakingVaultOwnable _stakingVault, IStakingVaultOwnable.Deposit[] calldata _deposits - ) public { + ) public payable { if (msg.sender != _stakingVault.nodeOperator()) { revert MustBeNodeOperator(); } + if (msg.value != 0) { + _topUpNodeOperatorBalance(_stakingVault.nodeOperator()); + } + + ERC7201Storage storage $ = _getStorage(); + for (uint256 i = 0; i < _deposits.length; i++) { IStakingVaultOwnable.Deposit calldata _deposit = _deposits[i]; - if (validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.PROVED) { - revert DepositToUnprovenValidator(_deposit.pubkey, validatorStatuses[_deposit.pubkey].bondStatus); + if ($.validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.PROVED) { + revert DepositToUnprovenValidator(_deposit.pubkey, $.validatorStatuses[_deposit.pubkey].bondStatus); } - if (validatorStatuses[_deposit.pubkey].stakingVault != _stakingVault) { + if ($.validatorStatuses[_deposit.pubkey].stakingVault != _stakingVault) { revert DepositToWrongVault(_deposit.pubkey, address(_stakingVault)); } } @@ -190,8 +235,8 @@ contract PredepositGuarantee is CLProofVerifier { /** @notice happy path shortcut for the node operator that allows: - - prove validators to free up collateral - - optionally top up collateral + - prove validators to free up bond + - optionally top up NO bond - trigger deposit to proven validators via vault NB! proven and deposited validators sets don't have to match */ function proveAndDeposit( @@ -200,60 +245,68 @@ contract PredepositGuarantee is CLProofVerifier { IStakingVaultOwnable _stakingVault ) external payable { for (uint256 i = 0; i < _witnesses.length; i++) { - _processWitnessProof(_witnesses[i]); + _processWCProof(_witnesses[i]); } - depositToProvenValidators(_stakingVault, _deposits); + depositToBeaconChain(_stakingVault, _deposits); } - /* - * - * NEGATIVE PROOF METHODS - * - */ + // * * * * * Negative Proof Flow * * * * * // function proveInvalidValidatorWC(ValidatorWitness calldata _witness, bytes32 _invalidWithdrawalCredentials) public { - ValidatorStatus storage validatorStatus = validatorStatuses[_witness.pubkey]; + ERC7201Storage storage $ = _getStorage(); + + ValidatorStatus storage validator = $.validatorStatuses[_witness.pubkey]; - if (validatorStatus.bondStatus != BondStatus.AWAITING_PROOF) { - revert ValidatorNotPreDeposited(_witness.pubkey, validatorStatus.bondStatus); + if (validator.bondStatus != BondStatus.AWAITING_PROOF) { + revert ValidatorNotPreDeposited(_witness.pubkey, validator.bondStatus); } - if (address(validatorStatus.stakingVault) == _wcToAddress(_invalidWithdrawalCredentials)) { + if (address(validator.stakingVault) == _wcToAddress(_invalidWithdrawalCredentials)) { revert WithdrawalCredentialsAreValid(); } _validatePubKeyWCProof(_witness, _invalidWithdrawalCredentials); // reduces total&locked NO deposit - nodeOperatorBonds[validatorStatus.nodeOperator].total -= PREDEPOSIT_AMOUNT; - nodeOperatorBonds[validatorStatus.nodeOperator].locked -= PREDEPOSIT_AMOUNT; + $.nodeOperatorBonds[validator.nodeOperator].total -= PREDEPOSIT_AMOUNT; + $.nodeOperatorBonds[validator.nodeOperator].locked -= PREDEPOSIT_AMOUNT; // freed ether only will returned to owner of the vault with this validator - validatorStatus.bondStatus = BondStatus.PROVED_INVALID; - - emit ValidatorDisproven(validatorStatus.nodeOperator, _witness.pubkey, address(validatorStatus.stakingVault), _invalidWithdrawalCredentials); + validator.bondStatus = BondStatus.PROVED_INVALID; + + emit ValidatorDisproven( + validator.nodeOperator, + _witness.pubkey, + address(validator.stakingVault), + _invalidWithdrawalCredentials + ); } // called by the staking vault owner if the predeposited validator was proven invalid // i.e. node operator was malicious and has stolen vault ether function withdrawDisprovenPredeposit(bytes calldata validatorPubkey, address _recipient) public { - ValidatorStatus storage validatorStatus = validatorStatuses[validatorPubkey]; + ValidatorStatus storage validator = _getStorage().validatorStatuses[validatorPubkey]; if (_recipient == address(0)) revert ZeroArgument("_recipient"); - if (_recipient == address(validatorStatus.stakingVault)) revert WithdrawToVaultNotAllowed(); + if (_recipient == address(validator.stakingVault)) revert WithdrawToVaultNotAllowed(); - if (msg.sender != validatorStatus.stakingVault.owner()) revert WithdrawSenderNotStakingVaultOwner(); + if (msg.sender != validator.stakingVault.owner()) revert WithdrawSenderNotStakingVaultOwner(); - if (validatorStatus.bondStatus != BondStatus.PROVED_INVALID) revert ValidatorNotProvenInvalid(validatorStatus.bondStatus); + if (validator.bondStatus != BondStatus.PROVED_INVALID) revert ValidatorNotProvenInvalid(validator.bondStatus); - validatorStatus.bondStatus = BondStatus.WITHDRAWN; + validator.bondStatus = BondStatus.WITHDRAWN; (bool success, ) = _recipient.call{value: PREDEPOSIT_AMOUNT}(""); if (!success) revert WithdrawalFailed(); - emit ValidatorDisprovenWithdrawn(validatorStatus.nodeOperator, validatorPubkey, address(validatorStatus.stakingVault), _recipient); + emit ValidatorDisprovenWithdrawn( + validator.nodeOperator, + validatorPubkey, + address(validator.stakingVault), + _recipient + ); } function disproveAndWithdraw( @@ -267,66 +320,107 @@ contract PredepositGuarantee is CLProofVerifier { /// Internal functions - function _validateNodeOperatorCaller(address _nodeOperator) internal view { - if (nodeOperatorVoucher[_nodeOperator] == msg.sender) return; - if (nodeOperatorVoucher[_nodeOperator] == address(0) && msg.sender == _nodeOperator) return; - revert MustBeNodeOperatorOrVoucher(); - } - - function _topUpNodeOperatorCollateral(address _nodeOperator) internal { + function _topUpNodeOperatorBalance(address _nodeOperator) internal { if (msg.value == 0) revert ZeroArgument("msg.value"); if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); _validateNodeOperatorCaller(_nodeOperator); - nodeOperatorBonds[_nodeOperator].total += uint128(msg.value); + _getStorage().nodeOperatorBonds[_nodeOperator].total += uint128(msg.value); emit NodeOperatorBondToppedUp(_nodeOperator, msg.value); } + function _validateNodeOperatorCaller(address _nodeOperator) internal view { + ERC7201Storage storage $ = _getStorage(); + if ($.nodeOperatorVouchers[_nodeOperator] == msg.sender) return; + if ($.nodeOperatorVouchers[_nodeOperator] == address(0) && msg.sender == _nodeOperator) return; + revert MustBeNodeOperatorOrVoucher(); + } + function _wcToAddress(bytes32 _withdrawalCredentials) internal pure returns (address _wcAddress) { uint64 _wcVersion = uint8(_withdrawalCredentials[0]); if (_wcVersion < 1) { - revert WithdrawalCredentialsAreInvalid(); + revert WithdrawalCredentialsInvalidVersion(_wcVersion); } _wcAddress = address(uint160(uint256(_withdrawalCredentials))); } - function _processWitnessProof(ValidatorWitness calldata _witness) internal { - ValidatorStatus storage validatorStatus = validatorStatuses[_witness.pubkey]; + function _processWCProof(ValidatorWitness calldata _witness) internal { + ValidatorStatus storage validator = _getStorage().validatorStatuses[_witness.pubkey]; - if (validatorStatus.bondStatus != BondStatus.AWAITING_PROOF) { - revert ValidatorNotPreDeposited(_witness.pubkey, validatorStatus.bondStatus); + if (validator.bondStatus != BondStatus.AWAITING_PROOF) { + revert ValidatorNotPreDeposited(_witness.pubkey, validator.bondStatus); } - bytes32 _withdrawalCredentials = validatorStatus.stakingVault.withdrawalCredentials(); + bytes32 _withdrawalCredentials = validator.stakingVault.withdrawalCredentials(); // ensures vault fair play - if (address(validatorStatus.stakingVault) != _wcToAddress(_withdrawalCredentials)) { - revert WithdrawalCredentialsAreInvalid(); + if (address(validator.stakingVault) != _wcToAddress(_withdrawalCredentials)) { + revert StakingVaultWithdrawalCredentialsMismatch( + address(validator.stakingVault), + _wcToAddress(_withdrawalCredentials) + ); } _validatePubKeyWCProof(_witness, _withdrawalCredentials); - validatorStatus.bondStatus = BondStatus.PROVED; - nodeOperatorBonds[validatorStatus.nodeOperator].locked -= PREDEPOSIT_AMOUNT; + validator.bondStatus = BondStatus.PROVED; + _getStorage().nodeOperatorBonds[validator.nodeOperator].locked -= PREDEPOSIT_AMOUNT; - emit ValidatorProven(validatorStatus.nodeOperator, _witness.pubkey, address(validatorStatus.stakingVault), _withdrawalCredentials); + emit ValidatorProven( + validator.nodeOperator, + _witness.pubkey, + address(validator.stakingVault), + _withdrawalCredentials + ); + } + + function _getStorage() private pure returns (ERC7201Storage storage $) { + assembly { + $.slot := ERC721_STORAGE_LOCATION + } } + // * * * * * Events * * * * * // + + event NodeOperatorBondToppedUp(address indexed nodeOperator, uint256 amount); + event NodeOperatorBondWithdrawn(address indexed nodeOperator, uint256 amount, address indexed recipient); + event NodeOperatorVoucherSet(address indexed nodeOperator, address indexed voucher); + event ValidatorPreDeposited(address indexed nodeOperator, address indexed stakingVault, uint256 numberOfDeposits); + event ValidatorProven( + address indexed nodeOperator, + bytes indexed validatorPubkey, + address indexed stakingVault, + bytes32 withdrawalCredentials + ); + event ValidatorDisproven( + address indexed nodeOperator, + bytes indexed validatorPubkey, + address indexed stakingVault, + bytes32 withdrawalCredentials + ); + event ValidatorDisprovenWithdrawn( + address indexed nodeOperator, + bytes indexed validatorPubkey, + address indexed stakingVault, + address recipient + ); + + // * * * * * Errors * * * * * // + // node operator accounting - error BondMustBeFullyUnlocked(); + error BondMustBeFullyUnlocked(uint256 locked); error CannotSetSelfAsVoucher(); // predeposit errors error PredepositNoDeposits(); error PredepositDepositAmountInvalid(bytes validatorPubkey, uint256 depositAmount); error MustBeNewValidatorPubkey(bytes validatorPubkey, BondStatus bondStatus); - error NotEnoughUnlockedCollateralToPredeposit(uint256 unlockedCollateral, uint256 totalDepositAmount); - error PredepositValueNotMultipleOfPrediposit(); - error stakingVaultWithdrawalCredentialsMismatch(); + error NotEnoughUnlockedUnlockedBondToPredeposit(uint256 unlocked, uint256 totalDepositAmount); + error StakingVaultWithdrawalCredentialsMismatch(address stakingVault, address withdrawalCredentialsAddress); // depositing errors error DepositToUnprovenValidator(bytes validatorPubkey, BondStatus bondStatus); @@ -336,8 +430,9 @@ contract PredepositGuarantee is CLProofVerifier { // prove error WithdrawalCredentialsAreInvalid(); error WithdrawalCredentialsAreValid(); + error WithdrawalCredentialsInvalidVersion(uint64 version); // withdrawal proven - error NotEnoughUnlockedCollateralToWithdraw(uint256 unlockedCollateral, uint256 amount); + error NotEnoughUnlockedBondToWithdraw(uint256 unlocked, uint256 amount); // withdrawal disproven error ValidatorNotProvenInvalid(BondStatus bondStatus); diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index 982d7c491..ac456e429 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -29,6 +29,7 @@ contract LidoLocator is ILidoLocator { address withdrawalVault; address oracleDaemonConfig; address accounting; + address predepositGuarantee; address wstETH; } @@ -49,6 +50,7 @@ contract LidoLocator is ILidoLocator { address public immutable withdrawalVault; address public immutable oracleDaemonConfig; address public immutable accounting; + address public immutable predepositGuarantee; address public immutable wstETH; /** @@ -72,35 +74,15 @@ contract LidoLocator is ILidoLocator { withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); accounting = _assertNonZero(_config.accounting); + predepositGuarantee = _assertNonZero(_config.predepositGuarantee); wstETH = _assertNonZero(_config.wstETH); } - function coreComponents() external view returns( - address, - address, - address, - address, - address, - address - ) { - return ( - elRewardsVault, - oracleReportSanityChecker, - stakingRouter, - treasury, - withdrawalQueue, - withdrawalVault - ); + function coreComponents() external view returns (address, address, address, address, address, address) { + return (elRewardsVault, oracleReportSanityChecker, stakingRouter, treasury, withdrawalQueue, withdrawalVault); } - function oracleReportComponents() external view returns( - address, - address, - address, - address, - address, - address - ) { + function oracleReportComponents() external view returns (address, address, address, address, address, address) { return ( accountingOracle, oracleReportSanityChecker, diff --git a/contracts/common/interfaces/ILidoLocator.sol b/contracts/common/interfaces/ILidoLocator.sol index c39db1e23..8f987995e 100644 --- a/contracts/common/interfaces/ILidoLocator.sol +++ b/contracts/common/interfaces/ILidoLocator.sol @@ -21,6 +21,7 @@ interface ILidoLocator { function postTokenRebaseReceiver() external view returns(address); function oracleDaemonConfig() external view returns(address); function accounting() external view returns (address); + function predepositGuarantee() external view returns (address); function wstETH() external view returns (address); /// @notice Returns core Lido protocol component addresses in a single call diff --git a/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol b/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol index 4be33e35e..dbc4e7ac7 100644 --- a/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol +++ b/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol @@ -16,6 +16,7 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, OwnableUpgradeabl uint128 locked; int128 inOutDelta; address nodeOperator; + address depositGuardian; } uint64 private constant _version = 2; @@ -40,7 +41,7 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, OwnableUpgradeabl function initialize( address _owner, address _nodeOperator, - address /* _depositGuardian */, + address _depositGuardian, bytes calldata /* _params */ ) external reinitializer(_version) { if (owner() != address(0)) { @@ -50,6 +51,11 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, OwnableUpgradeabl __StakingVault_init_v2(); __Ownable_init(_owner); _getVaultStorage().nodeOperator = _nodeOperator; + _getVaultStorage().depositGuardian = _depositGuardian; + } + + function depositGuardian() external view returns (address) { + return _getVaultStorage().depositGuardian; } function finalizeUpgrade_v2() public reinitializer(_version) { diff --git a/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol b/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol index c38818a9c..f1eeed6ba 100644 --- a/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol +++ b/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol @@ -23,6 +23,7 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { address postTokenRebaseReceiver; address oracleDaemonConfig; address accounting; + address predepositGuarantee; address wstETH; } @@ -41,6 +42,7 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { address public immutable postTokenRebaseReceiver; address public immutable oracleDaemonConfig; address public immutable accounting; + address public immutable predepositGuarantee; address public immutable wstETH; constructor(ContractAddresses memory addresses) { @@ -60,6 +62,7 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { oracleDaemonConfig = addresses.oracleDaemonConfig; accounting = addresses.accounting; wstETH = addresses.wstETH; + predepositGuarantee = addresses.predepositGuarantee; } function coreComponents() external view returns (address, address, address, address, address, address) { diff --git a/test/0.8.9/contracts/LidoLocator__MockMutable.sol b/test/0.8.9/contracts/LidoLocator__MockMutable.sol index e102d2a4d..e4656ea12 100644 --- a/test/0.8.9/contracts/LidoLocator__MockMutable.sol +++ b/test/0.8.9/contracts/LidoLocator__MockMutable.sol @@ -22,6 +22,7 @@ contract LidoLocator__MockMutable is ILidoLocator { address withdrawalVault; address oracleDaemonConfig; address accounting; + address predepositGuarantee; address wstETH; } @@ -42,6 +43,7 @@ contract LidoLocator__MockMutable is ILidoLocator { address public immutable withdrawalVault; address public immutable oracleDaemonConfig; address public immutable accounting; + address public immutable predepositGuarantee; address public immutable wstETH; /** @@ -66,6 +68,7 @@ contract LidoLocator__MockMutable is ILidoLocator { oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); accounting = _assertNonZero(_config.accounting); wstETH = _assertNonZero(_config.wstETH); + predepositGuarantee = _assertNonZero(_config.predepositGuarantee); } function coreComponents() external view returns (address, address, address, address, address, address) {