diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index c36f1fce9..9131d1f36 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -74,6 +74,8 @@ interface IEigenPodErrors { error InvalidEIP4788Response(); /// @dev Thrown when attempting to send an invalid amount to the beacon deposit contract. error MsgValueNot32ETH(); + /// @dev Thrown when attempting to send an amount less than one to the beacon deposit contract for compounding. + error MsgValueNotMoreThanOne(); /// @dev Thrown when provided `beaconTimestamp` is too far in the past. error BeaconTimestampTooFarInPast(); /// @dev Thrown when provided `beaconTimestamp` is before the last checkpoint @@ -87,7 +89,6 @@ interface IEigenPodTypes { INACTIVE, // doesnt exist ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod WITHDRAWN // withdrawn from the Beacon Chain - } /** @@ -140,47 +141,77 @@ interface IEigenPodEvents is IEigenPodTypes { /// @notice Emitted when an ETH validator stakes via this eigenPod event EigenPodStaked(bytes32 pubkeyHash); + /// @notice Emitted when an ETH validator compound stakes via this eigenPod + event EigenPodCompoundStaked(bytes32 indexed pubkeyHash, uint256 amount); + /// @notice Emitted when a pod owner updates the proof submitter address - event ProofSubmitterUpdated(address prevProofSubmitter, address newProofSubmitter); + event ProofSubmitterUpdated( + address prevProofSubmitter, + address newProofSubmitter + ); /// @notice Emitted when an ETH validator's withdrawal credentials are successfully verified to be pointed to this eigenPod event ValidatorRestaked(bytes32 pubkeyHash); /// @notice Emitted when an ETH validator's balance is proven to be updated. Here newValidatorBalanceGwei // is the validator's balance that is credited on EigenLayer. - event ValidatorBalanceUpdated(bytes32 pubkeyHash, uint64 balanceTimestamp, uint64 newValidatorBalanceGwei); + event ValidatorBalanceUpdated( + bytes32 pubkeyHash, + uint64 balanceTimestamp, + uint64 newValidatorBalanceGwei + ); /// @notice Emitted when restaked beacon chain ETH is withdrawn from the eigenPod. - event RestakedBeaconChainETHWithdrawn(address indexed recipient, uint256 amount); + event RestakedBeaconChainETHWithdrawn( + address indexed recipient, + uint256 amount + ); /// @notice Emitted when ETH is received via the `receive` fallback event NonBeaconChainETHReceived(uint256 amountReceived); /// @notice Emitted when a checkpoint is created event CheckpointCreated( - uint64 indexed checkpointTimestamp, bytes32 indexed beaconBlockRoot, uint256 validatorCount + uint64 indexed checkpointTimestamp, + bytes32 indexed beaconBlockRoot, + uint256 validatorCount ); /// @notice Emitted when a checkpoint is finalized - event CheckpointFinalized(uint64 indexed checkpointTimestamp, int256 totalShareDeltaWei); + event CheckpointFinalized( + uint64 indexed checkpointTimestamp, + int256 totalShareDeltaWei + ); /// @notice Emitted when a validator is proven for a given checkpoint - event ValidatorCheckpointed(uint64 indexed checkpointTimestamp, bytes32 indexed pubkeyHash); + event ValidatorCheckpointed( + uint64 indexed checkpointTimestamp, + bytes32 indexed pubkeyHash + ); /// @notice Emitted when a validator is proven to have 0 balance at a given checkpoint - event ValidatorWithdrawn(uint64 indexed checkpointTimestamp, bytes32 indexed pubkeyHash); + event ValidatorWithdrawn( + uint64 indexed checkpointTimestamp, + bytes32 indexed pubkeyHash + ); /// @notice Emitted when a consolidation request is initiated where source == target event SwitchToCompoundingRequested(bytes32 indexed validatorPubkeyHash); /// @notice Emitted when a standard consolidation request is initiated - event ConsolidationRequested(bytes32 indexed sourcePubkeyHash, bytes32 indexed targetPubkeyHash); + event ConsolidationRequested( + bytes32 indexed sourcePubkeyHash, + bytes32 indexed targetPubkeyHash + ); /// @notice Emitted when a withdrawal request is initiated where request.amountGwei == 0 event ExitRequested(bytes32 indexed validatorPubkeyHash); /// @notice Emitted when a partial withdrawal request is initiated - event WithdrawalRequested(bytes32 indexed validatorPubkeyHash, uint64 withdrawalAmountGwei); + event WithdrawalRequested( + bytes32 indexed validatorPubkeyHash, + uint64 withdrawalAmountGwei + ); } /** @@ -192,20 +223,33 @@ interface IEigenPodEvents is IEigenPodTypes { */ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// @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; + function initialize(address owner) external; /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. /// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract. - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable; + + /// @notice Called by EigenPodManager when the owner wants the compounding validator. + /// @dev This function only supports staking to a 0x02 validator. + function stakeCompounding( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable; /** * @notice Transfers `amountWei` from this contract to the `recipient`. Only callable by the EigenPodManager as part * of the DelegationManager's withdrawal flow. * @dev `amountWei` is not required to be a whole Gwei amount. Amounts less than a Gwei multiple may be unrecoverable due to Gwei conversion. */ - function withdrawRestakedBeaconChainETH(address recipient, uint256 amount) external; + function withdrawRestakedBeaconChainETH( + address recipient, + uint256 amount + ) external; /** * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed @@ -219,9 +263,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { * @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; + function startCheckpoint(bool revertIfNoBalance) external; /** * @dev Progress the current checkpoint towards completion by submitting one or more validator @@ -390,7 +432,11 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { ) external payable; /// @notice called by owner of a pod to remove any ERC20s deposited in the pod - function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external; /// @notice Allows the owner of a pod to update the proof submitter, a permissioned /// address that can call various EigenPod methods, but cannot trigger asset withdrawals @@ -400,9 +446,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// 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 EigenPod methods. - function setProofSubmitter( - address newProofSubmitter - ) external; + function setProofSubmitter(address newProofSubmitter) external; /** * @@ -418,7 +462,10 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// @notice Native ETH in the pod that has been accounted for in a checkpoint (denominated in gwei). /// This amount is withdrawable from the pod via the DelegationManager withdrawal flow. - function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); + function withdrawableRestakedExecutionLayerGwei() + external + view + returns (uint64); /// @notice The single EigenPodManager for EigenLayer function eigenPodManager() external view returns (IEigenPodManager); @@ -487,9 +534,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// - The final partial withdrawal for an exited validator will be likely be included in this mapping. /// i.e. if a validator was last checkpointed at 32.1 ETH before exiting, the next checkpoint will calculate their /// "exited" amount to be 32.1 ETH rather than 32 ETH. - function checkpointBalanceExitedGwei( - uint64 - ) external view returns (uint64); + function checkpointBalanceExitedGwei(uint64) external view returns (uint64); /// @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 diff --git a/src/contracts/interfaces/IEigenPodManager.sol b/src/contracts/interfaces/IEigenPodManager.sol index b7814e988..e9469e1ff 100644 --- a/src/contracts/interfaces/IEigenPodManager.sol +++ b/src/contracts/interfaces/IEigenPodManager.sol @@ -104,6 +104,16 @@ interface IEigenPodManager is */ function createPod() external returns (address); + /** + * @notice Stakes for a new beacon chain validator with compounding 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 stakeCompounding(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; + + /** * @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. diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 94117ddca..084be24ae 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -45,15 +45,18 @@ contract EigenPod is /// @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; + address internal constant BEACON_ROOTS_ADDRESS = + 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; /// @notice The address of the EIP-7002 withdrawal request predeploy /// (See https://eips.ethereum.org/EIPS/eip-7002) - address internal constant WITHDRAWAL_REQUEST_ADDRESS = 0x00000961Ef480Eb55e80D19ad83579A64c007002; + address internal constant WITHDRAWAL_REQUEST_ADDRESS = + 0x00000961Ef480Eb55e80D19ad83579A64c007002; /// @notice The address of the EIP-7251 consolidation request predeploy /// (See https://eips.ethereum.org/EIPS/eip-7251) - address internal constant CONSOLIDATION_REQUEST_ADDRESS = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; + address internal constant CONSOLIDATION_REQUEST_ADDRESS = + 0x0000BBdDc7CE488642fb579F8B00f3a590007251; /// @notice The length of the EIP-4788 beacon block root ring buffer uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; @@ -84,7 +87,10 @@ contract EigenPod is /// @notice Callable only by the pod's owner or proof submitter modifier onlyOwnerOrProofSubmitter() { - require(msg.sender == podOwner || msg.sender == proofSubmitter, OnlyEigenPodOwnerOrProofSubmitter()); + require( + msg.sender == podOwner || msg.sender == proofSubmitter, + OnlyEigenPodOwnerOrProofSubmitter() + ); _; } @@ -93,10 +99,11 @@ contract EigenPod is * 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), CurrentlyPaused()); + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + CurrentlyPaused() + ); _; } @@ -116,9 +123,7 @@ contract EigenPod is } /// @inheritdoc IEigenPod - function initialize( - address _podOwner - ) external initializer { + function initialize(address _podOwner) external initializer { require(_podOwner != address(0), InputAddressZero()); podOwner = _podOwner; } @@ -137,7 +142,11 @@ contract EigenPod is /// @inheritdoc IEigenPod function startCheckpoint( bool revertIfNoBalance - ) external onlyOwnerOrProofSubmitter onlyWhenNotPaused(PAUSED_START_CHECKPOINT) { + ) + external + onlyOwnerOrProofSubmitter + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { _startCheckpoint(revertIfNoBalance); } @@ -162,7 +171,9 @@ contract EigenPod is uint64 exitedBalancesGwei; for (uint256 i = 0; i < proofs.length; i++) { BeaconChainProofs.BalanceProof calldata proof = proofs[i]; - ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + 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 @@ -184,12 +195,17 @@ contract EigenPod is // 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. - (uint64 prevBalanceGwei, int64 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ - validatorInfo: validatorInfo, - checkpointTimestamp: checkpointTimestamp, - balanceContainerRoot: balanceContainerProof.balanceContainerRoot, - proof: proof - }); + ( + uint64 prevBalanceGwei, + int64 balanceDeltaGwei, + uint64 exitedBalanceGwei + ) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof + .balanceContainerRoot, + proof: proof + }); checkpoint.proofsRemaining--; checkpoint.prevBeaconBalanceGwei += prevBalanceGwei; @@ -213,21 +229,31 @@ contract EigenPod is uint40[] calldata validatorIndices, bytes[] calldata validatorFieldsProofs, bytes32[][] calldata validatorFields - ) external onlyOwnerOrProofSubmitter onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) { + ) + external + onlyOwnerOrProofSubmitter + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { require( - (validatorIndices.length == validatorFieldsProofs.length) - && (validatorFieldsProofs.length == validatorFields.length), + (validatorIndices.length == validatorFieldsProofs.length) && + (validatorFieldsProofs.length == validatorFields.length), InputArrayLengthMismatch() ); // 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, BeaconTimestampTooFarInPast()); + require( + beaconTimestamp > currentCheckpointTimestamp, + BeaconTimestampTooFarInPast() + ); // For sanity, we want to ensure that a newly-verified validator cannot be proven against state // that has already been checkpointed. This check makes the state transitions easier to reason about. - require(beaconTimestamp > lastCheckpointTimestamp, BeaconTimestampBeforeLatestCheckpoint()); + require( + beaconTimestamp > lastCheckpointTimestamp, + BeaconTimestampBeforeLatestCheckpoint() + ); // Verify passed-in `beaconStateRoot` against the beacon block root // forgefmt: disable-next-item @@ -261,9 +287,15 @@ contract EigenPod is uint64 beaconTimestamp, BeaconChainProofs.StateRootProof calldata stateRootProof, BeaconChainProofs.ValidatorProof calldata proof - ) external onlyWhenNotPaused(PAUSED_START_CHECKPOINT) onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) { + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); - ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + 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 @@ -282,13 +314,22 @@ contract EigenPod is // 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, BeaconTimestampTooFarInPast()); + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + BeaconTimestampTooFarInPast() + ); // Validator must be checkpoint-able - require(validatorInfo.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + ValidatorNotActiveInPod() + ); // Validator must be slashed on the beacon chain - require(proof.validatorFields.isValidatorSlashed(), ValidatorNotSlashedOnBeaconChain()); + require( + proof.validatorFields.isValidatorSlashed(), + ValidatorNotSlashedOnBeaconChain() + ); // Verify passed-in `beaconStateRoot` against the beacon block root // forgefmt: disable-next-item @@ -313,7 +354,12 @@ contract EigenPod is /// @inheritdoc IEigenPod function requestConsolidation( ConsolidationRequest[] calldata requests - ) external payable onlyWhenNotPaused(PAUSED_CONSOLIDATIONS) onlyOwnerOrProofSubmitter { + ) + external + payable + onlyWhenNotPaused(PAUSED_CONSOLIDATIONS) + onlyOwnerOrProofSubmitter + { uint256 fee = getConsolidationRequestFee(); uint256 totalFee = fee * requests.length; require(msg.value >= totalFee, InsufficientFunds()); @@ -325,16 +371,26 @@ contract EigenPod is // Ensure target has verified withdrawal credentials pointed at this pod bytes32 sourcePubkeyHash = _calcPubkeyHash(request.srcPubkey); bytes32 targetPubkeyHash = _calcPubkeyHash(request.targetPubkey); - require(validatorStatus(targetPubkeyHash) == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + require( + validatorStatus(targetPubkeyHash) == VALIDATOR_STATUS.ACTIVE, + ValidatorNotActiveInPod() + ); // Call the predeploy - bytes memory callData = bytes.concat(request.srcPubkey, request.targetPubkey); - (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); + bytes memory callData = bytes.concat( + request.srcPubkey, + request.targetPubkey + ); + (bool ok, ) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}( + callData + ); require(ok, PredeployFailed()); // Emit event depending on whether this is a switch to 0x02, or a regular consolidation - if (sourcePubkeyHash == targetPubkeyHash) emit SwitchToCompoundingRequested(sourcePubkeyHash); - else emit ConsolidationRequested(sourcePubkeyHash, targetPubkeyHash); + if (sourcePubkeyHash == targetPubkeyHash) + emit SwitchToCompoundingRequested(sourcePubkeyHash); + else + emit ConsolidationRequested(sourcePubkeyHash, targetPubkeyHash); } // Refund remainder of msg.value @@ -346,7 +402,12 @@ contract EigenPod is /// @inheritdoc IEigenPod function requestWithdrawal( WithdrawalRequest[] calldata requests - ) external payable onlyWhenNotPaused(PAUSED_WITHDRAWAL_REQUESTS) onlyOwnerOrProofSubmitter { + ) + external + payable + onlyWhenNotPaused(PAUSED_WITHDRAWAL_REQUESTS) + onlyOwnerOrProofSubmitter + { uint256 fee = getWithdrawalRequestFee(); uint256 totalFee = fee * requests.length; require(msg.value >= totalFee, InsufficientFunds()); @@ -357,11 +418,17 @@ contract EigenPod is bytes32 pubkeyHash = _calcPubkeyHash(request.pubkey); // Ensure validator has verified withdrawal credentials pointed at this pod - require(validatorStatus(pubkeyHash) == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + require( + validatorStatus(pubkeyHash) == VALIDATOR_STATUS.ACTIVE, + ValidatorNotActiveInPod() + ); // Call the predeploy - bytes memory callData = abi.encodePacked(request.pubkey, request.amountGwei); - (bool ok,) = WITHDRAWAL_REQUEST_ADDRESS.call{value: fee}(callData); + bytes memory callData = abi.encodePacked( + request.pubkey, + request.amountGwei + ); + (bool ok, ) = WITHDRAWAL_REQUEST_ADDRESS.call{value: fee}(callData); require(ok, PredeployFailed()); // Emit event depending on whether the request is a full exit or a partial withdrawal @@ -380,8 +447,15 @@ contract EigenPod is IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient - ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { - require(tokenList.length == amountsToWithdraw.length, InputArrayLengthMismatch()); + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) + { + require( + tokenList.length == amountsToWithdraw.length, + InputArrayLengthMismatch() + ); for (uint256 i = 0; i < tokenList.length; i++) { tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); } @@ -403,15 +477,52 @@ contract EigenPod is ) external payable onlyEigenPodManager { // stake on ethpos require(msg.value == 32 ether, MsgValueNot32ETH()); - ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + ethPOS.deposit{value: 32 ether}( + pubkey, + _podWithdrawalCredentials(), + signature, + depositDataRoot + ); emit EigenPodStaked(_calcPubkeyHash(pubkey)); } /// @inheritdoc IEigenPod - function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + function stakeCompounding( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + bytes32 validatorPubkeyHash = _calcPubkeyHash(pubkey); + + // check msg.value + if (validatorStatus(validatorPubkeyHash) == VALIDATOR_STATUS.INACTIVE) { + require(msg.value == 32 ether, MsgValueNot32ETH()); + } else { + require(msg.value >= 1 ether, MsgValueNotMoreThanOne()); + } + + // stake on ethpos + + ethPOS.deposit{value: msg.value}( + pubkey, + _podCompoundingWithdrawalCredentials(), + signature, + depositDataRoot + ); + emit EigenPodCompoundStaked(_calcPubkeyHash(pubkey), msg.value); + } + + /// @inheritdoc IEigenPod + function withdrawRestakedBeaconChainETH( + address recipient, + uint256 amountWei + ) external onlyEigenPodManager { uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); amountWei = amountGwei * GWEI_TO_WEI; - require(amountGwei <= restakedExecutionLayerGwei, InsufficientWithdrawableBalance()); + require( + amountGwei <= restakedExecutionLayerGwei, + InsufficientWithdrawableBalance() + ); restakedExecutionLayerGwei -= amountGwei; emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); // transfer ETH from pod to `recipient` directly @@ -438,10 +549,15 @@ contract EigenPod is bytes32[] calldata validatorFields ) internal returns (uint256) { bytes32 pubkeyHash = validatorFields.getPubkeyHash(); - ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[ + pubkeyHash + ]; // Withdrawal credential proofs should only be processed for "INACTIVE" validators - require(validatorInfo.status == VALIDATOR_STATUS.INACTIVE, CredentialsAlreadyVerified()); + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + CredentialsAlreadyVerified() + ); // 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 @@ -457,7 +573,9 @@ contract EigenPod is // 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, ValidatorInactiveOnBeaconChain() + validatorFields.getActivationEpoch() != + BeaconChainProofs.FAR_FUTURE_EPOCH, + ValidatorInactiveOnBeaconChain() ); // Validator should not already be in the process of exiting. This is an important property @@ -482,12 +600,18 @@ contract EigenPod is // 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, ValidatorIsExitingBeaconChain()); + require( + validatorFields.getExitEpoch() == + BeaconChainProofs.FAR_FUTURE_EPOCH, + ValidatorIsExitingBeaconChain() + ); // Ensure the validator's withdrawal credentials are pointed at this pod require( - validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()) - || validatorFields.getWithdrawalCredentials() == bytes32(_podCompoundingWithdrawalCredentials()), + validatorFields.getWithdrawalCredentials() == + bytes32(_podWithdrawalCredentials()) || + validatorFields.getWithdrawalCredentials() == + bytes32(_podCompoundingWithdrawalCredentials()), WithdrawalCredentialsNotForEigenPod() ); @@ -510,8 +634,9 @@ contract EigenPod is // 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; + uint64 lastCheckpointedAt = currentCheckpointTimestamp == 0 + ? lastCheckpointTimestamp + : currentCheckpointTimestamp; // Proofs complete - create the validator in state _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ @@ -527,7 +652,11 @@ contract EigenPod is _currentCheckpoint.prevBeaconBalanceGwei += restakedBalanceGwei; emit ValidatorRestaked(pubkeyHash); - emit ValidatorBalanceUpdated(pubkeyHash, lastCheckpointedAt, restakedBalanceGwei); + emit ValidatorBalanceUpdated( + pubkeyHash, + lastCheckpointedAt, + restakedBalanceGwei + ); return restakedBalanceGwei * GWEI_TO_WEI; } @@ -536,7 +665,14 @@ contract EigenPod is uint64 checkpointTimestamp, bytes32 balanceContainerRoot, BeaconChainProofs.BalanceProof calldata proof - ) internal returns (uint64 prevBalanceGwei, int64 balanceDeltaGwei, uint64 exitedBalanceGwei) { + ) + internal + returns ( + uint64 prevBalanceGwei, + int64 balanceDeltaGwei, + uint64 exitedBalanceGwei + ) + { // Verify validator balance against `balanceContainerRoot` prevBalanceGwei = validatorInfo.restakedBalanceGwei; uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ @@ -548,7 +684,11 @@ contract EigenPod is // Calculate change in the validator's balance since the last proof if (newBalanceGwei != prevBalanceGwei) { balanceDeltaGwei = int64(newBalanceGwei) - int64(prevBalanceGwei); - emit ValidatorBalanceUpdated(proof.pubkeyHash, checkpointTimestamp, newBalanceGwei); + emit ValidatorBalanceUpdated( + proof.pubkeyHash, + checkpointTimestamp, + newBalanceGwei + ); } validatorInfo.restakedBalanceGwei = newBalanceGwei; @@ -581,9 +721,7 @@ contract EigenPod is * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is * true, this method will revert */ - function _startCheckpoint( - bool revertIfNoBalance - ) internal { + function _startCheckpoint(bool revertIfNoBalance) internal { require(currentCheckpointTimestamp == 0, CheckpointAlreadyActive()); // Prevent a checkpoint being completable twice in the same block. This prevents an edge case @@ -591,7 +729,10 @@ contract EigenPod is // // 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), CannotCheckpointTwiceInSingleBlock()); + require( + lastCheckpointTimestamp != uint64(block.timestamp), + CannotCheckpointTwiceInSingleBlock() + ); // 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` @@ -602,7 +743,8 @@ contract EigenPod is // `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) - restakedExecutionLayerGwei; + uint64 podBalanceGwei = uint64(address(this).balance / GWEI_TO_WEI) - + restakedExecutionLayerGwei; // If the caller doesn't want a "0 balance" checkpoint, revert if (revertIfNoBalance && podBalanceGwei == 0) { @@ -625,7 +767,11 @@ contract EigenPod is currentCheckpointTimestamp = uint64(block.timestamp); _updateCheckpoint(checkpoint); - emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot, checkpoint.proofsRemaining); + emit CheckpointCreated( + uint64(block.timestamp), + checkpoint.beaconBlockRoot, + checkpoint.proofsRemaining + ); } /** @@ -636,9 +782,7 @@ contract EigenPod is * - `lastCheckpointTimestamp` is updated * - `currentCheckpointTimestamp` is set to zero */ - function _updateCheckpoint( - Checkpoint memory checkpoint - ) internal { + function _updateCheckpoint(Checkpoint memory checkpoint) internal { _currentCheckpoint = checkpoint; if (checkpoint.proofsRemaining != 0) { return; @@ -647,8 +791,10 @@ contract EigenPod is // Calculate the previous total restaked balance and change in restaked balance // Note: due to how these values are calculated, a negative `balanceDeltaGwei` // should NEVER be greater in magnitude than `prevRestakedBalanceGwei` - uint64 prevRestakedBalanceGwei = restakedExecutionLayerGwei + checkpoint.prevBeaconBalanceGwei; - int64 balanceDeltaGwei = int64(checkpoint.podBalanceGwei) + checkpoint.balanceDeltasGwei; + uint64 prevRestakedBalanceGwei = restakedExecutionLayerGwei + + checkpoint.prevBeaconBalanceGwei; + int64 balanceDeltaGwei = int64(checkpoint.podBalanceGwei) + + checkpoint.balanceDeltasGwei; // And native ETH when the checkpoint was started is now considered restaked. // Add it to `restakedExecutionLayerGwei`, which allows it to be withdrawn via @@ -676,7 +822,11 @@ contract EigenPod is return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } - function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) { + function _podCompoundingWithdrawalCredentials() + internal + view + returns (bytes memory) + { return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this)); } @@ -689,9 +839,7 @@ contract EigenPod is } /// @dev Returns the current fee required to query either the EIP-7002 or EIP-7251 predeploy - function _getFee( - address predeploy - ) internal view returns (uint256) { + function _getFee(address predeploy) internal view returns (uint256) { (bool success, bytes memory result) = predeploy.staticcall(""); require(success && result.length == 32, FeeQueryFailed()); @@ -709,9 +857,10 @@ contract EigenPod is /// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp` /// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block. - return proofTimestamp <= forkTimestamp - ? BeaconChainProofs.ProofVersion.DENEB - : BeaconChainProofs.ProofVersion.PECTRA; + return + proofTimestamp <= forkTimestamp + ? BeaconChainProofs.ProofVersion.DENEB + : BeaconChainProofs.ProofVersion.PECTRA; } /** @@ -721,7 +870,11 @@ contract EigenPod is */ /// @inheritdoc IEigenPod - function withdrawableRestakedExecutionLayerGwei() external view returns (uint64) { + function withdrawableRestakedExecutionLayerGwei() + external + view + returns (uint64) + { return restakedExecutionLayerGwei; } @@ -763,9 +916,15 @@ contract EigenPod is function getParentBlockRoot( uint64 timestamp ) public view returns (bytes32) { - require(block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, TimestampOutOfRange()); + require( + block.timestamp - timestamp < + BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + TimestampOutOfRange() + ); - (bool success, bytes memory result) = BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + (bool success, bytes memory result) = BEACON_ROOTS_ADDRESS.staticcall( + abi.encode(timestamp) + ); require(success && result.length > 0, InvalidEIP4788Response()); return abi.decode(result, (bytes32)); diff --git a/src/contracts/pods/EigenPodManager.sol b/src/contracts/pods/EigenPodManager.sol index 1c5822e92..2ab492b90 100644 --- a/src/contracts/pods/EigenPodManager.sol +++ b/src/contracts/pods/EigenPodManager.sol @@ -36,15 +36,16 @@ contract EigenPodManager is using Math for *; using SafeCast for *; - modifier onlyEigenPod( - address podOwner - ) { + modifier onlyEigenPod(address podOwner) { require(address(ownerToPod[podOwner]) == msg.sender, OnlyEigenPod()); _; } modifier onlyDelegationManager() { - require(msg.sender == address(delegationManager), OnlyDelegationManager()); + require( + msg.sender == address(delegationManager), + OnlyDelegationManager() + ); _; } @@ -67,13 +68,21 @@ contract EigenPodManager is _disableInitializers(); } - function initialize(address initialOwner, uint256 _initPausedStatus) external initializer { + function initialize( + address initialOwner, + uint256 _initPausedStatus + ) external initializer { _transferOwnership(initialOwner); _setPausedStatus(_initPausedStatus); } /// @inheritdoc IEigenPodManager - function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) nonReentrant returns (address) { + function createPod() + external + onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) + nonReentrant + returns (address) + { require(!hasPod(msg.sender), EigenPodAlreadyExists()); // deploy a pod if the sender doesn't have one already IEigenPod pod = _deployPod(); @@ -95,6 +104,24 @@ contract EigenPodManager is pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); } + /// @inheritdoc IEigenPodManager + function stakeCompounding( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) nonReentrant { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stakeCompounding{value: msg.value}( + pubkey, + signature, + depositDataRoot + ); + } + /// @inheritdoc IEigenPodManager function recordBeaconChainETHBalanceUpdate( address podOwner, @@ -102,7 +129,10 @@ contract EigenPodManager is int256 balanceDeltaWei ) external onlyEigenPod(podOwner) nonReentrant { require(podOwner != address(0), InputAddressZero()); - require(balanceDeltaWei % int256(GWEI_TO_WEI) == 0, SharesNotMultipleOfGwei()); + require( + balanceDeltaWei % int256(GWEI_TO_WEI) == 0, + SharesNotMultipleOfGwei() + ); // Negative shares only exist in certain cases where, prior to the slashing release, negative balance // deltas were reported after a pod owner queued a withdrawal for all their shares. // @@ -117,7 +147,10 @@ contract EigenPodManager is // a negative balance delta, the pod owner's beacon chain slashing factor is decreased, devaluing // their shares. If the delta is zero, then no action needs to be taken. if (balanceDeltaWei > 0) { - (uint256 prevDepositShares, uint256 addedShares) = _addShares(podOwner, uint256(balanceDeltaWei)); + (uint256 prevDepositShares, uint256 addedShares) = _addShares( + podOwner, + uint256(balanceDeltaWei) + ); // Update operator shares delegationManager.increaseDelegatedShares({ @@ -157,7 +190,8 @@ contract EigenPodManager is uint256 depositSharesToRemove ) external onlyDelegationManager nonReentrant returns (uint256) { require(strategy == beaconChainETHStrategy, InvalidStrategy()); - int256 updatedShares = podOwnerDepositShares[staker] - depositSharesToRemove.toInt256(); + int256 updatedShares = podOwnerDepositShares[staker] - + depositSharesToRemove.toInt256(); require(updatedShares >= 0, SharesNegative()); podOwnerDepositShares[staker] = updatedShares; @@ -220,7 +254,8 @@ contract EigenPodManager is sharesToWithdraw = 0; } - int256 updatedShares = currentDepositShares + int256(depositSharesToAdd); + int256 updatedShares = currentDepositShares + + int256(depositSharesToAdd); podOwnerDepositShares[staker] = updatedShares; emit PodSharesUpdated(staker, int256(depositSharesToAdd)); emit NewTotalShares(staker, updatedShares); @@ -228,7 +263,10 @@ contract EigenPodManager is // Withdraw ETH from EigenPod if (sharesToWithdraw > 0) { - ownerToPod[staker].withdrawRestakedBeaconChainETH(staker, sharesToWithdraw); + ownerToPod[staker].withdrawRestakedBeaconChainETH( + staker, + sharesToWithdraw + ); } } @@ -269,7 +307,10 @@ contract EigenPodManager is 0, bytes32(uint256(uint160(msg.sender))), // set the beacon address to the eigenPodBeacon and initialize it - abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + abi.encodePacked( + beaconProxyBytecode, + abi.encode(eigenPodBeacon, "") + ) ) ); pod.initialize(msg.sender); @@ -283,7 +324,10 @@ contract EigenPodManager is /// NOTE: if the staker ends with a non-positive balance, this returns (0, 0) /// @return prevDepositShares the shares the staker had before any were added /// @return addedShares the shares added to the staker's balance - function _addShares(address staker, uint256 shares) internal returns (uint256, uint256) { + function _addShares( + address staker, + uint256 shares + ) internal returns (uint256, uint256) { require(staker != address(0), InputAddressZero()); require(int256(shares) >= 0, SharesNegative()); @@ -317,32 +361,47 @@ contract EigenPodManager is uint256 prevRestakedBalanceWei, uint256 balanceDecreasedWei ) internal returns (uint64) { - uint256 newRestakedBalanceWei = prevRestakedBalanceWei - balanceDecreasedWei; + uint256 newRestakedBalanceWei = prevRestakedBalanceWei - + balanceDecreasedWei; uint64 prevBeaconSlashingFactor = beaconChainSlashingFactor(podOwner); // newBeaconSlashingFactor is less than prevBeaconSlashingFactor because // newRestakedBalanceWei < prevRestakedBalanceWei - uint64 newBeaconSlashingFactor = - uint64(prevBeaconSlashingFactor.mulDiv(newRestakedBalanceWei, prevRestakedBalanceWei)); - uint64 beaconChainSlashingFactorDecrease = prevBeaconSlashingFactor - newBeaconSlashingFactor; - _beaconChainSlashingFactor[podOwner] = - BeaconChainSlashingFactor({slashingFactor: newBeaconSlashingFactor, isSet: true}); - emit BeaconChainSlashingFactorDecreased(podOwner, prevBeaconSlashingFactor, newBeaconSlashingFactor); + uint64 newBeaconSlashingFactor = uint64( + prevBeaconSlashingFactor.mulDiv( + newRestakedBalanceWei, + prevRestakedBalanceWei + ) + ); + uint64 beaconChainSlashingFactorDecrease = prevBeaconSlashingFactor - + newBeaconSlashingFactor; + _beaconChainSlashingFactor[podOwner] = BeaconChainSlashingFactor({ + slashingFactor: newBeaconSlashingFactor, + isSet: true + }); + emit BeaconChainSlashingFactorDecreased( + podOwner, + prevBeaconSlashingFactor, + newBeaconSlashingFactor + ); return beaconChainSlashingFactorDecrease; } // VIEW FUNCTIONS /// @inheritdoc IEigenPodManager - function getPod( - address podOwner - ) public view returns (IEigenPod) { + 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 + keccak256( + abi.encodePacked( + beaconProxyBytecode, + abi.encode(eigenPodBeacon, "") + ) + ) //bytecode ) ); } @@ -350,25 +409,31 @@ contract EigenPodManager is } /// @inheritdoc IEigenPodManager - function hasPod( - address podOwner - ) public view returns (bool) { + function hasPod(address podOwner) public view returns (bool) { return address(ownerToPod[podOwner]) != address(0); } /// @notice Returns the current shares of `user` in `strategy` /// @dev strategy must be beaconChainETHStrategy /// @dev returns 0 if the user has negative shares - function stakerDepositShares(address user, IStrategy strategy) public view returns (uint256 depositShares) { + function stakerDepositShares( + address user, + IStrategy strategy + ) public view returns (uint256 depositShares) { require(strategy == beaconChainETHStrategy, InvalidStrategy()); - return podOwnerDepositShares[user] < 0 ? 0 : uint256(podOwnerDepositShares[user]); + return + podOwnerDepositShares[user] < 0 + ? 0 + : uint256(podOwnerDepositShares[user]); } /// @inheritdoc IEigenPodManager function beaconChainSlashingFactor( address podOwner ) public view returns (uint64) { - BeaconChainSlashingFactor memory bsf = _beaconChainSlashingFactor[podOwner]; + BeaconChainSlashingFactor memory bsf = _beaconChainSlashingFactor[ + podOwner + ]; return bsf.isSet ? bsf.slashingFactor : WAD; } } diff --git a/src/test/mocks/EigenPodMock.sol b/src/test/mocks/EigenPodMock.sol index f836f291d..a39e6abbe 100644 --- a/src/test/mocks/EigenPodMock.sol +++ b/src/test/mocks/EigenPodMock.sol @@ -17,6 +17,9 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { /// @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 {} + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator with compounding. + function stakeCompounding(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable {} + /** * @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.