diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index c02554e60..d5e2c4f6f 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -142,19 +142,19 @@ interface IEigenPod { /** * @notice This function records a full withdrawal on behalf of one of the Ethereum validators for this EigenPod * @param withdrawalProofs is the information needed to check the veracity of the block number and withdrawal being proven - * @param validatorFieldsProof is the proof of the validator's fields in the validator tree + * @param validatorFieldsProofs is the proof of the validator's fields in the validator tree * @param withdrawalFields are the fields of the withdrawal being proven * @param validatorFields are the fields of the validator being proven * @param beaconChainETHStrategyIndex is the index of the beaconChainETHStrategy for the pod owner for the callback to * the EigenPodManager to the StrategyManager in case it must be removed from the podOwner's list of strategies */ function verifyAndProcessWithdrawal( - BeaconChainProofs.WithdrawalProofs calldata withdrawalProofs, - bytes calldata validatorFieldsProof, - bytes32[] calldata validatorFields, - bytes32[] calldata withdrawalFields, + BeaconChainProofs.WithdrawalProofs[] calldata withdrawalProofs, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields, + bytes32[][] calldata withdrawalFields, uint256 beaconChainETHStrategyIndex, - uint64 oracleBlockNumber + uint64 oracleTimestamp ) external; /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 2674a63fa..30672b91e 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -299,7 +299,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen /** * @notice This function records a full withdrawal on behalf of one of the Ethereum validators for this EigenPod * @param withdrawalProofs is the information needed to check the veracity of the block number and withdrawal being proven - * @param validatorFieldsProof is the information needed to check the veracity of the validator fields being proven + * @param validatorFieldsProofs is the information needed to check the veracity of the validator fields being proven * @param withdrawalFields are the fields of the withdrawal being proven * @param validatorFields are the fields of the validator being proven * @param beaconChainETHStrategyIndex is the index of the beaconChainETHStrategy for the pod owner for the callback to @@ -307,66 +307,25 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen * @param oracleTimestamp is the Beacon Chain blockNumber whose state root the `proof` will be proven against. */ function verifyAndProcessWithdrawal( - BeaconChainProofs.WithdrawalProofs calldata withdrawalProofs, - bytes calldata validatorFieldsProof, - bytes32[] calldata validatorFields, - bytes32[] calldata withdrawalFields, + BeaconChainProofs.WithdrawalProofs[] calldata withdrawalProofs, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields, + bytes32[][] calldata withdrawalFields, uint256 beaconChainETHStrategyIndex, uint64 oracleTimestamp ) external onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_WITHDRAWAL) onlyNotFrozen - /** - * Check that the provided block number being proven against is after the `mostRecentWithdrawalTimestamp`. - * Without this check, there is an edge case where a user proves a past withdrawal for a validator whose funds they already withdrew, - * as a way to "withdraw the same funds twice" without providing adequate proof. - * Note that this check is not made using the oracleTimestamp as in the `verifyWithdrawalCredentials` proof; instead this proof - * proof is made for the block number of the withdrawal, which may be within 8192 slots of the oracleTimestamp. - * This difference in modifier usage is OK, since it is still not possible to `verifyAndProcessWithdrawal` against a slot that occurred - * *prior* to the proof provided in the `verifyWithdrawalCredentials` function. - */ - proofIsForValidTimestamp(Endian.fromLittleEndianUint64(withdrawalProofs.timestampRoot)) { - /** - * If the validator status is inactive, then withdrawal credentials were never verified for the validator, - * and thus we cannot know that the validator is related to this EigenPod at all! - */ - uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); - - bytes32 validatorPubkeyHash = validatorFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX]; - - require(_validatorPubkeyHashToInfo[validatorPubkeyHash].status != VALIDATOR_STATUS.INACTIVE, - "EigenPod.verifyAndProcessWithdrawal: Validator never proven to have withdrawal credentials pointed to this contract"); - require(!provenWithdrawal[validatorPubkeyHash][Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot)], - "EigenPod.verifyAndProcessWithdrawal: withdrawal has already been proven for this slot"); - - { - // verify that the provided state root is verified against the oracle-provided latest block header - BeaconChainProofs.verifyStateRootAgainstLatestBlockHeaderRoot(withdrawalProofs.beaconStateRoot, eigenPodManager.getBeaconChainStateRoot(oracleTimestamp), withdrawalProofs.latestBlockHeaderProof); - - // Verifying the withdrawal as well as the slot - BeaconChainProofs.verifyWithdrawalProofs(withdrawalProofs.beaconStateRoot, withdrawalProofs, withdrawalFields); - // Verifying the validator fields, specifically the withdrawable epoch - BeaconChainProofs.verifyValidatorFields(validatorIndex, withdrawalProofs.beaconStateRoot, validatorFieldsProof, validatorFields); - } - - { - uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); - - //check if the withdrawal occured after mostRecentWithdrawalTimestamp - uint64 slot = Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot); + require( + (validatorFields.length == validatorFieldsProofs.length) && + (validatorFieldsProofs.length == withdrawalProofs.length) && + (withdrawalProofs.length == withdrawalFields.length), "EigenPod.verifyAndProcessWithdrawal: inputs must be same length" + ); - /** - * if the validator's withdrawable epoch is less than or equal to the slot's epoch, then the validator has fully withdrawn because - * a full withdrawal is only processable after the withdrawable epoch has passed. - */ - // reference: uint64 withdrawableEpoch = Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); - if (Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]) <= slot/BeaconChainProofs.SLOTS_PER_EPOCH) { - _processFullWithdrawal(withdrawalAmountGwei, validatorIndex, validatorPubkeyHash, beaconChainETHStrategyIndex, podOwner, _validatorPubkeyHashToInfo[validatorPubkeyHash].status, slot); - } else { - _processPartialWithdrawal(slot, withdrawalAmountGwei, validatorIndex, validatorPubkeyHash, podOwner); - } + for (uint256 i = 0; i < withdrawalFields.length; i++) { + _verifyAndProcessWithdrawal(withdrawalProofs[i], validatorFieldsProofs[i], validatorFields[i], withdrawalFields[i], beaconChainETHStrategyIndex, oracleTimestamp); } } @@ -442,6 +401,68 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen eigenPodManager.restakeBeaconChainETH(podOwner, validatorInfo.restakedBalanceGwei * GWEI_TO_WEI); } + function _verifyAndProcessWithdrawal( + BeaconChainProofs.WithdrawalProofs calldata withdrawalProofs, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields, + bytes32[] calldata withdrawalFields, + uint256 beaconChainETHStrategyIndex, + uint64 oracleTimestamp + ) + internal + /** + * Check that the provided block number being proven against is after the `mostRecentWithdrawalTimestamp`. + * Without this check, there is an edge case where a user proves a past withdrawal for a validator whose funds they already withdrew, + * as a way to "withdraw the same funds twice" without providing adequate proof. + * Note that this check is not made using the oracleTimestamp as in the `verifyWithdrawalCredentials` proof; instead this proof + * proof is made for the block number of the withdrawal, which may be within 8192 slots of the oracleTimestamp. + * This difference in modifier usage is OK, since it is still not possible to `verifyAndProcessWithdrawal` against a slot that occurred + * *prior* to the proof provided in the `verifyWithdrawalCredentials` function. + */ + proofIsForValidTimestamp(Endian.fromLittleEndianUint64(withdrawalProofs.timestampRoot)) + { + /** + * If the validator status is inactive, then withdrawal credentials were never verified for the validator, + * and thus we cannot know that the validator is related to this EigenPod at all! + */ + uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); + + bytes32 validatorPubkeyHash = validatorFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX]; + + require(_validatorPubkeyHashToInfo[validatorPubkeyHash].status != VALIDATOR_STATUS.INACTIVE, + "EigenPod.verifyAndProcessWithdrawal: Validator never proven to have withdrawal credentials pointed to this contract"); + require(!provenWithdrawal[validatorPubkeyHash][Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot)], + "EigenPod.verifyAndProcessWithdrawal: withdrawal has already been proven for this slot"); + + { + // verify that the provided state root is verified against the oracle-provided latest block header + BeaconChainProofs.verifyStateRootAgainstLatestBlockHeaderRoot(withdrawalProofs.beaconStateRoot, eigenPodManager.getBeaconChainStateRoot(oracleTimestamp), withdrawalProofs.latestBlockHeaderProof); + + // Verifying the withdrawal as well as the slot + BeaconChainProofs.verifyWithdrawalProofs(withdrawalProofs.beaconStateRoot, withdrawalProofs, withdrawalFields); + // Verifying the validator fields, specifically the withdrawable epoch + BeaconChainProofs.verifyValidatorFields(validatorIndex, withdrawalProofs.beaconStateRoot, validatorFieldsProof, validatorFields); + } + + { + uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); + + //check if the withdrawal occured after mostRecentWithdrawalTimestamp + uint64 slot = Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot); + + /** + * if the validator's withdrawable epoch is less than or equal to the slot's epoch, then the validator has fully withdrawn because + * a full withdrawal is only processable after the withdrawable epoch has passed. + */ + // reference: uint64 withdrawableEpoch = Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); + if (Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]) <= slot/BeaconChainProofs.SLOTS_PER_EPOCH) { + _processFullWithdrawal(withdrawalAmountGwei, validatorIndex, validatorPubkeyHash, beaconChainETHStrategyIndex, podOwner, _validatorPubkeyHashToInfo[validatorPubkeyHash].status, slot); + } else { + _processPartialWithdrawal(slot, withdrawalAmountGwei, validatorIndex, validatorPubkeyHash, podOwner); + } + } + } + function _processFullWithdrawal( uint64 withdrawalAmountGwei, uint40 validatorIndex, diff --git a/src/test/EigenPod.t.sol b/src/test/EigenPod.t.sol index 033169bba..307929f12 100644 --- a/src/test/EigenPod.t.sol +++ b/src/test/EigenPod.t.sol @@ -330,27 +330,34 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); IEigenPod newPod = eigenPodManager.getPod(podOwner); - - // ./solidityProofGen "WithdrawalFieldsProof" 61336 2262 "data/slot_43222/oracle_capella_beacon_state_43300.ssz" "data/slot_43222/capella_block_header_43222.json" "data/slot_43222/capella_block_43222.json" fullWithdrawalProof.json setJSON("./src/test/test-data/fullWithdrawalProof.json"); - BeaconChainProofs.WithdrawalProofs memory withdrawalProofs = _getWithdrawalProof(); - bytes memory validatorFieldsProof = abi.encodePacked(getValidatorProof()); - withdrawalFields = getWithdrawalFields(); - validatorFields = getValidatorFields(); - bytes32 newLatestBlockHeaderRoot = getLatestBlockHeaderRoot(); - BeaconChainOracleMock(address(beaconChainOracle)).setBeaconChainStateRoot(newLatestBlockHeaderRoot); - + BeaconChainOracleMock(address(beaconChainOracle)).setBeaconChainStateRoot(getLatestBlockHeaderRoot()); uint64 restakedExecutionLayerGweiBefore = newPod.withdrawableRestakedExecutionLayerGwei(); + + withdrawalFields = getWithdrawalFields(); uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); + uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - newPod.REQUIRED_BALANCE_GWEI()) * uint64(GWEI_TO_WEI); uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); cheats.deal(address(newPod), leftOverBalanceWEI); uint256 delayedWithdrawalRouterContractBalanceBefore = address(delayedWithdrawalRouter).balance; - //cheats.expectEmit(true, true, true, true, address(newPod)); - emit FullWithdrawalRedeemed(validatorIndex, podOwner, withdrawalAmountGwei); - newPod.verifyAndProcessWithdrawal(withdrawalProofs, validatorFieldsProof, validatorFields, withdrawalFields, 0, 0); + { + BeaconChainProofs.WithdrawalProofs[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProofs[](1); + withdrawalProofsArray[0] = _getWithdrawalProof(); + bytes[] memory validatorFieldsProofArray = new bytes[](1); + validatorFieldsProofArray[0] = abi.encodePacked(getValidatorProof()); + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + validatorFieldsArray[0] = getValidatorFields(); + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + withdrawalFieldsArray[0] = withdrawalFields; + + + //cheats.expectEmit(true, true, true, true, address(newPod)); + emit FullWithdrawalRedeemed(validatorIndex, podOwner, withdrawalAmountGwei); + newPod.verifyAndProcessWithdrawal(withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray, 0, 0); + } require(newPod.withdrawableRestakedExecutionLayerGwei() - restakedExecutionLayerGweiBefore == newPod.REQUIRED_BALANCE_GWEI(), "restakedExecutionLayerGwei has not been incremented correctly"); require(address(delayedWithdrawalRouter).balance - delayedWithdrawalRouterContractBalanceBefore == leftOverBalanceWEI, @@ -371,33 +378,40 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { setJSON("./src/test/test-data/withdrawalCredentialAndBalanceProof_61068.json"); _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); IEigenPod newPod = eigenPodManager.getPod(podOwner); + //generate partialWithdrawalProofs.json with: // ./solidityProofGen "WithdrawalFieldsProof" 61068 656 "data/slot_58000/oracle_capella_beacon_state_58100.ssz" "data/slot_58000/capella_block_header_58000.json" "data/slot_58000/capella_block_58000.json" "partialWithdrawalProof.json" setJSON("./src/test/test-data/partialWithdrawalProof.json"); + withdrawalFields = getWithdrawalFields(); + validatorFields = getValidatorFields(); BeaconChainProofs.WithdrawalProofs memory withdrawalProofs = _getWithdrawalProof(); bytes memory validatorFieldsProof = abi.encodePacked(getValidatorProof()); - withdrawalFields = getWithdrawalFields(); - validatorFields = getValidatorFields(); - bytes32 newLatestBlocHeaderRoot = getLatestBlockHeaderRoot(); - BeaconChainOracleMock(address(beaconChainOracle)).setBeaconChainStateRoot(newLatestBlocHeaderRoot); - + BeaconChainOracleMock(address(beaconChainOracle)).setBeaconChainStateRoot(getLatestBlockHeaderRoot()); uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); - uint64 slot = Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot); uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); - bytes32 validatorPubkeyHash = validatorFields[0]; cheats.deal(address(newPod), stakeAmount); - - uint256 delayedWithdrawalRouterContractBalanceBefore = address(delayedWithdrawalRouter).balance; - cheats.expectEmit(true, true, true, true, address(newPod)); - emit PartialWithdrawalRedeemed(validatorIndex, podOwner, withdrawalAmountGwei); - newPod.verifyAndProcessWithdrawal(withdrawalProofs, validatorFieldsProof, validatorFields, withdrawalFields, 0, 0); - require(newPod.provenWithdrawal(validatorPubkeyHash, slot), "provenPartialWithdrawal should be true"); - withdrawalAmountGwei = uint64(withdrawalAmountGwei*GWEI_TO_WEI); - require(address(delayedWithdrawalRouter).balance - delayedWithdrawalRouterContractBalanceBefore == withdrawalAmountGwei, - "pod delayed withdrawal balance hasn't been updated correctly"); + { + BeaconChainProofs.WithdrawalProofs[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProofs[](1); + withdrawalProofsArray[0] = withdrawalProofs; + bytes[] memory validatorFieldsProofArray = new bytes[](1); + validatorFieldsProofArray[0] = validatorFieldsProof; + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + validatorFieldsArray[0] = validatorFields; + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + withdrawalFieldsArray[0] = withdrawalFields; + + uint256 delayedWithdrawalRouterContractBalanceBefore = address(delayedWithdrawalRouter).balance; + cheats.expectEmit(true, true, true, true, address(newPod)); + emit PartialWithdrawalRedeemed(validatorIndex, podOwner, withdrawalAmountGwei); + newPod.verifyAndProcessWithdrawal(withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray, 0, 0); + require(newPod.provenWithdrawal(validatorFields[0], Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot)), "provenPartialWithdrawal should be true"); + withdrawalAmountGwei = uint64(withdrawalAmountGwei*GWEI_TO_WEI); + require(address(delayedWithdrawalRouter).balance - delayedWithdrawalRouterContractBalanceBefore == withdrawalAmountGwei, + "pod delayed withdrawal balance hasn't been updated correctly"); + } cheats.roll(block.number + WITHDRAWAL_DELAY_BLOCKS + 1); uint podOwnerBalanceBefore = address(podOwner).balance; @@ -415,8 +429,17 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { withdrawalFields = getWithdrawalFields(); validatorFields = getValidatorFields(); + BeaconChainProofs.WithdrawalProofs[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProofs[](1); + withdrawalProofsArray[0] = withdrawalProofs; + bytes[] memory validatorFieldsProofArray = new bytes[](1); + validatorFieldsProofArray[0] = validatorFieldsProof; + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + validatorFieldsArray[0] = validatorFields; + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + withdrawalFieldsArray[0] = withdrawalFields; + cheats.expectRevert(bytes("EigenPod.verifyAndProcessWithdrawal: withdrawal has already been proven for this slot")); - newPod.verifyAndProcessWithdrawal(withdrawalProofs, validatorFieldsProof, validatorFields, withdrawalFields, 0, 0); + newPod.verifyAndProcessWithdrawal(withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray, 0, 0); } /// @notice verifies that multiple full withdrawals for a single validator fail @@ -433,8 +456,17 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - newPod.REQUIRED_BALANCE_GWEI()) * uint64(GWEI_TO_WEI); cheats.deal(address(newPod), leftOverBalanceWEI); + BeaconChainProofs.WithdrawalProofs[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProofs[](1); + withdrawalProofsArray[0] = withdrawalProofs; + bytes[] memory validatorFieldsProofArray = new bytes[](1); + validatorFieldsProofArray[0] = validatorFieldsProof; + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + validatorFieldsArray[0] = validatorFields; + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + withdrawalFieldsArray[0] = withdrawalFields; + cheats.expectRevert(bytes("EigenPod.verifyAndProcessWithdrawal: withdrawal has already been proven for this slot")); - newPod.verifyAndProcessWithdrawal(withdrawalProofs, validatorFieldsProof, validatorFields, withdrawalFields, 0, 0); + newPod.verifyAndProcessWithdrawal(withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray, 0, 0); return newPod; }