diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index c160a7712..000000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no -- commitlint --edit ${1} diff --git a/package.json b/package.json index 413cfd75b..cfcaa3407 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "fs": "^0.0.1-security", "hardhat": "^2.12.4", "hardhat-preprocessor": "^0.1.5", - "husky": "^8.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4", "yargs": "^17.7.2" diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 69a31274b..200051eba 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -49,6 +49,15 @@ interface IEigenPod { int256 sharesDeltaGwei; } + struct VerifiedPartialWithdrawalBatch{ + // amount being proven for withdrawal + uint64 provenPartialWithdrawalSumGwei; + // the latest timestamp proven until + uint64 mostRecentWithdrawalTimestamp; + // upper bound of the withdrawal period + uint64 endTimestamp; + } + enum PARTIAL_WITHDRAWAL_CLAIM_STATUS { REDEEMED, @@ -94,7 +103,6 @@ interface IEigenPod { /// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn); - /// @notice The max amount of eth, in gwei, that can be restaked per validator function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64); @@ -220,4 +228,10 @@ interface IEigenPod { /// @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; + + function fulfillPartialWithdrawalProofRequest( + IEigenPod.VerifiedPartialWithdrawalBatch calldata verifiedPartialWithdrawalBatch, + uint64 feeGwei, + address feeRecipient + ) external; } diff --git a/src/contracts/interfaces/IEigenPodManager.sol b/src/contracts/interfaces/IEigenPodManager.sol index 2ed9063c0..223c0587f 100644 --- a/src/contracts/interfaces/IEigenPodManager.sol +++ b/src/contracts/interfaces/IEigenPodManager.sol @@ -29,6 +29,12 @@ interface IEigenPodManager is IPausable { /// @notice Emitted when `maxPods` value is updated from `previousValue` to `newValue` event MaxPodsUpdated(uint256 previousValue, uint256 newValue); + /// @notice Emitted when a new proof fulfiller is added + event ProofServiceUpdated(ProofService proofService); + + /// @notice emitted when the partial withdrawal proof switch is turned on + event ProofServiceEnabled(); + /// @notice Emitted when a withdrawal of beacon chain ETH is completed event BeaconChainETHWithdrawalCompleted( address indexed podOwner, @@ -39,6 +45,51 @@ interface IEigenPodManager is IPausable { bytes32 withdrawalRoot ); + //info for each withdrawal called back by proof service + struct WithdrawalCallbackInfo { + // oracle timestamp + uint64 oracleTimestamp; + // prover fee for each pod being proven for + uint64[] feesGwei; + /// @notice SNARK proof acting as the cryptographic seal over the execution results. + bytes seal; + /// @notice Digest of the zkVM SystemState after execution. + /// @dev The relay does not additionally check any property of this digest, but needs the + /// digest in order to reconstruct the ReceiptMetadata hash to which the proof is linked. + bytes32 postStateDigest; + //journal generated by offchain proof + Journal journal; + // imageID generated by offchain proof + bytes32 imageId; + } + + struct Journal { + // amount being proven for withdrawal + uint64[] provenPartialWithdrawalSumsGwei; + // computed blockRoot + bytes32 blockRoot; + // the address of the pod being proven for + address[] podAddresses; + // the address of the pod owner + address[] podOwners; + // the latest timestamp proven until + uint64[] mostRecentWithdrawalTimestamps; + // upper bound of the withdrawal period + uint64[] endTimestamps; + // user signed fee + uint64[] maxFeesGwei; + //request nonce + uint64 nonce; + } + + struct ProofService { + address caller; + // fee recipient address of the proof service + address feeRecipient; + //address of the groth-16 verifier contract + address verifier; + } + /** * @notice Creates an EigenPod for the sender. * @dev Function will revert if the `msg.sender` already has an EigenPod. @@ -143,4 +194,20 @@ interface IEigenPodManager is IPausable { * @dev Reverts if `shares` is not a whole Gwei amount */ function withdrawSharesAsTokens(address podOwner, address destination, uint256 shares) external; + + + /// @notice Returns the status of the proof service + function proofServiceEnabled() external view returns (bool); + + /// @notice turns on offchain proof service + function enableProofService() external; + + /// @notice updates the proof service caller + function updateProofService(ProofService calldata newProofService) external; + + /// @notice callback for proof service + function proofServiceCallback( + WithdrawalCallbackInfo calldata callbackInfo + ) external; + } diff --git a/src/contracts/interfaces/IRiscZeroVerifier.sol b/src/contracts/interfaces/IRiscZeroVerifier.sol new file mode 100644 index 000000000..6f29f1978 --- /dev/null +++ b/src/contracts/interfaces/IRiscZeroVerifier.sol @@ -0,0 +1,88 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.9; + +/// @notice Indicator for the overall system at the end of execution covered by this proof. +enum SystemExitCode { + Halted, + Paused, + SystemSplit +} + +/// @notice Combination of system and user exit codes. +/// @dev If system exit code is SystemSplit, the user exit code must be zero. +struct ExitCode { + SystemExitCode system; + uint8 user; +} + +/// @notice Data associated with a receipt which is used for both input and +/// output of global state. +struct ReceiptMetadata { + /// Digest of the SystemState of a segment just before execution has begun. + bytes32 preStateDigest; + /// Digest of the SystemState of a segment just after execution has completed. + bytes32 postStateDigest; + /// The exit code for a segment + ExitCode exitCode; + /// A digest of the input, from the viewpoint of the guest. + bytes32 input; + /// A digest of the journal, from the viewpoint of the guest. + bytes32 output; +} + +library ReceiptMetadataLib { + bytes32 constant TAG_DIGEST = sha256("risc0.ReceiptMeta"); + + function digest(ReceiptMetadata memory meta) internal pure returns (bytes32) { + return sha256( + abi.encodePacked( + TAG_DIGEST, + // down + meta.input, + meta.preStateDigest, + meta.postStateDigest, + meta.output, + // data + uint32(meta.exitCode.system) << 24, + uint32(meta.exitCode.user) << 24, + // down.length + uint16(4) << 8 + ) + ); + } +} + +struct Receipt { + bytes seal; + ReceiptMetadata meta; +} + +interface IRiscZeroVerifier { + /// @notice verify that the given receipt is a valid Groth16 RISC Zero recursion receipt. + /// @return true if the receipt passes the verification checks. + function verify(Receipt calldata receipt) external view returns (bool); + + /// @notice verifies that the given seal is a valid Groth16 RISC Zero proof of execution over the + /// given image ID, post-state digest, and journal. Asserts that the input hash + // is all-zeros (i.e. no committed input) and the exit code is (Halted, 0). + /// @return true if the receipt passes the verification checks. + function verify(bytes calldata seal, bytes32 imageId, bytes32 postStateDigest, bytes32 journalHash) + external + view + returns (bool); +} \ No newline at end of file diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 1a2e3cdbf..4a9f04bcd 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -92,7 +92,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen uint256 public nonBeaconChainETHBalanceWei; /// @notice This variable tracks the total amount of partial withdrawals claimed via merkle proofs prior to a switch to ZK proofs for claiming partial withdrawals - uint64 public sumOfPartialWithdrawalsClaimedGwei; + uint64 public sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei; modifier onlyEigenPodManager() { require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); @@ -311,7 +311,6 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen (validatorFieldsProofs.length == validatorFields.length), "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" ); - /** * Withdrawal credential proof should not be "stale" (older than VERIFY_BALANCE_UPDATE_WINDOW_SECONDS) as we are doing a balance check here * The validator container persists as the state evolves and even after the validator exits. So we can use a more "fresh" credential proof within @@ -428,6 +427,49 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen _sendETH(recipient, amountWei); } + + /******************************************************************************* + EXTERNAL FUNCTIONS CALLABLE BY PERMISSIONED SERVICES + *******************************************************************************/ + + /// @notice Called by the EigenPodManager to fulfill a partial withdrawal proof request + function fulfillPartialWithdrawalProofRequest( + IEigenPod.VerifiedPartialWithdrawalBatch calldata verifiedPartialWithdrawalBatch, + uint64 feeGwei, + address feeRecipient + ) external onlyEigenPodManager { + + require(verifiedPartialWithdrawalBatch.mostRecentWithdrawalTimestamp == mostRecentWithdrawalTimestamp, "EigenPod.fulfillPartialWithdrawalProofRequest: proven mostRecentWithdrawalTimestamp must match mostRecentWithdrawalTimestamp in the EigenPod"); + require(mostRecentWithdrawalTimestamp < verifiedPartialWithdrawalBatch.endTimestamp, "EigenPod.fulfillPartialWithdrawalProofRequest: mostRecentWithdrawalTimestamp must precede endTimestamp"); + + require(verifiedPartialWithdrawalBatch.provenPartialWithdrawalSumGwei >= feeGwei, "EigenPod.fulfillPartialWithdrawalProofRequest: provenPartialWithdrawalSumGwei must be greater than the fee"); + + //update mostRecentWithdrawalTimestamp to currently proven endTimestamp + mostRecentWithdrawalTimestamp = verifiedPartialWithdrawalBatch.endTimestamp; + + uint64 provenPartialWithdrawalSumGwei = verifiedPartialWithdrawalBatch.provenPartialWithdrawalSumGwei; + // subtract an partial withdrawals that may have been claimed via merkle proofs + if(provenPartialWithdrawalSumGwei > sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei){ + if(sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei > 0){ + provenPartialWithdrawalSumGwei -= sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei; + sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei = 0; + } + + //Once sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei, we need to ensure that there is enough ETH in the pod to pay the fee + if(provenPartialWithdrawalSumGwei >= feeGwei){ + provenPartialWithdrawalSumGwei -= feeGwei; + //send proof service their fee + AddressUpgradeable.sendValue(payable(feeRecipient), feeGwei); + } + if(provenPartialWithdrawalSumGwei > 0){ + _sendETH_AsDelayedWithdrawal(podOwner, provenPartialWithdrawalSumGwei); + } + + } else { + sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei -= provenPartialWithdrawalSumGwei; + } + } + /******************************************************************************* INTERNAL FUNCTIONS *******************************************************************************/ @@ -712,6 +754,10 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen address recipient, uint64 partialWithdrawalAmountGwei ) internal returns (VerifiedWithdrawal memory) { + require( + !eigenPodManager.proofServiceEnabled(), + "EigenPod._processPartialWithdrawal: partial withdrawal merkle proofs are disabled" + ); emit PartialWithdrawalRedeemed( validatorIndex, withdrawalTimestamp, @@ -719,7 +765,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen partialWithdrawalAmountGwei ); - sumOfPartialWithdrawalsClaimedGwei += partialWithdrawalAmountGwei; + sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei += partialWithdrawalAmountGwei; // For partial withdrawals, the withdrawal amount is immediately sent to the pod owner return @@ -788,6 +834,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen return _validatorPubkeyHashToInfo[pubkeyHash].status; } + /// @notice Returns the validator status for a given validatorPubkey function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); @@ -795,6 +842,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. diff --git a/src/contracts/pods/EigenPodManager.sol b/src/contracts/pods/EigenPodManager.sol index 545f2ce55..302648cbf 100644 --- a/src/contracts/pods/EigenPodManager.sol +++ b/src/contracts/pods/EigenPodManager.sol @@ -7,11 +7,13 @@ import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; import "../interfaces/IBeaconChainOracle.sol"; +import "../interfaces/IRiscZeroVerifier.sol"; import "../permissions/Pausable.sol"; import "./EigenPodPausingConstants.sol"; import "./EigenPodManagerStorage.sol"; + /** * @title The contract used for creating and managing EigenPods * @author Layr Labs, Inc. @@ -44,6 +46,11 @@ contract EigenPodManager is _; } + modifier onlyProofService() { + require(msg.sender == proofService.caller, "EigenPodManager.onlyProofService: not a permissioned fulfiller"); + _; + } + constructor( IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, @@ -219,6 +226,54 @@ contract EigenPodManager is ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); } + /// @notice Called by proving service to fulfill partial withdrawal proof requests + function proofServiceCallback( + WithdrawalCallbackInfo calldata callbackInfo + ) external onlyProofService nonReentrant onlyWhenNotPaused(PAUSED_EIGENPODS_FULFILL_PARTIAL_WITHDRAWAL_PROOF_REQUEST){ + require(proofServiceEnabled, "EigenPodManager.proofServiceCallback: offchain partial withdrawal proofs are not enabled"); + + Journal memory journal = callbackInfo.journal; + _verifyJournalParameters(journal); + require(callbackInfo.feesGwei.length == journal.podOwners.length, "EigenPodManager.proofServiceCallback: feesGwei and podOwners must be the same length"); + + require(IRiscZeroVerifier(proofService.verifier).verify(callbackInfo.seal, callbackInfo.imageId, callbackInfo.postStateDigest, sha256(abi.encode(journal))), "EigenPodManager.proofServiceCallback: invalid proof"); + + require(journal.blockRoot == getBlockRootAtTimestamp(callbackInfo.oracleTimestamp), "EigenPodManager.proofServiceCallback: block root does not match oracleRoot for that timestamp"); + + for (uint256 i = 0; i < journal.podOwners.length; i++) { + + // these checks are verified in the snark, we add them here again as a sanity check + require(callbackInfo.oracleTimestamp >= journal.endTimestamps[i], "EigenPodManager.proofServiceCallback: oracle timestamp must be greater than or equal to callback timestamp"); + require(callbackInfo.feesGwei[i] <= journal.maxFeesGwei[i], "EigenPodManager.proofServiceCallback: fee must be less than or equal to maxFee"); + + //ensure the correct pod is being called + IEigenPod pod = ownerToPod[journal.podOwners[i]]; + require(address(pod) != address(0), "EigenPodManager.proofServiceCallback: pod does not exist"); + require(address(pod) == journal.podAddresses[i], "EigenPodManager.proofServiceCallback: pod address does not match"); + + IEigenPod.VerifiedPartialWithdrawalBatch memory partialWithdrawal = IEigenPod.VerifiedPartialWithdrawalBatch({ + provenPartialWithdrawalSumGwei: journal.provenPartialWithdrawalSumsGwei[i], + mostRecentWithdrawalTimestamp: journal.mostRecentWithdrawalTimestamps[i], + endTimestamp: journal.endTimestamps[i] + }); + + pod.fulfillPartialWithdrawalProofRequest(partialWithdrawal, callbackInfo.feesGwei[i], proofService.feeRecipient); + } + } + + /// @notice enables partial withdrawal proving via offchain proofs + function enableProofService() external onlyOwner { + require(!proofServiceEnabled, "EigenPodManager.enableProofService: proof service already enabled"); + proofServiceEnabled = true; + emit ProofServiceEnabled(); + } + + /// @notice changes the proof service related information + function updateProofService(ProofService calldata newProofService) external onlyOwner { + proofService = newProofService; + emit ProofServiceUpdated(newProofService); + } + /** * Sets the maximum number of pods that can be deployed * @param newMaxPods The new maximum number of pods that can be deployed @@ -271,6 +326,14 @@ contract EigenPodManager is maxPods = _maxPods; } + function _verifyJournalParameters(Journal memory journal) internal { + require(journal.podOwners.length == journal.podAddresses.length, "EigenPodManager.proofServiceCallback: podOwners and podAddresses must be the same length"); + require(journal.podOwners.length == journal.provenPartialWithdrawalSumsGwei.length, "EigenPodManager.proofServiceCallback: podOwners and provenPartialWithdrawalSumsGwei must be the same length"); + require(journal.podOwners.length == journal.mostRecentWithdrawalTimestamps.length, "EigenPodManager.proofServiceCallback: podOwners and mostRecentWithdrawalTimestamps must be the same length"); + require(journal.podOwners.length == journal.endTimestamps.length, "EigenPodManager.proofServiceCallback: podOwners and endTimestamps must be the same length"); + require(journal.podOwners.length == journal.maxFeesGwei.length, "EigenPodManager.proofServiceCallback: podOwners and maxFeesGwei must be the same length"); + } + /** * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. @@ -318,7 +381,7 @@ contract EigenPodManager is } /// @notice Returns the Beacon block root at `timestamp`. Reverts if the Beacon block root at `timestamp` has not yet been finalized. - function getBlockRootAtTimestamp(uint64 timestamp) external view returns (bytes32) { + function getBlockRootAtTimestamp(uint64 timestamp) public view returns (bytes32) { bytes32 stateRoot = beaconChainOracle.timestampToBlockRoot(timestamp); require( stateRoot != bytes32(0), diff --git a/src/contracts/pods/EigenPodManagerStorage.sol b/src/contracts/pods/EigenPodManagerStorage.sol index b893a0816..d7a9cd567 100644 --- a/src/contracts/pods/EigenPodManagerStorage.sol +++ b/src/contracts/pods/EigenPodManagerStorage.sol @@ -64,6 +64,12 @@ abstract contract EigenPodManagerStorage is IEigenPodManager { */ mapping(address => int256) public podOwnerShares; + /// @notice This is the offchain proving service + ProofService public proofService; + + /// @notice indicates offchain proofs as a service are enabled + bool public proofServiceEnabled; + constructor( IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, @@ -83,5 +89,5 @@ abstract contract EigenPodManagerStorage is IEigenPodManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[45] private __gap; + uint256[43] private __gap; } diff --git a/src/contracts/pods/EigenPodPausingConstants.sol b/src/contracts/pods/EigenPodPausingConstants.sol index 60a8a71e1..553d18d4b 100644 --- a/src/contracts/pods/EigenPodPausingConstants.sol +++ b/src/contracts/pods/EigenPodPausingConstants.sol @@ -21,6 +21,9 @@ abstract contract EigenPodPausingConstants { uint8 internal constant PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE = 3; /// @notice Index for flag that pauses the `verifyBeaconChainFullWithdrawal` function *of the EigenPods* when set. see EigenPod code for details. uint8 internal constant PAUSED_EIGENPODS_VERIFY_WITHDRAWAL = 4; + /// @notice Index for flag that pauses `fulfillPartialWithdrawalProofRequest` function *of the EigenPods* when set. see EigenPod code for details. + uint8 internal constant PAUSED_EIGENPODS_FULFILL_PARTIAL_WITHDRAWAL_PROOF_REQUEST = 5; /// @notice Pausability for EigenPod's "accidental transfer" withdrawal methods - uint8 internal constant PAUSED_NON_PROOF_WITHDRAWALS = 5; + uint8 internal constant PAUSED_NON_PROOF_WITHDRAWALS = 6; + } diff --git a/src/test/mocks/EigenPodManagerMock.sol b/src/test/mocks/EigenPodManagerMock.sol index f06730112..b65eb4b01 100644 --- a/src/test/mocks/EigenPodManagerMock.sol +++ b/src/test/mocks/EigenPodManagerMock.sol @@ -85,4 +85,14 @@ contract EigenPodManagerMock is IEigenPodManager, Test { function numPods() external view returns (uint256) {} function maxPods() external view returns (uint256) {} + + function proofServiceEnabled() external view returns (bool){} + + function updateProofService(ProofService calldata newProofService) external{} + + function proofServiceCallback( + WithdrawalCallbackInfo calldata callbackInfo + ) external{} + + function enableProofService() external {} } \ No newline at end of file diff --git a/src/test/mocks/EigenPodMock.sol b/src/test/mocks/EigenPodMock.sol index 38b515018..2a4e3682c 100644 --- a/src/test/mocks/EigenPodMock.sol +++ b/src/test/mocks/EigenPodMock.sol @@ -88,6 +88,12 @@ contract EigenPodMock is IEigenPod, Test { /// @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 {} + function fulfillPartialWithdrawalProofRequest( + IEigenPod.VerifiedPartialWithdrawalBatch calldata verifiedPartialWithdrawalBatch, + uint64 feeGwei, + address feeRecipient + ) external{} + function validatorStatus(bytes calldata pubkey) external view returns (VALIDATOR_STATUS){} function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory){} } \ No newline at end of file diff --git a/src/test/mocks/RiscZeroVerifierMock.sol b/src/test/mocks/RiscZeroVerifierMock.sol new file mode 100644 index 000000000..bff140ed1 --- /dev/null +++ b/src/test/mocks/RiscZeroVerifierMock.sol @@ -0,0 +1,39 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.9; + +import "../../contracts/interfaces/IRiscZeroVerifier.sol"; + + +contract RiscZeroVerifierMock is IRiscZeroVerifier { + /// @notice verify that the given receipt is a valid Groth16 RISC Zero recursion receipt. + /// @return true if the receipt passes the verification checks. + function verify(Receipt calldata receipt) external view returns (bool){ + return true; + } + + /// @notice verifies that the given seal is a valid Groth16 RISC Zero proof of execution over the + /// given image ID, post-state digest, and journal. Asserts that the input hash + // is all-zeros (i.e. no committed input) and the exit code is (Halted, 0). + /// @return true if the receipt passes the verification checks. + function verify(bytes calldata seal, bytes32 imageId, bytes32 postStateDigest, bytes32 journalHash) + external + view + returns (bool){ + return true; + } +} \ No newline at end of file diff --git a/src/test/test-data/withdrawal_credential_proof_test.json b/src/test/test-data/withdrawal_credential_proof_test.json new file mode 100644 index 000000000..c4347cca6 --- /dev/null +++ b/src/test/test-data/withdrawal_credential_proof_test.json @@ -0,0 +1,67 @@ +{ + "StateRootAgainstLatestBlockHeaderProof": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71" + ], + "beaconStateRoot": "0x9150ef194c1028ae7b938602b896a5c3649f8bb37943a0d742f0e675e1af71cf", + "validatorIndex": 302913, + "WithdrawalCredentialProof": [ + "0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e", + "0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee", + "0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1", + "0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1", + "0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac", + "0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537", + "0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38", + "0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748", + "0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a", + "0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c", + "0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b", + "0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e", + "0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee", + "0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9", + "0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74", + "0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3", + "0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f", + "0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6", + "0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528", + "0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0x846b080000000000000000000000000000000000000000000000000000000000", + "0x5a6c050000000000000000000000000000000000000000000000000000000000", + "0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e", + "0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b", + "0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b", + "0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d" + ], + "ValidatorFields": [ + "0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d", + "0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148", + "0x0040597307000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xea65010000000000000000000000000000000000000000000000000000000000", + "0xf265010000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file diff --git a/src/test/test-data/withdrawal_proof_test.json b/src/test/test-data/withdrawal_proof_test.json new file mode 100644 index 000000000..3ffba9770 --- /dev/null +++ b/src/test/test-data/withdrawal_proof_test.json @@ -0,0 +1 @@ +{"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"beaconStateRoot":"0x9150ef194c1028ae7b938602b896a5c3649f8bb37943a0d742f0e675e1af71cf","WithdrawalProof":["0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f","0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e","0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6","0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8","0x1000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92","0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729","0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"],"SlotProof":["0x89c5010000000000000000000000000000000000000000000000000000000000","0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2","0xb25904ef9045e860747d260b8d0d8aea5b082430a72209fc43757c68b23d541a"],"ExecutionPayloadProof":["0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474","0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053","0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"],"TimestampProof":["0x28a2c80000000000000000000000000000000000000000000000000000000000","0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df","0x4d8ad7ffe6efda168a4d2a908228a88e5c03553d422e6e65e350b6fc4beea417","0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"],"HistoricalSummaryProof":["0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc","0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb","0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529","0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a","0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2","0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048","0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e","0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e","0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f","0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15","0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3","0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf","0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8","0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be","0x0000000000000000000000000000000000000000000000000000000000000000","0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c","0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f","0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30","0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1","0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7","0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193","0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1","0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b","0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220","0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f","0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e","0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784","0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb","0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb","0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x9300000000000000000000000000000000000000000000000000000000000000","0xd9ed050000000000000000000000000000000000000000000000000000000000","0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1","0xe237bc62b6b5269da5f4093c292d0f3bf2cf4d2eb93b4f366dba675c4df9cc62"],"blockHeaderRootIndex":8092,"historicalSummaryIndex":146,"withdrawalIndex":0,"blockHeaderRoot":"0x602079191479eb9a5dce271b11aa16c5035795f44e3deb4bb02bc6f7f4fd15a6","slotRoot":"0x9c9f610000000000000000000000000000000000000000000000000000000000","timestampRoot":"0xb06fed6400000000000000000000000000000000000000000000000000000000","executionPayloadRoot":"0x7a882c601a1e6bb6d76fbff30cd46ec84bf7eccced88e085f1c5a291fd9c5c00","ValidatorProof":["0xa551043de43704c1ec4ea19cf0f78b0fd0435f8ed4b9239a4ecf45146c992055","0x7d00fbb77892999b9a3c9a59aae241405c7d28985ec1cf3bc4141371fde86236","0x578200ee4a0b0203db67a6d85bb6e826e2dda09c0e558099743800b805ee9d83","0xad3f688069225989095e1d4ba532d85f18812c26678f4ecd6ef9481f976ab112","0xee691172855dcfdaf3ac4fde83e16c81a8dc4bf1a936088245efce61ea62e7a1","0x6a421ff9e5121f54b9eab04758cfd01942079a7c6959b1c5fdad7972d8133ffd","0x8a631b87145c9ac69adc27a08b11e237c07458e889e183528e3852fc94b9ec4f","0xacab6be49023dadb3a2ab84c1b20e2abbaf92788091768178236a5fe5afe39d0","0x0759f08bd9d31971ada5f94a21e5a616871f10865893b0e4172e01178fb903db","0x87d65be424fb1debe7a20b26ef5f968d7961a1adaecb258540440d7cee3d4cdc","0x30e8565ca8cc1c1465b8a3342a79c4ea16e12ff46734bf5c6aeb2e4517e0f50b","0xd84dedf5932c76cca76fc185bb20af4ea0917fb45f79107c3444392eb1de949e","0x3c1735187981807ec62005c128ddbf1793c1a4d78c39e191249332452ebb9e14","0x5bd5dfe5ed45182bf45a7af5a4b088ed4408ec3018b2a5ca1f4cebbdead3164a","0x4eff245fad29ee570349a5001e3e62c7cb7e0fe0f49172e3bfa5b7b6c5c03b5e","0xe039e904678355d2de44d93ee3537061634425e41d243520d36129792852215d","0x7319b9f76b971853f0e6e2fe0cc559edb31c1370642be78f1d25a6c2bfb92103","0x036fd6439ae6e618ef00290144d95d21d71b9d6118bc72eb0af4e5d5a025941a","0xcce600acb536bbaf3ee8c74f9a573717eedb2756b9bf92983455b536084a6057","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"ValidatorFields":["0xf5636344319e7ca95ab4767480405cddbbbb7fcf437c5504fe336ae7995ce1fc","0x01000000000000000000000059b0d71688da01057c08e4c1baa8faa629819c2a","0x0040597307000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"WithdrawalFields":["0x45cee50000000000000000000000000000000000000000000000000000000000","0x300e030000000000000000000000000000000000000000000000000000000000","0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000","0xbd56200000000000000000000000000000000000000000000000000000000000"]} \ No newline at end of file diff --git a/src/test/unit/EigenPodManagerUnit.t.sol b/src/test/unit/EigenPodManagerUnit.t.sol index 8d797923f..252295503 100644 --- a/src/test/unit/EigenPodManagerUnit.t.sol +++ b/src/test/unit/EigenPodManagerUnit.t.sol @@ -11,6 +11,8 @@ import "src/test/utils/EigenLayerUnitTestSetup.sol"; import "src/test/harnesses/EigenPodManagerWrapper.sol"; import "src/test/mocks/EigenPodMock.sol"; import "src/test/mocks/ETHDepositMock.sol"; +import "src/test/mocks/BeaconChainOracleMock.sol"; +import "src/test/mocks/RiscZeroVerifierMock.sol"; contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { // Contracts Under Test: EigenPodManager @@ -23,6 +25,7 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { IETHPOSDeposit public ethPOSMock; IEigenPod public eigenPodMockImplementation; IBeacon public eigenPodBeacon; // Proxy for eigenPodMockImplementation + BeaconChainOracleMock public beaconChainOracle; // Constants uint256 public constant GWEI_TO_WEI = 1e9; @@ -36,6 +39,7 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { // Deploy Mocks ethPOSMock = new ETHPOSDepositMock(); eigenPodMockImplementation = new EigenPodMock(); + beaconChainOracle = new BeaconChainOracleMock(); eigenPodBeacon = new UpgradeableBeacon(address(eigenPodMockImplementation)); // Deploy EPM Implementation & Proxy @@ -54,7 +58,7 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { abi.encodeWithSelector( EigenPodManager.initialize.selector, type(uint256).max /*maxPods*/, - IBeaconChainOracle(address(0)) /*beaconChainOracle*/, + beaconChainOracle /*beaconChainOracle*/, initialOwner, pauserRegistry, 0 /*initialPausedStatus*/ @@ -65,6 +69,8 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { // Set defaultPod defaultPod = eigenPodManager.getPod(defaultStaker); + emit log_named_address("defaultPod", address(defaultPod)); + emit log_named_address("defaultStaker", defaultStaker); // Exclude the zero address, and the eigenPodManager itself from fuzzed inputs addressIsExcludedFromFuzzedInputs[address(0)] = true; @@ -101,6 +107,13 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { assertEq(address(eigenPodManager.ownerToPod(staker)), expectedPod, "Expected pod not deployed"); assertEq(eigenPodManager.numPods(), numPodsBefore + 1, "Num pods not incremented"); } + + function _turnOnPartialWithdrawalSwitch(EigenPodManager epm) internal { + // Turn on partial withdrawal switch + cheats.prank(epm.owner()); + epm.enableProofService(); + cheats.stopPrank(); + } } contract EigenPodManagerUnitTests_Initialization_Setters is EigenPodManagerUnitTests, IEigenPodManagerEvents { @@ -112,7 +125,6 @@ contract EigenPodManagerUnitTests_Initialization_Setters is EigenPodManagerUnitT function test_initialization() public { // Check max pods, beacon chain, owner, and pauser assertEq(eigenPodManager.maxPods(), type(uint256).max, "Initialization: max pods incorrect"); - assertEq(address(eigenPodManager.beaconChainOracle()), address(IBeaconChainOracle(address(0))), "Initialization: beacon chain oracle incorrect"); assertEq(eigenPodManager.owner(), initialOwner, "Initialization: owner incorrect"); assertEq(address(eigenPodManager.pauserRegistry()), address(pauserRegistry), "Initialization: pauser registry incorrect"); assertEq(eigenPodManager.paused(), 0, "Initialization: paused value not 0"); @@ -550,3 +562,104 @@ contract EigenPodManagerUnitTests_ShareAdjustmentCalculationTests is EigenPodMan assertEq(sharesDelta, sharesAfter - sharesBefore, "Shares delta must be equal to the difference between sharesAfter and sharesBefore"); } } + +contract EigenPodManagerUnitTests_OffchainProofGenerationTests is EigenPodManagerUnitTests { + address defaultProver = address(123); + bytes32 blockRoot = bytes32(uint256(123)); + + uint64[] public feesArray; + + + + function setUp() virtual override public { + super.setUp(); + RiscZeroVerifierMock defaultVerifier = new RiscZeroVerifierMock(); + cheats.startPrank(eigenPodManager.owner()); + eigenPodManager.updateProofService(IEigenPodManager.ProofService({caller: defaultProver, feeRecipient: defaultProver, verifier: address(defaultVerifier)})); + cheats.stopPrank(); + + cheats.startPrank(defaultStaker); + eigenPodManager.stake(new bytes(0), new bytes(0), bytes32(0)); + cheats.stopPrank(); + + feesArray.push(0); + + beaconChainOracle.setOracleBlockRootAtTimestamp(blockRoot); + } + function testFuzz_proofCallback_revert_incorrectOracleTimestamp(uint64 oracleTimestamp, uint64 startTimestamp, uint64 endTimestamp) public { + cheats.assume(oracleTimestamp < endTimestamp); + _turnOnPartialWithdrawalSwitch(eigenPodManager); + + IEigenPodManager.Journal memory journal = _assembleJournal(defaultPod, defaultStaker, 0, endTimestamp, 0, blockRoot); + IEigenPodManager.WithdrawalCallbackInfo memory withdrawalCallbackInfo = IEigenPodManager.WithdrawalCallbackInfo(oracleTimestamp, feesArray, new bytes(0), bytes32(0), journal, bytes32(0)); + + cheats.startPrank(defaultProver); + cheats.expectRevert(bytes("EigenPodManager.proofServiceCallback: oracle timestamp must be greater than or equal to callback timestamp")); + eigenPodManager.proofServiceCallback(withdrawalCallbackInfo); + cheats.stopPrank(); + } + + function testFuzz_proofCallback_revert_feeExceedsMaxFee(uint64 oracleTimestamp, uint64 endTimestamp, uint64 maxFee, uint64 fee) public { + cheats.assume(oracleTimestamp > endTimestamp); + cheats.assume(fee > maxFee); + feesArray[0] = fee; + + _turnOnPartialWithdrawalSwitch(eigenPodManager); + cheats.startPrank(defaultProver); + cheats.expectRevert(bytes("EigenPodManager.proofServiceCallback: fee must be less than or equal to maxFee")); + eigenPodManager.proofServiceCallback(IEigenPodManager.WithdrawalCallbackInfo(oracleTimestamp, feesArray, new bytes(0), bytes32(0), _assembleJournal(defaultPod, defaultStaker, 0, endTimestamp, maxFee, blockRoot), bytes32(0))); + cheats.stopPrank(); + } + + function testFuzz_proofCallback_revert_incorrectBlockRoot(bytes32 incorrectBlockRoot) public { + cheats.assume(incorrectBlockRoot != blockRoot); + + _turnOnPartialWithdrawalSwitch(eigenPodManager); + emit log_address(address(defaultPod)); + IEigenPodManager.Journal memory journal = _assembleJournal(defaultPod, defaultStaker, 0, 0, 0, incorrectBlockRoot); + + IEigenPodManager.WithdrawalCallbackInfo memory withdrawalCallbackInfo = IEigenPodManager.WithdrawalCallbackInfo(0, feesArray, new bytes(0), bytes32(0), journal, bytes32(0)); + + cheats.startPrank(defaultProver); + cheats.expectRevert(bytes("EigenPodManager.proofServiceCallback: block root does not match oracleRoot for that timestamp")); + eigenPodManager.proofServiceCallback(withdrawalCallbackInfo); + cheats.stopPrank(); + } + + function _assembleJournal( + IEigenPod eigenPod, + address podOwner, + uint64 mostRecentWithdrawalTimestamp, + uint64 endTimestamp, + uint64 maxFee, + bytes32 blockRoot + ) internal returns (IEigenPodManager.Journal memory) { + address[] memory eigenPodAddressArray = new address[](1); + eigenPodAddressArray[0] = address(eigenPod); + address[] memory podOwnerArray = new address[](1); // Replace YourType with the actual type of podOwner + podOwnerArray[0] = podOwner; + uint64[] memory withdrawalTimestampArray = new uint64[](1); + withdrawalTimestampArray[0] = mostRecentWithdrawalTimestamp; + uint64[] memory endTimestampArray = new uint64[](1); + endTimestampArray[0] = endTimestamp; + uint64[] memory feeArray = new uint64[](1); + feeArray[0] = maxFee; + return IEigenPodManager.Journal({ + provenPartialWithdrawalSumsGwei: new uint64[](1), + blockRoot: blockRoot, + podAddresses: eigenPodAddressArray, + podOwners: podOwnerArray, + mostRecentWithdrawalTimestamps: withdrawalTimestampArray, + endTimestamps: endTimestampArray, + maxFeesGwei: feeArray, + nonce: uint64(0) + }); + } +} + + + + + + + diff --git a/src/test/unit/EigenPodUnit.t.sol b/src/test/unit/EigenPodUnit.t.sol index 182562e04..99581943b 100644 --- a/src/test/unit/EigenPodUnit.t.sol +++ b/src/test/unit/EigenPodUnit.t.sol @@ -898,7 +898,7 @@ contract EigenPodUnitTests_WithdrawalTests is EigenPodHarnessSetup, ProofParsing assertTrue(eigenPodHarness.provenWithdrawal(validatorPubKeyHash, withdrawalTimestamp), "Withdrawal not set to proven"); // Checks from _processPartialWithdrawal - assertEq(eigenPod.sumOfPartialWithdrawalsClaimedGwei(), withdrawalAmountGwei, "Incorrect partial withdrawal amount"); + assertEq(eigenPod.sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei(), withdrawalAmountGwei, "Incorrect partial withdrawal amount"); assertEq(vw.amountToSendGwei, withdrawalAmountGwei, "Amount to send via router is not correct"); assertEq(vw.sharesDeltaGwei, 0, "Shares delta should be 0"); @@ -962,7 +962,7 @@ contract EigenPodUnitTests_WithdrawalTests is EigenPodHarnessSetup, ProofParsing IEigenPod.VerifiedWithdrawal memory vw = eigenPodHarness.processPartialWithdrawal(validatorIndex, withdrawalTimestamp, recipient, partialWithdrawalAmountGwei); // Checks - assertEq(eigenPod.sumOfPartialWithdrawalsClaimedGwei(), partialWithdrawalAmountGwei, "Incorrect partial withdrawal amount"); + assertEq(eigenPod.sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei(), partialWithdrawalAmountGwei, "Incorrect partial withdrawal amount"); assertEq(vw.amountToSendGwei, partialWithdrawalAmountGwei, "Amount to send via router is not correct"); assertEq(vw.sharesDeltaGwei, 0, "Shares delta should be 0"); } @@ -1025,4 +1025,105 @@ contract EigenPodUnitTests_WithdrawalTests is EigenPodHarnessSetup, ProofParsing ); _; } +} + +contract EigenPodUnitTests_OffchainPartialWithdrawalProofTests is EigenPodUnitTests, IEigenPodEvents { + address feeRecipient = address(123); + uint256 internal constant GWEI_TO_WEI = 1e9; + + function testFuzz_proofCallbackRequest_revert_inconsistentTimestamps(uint64 endTimestamp) external { + cheats.assume(eigenPod.mostRecentWithdrawalTimestamp() >= endTimestamp); + + IEigenPod.VerifiedPartialWithdrawalBatch memory vp = IEigenPod.VerifiedPartialWithdrawalBatch(0, eigenPod.mostRecentWithdrawalTimestamp(), endTimestamp); + cheats.startPrank(address(eigenPodManagerMock)); + cheats.expectRevert("EigenPod.fulfillPartialWithdrawalProofRequest: mostRecentWithdrawalTimestamp must precede endTimestamp"); + eigenPod.fulfillPartialWithdrawalProofRequest(vp, 0, address(this)); + cheats.stopPrank(); + } + + function testFuzz_proofCallbackRequest_revert_inconsistentMostRecentWithdrawalTimestamps(uint64 mostRecentWithdrawalTimestamp) external { + cheats.assume(mostRecentWithdrawalTimestamp != eigenPod.mostRecentWithdrawalTimestamp()); + + // IEigenPodManager.WithdrawalCallbackInfo memory withdrawalCallbackInfo = IEigenPodManager.WithdrawalCallbackInfo(podOwner, address(eigenPod), 0, mostRecentWithdrawalTimestamp, 0, 0, 0); + IEigenPod.VerifiedPartialWithdrawalBatch memory vp = IEigenPod.VerifiedPartialWithdrawalBatch(0, mostRecentWithdrawalTimestamp, 0); + + cheats.startPrank(address(eigenPodManagerMock)); + cheats.expectRevert("EigenPod.fulfillPartialWithdrawalProofRequest: proven mostRecentWithdrawalTimestamp must match mostRecentWithdrawalTimestamp in the EigenPod"); + eigenPod.fulfillPartialWithdrawalProofRequest(vp, 0, address(this)); + cheats.stopPrank(); + } + + function testFuzz_proofCallbackRequest_PartialWithdrawalSumLessThanFee(uint64 endTimestamp, uint64 provenAmount, uint64 fee) external { + cheats.assume(provenAmount < fee); + + cheats.assume(eigenPod.mostRecentWithdrawalTimestamp() < endTimestamp); + IEigenPod.VerifiedPartialWithdrawalBatch memory vp = IEigenPod.VerifiedPartialWithdrawalBatch({provenPartialWithdrawalSumGwei: provenAmount, mostRecentWithdrawalTimestamp: eigenPod.mostRecentWithdrawalTimestamp(), endTimestamp: endTimestamp}); + + cheats.startPrank(address(eigenPodManagerMock)); + cheats.expectRevert(bytes("EigenPod.fulfillPartialWithdrawalProofRequest: provenPartialWithdrawalSumGwei must be greater than the fee")); + eigenPod.fulfillPartialWithdrawalProofRequest(vp, fee, feeRecipient); + cheats.stopPrank(); + } + + function testFuzz_proofCallbackRequest_ProvenAmountIsLessThanMerkleProvenAmount(uint64 endTimestamp, uint64 sumOfPartialWithdrawalsClaimedGwei, uint64 provenAmount, uint64 fee) external { + cheats.assume(provenAmount < sumOfPartialWithdrawalsClaimedGwei); + cheats.assume(provenAmount > fee); + + cheats.assume(eigenPod.mostRecentWithdrawalTimestamp() < endTimestamp); + bytes32 slot = bytes32(uint256(56)); + bytes32 value = bytes32(uint256(sumOfPartialWithdrawalsClaimedGwei)); + cheats.store(address(eigenPod), slot, value); + IEigenPod.VerifiedPartialWithdrawalBatch memory vp = IEigenPod.VerifiedPartialWithdrawalBatch({provenPartialWithdrawalSumGwei: provenAmount, mostRecentWithdrawalTimestamp: eigenPod.mostRecentWithdrawalTimestamp(), endTimestamp: endTimestamp}); + + cheats.startPrank(address(eigenPodManagerMock)); + eigenPod.fulfillPartialWithdrawalProofRequest(vp, fee, feeRecipient); + cheats.stopPrank(); + + assertEq(eigenPod.sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei(), sumOfPartialWithdrawalsClaimedGwei - provenAmount, "Incorrect sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei"); + } + + function testFuzz_proofCallbackRequest_MerkleProvenPartialWithdrawalsIsZero(uint64 endTimestamp, uint64 provenAmount, uint64 fee) external { + cheats.assume(provenAmount > fee); + + cheats.assume(eigenPod.mostRecentWithdrawalTimestamp() < endTimestamp); + IEigenPod.VerifiedPartialWithdrawalBatch memory vp = IEigenPod.VerifiedPartialWithdrawalBatch({provenPartialWithdrawalSumGwei: provenAmount, mostRecentWithdrawalTimestamp: eigenPod.mostRecentWithdrawalTimestamp(), endTimestamp: endTimestamp}); + + uint256 feeRecipientBalanceBefore = feeRecipient.balance; + cheats.deal(address(eigenPod), fee); + cheats.deal(address(eigenPod), provenAmount); + cheats.startPrank(address(eigenPodManagerMock)); + eigenPod.fulfillPartialWithdrawalProofRequest(vp, fee, feeRecipient); + cheats.stopPrank(); + + assertEq(feeRecipient.balance - feeRecipientBalanceBefore == fee, true, "Fee recipient should have received fee"); + assertEq(uint64(address(delayedWithdrawalRouterMock).balance), provenAmount - fee, "Incorrect amount set to delayed withdrawal router"); + } + + function testFuzz_proofCallbackRequest_MerkleProvenPartialWithdrawalsIsNonZero(uint64 endTimestamp, uint64 sumOfPartialWithdrawalsClaimedGwei, uint64 provenAmount, uint64 fee) external { + cheats.assume(provenAmount > fee); + cheats.assume(provenAmount > sumOfPartialWithdrawalsClaimedGwei); + + cheats.assume(eigenPod.mostRecentWithdrawalTimestamp() < endTimestamp); + bytes32 slot = bytes32(uint256(56)); + bytes32 value = bytes32(uint256(sumOfPartialWithdrawalsClaimedGwei)); + cheats.store(address(eigenPod), slot, value); + + + IEigenPod.VerifiedPartialWithdrawalBatch memory vp = IEigenPod.VerifiedPartialWithdrawalBatch({provenPartialWithdrawalSumGwei: provenAmount, mostRecentWithdrawalTimestamp: eigenPod.mostRecentWithdrawalTimestamp(), endTimestamp: endTimestamp}); + + uint256 feeRecipientBalanceBefore = feeRecipient.balance; + cheats.deal(address(eigenPod), fee); + cheats.deal(address(eigenPod), provenAmount); + cheats.startPrank(address(eigenPodManagerMock)); + eigenPod.fulfillPartialWithdrawalProofRequest(vp, fee, feeRecipient); + cheats.stopPrank(); + + if(provenAmount - sumOfPartialWithdrawalsClaimedGwei >= fee){ + assertEq(feeRecipient.balance - feeRecipientBalanceBefore == fee, true, "Fee recipient should have received fee"); + assertEq(uint64(address(delayedWithdrawalRouterMock).balance), provenAmount - sumOfPartialWithdrawalsClaimedGwei - fee, "Incorrect amount set to delayed withdrawal router"); + } else { + assertEq(uint64(address(delayedWithdrawalRouterMock).balance), provenAmount - sumOfPartialWithdrawalsClaimedGwei, "Incorrect amount set to delayed withdrawal router"); + } + assertEq(eigenPod.sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei(), 0, "sumOfPartialWithdrawalsClaimedViaMerkleProvenGwei should be set to 0"); + } } \ No newline at end of file