diff --git a/script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json b/script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json deleted file mode 100644 index b83fcb7d80..0000000000 --- a/script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "addresses": { - "allocationManager": "0xAbD5Dd30CaEF8598d4EadFE7D45Fd582EDEade15", - "allocationManagerImplementation": "0xBFF7154bAa41e702E78Fb082a8Ce257Ce13E6f55", - "avsDirectory": "0xCa839541648D3e23137457b1Fd4A06bccEADD33a", - "avsDirectoryImplementation": "0x1362e9Cb37831C433095f1f1568215B7FDeD37Ef", - "baseStrategyImplementation": "0x61C6A250AEcAbf6b5e4611725b4f99C4DC85DB34", - "delegationManager": "0x3391eBafDD4b2e84Eeecf1711Ff9FC06EF9Ed182", - "delegationManagerImplementation": "0x4073a9B0fb0f31420C2A2263fB6E9adD33ea6F2A", - "eigenLayerPauserReg": "0xBb02ACE793e921D6a454062D2933064F31Fae0B2", - "eigenLayerProxyAdmin": "0xBf0c97a7df334BD83e0912c1218E44FD7953d122", - "eigenPodBeacon": "0x8ad244c2a986e48862c5bE1FdCA27cef0aaa6E15", - "eigenPodImplementation": "0x93cecf40F05389E99e163539F8d1CCbd4267f9A7", - "eigenPodManager": "0x8C9781FD55c67CE4DC08e3035ECbdB2B67a07307", - "eigenPodManagerImplementation": "0x3013B13BF3a464ff9078EFa40b7dbfF8fA67138d", - "emptyContract": "0x689CEE9134e4234caEF6c15Bf1D82779415daFAe", - "rewardsCoordinator": "0xa7DB7B0E63B5B75e080924F9C842758711177c07", - "rewardsCoordinatorImplementation": "0x0e93df1A21CA53F93160AbDee19A92A20f8b397B", - "strategies": [ - { - "strategy_address": "0x4f812633943022fA97cb0881683aAf9f318D5Caa", - "token_address": "0x94373a4919B3240D86eA41593D5eBa789FEF3848", - "token_symbol": "WETH" - } - ], - "strategyBeacon": "0x957c04A5666079255fD75220a15918ecBA6039c6", - "strategyFactory": "0x09F8f1c1ca1815083a8a05E1b4A0c65EFB509141", - "strategyFactoryImplementation": "0x8b1F09f8292fd658Da35b9b3b1d4F7d1C0F3F592", - "strategyManager": "0x70f8bC2Da145b434de66114ac539c9756eF64fb3", - "strategyManagerImplementation": "0x1562BfE7Cb4644ff030C1dE4aA5A9aBb88a61aeC", - "token": { - "tokenProxyAdmin": "0x0000000000000000000000000000000000000000", - "EIGEN": "0x0000000000000000000000000000000000000000", - "bEIGEN": "0x0000000000000000000000000000000000000000", - "EIGENImpl": "0x0000000000000000000000000000000000000000", - "bEIGENImpl": "0x0000000000000000000000000000000000000000", - "eigenStrategy": "0x0000000000000000000000000000000000000000", - "eigenStrategyImpl": "0x0000000000000000000000000000000000000000" - } - }, - "chainInfo": { - "chainId": 17000, - "deploymentBlock": 2548240 - }, - "parameters": { - "communityMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "executorMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "operationsMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "pauserMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "timelock": "0x0000000000000000000000000000000000000000" - } -} \ No newline at end of file diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 2192864015..f5ba7ed94d 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -59,6 +59,17 @@ interface IEigenPodErrors { /// @dev Thrown when a validator has not been slashed on the beacon chain. error ValidatorNotSlashedOnBeaconChain(); + /// Consolidation and Withdrawal Requests + + /// @dev Thrown when a predeploy request is initiated with insufficient msg.value + error InsufficientFunds(); + /// @dev Thrown when refunding excess fees from a predeploy fails + error RefundFailed(); + /// @dev Thrown when calling the predeploy fails + error PredeployFailed(); + /// @dev Thrown when querying a predeploy for its current fee fails + error FeeQueryFailed(); + /// Misc /// @dev Thrown when an invalid block root is returned by the EIP-4788 oracle. @@ -79,14 +90,16 @@ interface IEigenPodTypes { } + /** + * @param validatorIndex index of the validator on the beacon chain + * @param restakedBalanceGwei amount of beacon chain ETH restaked on EigenLayer in gwei + * @param lastCheckpointedAt timestamp of the validator's most recent balance update + * @param status last recorded status of the validator + */ struct ValidatorInfo { - // index of the validator in the beacon chain uint64 validatorIndex; - // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; - //timestamp of the validator's most recent balance update uint64 lastCheckpointedAt; - // status of the validator VALIDATOR_STATUS status; } @@ -97,6 +110,30 @@ interface IEigenPodTypes { int64 balanceDeltasGwei; uint64 prevBeaconBalanceGwei; } + + /** + * @param srcPubkey the pubkey of the source validator for the consolidation + * @param targetPubkey the pubkey of the target validator for the consolidation + * @dev Note that if srcPubkey == targetPubkey, this is a "switch request," and will + * change the validator's withdrawal credential type from 0x01 to 0x02. + * For more notes on usage, see `requestConsolidation` + */ + struct ConsolidationRequest { + bytes srcPubkey; + bytes targetPubkey; + } + + /** + * @param pubkey the pubkey of the validator to withdraw from + * @param amountGwei the amount (in gwei) to withdraw from the beacon chain to the pod + * @dev Note that if amountGwei == 0, this is a "full exit request," and will fully exit + * the validator to the pod. + * For more notes on usage, see `requestWithdrawal` + */ + struct WithdrawalRequest { + bytes pubkey; + uint64 amountGwei; + } } interface IEigenPodEvents is IEigenPodTypes { @@ -132,6 +169,18 @@ interface IEigenPodEvents is IEigenPodTypes { /// @notice Emitted when a validaor is proven to have 0 balance at a given checkpoint event ValidatorWithdrawn(uint64 indexed checkpointTimestamp, uint40 indexed validatorIndex); + + /// @notice Emitted when a consolidation request is initiated where source == target + event SwitchToCompoundingRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a standard consolidation request is initiated + event ConsolidationRequested(bytes32 indexed sourcePubkeyHash, bytes32 indexed targetPubkeyHash); + + /// @notice Emitted when a withdrawal request is initiated where request.amountGwei == 0 + event ExitRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a partial withdrawal request is initiated + event WithdrawalRequested(bytes32 indexed validatorPubkeyHash, uint64 withdrawalAmountGwei); } /** @@ -249,6 +298,98 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { BeaconChainProofs.ValidatorProof calldata proof ) external; + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// consolidate their validators on the beacon chain. + /// @param requests An array of requests consisting of the source and target pubkeys + /// of the validators to be consolidated + /// @dev Both the source and target validator MUST have active withdrawal credentials + /// pointed at the pod + /// @dev The consolidation request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getConsolidationRequestFee` to query the current fee for + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to (fee * requests.length). This fee is updated at the end of each block. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for details) + /// + /// @dev Note on beacon chain behavior: + /// - If request.srcPubkey == request.targetPubkey, this is a "switch" consolidation. Once + /// processed on the beacon chain, the validator's withdrawal credentials will be changed + /// to compounding (0x02). + /// - The rest of the notes assume src != target. + /// - The target validator MUST already have 0x02 credentials. The source validator can have either. + /// - Consoldiation sets the source validator's exit_epoch and withdrawable_epoch, similar to an exit. + /// When the exit epoch is reached, an epoch sweep will process the consolidation and transfer balance + /// from the source to the target validator. + /// - Consolidation transfers min(srcValidator.effective_balance, state.balance[srcIndex]) to the target. + /// This may not be the entirety of the source validator's balance; any remainder will be moved to the + /// pod when hit by a subsequent withdrawal sweep. + /// + /// @dev Note that consolidation requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - If request.srcPubkey == request.targetPubkey, the validator MUST have 0x01 credentials + /// - If request.srcPubkey != request.targetPubkey, the target validator MUST have 0x02 credentials + /// - Both the source and target validators MUST be active and MUST NOT have initiated exits + /// - The source validator MUST NOT have pending partial withdrawal requests (via `requestWithdrawal`) + /// - If the source validator is slashed after requesting consolidation (but before processing), + /// the consolidation will be skipped. + /// + /// For further reference, see consolidation processing at block and epoch boundaries: + /// - Block: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_consolidation_request + /// - Epoch: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_pending_consolidations + function requestConsolidation( + ConsolidationRequest[] calldata requests + ) external payable; + + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// withdraw funds from validators on the beacon chain. + /// @param requests An array of requests consisting of the source validator and an + /// amount to withdraw + /// @dev The withdrawal request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getWithdrawalRequestFee` to query the current fee for + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to (fee * requests.length). This fee is updated at the end of each block. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for details) + /// + /// @dev Note on beacon chain behavior: + /// - Withdrawal requests have two types: full exit requests, and partial exit requests. + /// Partial exit requests will be skipped if the validator has 0x01 withdrawal credentials. + /// If you want your validators to have access to partial exits, use `requestConsolidation` + /// to change their withdrawal credentials to compounding (0x02). + /// - If request.amount == 0, this is a FULL exit request. A full exit request initiates a + /// standard validator exit. + /// - Other amounts are treated as PARTIAL exit requests. A partial exit request will NOT result + /// in a validator with less than 32 ETH balance. Any requested amount above this is ignored. + /// - The actual amount withdrawn for a partial exit is given by the formula: + /// min(request.amount, state.balances[vIdx] - 32 ETH - pending_balance_to_withdraw) + /// (where `pending_balance_to_withdraw` is the sum of any outstanding partial exit requests) + /// (Note that this means you may request more than is actually withdrawn!) + /// + /// @dev Note that withdrawal requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - request.pubkey MUST be a valid validator pubkey + /// - request.pubkey MUST belong to a validator whose withdrawal credentials are this pod + /// - If request.amount is for a partial exit, the validator MUST have 0x02 withdrawal credentials + /// - If request.amount is for a full exit, the validator MUST NOT have any pending partial exits + /// - The validator MUST be active and MUST NOT have initiated exit + /// + /// For further reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_withdrawal_request + function requestWithdrawal( + WithdrawalRequest[] calldata requests + ) external payable; + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; @@ -355,4 +496,14 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { function getParentBlockRoot( uint64 timestamp ) external view returns (bytes32); + + /// @notice Returns the fee required to add a consolidation request to the EIP-7251 predeploy this block. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7251#fee-calculation + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. + function getConsolidationRequestFee() external view returns (uint256); + + /// @notice Returns the current fee required to add a withdrawal request to the EIP-7002 predeploy. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. + function getWithdrawalRequestFee() external view returns (uint256); } diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index 5c8297f98d..5d7af5d8ad 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -18,14 +18,14 @@ library BeaconChainProofs { error InvalidValidatorFieldsLength(); /// @notice Heights of various merkle trees in the beacon chain - /// - beaconBlockRoot - /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT - /// -- beaconStateRoot - /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// beaconStateRoot + /// / \ HEIGHT: BEACON_STATE_TREE_HEIGHT /// validatorContainerRoot, balanceContainerRoot - /// | | HEIGHT: BALANCE_TREE_HEIGHT - /// | individual balances - /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT /// individual validators uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; uint256 internal constant DENEB_BEACON_STATE_TREE_HEIGHT = 5; diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index a7c10359e7..82556a0a47 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -47,6 +47,14 @@ contract EigenPod is /// (See https://eips.ethereum.org/EIPS/eip-4788) address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + /// @notice The address of the EIP-7002 withdrawal request predeploy + /// (See https://eips.ethereum.org/EIPS/eip-7002) + address internal constant WITHDRAWAL_REQUEST_ADDRESS = 0x00000961Ef480Eb55e80D19ad83579A64c007002; + + /// @notice The address of the EIP-7251 consolidation request predeploy + /// (See https://eips.ethereum.org/EIPS/eip-7251) + address internal constant CONSOLIDATION_REQUEST_ADDRESS = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; + /// @notice The length of the EIP-4788 beacon block root ring buffer uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; @@ -365,6 +373,75 @@ contract EigenPod is _startCheckpoint(false); } + /// @inheritdoc IEigenPod + function requestConsolidation( + ConsolidationRequest[] calldata requests + ) external payable onlyWhenNotPaused(PAUSED_CONSOLIDATIONS) onlyOwnerOrProofSubmitter { + uint256 fee = getConsolidationRequestFee(); + require(msg.value >= fee * requests.length, InsufficientFunds()); + uint256 remainder = msg.value - (fee * requests.length); + + for (uint256 i = 0; i < requests.length; i++) { + ConsolidationRequest calldata request = requests[i]; + // Validate pubkeys are well-formed + require(request.srcPubkey.length == 48, InvalidPubKeyLength()); + require(request.targetPubkey.length == 48, InvalidPubKeyLength()); + + // Ensure target has verified withdrawal credentials pointed at this pod + bytes32 sourcePubkeyHash = _calcPubkeyHash(request.srcPubkey); + bytes32 targetPubkeyHash = _calcPubkeyHash(request.targetPubkey); + ValidatorInfo memory target = validatorPubkeyHashToInfo(targetPubkeyHash); + require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + + // Call the predeploy + bytes memory callData = bytes.concat(request.srcPubkey, request.targetPubkey); + (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); + require(ok, PredeployFailed()); + + // Emit event depending on whether this is a switch to 0x02, or a regular consolidation + if (sourcePubkeyHash == targetPubkeyHash) emit SwitchToCompoundingRequested(sourcePubkeyHash); + else emit ConsolidationRequested(sourcePubkeyHash, targetPubkeyHash); + } + + // Refund remainder of msg.value + if (remainder > 0) { + Address.sendValue(payable(msg.sender), remainder); + } + } + + /// @inheritdoc IEigenPod + function requestWithdrawal( + WithdrawalRequest[] calldata requests + ) external payable onlyWhenNotPaused(PAUSED_WITHDRAWAL_REQUESTS) onlyOwnerOrProofSubmitter { + uint256 fee = getWithdrawalRequestFee(); + require(msg.value >= fee * requests.length, InsufficientFunds()); + uint256 remainder = msg.value - (fee * requests.length); + + for (uint256 i = 0; i < requests.length; i++) { + WithdrawalRequest calldata request = requests[i]; + // Validate pubkey is well-formed. + // + // It's not necessary to perform any additional validation; the worst-case + // scenario is just that the consensus layer skips an invalid request. + require(request.pubkey.length == 48, InvalidPubKeyLength()); + + // Call the predeploy + bytes memory callData = abi.encodePacked(request.pubkey, request.amountGwei); + (bool ok,) = WITHDRAWAL_REQUEST_ADDRESS.call{value: fee}(callData); + require(ok, PredeployFailed()); + + // Emit event depending on whether the request is a full exit or a partial withdrawal + bytes32 pubkeyHash = _calcPubkeyHash(request.pubkey); + if (request.amountGwei == 0) emit ExitRequested(pubkeyHash); + else emit WithdrawalRequested(pubkeyHash, request.amountGwei); + } + + // Refund remainder of msg.value + if (remainder > 0) { + Address.sendValue(payable(msg.sender), remainder); + } + } + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens( IERC20[] memory tokenList, @@ -688,13 +765,39 @@ contract EigenPod is } ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec - function _calculateValidatorPubkeyHash( + function _calcPubkeyHash( bytes memory validatorPubkey ) internal pure returns (bytes32) { require(validatorPubkey.length == 48, InvalidPubKeyLength()); return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); } + /// @dev Returns the current fee required to query either the EIP-7002 or EIP-7251 predeploy + function _getFee( + address predeploy + ) internal view returns (uint256) { + (bool success, bytes memory result) = predeploy.staticcall(""); + require(success && result.length == 32, FeeQueryFailed()); + + return uint256(bytes32(result)); + } + + /// @notice Returns the PROOF_TYPE depending on the `proofTimestamp` in relation to the fork timestamp. + function _getProofVersion( + uint64 proofTimestamp + ) internal view returns (BeaconChainProofs.ProofVersion) { + /// Get the timestamp of the Pectra fork, read from the `EigenPodManager` + /// This returns the timestamp of the first non-missed slot at or after the Pectra hard fork + uint64 forkTimestamp = eigenPodManager.pectraForkTimestamp(); + require(forkTimestamp != 0, ForkTimestampZero()); + + /// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp` + /// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block. + return proofTimestamp <= forkTimestamp + ? BeaconChainProofs.ProofVersion.DENEB + : BeaconChainProofs.ProofVersion.PECTRA; + } + /** * * VIEW FUNCTIONS @@ -709,15 +812,15 @@ contract EigenPod is /// @notice Returns the validatorInfo for a given validatorPubkeyHash function validatorPubkeyHashToInfo( bytes32 validatorPubkeyHash - ) external view returns (ValidatorInfo memory) { + ) public view returns (ValidatorInfo memory) { return _validatorPubkeyHashToInfo[validatorPubkeyHash]; } /// @notice Returns the validatorInfo for a given validatorPubkey function validatorPubkeyToInfo( bytes calldata validatorPubkey - ) external view returns (ValidatorInfo memory) { - return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + ) public view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calcPubkeyHash(validatorPubkey)]; } function validatorStatus( @@ -730,7 +833,7 @@ contract EigenPod is function validatorStatus( bytes calldata validatorPubkey ) external view returns (VALIDATOR_STATUS) { - bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + bytes32 validatorPubkeyHash = _calcPubkeyHash(validatorPubkey); return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; } @@ -754,19 +857,13 @@ contract EigenPod is return abi.decode(result, (bytes32)); } - /// @notice Returns the PROOF_TYPE depending on the `proofTimestamp` in relation to the fork timestamp. - function _getProofVersion( - uint64 proofTimestamp - ) internal view returns (BeaconChainProofs.ProofVersion) { - /// Get the timestamp of the Pectra fork, read from the `EigenPodManager` - /// This returns the timestamp of the first non-missed slot at or after the Pectra hard fork - uint64 forkTimestamp = eigenPodManager.pectraForkTimestamp(); - require(forkTimestamp != 0, ForkTimestampZero()); + /// @inheritdoc IEigenPod + function getConsolidationRequestFee() public view returns (uint256) { + return _getFee(CONSOLIDATION_REQUEST_ADDRESS); + } - /// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp` - /// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block. - return proofTimestamp <= forkTimestamp - ? BeaconChainProofs.ProofVersion.DENEB - : BeaconChainProofs.ProofVersion.PECTRA; + /// @inheritdoc IEigenPod + function getWithdrawalRequestFee() public view returns (uint256) { + return _getFee(WITHDRAWAL_REQUEST_ADDRESS); } } diff --git a/src/contracts/pods/EigenPodPausingConstants.sol b/src/contracts/pods/EigenPodPausingConstants.sol index 0c1d337c80..afec0903a4 100644 --- a/src/contracts/pods/EigenPodPausingConstants.sol +++ b/src/contracts/pods/EigenPodPausingConstants.sol @@ -31,4 +31,8 @@ abstract contract EigenPodPausingConstants { uint8 internal constant PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS = 7; uint8 internal constant PAUSED_VERIFY_STALE_BALANCE = 8; + + uint8 internal constant PAUSED_CONSOLIDATIONS = 9; + + uint8 internal constant PAUSED_WITHDRAWAL_REQUESTS = 10; } diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 5b768f7ae4..f191bfe9ca 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -63,6 +63,15 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { return (staker, tokenBalances); } + /// @dev Creates a blank slate user with no assets + function _newEmptyStaker() internal returns (User) { + User staker = _randUser_NoAssets(_getStakerName()); + + if (!isUpgraded) stakersToMigrate.push(staker); + + return staker; + } + /** * @dev Create a new operator according to configured random variants. * This user will immediately deposit their randomized assets into eigenlayer. diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index 09cd3642cd..9a997e006e 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -9,18 +9,12 @@ import "src/contracts/pods/EigenPodManager.sol"; import "src/test/mocks/ETHDepositMock.sol"; import "src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol"; +import "src/test/integration/mocks/EIP_7002_Mock.t.sol"; +import "src/test/integration/mocks/EIP_7251_Mock.t.sol"; +import "src/test/integration/mocks/LibValidator.t.sol"; +import "src/test/integration/mocks/LibProofGen.t.sol"; import "src/test/utils/Logger.t.sol"; -struct ValidatorFieldsProof { - bytes32[] validatorFields; - bytes validatorFieldsProof; -} - -struct BalanceRootProof { - bytes32 balanceRoot; - bytes proof; -} - struct CheckpointProofs { BeaconChainProofs.BalanceContainerProof balanceContainerProof; BeaconChainProofs.BalanceProof[] balanceProofs; @@ -50,16 +44,8 @@ struct StaleBalanceProofs { contract BeaconChainMock is Logger { using StdStyle for *; using print for *; - - struct Validator { - bool isDummy; - bool isSlashed; - bytes32 pubkeyHash; - bytes withdrawalCreds; - uint64 effectiveBalanceGwei; - uint64 activationEpoch; - uint64 exitEpoch; - } + using LibValidator for *; + using LibProofGen for *; /// @dev The type of slash to apply to a validator enum SlashType { @@ -69,39 +55,31 @@ contract BeaconChainMock is Logger { } - /// @dev All withdrawals are processed with index == 0 - uint constant ZERO_NODES_LENGTH = 100; - // Rewards given to each validator during epoch processing uint64 public constant CONSENSUS_REWARD_AMOUNT_GWEI = 1; uint64 public constant MINOR_SLASH_AMOUNT_GWEI = 10; - // Max effective balance for a validator + // Min/max balances for valdiators // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values - uint public MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; - uint64 public MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; - - /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): - /// @dev Non-constant values will change with the Pectra hard fork - - // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate - uint BEACON_STATE_FIELDS = 37; - // see https://eth2book.info/capella/part3/containers/blocks/#beaconblock - uint constant BEACON_BLOCK_FIELDS = 5; - - uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; - uint VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - uint BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - uint immutable BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + uint constant MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; + uint64 constant MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; + uint constant MIN_ACTIVATION_BALANCE_WEI = 32 ether; + uint64 constant MIN_ACTIVATION_BALANCE_GWEI = 32 gwei; uint64 genesisTime; uint64 public nextTimestamp; EigenPodManager eigenPodManager; + + // Used to call predeploys as the system + address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + /// Canonical beacon chain predeploy addresses IETHPOSDeposit constant DEPOSIT_CONTRACT = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); EIP_4788_Oracle_Mock constant EIP_4788_ORACLE = EIP_4788_Oracle_Mock(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02); - + EIP_7002_Mock constant WITHDRAWAL_PREDEPLOY = EIP_7002_Mock(payable(0x0000BBdDc7CE488642fb579F8B00f3a590007251)); + EIP_7251_Mock constant CONSOLIDATION_PREDEPLOY = EIP_7251_Mock(payable(0x00000961Ef480Eb55e80D19ad83579A64c007002)); + /** * BeaconState containers, used for proofgen: * https://eth2book.info/capella/part3/containers/state/#beaconstate @@ -128,20 +106,6 @@ contract BeaconChainMock is Logger { uint lastIndexProcessed; uint64 curTimestamp; - // Maps block.timestamp -> beacon state root and proof - mapping(uint64 => BeaconChainProofs.StateRootProof) stateRootProofs; - - // Maps block.timestamp -> balance container root and proof - mapping(uint64 => BeaconChainProofs.BalanceContainerProof) balanceContainerProofs; - - // Maps block.timestamp -> validatorIndex -> credential proof for that timestamp - mapping(uint64 => mapping(uint40 => ValidatorFieldsProof)) validatorFieldsProofs; - - // Maps block.timestamp -> balanceRootIndex -> balance proof for that timestamp - mapping(uint64 => mapping(uint40 => BalanceRootProof)) balanceRootProofs; - - bytes32[] zeroNodes; - constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) { genesisTime = _genesisTime; eigenPodManager = _eigenPodManager; @@ -149,16 +113,10 @@ contract BeaconChainMock is Logger { // Create mock 4788 oracle cheats.etch(address(DEPOSIT_CONTRACT), type(ETHPOSDepositMock).runtimeCode); cheats.etch(address(EIP_4788_ORACLE), type(EIP_4788_Oracle_Mock).runtimeCode); + cheats.etch(address(CONSOLIDATION_PREDEPLOY), type(EIP_7251_Mock).runtimeCode); + cheats.etch(address(WITHDRAWAL_PREDEPLOY), type(EIP_7002_Mock).runtimeCode); - // Calculate nodes of empty merkle tree - bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); - zeroNodes = new bytes32[](ZERO_NODES_LENGTH); - zeroNodes[0] = curNode; - - for (uint i = 1; i < zeroNodes.length; i++) { - zeroNodes[i] = sha256(abi.encodePacked(curNode, curNode)); - curNode = zeroNodes[i]; - } + LibProofGen.usePectra(); } function NAME() public pure virtual override returns (string memory) { @@ -225,7 +183,7 @@ contract BeaconChainMock is Logger { _setCurrentBalance(validatorIndex, 0); // Send current balance to pod - address destination = _toAddress(validators[validatorIndex].withdrawalCreds); + address destination = v.withdrawalAddress(); cheats.deal(destination, address(destination).balance + uint(uint(exitedBalanceGwei) * GWEI_TO_WEI)); return exitedBalanceGwei; @@ -306,8 +264,15 @@ contract BeaconChainMock is Logger { } } + modifier onEpoch() { + _updateCurrentEpoch(); + _; + _advanceEpoch(); + } + /// @dev Move forward one epoch on the beacon chain, taking care of important epoch processing: /// - Award ALL validators CONSENSUS_REWARD_AMOUNT + /// - Process any pending partial withdrawals (new in Pectra) /// - Withdraw any balance over Max EB /// - Withdraw any balance for exited validators /// - Effective balances updated (NOTE: we do not use hysteresis!) @@ -318,11 +283,11 @@ contract BeaconChainMock is Logger { /// Note: /// - DOES generate consensus rewards for ALL non-exited validators /// - DOES withdraw in excess of Max EB / if validator is exited - function advanceEpoch() public { + function advanceEpoch() public onEpoch { print.method("advanceEpoch"); + _generateRewards(); - _withdrawExcess(); - _advanceEpoch(); + _processWithdrawals(); } /// @dev Like `advanceEpoch`, but does NOT generate consensus rewards for validators. @@ -332,10 +297,10 @@ contract BeaconChainMock is Logger { /// Note: /// - does NOT generate consensus rewards /// - DOES withdraw in excess of Max EB / if validator is exited - function advanceEpoch_NoRewards() public { + function advanceEpoch_NoRewards() public onEpoch { print.method("advanceEpoch_NoRewards"); - _withdrawExcess(); - _advanceEpoch(); + + _processWithdrawals(); } /// @dev Like `advanceEpoch`, but explicitly does NOT withdraw if balances @@ -346,15 +311,14 @@ contract BeaconChainMock is Logger { /// - DOES generate consensus rewards for ALL non-exited validators /// - does NOT withdraw in excess of Max EB /// - does NOT withdraw if validator is exited - function advanceEpoch_NoWithdraw() public { + function advanceEpoch_NoWithdraw() public onEpoch { print.method("advanceEpoch_NoWithdraw"); + _generateRewards(); - _advanceEpoch(); } - function advanceEpoch_NoWithdrawNoRewards() public { + function advanceEpoch_NoWithdrawNoRewards() public onEpoch { print.method("advanceEpoch_NoWithdrawNoRewards"); - _advanceEpoch(); } /// @dev Iterate over all validators. If the validator is still active, @@ -378,45 +342,223 @@ contract BeaconChainMock is Logger { console.log(" - Generated rewards for %s of %s validators.", totalRewarded, validators.length); } - /// @dev Iterate over all validators. If the validator has > Max EB current balance - /// OR is exited, withdraw the excess to the validator's withdrawal address. - function _withdrawExcess() internal { - uint totalExcessWei; + /// @dev Process EIP-7002 partial withdrawal requests and any withdrawals from the beacon chain + function _processWithdrawals() internal { + _process_EIP_7002_Requests(); + _process_EIP_7521_Requests(); + _withdrawFromBeaconChain(); + } + + /// @dev Handle consolidation requests + function _process_EIP_7521_Requests() internal { + // Call EIP-7521 predeploy and dequeue any consolidation requests + cheats.prank(SYSTEM_ADDRESS); + (bool ok, bytes memory data) = address(CONSOLIDATION_PREDEPLOY).call(""); + require(ok, "BeaconChainMock._process_EIP_7521_Requests: CONSOLIDATION_PREDEPLOY failed"); + + ConsolidationRequest[] memory requests = abi.decode(data, (ConsolidationRequest[])); + console.log(" - Dequeued %d consolidation requests.", requests.length); + + for (uint i = 0; i < requests.length; i++) { + ConsolidationRequest memory request = requests[i]; + uint40 sourceIndex = request.sourcePubkey.toIndex(); + uint40 targetIndex = request.targetPubkey.toIndex(); + + if (sourceIndex >= validators.length || targetIndex >= validators.length) { + _logSkip("source or target does not exist"); + continue; + } + + Validator storage source = validators[sourceIndex]; + Validator storage target = validators[targetIndex]; + + if (source.isDummy || target.isDummy) { + _logSkip("dummy validator"); + } else if (request.source != source.withdrawalAddress()) { + _logSkip("incorrect source address"); + } else if (source.hasBLSWC()) { + _logSkip("source has BLS withdrawal credentials"); + } else if (!source.isActiveAt(currentEpoch())) { + _logSkip("source is not active at current epoch"); + } else if (source.isExiting()) { + _logSkip("source validator is exiting"); + } else if (sourceIndex == targetIndex) { + // Handle switch to 0x02 request + if (source.hasCompoundingWC()) { + _logSkip("switch request source already has 0x02 credentials"); + } else { + console.log(" -- Switching validator to 0x02 creds (idx: %d).", sourceIndex); + + // The beacon chain would queue excess balance here as a "pending deposit", but + // we don't follow the spec that closely. + source.withdrawalCreds[0] = 0x02; + } + } else if (!target.hasCompoundingWC()) { + _logSkip("target does not have compounding withdrawal credentials"); + } else if (!target.isActiveAt(currentEpoch())) { + _logSkip("target is not active at current epoch"); + } else if (target.isExiting()) { + _logSkip("target validator is exiting"); + } else if (source.pendingBalanceToWithdrawGwei != 0) { + _logSkip("source has pending withdrawals in queue"); + } else if (source.isSlashed) { + _logSkip("source validator is slashed"); + } else { + console.log(" -- Consolidating source (%d) to target (%d).", sourceIndex, targetIndex); + + // Mark source validator exited + source.exitEpoch = currentEpoch(); + + // Transfer balance from source to target + // Note the beacon chain would cap this transfer to the effective balance, + // and withdraw the rest. Our effective balances aren't using hysteresis, so + // we approximate this behavior by withdrawing anything over 32 ETH. + uint64 transferAmtGwei = _currentBalanceGwei(sourceIndex); + uint64 withdrawAmtGwei; + if (transferAmtGwei > MIN_ACTIVATION_BALANCE_GWEI) { + withdrawAmtGwei = transferAmtGwei - MIN_ACTIVATION_BALANCE_GWEI; + transferAmtGwei = MIN_ACTIVATION_BALANCE_GWEI; + } + + uint64 targetBalanceGwei = _currentBalanceGwei(targetIndex); + _setCurrentBalance(sourceIndex, 0); + _setCurrentBalance(targetIndex, targetBalanceGwei + transferAmtGwei); + + // Withdraw excess balance + address destination = source.withdrawalAddress(); + cheats.deal(destination, address(destination).balance + uint(withdrawAmtGwei * GWEI_TO_WEI)); + } + } + } + + /// @dev Handle pending partial withdrawals AND withdraw excess validator balance + function _process_EIP_7002_Requests() internal { + // Call EIP-7002 predeploy and dequeue any withdrawal requests + cheats.prank(SYSTEM_ADDRESS); + (bool ok, bytes memory data) = address(WITHDRAWAL_PREDEPLOY).call(""); + require(ok, "BeaconChainMock._process_EIP_7002_Requests: WITHDRAWAL_PREDEPLOY failed"); + + WithdrawalRequest[] memory requests = abi.decode(data, (WithdrawalRequest[])); + console.log(" - Dequeued %d withdrawal requests.", requests.length); + + for (uint i = 0; i < requests.length; i++) { + WithdrawalRequest memory request = requests[i]; + uint40 validatorIndex = request.validatorPubkey.toIndex(); + + if (validatorIndex >= validators.length) { + _logSkip("validator does not exist"); + continue; + } + + Validator storage v = validators[validatorIndex]; + + bool isFullExitRequest = request.amountGwei == 0; + + uint64 balanceGwei = _currentBalanceGwei(validatorIndex); + bool hasExcessBalance = balanceGwei > MIN_ACTIVATION_BALANCE_GWEI + v.pendingBalanceToWithdrawGwei; + + if (v.isDummy) { + _logSkip("dummy validator"); + } else if (request.source != v.withdrawalAddress()) { + _logSkip("incorrect source address"); + } else if (!v.isActiveAt(currentEpoch())) { + _logSkip("inactive validator"); + } else if (v.isExiting()) { + _logSkip("exit in progress"); + } else if (isFullExitRequest && v.pendingBalanceToWithdrawGwei != 0) { + _logSkip("attempted full exit while pending withdrawal in queue"); + } else if (isFullExitRequest) { + // TODO - swap to internal method + exitValidator(validatorIndex); + } else if (!v.hasCompoundingWC()) { + _logSkip("attempted partial exit without 0x02 credentials"); + } else if (!hasExcessBalance) { + _logSkip("validator does not have excess balance"); + } else { + // Partial withdrawal - only withdraw down to 32 ETH + uint64 toWithdrawGwei = balanceGwei - MIN_ACTIVATION_BALANCE_GWEI - v.pendingBalanceToWithdrawGwei; + toWithdrawGwei = toWithdrawGwei > request.amountGwei ? request.amountGwei : toWithdrawGwei; + + v.pendingBalanceToWithdrawGwei += toWithdrawGwei; + } + } + } + + function _logSkip(string memory reason) internal { + console.log(" -- Skipping request with reason: %s.", reason); + } + + /// @dev Iterate over all validators. If the validator: + /// - has > than Max EB current balance + /// - is exited + /// - has a pending partial withdrawal + /// + /// ... withdraw the appropriate amount to the validator's withdrawal credentials + function _withdrawFromBeaconChain() internal { + uint64 totalWithdrawnGwei; for (uint i = 0; i < validators.length; i++) { Validator storage v = validators[i]; if (v.isDummy) continue; // don't process dummy validators - uint balanceWei = uint(_currentBalanceGwei(uint40(i))) * GWEI_TO_WEI; - address destination = _toAddress(v.withdrawalCreds); - uint excessBalanceWei; - uint64 newBalanceGwei = uint64(balanceWei / GWEI_TO_WEI); + uint64 withdrawAmtGwei; + uint64 curBalanceGwei = _currentBalanceGwei(uint40(i)); + // If the validator has nothing to withdraw, continue + if (curBalanceGwei == 0) continue; - // If the validator has exited, withdraw any existing balance - // - // If the validator has > Max EB, withdraw anything over that if (v.exitEpoch != BeaconChainProofs.FAR_FUTURE_EPOCH) { - if (balanceWei == 0) continue; + // Process full withdrawal for exited validator + withdrawAmtGwei = curBalanceGwei; + } else { + // If the validator has a pending withdrawal request, withdraw (but do not go below 32 eth) + // Note that these reqs are only fulfilled for 0x02 validators (this is enforced elsewhere) + if (v.pendingBalanceToWithdrawGwei != 0 && curBalanceGwei > MIN_ACTIVATION_BALANCE_GWEI) { + uint64 excessBalanceGwei = curBalanceGwei - MIN_ACTIVATION_BALANCE_GWEI; + + withdrawAmtGwei = v.pendingBalanceToWithdrawGwei; + if (withdrawAmtGwei > excessBalanceGwei) withdrawAmtGwei = excessBalanceGwei; - excessBalanceWei = balanceWei; - newBalanceGwei = 0; - } else if (balanceWei > MAX_EFFECTIVE_BALANCE_WEI) { - excessBalanceWei = balanceWei - MAX_EFFECTIVE_BALANCE_WEI; - newBalanceGwei = MAX_EFFECTIVE_BALANCE_GWEI; + // Reduce balance and set pending to 0 + curBalanceGwei -= withdrawAmtGwei; + v.pendingBalanceToWithdrawGwei = 0; + } + + // Withdraw any amount above the max effective balance: + // - For 0x01 validators, this is 32 ETH + // - For 0x02 validators, this is 2048 ETH + uint64 maxEBGwei = _getMaxEffectiveBalanceGwei(v); + if (curBalanceGwei > maxEBGwei) { + uint64 excessBalanceGwei = curBalanceGwei - maxEBGwei; + + withdrawAmtGwei += excessBalanceGwei; + } } + uint64 newBalanceGwei = curBalanceGwei - withdrawAmtGwei; + // Send ETH to withdrawal address - cheats.deal(destination, address(destination).balance + excessBalanceWei); - totalExcessWei += excessBalanceWei; + address destination = v.withdrawalAddress(); + uint withdrawAmtWei = withdrawAmtGwei * GWEI_TO_WEI; + + cheats.deal(destination, address(destination).balance + withdrawAmtWei); + totalWithdrawnGwei += withdrawAmtGwei; // Update validator's current balance _setCurrentBalance(uint40(i), newBalanceGwei); } - if (totalExcessWei != 0) console.log("- Withdrew excess balance:", totalExcessWei.asGwei()); + if (totalWithdrawnGwei != 0) console.log("- Total withdrawals from CL (gwei):", totalWithdrawnGwei); } - function _advanceEpoch() public virtual { + function _updateCurrentEpoch() internal { + uint64 curEpoch = currentEpoch(); + cheats.warp(_nextEpochStartTimestamp(curEpoch)); + } + + mapping(uint64 => StateProofs) proofs; + + function _advanceEpoch() internal virtual { cheats.pauseTracing(); + curTimestamp = uint64(block.timestamp); // Update effective balances for each validator for (uint i = 0; i < validators.length; i++) { @@ -425,23 +567,12 @@ contract BeaconChainMock is Logger { // Get current balance and trim anything over MAX EB uint64 balanceGwei = _currentBalanceGwei(uint40(i)); - if (balanceGwei > MAX_EFFECTIVE_BALANCE_GWEI) balanceGwei = MAX_EFFECTIVE_BALANCE_GWEI; + uint64 maxEBGwei = _getMaxEffectiveBalanceGwei(v); + if (balanceGwei > maxEBGwei) balanceGwei = maxEBGwei; v.effectiveBalanceGwei = balanceGwei; } - // console.log(" Updated effective balances...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - - uint64 curEpoch = currentEpoch(); - cheats.warp(_nextEpochStartTimestamp(curEpoch)); - curTimestamp = uint64(block.timestamp); - - // console.log(" Jumping to next epoch...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - // console.log(" Building beacon state trees...".dim()); // Log total number of validators and number being processed for the first time @@ -455,48 +586,14 @@ contract BeaconChainMock is Logger { return; } - // Build merkle tree for validators - bytes32 validatorsRoot = _buildMerkleTree({ - leaves: _getValidatorLeaves(), - treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1, - tree: trees[curTimestamp].validatorTree - }); - // console.log("-- validator container root", validatorsRoot); - - // Build merkle tree for current balances - bytes32 balanceContainerRoot = _buildMerkleTree({ - leaves: _getBalanceLeaves(), - treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1, - tree: trees[curTimestamp].balancesTree - }); - // console.log("-- balances container root", balanceContainerRoot); - - // Build merkle tree for BeaconState - bytes32 beaconStateRoot = _buildMerkleTree({ - leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), - treeHeight: BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT, - tree: trees[curTimestamp].stateTree - }); - // console.log("-- beacon state root", beaconStateRoot); - - // Build merkle tree for BeaconBlock - bytes32 beaconBlockRoot = _buildMerkleTree({ - leaves: _getBeaconBlockLeaves(beaconStateRoot), - treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT, - tree: trees[curTimestamp].blockTree + bytes32 beaconBlockRoot = proofs[curTimestamp].generate({ + validators: validators, + balances: balances }); - // console.log("-- beacon block root", cheats.toString(beaconBlockRoot)); - // Push new block root to oracle EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot); - // Pre-generate proofs to pass to EigenPod methods - _genStateRootProof(beaconStateRoot); - _genBalanceContainerProof(balanceContainerRoot); - _genCredentialProofs(); - _genBalanceProofs(); - cheats.resumeTracing(); } @@ -518,19 +615,16 @@ contract BeaconChainMock is Logger { if (validatorIndex % 4 == 0) { uint64 dummyBalanceGwei = type(uint64).max - uint64(validators.length); - bytes memory _dummyPubkey = new bytes(48); - assembly { - mstore(add(48, _dummyPubkey), validatorIndex) - } validators.push( Validator({ isDummy: true, isSlashed: false, - pubkeyHash: sha256(abi.encodePacked(_dummyPubkey, bytes16(0))), - withdrawalCreds: "", + pubkeyHash: validatorIndex.toPubkey().pubkeyHash(), + withdrawalCreds: new bytes(1), effectiveBalanceGwei: dummyBalanceGwei, activationEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, - exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH + exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, + pendingBalanceToWithdrawGwei: 0 }) ); _setCurrentBalance(validatorIndex, dummyBalanceGwei); @@ -538,20 +632,16 @@ contract BeaconChainMock is Logger { validatorIndex++; } - // Use pubkey format from `EigenPod._calculateValidatorPubkeyHash` - bytes memory _pubkey = new bytes(48); - assembly { - mstore(add(48, _pubkey), validatorIndex) - } validators.push( Validator({ isDummy: false, isSlashed: false, - pubkeyHash: sha256(abi.encodePacked(_pubkey, bytes16(0))), + pubkeyHash: validatorIndex.toPubkey().pubkeyHash(), withdrawalCreds: withdrawalCreds, effectiveBalanceGwei: balanceGwei, activationEpoch: currentEpoch(), - exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH + exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, + pendingBalanceToWithdrawGwei: 0 }) ); _setCurrentBalance(validatorIndex, balanceGwei); @@ -559,253 +649,7 @@ contract BeaconChainMock is Logger { cheats.resumeTracing(); return validatorIndex; - } - - struct Tree { - mapping(bytes32 => bytes32) siblings; - mapping(bytes32 => bytes32) parents; - } - - struct MerkleTrees { - Tree validatorTree; - Tree balancesTree; - Tree stateTree; - Tree blockTree; - } - - /// Timestamp -> merkle trees constructed at that timestamp - /// Used to generate proofs - mapping(uint64 => MerkleTrees) trees; - - /// @dev Builds a merkle tree using the given leaves and height - /// -- if the leaves given are not complete (i.e. the depth should have more leaves), - /// a pre-calculated zero-node is used to complete the tree. - /// -- each pair of nodes is stored in `siblings`, and their parent in `parents`. - /// These mappings are used to build proofs for any individual leaf - /// @return The root of the merkle tree - /// - /// HACK: this sibling/parent method of tree construction relies on all passed-in leaves - /// being unique, so that we don't overwrite siblings/parents. This is simple for trees - /// like the validator tree, as each leaf is a validator's unique validatorFields. - /// However, for the balances tree, the leaves may not be distinct. To get around this, - /// _createValidator adds "dummy" validators every 4 validators created, with a unique - /// balance value. This ensures each balance root is unique. - function _buildMerkleTree(bytes32[] memory leaves, uint treeHeight, Tree storage tree) internal returns (bytes32) { - for (uint depth = 0; depth < treeHeight; depth++) { - uint newLength = (leaves.length + 1) / 2; - bytes32[] memory newLeaves = new bytes32[](newLength); - - // Hash each pair of nodes in this level of the tree - for (uint i = 0; i < newLength; i++) { - uint leftIdx = 2 * i; - uint rightIdx = leftIdx + 1; - - // Get left leaf - bytes32 leftLeaf = leaves[leftIdx]; - - // Calculate right leaf - bytes32 rightLeaf; - if (rightIdx < leaves.length) rightLeaf = leaves[rightIdx]; - else rightLeaf = _getZeroNode(depth); - - // Hash left and right - bytes32 result = sha256(abi.encodePacked(leftLeaf, rightLeaf)); - newLeaves[i] = result; - - // Record results, used to generate individual proofs later: - // Record left and right as siblings - tree.siblings[leftLeaf] = rightLeaf; - tree.siblings[rightLeaf] = leftLeaf; - // Record the result as the parent of left and right - tree.parents[leftLeaf] = result; - tree.parents[rightLeaf] = result; - } - - // Move up one level - leaves = newLeaves; - } - - require(leaves.length == 1, "BeaconChainMock._buildMerkleTree: invalid tree somehow"); - return leaves[0]; - } - - function _genStateRootProof(bytes32 beaconStateRoot) internal { - bytes memory proof = new bytes(BLOCKROOT_PROOF_LEN); - bytes32 curNode = beaconStateRoot; - - uint depth = 0; - for (uint i = 0; i < BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; i++) { - bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].blockTree.parents[curNode]; - depth++; - } - - stateRootProofs[curTimestamp] = BeaconChainProofs.StateRootProof({beaconStateRoot: beaconStateRoot, proof: proof}); - } - - function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal virtual { - bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); - bytes32 curNode = balanceContainerRoot; - - uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; - uint depth = 0; - for (uint i = 0; i < BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; i++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - for (uint i = depth; i < totalHeight; i++) { - bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].blockTree.parents[curNode]; - depth++; - } - - balanceContainerProofs[curTimestamp] = - BeaconChainProofs.BalanceContainerProof({balanceContainerRoot: balanceContainerRoot, proof: proof}); - } - - function _genCredentialProofs() internal virtual { - mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp]; - - // Calculate credential proofs for each validator - for (uint i = 0; i < validators.length; i++) { - bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); - bytes32[] memory validatorFields = _getValidatorFields(uint40(i)); - bytes32 curNode = Merkle.merkleizeSha256(validatorFields); - - // Validator fields leaf -> validator container root - uint depth = 0; - for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].validatorTree.parents[curNode]; - depth++; - } - - // Validator container root -> beacon state root - for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - vfProofs[uint40(i)].validatorFields = validatorFields; - vfProofs[uint40(i)].validatorFieldsProof = proof; - } - } - - function _genBalanceProofs() internal { - mapping(uint40 => BalanceRootProof) storage brProofs = balanceRootProofs[curTimestamp]; - - // Calculate current balance proofs for each balance root - uint numBalanceRoots = _numBalanceRoots(); - for (uint i = 0; i < numBalanceRoots; i++) { - bytes memory proof = new bytes(BALANCE_PROOF_LEN); - bytes32 balanceRoot = balances[uint40(i)]; - bytes32 curNode = balanceRoot; - - // Balance root leaf -> balances container root - uint depth = 0; - for (uint j = 0; j < 1 + BeaconChainProofs.BALANCE_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].balancesTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].balancesTree.parents[curNode]; - depth++; - } - - brProofs[uint40(i)].balanceRoot = balanceRoot; - brProofs[uint40(i)].proof = proof; - } - } - - function _getValidatorLeaves() internal view returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](validators.length); - - // Place each validator's validatorFields into tree - for (uint i = 0; i < validators.length; i++) { - leaves[i] = Merkle.merkleizeSha256(_getValidatorFields(uint40(i))); - } - - return leaves; - } - - function _getBalanceLeaves() internal view returns (bytes32[] memory) { - // Place each validator's current balance into tree - bytes32[] memory leaves = new bytes32[](_numBalanceRoots()); - for (uint i = 0; i < leaves.length; i++) { - leaves[i] = balances[uint40(i)]; - } - - return leaves; - } - - function _numBalanceRoots() internal view returns (uint) { - // Each balance leaf is shared by 4 validators. This uses div_ceil - // to calculate the number of balance leaves - return (validators.length == 0) ? 0 : ((validators.length - 1) / 4) + 1; - } - - function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal view returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS); - - // Pre-populate leaves with dummy values so sibling/parent tracking is correct - for (uint i = 0; i < leaves.length; i++) { - leaves[i] = bytes32(i + 1); - } - - // Place validatorsRoot and balancesRoot into tree - leaves[BeaconChainProofs.VALIDATOR_CONTAINER_INDEX] = validatorsRoot; - leaves[BeaconChainProofs.BALANCE_CONTAINER_INDEX] = balancesRoot; - return leaves; - } - - function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal pure returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](BEACON_BLOCK_FIELDS); - - // Pre-populate leaves with dummy values so sibling/parent tracking is correct - for (uint i = 0; i < leaves.length; i++) { - leaves[i] = bytes32(i + 1); - } - - // Place beaconStateRoot into tree - leaves[BeaconChainProofs.STATE_ROOT_INDEX] = beaconStateRoot; - return leaves; - } + } function _currentBalanceGwei(uint40 validatorIndex) internal view returns (uint64) { return currentBalance(validatorIndex); @@ -848,20 +692,6 @@ contract BeaconChainMock is Logger { return validatorIndex / 4; } - function _getValidatorFields(uint40 validatorIndex) internal view returns (bytes32[] memory) { - bytes32[] memory vFields = new bytes32[](8); - Validator memory v = validators[validatorIndex]; - - vFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = v.pubkeyHash; - vFields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = bytes32(v.withdrawalCreds); - vFields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = _toLittleEndianUint64(v.effectiveBalanceGwei); - vFields[BeaconChainProofs.VALIDATOR_SLASHED_INDEX] = bytes32(abi.encode(v.isSlashed)); - vFields[BeaconChainProofs.VALIDATOR_ACTIVATION_EPOCH_INDEX] = _toLittleEndianUint64(v.activationEpoch); - vFields[BeaconChainProofs.VALIDATOR_EXIT_EPOCH_INDEX] = _toLittleEndianUint64(v.exitEpoch); - - return vFields; - } - /// @dev Update the validator's current balance function _setCurrentBalance(uint40 validatorIndex, uint64 newBalanceGwei) internal { bytes32 balanceRoot = balances[validatorIndex / 4]; @@ -883,30 +713,6 @@ contract BeaconChainMock is Logger { return (BeaconChainProofs.BALANCE_CONTAINER_INDEX << (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1)) | uint(balanceRootIndex); } - function _getZeroNode(uint depth) internal view returns (bytes32) { - require(depth < ZERO_NODES_LENGTH, "_getZeroNode: invalid depth"); - - return zeroNodes[depth]; - } - - /// @dev Opposite of Endian.fromLittleEndianUint64 - function _toLittleEndianUint64(uint64 num) internal pure returns (bytes32) { - uint lenum; - - // Rearrange the bytes from big-endian to little-endian format - lenum |= uint((num & 0xFF) << 56); - lenum |= uint((num & 0xFF00) << 40); - lenum |= uint((num & 0xFF0000) << 24); - lenum |= uint((num & 0xFF000000) << 8); - lenum |= uint((num & 0xFF00000000) >> 8); - lenum |= uint((num & 0xFF0000000000) >> 24); - lenum |= uint((num & 0xFF000000000000) >> 40); - lenum |= uint((num & 0xFF00000000000000) >> 56); - - // Shift the little-endian bytes to the end of the bytes32 value - return bytes32(lenum << 192); - } - /// @dev Opposite of BeaconChainProofs.getBalanceAtIndex, calculates a new balance /// root by updating the balance at validatorIndex /// @return The new, updated balance root @@ -917,22 +723,12 @@ contract BeaconChainMock is Logger { uint clearedRoot = uint(balanceRoot) & mask; // Convert validator balance to little endian and shift to correct position - uint leBalance = uint(_toLittleEndianUint64(newBalanceGwei)); + uint leBalance = uint(newBalanceGwei.toLittleEndianUint64()); uint shiftedBalance = leBalance >> (192 - bitShiftAmount); return bytes32(clearedRoot | shiftedBalance); } - /// @dev Helper to convert 32-byte withdrawal credentials to an address - function _toAddress(bytes memory withdrawalCreds) internal pure returns (address a) { - bytes32 creds = bytes32(withdrawalCreds); - uint160 mask = type(uint160).max; - - assembly { - a := and(creds, mask) - } - } - /** * * VIEW METHODS @@ -949,36 +745,42 @@ contract BeaconChainMock is Logger { ); } - CredentialProofs memory proofs = CredentialProofs({ + StateProofs storage p = proofs[curTimestamp]; + + CredentialProofs memory credentialProofs = CredentialProofs({ beaconTimestamp: curTimestamp, - stateRootProof: stateRootProofs[curTimestamp], + stateRootProof: p.stateRootProof, validatorFieldsProofs: new bytes[](_validators.length), validatorFields: new bytes32[][](_validators.length) }); // Get proofs for each validator for (uint i = 0; i < _validators.length; i++) { - ValidatorFieldsProof memory proof = validatorFieldsProofs[curTimestamp][_validators[i]]; - proofs.validatorFieldsProofs[i] = proof.validatorFieldsProof; - proofs.validatorFields[i] = proof.validatorFields; + ValidatorFieldsProof memory proof = p.validatorFieldsProofs[_validators[i]]; + + credentialProofs.validatorFieldsProofs[i] = proof.validatorFieldsProof; + credentialProofs.validatorFields[i] = proof.validatorFields; } - return proofs; + return credentialProofs; } function getCheckpointProofs(uint40[] memory _validators, uint64 timestamp) public view returns (CheckpointProofs memory) { // If we have not advanced an epoch since a validator was created, no proofs have been // generated for that validator. We check this here and revert early so we don't return // empty proofs. + require(timestamp <= curTimestamp, "BeaconChain.getCheckpointProofs: future timestamp provided (did you call advanceEpoch yet?)"); for (uint i = 0; i < _validators.length; i++) { require( _validators[i] <= lastIndexProcessed, - "BeaconChain.getCredentialProofs: no checkpoint proof found (did you call advanceEpoch yet?)" + "BeaconChain.getCheckpointProofs: no checkpoint proof found (did you call advanceEpoch yet?)" ); } - CheckpointProofs memory proofs = CheckpointProofs({ - balanceContainerProof: balanceContainerProofs[timestamp], + StateProofs storage p = proofs[timestamp]; + + CheckpointProofs memory checkpointProofs = CheckpointProofs({ + balanceContainerProof: p.balanceContainerProof, balanceProofs: new BeaconChainProofs.BalanceProof[](_validators.length) }); @@ -986,23 +788,30 @@ contract BeaconChainMock is Logger { for (uint i = 0; i < _validators.length; i++) { uint40 validatorIndex = _validators[i]; uint40 balanceRootIndex = _getBalanceRootIndex(validatorIndex); - BalanceRootProof memory proof = balanceRootProofs[timestamp][balanceRootIndex]; + BalanceRootProof memory proof = p.balanceRootProofs[balanceRootIndex]; - proofs.balanceProofs[i] = BeaconChainProofs.BalanceProof({ + checkpointProofs.balanceProofs[i] = BeaconChainProofs.BalanceProof({ pubkeyHash: validators[validatorIndex].pubkeyHash, balanceRoot: proof.balanceRoot, proof: proof.proof }); } - return proofs; + return checkpointProofs; } function getStaleBalanceProofs(uint40 validatorIndex) public view returns (StaleBalanceProofs memory) { - ValidatorFieldsProof memory vfProof = validatorFieldsProofs[curTimestamp][validatorIndex]; + // If we have not advanced an epoch since a validator was created, no proofs have been + // generated for that validator. We check this here and revert early so we don't return + // empty proofs. + require(validatorIndex <= lastIndexProcessed, "BeaconChain.getStaleBalanceProofs: no proof found (did you call advanceEpoch yet?)"); + + StateProofs storage p = proofs[curTimestamp]; + + ValidatorFieldsProof memory vfProof = p.validatorFieldsProofs[validatorIndex]; return StaleBalanceProofs({ beaconTimestamp: curTimestamp, - stateRootProof: stateRootProofs[curTimestamp], + stateRootProof: p.stateRootProof, validatorProof: BeaconChainProofs.ValidatorProof({validatorFields: vfProof.validatorFields, proof: vfProof.validatorFieldsProof}) }); } @@ -1011,16 +820,16 @@ contract BeaconChainMock is Logger { return validators[validatorIndex].effectiveBalanceGwei; } + function _getMaxEffectiveBalanceGwei(Validator storage v) internal virtual view returns (uint64) { + return v.hasCompoundingWC() ? MAX_EFFECTIVE_BALANCE_GWEI : MIN_ACTIVATION_BALANCE_GWEI; + } + function pubkeyHash(uint40 validatorIndex) public view returns (bytes32) { return validators[validatorIndex].pubkeyHash; } function pubkey(uint40 validatorIndex) public pure returns (bytes memory) { - bytes memory _pubkey = new bytes(48); - assembly { - mstore(add(48, _pubkey), validatorIndex) - } - return _pubkey; + return validatorIndex.toPubkey(); } function getPubkeyHashes(uint40[] memory _validators) public view returns (bytes32[] memory) { @@ -1034,6 +843,6 @@ contract BeaconChainMock is Logger { } function isActive(uint40 validatorIndex) public view returns (bool) { - return validators[validatorIndex].exitEpoch == BeaconChainProofs.FAR_FUTURE_EPOCH; + return validators[validatorIndex].isActiveAt(currentEpoch()); } } diff --git a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol index aeb47f6e03..1b90c8adab 100644 --- a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol +++ b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol @@ -2,11 +2,14 @@ pragma solidity ^0.8.27; import "src/test/integration/mocks/BeaconChainMock.t.sol"; +import "src/test/integration/mocks/LibProofGen.t.sol"; /// @notice A backwards-compatible BeaconChain Mock that updates containers & proofs from Deneb to Pectra contract BeaconChainMock_DenebForkable is BeaconChainMock { using StdStyle for *; using print for *; + using LibValidator for *; + using LibProofGen for *; // Denotes whether the beacon chain has been forked to Pectra bool isPectra; @@ -14,209 +17,28 @@ contract BeaconChainMock_DenebForkable is BeaconChainMock { // The timestamp of the Pectra hard fork uint64 public pectraForkTimestamp; - constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) BeaconChainMock(_eigenPodManager, _genesisTime) { - /// DENEB SPECIFIC CONSTANTS (PROOF LENGTHS, FIELD SIZES): - // see https://eth2book.info/capella/part3/containers/state/#beaconstate - BEACON_STATE_FIELDS = 32; - - VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); - - BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); - - MAX_EFFECTIVE_BALANCE_GWEI = 32 gwei; - MAX_EFFECTIVE_BALANCE_WEI = 32 ether; + constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) + BeaconChainMock(_eigenPodManager, _genesisTime) + { + LibProofGen.useDencun(); } function NAME() public pure override returns (string memory) { return "BeaconChain_PectraForkable"; } - /** - * - * INTERNAL FUNCTIONS - * - */ - function _advanceEpoch() public override { - cheats.pauseTracing(); - - // Update effective balances for each validator - for (uint i = 0; i < validators.length; i++) { - Validator storage v = validators[i]; - if (v.isDummy) continue; // don't process dummy validators - - // Get current balance and trim anything over MaxEB - uint64 balanceGwei = _currentBalanceGwei(uint40(i)); - if (balanceGwei > MAX_EFFECTIVE_BALANCE_GWEI) balanceGwei = MAX_EFFECTIVE_BALANCE_GWEI; - - v.effectiveBalanceGwei = balanceGwei; - } - - // console.log(" Updated effective balances...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - - uint64 curEpoch = currentEpoch(); - cheats.warp(_nextEpochStartTimestamp(curEpoch)); - curTimestamp = uint64(block.timestamp); - - // console.log(" Jumping to next epoch...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - - // console.log(" Building beacon state trees...".dim()); - - // Log total number of validators and number being processed for the first time - if (validators.length > 0) { - lastIndexProcessed = validators.length - 1; - } else { - // generate an empty root if we don't have any validators - EIP_4788_ORACLE.setBlockRoot(curTimestamp, keccak256("")); - - // console.log("-- no validators; added empty block root"); - return; - } - - // Build merkle tree for validators - bytes32 validatorsRoot = _buildMerkleTree({ - leaves: _getValidatorLeaves(), - treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1, - tree: trees[curTimestamp].validatorTree - }); - // console.log("-- validator container root", validatorsRoot); - - // Build merkle tree for current balances - bytes32 balanceContainerRoot = _buildMerkleTree({ - leaves: _getBalanceLeaves(), - treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1, - tree: trees[curTimestamp].balancesTree - }); - // console.log("-- balances container root", balanceContainerRoot); - - // Build merkle tree for BeaconState - bytes32 beaconStateRoot = _buildMerkleTree({ - leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), - treeHeight: getBeaconStateTreeHeight(), - tree: trees[curTimestamp].stateTree - }); - // console.log("-- beacon state root", beaconStateRoot); - - // Build merkle tree for BeaconBlock - bytes32 beaconBlockRoot = _buildMerkleTree({ - leaves: _getBeaconBlockLeaves(beaconStateRoot), - treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT, - tree: trees[curTimestamp].blockTree - }); - - // console.log("-- beacon block root", cheats.toString(beaconBlockRoot)); - - // Push new block root to oracle - EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot); - - // Pre-generate proofs to pass to EigenPod methods - _genStateRootProof(beaconStateRoot); - _genBalanceContainerProof(balanceContainerRoot); - _genCredentialProofs(); - _genBalanceProofs(); - - cheats.resumeTracing(); - } - - function _genCredentialProofs() internal override { - mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp]; - - // Calculate credential proofs for each validator - for (uint i = 0; i < validators.length; i++) { - bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); - bytes32[] memory validatorFields = _getValidatorFields(uint40(i)); - bytes32 curNode = Merkle.merkleizeSha256(validatorFields); - - // Validator fields leaf -> validator container root - uint depth = 0; - for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].validatorTree.parents[curNode]; - depth++; - } - - // Validator container root -> beacon state root - for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + getBeaconStateTreeHeight(); j++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - vfProofs[uint40(i)].validatorFields = validatorFields; - vfProofs[uint40(i)].validatorFieldsProof = proof; - } - } - - function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal override { - bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); - bytes32 curNode = balanceContainerRoot; - - uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; - uint depth = 0; - for (uint i = 0; i < getBeaconStateTreeHeight(); i++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - for (uint i = depth; i < totalHeight; i++) { - bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].blockTree.parents[curNode]; - depth++; - } - - balanceContainerProofs[curTimestamp] = - BeaconChainProofs.BalanceContainerProof({balanceContainerRoot: balanceContainerRoot, proof: proof}); + /// @dev Always return 32 ETH in gwei + function _getMaxEffectiveBalanceGwei(Validator storage v) internal override view returns (uint64) { + return isPectra ? super._getMaxEffectiveBalanceGwei(v) : MIN_ACTIVATION_BALANCE_GWEI; } /// @notice Forks the beacon chain to Pectra /// @dev Test battery should warp to the fork timestamp after calling this method function forkToPectra(uint64 _pectraForkTimestamp) public { - // https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate - BEACON_STATE_FIELDS = 37; - - VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - - MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; - MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; - isPectra = true; + LibProofGen.usePectra(); cheats.warp(_pectraForkTimestamp); pectraForkTimestamp = _pectraForkTimestamp; } - - function getBeaconStateTreeHeight() public view returns (uint) { - return isPectra ? BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT : BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT; - } } diff --git a/src/test/integration/mocks/EIP_7002_Mock.t.sol b/src/test/integration/mocks/EIP_7002_Mock.t.sol new file mode 100644 index 0000000000..65b8e29bb3 --- /dev/null +++ b/src/test/integration/mocks/EIP_7002_Mock.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; +struct WithdrawalRequest { + address source; + bytes validatorPubkey; + uint64 amountGwei; +} + +contract EIP_7002_Mock { + + address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + uint constant EXCESS_INHIBITOR = type(uint).max; + uint constant MIN_WITHDRAWAL_REQUEST_FEE = 1; + uint constant WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION = 17; + + uint MAX_WITHDRAWAL_REQUESTS_PER_BLOCK = 16; + uint TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK = 2; + + uint excessRequests; + uint withdrawalRequestCount; + + uint queueHead; + uint queueTail; + mapping(uint => WithdrawalRequest) requestQueue; + + fallback() external payable { + if (msg.sender == SYSTEM_ADDRESS) { + _doReadRequests(); + } else if (msg.data.length == 0) { + _doGetFee(); + } else if (msg.data.length == 56) { + _doAddRequest(); + } else { + revert("EIP_7002_Mock: unknown function"); + } + } + + /** + * + * "PUBLIC" METHODS + * + */ + + function _doAddRequest() internal { + uint fee = _getFee(); + require(msg.value >= fee, "EIP_7002_Mock: insuffient value for fee"); + + withdrawalRequestCount++; + + bytes memory pubkey = msg.data[0:48]; + uint64 amountGwei; + assembly { + calldatacopy(24, 48, 8) // copy uint64 amountGwei to memory[0] + amountGwei := mload(0) + } + + WithdrawalRequest storage request = requestQueue[queueTail]; + request.source = msg.sender; + request.validatorPubkey = pubkey; + request.amountGwei = amountGwei; + + queueTail++; + } + + function _doGetFee() internal view { + uint fee = _getFee(); + + assembly { + mstore(0, fee) + return(0, 32) + } + } + + /// (see https://eips.ethereum.org/EIPS/eip-7002#system-call) + function _doReadRequests() internal { + WithdrawalRequest[] memory reqs = _dequeueWithdrawalRequests(); + _updateExcessWithdrawalRequests(); + _resetWithdrawalRequestCount(); + + bytes memory returnData = abi.encode(reqs); + assembly { return(add(32, returnData), mload(returnData)) } + } + + /** + * + * PRIVATE METHODS + * + */ + + function _dequeueWithdrawalRequests() internal returns (WithdrawalRequest[] memory reqs) { + uint numInQueue = queueTail - queueHead; + uint numToDequeue = + numInQueue > MAX_WITHDRAWAL_REQUESTS_PER_BLOCK ? MAX_WITHDRAWAL_REQUESTS_PER_BLOCK : numInQueue; + + reqs = new WithdrawalRequest[](numToDequeue); + for (uint i = 0; i < numToDequeue; i++) { + reqs[i] = requestQueue[queueHead + i]; + } + + uint newQueueHead = queueHead + numToDequeue; + if (newQueueHead == queueTail) { + queueHead = 0; + queueTail = 0; + } else { + queueHead = newQueueHead; + } + + return reqs; + } + + function _updateExcessWithdrawalRequests() internal { + uint prevExcess = excessRequests; + if (prevExcess == EXCESS_INHIBITOR) { + prevExcess = 0; + } + + uint newExcess = 0; + if (prevExcess + withdrawalRequestCount > TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK) { + newExcess = prevExcess + withdrawalRequestCount - TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK; + } + + excessRequests = newExcess; + } + + function _resetWithdrawalRequestCount() internal { + withdrawalRequestCount = 0; + } + + function _getFee() private view returns (uint) { + require(excessRequests != EXCESS_INHIBITOR, "EIP_7002_Mock: excess inhibitor reached"); + + return _fakeExponential({ + factor: MIN_WITHDRAWAL_REQUEST_FEE, + numerator: excessRequests, + denominator: WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION + }); + } + + /// (see https://eips.ethereum.org/EIPS/eip-7002#fee-calculation) + function _fakeExponential(uint factor, uint numerator, uint denominator) private pure returns (uint) { + uint i = 1; + uint output = 0; + uint numeratorAccum = factor * denominator; + + while (numeratorAccum > 0) { + output += numeratorAccum; + numeratorAccum = (numeratorAccum * numerator) / (denominator * i); + i++; + } + + return output / denominator; + } +} diff --git a/src/test/integration/mocks/EIP_7251_Mock.t.sol b/src/test/integration/mocks/EIP_7251_Mock.t.sol new file mode 100644 index 0000000000..4c8e85e11d --- /dev/null +++ b/src/test/integration/mocks/EIP_7251_Mock.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +struct ConsolidationRequest { + address source; + bytes sourcePubkey; + bytes targetPubkey; +} + +contract EIP_7251_Mock { + + address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + uint constant EXCESS_INHIBITOR = type(uint).max; + uint constant MIN_CONSOLIDATION_REQUEST_FEE = 1; + uint constant CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION = 17; + + uint MAX_CONSOLIDATION_REQUESTS_PER_BLOCK = 2; + uint TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK = 1; + + uint excessRequests; + uint consolidationRequestCount; + + uint queueHead; + uint queueTail; + mapping(uint => ConsolidationRequest) requestQueue; + + fallback() external payable { + if (msg.sender == SYSTEM_ADDRESS) { + _doReadRequests(); + } else if (msg.data.length == 0) { + _doGetFee(); + } else if (msg.data.length == 96) { + _doAddRequest(); + } else { + revert("EIP_7251_Mock: unknown function"); + } + } + + /** + * + * "PUBLIC" METHODS + * + */ + + function _doAddRequest() internal { + uint fee = _getFee(); + require(msg.value >= fee, "EIP_7251_Mock: insuffient value for fee"); + + consolidationRequestCount++; + + bytes memory sourcePubkey = msg.data[0:48]; + bytes memory targetPubkey = msg.data[48:]; + + ConsolidationRequest storage request = requestQueue[queueTail]; + request.source = msg.sender; + request.sourcePubkey = sourcePubkey; + request.targetPubkey = targetPubkey; + + queueTail++; + } + + function _doGetFee() internal view { + uint fee = _getFee(); + + assembly { + mstore(0, fee) + return(0, 32) + } + } + + /// (see https://eips.ethereum.org/EIPS/eip-7251#system-call) + function _doReadRequests() internal { + ConsolidationRequest[] memory reqs = _dequeueConsolidationRequests(); + _updateExcessConsolidationRequests(); + _resetConsolidationRequestCount(); + + bytes memory returnData = abi.encode(reqs); + assembly { return(add(32, returnData), mload(returnData)) } + } + + /** + * + * PRIVATE METHODS + * + */ + + function _dequeueConsolidationRequests() internal returns (ConsolidationRequest[] memory reqs) { + uint numInQueue = queueTail - queueHead; + uint numToDequeue = + numInQueue > MAX_CONSOLIDATION_REQUESTS_PER_BLOCK ? MAX_CONSOLIDATION_REQUESTS_PER_BLOCK : numInQueue; + + reqs = new ConsolidationRequest[](numToDequeue); + for (uint i = 0; i < numToDequeue; i++) { + reqs[i] = requestQueue[queueHead + i]; + } + + uint newQueueHead = queueHead + numToDequeue; + if (newQueueHead == queueTail) { + queueHead = 0; + queueTail = 0; + } else { + queueHead = newQueueHead; + } + + return reqs; + } + + function _updateExcessConsolidationRequests() internal { + uint prevExcess = excessRequests; + if (prevExcess == EXCESS_INHIBITOR) { + prevExcess = 0; + } + + uint newExcess = 0; + if (prevExcess + consolidationRequestCount > TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK) { + newExcess = prevExcess + consolidationRequestCount - TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK; + } + + excessRequests = newExcess; + } + + function _resetConsolidationRequestCount() internal { + consolidationRequestCount = 0; + } + + function _getFee() private view returns (uint) { + require(excessRequests != EXCESS_INHIBITOR, "EIP_7251_Mock: excess inhibitor reached"); + + return _fakeExponential({ + factor: MIN_CONSOLIDATION_REQUEST_FEE, + numerator: excessRequests, + denominator: CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION + }); + } + + /// (see https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + function _fakeExponential(uint factor, uint numerator, uint denominator) private pure returns (uint) { + uint i = 1; + uint output = 0; + uint numeratorAccum = factor * denominator; + + while (numeratorAccum > 0) { + output += numeratorAccum; + numeratorAccum = (numeratorAccum * numerator) / (denominator * i); + i++; + } + + return output / denominator; + } +} diff --git a/src/test/integration/mocks/LibProofGen.t.sol b/src/test/integration/mocks/LibProofGen.t.sol new file mode 100644 index 0000000000..942aed858f --- /dev/null +++ b/src/test/integration/mocks/LibProofGen.t.sol @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; + +import "src/contracts/libraries/BeaconChainProofs.sol"; +import "src/contracts/libraries/Merkle.sol"; + +import "src/test/integration/mocks/LibValidator.t.sol"; + +struct ValidatorFieldsProof { + bytes32[] validatorFields; + bytes validatorFieldsProof; +} + +struct BalanceRootProof { + bytes32 balanceRoot; + bytes proof; +} + +struct Tree { + mapping(bytes32 => bytes32) siblings; + mapping(bytes32 => bytes32) parents; +} + +struct MerkleTrees { + Tree validatorTree; + Tree balancesTree; + Tree stateTree; + Tree blockTree; +} + +struct StateProofs { + MerkleTrees trees; + + BeaconChainProofs.StateRootProof stateRootProof; + BeaconChainProofs.BalanceContainerProof balanceContainerProof; + mapping(uint40 => ValidatorFieldsProof) validatorFieldsProofs; + mapping(uint40 => BalanceRootProof) balanceRootProofs; +} + +struct Config { + uint BEACON_STATE_TREE_HEIGHT; + uint BEACON_STATE_FIELDS; + uint BEACON_BLOCK_FIELDS; + uint BLOCKROOT_PROOF_LEN; + uint VAL_FIELDS_PROOF_LEN; + uint BALANCE_CONTAINER_PROOF_LEN; + uint BALANCE_PROOF_LEN; +} + +library LibProofGen { + + using LibProofGen for *; + using LibValidator for *; + + /** + * + * CONSTANTS AND CONFIG + * + */ + + // /// @dev Maximum height of a merkle tree used for proofs. We use the max height + // /// to pre-generate filler nodes so we don't need to store entire merkle trees in memory + // /// + // /// As of Pectra, the max tree height we need is BALANCE_TREE_HEIGHT + 1, which is 41. + // /// This constant is set to 50 on the off chance a future hard fork uses a different tree + // /// height and we forget to update this value. + // uint constant MAX_TREE_HEIGHT = 50; TODO + + bytes32 constant CONFIG_SLOT = keccak256("LibProofGen.config"); + + function config() internal view returns (Config storage) { + Config storage cfg; + bytes32 _CONFIG_SLOT = CONFIG_SLOT; + assembly { cfg.slot := _CONFIG_SLOT } + + return cfg; + } + + /// @dev Dencun-specific config + /// See (https://eth2book.info/capella/part3/containers/state/#beaconstate) + function useDencun() internal { + Config storage cfg = config(); + + cfg.BEACON_STATE_TREE_HEIGHT = BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT; + cfg.BEACON_STATE_FIELDS = 32; + cfg.BEACON_BLOCK_FIELDS = 5; + cfg.BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; + cfg.VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_CONTAINER_PROOF_LEN = 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + } + + /// @dev Pectra-specific config + /// See (see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate) + function usePectra() internal { + Config storage cfg = config(); + + cfg.BEACON_STATE_TREE_HEIGHT = BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; + cfg.BEACON_STATE_FIELDS = 37; + cfg.BEACON_BLOCK_FIELDS = 5; + cfg.BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; + cfg.VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_CONTAINER_PROOF_LEN = 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + } + + /** + * + * PROOFGEN: MAIN METHOD + * + */ + + /// @dev This method is how our mock beacon chain provides EigenPods with valid proofs of beacon state. + /// + /// INPUT: the beacon state for the current timestamp (validators and balances) + /// OUTPUT: the beacon block root calculated from the input. The mock beacon chain will inject this root + /// into the EIP-4788 beacon block root oracle, as EigenPods query this oracle during proof verification. + /// + /// Proofs against the beacon block root are also generated and stored in StateProofs; these proofs can + /// be fetched before calling an EigenPod method. + /// + /// This process is broken into 2 steps: + /// + /// 1. The merkle tree builder builds 4 merkle trees, one for each beacon state object we care about: + /// + /// beaconBlockRoot + /// | (beacon block tree) + /// beaconStateRoot + /// / \ (beacon state tree) + /// validatorContainerRoot, balanceContainerRoot + /// | | (balances tree) + /// | individual balances + /// | (validators tree) + /// individual validators + /// + /// Since the full beacon state is quite large, the merkle tree builder uses some hacks to generate + /// only as much of the tree as we need for our tests. See `build` for details. + /// + /// 2. Once the merkle trees are built, we pre-generate proofs for EigenPod methods. + function generate( + StateProofs storage p, + Validator[] memory validators, + mapping(uint40 => bytes32) storage balances + ) internal returns (bytes32 beaconBlockRoot) { + MerkleTrees storage trees = p.trees; + + // Build merkle tree for validators + bytes32 validatorsRoot = trees.validatorTree.build({ + leaves: _getValidatorLeaves(validators), + height: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1 + }); + + // Build merkle tree for current balances + bytes32[] memory balanceLeaves = _getBalanceLeaves(balances, validators.length); + bytes32 balanceContainerRoot = trees.balancesTree.build({ + leaves: balanceLeaves, + height: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1 + }); + + // Build merkle tree for BeaconState + bytes32 beaconStateRoot = trees.stateTree.build({ + leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), + height: config().BEACON_STATE_TREE_HEIGHT + }); + + // Build merkle tree for BeaconBlock + beaconBlockRoot = trees.blockTree.build({ + leaves: _getBeaconBlockLeaves(beaconStateRoot), + height: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + }); + + // Pre-generate proofs for EigenPod methods + p.genStateRootProof(beaconStateRoot); + p.genBalanceContainerProof(balanceContainerRoot); + p.genCredentialProofs(validators); + p.genBalanceProofs(balanceLeaves); + + return beaconBlockRoot; + } + + /** + * + * MERKLE TREE BUILDER + * + */ + + /// @dev Builds a merkle tree in storage using the given leaves and height + /// -- if the leaves given are not complete (i.e. the depth should have more leaves), + /// a pre-calculated zero-node is used to complete the tree. + /// -- each pair of nodes is stored in `siblings`, and their parent in `parents`. + /// These mappings are used to build proofs for any individual leaf + /// @return root the root of the merkle tree + /// + /// HACK: this sibling/parent method of tree construction relies on all passed-in leaves + /// being unique, so that we don't overwrite siblings/parents. This is simple for trees + /// like the validator tree, as each leaf is a validator's unique validatorFields. + /// However, for the balances tree, the leaves may not be distinct. To get around this, + /// BeaconChainMock._createValidator adds "dummy" validators every 4 validators created, + /// with a unique balance value. This ensures each balance root is unique. + function build(Tree storage tree, bytes32[] memory leaves, uint height) internal returns (bytes32 root) { + for (uint depth = 0; depth < height; depth++) { + uint newLength = (leaves.length + 1) / 2; + bytes32[] memory newLeaves = new bytes32[](newLength); + + // Hash each pair of nodes in this level of the tree + for (uint i = 0; i < newLength; i++) { + uint leftIdx = 2 * i; + uint rightIdx = leftIdx + 1; + + // Get left leaf + bytes32 leftLeaf = leaves[leftIdx]; + + // Calculate right leaf + bytes32 rightLeaf; + if (rightIdx < leaves.length) rightLeaf = leaves[rightIdx]; + else rightLeaf = _getFillerNode(depth); + + // Hash left and right + bytes32 result = sha256(abi.encodePacked(leftLeaf, rightLeaf)); + newLeaves[i] = result; + + // Record results, used to generate individual proofs later: + // Record left and right as siblings + tree.siblings[leftLeaf] = rightLeaf; + tree.siblings[rightLeaf] = leftLeaf; + // Record the result as the parent of left and right + tree.parents[leftLeaf] = result; + tree.parents[rightLeaf] = result; + } + + // Move up one level + leaves = newLeaves; + } + + require(leaves.length == 1, "LibProofGen.build: invalid tree somehow"); + return leaves[0]; + } + + /// @dev Fetch unique filler node for given depth + function _getFillerNode(uint depth) internal view returns (bytes32) { + bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); + + for (uint i = 1; i < depth; i++) { + curNode = sha256(abi.encodePacked(curNode, curNode)); + } + + return curNode; + } + + // /// @dev Pre-generate filler nodes used to fill in incomplete merkle trees at various depths + // function _calcZeroNodes() internal pure returns (bytes32[] memory) { + // // Calculate nodes of empty merkle tree + // bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); + // bytes32[] memory zeroNodes = new bytes32[](MAX_TREE_HEIGHT); + // zeroNodes[0] = curNode; + + // for (uint i = 1; i < zeroNodes.length; i++) { + // zeroNodes[i] = sha256(abi.encodePacked(curNode, curNode)); + // curNode = zeroNodes[i]; + // } + + // return zeroNodes; + // } TODO + + /** + * + * PROOF GENERATION + * + */ + + /// @dev Generate global proof of beaconStateRoot -> beaconBlockRoot + /// Used in verifyWithdrawalCredentials and verifyStaleBalance + function genStateRootProof(StateProofs storage p, bytes32 beaconStateRoot) internal { + bytes memory proof = new bytes(config().BLOCKROOT_PROOF_LEN); + bytes32 curNode = beaconStateRoot; + + uint depth = 0; + for (uint i = 0; i < BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; i++) { + bytes32 sibling = p.trees.blockTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, i))), sibling) + } + + curNode = p.trees.blockTree.parents[curNode]; + depth++; + } + + p.stateRootProof.beaconStateRoot = beaconStateRoot; + p.stateRootProof.proof = proof; + } + + /// @dev Generate global proof of balanceContainerRoot -> beaconStateRoot -> beaconBlockRoot + /// Used in verifyCheckpointProofs + function genBalanceContainerProof(StateProofs storage p, bytes32 balanceContainerRoot) internal { + bytes memory proof = new bytes(config().BALANCE_CONTAINER_PROOF_LEN); + bytes32 curNode = balanceContainerRoot; + + uint totalHeight = config().BALANCE_CONTAINER_PROOF_LEN / 32; + uint depth = 0; + for (uint i = 0; i < config().BEACON_STATE_TREE_HEIGHT; i++) { + bytes32 sibling = p.trees.stateTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, i))), sibling) + } + + curNode = p.trees.stateTree.parents[curNode]; + depth++; + } + + for (uint i = depth; i < totalHeight; i++) { + bytes32 sibling = p.trees.blockTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, i))), sibling) + } + + curNode = p.trees.blockTree.parents[curNode]; + depth++; + } + + p.balanceContainerProof.balanceContainerRoot = balanceContainerRoot; + p.balanceContainerProof.proof = proof; + } + + /// @dev Generate per-validator proofs of their validator -> validatorsRoot + /// Used in verifyWithdrawalCredentials and verifyStaleBalance + function genCredentialProofs(StateProofs storage p, Validator[] memory validators) internal { + mapping(uint40 => ValidatorFieldsProof) storage vfProofs = p.validatorFieldsProofs; + + // Calculate credential proofs for each validator + for (uint i = 0; i < validators.length; i++) { + bytes memory proof = new bytes(config().VAL_FIELDS_PROOF_LEN); + bytes32[] memory validatorFields = validators[i].getValidatorFields(); + bytes32 curNode = Merkle.merkleizeSha256(validatorFields); + + // Validator fields leaf -> validator container root + uint depth = 0; + for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { + bytes32 sibling = p.trees.validatorTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, j))), sibling) + } + + curNode = p.trees.validatorTree.parents[curNode]; + depth++; + } + + // Validator container root -> beacon state root + for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + config().BEACON_STATE_TREE_HEIGHT; j++) { + bytes32 sibling = p.trees.stateTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, j))), sibling) + } + + curNode = p.trees.stateTree.parents[curNode]; + depth++; + } + + vfProofs[uint40(i)].validatorFields = validatorFields; + vfProofs[uint40(i)].validatorFieldsProof = proof; + } + } + + /// @dev Generate per-balance root proofs of each balance root -> balanceContainerRoot + /// Used in verifyCheckpointProofs + function genBalanceProofs(StateProofs storage p, bytes32[] memory balanceLeaves) internal { + mapping(uint40 => BalanceRootProof) storage brProofs = p.balanceRootProofs; + + // Calculate current balance proofs for each balance root + for (uint i = 0; i < balanceLeaves.length; i++) { + bytes memory proof = new bytes(config().BALANCE_PROOF_LEN); + bytes32 balanceRoot = balanceLeaves[i]; + bytes32 curNode = balanceRoot; + + // Balance root leaf -> balances container root + uint depth = 0; + for (uint j = 0; j < 1 + BeaconChainProofs.BALANCE_TREE_HEIGHT; j++) { + bytes32 sibling = p.trees.balancesTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, j))), sibling) + } + + curNode = p.trees.balancesTree.parents[curNode]; + depth++; + } + + brProofs[uint40(i)].balanceRoot = balanceRoot; + brProofs[uint40(i)].proof = proof; + } + } + + /** + * + * MERKLE LEAVES GETTERS + * + */ + + /// @dev Get the leaves of the beacon block tree + function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal view returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](config().BEACON_BLOCK_FIELDS); + + // Pre-populate leaves with dummy values so sibling/parent tracking is correct + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = bytes32(i + 1); + } + + // Place beaconStateRoot into tree + leaves[BeaconChainProofs.STATE_ROOT_INDEX] = beaconStateRoot; + return leaves; + } + + /// @dev Get the leaves of the beacon state tree + function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal view returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](config().BEACON_STATE_FIELDS); + + // Pre-populate leaves with dummy values so sibling/parent tracking is correct + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = bytes32(i + 1); + } + + // Place validatorsRoot and balancesRoot into tree + leaves[BeaconChainProofs.VALIDATOR_CONTAINER_INDEX] = validatorsRoot; + leaves[BeaconChainProofs.BALANCE_CONTAINER_INDEX] = balancesRoot; + return leaves; + } + + /// @dev Get the leaves of the validators merkle tree + function _getValidatorLeaves(Validator[] memory validators) internal pure returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](validators.length); + + // Place each validator's validatorFields into tree + for (uint i = 0; i < validators.length; i++) { + leaves[i] = Merkle.merkleizeSha256(validators[i].getValidatorFields()); + } + + return leaves; + } + + /// @dev Get the leaves of the balances merkle tree + function _getBalanceLeaves(mapping(uint40 => bytes32) storage balances, uint numValidators) internal view returns (bytes32[] memory) { + // Each balance leaf is shared by 4 validators. This uses div_ceil + // to calculate the number of balance leaves + uint numBalanceRoots = numValidators == 0 ? 0 : ((numValidators - 1) / 4) + 1; + + // Place each validator's current balance into tree + bytes32[] memory leaves = new bytes32[](numBalanceRoots); + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = balances[uint40(i)]; + } + + return leaves; + } +} \ No newline at end of file diff --git a/src/test/integration/mocks/LibValidator.t.sol b/src/test/integration/mocks/LibValidator.t.sol new file mode 100644 index 0000000000..64d3d2a4c0 --- /dev/null +++ b/src/test/integration/mocks/LibValidator.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; + +import "src/contracts/libraries/BeaconChainProofs.sol"; + +struct Validator { + bool isDummy; + bool isSlashed; + bytes32 pubkeyHash; + bytes withdrawalCreds; + uint64 effectiveBalanceGwei; + uint64 activationEpoch; + uint64 exitEpoch; + + // cumulative unprocessed withdraw requests + uint64 pendingBalanceToWithdrawGwei; +} + +library LibValidator { + + /// @dev Generates a faux-pubkey from a uint40 validator index + function toPubkey(uint40 index) internal pure returns (bytes memory pubkey) { + pubkey = new bytes(48); + assembly { mstore(add(48, pubkey), index) } + } + + /// @dev Converts a validator pubkey to the index it uses in BeaconChainMock + /// NOTE this assumes a valid size pubkey + function toIndex(bytes memory pubkey) internal pure returns (uint40 validatorIndex) { + assembly { validatorIndex := mload(add(48, pubkey)) } + } + + /// @dev Computes a pubkey hash from a validator's pubkey + /// NOTE this assumes a valid size pubkey + function pubkeyHash(bytes memory pubkey) internal pure returns (bytes32) { + return sha256(abi.encodePacked(pubkey, bytes16(0))); + } + + function getValidatorFields(Validator memory self) internal pure returns (bytes32[] memory fields) { + fields = new bytes32[](8); + + fields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = self.pubkeyHash; + fields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = bytes32(self.withdrawalCreds); + fields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = toLittleEndianUint64(self.effectiveBalanceGwei); + fields[BeaconChainProofs.VALIDATOR_SLASHED_INDEX] = bytes32(abi.encode(self.isSlashed)); + fields[BeaconChainProofs.VALIDATOR_ACTIVATION_EPOCH_INDEX] = toLittleEndianUint64(self.activationEpoch); + fields[BeaconChainProofs.VALIDATOR_EXIT_EPOCH_INDEX] = toLittleEndianUint64(self.exitEpoch); + } + + /// @dev Returns whether the validator is considered "active" at the given epoch + function isActiveAt(Validator memory self, uint64 epoch) internal pure returns (bool) { + return self.activationEpoch <= epoch && epoch < self.exitEpoch; + } + + /// @dev Returns true if the validator has initiated exit + function isExiting(Validator memory self) internal pure returns (bool) { + return self.exitEpoch != BeaconChainProofs.FAR_FUTURE_EPOCH; + } + + /// @dev Returns the withdrawal address of the validator + /// NOTE this assumes the validator has 0x01 or 0x02 withdrawal credentials + function withdrawalAddress(Validator memory self) internal pure returns (address addr) { + bytes32 creds = bytes32(self.withdrawalCreds); + uint160 mask = type(uint160).max; + + assembly { addr := and(creds, mask) } + } + + /// @dev Returns true if the validator does not have 0x01/0x02 withdrawal credentials + function hasBLSWC(Validator memory self) internal pure returns (bool) { + return self.withdrawalCreds[0] != 0x01 && self.withdrawalCreds[0] != 0x02; + } + + /// @dev Returns true IFF the validator has 0x02 withdrawal credentials + function hasCompoundingWC(Validator memory self) internal pure returns (bool) { + return self.withdrawalCreds[0] == 0x02; + } + + /// @dev Opposite of Endian.fromLittleEndianUint64 + function toLittleEndianUint64(uint64 num) internal pure returns (bytes32) { + uint lenum; + + // Rearrange the bytes from big-endian to little-endian format + lenum |= uint((num & 0xFF) << 56); + lenum |= uint((num & 0xFF00) << 40); + lenum |= uint((num & 0xFF0000) << 24); + lenum |= uint((num & 0xFF000000) << 8); + lenum |= uint((num & 0xFF00000000) >> 8); + lenum |= uint((num & 0xFF0000000000) >> 24); + lenum |= uint((num & 0xFF000000000000) >> 40); + lenum |= uint((num & 0xFF00000000000000) >> 56); + + // Shift the little-endian bytes to the end of the bytes32 value + return bytes32(lenum << 192); + } +} \ No newline at end of file diff --git a/src/test/integration/tests/eigenpod/Pectra_Features.t.sol b/src/test/integration/tests/eigenpod/Pectra_Features.t.sol new file mode 100644 index 0000000000..0de11737de --- /dev/null +++ b/src/test/integration/tests/eigenpod/Pectra_Features.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/IntegrationChecks.t.sol"; + +contract Integration_Pectra_Features_Base is IntegrationCheckUtils { + using ArrayLib for *; + + function _init() internal virtual override { + _configAssetTypes(HOLDS_ETH); + // (staker, strategies, initTokenBalances) = _newRandomStaker(); + // cheats.assume(initTokenBalances[0] >= 64 ether); + + // // Deposit staker + // uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances); + // staker.depositIntoEigenlayer(strategies, initTokenBalances); + // check_Deposit_State(staker, strategies, shares); + // initDepositShares = shares; + // validators = staker.getActiveValidators(); + + // // Slash all validators fully + // slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full); + // beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod + } + + function testFuzz_consolidate(uint24 _r) public rand(_r) { + User staker = _newEmptyStaker(); + + // Deal ETH and start 2 validators; both ETH1 + (uint40[] memory validators, uint balanceWei,) = staker.startValidators(2); + + staker.verifyWithdrawalCredentials(validators); + } + + function _zip(uint40[] memory a1, uint40[] memory a2) internal pure returns (uint40[] memory) { + uint40[] memory result = new uint40[](a1.length + a2.length); + + uint resultIdx; + for (uint i = 0; i < a1.length; i++) { + result[resultIdx] = a1[i]; + resultIdx++; + } + + for (uint i = 0; i < a2.length; i++) { + result[resultIdx] = a2[i]; + resultIdx++; + } + + return result; + } +} + +// contract Integration_FullySlashedEigenpod_Checkpointed is Integration_FullySlashedEigenpod_Base { +// function _init() internal override { +// super._init(); + +// // // Start & complete a checkpoint +// // staker.startCheckpoint(); +// // check_StartCheckpoint_WithPodBalance_State(staker, 0); +// // staker.completeCheckpoint(); +// // check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei); +// } + +// function testFuzz_fullSlash_registerStakerAsOperator_Revert_Redeposit(uint24 _rand) public rand(_rand) { +// // // Register staker as operator +// // staker.registerAsOperator(); + +// // // Start a new validator & verify withdrawal credentials +// // cheats.deal(address(staker), 32 ether); +// // (uint40[] memory newValidators,,) = staker.startValidators(); +// // beaconChain.advanceEpoch_NoRewards(); + +// // // We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0 +// // cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector); +// // staker.verifyWithdrawalCredentials(newValidators); +// } + +// function testFuzz_fullSlash_registerStakerAsOperator_delegate_undelegate_completeAsShares(uint24 _rand) public rand(_rand) { +// // // Register staker as operator +// // staker.registerAsOperator(); +// // User operator = User(payable(address(staker))); + +// // // Initialize new staker +// // (User staker2, IStrategy[] memory strategies2, uint[] memory initTokenBalances2) = _newRandomStaker(); +// // uint[] memory shares = _calculateExpectedShares(strategies2, initTokenBalances2); +// // staker2.depositIntoEigenlayer(strategies2, initTokenBalances2); +// // check_Deposit_State(staker2, strategies2, shares); + +// // // Delegate to an operator who has now become a staker, this should succeed as slashed operator's BCSF should not affect the staker +// // staker2.delegateTo(operator); +// // check_Delegation_State(staker2, operator, strategies2, shares); + +// // // Register as operator and undelegate - the equivalent of redelegating to yourself +// // Withdrawal[] memory withdrawals = staker2.undelegate(); +// // bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); +// // check_Undelegate_State(staker2, operator, withdrawals, withdrawalRoots, strategies2, shares); + +// // // Complete withdrawals as shares +// // _rollBlocksForCompleteWithdrawals(withdrawals); +// // for (uint i = 0; i < withdrawals.length; i++) { +// // staker2.completeWithdrawalAsShares(withdrawals[i]); +// // check_Withdrawal_AsShares_Undelegated_State(staker2, operator, withdrawals[i], strategies2, shares); +// // } +// } +// } \ No newline at end of file diff --git a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol index 0dad998bee..c4037ad8f5 100644 --- a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol +++ b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol @@ -24,7 +24,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { EigenPod pod = staker.pod(); CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators); - cheats.startPrank(address(staker)); + cheats.prank(address(staker)); cheats.resumeGasMetering(); uint startGas = gasleft(); @@ -51,6 +51,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { CheckpointProofs memory cpProofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp()); + cheats.prank(address(staker)); cheats.resumeGasMetering(); startGas = gasleft(); pod.verifyCheckpointProofs({balanceContainerProof: cpProofs.balanceContainerProof, proofs: cpProofs.balanceProofs}); @@ -517,22 +518,24 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { (uint40[] memory validators, uint64 beaconBalanceGwei, uint maxEBValidators) = staker.startValidators(); // Advance epoch and generate consensus rewards, but don't withdraw to pod beaconChain.advanceEpoch_NoWithdraw(); - - // Get the expected increase in beacon balance, accounting for validators with MaxEB - // The validators < MaxEB will have their rewards proven in the checkpoint - // The validators == MaxEB will have their rewards proven in verifyWC - uint64 beaconBalanceIncreaseGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); - uint64 expectedWithdrawnGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); - uint64 verifyWCRewardsIncreaseGwei = beaconBalanceIncreaseGwei - expectedWithdrawnGwei; + + /// "Expected effective balance increase gwei": + // For verifyWithdrawalCredentials, we expect beacon chain rewards to be reflected in effective balance + // for any validator NOT at max effective balance. + uint64 expectedEBIncreaseGwei = uint64((validators.length - maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI()); + /// "Expected current balance increase gwei": + // For checkpointing, we expect additional beacon chain rewards to be reflected in current balance + // for any validator at max effective balance + uint64 expectedCBIncreaseGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); staker.verifyWithdrawalCredentials(validators); - check_VerifyWC_State(staker, validators, beaconBalanceGwei + verifyWCRewardsIncreaseGwei); + check_VerifyWC_State(staker, validators, beaconBalanceGwei + expectedEBIncreaseGwei); staker.startCheckpoint(); check_StartCheckpoint_State(staker); staker.completeCheckpoint(); - check_CompleteCheckpoint_EarnOnBeacon_State(staker, expectedWithdrawnGwei); + check_CompleteCheckpoint_EarnOnBeacon_State(staker, expectedCBIncreaseGwei); } /// 1. Verify validators' withdrawal credentials diff --git a/src/test/integration/users/User.t.sol b/src/test/integration/users/User.t.sol index f97904acfc..c4f29c6a09 100644 --- a/src/test/integration/users/User.t.sol +++ b/src/test/integration/users/User.t.sol @@ -15,10 +15,6 @@ import "src/test/integration/mocks/BeaconChainMock.t.sol"; import "src/test/utils/Logger.t.sol"; import "src/test/utils/ArrayLib.sol"; -struct Validator { - uint40 index; -} - interface IUserDeployer { function allocationManager() external view returns (AllocationManager); function delegationManager() external view returns (DelegationManager); @@ -337,11 +333,10 @@ contract User is Logger, IDelegationManagerTypes, IAllocationManagerTypes { /// @return The amount of wei sent to the beacon chain function startValidators(uint8 numValidators) public virtual createSnapshot returns (uint40[] memory, uint64, uint) { require(numValidators > 0 && numValidators <= 10, "startValidators: numValidators must be between 1 and 10"); + + // Deal ETH for the new validators uint balanceWei = address(this).balance; - - // given a number of validators, the current balance, calculate the amount of ETH needed to start that many validators - uint ethNeeded = numValidators * 32 ether - balanceWei; - cheats.deal(address(this), ethNeeded); + cheats.deal(address(this), balanceWei + (numValidators * 32 ether)); print.method("startValidators"); return _startValidators(); @@ -517,13 +512,19 @@ contract User is Logger, IDelegationManagerTypes, IAllocationManagerTypes { else break; } - // Track validators with maximum effective balance - if (validatorEth == 2048 ether) maxEBValidators++; - // Create the validator bytes memory withdrawalCredentials = validatorEth == 32 ether ? _podWithdrawalCredentials() : _podCompoundingWithdrawalCredentials(); + // Track validators with max effective balance + // - For 0x01 validators, this is 32 ETH + // - For 0x02 validators, this is 2048 ETH + if (withdrawalCredentials[0] == 0x01 && validatorEth == 32 ether) { + maxEBValidators++; + } else if (withdrawalCredentials[0] == 0x02 && validatorEth == 2048 ether) { + maxEBValidators++; + } + uint40 validatorIndex = beaconChain.newValidator{value: validatorEth}(withdrawalCredentials); newValidators[numValidators] = validatorIndex; diff --git a/src/test/mocks/EigenPodMock.sol b/src/test/mocks/EigenPodMock.sol index 42fdb4d526..89b08fa63a 100644 --- a/src/test/mocks/EigenPodMock.sol +++ b/src/test/mocks/EigenPodMock.sol @@ -8,8 +8,6 @@ import "../../contracts/mixins/SemVerMixin.sol"; contract EigenPodMock is IEigenPod, SemVerMixin, Test { constructor() SemVerMixin("v9.9.9") {} - function nonBeaconChainETHBalanceWei() external view returns (uint) {} - /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer), function withdrawableRestakedExecutionLayerGwei() external view returns (uint64) {} @@ -33,12 +31,6 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { /// @notice The owner of this EigenPod function podOwner() external view returns (address) {} - /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. - function hasRestaked() external view returns (bool) {} - - /// @notice block timestamp of the most recent withdrawal - function mostRecentWithdrawalTimestamp() external view returns (uint64) {} - /// @notice Returns the validatorInfo struct for the provided pubkeyHash function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) {} @@ -80,14 +72,8 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { bytes32[][] calldata validatorFields ) external {} - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false - function activateRestaking() external {} - - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false - function withdrawBeforeRestaking() external {} - - /// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei - function withdrawNonBeaconChainETHBalanceWei(address recipient, uint amountToWithdraw) external {} + function requestConsolidation(ConsolidationRequest[] calldata) external payable {} + function requestWithdrawal(WithdrawalRequest[] calldata) external payable {} /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint[] memory amountsToWithdraw, address recipient) external {} @@ -106,4 +92,7 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { function getParentBlockRoot(uint64 timestamp) external view returns (bytes32) {} function getPectraForkTimestamp() external view returns (uint64) {} + + function getConsolidationRequestFee() external view returns (uint) {} + function getWithdrawalRequestFee() external view returns (uint) {} } diff --git a/src/test/utils/EigenPodUser.t.sol b/src/test/utils/EigenPodUser.t.sol index 089ff3ac67..420b74333f 100644 --- a/src/test/utils/EigenPodUser.t.sol +++ b/src/test/utils/EigenPodUser.t.sol @@ -12,10 +12,6 @@ import "src/test/integration/TimeMachine.t.sol"; import "src/test/integration/mocks/BeaconChainMock.t.sol"; import "src/test/utils/Logger.t.sol"; -struct Validator { - uint40 index; -} - interface IUserDeployer { function timeMachine() external view returns (TimeMachine); function beaconChain() external view returns (BeaconChainMock);