diff --git a/docs/core/EigenPodManager.md b/docs/core/EigenPodManager.md index 3da6f1fba..78890ec8e 100644 --- a/docs/core/EigenPodManager.md +++ b/docs/core/EigenPodManager.md @@ -59,7 +59,7 @@ Note: the functions of the `EigenPodManager` and `EigenPod` contracts are tightl * The calculation subtracts an offset (`RESTAKED_BALANCE_OFFSET_GWEI`) from the validator's proven balance, and round down to the nearest ETH * Related: `uint64 RESTAKED_BALANCE_OFFSET_GWEI` * As of M2, this is 0.75 ETH (in Gwei) - * Related: `uint64 MAX_VALIDATOR_BALANCE_GWEI` + * Related: `uint64 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR` * As of M2, this is 31 ETH (in Gwei) * This is the maximum amount of restaked ETH a single validator can be credited with in EigenLayer * `_podWithdrawalCredentials() -> (bytes memory)`: @@ -363,7 +363,7 @@ Whether each withdrawal is a full or partial withdrawal is determined by the val * The validator in question is recorded as having a proven withdrawal at the timestamp given by `withdrawalProof.timestampRoot` * This is to prevent the same withdrawal from being proven twice * If this is a full withdrawal: - * Any withdrawal amount in excess of `_calculateRestakedBalanceGwei(MAX_VALIDATOR_BALANCE_GWEI)` is immediately withdrawn (see [`DelayedWithdrawalRouter.createDelayedWithdrawal`](#delayedwithdrawalroutercreatedelayedwithdrawal)) + * Any withdrawal amount in excess of `_calculateRestakedBalanceGwei(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR)` is immediately withdrawn (see [`DelayedWithdrawalRouter.createDelayedWithdrawal`](#delayedwithdrawalroutercreatedelayedwithdrawal)) * The remainder must be withdrawn through `EigenPodManager.queueWithdrawal`, but in the meantime is added to `EigenPod.withdrawableRestakedExecutionLayerGwei` * If the amount being withdrawn is not equal to the current accounted-for validator balance, a `shareDelta` is calculated to be sent to ([`EigenPodManager.recordBeaconChainETHBalanceUpdate`](#eigenpodmanagerrecordbeaconchainethbalanceupdate)). * The validator's info is updated to reflect its `WITHDRAWN` status: diff --git a/docs/outdated/EigenPods.md b/docs/outdated/EigenPods.md index 45b9c9066..69e6d72f3 100644 --- a/docs/outdated/EigenPods.md +++ b/docs/outdated/EigenPods.md @@ -34,7 +34,7 @@ The following sections are all related to managing Consensus Layer (CL) and Exec When EigenPod contracts are initially deployed, the "restaking" functionality is turned off - the withdrawal credential proof has not been initiated yet. In this "non-restaking" mode, the contract may be used by its owner freely to withdraw validator balances from the beacon chain via the `withdrawBeforeRestaking` function. This function routes the withdrawn balance directly to the `DelayedWithdrawalRouter` contract. Once the EigenPod's owner verifies that their withdrawal credentials are pointed to the EigenPod via `verifyWithdrawalCredentialsAndBalance`, the `hasRestaked` flag will be set to true and any withdrawals must now be proven for via the `verifyAndProcessWithdrawal` function. ### Merkle Proof of Correctly Pointed Withdrawal Credentials -After staking an Ethereum validator with its withdrawal credentials pointed to their EigenPod, a staker must show that the new validator exists and has its withdrawal credentials pointed to the EigenPod, by proving it against a beacon state root with a call to `verifyWithdrawalCredentialsAndBalance`. The EigenPod will verify the proof (along with checking for replays and other conditions) and, if the ETH validator's effective balance is proven to be greater than or equal to `MAX_VALIDATOR_BALANCE_GWEI`, then the EigenPod will pass the validator's effective balance value through its own hysteresis calculation (see [here](#hysteresis)), which effectively underestimates the effective balance of the validator by 1 ETH. Then a call is made to the EigenPodManager to forward a call to the StrategyManager, crediting the staker with those shares of the virtual beacon chain ETH strategy. +After staking an Ethereum validator with its withdrawal credentials pointed to their EigenPod, a staker must show that the new validator exists and has its withdrawal credentials pointed to the EigenPod, by proving it against a beacon state root with a call to `verifyWithdrawalCredentialsAndBalance`. The EigenPod will verify the proof (along with checking for replays and other conditions) and, if the ETH validator's effective balance is proven to be greater than or equal to `MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR`, then the EigenPod will pass the validator's effective balance value through its own hysteresis calculation (see [here](#hysteresis)), which effectively underestimates the effective balance of the validator by 1 ETH. Then a call is made to the EigenPodManager to forward a call to the StrategyManager, crediting the staker with those shares of the virtual beacon chain ETH strategy. ### Effective Restaked Balance - Hysteresis {#hysteresis} To convey to EigenLayer that an EigenPod has validator(s) restaked on it, anyone can submit a proof against a beacon chain state root the proves that a validator has their withdrawal credentials pointed to the pod. The proof is verified and the EigenPod calls the EigenPodMananger that calls the StrategyManager which records the validators proven balance run through the hysteresis function worth of ETH in the "beaconChainETH" strategy. Each EigenPod keeps track of all of the validators by the hash of their public key. For each validator, their validator index and current balance in EigenLayer is kept track of. @@ -54,9 +54,9 @@ We also must prove the `executionPayload.blockNumber > mostRecentWithdrawalBlock In this second case, in order to withdraw their balance from the EigenPod, stakers must provide a valid proof of their full withdrawal against a beacon chain state root. Full withdrawals are differentiated from partial withdrawals by checking against the validator in question's 'withdrawable epoch'; 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 at or after the withdrawable epoch has passed. Once the full withdrawal is successfully verified, there are 2 cases, each handled slightly differently: -1. If the withdrawn amount is greater than `MAX_VALIDATOR_BALANCE_GWEI_GWEI`, then `MAX_VALIDATOR_BALANCE_GWEI` is held for processing through EigenLayer's normal withdrawal path, while the excess amount above `MAX_VALIDATOR_BALANCE_GWEI` is marked as instantly withdrawable. +1. If the withdrawn amount is greater than `MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR`, then `MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR` is held for processing through EigenLayer's normal withdrawal path, while the excess amount above `MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR` is marked as instantly withdrawable. -2. If the withdrawn amount is less than `MAX_VALIDATOR_BALANCE_GWEI`, then the amount being withdrawn is held for processing through EigenLayer's normal withdrawal path. +2. If the withdrawn amount is less than `MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR`, then the amount being withdrawn is held for processing through EigenLayer's normal withdrawal path. ### The EigenPod Invariant The core complexity of the EigenPods system is to ensure that EigenLayer continuously has an accurate picture of the state of the beacon chain balances repointed to it. In other words, the invariant that governs this system is: diff --git a/script/M1_Deploy.s.sol b/script/M1_Deploy.s.sol index e84a0f2eb..ea57695b2 100644 --- a/script/M1_Deploy.s.sol +++ b/script/M1_Deploy.s.sol @@ -77,9 +77,9 @@ contract Deployer_M1 is Script, Test { // IMMUTABLES TO SET uint256 REQUIRED_BALANCE_WEI; - uint256 MAX_VALIDATOR_BALANCE_GWEI; + uint256 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; uint256 EFFECTIVE_RESTAKED_BALANCE_OFFSET_GWEI; - uint64 GENESIS_TIME = 1616508000; + uint64 GOERLI_GENESIS_TIME = 1616508000; // OTHER DEPLOYMENT PARAMETERS uint256 STRATEGY_MANAGER_INIT_PAUSED_STATUS; @@ -113,7 +113,7 @@ contract Deployer_M1 is Script, Test { DELAYED_WITHDRAWAL_ROUTER_INIT_WITHDRAWAL_DELAY_BLOCKS = uint32(stdJson.readUint(config_data, ".strategyManager.init_withdrawal_delay_blocks")); REQUIRED_BALANCE_WEI = stdJson.readUint(config_data, ".eigenPod.REQUIRED_BALANCE_WEI"); - MAX_VALIDATOR_BALANCE_GWEI = stdJson.readUint(config_data, ".eigenPod.MAX_VALIDATOR_BALANCE_GWEI"); + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = stdJson.readUint(config_data, ".eigenPod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR"); EFFECTIVE_RESTAKED_BALANCE_OFFSET_GWEI = stdJson.readUint(config_data, ".eigenPod.EFFECTIVE_RESTAKED_BALANCE_OFFSET_GWEI"); // tokens to deploy strategies for @@ -176,8 +176,9 @@ contract Deployer_M1 is Script, Test { ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, - uint64(MAX_VALIDATOR_BALANCE_GWEI), - uint64(EFFECTIVE_RESTAKED_BALANCE_OFFSET_GWEI) + uint64(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR), + uint64(EFFECTIVE_RESTAKED_BALANCE_OFFSET_GWEI), + GOERLI_GENESIS_TIME ); eigenPodBeacon = new UpgradeableBeacon(address(eigenPodImplementation)); diff --git a/script/M1_deploy.config.json b/script/M1_deploy.config.json index a7da10aa9..3829ad7b2 100644 --- a/script/M1_deploy.config.json +++ b/script/M1_deploy.config.json @@ -38,7 +38,7 @@ { "PARTIAL_WITHDRAWAL_FRAUD_PROOF_PERIOD_BLOCKS": 50400, "REQUIRED_BALANCE_WEI": "31000000000000000000", - "MAX_VALIDATOR_BALANCE_GWEI": "32000000000", + "MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR": "31000000000", "EFFECTIVE_RESTAKED_BALANCE_OFFSET_GWEI": "750000000" }, "eigenPodManager": diff --git a/script/testing/M2_Deploy_From_Scratch.s.sol b/script/testing/M2_Deploy_From_Scratch.s.sol index 46b2b072c..1fc9c130e 100644 --- a/script/testing/M2_Deploy_From_Scratch.s.sol +++ b/script/testing/M2_Deploy_From_Scratch.s.sol @@ -77,9 +77,9 @@ contract Deployer_M2 is Script, Test { StrategyBaseTVLLimits[] public deployedStrategyArray; // IMMUTABLES TO SET - uint64 MAX_VALIDATOR_BALANCE_GWEI; + uint64 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; uint64 RESTAKED_BALANCE_OFFSET_GWEI; - uint64 GENESIS_TIME = 1616508000; + uint64 GOERLI_GENESIS_TIME = 1616508000; // OTHER DEPLOYMENT PARAMETERS uint256 STRATEGY_MANAGER_INIT_PAUSED_STATUS; @@ -113,7 +113,7 @@ contract Deployer_M2 is Script, Test { STRATEGY_MANAGER_INIT_WITHDRAWAL_DELAY_BLOCKS = uint32(stdJson.readUint(config_data, ".strategyManager.init_withdrawal_delay_blocks")); DELAYED_WITHDRAWAL_ROUTER_INIT_WITHDRAWAL_DELAY_BLOCKS = uint32(stdJson.readUint(config_data, ".strategyManager.init_withdrawal_delay_blocks")); - MAX_VALIDATOR_BALANCE_GWEI = uint64(stdJson.readUint(config_data, ".eigenPod.MAX_VALIDATOR_BALANCE_GWEI")); + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = uint64(stdJson.readUint(config_data, ".eigenPod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR")); RESTAKED_BALANCE_OFFSET_GWEI = uint64(stdJson.readUint(config_data, ".eigenPod.RESTAKED_BALANCE_OFFSET_GWEI")); // tokens to deploy strategies for @@ -176,8 +176,9 @@ contract Deployer_M2 is Script, Test { ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, - MAX_VALIDATOR_BALANCE_GWEI, - RESTAKED_BALANCE_OFFSET_GWEI + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + RESTAKED_BALANCE_OFFSET_GWEI, + GOERLI_GENESIS_TIME ); eigenPodBeacon = new UpgradeableBeacon(address(eigenPodImplementation)); @@ -444,9 +445,9 @@ contract Deployer_M2 is Script, Test { // "strategyManager: withdrawalDelayBlocks initialized incorrectly"); // require(delayedWithdrawalRouter.withdrawalDelayBlocks() == 7 days / 12 seconds, // "delayedWithdrawalRouter: withdrawalDelayBlocks initialized incorrectly"); - // uint256 MAX_VALIDATOR_BALANCE_GWEI = 31 ether; - require(eigenPodImplementation.MAX_VALIDATOR_BALANCE_GWEI() == 31 gwei, - "eigenPod: MAX_VALIDATOR_BALANCE_GWEI initialized incorrectly"); + // uint256 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 31 ether; + require(eigenPodImplementation.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() == 31 gwei, + "eigenPod: MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR initialized incorrectly"); require(strategyManager.strategyWhitelister() == operationsMultisig, "strategyManager: strategyWhitelister address not set correctly"); diff --git a/script/testing/M2_deploy_from_scratch.anvil.config.json b/script/testing/M2_deploy_from_scratch.anvil.config.json index 4b938fdbc..639a0825b 100644 --- a/script/testing/M2_deploy_from_scratch.anvil.config.json +++ b/script/testing/M2_deploy_from_scratch.anvil.config.json @@ -12,7 +12,7 @@ }, "eigenPod": { "PARTIAL_WITHDRAWAL_FRAUD_PROOF_PERIOD_BLOCKS": 1, - "MAX_VALIDATOR_BALANCE_GWEI": "31000000000", + "MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR": "31000000000", "RESTAKED_BALANCE_OFFSET_GWEI": "750000000" }, "eigenPodManager": { diff --git a/script/testing/M2_deploy_from_scratch.mainnet.config.json b/script/testing/M2_deploy_from_scratch.mainnet.config.json index e8aba1699..d3619b464 100644 --- a/script/testing/M2_deploy_from_scratch.mainnet.config.json +++ b/script/testing/M2_deploy_from_scratch.mainnet.config.json @@ -35,7 +35,7 @@ "eigenPod": { "PARTIAL_WITHDRAWAL_FRAUD_PROOF_PERIOD_BLOCKS": 50400, - "MAX_VALIDATOR_BALANCE_GWEI": "31000000000", + "MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR": "31000000000", "RESTAKED_BALANCE_OFFSET_GWEI": "750000000" }, diff --git a/script/upgrade/GoerliM2Upgrade.s.sol b/script/upgrade/GoerliM2Upgrade.s.sol index 7c8bf4d46..4bb5c9da4 100644 --- a/script/upgrade/GoerliM2Upgrade.s.sol +++ b/script/upgrade/GoerliM2Upgrade.s.sol @@ -86,8 +86,9 @@ contract GoerliM2Deployment is Script, Test { _ethPOS: ethPOS, _delayedWithdrawalRouter: delayedWithdrawalRouter, _eigenPodManager: eigenPodManager, - _MAX_VALIDATOR_BALANCE_GWEI: 31 gwei, - _RESTAKED_BALANCE_OFFSET_GWEI: 0.5 gwei + _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR: 31 gwei, + _RESTAKED_BALANCE_OFFSET_GWEI: 0.5 gwei, + _GENESIS_TIME: 1616508000 }); // write the output to a contract diff --git a/script/upgrade/GoerliUpgrade1.s.sol b/script/upgrade/GoerliUpgrade1.s.sol index da1e337c5..9459c7786 100644 --- a/script/upgrade/GoerliUpgrade1.s.sol +++ b/script/upgrade/GoerliUpgrade1.s.sol @@ -73,7 +73,8 @@ contract GoerliUpgrade1 is Script, Test { delayedWithdrawalRouter, eigenPodManager, 32e9, - 75e7 + 75e7, + 1616508000 ) ); diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index db5f7632a..aa32b393f 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -96,11 +96,14 @@ interface IEigenPod { /// @notice The max amount of eth, in gwei, that can be restaked per validator - function MAX_VALIDATOR_BALANCE_GWEI() external view returns (uint64); + function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64); /// @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); + /// @notice any ETH deposited into the EigenPod contract via the `receive` fallback function + function nonBeaconChainETHBalanceWei() external view returns (uint256); + /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager function initialize(address owner) external; diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index 37128c02f..e6d728916 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -285,6 +285,11 @@ library BeaconChainProofs { "BeaconChainProofs.verifyWithdrawal: withdrawalIndex is too large" ); + require( + withdrawalProof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT, + "BeaconChainProofs.verifyWithdrawal: historicalSummaryIndex is too large" + ); + require( withdrawalProof.withdrawalProof.length == 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + WITHDRAWALS_TREE_HEIGHT + 1), diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 94314e995..5e98aa388 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -19,7 +19,6 @@ import "../interfaces/IDelayedWithdrawalRouter.sol"; import "../interfaces/IPausable.sol"; import "./EigenPodPausingConstants.sol"; - /** * @title The implementation contract used for restaking beacon chain ETH on EigenLayer * @author Layr Labs, Inc. @@ -45,6 +44,9 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen /// @notice Maximum "staleness" of a Beacon Chain state root against which `verifyBalanceUpdate` or `verifyWithdrawalCredentials` may be proven. uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; + /// @notice The number of seconds in a slot in the beacon chain + uint256 internal constant SECONDS_PER_SLOT = 12; + /// @notice This is the beacon chain deposit contract IETHPOSDeposit public immutable ethPOS; @@ -54,8 +56,8 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen /// @notice The single EigenPodManager for EigenLayer IEigenPodManager public immutable eigenPodManager; - ///@notice The maximum amount of ETH, in gwei, a validator can have staked in the beacon chain - uint64 public immutable MAX_VALIDATOR_BALANCE_GWEI; + ///@notice The maximum amount of ETH, in gwei, a validator can have restaked in the eigenlayer + uint64 public immutable MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; /** * @notice The value used in our effective restaked balance calculation, to set the @@ -63,6 +65,9 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen */ uint64 public immutable RESTAKED_BALANCE_OFFSET_GWEI; + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + // STORAGE VARIABLES /// @notice The owner of this EigenPod address public podOwner; @@ -141,14 +146,16 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen IETHPOSDeposit _ethPOS, IDelayedWithdrawalRouter _delayedWithdrawalRouter, IEigenPodManager _eigenPodManager, - uint64 _MAX_VALIDATOR_BALANCE_GWEI, - uint64 _RESTAKED_BALANCE_OFFSET_GWEI + uint64 _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + uint64 _RESTAKED_BALANCE_OFFSET_GWEI, + uint64 _GENESIS_TIME ) { ethPOS = _ethPOS; delayedWithdrawalRouter = _delayedWithdrawalRouter; eigenPodManager = _eigenPodManager; - MAX_VALIDATOR_BALANCE_GWEI = _MAX_VALIDATOR_BALANCE_GWEI; + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; RESTAKED_BALANCE_OFFSET_GWEI = _RESTAKED_BALANCE_OFFSET_GWEI; + GENESIS_TIME = _GENESIS_TIME; _disableInitializers(); } @@ -203,6 +210,23 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen "EigenPod.verifyBalanceUpdate: specified timestamp is too far in past" ); + uint64 validatorBalance = BeaconChainProofs.getBalanceFromBalanceRoot(validatorIndex, balanceUpdateProof.balanceRoot); + + /** + * Reference: + * uint64 validatorWithdrawableEpoch = Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); + * uint64 oracleEpoch = _computeSlotAtTimestamp(oracleTimestamp)) / BeaconChainProofs.SLOTS_PER_EPOCH; + * require(validatorWithdrawableEpoch > oracleEpoch) + * checks that a balance update can only be made before the validator is withdrawable. If this is not checked + * anyone can prove the withdrawn validator's balance as 0 before the validator is able to prove their full withdrawal + */ + if ( + Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]) <= + (_computeSlotAtTimestamp(oracleTimestamp)) / BeaconChainProofs.SLOTS_PER_EPOCH + ) { + require(validatorBalance > 0, "EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn"); + } + bytes32 validatorPubkeyHash = validatorFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX]; ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkeyHash]; @@ -248,9 +272,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen uint64 currentRestakedBalanceGwei = validatorInfo.restakedBalanceGwei; // deserialize the balance field from the balanceRoot and calculate the effective (pessimistic) restaked balance - uint64 newRestakedBalanceGwei = _calculateRestakedBalanceGwei( - BeaconChainProofs.getBalanceFromBalanceRoot(validatorIndex, balanceUpdateProof.balanceRoot) - ); + uint64 newRestakedBalanceGwei = _calculateRestakedBalanceGwei(validatorBalance); // update the balance validatorInfo.restakedBalanceGwei = newRestakedBalanceGwei; @@ -667,20 +689,18 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen * in the beacon chain as a full withdrawal. Thus such a validator can prove another full withdrawal, and * withdraw that ETH via the queuedWithdrawal flow in the strategy manager. */ - // if the withdrawal amount is greater than the MAX_VALIDATOR_BALANCE_GWEI (i.e. the max amount restaked on EigenLayer, per ETH validator) - uint64 maxRestakedBalanceGwei = _calculateRestakedBalanceGwei(MAX_VALIDATOR_BALANCE_GWEI); - if (withdrawalAmountGwei > maxRestakedBalanceGwei) { + // if the withdrawal amount is greater than the MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR (i.e. the max amount restaked on EigenLayer, per ETH validator) + if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { // then the excess is immediately withdrawable verifiedWithdrawal.amountToSend = - uint256(withdrawalAmountGwei - maxRestakedBalanceGwei) * + uint256(withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * uint256(GWEI_TO_WEI); - // and the extra execution layer ETH in the contract is MAX_VALIDATOR_BALANCE_GWEI, which must be withdrawn through EigenLayer's normal withdrawal process - withdrawableRestakedExecutionLayerGwei += maxRestakedBalanceGwei; - withdrawalAmountWei = maxRestakedBalanceGwei * GWEI_TO_WEI; + // and the extra execution layer ETH in the contract is MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, which must be withdrawn through EigenLayer's normal withdrawal process + withdrawableRestakedExecutionLayerGwei += MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + withdrawalAmountWei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; } else { // otherwise, just use the full withdrawal amount to continue to "back" the podOwner's remaining shares in EigenLayer // (i.e. none is instantly withdrawable) - withdrawalAmountGwei = _calculateRestakedBalanceGwei(withdrawalAmountGwei); withdrawableRestakedExecutionLayerGwei += withdrawalAmountGwei; withdrawalAmountWei = withdrawalAmountGwei * GWEI_TO_WEI; } @@ -727,6 +747,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen function _processWithdrawalBeforeRestaking(address _podOwner) internal { mostRecentWithdrawalTimestamp = uint32(block.timestamp); + nonBeaconChainETHBalanceWei = 0; _sendETH_AsDelayedWithdrawal(_podOwner, address(this).balance); } @@ -749,7 +770,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen */ // slither-disable-next-line divide-before-multiply uint64 effectiveBalanceGwei = uint64(((amountGwei - RESTAKED_BALANCE_OFFSET_GWEI) / GWEI_TO_WEI) * GWEI_TO_WEI); - return uint64(MathUpgradeable.min(MAX_VALIDATOR_BALANCE_GWEI, effectiveBalanceGwei)); + return uint64(MathUpgradeable.min(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, effectiveBalanceGwei)); } function _podWithdrawalCredentials() internal view returns (bytes memory) { @@ -760,6 +781,13 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen return (int256(newAmountWei) - int256(currentAmountWei)); } + // reference: https://github.com/ethereum/consensus-specs/blob/ce240ca795e257fc83059c4adfd591328c7a7f21/specs/bellatrix/beacon-chain.md#compute_timestamp_at_slot + function _computeSlotAtTimestamp(uint64 timestamp) internal view returns (uint64) { + require(timestamp >= GENESIS_TIME, "EigenPod._computeSlotAtTimestamp: timestamp is before genesis"); + return uint64((timestamp - GENESIS_TIME) / SECONDS_PER_SLOT); + } + + /** * @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/test/DepositWithdraw.t.sol b/src/test/DepositWithdraw.t.sol index 228cfe01c..b25323fe3 100644 --- a/src/test/DepositWithdraw.t.sol +++ b/src/test/DepositWithdraw.t.sol @@ -504,7 +504,7 @@ contract DepositWithdrawTests is EigenLayerTestHelper { ); ethPOSDeposit = new ETHPOSDepositMock(); - pod = new EigenPod(ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, MAX_VALIDATOR_BALANCE_GWEI, EFFECTIVE_RESTAKED_BALANCE_OFFSET); + pod = new EigenPod(ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, EFFECTIVE_RESTAKED_BALANCE_OFFSET, GOERLI_GENESIS_TIME); eigenPodBeacon = new UpgradeableBeacon(address(pod)); diff --git a/src/test/EigenLayerDeployer.t.sol b/src/test/EigenLayerDeployer.t.sol index 2697ac71f..aed8dabab 100644 --- a/src/test/EigenLayerDeployer.t.sol +++ b/src/test/EigenLayerDeployer.t.sol @@ -79,9 +79,9 @@ contract EigenLayerDeployer is Operators { uint32 PARTIAL_WITHDRAWAL_FRAUD_PROOF_PERIOD_BLOCKS = 7 days / 12 seconds; uint256 REQUIRED_BALANCE_WEI = 31 ether; uint64 MAX_PARTIAL_WTIHDRAWAL_AMOUNT_GWEI = 1 ether / 1e9; - uint64 MAX_VALIDATOR_BALANCE_GWEI = 32e9; + uint64 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; uint64 EFFECTIVE_RESTAKED_BALANCE_OFFSET = 75e7; - uint64 GENESIS_TIME = 1616508000; + uint64 GOERLI_GENESIS_TIME = 1616508000; address pauser; address unpauser; @@ -170,7 +170,7 @@ contract EigenLayerDeployer is Operators { beaconChainOracleAddress = address(new BeaconChainOracleMock()); ethPOSDeposit = new ETHPOSDepositMock(); - pod = new EigenPod(ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, MAX_VALIDATOR_BALANCE_GWEI, EFFECTIVE_RESTAKED_BALANCE_OFFSET); + pod = new EigenPod(ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, EFFECTIVE_RESTAKED_BALANCE_OFFSET, GOERLI_GENESIS_TIME); eigenPodBeacon = new UpgradeableBeacon(address(pod)); @@ -250,7 +250,7 @@ contract EigenLayerDeployer is Operators { ); ethPOSDeposit = new ETHPOSDepositMock(); - pod = new EigenPod(ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, MAX_VALIDATOR_BALANCE_GWEI, EFFECTIVE_RESTAKED_BALANCE_OFFSET); + pod = new EigenPod(ethPOSDeposit, delayedWithdrawalRouter, eigenPodManager, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, EFFECTIVE_RESTAKED_BALANCE_OFFSET, GOERLI_GENESIS_TIME); eigenPodBeacon = new UpgradeableBeacon(address(pod)); diff --git a/src/test/EigenPod.t.sol b/src/test/EigenPod.t.sol index 2e7daf8c5..20a15eeeb 100644 --- a/src/test/EigenPod.t.sol +++ b/src/test/EigenPod.t.sol @@ -42,6 +42,8 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { IDelayedWithdrawalRouter public delayedWithdrawalRouter; IETHPOSDeposit public ethPOSDeposit; IBeacon public eigenPodBeacon; + EPInternalFunctions public podInternalFunctionTester; + BeaconChainOracleMock public beaconChainOracle; MiddlewareRegistryMock public generalReg1; ServiceManagerMock public generalServiceManager1; @@ -59,9 +61,9 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { bytes32[] validatorFields; uint32 WITHDRAWAL_DELAY_BLOCKS = 7 days / 12 seconds; - uint64 MAX_VALIDATOR_BALANCE_GWEI = 32e9; + uint64 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 31e9; uint64 RESTAKED_BALANCE_OFFSET_GWEI = 75e7; - uint64 internal constant GENESIS_TIME = 1616508000; + uint64 internal constant GOERLI_GENESIS_TIME = 1616508000; uint64 internal constant SECONDS_PER_SLOT = 12; // bytes validatorPubkey = hex"93a0dd04ccddf3f1b419fdebf99481a2182c17d67cf14d32d6e50fc4bf8effc8db4a04b7c2f3a5975c1b9b74e2841888"; @@ -152,9 +154,11 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { ethPOSDeposit, delayedWithdrawalRouter, IEigenPodManager(podManagerAddress), - MAX_VALIDATOR_BALANCE_GWEI, - RESTAKED_BALANCE_OFFSET_GWEI + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + RESTAKED_BALANCE_OFFSET_GWEI, + GOERLI_GENESIS_TIME ); + eigenPodBeacon = new UpgradeableBeacon(address(podImplementation)); // this contract is deployed later to keep its address the same (for these tests) @@ -237,7 +241,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { strategyManager ); - cheats.deal(address(podOwner), 5*stakeAmount); + cheats.deal(address(podOwner), 5*stakeAmount); fuzzedAddressMapping[address(0)] = true; fuzzedAddressMapping[address(eigenLayerProxyAdmin)] = true; @@ -276,25 +280,16 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { require(pod.mostRecentWithdrawalTimestamp() == uint64(block.timestamp), "Most recent withdrawal block number not updated"); } - - function testCheckThatHasRestakedIsSetToTrue() public { - testStaking(); - IEigenPod pod = eigenPodManager.getPod(podOwner); - require(pod.hasRestaked() == true, "Pod should not be restaked"); - } - function testDeployEigenPodWithoutActivateRestaking() public { // ./solidityProofGen "ValidatorFieldsProof" 302913 true "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "withdrawal_credential_proof_510257.json" setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); - IEigenPod newPod = eigenPodManager.getPod(podOwner); cheats.startPrank(podOwner); eigenPodManager.stake{value: stakeAmount}(pubkey, signature, depositDataRoot); cheats.stopPrank(); - uint64 timestamp = 0; bytes32[][] memory validatorFieldsArray = new bytes32[][](1); validatorFieldsArray[0] = getValidatorFields(); bytes[] memory proofsArray = new bytes[](1); @@ -302,14 +297,15 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); uint40[] memory validatorIndices = new uint40[](1); validatorIndices[0] = uint40(getValidatorIndex()); + BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(getLatestBlockRoot()); //this simulates that hasRestaking is set to false, as would be the case for deployed pods that have not yet restaked prior to M2 - cheats.store(address(newPod), bytes32(uint256(52)), bytes32(uint256(1))); + cheats.store(address(newPod), bytes32(uint256(52)), bytes32(uint256(0))); cheats.startPrank(podOwner); - cheats.warp(timestamp += 1); + cheats.warp(GOERLI_GENESIS_TIME); cheats.expectRevert(bytes("EigenPod.hasEnabledRestaking: restaking is not enabled")); - newPod.verifyWithdrawalCredentials(timestamp, stateRootProofStruct, validatorIndices, proofsArray, validatorFieldsArray); + newPod.verifyWithdrawalCredentials(GOERLI_GENESIS_TIME, stateRootProofStruct, validatorIndices, proofsArray, validatorFieldsArray); cheats.stopPrank(); } @@ -338,18 +334,6 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { cheats.stopPrank(); } - function testWithdrawBeforeRestakingAfterRestaking() public { - // ./solidityProofGen "ValidatorFieldsProof" 302913 true "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "withdrawal_credential_proof_510257.json" - setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); - - IEigenPod pod = _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); - - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - cheats.startPrank(podOwner); - pod.withdrawBeforeRestaking(); - cheats.stopPrank(); - } - function testWithdrawFromPod() public { cheats.startPrank(podOwner); eigenPodManager.stake{value: stakeAmount}(pubkey, signature, depositDataRoot); @@ -371,15 +355,6 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { require(address(pod).balance == 0, "Pod balance should be 0"); } - function testAttemptedWithdrawalAfterVerifyingWithdrawalCredentials() public { - testDeployAndVerifyNewEigenPod(); - IEigenPod pod = eigenPodManager.getPod(podOwner); - cheats.startPrank(podOwner); - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - IEigenPod(pod).withdrawBeforeRestaking(); - cheats.stopPrank(); - } - function testFullWithdrawalProof() public { setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); BeaconChainProofs.WithdrawalProof memory proofs = _getWithdrawalProof(); @@ -392,6 +367,43 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { relay.verifyWithdrawal(beaconStateRoot, withdrawalFields, proofs); } + function testFullWithdrawalProofWithWrongIndices(uint64 wrongBlockRootIndex, uint64 wrongWithdrawalIndex, uint64 wrongHistoricalSummariesIndex) public { + uint256 BLOCK_ROOTS_TREE_HEIGHT = 13; + uint256 WITHDRAWALS_TREE_HEIGHT = 4; + uint256 HISTORICAL_SUMMARIES_TREE_HEIGHT = 24; + cheats.assume(wrongBlockRootIndex > 2 ** BLOCK_ROOTS_TREE_HEIGHT); + cheats.assume(wrongWithdrawalIndex > 2 ** WITHDRAWALS_TREE_HEIGHT); + cheats.assume(wrongHistoricalSummariesIndex > 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT); + + Relayer relay = new Relayer(); + + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + bytes32 beaconStateRoot = getBeaconStateRoot(); + validatorFields = getValidatorFields(); + withdrawalFields = getWithdrawalFields(); + + { + BeaconChainProofs.WithdrawalProof memory wrongProofs = _getWithdrawalProof(); + wrongProofs.blockRootIndex = wrongBlockRootIndex; + cheats.expectRevert(bytes("BeaconChainProofs.verifyWithdrawal: blockRootIndex is too large")); + relay.verifyWithdrawal(beaconStateRoot, withdrawalFields, wrongProofs); + } + + { + BeaconChainProofs.WithdrawalProof memory wrongProofs = _getWithdrawalProof(); + wrongProofs.withdrawalIndex = wrongWithdrawalIndex; + cheats.expectRevert(bytes("BeaconChainProofs.verifyWithdrawal: withdrawalIndex is too large")); + relay.verifyWithdrawal(beaconStateRoot, withdrawalFields, wrongProofs); + } + + { + BeaconChainProofs.WithdrawalProof memory wrongProofs = _getWithdrawalProof(); + wrongProofs.historicalSummaryIndex = wrongHistoricalSummariesIndex; + cheats.expectRevert(bytes("BeaconChainProofs.verifyWithdrawal: historicalSummaryIndex is too large")); + relay.verifyWithdrawal(beaconStateRoot, withdrawalFields, wrongProofs); + } + } + /// @notice This test is to ensure the full withdrawal flow works function testFullWithdrawalFlow() public returns (IEigenPod) { //this call is to ensure that validator 302913 has proven their withdrawalcreds @@ -404,48 +416,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { // To get block header: curl -H "Accept: application/json" 'https://eigenlayer.spiceai.io/goerli/beacon/eth/v1/beacon/headers/6399000?api_key\="343035|f6ebfef661524745abb4f1fd908a76e8"' > block_header_6399000.json // To get block: curl -H "Accept: application/json" 'https://eigenlayer.spiceai.io/goerli/beacon/eth/v2/beacon/blocks/6399000?api_key\="343035|f6ebfef661524745abb4f1fd908a76e8"' > block_6399000.json setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); - BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(getLatestBlockRoot()); - uint64 restakedExecutionLayerGweiBefore = newPod.withdrawableRestakedExecutionLayerGwei(); - - withdrawalFields = getWithdrawalFields(); - uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); - - uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - _calculateRestakedBalanceGwei(newPod.MAX_VALIDATOR_BALANCE_GWEI())) * uint64(GWEI_TO_WEI); - uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); - cheats.deal(address(newPod), leftOverBalanceWEI); - emit log_named_uint("leftOverBalanceWEI", leftOverBalanceWEI); - emit log_named_uint("address(newPod)", address(newPod).balance); - emit log_named_uint("withdrawalAmountGwei", withdrawalAmountGwei); - - uint256 delayedWithdrawalRouterContractBalanceBefore = address(delayedWithdrawalRouter).balance; - { - BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](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; - - BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); - - - //cheats.expectEmit(true, true, true, true, address(newPod)); - emit FullWithdrawalRedeemed(validatorIndex, _computeTimestampAtSlot(Endian.fromLittleEndianUint64(withdrawalProofsArray[0].slotRoot)), podOwner, withdrawalAmountGwei); - newPod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); - } - require(newPod.withdrawableRestakedExecutionLayerGwei() - restakedExecutionLayerGweiBefore == _calculateRestakedBalanceGwei(newPod.MAX_VALIDATOR_BALANCE_GWEI()), - "restakedExecutionLayerGwei has not been incremented correctly"); - require(address(delayedWithdrawalRouter).balance - delayedWithdrawalRouterContractBalanceBefore == leftOverBalanceWEI, - "pod delayed withdrawal balance hasn't been updated correctly"); - require(newPod.validatorPubkeyHashToInfo(getValidatorPubkeyHash()).restakedBalanceGwei == 0, "balance not reset correctly"); - - cheats.roll(block.number + WITHDRAWAL_DELAY_BLOCKS + 1); - uint256 podOwnerBalanceBefore = address(podOwner).balance; - delayedWithdrawalRouter.claimDelayedWithdrawals(podOwner, 1); - require(address(podOwner).balance - podOwnerBalanceBefore == leftOverBalanceWEI, "Pod owner balance hasn't been updated correctly"); - return newPod; + return _proveWithdrawalForPod(newPod); } /** @@ -463,7 +434,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); withdrawalFields = getWithdrawalFields(); uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); - uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - _calculateRestakedBalanceGwei(pod.MAX_VALIDATOR_BALANCE_GWEI())) * uint64(GWEI_TO_WEI); + uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - _calculateRestakedBalanceGwei(pod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR())) * uint64(GWEI_TO_WEI); cheats.deal(address(pod), leftOverBalanceWEI); { BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); @@ -583,14 +554,15 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { /// @notice verifies that multiple full withdrawals for a single validator fail function testDoubleFullWithdrawal() public returns(IEigenPod newPod) { newPod = testFullWithdrawalFlow(); + uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); + uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - newPod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR()) * uint64(GWEI_TO_WEI); + cheats.deal(address(newPod), leftOverBalanceWEI); + + BeaconChainProofs.WithdrawalProof memory withdrawalProofs = _getWithdrawalProof(); bytes memory validatorFieldsProof = abi.encodePacked(getValidatorProof()); withdrawalFields = getWithdrawalFields(); validatorFields = getValidatorFields(); - - uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); - uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - newPod.MAX_VALIDATOR_BALANCE_GWEI()) * uint64(GWEI_TO_WEI); - cheats.deal(address(newPod), leftOverBalanceWEI); BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); withdrawalProofsArray[0] = withdrawalProofs; @@ -623,7 +595,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); IEigenPod newPod = eigenPodManager.getPod(podOwner); - cheats.roll(block.number + 1); + cheats.warp(GOERLI_GENESIS_TIME); // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); _proveOverCommittedStake(newPod); @@ -746,7 +718,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { uint256 beaconChainETHAfter = eigenPodManager.podOwnerShares(pod.podOwner()); emit log_named_uint("beaconChainETHBefore", beaconChainETHBefore); emit log_named_uint("beaconChainETHAfter", beaconChainETHAfter); - assertTrue(beaconChainETHAfter - beaconChainETHBefore == _calculateRestakedBalanceGwei(pod.MAX_VALIDATOR_BALANCE_GWEI())*GWEI_TO_WEI, "pod balance not updated correcty"); + assertTrue(beaconChainETHAfter - beaconChainETHBefore == (pod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR())*GWEI_TO_WEI, "pod balance not updated correcty"); assertTrue(pod.validatorStatus(validatorPubkeyHash) == IEigenPod.VALIDATOR_STATUS.ACTIVE, "wrong validator status"); } @@ -769,7 +741,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); // prove overcommitted balance - cheats.roll(block.number + 1); + cheats.warp(GOERLI_GENESIS_TIME); _proveOverCommittedStake(newPod); uint256 validatorRestakedBalanceAfter = newPod.validatorPubkeyHashToInfo(validatorPubkeyHash).restakedBalanceGwei; @@ -794,13 +766,12 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); // prove overcommitted balance - cheats.roll(block.number + 1); + cheats.warp(GOERLI_GENESIS_TIME); _proveOverCommittedStake(newPod); - cheats.roll(block.number + 1); + cheats.warp(block.timestamp + 1); // ./solidityProofGen "BalanceUpdateProof" 302913 false 100 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_notOverCommitted_302913_incrementedBlockBy100.json" setJSON("./src/test/test-data/balanceUpdateProof_notOverCommitted_302913_incrementedBlockBy100.json"); - cheats.roll(block.number + 1); _proveUnderCommittedStake(newPod); uint256 validatorRestakedBalanceAfter = newPod.validatorPubkeyHashToInfo(validatorPubkeyHash).restakedBalanceGwei; @@ -821,7 +792,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); // prove overcommitted balance - cheats.roll(block.number + 1); + cheats.warp(GOERLI_GENESIS_TIME); _proveOverCommittedStake(newPod); // ./solidityProofGen "BalanceUpdateProof" 302913 false 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_notOverCommitted_302913.json" @@ -834,7 +805,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); cheats.expectRevert(bytes("EigenPod.verifyBalanceUpdate: Validators balance has already been updated for this timestamp")); - newPod.verifyBalanceUpdate(uint64(block.number), validatorIndex, stateRootProofStruct, proofs, validatorFields); + newPod.verifyBalanceUpdate(uint64(block.timestamp), validatorIndex, stateRootProofStruct, proofs, validatorFields); } function testDeployingEigenPodRevertsWhenPaused() external { @@ -881,8 +852,10 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { cheats.assume(nonPodOwner != podOwner); testStaking(); IEigenPod pod = eigenPodManager.getPod(podOwner); + bool restakedStatus = false; + // this is testing if pods deployed before M2 that do not have hasRestaked initialized to true, will revert - cheats.store(address(pod), bytes32(uint256(52)), bytes32(uint256(1))); + cheats.store(address(pod), bytes32(uint256(52)), bytes32(0)); require(pod.hasRestaked() == false, "Pod should not be restaked"); //simulate a withdrawal @@ -980,7 +953,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); //cheats.expectEmit(true, true, true, true, address(newPod)); emit ValidatorBalanceUpdated(validatorIndex, uint64(block.number), _calculateRestakedBalanceGwei(Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX]))); - newPod.verifyBalanceUpdate(uint64(block.number), validatorIndex, stateRootProofStruct, proofs, validatorFields); + newPod.verifyBalanceUpdate(uint64(block.timestamp), validatorIndex, stateRootProofStruct, proofs, validatorFields); } function _proveUnderCommittedStake(IEigenPod newPod) internal { @@ -993,7 +966,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { emit ValidatorBalanceUpdated(validatorIndex, uint64(block.number), _calculateRestakedBalanceGwei(Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX]))); //cheats.expectEmit(true, true, true, true, address(newPod)); - newPod.verifyBalanceUpdate(uint64(block.number), validatorIndex, stateRootProofStruct, proofs, validatorFields); + newPod.verifyBalanceUpdate(uint64(block.timestamp), validatorIndex, stateRootProofStruct, proofs, validatorFields); require(newPod.validatorPubkeyHashToInfo(getValidatorPubkeyHash()).status == IEigenPod.VALIDATOR_STATUS.ACTIVE); } @@ -1173,7 +1146,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { uint256 withdrawableRestakedExecutionLayerGweiBefore = pod.withdrawableRestakedExecutionLayerGwei(); - uint256 shareAmount = _calculateRestakedBalanceGwei(pod.MAX_VALIDATOR_BALANCE_GWEI()) * GWEI_TO_WEI; + uint256 shareAmount = _calculateRestakedBalanceGwei(pod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR()) * GWEI_TO_WEI; _verifyEigenPodBalanceSharesInvariant(podOwner, pod, validatorPubkeyHash); _testQueueWithdrawal(podOwner, shareAmount); _verifyEigenPodBalanceSharesInvariant(podOwner, pod, validatorPubkeyHash); @@ -1196,6 +1169,48 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { "EigenPod invariant violated: sharesInSM != withdrawableRestakedExecutionLayerGwei"); } + function _proveWithdrawalForPod(IEigenPod newPod) internal returns(IEigenPod) { + BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(getLatestBlockRoot()); + uint64 restakedExecutionLayerGweiBefore = newPod.withdrawableRestakedExecutionLayerGwei(); + + withdrawalFields = getWithdrawalFields(); + uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); + + uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - newPod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR()) * uint64(GWEI_TO_WEI); + uint40 validatorIndex = uint40(Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX])); + cheats.deal(address(newPod), leftOverBalanceWEI); + emit log_named_uint("leftOverBalanceWEI", leftOverBalanceWEI); + emit log_named_uint("address(newPod)", address(newPod).balance); + emit log_named_uint("withdrawalAmountGwei", withdrawalAmountGwei); + + uint256 delayedWithdrawalRouterContractBalanceBefore = address(delayedWithdrawalRouter).balance; + { + BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](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; + + BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + + newPod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); + } + require(newPod.withdrawableRestakedExecutionLayerGwei() - restakedExecutionLayerGweiBefore == newPod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR(), + "restakedExecutionLayerGwei has not been incremented correctly"); + require(address(delayedWithdrawalRouter).balance - delayedWithdrawalRouterContractBalanceBefore == leftOverBalanceWEI, + "pod delayed withdrawal balance hasn't been updated correctly"); + require(newPod.validatorPubkeyHashToInfo(getValidatorPubkeyHash()).restakedBalanceGwei == 0, "balance not reset correctly"); + + cheats.roll(block.number + WITHDRAWAL_DELAY_BLOCKS + 1); + uint256 podOwnerBalanceBefore = address(podOwner).balance; + delayedWithdrawalRouter.claimDelayedWithdrawals(podOwner, 1); + require(address(podOwner).balance - podOwnerBalanceBefore == leftOverBalanceWEI, "Pod owner balance hasn't been updated correctly"); + return newPod; + } + // simply tries to register 'sender' as a delegate, setting their 'DelegationTerms' contract in DelegationManager to 'dt' // verifies that the storage of DelegationManager contract is updated appropriately function _testRegisterAsOperator(address sender, IDelegationManager.OperatorDetails memory operatorDetails) internal { @@ -1288,6 +1303,10 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { eigenPodManager.stake{value: stakeAmount}(pubkey, _signature, _depositDataRoot); cheats.stopPrank(); + return _verifyWithdrawalCredentials(newPod, _podOwner); + } + + function _verifyWithdrawalCredentials(IEigenPod newPod, address _podOwner) internal returns(IEigenPod) { uint64 timestamp = 0; // cheats.expectEmit(true, true, true, true, address(newPod)); // emit ValidatorRestaked(validatorIndex); @@ -1428,11 +1447,22 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { * the nearest ETH, effectively calculating the floor of amountGwei. */ uint64 effectiveBalanceGwei = uint64((amountGwei - RESTAKED_BALANCE_OFFSET_GWEI) / GWEI_TO_WEI * GWEI_TO_WEI); - return uint64(MathUpgradeable.min(MAX_VALIDATOR_BALANCE_GWEI, effectiveBalanceGwei)); + return uint64(MathUpgradeable.min(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, effectiveBalanceGwei)); } function _computeTimestampAtSlot(uint64 slot) internal pure returns (uint64) { - return uint64(GENESIS_TIME + slot * SECONDS_PER_SLOT); + return uint64(GOERLI_GENESIS_TIME + slot * SECONDS_PER_SLOT); + } + + function _deployInternalFunctionTester() internal { + podInternalFunctionTester = new EPInternalFunctions( + ethPOSDeposit, + delayedWithdrawalRouter, + IEigenPodManager(podManagerAddress), + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + RESTAKED_BALANCE_OFFSET_GWEI, + GOERLI_GENESIS_TIME +); } @@ -1447,4 +1477,41 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { ) public view { BeaconChainProofs.verifyWithdrawal(beaconStateRoot, withdrawalFields, proofs); } + } + + contract EPInternalFunctions is EigenPod { + + constructor( + IETHPOSDeposit _ethPOS, + IDelayedWithdrawalRouter _delayedWithdrawalRouter, + IEigenPodManager _eigenPodManager, + uint64 _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + uint64 _RESTAKED_BALANCE_OFFSET_GWEI, + uint64 _GENESIS_TIME + ) EigenPod( + _ethPOS, + _delayedWithdrawalRouter, + _eigenPodManager, + _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + _RESTAKED_BALANCE_OFFSET_GWEI, + _GENESIS_TIME + ) {} + + function processFullWithdrawal( + uint40 validatorIndex, + bytes32 validatorPubkeyHash, + uint64 withdrawalHappenedTimestamp, + address recipient, + uint64 withdrawalAmountGwei, + ValidatorInfo memory validatorInfo + ) public { + _processFullWithdrawal( + validatorIndex, + validatorPubkeyHash, + withdrawalHappenedTimestamp, + recipient, + withdrawalAmountGwei, + validatorInfo + ); + } } \ No newline at end of file diff --git a/src/test/mocks/EigenPodMock.sol b/src/test/mocks/EigenPodMock.sol index 279101aad..c5a28ecec 100644 --- a/src/test/mocks/EigenPodMock.sol +++ b/src/test/mocks/EigenPodMock.sol @@ -6,7 +6,9 @@ import "../../contracts/interfaces/IEigenPod.sol"; contract EigenPodMock is IEigenPod, Test { /// @notice The max amount of eth, in gwei, that can be restaked per validator - function MAX_VALIDATOR_BALANCE_GWEI() external view returns(uint64) {} + function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns(uint64) {} + + function nonBeaconChainETHBalanceWei() external view returns(uint256) {} /// @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) {} diff --git a/src/test/unit/EigenPodUnit.t.sol b/src/test/unit/EigenPodUnit.t.sol new file mode 100644 index 000000000..2b973aae0 --- /dev/null +++ b/src/test/unit/EigenPodUnit.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "./../EigenPod.t.sol"; + +contract EigenPodUnitTests is EigenPodTests { + function testFullWithdrawalProofWithWrongWithdrawalFields(bytes32[] memory wrongWithdrawalFields) public { + Relayer relay = new Relayer(); + uint256 WITHDRAWAL_FIELD_TREE_HEIGHT = 2; + + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + BeaconChainProofs.WithdrawalProof memory proofs = _getWithdrawalProof(); + bytes32 beaconStateRoot = getBeaconStateRoot(); + cheats.assume(wrongWithdrawalFields.length != 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT); + validatorFields = getValidatorFields(); + + cheats.expectRevert(bytes("BeaconChainProofs.verifyWithdrawal: withdrawalFields has incorrect length")); + relay.verifyWithdrawal(beaconStateRoot, wrongWithdrawalFields, proofs); + } + + function testCheckThatHasRestakedIsSetToTrue() public returns (IEigenPod){ + testStaking(); + IEigenPod pod = eigenPodManager.getPod(podOwner); + require(pod.hasRestaked() == true, "Pod should not be restaked"); + return pod; + } + + function testActivateRestakingWithM2Pods() external { + IEigenPod pod = testCheckThatHasRestakedIsSetToTrue(); + cheats.startPrank(podOwner); + cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); + pod.activateRestaking(); + cheats.stopPrank(); + } + + function testWithdrawBeforeRestakingWithM2Pods() external { + IEigenPod pod = testCheckThatHasRestakedIsSetToTrue(); + cheats.startPrank(podOwner); + cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); + pod.withdrawBeforeRestaking(); + cheats.stopPrank(); + } + + function testAttemptedWithdrawalAfterVerifyingWithdrawalCredentials() public { + testDeployAndVerifyNewEigenPod(); + IEigenPod pod = eigenPodManager.getPod(podOwner); + cheats.startPrank(podOwner); + cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); + IEigenPod(pod).withdrawBeforeRestaking(); + cheats.stopPrank(); + } + + function testBalanceProofWithWrongTimestamp(uint64 timestamp) public { + cheats.assume(timestamp > GOERLI_GENESIS_TIME); + // ./solidityProofGen "BalanceUpdateProof" 302913 false 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_notOverCommitted_302913.json" + setJSON("./src/test/test-data/balanceUpdateProof_notOverCommitted_302913.json"); + IEigenPod newPod = _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + // get beaconChainETH shares + uint256 beaconChainETHBefore = eigenPodManager.podOwnerShares(podOwner); + bytes32 validatorPubkeyHash = getValidatorPubkeyHash(); + uint256 validatorRestakedBalanceBefore = newPod.validatorPubkeyHashToInfo(validatorPubkeyHash).restakedBalanceGwei; + + // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" + setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); + // prove overcommitted balance + cheats.warp(timestamp); + _proveOverCommittedStake(newPod); + + + validatorFields = getValidatorFields(); + uint40 validatorIndex = uint40(getValidatorIndex()); + bytes32 newLatestBlockRoot = getLatestBlockRoot(); + BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(newLatestBlockRoot); + BeaconChainProofs.BalanceUpdateProof memory proofs = _getBalanceUpdateProof(); + BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + + cheats.expectRevert(bytes("EigenPod.verifyBalanceUpdate: Validators balance has already been updated for this timestamp")); + newPod.verifyBalanceUpdate(uint64(block.timestamp - 1), validatorIndex, stateRootProofStruct, proofs, validatorFields); + } + + function testProcessFullWithdrawalForLessThanMaxRestakedBalance(uint64 withdrawalAmount) public { + _deployInternalFunctionTester(); + cheats.assume(withdrawalAmount > 0 && withdrawalAmount < MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); + IEigenPod.ValidatorInfo memory validatorInfo = IEigenPod.ValidatorInfo({ + validatorIndex: 0, + restakedBalanceGwei: 0, + mostRecentBalanceUpdateTimestamp: 0, + status: IEigenPod.VALIDATOR_STATUS.ACTIVE + }); + uint64 balanceBefore = podInternalFunctionTester.withdrawableRestakedExecutionLayerGwei(); + podInternalFunctionTester.processFullWithdrawal(0, bytes32(0), 0, podOwner, withdrawalAmount, validatorInfo); + require(podInternalFunctionTester.withdrawableRestakedExecutionLayerGwei() - balanceBefore == withdrawalAmount, "withdrawableRestakedExecutionLayerGwei hasn't been updated correctly"); + } + + + function testWithdrawBeforeRestakingAfterRestaking() public { + // ./solidityProofGen "ValidatorFieldsProof" 302913 true "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "withdrawal_credential_proof_510257.json" + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + + IEigenPod pod = _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + + cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); + cheats.startPrank(podOwner); + pod.withdrawBeforeRestaking(); + cheats.stopPrank(); + } + + //post M2, all new pods deployed will have "hasRestaked = true". THis tests that + function testDeployedPodIsRestaked(address podOwner) public { + cheats.startPrank(podOwner); + eigenPodManager.createPod(); + cheats.stopPrank(); + + IEigenPod pod = eigenPodManager.getPod(podOwner); + require(pod.hasRestaked() == true, "Pod should be restaked"); + } + + function testTryToActivateRestakingAfterHasRestakedIsSet() public { + cheats.startPrank(podOwner); + eigenPodManager.createPod(); + cheats.stopPrank(); + + IEigenPod pod = eigenPodManager.getPod(podOwner); + require(pod.hasRestaked() == true, "Pod should be restaked"); + + cheats.startPrank(podOwner); + cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); + pod.activateRestaking(); + + } + + function testTryToWithdrawBeforeRestakingAfterHasRestakedIsSet() public { + cheats.startPrank(podOwner); + eigenPodManager.createPod(); + cheats.stopPrank(); + + IEigenPod pod = eigenPodManager.getPod(podOwner); + require(pod.hasRestaked() == true, "Pod should be restaked"); + + cheats.startPrank(podOwner); + cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); + pod.withdrawBeforeRestaking(); + } + + function testMismatchedWithdrawalProofInputs(uint64 numValidators, uint64 numValidatorProofs) external { + cheats.assume(numValidators < numValidatorProofs && numValidatorProofs < 5); + + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + IEigenPod newPod = eigenPodManager.getPod(podOwner); + + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + bytes[] memory validatorFieldsProofArray = new bytes[](numValidatorProofs); + for (uint256 index = 0; index < numValidators; index++) { + validatorFieldsProofArray[index] = abi.encodePacked(getValidatorProof()); + } + bytes32[][] memory validatorFieldsArray = new bytes32[][](numValidators); + for (uint256 index = 0; index < validatorFieldsArray.length; index++) { + validatorFieldsArray[index] = getValidatorFields(); + } + + BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); + withdrawalProofsArray[0] = _getWithdrawalProof(); + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + withdrawalFieldsArray[0] = withdrawalFields; + + cheats.expectRevert(bytes("EigenPod.verifyAndProcessWithdrawals: inputs must be same length")); + newPod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); + } + + function testProveWithdrawalFromBeforeLastWithdrawBeforeRestaking() external { + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + IEigenPod pod = eigenPodManager.getPod(podOwner); + + cheats.store(address(pod), bytes32(uint256(52)), bytes32(uint256(1))); + require(pod.hasRestaked() != true, "Pod should not be restaked"); + + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(getLatestBlockRoot()); + + BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); + withdrawalProofsArray[0] = _getWithdrawalProof(); + uint64 timestampOfWithdrawal = Endian.fromLittleEndianUint64(withdrawalProofsArray[0].timestampRoot); + uint256 newTimestamp = timestampOfWithdrawal + 2500; + cheats.warp(newTimestamp); + cheats.startPrank(podOwner); + pod.withdrawBeforeRestaking(); + cheats.stopPrank(); + + + bytes[] memory validatorFieldsProofArray = new bytes[](1); + validatorFieldsProofArray[0] = abi.encodePacked(getValidatorProof()); + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + validatorFieldsArray[0] = getValidatorFields(); + + BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + withdrawalFieldsArray[0] = withdrawalFields; + cheats.warp(timestampOfWithdrawal); + + cheats.expectRevert(bytes("EigenPod.proofIsForValidTimestamp: beacon chain proof must be for timestamp after mostRecentWithdrawalTimestamp")); + pod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); + } + + function testIncrementWithdrawableRestakedExecutionLayerGwei(uint256 amount) public returns (IEigenPod) { + cheats.assume(amount > GWEI_TO_WEI); + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + IEigenPod pod = eigenPodManager.getPod(podOwner); + + uint256 withdrawableRestakedExecutionLayerGweiBefore = pod.withdrawableRestakedExecutionLayerGwei(); + + cheats.startPrank(address(eigenPodManager)); + pod.incrementWithdrawableRestakedExecutionLayerGwei(amount); + + uint256 withdrawableRestakedExecutionLayerGweiAfter = pod.withdrawableRestakedExecutionLayerGwei(); + uint64 amountGwei = uint64(amount / GWEI_TO_WEI); + require(withdrawableRestakedExecutionLayerGweiAfter == withdrawableRestakedExecutionLayerGweiBefore + amountGwei, "WithdrawableRestakedExecutionLayerGwei should have been incremented by amount"); + return pod; + } + + function testDecrementWithdrawableRestakedExecutionLayerGwei(uint256 amount) external { + IEigenPod pod = testIncrementWithdrawableRestakedExecutionLayerGwei(amount); + + uint256 withdrawableRestakedExecutionLayerGweiBefore = pod.withdrawableRestakedExecutionLayerGwei(); + pod.decrementWithdrawableRestakedExecutionLayerGwei(amount); + + uint256 withdrawableRestakedExecutionLayerGweiAfter = pod.withdrawableRestakedExecutionLayerGwei(); + + uint64 amountGwei = uint64(amount / GWEI_TO_WEI); + require(withdrawableRestakedExecutionLayerGweiAfter == withdrawableRestakedExecutionLayerGweiBefore - amountGwei, "WithdrawableRestakedExecutionLayerGwei should have been incremented by amount"); + } + + function testDecrementMoreThanRestakedExecutionLayerGwei(uint256 largerAmount) external { + cheats.assume(largerAmount > GWEI_TO_WEI); + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + IEigenPod pod = eigenPodManager.getPod(podOwner); + + cheats.startPrank(address(eigenPodManager)); + + cheats.expectRevert(bytes("EigenPod.decrementWithdrawableRestakedExecutionLayerGwei: amount to decrement is greater than current withdrawableRestakedRxecutionLayerGwei balance")); + pod.decrementWithdrawableRestakedExecutionLayerGwei(largerAmount); + cheats.stopPrank(); + } + + function testPodReceiveFallBack(uint256 amountETH) external { + cheats.assume(amountETH > 0); + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + IEigenPod pod = eigenPodManager.getPod(podOwner); + cheats.deal(address(this), amountETH); + + Address.sendValue(payable(address(pod)), amountETH); + require(address(pod).balance == amountETH, "Pod should have received ETH"); + } + + /** + * This is a regression test for a bug (EIG-14) found by Hexens. Lets say podOwner sends 32 ETH to the EigenPod, + * the nonBeaconChainETHBalanceWei increases by 32 ETH. podOwner calls withdrawBeforeRestaking, which + * will simply send the entire ETH balance (32 ETH) to the owner. The owner activates restaking, + * creates a validator and verifies the withdrawal credentials, receiving 32 ETH in shares. + * They can exit the validator, the pod gets the 32ETH and they can call withdrawNonBeaconChainETHBalanceWei + * And simply withdraw the 32ETH because nonBeaconChainETHBalanceWei is 32ETH. This was an issue because + * nonBeaconChainETHBalanceWei was never zeroed out in _processWithdrawalBeforeRestaking + */ + function testValidatorBalanceCannotBeRemovedFromPodViaNonBeaconChainETHBalanceWei() external { + cheats.startPrank(podOwner); + IEigenPod newPod = eigenPodManager.getPod(podOwner); + cheats.expectEmit(true, true, true, true, address(newPod)); + emit EigenPodStaked(pubkey); + eigenPodManager.stake{value: stakeAmount}(pubkey, signature, depositDataRoot); + cheats.stopPrank(); + + uint256 amount = 32 ether; + + cheats.store(address(newPod), bytes32(uint256(52)), bytes32(0)); + cheats.deal(address(this), amount); + // simulate a withdrawal processed on the beacon chain, pod balance goes to 32 ETH + Address.sendValue(payable(address(newPod)), amount); + require(newPod.nonBeaconChainETHBalanceWei() == amount, "nonBeaconChainETHBalanceWei should be 32 ETH"); + //simulate that hasRestaked is set to false, so that we can test withdrawBeforeRestaking for pods deployed before M2 activation + cheats.store(address(newPod), bytes32(uint256(52)), bytes32(uint256(1))); + //this is an M1 pod so hasRestaked should be false + require(newPod.hasRestaked() == false, "Pod should be restaked"); + cheats.startPrank(podOwner); + newPod.activateRestaking(); + cheats.stopPrank(); + require(newPod.nonBeaconChainETHBalanceWei() == 0, "nonBeaconChainETHBalanceWei should be 32 ETH"); + } + + /** + * Regression test for a bug that allowed balance updates to be made for withdrawn validators. Thus + * the validator's balance could be maliciously proven to be 0 before the validator themselves are + * able to prove their withdrawal. + */ + function testBalanceUpdateMadeAfterWithdrawableEpochFails() external { + //make initial deposit + // ./solidityProofGen "BalanceUpdateProof" 302913 false 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_notOverCommitted_302913.json" + setJSON("./src/test/test-data/balanceUpdateProof_notOverCommitted_302913.json"); + _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); + IEigenPod newPod = eigenPodManager.getPod(podOwner); + + cheats.roll(block.number + 1); + // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" + setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); + validatorFields = getValidatorFields(); + uint40 validatorIndex = uint40(getValidatorIndex()); + bytes32 newLatestBlockRoot = getLatestBlockRoot(); + BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(newLatestBlockRoot); + BeaconChainProofs.BalanceUpdateProof memory proofs = _getBalanceUpdateProof(); + BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + proofs.balanceRoot = bytes32(uint256(0)); + + validatorFields[7] = bytes32(uint256(0)); + cheats.warp(GOERLI_GENESIS_TIME + 1 days); + uint64 oracleTimestamp = uint64(block.timestamp); + cheats.expectRevert(bytes("EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn")); + newPod.verifyBalanceUpdate(oracleTimestamp, validatorIndex, stateRootProofStruct, proofs, validatorFields); + } + +} \ No newline at end of file