diff --git a/contracts/0.8.25/lib/SSZ.sol b/contracts/0.8.25/lib/SSZ.sol index 59cf7f68d..cf17519d2 100644 --- a/contracts/0.8.25/lib/SSZ.sol +++ b/contracts/0.8.25/lib/SSZ.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md @@ -6,6 +6,14 @@ pragma solidity 0.8.25; import {GIndex} from "./GIndex.sol"; +struct BeaconBlockHeader { + uint64 slot; + uint64 proposerIndex; + bytes32 parentRoot; + bytes32 stateRoot; + bytes32 bodyRoot; +} + /* Cut and modified version of SSZ library from CSM only has methods for merkilized SSZ proof validation original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol @@ -15,6 +23,83 @@ library SSZ { error BranchHasExtraItem(); error InvalidProof(); error InvalidPubkeyLength(); + error InvalidBlockHeader(); + + /// @notice Modified version of `hashTreeRoot` from CSM to verify beacon block header against beacon root + /// @dev Reverts with InvalidBlockHeader` if calculated root doesn't match expected root + function verifyBeaconBlockHeader(BeaconBlockHeader calldata header, bytes32 expectedRoot) internal view { + bytes32[8] memory nodes = [ + toLittleEndian(header.slot), + toLittleEndian(header.proposerIndex), + header.parentRoot, + header.stateRoot, + header.bodyRoot, + bytes32(0), + bytes32(0), + bytes32(0) + ]; + + bytes32 root; + + /// @solidity memory-safe-assembly + assembly { + // Count of nodes to hash + let count := 8 + + // Loop over levels + // prettier-ignore + for { } 1 { } { + // Loop over nodes at the given depth + + // Initialize `offset` to the offset of `proof` elements in memory. + let target := nodes + let source := nodes + let end := add(source, shl(5, count)) + + // prettier-ignore + for { } 1 { } { + // Read next two hashes to hash + mcopy(0x00, source, 0x40) + + // Call sha256 precompile + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + // Store the resulting hash at the target location + mstore(target, mload(0x00)) + + // Advance the pointers + target := add(target, 0x20) + source := add(source, 0x40) + + if iszero(lt(source, end)) { + break + } + } + + count := shr(1, count) + if eq(count, 1) { + root := mload(0x00) + break + } + } + } + + if (root != expectedRoot) { + revert InvalidProof(); + } + } /// @notice Modified version of `verify` from Solady `MerkleProofLib` to support generalized indices and sha256 precompile. /// @dev Reverts if `leaf` doesn't exist in the Merkle tree with `root`, given `proof`. @@ -114,12 +199,12 @@ library SSZ { /// @solidity memory-safe-assembly assembly { - // Copy 48 bytes of `pubkey` to memory at 0x00 + // write 32 bytes to 32-64 bytes of scratch space + // to ensure last 49-64 bytes of pubkey are zeroed + mstore(0x20, 0) + // Copy 48 bytes of `pubkey` to start of scratch space calldatacopy(0x00, pubkey.offset, 48) - // Zero the remaining 16 bytes to form a 64-byte input block - mstore(0x30, 0) - // Call the SHA-256 precompile (0x02) with the 64-byte input if iszero(staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)) { revert(0, 0) @@ -129,4 +214,22 @@ library SSZ { _pubkeyRoot := mload(0x00) } } + + // See https://github.com/succinctlabs/telepathy-contracts/blob/5aa4bb7/src/libraries/SimpleSerialize.sol#L17-L28 + function toLittleEndian(uint256 v) public pure returns (bytes32) { + v = + ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + v = + ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + v = + ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + v = + ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) | + ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + v = (v >> 128) | (v << 128); + return bytes32(v); + } } diff --git a/contracts/0.8.25/vaults/Permissions.sol b/contracts/0.8.25/vaults/Permissions.sol index 6e54077ea..bddbb6e82 100644 --- a/contracts/0.8.25/vaults/Permissions.sol +++ b/contracts/0.8.25/vaults/Permissions.sol @@ -9,7 +9,7 @@ import {AccessControlVoteable} from "contracts/0.8.25/utils/AccessControlVoteabl import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; -import {IPredepositGuarantee} from "./interfaces/IPredepositGuarantee.sol"; +import {PredepositGuarantee} from "./predeposit_guarantee/PredepositGuarantee.sol"; import {VaultHub} from "./VaultHub.sol"; /** @@ -164,7 +164,7 @@ abstract contract Permissions is AccessControlVoteable { bytes calldata _pubkey, address _recipient ) internal onlyRole(PDG_WITHDRAWAL_ROLE) returns (uint128) { - return IPredepositGuarantee(stakingVault().depositGuardian()).withdrawDisprovenPredeposit(_pubkey, _recipient); + return PredepositGuarantee(stakingVault().depositGuardian()).withdrawDisprovenPredeposit(_pubkey, _recipient); } function _transferStakingVaultOwnership(address _newOwner) internal onlyIfVotedBy(_votingCommittee()) { diff --git a/contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol b/contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol deleted file mode 100644 index df84a62d0..000000000 --- a/contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -// See contracts/COMPILERS.md -pragma solidity 0.8.25; - -import {IStakingVaultOwnable} from "./IStakingVault.sol"; - -/** - * @title ICLProofVerifier - * @author Lido - * @notice Interface for the internal `CLProofVerifier` contract - */ -interface ICLProofVerifier { - struct ValidatorWitness { - bytes32[] proof; - bytes pubkey; - uint256 validatorIndex; - uint64 childBlockTimestamp; - } -} - -/** - * @title IPredepositGuarantee - * @author Lido - * @notice Interface for the `PredepositGuarantee` contract - */ -interface IPredepositGuarantee is ICLProofVerifier { - enum BondStatus { - NONE, - AWAITING_PROOF, - PROVED, - PROVED_INVALID - } - - struct NodeOperatorBond { - uint128 total; - uint128 locked; - } - - struct ValidatorStatus { - BondStatus bondStatus; - IStakingVaultOwnable stakingVault; - address nodeOperator; - } - - // constructor and initializer interfaces not needed in interface definition - - function nodeOperatorBond(address _nodeOperator) external view returns (NodeOperatorBond memory); - - function nodeOperatorVoucher(address _nodeOperator) external view returns (address); - - function validatorStatus(bytes calldata _validatorPubkey) external view returns (ValidatorStatus memory); - - function topUpNodeOperatorBond(address _nodeOperator) external payable; - - function withdrawNodeOperatorBond(address _nodeOperator, uint128 _amount, address _recipient) external; - - function setNodeOperatorVoucher(address _voucher) external payable; - - function predeposit( - IStakingVaultOwnable _stakingVault, - IStakingVaultOwnable.Deposit[] calldata _deposits - ) external payable; - - function proveValidatorWC(ValidatorWitness calldata _witness) external; - - function depositToBeaconChain( - IStakingVaultOwnable _stakingVault, - IStakingVaultOwnable.Deposit[] calldata _deposits - ) external payable; - - function proveAndDeposit( - ValidatorWitness[] calldata _witnesses, - IStakingVaultOwnable.Deposit[] calldata _deposits, - IStakingVaultOwnable _stakingVault - ) external payable; - - function proveInvalidValidatorWC( - ValidatorWitness calldata _witness, - bytes32 _invalidWithdrawalCredentials - ) external; - - function withdrawDisprovenPredeposit( - bytes calldata _validatorPubkey, - address _recipient - ) external returns (uint128 amount); - - function disproveAndWithdraw( - ValidatorWitness calldata _witness, - bytes32 _invalidWithdrawalCredentials, - address _recipient - ) external returns (uint128 amount); -} diff --git a/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol b/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol index 47613ee81..964e6e8be 100644 --- a/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol +++ b/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol @@ -5,9 +5,7 @@ pragma solidity 0.8.25; import {GIndex, pack, concat} from "contracts/0.8.25/lib/GIndex.sol"; -import {SSZ} from "contracts/0.8.25/lib/SSZ.sol"; - -import {ICLProofVerifier} from "../interfaces/IPredepositGuarantee.sol"; +import {SSZ, BeaconBlockHeader} from "contracts/0.8.25/lib/SSZ.sol"; /** * @title CLProofVerifier @@ -17,29 +15,100 @@ import {ICLProofVerifier} from "../interfaces/IPredepositGuarantee.sol"; * CLProofVerifier is base abstract contract that provides internal method to verify * merkle proofs of validator entry in CL. It uses concatenated proofs that prove * validator existence in CL just from pubkey and withdrawalCredentials againts Beacon block root - * stored in BeaconRoots contract. + * stored in BeaconRoots system contract. * * * NB!: GI_FIRST_VALIDATOR must be updated if Ethereum hardfork changes order of CL state tree - * (e.g. Pectra, Altair, etc.) + * (e.g. Pectra, Fusaka, etc.) * */ -abstract contract CLProofVerifier is ICLProofVerifier { +abstract contract CLProofVerifier { + struct ValidatorWitness { + bytes32[] proof; + bytes pubkey; + uint256 validatorIndex; + uint64 childBlockTimestamp; + } + + struct CLProofVerifierERC7201Storage { + uint64 latestProvenSlot; + uint64 latestProvenSlotTimestamp; + } + + /** + * @notice Storage offset slot for ERC-7201 namespace + * The storage namespace is used to prevent upgrade collisions + * keccak256(abi.encode(uint256(keccak256("Lido.Vaults.CLProofVerifier")) - 1)) & ~bytes32(uint256(0xff)); + */ + bytes32 private constant CL_PROOF_VERIFIER_STORAGE_LOCATION = + 0x345c2759b654c4a1f4e918fb90cc43c20694c04e946964cebe7cf9d73c2c0200; + // See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788. address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; - // GIndex of parent node for (Pubkey,WC) in validator container - // unlikely to change, same between mainnet/testnets + /** GIndex of parent node for (Pubkey,WC) in validator container + * unlikely to change, same between mainnet/testnets. + * Scheme of Validator Container Tree: + * + Validator Container Root + │ + ┌───────────────┴───────────────┐ + │ │ + node proof[1] **DEPTH = 1 + │ │ + ┌───────┴───────┐ ┌───────┴───────┐ + │ │ │ │ + Proven Parent proof[0] node node **DEPTH = 2 + │ │ │ │ + ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ + │ │ │ │ │ │ │ │ + [pubkeyRoot] [wc] [EB] [slashed] [AEE] [AE] [EE] [WE] **DEPTH = 3 + {.................} + ↑ + what needs to be proven + + * VALIDATOR_TREE_DEPTH = 2 + * POSITION = 0 as parent node is first in the row at DEPTH 2 + * GI_PUBKEY_WC_PARENT = pack((1 << VALIDATOR_TREE_DEPTH) + POSITION, VALIDATOR_TREE_DEPTH); + * + */ GIndex public immutable GI_PUBKEY_WC_PARENT = pack(1 << 2, 2); - // GIndex of stateRoot in Beacon Block state - // unlikely to change, same between mainnet/testnets + // + // + /** GIndex of stateRoot in Beacon Block state + * unlikely to change, same between mainnet/testnets + * Scheme of Beacon Block Tree: + * + Beacon Block Root + │ + ┌───────────────┴──────────────────────────┐ + │ │ + node proof[2] **DEPTH = 1 + │ │ + ┌───────┴───────┐ ┌───────┴───────┐ + │ │ │ │ + proof[1] node node node **DEPTH = 2 + │ │ │ │ + ┌─────────┴─────┐ ┌─────┴───────────┐ ┌─────┴─────┐ ┌───┴──┐ + │ │ │ │ │ │ │ │ + [slot] [proposerInd] [parentRoot] [stateRoot] [bodyRoot] [0] [0] [0] **DEPTH = 3 + (proof[0]) ↑ + what needs to be proven + * BEACON_HEADER_TREE_DEPTH = 3 + * POSITION = 3 as stateRoot position in a leaf row of the tree + * GI_STATE_VIEW = pack((1 << BEACON_HEADER_TREE_DEPTH) + POSITION, BEACON_HEADER_TREE_DEPTH); + */ GIndex public immutable GI_STATE_VIEW = pack((1 << 3) + 3, 3); // Index of first validator in CL state // can change between hardforks and must be updated GIndex public immutable GI_FIRST_VALIDATOR; + GIndex public immutable GI_FIRST_VALIDATOR_AFTER_CHANGE; + uint64 public immutable SLOT_CHANGE_GI; - constructor(GIndex _gIFirstValidator) { + constructor(GIndex _gIFirstValidator, GIndex _gIFirstValidatorAfterChange, uint64 _changeSlot) { GI_FIRST_VALIDATOR = _gIFirstValidator; + GI_FIRST_VALIDATOR_AFTER_CHANGE = _gIFirstValidatorAfterChange; + SLOT_CHANGE_GI = _changeSlot; } /** @@ -57,7 +126,10 @@ abstract contract CLProofVerifier is ICLProofVerifier { // pubkey + wc bytes32 _leaf = SSZ.sha256Pair(SSZ.pubkeyRoot(_witness.pubkey), _withdrawalCredentials); // concatenated index for parent(pubkey + wc) -> Validator Index in state tree -> stateView Index in Beacon block Tree - GIndex _gIndex = concat(GI_STATE_VIEW, concat(_getValidatorGI(_witness.validatorIndex), GI_PUBKEY_WC_PARENT)); + GIndex _gIndex = concat( + GI_STATE_VIEW, + concat(_getValidatorGI(_witness.validatorIndex, _witness.childBlockTimestamp), GI_PUBKEY_WC_PARENT) + ); SSZ.verifyProof({ proof: _witness.proof, @@ -67,6 +139,32 @@ abstract contract CLProofVerifier is ICLProofVerifier { }); } + /** + * @notice updates current slot of CL to enact first Validator GI transition + * @param _beaconBlockHeader object containing Beacon block header container from CL + * @param _childBlockTimestamp to access Beacon block root + * @dev reverts with `InvalidProof` when _beaconBlockHeader cannot be proven to Beacon block root + */ + function proveSlotChange(BeaconBlockHeader calldata _beaconBlockHeader, uint64 _childBlockTimestamp) public { + CLProofVerifierERC7201Storage storage $ = _getCLProofVerifierStorage(); + SSZ.verifyBeaconBlockHeader(_beaconBlockHeader, _getParentBlockRoot(_childBlockTimestamp)); + + uint64 provenSlot = _beaconBlockHeader.slot; + + if ($.latestProvenSlot > provenSlot) { + revert SlotAlreadyProven(); + } + + if (provenSlot < SLOT_CHANGE_GI || $.latestProvenSlot >= SLOT_CHANGE_GI) { + revert SlotUpdateHasNoEffect(); + } + + $.latestProvenSlot = _beaconBlockHeader.slot; + $.latestProvenSlotTimestamp = _childBlockTimestamp; + + emit SlotProven(_beaconBlockHeader.slot, _childBlockTimestamp); + } + /** * @notice returns parent CL block root for given child block timestamp * @param _childBlockTimestamp timestamp of child block @@ -88,9 +186,27 @@ abstract contract CLProofVerifier is ICLProofVerifier { * @param _offset from first validator (Validator Index) * @return gIndex of container in CL state tree */ - function _getValidatorGI(uint256 _offset) internal view returns (GIndex) { - return GI_FIRST_VALIDATOR.shr(_offset); + function _getValidatorGI(uint256 _offset, uint64 childBlockTimestamp) internal view returns (GIndex) { + CLProofVerifierERC7201Storage storage $ = _getCLProofVerifierStorage(); + // will only activate change if proving for block that has timestamp after SLOT_CHANGE_GI + // can survive upgrade of implementation where GI_FIRST_VALIDATOR_AFTER_CHANGE->GI_FIRST_VALIDATOR + if (childBlockTimestamp >= $.latestProvenSlotTimestamp && $.latestProvenSlot >= SLOT_CHANGE_GI) { + return GI_FIRST_VALIDATOR_AFTER_CHANGE.shr(_offset); + } else { + return GI_FIRST_VALIDATOR.shr(_offset); + } + } + + function _getCLProofVerifierStorage() private pure returns (CLProofVerifierERC7201Storage storage $) { + assembly { + $.slot := CL_PROOF_VERIFIER_STORAGE_LOCATION + } } + event SlotProven(uint64 provenSlot, uint64 provenSlotTimestamp); + + error InvalidTimestamp(); + error SlotUpdateHasNoEffect(); + error SlotAlreadyProven(); error RootNotFound(); } diff --git a/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol b/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol index c433d029b..430b75911 100644 --- a/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol +++ b/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol @@ -5,28 +5,89 @@ pragma solidity 0.8.25; import {GIndex} from "contracts/0.8.25/lib/GIndex.sol"; +import {BeaconBlockHeader} from "contracts/0.8.25/lib/SSZ.sol"; import {PausableUntilWithRoles} from "contracts/0.8.25/utils/PausableUntilWithRoles.sol"; import {CLProofVerifier} from "./CLProofVerifier.sol"; import {IStakingVaultOwnable} from "../interfaces/IStakingVault.sol"; -import {IPredepositGuarantee} from "../interfaces/IPredepositGuarantee.sol"; /** * @title PredepositGuarantee * @author Lido - * @notice + * @notice This contract acts as permissionless deposit security layer for all compatible staking vaults. + * It allows Node Operators(NO) to provide ether to back up their validators' deposits. + * While only Staking Vault ether is used to deposit to the beacon chain, NO's ether is locked. + * And can only be unlocked if the validator is proven to have valid Withdrawal Credentials on Ethereum Consensus Layer. + * Merkle proofs against Beacon Block Root are used to prove both validator's validity and invalidity + * where invalid validators's ether can be withdrawn by the staking vault owner. + * A system of NO's guarantors can be used to allow NOs to handle deposits and verifications + * while guarantors provide ether. + * + * !NB: + * There is a mutual trust assumption between NO's and guarantors. + * Internal guards for NO<->Guarantor are used only to prevent mistakes and provide recovery in OP-SEC incidents. + * But can not be used to fully prevent malicious behavior in this relationship where NO's can access guarantor provided ether. + * + * + * !NB: + * PDG is permissionless by design. Anyone can be an NO, provided there is a compatible staking vault + * that has `nodeOperator()` as NO and allows PDG to perform `depositToBeaconChain()` on it + * Staking Vault does not have to be connected to Lido or any other system to be compatible with PDG + * but a reverse constraint can be AND are applied. */ -contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableUntilWithRoles { +contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles { + /** + * @notice represents validator stages in PDG flow + * @dev if validator is in PROVED_INVALID and it's PREDEPOSIT_AMOUNT is withdrawn + * it's deleted from the storage and status returns to NONE to free up storage/gas + * @param NONE - initial stage + * @param AWAITING_PROOF - PREDEPOSIT_AMOUNT is deposited with this validator by the vault + * @param PROVEN - validator is proven to be valid and can be used to deposit to beacon chain + * @param PROVEN_INVALID - validator is proven to be invalid and it's PREDEPOSIT_AMOUNT can be withdrawn by staking vault owner + */ + enum validatorStage { + NONE, + AWAITING_PROOF, + PROVEN, + PROVEN_INVALID + } + /** + * @notice represents NO balance in PDG + * @dev fits into single 32 bytes slot + * @param total total ether balance of the NO + * @param locked ether locked in unproved predeposits + */ + struct NodeOperatorBalance { + uint128 total; + uint128 locked; + } + /** + * @notice represents status of the validator in PDG + * @dev is used to track validator from predeposit -> proof -> deposit + * @param stage represents validator stage in PDG flow + * @param stakingVault hard links validator to specific StakingVault to prevent cross-deposit + * @param nodeOperator hard links validator to specific NO to prevent malicious vault-mimic for stealing balance + */ + struct ValidatorStatus { + validatorStage stage; + IStakingVaultOwnable stakingVault; + address nodeOperator; + } + /** * @notice ERC-7201 storage namespace for the vault * @dev ERC-7201 namespace is used to prevent upgrade collisions - * @custom: TODO + * @param nodeOperatorBalance - balance of NO in PDG + * @param nodeOperatorGuarantor - mapping of NO to guarantor, where zero address means NO is self-guarantor + * @param guarantorClaimableEther - ether that guarantor can claim back if NO has changed guarantor with balance + * @param validatorStatus - status of the validators in PDG */ struct ERC7201Storage { - mapping(address nodeOperator => NodeOperatorBond bond) nodeOperatorBonds; - mapping(address nodeOperator => address voucher) nodeOperatorVouchers; - mapping(bytes validatorPubkey => ValidatorStatus validatorStatus) validatorStatuses; + mapping(address nodeOperator => NodeOperatorBalance balance) nodeOperatorBalance; + mapping(address nodeOperator => address guarantor) nodeOperatorGuarantor; + mapping(address guarantor => uint256 claimableEther) guarantorClaimableEther; + mapping(bytes validatorPubkey => ValidatorStatus validatorStatus) validatorStatus; } uint128 public constant PREDEPOSIT_AMOUNT = 1 ether; @@ -34,12 +95,16 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU /** * @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))` + * keccak256(abi.encode(uint256(keccak256("Lido.Vaults.PredepositGuarantee")) - 1)) & ~bytes32(uint256(0xff)) */ - bytes32 private constant ERC721_STORAGE_LOCATION = + bytes32 private constant ERC7201_STORAGE_LOCATION = 0xf66b5a365356c5798cc70e3ea6a236b181a826a69f730fc07cc548244bee5200; - constructor(GIndex _gIFirstValidator) CLProofVerifier(_gIFirstValidator) { + constructor( + GIndex _gIFirstValidator, + GIndex _gIFirstValidatorAfterChange, + uint64 _changeSlot + ) CLProofVerifier(_gIFirstValidator, _gIFirstValidatorAfterChange, _changeSlot) { _disableInitializers(); } @@ -52,89 +117,102 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU } // * * * * * * * * * * * * * * * * * * * * // - // * * * Node Operator Balance Logic * * * // + // * * * Node Operator Accounting Logic * * * // // * * * * * * * * * * * * * * * * * * * * // - function nodeOperatorBond(address _nodeOperator) external view returns (NodeOperatorBond memory) { - return _getStorage().nodeOperatorBonds[_nodeOperator]; + function nodeOperatorBalance(address _nodeOperator) external view returns (NodeOperatorBalance memory) { + return _getStorage().nodeOperatorBalance[_nodeOperator]; } - function nodeOperatorVoucher(address _nodeOperator) external view returns (address) { - return _getStorage().nodeOperatorVouchers[_nodeOperator]; + function nodeOperatorGuarantor(address _nodeOperator) external view returns (address) { + return _getStorage().nodeOperatorGuarantor[_nodeOperator]; } function validatorStatus(bytes calldata _validatorPubkey) external view returns (ValidatorStatus memory) { - return _getStorage().validatorStatuses[_validatorPubkey]; + return _getStorage().validatorStatus[_validatorPubkey]; } - function topUpNodeOperatorBond(address _nodeOperator) external payable whenResumed { + function topUpNodeOperatorBalance(address _nodeOperator) external payable whenResumed { _topUpNodeOperatorBalance(_nodeOperator); } - function withdrawNodeOperatorBond(address _nodeOperator, uint128 _amount, address _recipient) external whenResumed { + function withdrawNodeOperatorBalance( + address _nodeOperator, + uint128 _amount, + address _recipient + ) external onlyNodeOperatorOrGuarantor(_nodeOperator) whenResumed { if (_amount == 0) revert ZeroArgument("amount"); if (_amount % PREDEPOSIT_AMOUNT != 0) revert ValueMustBeMultipleOfPredepositAmount(_amount); if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); ERC7201Storage storage $ = _getStorage(); - _validateNodeOperatorCaller(_nodeOperator); - - uint256 unlocked = $.nodeOperatorBonds[_nodeOperator].total - $.nodeOperatorBonds[_nodeOperator].locked; + uint256 unlocked = $.nodeOperatorBalance[_nodeOperator].total - $.nodeOperatorBalance[_nodeOperator].locked; if (unlocked < _amount) revert NotEnoughUnlockedBondToWithdraw(unlocked, _amount); - $.nodeOperatorBonds[_nodeOperator].total -= _amount; + $.nodeOperatorBalance[_nodeOperator].total -= _amount; (bool success, ) = _recipient.call{value: uint256(_amount)}(""); if (!success) revert WithdrawalFailed(); - emit NodeOperatorBondWithdrawn(_nodeOperator, _amount, _recipient); + emit NodeOperatorBalanceWithdrawn(_nodeOperator, _recipient, _amount); } - function setNodeOperatorVoucher(address _voucher) external payable whenResumed { + /** + * @notice changes guarantor for the NO and provides refund to guarantor if NO has balance + * @param _newGuarantor address of the new guarantor, zero address to make NO self-guarantor + * @dev refunded ether can be claimed by guarantor with `claimGuarantorRefund()` + */ + function setNodeOperatorGuarantor(address _newGuarantor) external whenResumed { ERC7201Storage storage $ = _getStorage(); - NodeOperatorBond storage bond = $.nodeOperatorBonds[msg.sender]; - - if (_voucher == msg.sender) revert CannotSetSelfAsVoucher(); + NodeOperatorBalance storage balance = $.nodeOperatorBalance[msg.sender]; - if (bond.locked != 0) revert BondMustBeFullyUnlocked(bond.locked); + if (_newGuarantor == msg.sender) revert CannotSetSelfAsGuarantor(); - if (bond.total > 0 && $.nodeOperatorVouchers[msg.sender] != address(0)) { - uint256 _ejected = $.nodeOperatorBonds[msg.sender].total; - $.nodeOperatorBonds[msg.sender].total = 0; + if (balance.locked != 0) revert BondMustBeFullyUnlocked(balance.locked); - /* NB! - * Malicious voucher can block change to a new voucher by reverting `call` - * But due to balance top up & withdrawal amounts always being multiple of `PREDEPOSIT_AMOUNT` - * Voucher can't leave unusable balance ("dust") on NO balance - * In case malicious voucher leaves single PREDEPOSIT_AMOUNT of ether NO can: - * - perform frontrun deposit manually to a validator - * - predeposit this validator and `steal` vault PREDEPOSIT_AMOUNT ether via wrong WC - * - disprove predeposit with proof of wrong WC and burn bond towards staking vault - * - allow staking vault owner to withdraw PREDEPOSIT_AMOUNT ether - * Thus making this griefing attack not feasible - */ - (bool success, ) = $.nodeOperatorVouchers[msg.sender].call{value: _ejected}(""); - if (!success) revert WithdrawalFailed(); + if (balance.total > 0 && $.nodeOperatorGuarantor[msg.sender] != address(0)) { + uint256 refund = $.nodeOperatorBalance[msg.sender].total; + $.nodeOperatorBalance[msg.sender].total = 0; + $.guarantorClaimableEther[$.nodeOperatorGuarantor[msg.sender]] += refund; - emit NodeOperatorBondWithdrawn(msg.sender, _ejected, _voucher); + emit GuarantorRefunded(_newGuarantor, msg.sender, refund); } - $.nodeOperatorVouchers[msg.sender] = _voucher; + $.nodeOperatorGuarantor[msg.sender] = _newGuarantor; - // optional top up that will only work in NO sets voucher to zero address - if (msg.value != 0) { - _topUpNodeOperatorBalance(msg.sender); - } + emit NodeOperatorGuarantorSet(msg.sender, _newGuarantor); + } + + function claimGuarantorRefund(address _recipient) external returns (uint256) { + ERC7201Storage storage $ = _getStorage(); + + uint256 claimableEther = $.guarantorClaimableEther[msg.sender]; + + if (claimableEther == 0) revert EmptyRefund(); + + $.guarantorClaimableEther[msg.sender] = 0; + + (bool success, ) = _recipient.call{value: claimableEther}(""); + + if (!success) revert RefundFailed(); - emit NodeOperatorVoucherSet(msg.sender, _voucher); + emit GuarantorRefundClaimed(msg.sender, _recipient, claimableEther); + + return claimableEther; } // * * * * * * * * * * * * * * * * * * * * // // * * * * * Deposit Operations * * * * * // // * * * * * * * * * * * * * * * * * * * * // + /** + * @notice deposits NO's validators with PREDEPOSIT_AMOUNT ether from StakingVault and locks up NO's balance + * @dev if NO has no guarantor, accepts multiples of`PREDEPOSIT_AMOUNT` in msg.value to top up NO balance + * @param _stakingVault to deposit validators to + * @param _deposits StakingVault deposit struct that has amount as PREDEPOSIT_AMOUNT + */ function predeposit( IStakingVaultOwnable _stakingVault, IStakingVaultOwnable.Deposit[] calldata _deposits @@ -144,12 +222,12 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU address _nodeOperator = _stakingVault.nodeOperator(); if (msg.sender != _nodeOperator) revert MustBeNodeOperator(); - // optional top up when NO has no voucher + // check that node operator can top up themselves is inside if (msg.value != 0) { _topUpNodeOperatorBalance(_nodeOperator); } - // ensures vault fair play + // sanity check that vault returns correct WC if (address(_stakingVault) != _wcToAddress(_stakingVault.withdrawalCredentials())) { revert StakingVaultWithdrawalCredentialsMismatch( address(_stakingVault), @@ -160,30 +238,29 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU ERC7201Storage storage $ = _getStorage(); uint128 totalDepositAmount = PREDEPOSIT_AMOUNT * uint128(_deposits.length); - uint256 unlocked = $.nodeOperatorBonds[_nodeOperator].total - $.nodeOperatorBonds[_nodeOperator].locked; + uint128 unlocked = $.nodeOperatorBalance[_nodeOperator].total - $.nodeOperatorBalance[_nodeOperator].locked; - if (unlocked < totalDepositAmount) - revert NotEnoughUnlockedUnlockedBondToPredeposit(unlocked, totalDepositAmount); + if (unlocked < totalDepositAmount) revert NotEnoughUnlockedBondToPredeposit(unlocked, totalDepositAmount); for (uint256 i = 0; i < _deposits.length; i++) { IStakingVaultOwnable.Deposit calldata _deposit = _deposits[i]; - if ($.validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.NONE) { - revert MustBeNewValidatorPubkey(_deposit.pubkey, $.validatorStatuses[_deposit.pubkey].bondStatus); + if ($.validatorStatus[_deposit.pubkey].stage != validatorStage.NONE) { + revert MustBeNewValidatorPubkey(_deposit.pubkey, $.validatorStatus[_deposit.pubkey].stage); } // cannot predeposit a validator with a deposit amount that is not 1 ether if (_deposit.amount != PREDEPOSIT_AMOUNT) revert PredepositDepositAmountInvalid(_deposit.pubkey, _deposit.amount); - $.validatorStatuses[_deposit.pubkey] = ValidatorStatus({ - bondStatus: BondStatus.AWAITING_PROOF, + $.validatorStatus[_deposit.pubkey] = ValidatorStatus({ + stage: validatorStage.AWAITING_PROOF, stakingVault: _stakingVault, nodeOperator: _nodeOperator }); } - $.nodeOperatorBonds[_nodeOperator].locked += totalDepositAmount; + $.nodeOperatorBalance[_nodeOperator].locked += totalDepositAmount; _stakingVault.depositToBeaconChain(_deposits); emit ValidatorsPreDeposited(_nodeOperator, address(_stakingVault), _deposits.length); @@ -192,33 +269,28 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU // * * * * * Positive Proof Flow * * * * * // function proveValidatorWC(ValidatorWitness calldata _witness) public whenResumed { - ValidatorStatus storage validator = _getStorage().validatorStatuses[_witness.pubkey]; + bytes32 withdrawalCredentials = _getStorage() + .validatorStatus[_witness.pubkey] + .stakingVault + .withdrawalCredentials(); - if (validator.bondStatus != BondStatus.AWAITING_PROOF) { - revert ValidatorNotPreDeposited(_witness.pubkey, validator.bondStatus); - } - - bytes32 _withdrawalCredentials = validator.stakingVault.withdrawalCredentials(); + _validatePubKeyWCProof(_witness, withdrawalCredentials); - // ensures vault fair play - if (address(validator.stakingVault) != _wcToAddress(_withdrawalCredentials)) { - revert StakingVaultWithdrawalCredentialsMismatch( - address(validator.stakingVault), - _wcToAddress(_withdrawalCredentials) - ); - } + _processPositiveProof(_witness.pubkey, withdrawalCredentials); + } - _validatePubKeyWCProof(_witness, _withdrawalCredentials); + function proveValidatorWCWithBeaconHeader( + ValidatorWitness calldata _witness, + BeaconBlockHeader calldata _header + ) public whenResumed { + bytes32 withdrawalCredentials = _getStorage() + .validatorStatus[_witness.pubkey] + .stakingVault + .withdrawalCredentials(); - validator.bondStatus = BondStatus.PROVED; - _getStorage().nodeOperatorBonds[validator.nodeOperator].locked -= PREDEPOSIT_AMOUNT; + proveSlotChange(_header, _witness.childBlockTimestamp); - emit ValidatorProven( - validator.nodeOperator, - _witness.pubkey, - address(validator.stakingVault), - _withdrawalCredentials - ); + _processPositiveProof(_witness.pubkey, withdrawalCredentials); } function depositToBeaconChain( @@ -234,11 +306,11 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU 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 ($.validatorStatus[_deposit.pubkey].stage != validatorStage.PROVEN) { + revert DepositToUnprovenValidator(_deposit.pubkey, $.validatorStatus[_deposit.pubkey].stage); } - if ($.validatorStatuses[_deposit.pubkey].stakingVault != _stakingVault) { + if ($.validatorStatus[_deposit.pubkey].stakingVault != _stakingVault) { revert DepositToWrongVault(_deposit.pubkey, address(_stakingVault)); } } @@ -270,32 +342,20 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU ValidatorWitness calldata _witness, bytes32 _invalidWithdrawalCredentials ) public whenResumed { - ERC7201Storage storage $ = _getStorage(); - - ValidatorStatus storage validator = $.validatorStatuses[_witness.pubkey]; - - if (validator.bondStatus != BondStatus.AWAITING_PROOF) { - revert ValidatorNotPreDeposited(_witness.pubkey, validator.bondStatus); - } + _validatePubKeyWCProof(_witness, _invalidWithdrawalCredentials); - if (address(validator.stakingVault) == _wcToAddress(_invalidWithdrawalCredentials)) { - revert WithdrawalCredentialsAreValid(); - } + _processNegativeProof(_witness.pubkey, _invalidWithdrawalCredentials); + } + function proveInvalidValidatorWCWithBeaconHeader( + ValidatorWitness calldata _witness, + BeaconBlockHeader calldata _header, + bytes32 _invalidWithdrawalCredentials + ) public whenResumed { + proveSlotChange(_header, _witness.childBlockTimestamp); _validatePubKeyWCProof(_witness, _invalidWithdrawalCredentials); - // reduces total&locked NO deposit - $.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 - validator.bondStatus = BondStatus.PROVED_INVALID; - - emit ValidatorDisproven( - validator.nodeOperator, - _witness.pubkey, - address(validator.stakingVault), - _invalidWithdrawalCredentials - ); + _processNegativeProof(_witness.pubkey, _invalidWithdrawalCredentials); } // called by the staking vault owner if the predeposited validator was proven invalid @@ -304,7 +364,7 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU bytes calldata _validatorPubkey, address _recipient ) public whenResumed returns (uint128) { - ValidatorStatus storage validator = _getStorage().validatorStatuses[_validatorPubkey]; + ValidatorStatus storage validator = _getStorage().validatorStatus[_validatorPubkey]; IStakingVaultOwnable _stakingVault = validator.stakingVault; address _nodeOperator = validator.nodeOperator; @@ -315,9 +375,9 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU if (msg.sender != _stakingVault.owner()) revert WithdrawSenderNotStakingVaultOwner(); - if (validator.bondStatus != BondStatus.PROVED_INVALID) revert ValidatorNotProvenInvalid(validator.bondStatus); + if (validator.stage != validatorStage.PROVEN_INVALID) revert ValidatorNotProvenInvalid(validator.stage); - delete _getStorage().validatorStatuses[_validatorPubkey]; + delete _getStorage().validatorStatus[_validatorPubkey]; (bool success, ) = _recipient.call{value: PREDEPOSIT_AMOUNT}(""); @@ -339,23 +399,80 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU /// Internal functions - function _topUpNodeOperatorBalance(address _nodeOperator) internal { + function _processPositiveProof(bytes calldata _pubkey, bytes32 _withdrawalCredentials) internal { + ERC7201Storage storage $ = _getStorage(); + ValidatorStatus storage _validator = $.validatorStatus[_pubkey]; + + if (_validator.stage != validatorStage.AWAITING_PROOF) { + revert ValidatorNotPreDeposited(_pubkey, _validator.stage); + } + + // sanity check that vault returns correct WC + if (address(_validator.stakingVault) != _wcToAddress(_withdrawalCredentials)) { + revert StakingVaultWithdrawalCredentialsMismatch( + address(_validator.stakingVault), + _wcToAddress(_withdrawalCredentials) + ); + } + + _validator.stage = validatorStage.PROVEN; + $.nodeOperatorBalance[_validator.nodeOperator].locked -= PREDEPOSIT_AMOUNT; + + emit ValidatorProven( + _validator.nodeOperator, + _pubkey, + address(_validator.stakingVault), + _withdrawalCredentials + ); + } + + function _processNegativeProof(bytes calldata _pubkey, bytes32 _invalidWithdrawalCredentials) internal { + ERC7201Storage storage $ = _getStorage(); + ValidatorStatus storage validator = $.validatorStatus[_pubkey]; + + if (validator.stage != validatorStage.AWAITING_PROOF) { + revert ValidatorNotPreDeposited(_pubkey, validator.stage); + } + + uint64 wcVersion = uint8(_invalidWithdrawalCredentials[0]); + // 0x00 WC are invalid by default + if (wcVersion != 0 && address(validator.stakingVault) == _wcToAddress(_invalidWithdrawalCredentials)) { + revert WithdrawalCredentialsAreValid(); + } + + // reduces total&locked NO deposit + $.nodeOperatorBalance[validator.nodeOperator].total -= PREDEPOSIT_AMOUNT; + $.nodeOperatorBalance[validator.nodeOperator].locked -= PREDEPOSIT_AMOUNT; + // freed ether only will returned to owner of the vault with this validator + validator.stage = validatorStage.PROVEN_INVALID; + + emit ValidatorDisproven( + validator.nodeOperator, + _pubkey, + address(validator.stakingVault), + _invalidWithdrawalCredentials + ); + } + + function _topUpNodeOperatorBalance(address _nodeOperator) internal onlyNodeOperatorOrGuarantor(_nodeOperator) { if (msg.value == 0) revert ZeroArgument("msg.value"); if (msg.value % PREDEPOSIT_AMOUNT != 0) revert ValueMustBeMultipleOfPredepositAmount(msg.value); if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); - _validateNodeOperatorCaller(_nodeOperator); - - _getStorage().nodeOperatorBonds[_nodeOperator].total += uint128(msg.value); + _getStorage().nodeOperatorBalance[_nodeOperator].total += uint128(msg.value); - emit NodeOperatorBondToppedUp(_nodeOperator, msg.value); + emit NodeOperatorBalanceToppedUp(_nodeOperator, msg.sender, msg.value); } - function _validateNodeOperatorCaller(address _nodeOperator) internal view { + modifier onlyNodeOperatorOrGuarantor(address _nodeOperator) { ERC7201Storage storage $ = _getStorage(); - if ($.nodeOperatorVouchers[_nodeOperator] == msg.sender) return; - if ($.nodeOperatorVouchers[_nodeOperator] == address(0) && msg.sender == _nodeOperator) return; - revert MustBeNodeOperatorOrVoucher(); + if ( + !($.nodeOperatorGuarantor[_nodeOperator] == msg.sender || + ($.nodeOperatorGuarantor[_nodeOperator] == address(0) && msg.sender == _nodeOperator)) + ) { + revert MustBeNodeOperatorOrGuarantor(); + } + _; } function _wcToAddress(bytes32 _withdrawalCredentials) internal pure returns (address _wcAddress) { @@ -370,15 +487,19 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU function _getStorage() private pure returns (ERC7201Storage storage $) { assembly { - $.slot := ERC721_STORAGE_LOCATION + $.slot := ERC7201_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 NodeOperatorBalanceToppedUp(address indexed nodeOperator, address indexed sender, uint256 amount); + event NodeOperatorBalanceWithdrawn(address indexed nodeOperator, address indexed recipient, uint256 amount); + + event GuarantorRefunded(address indexed guarantor, address indexed nodeOperator, uint256 amount); + event GuarantorRefundClaimed(address indexed guarantor, address indexed recipient, uint256 amount); + + event NodeOperatorGuarantorSet(address indexed nodeOperator, address indexed guarantor); event ValidatorsPreDeposited( address indexed nodeOperator, address indexed stakingVault, @@ -407,20 +528,22 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU // node operator accounting error BondMustBeFullyUnlocked(uint256 locked); - error CannotSetSelfAsVoucher(); + error CannotSetSelfAsGuarantor(); error ValueMustBeMultipleOfPredepositAmount(uint256 value); + error EmptyRefund(); + error RefundFailed(); // predeposit errors error PredepositNoDeposits(); error PredepositDepositAmountInvalid(bytes validatorPubkey, uint256 depositAmount); - error MustBeNewValidatorPubkey(bytes validatorPubkey, BondStatus bondStatus); - error NotEnoughUnlockedUnlockedBondToPredeposit(uint256 unlocked, uint256 totalDepositAmount); + error MustBeNewValidatorPubkey(bytes validatorPubkey, validatorStage bondStatus); + error NotEnoughUnlockedBondToPredeposit(uint256 unlocked, uint256 totalDepositAmount); error StakingVaultWithdrawalCredentialsMismatch(address stakingVault, address withdrawalCredentialsAddress); // depositing errors - error DepositToUnprovenValidator(bytes validatorPubkey, BondStatus bondStatus); + error DepositToUnprovenValidator(bytes validatorPubkey, validatorStage bondStatus); error DepositToWrongVault(bytes validatorPubkey, address stakingVault); - error ValidatorNotPreDeposited(bytes validatorPubkey, BondStatus bondStatus); + error ValidatorNotPreDeposited(bytes validatorPubkey, validatorStage bondStatus); // prove error WithdrawalCredentialsAreInvalid(); @@ -430,14 +553,14 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU error NotEnoughUnlockedBondToWithdraw(uint256 unlocked, uint256 amount); // withdrawal disproven - error ValidatorNotProvenInvalid(BondStatus bondStatus); + error ValidatorNotProvenInvalid(validatorStage bondStatus); error WithdrawSenderNotStakingVaultOwner(); /// withdrawal generic error WithdrawalFailed(); error WithdrawToVaultNotAllowed(); // auth - error MustBeNodeOperatorOrVoucher(); + error MustBeNodeOperatorOrGuarantor(); error MustBeNodeOperator(); // general diff --git a/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts b/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts index 9651ea048..f414fa0ef 100644 --- a/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts +++ b/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts @@ -106,7 +106,11 @@ describe("CLProofVerifier.sol", () => { await sszMerkleTree.addValidatorLeaf(generateValidator()); } - CLProofVerifier = await ethers.deployContract("CLProofVerifier__Harness", [localTree.gIFirstValidator], {}); + CLProofVerifier = await ethers.deployContract( + "CLProofVerifier__Harness", + [localTree.gIFirstValidator, localTree.gIFirstValidator, 0], + {}, + ); // test mocker const mockRoot = randomBytes32(); @@ -125,7 +129,7 @@ describe("CLProofVerifier.sol", () => { it("should verify precalclulated validator object in merkle tree", async () => { const StaticCLProofVerifier: CLProofVerifier__Harness = await ethers.deployContract( "CLProofVerifier__Harness", - [STATIC_VALIDATOR.gIFirstValidator], + [STATIC_VALIDATOR.gIFirstValidator, STATIC_VALIDATOR.gIFirstValidator, 0], {}, ); diff --git a/test/0.8.25/vaults/predeposit-guarantee/contracts/CLProofVerifier__harness.sol b/test/0.8.25/vaults/predeposit-guarantee/contracts/CLProofVerifier__harness.sol index 795b38bf3..3801f8aa2 100644 --- a/test/0.8.25/vaults/predeposit-guarantee/contracts/CLProofVerifier__harness.sol +++ b/test/0.8.25/vaults/predeposit-guarantee/contracts/CLProofVerifier__harness.sol @@ -7,7 +7,11 @@ import {pack, concat} from "contracts/0.8.25/lib/GIndex.sol"; import {CLProofVerifier, SSZ, GIndex} from "contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol"; contract CLProofVerifier__Harness is CLProofVerifier { - constructor(GIndex _gIFirstValidator) CLProofVerifier(_gIFirstValidator) {} + constructor( + GIndex _gIFirstValidator, + GIndex _gIFirstValidatorAfterChange, + uint64 _changeSlot + ) CLProofVerifier(_gIFirstValidator, _gIFirstValidatorAfterChange, _changeSlot) {} function TEST_validatePubKeyWCProof( ValidatorWitness calldata _witness, diff --git a/test/0.8.25/vaults/predeposit-guarantee/contracts/SSZHelpers.sol b/test/0.8.25/vaults/predeposit-guarantee/contracts/SSZHelpers.sol index c5f5d3893..157510731 100644 --- a/test/0.8.25/vaults/predeposit-guarantee/contracts/SSZHelpers.sol +++ b/test/0.8.25/vaults/predeposit-guarantee/contracts/SSZHelpers.sol @@ -59,15 +59,17 @@ contract SSZHelpers { // `validator` is the offset for `pubkey`. (Remember that `pubkey` is expected // to be exactly 48 bytes long.) let pubkeyOffset := calldataload(validator) + // write 32 bytes to 32-64 bytes of scratch space + // to ensure last 49-64 bytes of pubkey are zeroed + mstore(0x20, 0) // The pubkey’s actual data is encoded at: - // validator + pubkeyOffset + 32 + // validator + pubkeyOffset + 32 // because the first word at that location is the length. - // Copy 48 bytes of pubkey data into memory at 0x00. calldatacopy(0x00, add(validator, add(pubkeyOffset, 32)), 48) // Zero the remaining 16 bytes to form a 64‐byte block. // (0x30 = 48, so mstore at 0x30 will zero 32 bytes covering addresses 48–79; // only bytes 48–63 matter for our 64-byte input.) - mstore(0x30, 0) + // Call the SHA‑256 precompile (at address 0x02) with the 64-byte block. if iszero(staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)) { revert(0, 0) diff --git a/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts b/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts index e0c7f6d1f..3d1a32240 100644 --- a/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts +++ b/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts @@ -35,7 +35,7 @@ describe("PredepositGuarantee.sol", () => { let admin: HardhatEthersSigner; let vaultOwner: HardhatEthersSigner; let vaultOperator: HardhatEthersSigner; - let vaultOperatorVoucher: HardhatEthersSigner; + let vaultOperatorGuarantor: HardhatEthersSigner; let stranger: HardhatEthersSigner; let proxy: OssifiableProxy; @@ -75,7 +75,7 @@ describe("PredepositGuarantee.sol", () => { } before(async () => { - [deployer, admin, vaultOwner, vaultOperator, vaultOperatorVoucher, stranger] = await ethers.getSigners(); + [deployer, admin, vaultOwner, vaultOperator, vaultOperatorGuarantor, stranger] = await ethers.getSigners(); // local merkle tree with 1st validator const localMerkle = await prepareLocalMerkleTree(); @@ -85,7 +85,11 @@ describe("PredepositGuarantee.sol", () => { depositContract = await ethers.deployContract("DepositContract__MockForStakingVault"); // PDG - pdgImpl = await ethers.deployContract("PredepositGuarantee", [localMerkle.gIFirstValidator], { from: deployer }); + pdgImpl = await ethers.deployContract( + "PredepositGuarantee", + [localMerkle.gIFirstValidator, localMerkle.gIFirstValidator, 0], + { from: deployer }, + ); proxy = await ethers.deployContract("OssifiableProxy", [pdgImpl, admin, new Uint8Array()], admin); pdg = await ethers.getContractAt("PredepositGuarantee", proxy, vaultOperator); @@ -119,13 +123,13 @@ describe("PredepositGuarantee.sol", () => { context("happy path", () => { it("can use PDG happy path", async () => { - // NO sets voucher - await pdg.setNodeOperatorVoucher(vaultOperatorVoucher); - expect(await pdg.nodeOperatorVoucher(vaultOperator)).to.equal(vaultOperatorVoucher); + // NO sets guarantor + await pdg.setNodeOperatorGuarantor(vaultOperatorGuarantor); + expect(await pdg.nodeOperatorGuarantor(vaultOperator)).to.equal(vaultOperatorGuarantor); - // Voucher funds PDG for operator - await pdg.connect(vaultOperatorVoucher).topUpNodeOperatorBond(vaultOperator, { value: ether("1") }); - let [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator); + // guarantor funds PDG for operator + await pdg.connect(vaultOperatorGuarantor).topUpNodeOperatorBalance(vaultOperator, { value: ether("1") }); + let [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBalance(vaultOperator); expect(operatorBondTotal).to.equal(ether("1")); expect(operatorBondLocked).to.equal(0n); @@ -149,7 +153,7 @@ describe("PredepositGuarantee.sol", () => { .to.emit(depositContract, "DepositEvent") .withArgs(predepositData.pubkey, vaultWC, predepositData.signature, predepositData.depositDataRoot); - [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator); + [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBalance(vaultOperator); expect(operatorBondTotal).to.equal(ether("1")); expect(operatorBondLocked).to.equal(ether("1")); @@ -186,13 +190,13 @@ describe("PredepositGuarantee.sol", () => { .to.emit(depositContract, "DepositEvent") .withArgs(postDepositData.pubkey, vaultWC, postDepositData.signature, postDepositData.depositDataRoot); - [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator); + [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBalance(vaultOperator); expect(operatorBondTotal).to.equal(ether("1")); expect(operatorBondLocked).to.equal(ether("0")); - // NOs voucher withdraws bond from PDG - await pdg.connect(vaultOperatorVoucher).withdrawNodeOperatorBond(vaultOperator, ether("1"), vaultOperator); - [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator); + // NOs guarantor withdraws bond from PDG + await pdg.connect(vaultOperatorGuarantor).withdrawNodeOperatorBalance(vaultOperator, ether("1"), vaultOperator); + [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBalance(vaultOperator); expect(operatorBondTotal).to.equal(ether("0")); expect(operatorBondLocked).to.equal(ether("0")); });