From 330e354f0c6e32067c9427bdcf7f3359e426836d Mon Sep 17 00:00:00 2001 From: Yash Patil Date: Sat, 26 Oct 2024 17:36:22 -0700 Subject: [PATCH] test: fix epm and ep unit tests --- src/contracts/interfaces/IEigenPod.sol | 2 +- src/contracts/pods/EigenPod.sol | 26 +- src/contracts/pods/EigenPodManager.sol | 4 +- src/contracts/pods/PodManagerOld.sol | 300 +++++++ src/contracts/pods/PodOld.sol | 737 ++++++++++++++++++ src/test/harnesses/EigenPodManagerWrapper.sol | 8 +- src/test/unit/EigenPodManagerUnit.t.sol | 286 ++++--- src/test/unit/EigenPodUnit.t.sol | 2 +- 8 files changed, 1194 insertions(+), 171 deletions(-) create mode 100644 src/contracts/pods/PodManagerOld.sol create mode 100644 src/contracts/pods/PodOld.sol diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 1691be382..5214b5f8d 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -50,7 +50,7 @@ interface IEigenPodErrors { /// @dev Thrown when a validator's withdrawal credentials have already been verified. error CredentialsAlreadyVerified(); /// @dev Thrown if the provided proof is not valid for this EigenPod. - error WithdrawCredentialsNotForEigenPod(); + error WithdrawalCredentialsNotForEigenPod(); /// @dev Thrown when a validator is not in the ACTIVE status in the pod. error ValidatorNotActiveInPod(); /// @dev Thrown when validator is not active yet on the beacon chain. diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index d92631e34..5fc0e9b25 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -474,7 +474,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // Ensure the validator's withdrawal credentials are pointed at this pod require( validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), - WithdrawCredentialsNotForEigenPod() + WithdrawalCredentialsNotForEigenPod() ); // Get the validator's effective balance. Note that this method uses effective balance, while @@ -495,10 +495,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. activeValidatorCount++; - uint64 lastCheckpointedAt = lastCheckpointTimestamp; - if (currentCheckpointTimestamp != 0) { - lastCheckpointedAt = currentCheckpointTimestamp; - } + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; // Proofs complete - create the validator in state _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ @@ -632,6 +630,15 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC int256 totalShareDeltaWei = (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + // Calculate the slashing proportion + uint64 proportionOfOldBalance = 0; + if (totalShareDeltaWei < 0) { + uint256 totalRestakedBeforeWei = + (withdrawableRestakedExecutionLayerGwei + checkpoint.beaconChainBalanceBeforeGwei) * GWEI_TO_WEI; + proportionOfOldBalance = + uint64((totalRestakedBeforeWei - uint256(-totalShareDeltaWei)) * WAD / totalRestakedBeforeWei); + } + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; @@ -641,15 +648,6 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC delete currentCheckpointTimestamp; delete _currentCheckpoint; - // Calculate the slashing proportion - uint64 proportionOfOldBalance = 0; - if (totalShareDeltaWei < 0) { - uint256 totalRestakedBeforeWei = - (withdrawableRestakedExecutionLayerGwei + checkpoint.beaconChainBalanceBeforeGwei) * GWEI_TO_WEI; - proportionOfOldBalance = - uint64((totalRestakedBeforeWei + uint256(-totalShareDeltaWei)) * WAD / totalRestakedBeforeWei); - } - // Update pod owner's shares eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei, proportionOfOldBalance); emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); diff --git a/src/contracts/pods/EigenPodManager.sol b/src/contracts/pods/EigenPodManager.sol index 64335bf08..a8812c26a 100644 --- a/src/contracts/pods/EigenPodManager.sol +++ b/src/contracts/pods/EigenPodManager.sol @@ -125,7 +125,7 @@ contract EigenPodManager is } /** - * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * @notice Used by the DelegationManager to remove a pod owner's deposit shares when they enter the withdrawal queue. * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. * @dev This function reverts if it would result in `podOwnerDepositShares[podOwner]` being less than zero, i.e. it is forbidden for this function to * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive @@ -173,6 +173,7 @@ contract EigenPodManager is ) external onlyDelegationManager { require(strategy == beaconChainETHStrategy, InvalidStrategy()); require(staker != address(0), InputAddressZero()); + require(int256(shares) >= 0, SharesNegative()); int256 currentDepositShares = podOwnerDepositShares[staker]; uint256 sharesToWithdraw; @@ -232,6 +233,7 @@ contract EigenPodManager is function _addShares(address staker, uint256 shares) internal { require(staker != address(0), InputAddressZero()); + require(int256(shares) >= 0, SharesNegative()); int256 sharesToAdd = int256(shares); int256 currentDepositShares = podOwnerDepositShares[staker]; diff --git a/src/contracts/pods/PodManagerOld.sol b/src/contracts/pods/PodManagerOld.sol new file mode 100644 index 000000000..3ed40e4c2 --- /dev/null +++ b/src/contracts/pods/PodManagerOld.sol @@ -0,0 +1,300 @@ +// // SPDX-License-Identifier: BUSL-1.1 +// pragma solidity ^0.8.12; + +// import "@openzeppelin/contracts/utils/Create2.sol"; +// import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +// import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +// import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +// import "../permissions/Pausable.sol"; +// import "./EigenPodPausingConstants.sol"; +// import "./EigenPodManagerStorage.sol"; + +// /** +// * @title The contract used for creating and managing EigenPods +// * @author Layr Labs, Inc. +// * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service +// * @notice The main functionalities are: +// * - creating EigenPods +// * - staking for new validators on EigenPods +// * - keeping track of the restaked balances of all EigenPod owners +// * - withdrawing eth when withdrawals are completed +// */ +// contract EigenPodManager is +// Initializable, +// OwnableUpgradeable, +// Pausable, +// EigenPodPausingConstants, +// EigenPodManagerStorage, +// ReentrancyGuardUpgradeable +// { +// modifier onlyEigenPod(address podOwner) { +// require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); +// _; +// } + +// modifier onlyDelegationManager() { +// require( +// msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" +// ); +// _; +// } + +// constructor( +// IETHPOSDeposit _ethPOS, +// IBeacon _eigenPodBeacon, +// IStrategyManager _strategyManager, +// ISlasher _slasher, +// IDelegationManager _delegationManager +// ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { +// _disableInitializers(); +// } + +// function initialize( +// address initialOwner, +// IPauserRegistry _pauserRegistry, +// uint256 _initPausedStatus +// ) external initializer { +// _transferOwnership(initialOwner); +// _initializePauser(_pauserRegistry, _initPausedStatus); +// } + +// /** +// * @notice Creates an EigenPod for the sender. +// * @dev Function will revert if the `msg.sender` already has an EigenPod. +// * @dev Returns EigenPod address +// */ +// function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { +// require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); +// // deploy a pod if the sender doesn't have one already +// IEigenPod pod = _deployPod(); + +// return address(pod); +// } + +// /** +// * @notice Stakes for a new beacon chain validator on the sender's EigenPod. +// * Also creates an EigenPod for the sender if they don't have one already. +// * @param pubkey The 48 bytes public key of the beacon chain validator. +// * @param signature The validator's signature of the deposit data. +// * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. +// */ +// function stake( +// bytes calldata pubkey, +// bytes calldata signature, +// bytes32 depositDataRoot +// ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { +// IEigenPod pod = ownerToPod[msg.sender]; +// if (address(pod) == address(0)) { +// //deploy a pod if the sender doesn't have one already +// pod = _deployPod(); +// } +// pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); +// } + +// /** +// * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager +// * to ensure that delegated shares are also tracked correctly +// * @param podOwner is the pod owner whose balance is being updated. +// * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares +// * @dev Callable only by the podOwner's EigenPod contract. +// * @dev Reverts if `sharesDelta` is not a whole Gwei amount +// */ +// function recordBeaconChainETHBalanceUpdate( +// address podOwner, +// int256 sharesDelta +// ) external onlyEigenPod(podOwner) nonReentrant { +// require( +// podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" +// ); +// require( +// sharesDelta % int256(GWEI_TO_WEI) == 0, +// "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" +// ); +// int256 currentPodOwnerShares = podOwnerShares[podOwner]; +// int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; +// podOwnerShares[podOwner] = updatedPodOwnerShares; + +// // inform the DelegationManager of the change in delegateable shares +// int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ +// sharesBefore: currentPodOwnerShares, +// sharesAfter: updatedPodOwnerShares +// }); +// // skip making a call to the DelegationManager if there is no change in delegateable shares +// if (changeInDelegatableShares != 0) { +// if (changeInDelegatableShares < 0) { +// delegationManager.decreaseDelegatedShares({ +// staker: podOwner, +// strategy: beaconChainETHStrategy, +// shares: uint256(-changeInDelegatableShares) +// }); +// } else { +// delegationManager.increaseDelegatedShares({ +// staker: podOwner, +// strategy: beaconChainETHStrategy, +// shares: uint256(changeInDelegatableShares) +// }); +// } +// } +// emit PodSharesUpdated(podOwner, sharesDelta); +// emit NewTotalShares(podOwner, updatedPodOwnerShares); +// } + +// /** +// * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. +// * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. +// * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to +// * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive +// * shares from the operator to whom the staker is delegated. +// * @dev Reverts if `shares` is not a whole Gwei amount +// * @dev The delegation manager validates that the podOwner is not address(0) +// */ +// function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { +// require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); +// require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); +// int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); +// require( +// updatedPodOwnerShares >= 0, +// "EigenPodManager.removeShares: cannot result in pod owner having negative shares" +// ); +// podOwnerShares[podOwner] = updatedPodOwnerShares; + +// emit NewTotalShares(podOwner, updatedPodOwnerShares); +// } + +// /** +// * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. +// * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue +// * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input +// * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) +// * @dev Reverts if `shares` is not a whole Gwei amount +// */ +// function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { +// require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); +// require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); +// require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); +// int256 currentPodOwnerShares = podOwnerShares[podOwner]; +// int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); +// podOwnerShares[podOwner] = updatedPodOwnerShares; + +// emit PodSharesUpdated(podOwner, int256(shares)); +// emit NewTotalShares(podOwner, updatedPodOwnerShares); + +// return uint256( +// _calculateChangeInDelegatableShares({ +// sharesBefore: currentPodOwnerShares, +// sharesAfter: updatedPodOwnerShares +// }) +// ); +// } + +// /** +// * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address +// * @dev Prioritizes decreasing the podOwner's share deficit, if they have one +// * @dev Reverts if `shares` is not a whole Gwei amount +// * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why +// * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive +// */ +// function withdrawSharesAsTokens( +// address podOwner, +// address destination, +// uint256 shares +// ) external onlyDelegationManager { +// require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); +// require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); +// require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); +// require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); +// int256 currentPodOwnerShares = podOwnerShares[podOwner]; + +// // if there is an existing shares deficit, prioritize decreasing the deficit first +// if (currentPodOwnerShares < 0) { +// uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + +// if (shares > currentShareDeficit) { +// // get rid of the whole deficit if possible, and pass any remaining shares onto destination +// podOwnerShares[podOwner] = 0; +// shares -= currentShareDeficit; +// emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); +// emit NewTotalShares(podOwner, 0); +// } else { +// // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on +// int256 updatedPodOwnerShares = podOwnerShares[podOwner] + int256(shares); +// podOwnerShares[podOwner] = updatedPodOwnerShares; +// emit PodSharesUpdated(podOwner, int256(shares)); +// emit NewTotalShares(podOwner, updatedPodOwnerShares); +// return; +// } +// } +// // Actually withdraw to the destination +// ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); +// } + +// // INTERNAL FUNCTIONS + +// function _deployPod() internal returns (IEigenPod) { +// ++numPods; +// // create the pod +// IEigenPod pod = IEigenPod( +// Create2.deploy( +// 0, +// bytes32(uint256(uint160(msg.sender))), +// // set the beacon address to the eigenPodBeacon and initialize it +// abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) +// ) +// ); +// pod.initialize(msg.sender); +// // store the pod in the mapping +// ownerToPod[msg.sender] = pod; +// emit PodDeployed(address(pod), msg.sender); +// return pod; +// } + +// /** +// * @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. +// */ +// function _calculateChangeInDelegatableShares( +// int256 sharesBefore, +// int256 sharesAfter +// ) internal pure returns (int256) { +// if (sharesBefore <= 0) { +// if (sharesAfter <= 0) { +// // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares +// return 0; +// } else { +// // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount +// return sharesAfter; +// } +// } else { +// if (sharesAfter <= 0) { +// // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount +// return (-sharesBefore); +// } else { +// // if the shares started positive and stayed positive, then the change in delegateable shares +// // is the difference between starting and ending amounts +// return (sharesAfter - sharesBefore); +// } +// } +// } + +// // VIEW FUNCTIONS +// /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). +// function getPod(address podOwner) public view returns (IEigenPod) { +// IEigenPod pod = ownerToPod[podOwner]; +// // if pod does not exist already, calculate what its address *will be* once it is deployed +// if (address(pod) == address(0)) { +// pod = IEigenPod( +// Create2.computeAddress( +// bytes32(uint256(uint160(podOwner))), //salt +// keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode +// ) +// ); +// } +// return pod; +// } + +// /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. +// function hasPod(address podOwner) public view returns (bool) { +// return address(ownerToPod[podOwner]) != address(0); +// } +// } \ No newline at end of file diff --git a/src/contracts/pods/PodOld.sol b/src/contracts/pods/PodOld.sol new file mode 100644 index 000000000..dc4243f08 --- /dev/null +++ b/src/contracts/pods/PodOld.sol @@ -0,0 +1,737 @@ +// // SPDX-License-Identifier: BUSL-1.1 +// pragma solidity ^0.8.12; + +// import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +// import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +// import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// import "../libraries/BeaconChainProofs.sol"; +// import "../libraries/BytesLib.sol"; + +// import "../interfaces/IETHPOSDeposit.sol"; +// import "../interfaces/IEigenPodManager.sol"; +// import "../interfaces/IPausable.sol"; + +// import "./EigenPodPausingConstants.sol"; +// import "./EigenPodStorage.sol"; + +// /** +// * @title The implementation contract used for restaking beacon chain ETH on EigenLayer +// * @author Layr Labs, Inc. +// * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service +// * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs +// * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose +// * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts +// */ +// contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingConstants, EigenPodStorage { +// using BytesLib for bytes; +// using SafeERC20 for IERC20; +// using BeaconChainProofs for *; + +// /** +// * +// * CONSTANTS / IMMUTABLES +// * +// */ + +// /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two +// uint256 internal constant GWEI_TO_WEI = 1e9; + +// /// @notice The address of the EIP-4788 beacon block root oracle +// /// (See https://eips.ethereum.org/EIPS/eip-4788) +// address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + +// /// @notice The length of the EIP-4788 beacon block root ring buffer +// uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + +// /// @notice The beacon chain deposit contract +// IETHPOSDeposit public immutable ethPOS; + +// /// @notice The single EigenPodManager for EigenLayer +// IEigenPodManager public immutable eigenPodManager; + +// /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp +// uint64 public immutable GENESIS_TIME; + +// /** +// * +// * MODIFIERS +// * +// */ + +// /// @notice Callable only by the EigenPodManager +// modifier onlyEigenPodManager() { +// require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); +// _; +// } + +// /// @notice Callable only by the pod's owner +// modifier onlyEigenPodOwner() { +// require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); +// _; +// } + +// /// @notice Callable only by the pod's owner or proof submitter +// modifier onlyOwnerOrProofSubmitter() { +// require( +// msg.sender == podOwner || msg.sender == proofSubmitter, +// "EigenPod.onlyOwnerOrProofSubmitter: caller is not pod owner or proof submitter" +// ); +// _; +// } + +// /** +// * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction +// * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). +// * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. +// */ +// modifier onlyWhenNotPaused(uint8 index) { +// require( +// !IPausable(address(eigenPodManager)).paused(index), +// "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" +// ); +// _; +// } + +// /** +// * +// * CONSTRUCTOR / INIT +// * +// */ +// constructor(IETHPOSDeposit _ethPOS, IEigenPodManager _eigenPodManager, uint64 _GENESIS_TIME) { +// ethPOS = _ethPOS; +// eigenPodManager = _eigenPodManager; +// GENESIS_TIME = _GENESIS_TIME; +// _disableInitializers(); +// } + +// /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. +// function initialize(address _podOwner) external initializer { +// require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); +// podOwner = _podOwner; +// } + +// /** +// * +// * EXTERNAL METHODS +// * +// */ + +// /// @notice payable fallback function that receives ether deposited to the eigenpods contract +// receive() external payable { +// emit NonBeaconChainETHReceived(msg.value); +// } + +// /** +// * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed +// * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total +// * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. +// * @dev Once finalized, the pod owner is awarded shares corresponding to: +// * - the total change in their ACTIVE validator balances +// * - any ETH in the pod not already awarded shares +// * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If +// * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. +// * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner +// * to prevent accidentally starting a checkpoint that will not increase their shares +// */ +// function startCheckpoint(bool revertIfNoBalance) +// external +// onlyOwnerOrProofSubmitter +// onlyWhenNotPaused(PAUSED_START_CHECKPOINT) +// { +// _startCheckpoint(revertIfNoBalance); +// } + +// /** +// * @dev Progress the current checkpoint towards completion by submitting one or more validator +// * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. +// * For each validator proven, the current checkpoint's `proofsRemaining` decreases. +// * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. +// * (see `_updateCheckpoint` for more details) +// * @dev This method can only be called when there is a currently-active checkpoint. +// * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` +// * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` +// */ +// function verifyCheckpointProofs( +// BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, +// BeaconChainProofs.BalanceProof[] calldata proofs +// ) external onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) { +// uint64 checkpointTimestamp = currentCheckpointTimestamp; +// require( +// checkpointTimestamp != 0, +// "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" +// ); + +// Checkpoint memory checkpoint = _currentCheckpoint; + +// // Verify `balanceContainerProof` against `beaconBlockRoot` +// BeaconChainProofs.verifyBalanceContainer({ +// beaconBlockRoot: checkpoint.beaconBlockRoot, +// proof: balanceContainerProof +// }); + +// // Process each checkpoint proof submitted +// uint64 exitedBalancesGwei; +// for (uint256 i = 0; i < proofs.length; i++) { +// BeaconChainProofs.BalanceProof calldata proof = proofs[i]; +// ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + +// // Validator must be in the ACTIVE state to be provable during a checkpoint. +// // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials +// // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance +// if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { +// continue; +// } + +// // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: +// // - validator submitted twice during this checkpoint +// // - validator withdrawal credentials verified after checkpoint starts, then submitted +// // as a checkpoint proof +// if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { +// continue; +// } + +// // Process a checkpoint proof for a validator and update its balance. +// // +// // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. +// // The assumption is that if this is the case, any withdrawn ETH was already in +// // the pod when `startCheckpoint` was originally called. +// (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ +// validatorInfo: validatorInfo, +// checkpointTimestamp: checkpointTimestamp, +// balanceContainerRoot: balanceContainerProof.balanceContainerRoot, +// proof: proof +// }); + +// checkpoint.proofsRemaining--; +// checkpoint.balanceDeltasGwei += balanceDeltaGwei; +// exitedBalancesGwei += exitedBalanceGwei; + +// // Record the updated validator in state +// _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; +// emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); +// } + +// // Update the checkpoint and the total amount attributed to exited validators +// checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; +// _updateCheckpoint(checkpoint); +// } + +// /** +// * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award +// * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and +// * future checkpoint proofs will need to include them. +// * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. +// * @dev Validators proven via this method MUST NOT have an exit epoch set already. +// * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds +// * to the parent beacon block root against which the proof is verified. +// * @param stateRootProof proves a beacon state root against a beacon block root +// * @param validatorIndices a list of validator indices being proven +// * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root +// * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for +// * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator +// */ +// function verifyWithdrawalCredentials( +// uint64 beaconTimestamp, +// BeaconChainProofs.StateRootProof calldata stateRootProof, +// uint40[] calldata validatorIndices, +// bytes[] calldata validatorFieldsProofs, +// bytes32[][] calldata validatorFields +// ) external onlyOwnerOrProofSubmitter onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) { +// require( +// (validatorIndices.length == validatorFieldsProofs.length) +// && (validatorFieldsProofs.length == validatorFields.length), +// "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" +// ); + +// // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow +// // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress +// // on an existing checkpoint. +// require( +// beaconTimestamp > currentCheckpointTimestamp, +// "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" +// ); + +// // Verify passed-in `beaconStateRoot` against the beacon block root +// // forgefmt: disable-next-item +// BeaconChainProofs.verifyStateRoot({ +// beaconBlockRoot: getParentBlockRoot(beaconTimestamp), +// proof: stateRootProof +// }); + +// uint256 totalAmountToBeRestakedWei; +// for (uint256 i = 0; i < validatorIndices.length; i++) { +// // forgefmt: disable-next-item +// totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( +// stateRootProof.beaconStateRoot, +// validatorIndices[i], +// validatorFieldsProofs[i], +// validatorFields[i] +// ); +// } + +// // Update the EigenPodManager on this pod's new balance +// eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); +// } + +// /** +// * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful +// * staleness proof allows the caller to start a checkpoint. +// * +// * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! +// * (See `_startCheckpoint` for details) +// * +// * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon +// * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. +// * +// * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event +// * and the validator's final exit back to the execution layer. During this time, the validator's balance may or +// * may not drop further due to a correlation penalty. This method allows proof of a slashed validator +// * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator +// * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven +// * "stale" via this method. +// * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. +// * +// * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds +// * to the parent beacon block root against which the proof is verified. +// * @param stateRootProof proves a beacon state root against a beacon block root +// * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against +// * the beacon state root. See the consensus specs for more details: +// * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator +// * +// * @dev Staleness conditions: +// * - Validator's last checkpoint is older than `beaconTimestamp` +// * - Validator MUST be in `ACTIVE` status in the pod +// * - Validator MUST be slashed on the beacon chain +// */ +// function verifyStaleBalance( +// uint64 beaconTimestamp, +// BeaconChainProofs.StateRootProof calldata stateRootProof, +// BeaconChainProofs.ValidatorProof calldata proof +// ) external onlyWhenNotPaused(PAUSED_START_CHECKPOINT) onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) { +// bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); +// ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + +// // Validator must be eligible for a staleness proof. Generally, this condition +// // ensures that the staleness proof is newer than the last time we got an update +// // on this validator. +// // +// // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if +// // a validator's withdrawal credentials are verified when no checkpoint has +// // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` +// // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the +// // require below. +// // +// // However, in practice, if the only update we've seen from a validator is their +// // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is +// // necessarily newer. This is because when a validator is initially slashed, their +// // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators +// // that have initiated exits, we know that if we're seeing a proof where the validator +// // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof +// // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). +// require( +// beaconTimestamp > validatorInfo.lastCheckpointedAt, +// "EigenPod.verifyStaleBalance: proof is older than last checkpoint" +// ); + +// // Validator must be checkpoint-able +// require(validatorInfo.status == VALIDATOR_STATUS.ACTIVE, "EigenPod.verifyStaleBalance: validator is not active"); + +// // Validator must be slashed on the beacon chain +// require( +// proof.validatorFields.isValidatorSlashed(), +// "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" +// ); + +// // Verify passed-in `beaconStateRoot` against the beacon block root +// // forgefmt: disable-next-item +// BeaconChainProofs.verifyStateRoot({ +// beaconBlockRoot: getParentBlockRoot(beaconTimestamp), +// proof: stateRootProof +// }); + +// // Verify Validator container proof against `beaconStateRoot` +// BeaconChainProofs.verifyValidatorFields({ +// beaconStateRoot: stateRootProof.beaconStateRoot, +// validatorFields: proof.validatorFields, +// validatorFieldsProof: proof.proof, +// validatorIndex: uint40(validatorInfo.validatorIndex) +// }); + +// // Validator verified to be stale - start a checkpoint +// _startCheckpoint(false); +// } + +// /// @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 onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { +// require( +// tokenList.length == amountsToWithdraw.length, +// "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" +// ); +// for (uint256 i = 0; i < tokenList.length; i++) { +// tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); +// } +// } + +// /// @notice Allows the owner of a pod to update the proof submitter, a permissioned +// /// address that can call `startCheckpoint` and `verifyWithdrawalCredentials`. +// /// @dev Note that EITHER the podOwner OR proofSubmitter can access these methods, +// /// so it's fine to set your proofSubmitter to 0 if you want the podOwner to be the +// /// only address that can call these methods. +// /// @param newProofSubmitter The new proof submitter address. If set to 0, only the +// /// pod owner will be able to call `startCheckpoint` and `verifyWithdrawalCredentials` +// function setProofSubmitter(address newProofSubmitter) external onlyEigenPodOwner { +// emit ProofSubmitterUpdated(proofSubmitter, newProofSubmitter); +// proofSubmitter = newProofSubmitter; +// } + +// /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. +// function stake( +// bytes calldata pubkey, +// bytes calldata signature, +// bytes32 depositDataRoot +// ) external payable onlyEigenPodManager { +// // stake on ethpos +// require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); +// ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); +// emit EigenPodStaked(pubkey); +// } + +// /** +// * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address +// * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. +// * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the +// * `amountWei` input (when converted to GWEI). +// * @dev Reverts if `amountWei` is not a whole Gwei amount +// */ +// function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { +// require( +// amountWei % GWEI_TO_WEI == 0, +// "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" +// ); +// uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); +// require( +// amountGwei <= withdrawableRestakedExecutionLayerGwei, +// "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" +// ); +// withdrawableRestakedExecutionLayerGwei -= amountGwei; +// emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); +// // transfer ETH from pod to `recipient` directly +// Address.sendValue(payable(recipient), amountWei); +// } + +// /** +// * +// * INTERNAL FUNCTIONS +// * +// */ + +// /** +// * @notice internal function that proves an individual validator's withdrawal credentials +// * @param validatorIndex is the index of the validator being proven +// * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root +// * @param validatorFields are the fields of the "Validator Container", refer to consensus specs +// */ +// function _verifyWithdrawalCredentials( +// bytes32 beaconStateRoot, +// uint40 validatorIndex, +// bytes calldata validatorFieldsProof, +// bytes32[] calldata validatorFields +// ) internal returns (uint256) { +// bytes32 pubkeyHash = validatorFields.getPubkeyHash(); +// ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + +// // Withdrawal credential proofs should only be processed for "INACTIVE" validators +// require( +// validatorInfo.status == VALIDATOR_STATUS.INACTIVE, +// "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" +// ); + +// // Validator should be active on the beacon chain, or in the process of activating. +// // This implies the validator has reached the minimum effective balance required +// // to become active on the beacon chain. +// // +// // This check is important because the Pectra upgrade will move any validators that +// // do NOT have an activation epoch to a "pending deposit queue," temporarily resetting +// // their current and effective balances to 0. This balance can be restored if a deposit +// // is made to bring the validator's balance above the minimum activation balance. +// // (See https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/fork.md#upgrading-the-state) +// // +// // In the context of EigenLayer slashing, this temporary reset would allow pod shares +// // to temporarily decrease, then be restored later. This would effectively prevent these +// // shares from being slashable on EigenLayer for a short period of time. +// require( +// validatorFields.getActivationEpoch() != BeaconChainProofs.FAR_FUTURE_EPOCH, +// "EigenPod._verifyWithdrawalCredentials: validator must be in the process of activating" +// ); + +// // Validator should not already be in the process of exiting. This is an important property +// // this method needs to enforce to ensure a validator cannot be already-exited by the time +// // its withdrawal credentials are verified. +// // +// // Note that when a validator initiates an exit, two values are set: +// // - exit_epoch +// // - withdrawable_epoch +// // +// // The latter of these two values describes an epoch after which the validator's ETH MIGHT +// // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal +// // queue. +// // +// // Requiring that a validator has not initiated exit by the time the EigenPod sees their +// // withdrawal credentials guarantees that the validator has not fully exited at this point. +// // +// // This is because: +// // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest +// // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. +// // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to +// // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). +// // +// // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) +// require( +// validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, +// "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" +// ); + +// // Ensure the validator's withdrawal credentials are pointed at this pod +// require( +// validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), +// "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" +// ); + +// // Get the validator's effective balance. Note that this method uses effective balance, while +// // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's +// // less accurate, but is good enough for verifying withdrawal credentials. +// uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + +// // Verify passed-in validatorFields against verified beaconStateRoot: +// BeaconChainProofs.verifyValidatorFields({ +// beaconStateRoot: beaconStateRoot, +// validatorFields: validatorFields, +// validatorFieldsProof: validatorFieldsProof, +// validatorIndex: validatorIndex +// }); + +// // Account for validator in future checkpoints. Note that if this pod has never started a +// // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main +// // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not +// // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. +// activeValidatorCount++; +// uint64 lastCheckpointedAt = +// currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + +// // Proofs complete - create the validator in state +// _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ +// validatorIndex: validatorIndex, +// restakedBalanceGwei: restakedBalanceGwei, +// lastCheckpointedAt: lastCheckpointedAt, +// status: VALIDATOR_STATUS.ACTIVE +// }); + +// emit ValidatorRestaked(validatorIndex); +// emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); +// return restakedBalanceGwei * GWEI_TO_WEI; +// } + +// function _verifyCheckpointProof( +// ValidatorInfo memory validatorInfo, +// uint64 checkpointTimestamp, +// bytes32 balanceContainerRoot, +// BeaconChainProofs.BalanceProof calldata proof +// ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { +// uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + +// // Verify validator balance against `balanceContainerRoot` +// uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; +// uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ +// balanceContainerRoot: balanceContainerRoot, +// validatorIndex: validatorIndex, +// proof: proof +// }); + +// // Calculate change in the validator's balance since the last proof +// if (newBalanceGwei != prevBalanceGwei) { +// // forgefmt: disable-next-item +// balanceDeltaGwei = _calcBalanceDelta({ +// newAmountGwei: newBalanceGwei, +// previousAmountGwei: prevBalanceGwei +// }); + +// emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); +// } + +// validatorInfo.restakedBalanceGwei = newBalanceGwei; +// validatorInfo.lastCheckpointedAt = checkpointTimestamp; + +// // If the validator's new balance is 0, mark them withdrawn +// if (newBalanceGwei == 0) { +// activeValidatorCount--; +// validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; +// // If we reach this point, `balanceDeltaGwei` should always be negative, +// // so this should be a safe conversion +// exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + +// emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); +// } + +// return (balanceDeltaGwei, exitedBalanceGwei); +// } + +// /** +// * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the +// * current block's parent block root. After providing a checkpoint proof for each of the +// * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. +// * @dev ACTIVE validators are validators with verified withdrawal credentials (See +// * `verifyWithdrawalCredentials` for details) +// * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically +// * finalized. +// * @dev Once started, a checkpoint MUST be completed! It is not possible to start a +// * checkpoint if the existing one is incomplete. +// * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is +// * true, this method will revert +// */ +// function _startCheckpoint(bool revertIfNoBalance) internal { +// require( +// currentCheckpointTimestamp == 0, +// "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" +// ); + +// // Prevent a checkpoint being completable twice in the same block. This prevents an edge case +// // where the second checkpoint would not be completable. +// // +// // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` +// // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` +// require( +// lastCheckpointTimestamp != uint64(block.timestamp), +// "EigenPod._startCheckpoint: cannot checkpoint twice in one block" +// ); + +// // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has +// // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` +// // will be added to the total validator balance delta and credited as shares. +// // +// // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` +// // to denote that it has been credited with shares. Because this value is denominated in gwei, +// // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts +// // sent to the pod are not credited with shares and are therefore not withdrawable. +// // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. +// uint64 podBalanceGwei = uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + +// // If the caller doesn't want a "0 balance" checkpoint, revert +// if (revertIfNoBalance && podBalanceGwei == 0) { +// revert("EigenPod._startCheckpoint: no balance available to checkpoint"); +// } + +// // Create checkpoint using the previous block's root for proofs, and the current +// // `activeValidatorCount` as the number of checkpoint proofs needed to finalize +// // the checkpoint. +// Checkpoint memory checkpoint = Checkpoint({ +// beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), +// proofsRemaining: uint24(activeValidatorCount), +// podBalanceGwei: podBalanceGwei, +// balanceDeltasGwei: 0 +// }); + +// // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint +// // is automatically finalized. +// currentCheckpointTimestamp = uint64(block.timestamp); +// _updateCheckpoint(checkpoint); + +// emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot, checkpoint.proofsRemaining); +// } + +// /** +// * @dev Finish progress on a checkpoint and store it in state. +// * @dev If the checkpoint has no proofs remaining, it is finalized: +// * - a share delta is calculated and sent to the `EigenPodManager` +// * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` +// * - `lastCheckpointTimestamp` is updated +// * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted +// */ +// function _updateCheckpoint(Checkpoint memory checkpoint) internal { +// if (checkpoint.proofsRemaining == 0) { +// int256 totalShareDeltaWei = +// (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + +// // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` +// // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue +// withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + +// // Finalize the checkpoint +// lastCheckpointTimestamp = currentCheckpointTimestamp; +// delete currentCheckpointTimestamp; +// delete _currentCheckpoint; + +// // Update pod owner's shares +// eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); +// emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); +// } else { +// _currentCheckpoint = checkpoint; +// } +// } + +// function _podWithdrawalCredentials() internal view returns (bytes memory) { +// return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); +// } + +// ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec +// function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { +// require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); +// return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); +// } + +// /// @dev Calculates the delta between two Gwei amounts and returns as an int256 +// function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { +// return int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); +// } + +// /** +// * +// * VIEW FUNCTIONS +// * +// */ + +// /// @notice Returns the validatorInfo for a given validatorPubkeyHash +// function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { +// return _validatorPubkeyHashToInfo[validatorPubkeyHash]; +// } + +// /// @notice Returns the validatorInfo for a given validatorPubkey +// function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { +// return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; +// } + +// function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { +// 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); +// return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; +// } + +// /// @notice Returns the currently-active checkpoint +// function currentCheckpoint() public view returns (Checkpoint memory) { +// return _currentCheckpoint; +// } + +// /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` +// /// @param timestamp of the block for which the parent block root will be returned. MUST correspond +// /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method +// /// will revert. +// function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { +// require( +// block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, +// "EigenPod.getParentBlockRoot: timestamp out of range" +// ); + +// (bool success, bytes memory result) = BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + +// require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); +// return abi.decode(result, (bytes32)); +// } +// } \ No newline at end of file diff --git a/src/test/harnesses/EigenPodManagerWrapper.sol b/src/test/harnesses/EigenPodManagerWrapper.sol index 024c3af9c..1d8bc4525 100644 --- a/src/test/harnesses/EigenPodManagerWrapper.sol +++ b/src/test/harnesses/EigenPodManagerWrapper.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import "../../contracts/pods/EigenPodManager.sol"; -///@notice This contract exposed the internal `_calculateChangeInDelegatableShares` function for testing +///@notice This contract exposes a manual setter for podShares in order to initialize podShares as negative contract EigenPodManagerWrapper is EigenPodManager { constructor( IETHPOSDeposit _ethPOS, @@ -12,7 +12,11 @@ contract EigenPodManagerWrapper is EigenPodManager { IDelegationManager _delegationManager ) EigenPodManager(_ethPOS, _eigenPodBeacon, _strategyManager, _delegationManager) {} - function setPodAddress(address owner, IEigenPod pod) external { + function setPodOwnerShares(address owner, IEigenPod pod) external { ownerToPod[owner] = pod; } + + function setPodOwnerShares(address owner, int256 shares) external { + podOwnerDepositShares[owner] = shares; + } } diff --git a/src/test/unit/EigenPodManagerUnit.t.sol b/src/test/unit/EigenPodManagerUnit.t.sol index 96ab21534..f57377d29 100644 --- a/src/test/unit/EigenPodManagerUnit.t.sol +++ b/src/test/unit/EigenPodManagerUnit.t.sol @@ -77,9 +77,12 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup, IEigenPodManagerEv // Deploy pod IEigenPod deployedPod = _deployAndReturnEigenPodForStaker(podOwner); - // Set shares - cheats.prank(address(deployedPod)); - eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, shares, 0); + if (shares >= 0) { + cheats.prank(address(delegationManagerMock)); + eigenPodManager.addShares(podOwner, beaconChainETHStrategy, IERC20(address(0)), uint256(shares)); + } else { + EigenPodManagerWrapper(address(eigenPodManager)).setPodOwnerShares(podOwner, shares); + } } @@ -196,30 +199,18 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { eigenPodManager.addShares(defaultStaker, IStrategy(address(0)), IERC20(address(0)), 0); } - // TODO: fix test - // function test_addShares_revert_podOwnerZeroAddress() public { - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector); - // eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), 0); - // } - - // TODO: fix test - // function testFuzz_addShares_revert_sharesNegative(int256 shares) public { - // cheats.assume(shares < 0); - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); - // eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), uint256(shares)); - // eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), uint256(shares)); - // } - - // TODO: fix test - // function testFuzz_addShares_revert_sharesNotWholeGwei(uint256 shares) public { - // cheats.assume(int256(shares) >= 0); - // cheats.assume(shares % GWEI_TO_WEI != 0); - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodManagerErrors.SharesNotMultipleOfGwei.selector); - // eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), shares); - // } + function test_addShares_revert_podOwnerZeroAddress() public { + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector); + eigenPodManager.addShares(address(0), beaconChainETHStrategy, IERC20(address(0)), 0); + } + + function testFuzz_addShares_revert_sharesNegative(int256 shares) public { + cheats.assume(shares < 0); + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); + eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), uint256(shares)); + } function testFuzz_addShares(uint256 shares) public { // Fuzz inputs @@ -229,7 +220,7 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { // Add shares cheats.prank(address(delegationManagerMock)); - eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), shares); + eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), shares); // Check storage update assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int256(shares), "Incorrect number of shares added"); @@ -239,31 +230,21 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { Remove Shares Tests ******************************************************************************/ - function testFuzz_removeShares_revert_notDelegationManager(address notDelegationManager) public filterFuzzedAddressInputs(notDelegationManager) { + function testFuzz_removeDepositShares_revert_notDelegationManager(address notDelegationManager) public filterFuzzedAddressInputs(notDelegationManager) { cheats.assume(notDelegationManager != address(delegationManagerMock)); cheats.prank(notDelegationManager); cheats.expectRevert(IEigenPodManagerErrors.OnlyDelegationManager.selector); eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, 0); } - // TODO: fix test - // function testFuzz_removeShares_revert_sharesNegative(int256 shares) public { - // cheats.assume(shares < 0); - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); - // eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, uint256(shares)); - // } - - // TODO: fix test - // function testFuzz_removeShares_revert_sharesNotWholeGwei(uint256 shares) public { - // cheats.assume(int256(shares) >= 0); - // cheats.assume(shares % GWEI_TO_WEI != 0); - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodManagerErrors.SharesNotMultipleOfGwei.selector); - // eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, shares); - // } - - function testFuzz_removeShares_revert_tooManySharesRemoved(uint224 sharesToAdd, uint224 sharesToRemove) public { + function testFuzz_removeDepositShares_revert_sharesNegative(uint224 sharesToRemove) public { + cheats.assume(sharesToRemove > 0); + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); + eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, sharesToRemove); + } + + function testFuzz_removeDepositShares_revert_tooManySharesRemoved(uint224 sharesToAdd, uint224 sharesToRemove) public { // Constrain inputs cheats.assume(sharesToRemove > sharesToAdd); uint256 sharesAdded = sharesToAdd * GWEI_TO_WEI; @@ -295,7 +276,7 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int256(sharesAdded - sharesRemoved), "Incorrect number of shares removed"); } - function testFuzz_removeShares_zeroShares(address podOwner, uint256 shares) public filterFuzzedAddressInputs(podOwner) { + function testFuzz_removeDepositShares_zeroShares(address podOwner, uint256 shares) public filterFuzzedAddressInputs(podOwner) { // Constrain inputs cheats.assume(podOwner != address(0)); cheats.assume(shares < type(uint256).max / 2); @@ -312,72 +293,89 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { // Check storage update assertEq(eigenPodManager.podOwnerDepositShares(podOwner), 0, "Shares not reset to zero"); } +} + +contract EigenPodManagerUnitTests_WithdrawSharesAsTokensTests is EigenPodManagerUnitTests { + // Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function + EigenPodManagerWrapper public eigenPodManagerWrapper; + + function setUp() virtual override public { + super.setUp(); + // Upgrade eigenPodManager to wrapper + eigenPodManagerWrapper = new EigenPodManagerWrapper( + ethPOSMock, + eigenPodBeacon, + IStrategyManager(address(strategyManagerMock)), + IDelegationManager(address(delegationManagerMock)) + ); + eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper)); + } /******************************************************************************* WithdrawSharesAsTokens Tests ******************************************************************************/ + function test_withdrawSharesAsTokekns_revert_invalidStrategy() public { + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IEigenPodManagerErrors.InvalidStrategy.selector); + eigenPodManager.withdrawSharesAsTokens(defaultStaker, IStrategy(address(0)), IERC20(address(0)), 0); + } + function test_withdrawSharesAsTokens_revert_podOwnerZeroAddress() public { cheats.prank(address(delegationManagerMock)); cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector); - eigenPodManager.withdrawSharesAsTokens(address(0), beaconChainETHStrategy, IERC20(address(this)), 0); + eigenPodManager.withdrawSharesAsTokens(address(0), beaconChainETHStrategy, IERC20(address(0)), 0); } - // TODO: fix test - // function testFuzz_withdrawSharesAsTokens_revert_sharesNegative(int256 shares) public { - // cheats.assume(shares < 0); - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); - // eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), uint256(shares)); - // } - - // TODO: fix test - // function testFuzz_withdrawSharesAsTokens_revert_sharesNotWholeGwei(uint256 shares) public { - // cheats.assume(int256(shares) >= 0); - // cheats.assume(shares % GWEI_TO_WEI != 0); - - // cheats.prank(address(delegationManagerMock)); - // cheats.expectRevert(IEigenPodManagerErrors.SharesNotMultipleOfGwei.selector); - // eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), shares); - // } + function testFuzz_withdrawSharesAsTokens_revert_sharesNegative(int256 shares) public { + cheats.assume(shares < 0); + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); + eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), uint256(shares)); + } /** * @notice The `withdrawSharesAsTokens` is called in the `completeQueuedWithdrawal` function from the * delegationManager. When a withdrawal is queued in the delegationManager, `removeDepositShares is called` */ - function test_withdrawSharesAsTokens_reduceEntireDeficit() public { + function test_withdrawSharesAsTokens_m2NegativeShares_reduceEntireDeficit() public { // Shares to initialize & withdraw int256 sharesBeginning = -100e18; uint256 sharesToWithdraw = 101e18; // Deploy Pod And initialize with negative shares _initializePodWithShares(defaultStaker, sharesBeginning); + assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBeginning, "Shares not initialized correctly"); // Withdraw shares cheats.prank(address(delegationManagerMock)); - eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), sharesToWithdraw); + cheats.expectEmit(true, true, true, true); + emit PodSharesUpdated(defaultStaker, 100e18); + cheats.expectEmit(true, true, true, true); + emit NewTotalShares(defaultStaker, 0); + eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), sharesToWithdraw); // Check storage update assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int256(0), "Shares not reduced to 0"); } - // TODO: fix test - // function test_withdrawSharesAsTokens_partialDefecitReduction() public { - // // Shares to initialize & withdraw - // int256 sharesBeginning = -100e18; - // uint256 sharesToWithdraw = 50e18; + function test_withdrawSharesAsTokens_m2NegativeShares_partialDefecitReduction() public { + // Shares to initialize & withdraw + int256 sharesBeginning = -100e18; + uint256 sharesToWithdraw = 50e18; - // // Deploy Pod And initialize with negative shares - // _initializePodWithShares(defaultStaker, sharesBeginning); + // Deploy Pod And initialize with negative shares + _initializePodWithShares(defaultStaker, sharesBeginning); + assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBeginning, "Shares not initialized correctly"); - // // Withdraw shares - // cheats.prank(address(delegationManagerMock)); - // eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), sharesToWithdraw); + // Withdraw shares + cheats.prank(address(delegationManagerMock)); + eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), sharesToWithdraw); - // // Check storage update - // int256 expectedShares = sharesBeginning + int256(sharesToWithdraw); - // assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), expectedShares, "Shares not reduced to expected amount"); - // } + // Check storage update + int256 expectedShares = sharesBeginning + int256(sharesToWithdraw); + assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), expectedShares, "Shares not reduced to expected amount"); + } function test_withdrawSharesAsTokens_withdrawPositive() public { // Shares to initialize & withdraw @@ -389,7 +387,7 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { // Withdraw shares cheats.prank(address(delegationManagerMock)); - eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(this)), sharesToWithdraw); + eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), sharesToWithdraw); // Check storage remains the same assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBeginning, "Shares should not be adjusted"); @@ -397,96 +395,80 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests { } contract EigenPodManagerUnitTests_BeaconChainETHBalanceUpdateTests is EigenPodManagerUnitTests { + // Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function + EigenPodManagerWrapper public eigenPodManagerWrapper; + + function setUp() virtual override public { + super.setUp(); + + // Upgrade eigenPodManager to wrapper + eigenPodManagerWrapper = new EigenPodManagerWrapper( + ethPOSMock, + eigenPodBeacon, + IStrategyManager(address(strategyManagerMock)), + IDelegationManager(address(delegationManagerMock)) + ); + eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper)); + } - function testFuzz_recordBalanceUpdate_revert_notPod(address invalidCaller) public filterFuzzedAddressInputs(invalidCaller) deployPodForStaker(defaultStaker) { + function testFuzz_revert_notPod(address invalidCaller) public filterFuzzedAddressInputs(invalidCaller) deployPodForStaker(defaultStaker) { cheats.assume(invalidCaller != address(defaultPod)); cheats.prank(invalidCaller); cheats.expectRevert(IEigenPodManagerErrors.OnlyEigenPod.selector); eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, 0, 0); } - function test_recordBalanceUpdate_revert_zeroAddress() public { + function test_revert_zeroAddress() public { IEigenPod zeroAddressPod = _deployAndReturnEigenPodForStaker(address(0)); cheats.prank(address(zeroAddressPod)); cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector); eigenPodManager.recordBeaconChainETHBalanceUpdate(address(0), 0, 0); } - function testFuzz_recordBalanceUpdate_revert_nonWholeGweiAmount(int256 sharesDelta) public deployPodForStaker(defaultStaker) { + function testFuzz_revert_nonWholeGweiAmount(int256 sharesDelta) public deployPodForStaker(defaultStaker) { cheats.assume(sharesDelta % int256(GWEI_TO_WEI) != 0); cheats.prank(address(defaultPod)); cheats.expectRevert(IEigenPodManagerErrors.SharesNotMultipleOfGwei.selector); eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, sharesDelta, 0); } - // TODO: fix test - // function testFuzz_recordBalanceUpdateX(int224 sharesBefore, int224 sharesDelta) public { - // // Constrain inputs - // int256 scaledSharesBefore = sharesBefore * int256(GWEI_TO_WEI); - // int256 scaledSharesDelta = sharesDelta * int256(GWEI_TO_WEI); - - // // Initialize shares - // _initializePodWithShares(defaultStaker, scaledSharesBefore); - - // // Update balance - // cheats.expectEmit(true, true, true, true); - // emit PodSharesUpdated(defaultStaker, scaledSharesDelta); - // cheats.expectEmit(true, true, true, true); - // emit NewTotalShares(defaultStaker, scaledSharesBefore + scaledSharesDelta); - // cheats.prank(address(defaultPod)); - // eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, scaledSharesDelta, 0); - - // // Check storage - // assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), scaledSharesBefore + scaledSharesDelta, "Shares not updated correctly"); - // } -} - -contract EigenPodManagerUnitTests_ShareAdjustmentCalculationTests is EigenPodManagerUnitTests { - // Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function - EigenPodManagerWrapper public eigenPodManagerWrapper; - - function setUp() virtual override public { - super.setUp(); - - // Upgrade eigenPodManager to wrapper - eigenPodManagerWrapper = new EigenPodManagerWrapper( - ethPOSMock, - eigenPodBeacon, - IStrategyManager(address(strategyManagerMock)), - IDelegationManager(address(delegationManagerMock)) - ); - eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper)); - } - - // function testFuzz_shareAdjustment_negativeToNegative(int256 sharesBefore, int256 sharesAfter) public { - // cheats.assume(sharesBefore <= 0); - // cheats.assume(sharesAfter <= 0); + function testFuzz_revert_negativeDepositShares(int224 sharesBefore) public { + cheats.assume(sharesBefore < 0); - // int256 sharesDelta = eigenPodManagerWrapper.calculateChangeInDelegatableShares(sharesBefore, sharesAfter); - // assertEq(sharesDelta, 0, "Shares delta must be 0"); - // } + // Initialize shares + _initializePodWithShares(defaultStaker, sharesBefore); - // function testFuzz_shareAdjustment_negativeToPositive(int256 sharesBefore, int256 sharesAfter) public { - // cheats.assume(sharesBefore <= 0); - // cheats.assume(sharesAfter > 0); - - // int256 sharesDelta = eigenPodManagerWrapper.calculateChangeInDelegatableShares(sharesBefore, sharesAfter); - // assertEq(sharesDelta, sharesAfter, "Shares delta must be equal to sharesAfter"); - // } + // Record balance update + cheats.prank(address(defaultPod)); + cheats.expectRevert(IEigenPodManagerErrors.LegacyWithdrawalsNotCompleted.selector); + eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, 0, 0); + } - // function testFuzz_shareAdjustment_positiveToNegative(int256 sharesBefore, int256 sharesAfter) public { - // cheats.assume(sharesBefore > 0); - // cheats.assume(sharesAfter <= 0); - - // int256 sharesDelta = eigenPodManagerWrapper.calculateChangeInDelegatableShares(sharesBefore, sharesAfter); - // assertEq(sharesDelta, -sharesBefore, "Shares delta must be equal to the negative of sharesBefore"); - // } + function testFuzz_recordBalanceUpdate(int224 sharesBefore, int224 sharesDelta) public { + // Constrain inputs + int256 sharesBefore = sharesBefore * int256(GWEI_TO_WEI); + int256 sharesDelta = sharesDelta * int256(GWEI_TO_WEI); + + // Initialize shares + _initializePodWithShares(defaultStaker, sharesBefore); + + if (sharesBefore < 0) { + cheats.expectRevert(IEigenPodManagerErrors.LegacyWithdrawalsNotCompleted.selector); + } else if (sharesDelta > 0) { + // Add shares + cheats.expectEmit(true, true, true, true); + emit PodSharesUpdated(defaultStaker, sharesDelta); + cheats.expectEmit(true, true, true, true); + emit NewTotalShares(defaultStaker, sharesBefore + sharesDelta); + } + cheats.prank(address(defaultPod)); + eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, sharesDelta, 0); - // function testFuzz_shareAdjustment_positiveToPositive(int256 sharesBefore, int256 sharesAfter) public { - // cheats.assume(sharesBefore > 0); - // cheats.assume(sharesAfter > 0); - - // int256 sharesDelta = eigenPodManagerWrapper.calculateChangeInDelegatableShares(sharesBefore, sharesAfter); - // assertEq(sharesDelta, sharesAfter - sharesBefore, "Shares delta must be equal to the difference between sharesAfter and sharesBefore"); - // } + // Check storage + if (sharesBefore >= 0 && sharesDelta > 0) { + assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBefore + sharesDelta, "Shares not updated correctly"); + } else { + assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBefore, "Shares should not be adjusted"); + } + } } \ No newline at end of file diff --git a/src/test/unit/EigenPodUnit.t.sol b/src/test/unit/EigenPodUnit.t.sol index 4755bbc1d..bc278b9c8 100644 --- a/src/test/unit/EigenPodUnit.t.sol +++ b/src/test/unit/EigenPodUnit.t.sol @@ -807,7 +807,7 @@ contract EigenPodUnitTests_verifyWithdrawalCredentials is EigenPodUnitTests, Pro proofs.validatorFields[0][VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = invalidWithdrawalCredentials; cheats.startPrank(address(staker)); - cheats.expectRevert(IEigenPodErrors.WithdrawCredentialsNotForEigenPod.selector); + cheats.expectRevert(IEigenPodErrors.WithdrawalCredentialsNotForEigenPod.selector); pod.verifyWithdrawalCredentials({ beaconTimestamp: proofs.beaconTimestamp, stateRootProof: proofs.stateRootProof,