diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index de7cc3974..3742f54e6 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -44,9 +44,9 @@ interface IEigenPod { */ struct VerifiedWithdrawal { // amount to send to a podOwner from a proven withdrawal - uint256 amountToSend; + uint256 amountToSendGwei; // difference in shares to be recorded in the eigenPodManager, as a result of the withdrawal - int256 sharesDelta; + int256 sharesDeltaGwei; } diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index e6d728916..a31795f8b 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -102,7 +102,15 @@ library BeaconChainProofs { uint256 internal constant HISTORICALBATCH_STATEROOTS_INDEX = 1; //Misc Constants - uint256 internal constant SLOTS_PER_EPOCH = 32; + + /// @notice The number of slots each epoch in the beacon chain + uint64 internal constant SLOTS_PER_EPOCH = 32; + + /// @notice The number of seconds in a slot in the beacon chain + uint64 internal constant SECONDS_PER_SLOT = 12; + + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; @@ -135,22 +143,6 @@ library BeaconChainProofs { bytes proof; } - /** - * - * @notice This function is parses the balanceRoot to get the uint64 balance of a validator. During merkleization of the - * beacon state balance tree, four uint64 values (making 32 bytes) are grouped together and treated as a single leaf in the merkle tree. Thus the - * validatorIndex mod 4 is used to determine which of the four uint64 values to extract from the balanceRoot. - * @param validatorIndex is the index of the validator being proven for. - * @param balanceRoot is the combination of 4 validator balances being proven for. - * @return The validator's balance, in Gwei - */ - function getBalanceFromBalanceRoot(uint40 validatorIndex, bytes32 balanceRoot) internal pure returns (uint64) { - uint256 bitShiftAmount = (validatorIndex % 4) * 64; - bytes32 validatorBalanceLittleEndian = bytes32((uint256(balanceRoot) << bitShiftAmount)); - uint64 validatorBalance = Endian.fromLittleEndianUint64(validatorBalanceLittleEndian); - return validatorBalance; - } - /** * @notice This function verifies merkle proofs of the fields of a certain validator against a beacon chain state root * @param validatorIndex the index of the proven validator @@ -198,7 +190,7 @@ library BeaconChainProofs { * @param validatorIndex the index of the proven validator * @param beaconStateRoot is the beacon chain state root to be proven against. * @param validatorBalanceProof is the proof of the balance against the beacon chain state root - * @param balanceRoot is the serialized balance used to prove the balance of the validator (refer to `getBalanceFromBalanceRoot` above for detailed explanation) + * @param balanceRoot is the serialized balance used to prove the balance of the validator (refer to `getBalanceAtIndex` for detailed explanation) */ function verifyValidatorBalance( bytes32 beaconStateRoot, @@ -414,4 +406,100 @@ library BeaconChainProofs { require(validatorPubkey.length == 48, "Input should be 48 bytes in length"); return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /** + * @dev Retrieve the withdrawal timestamp + */ + function getWithdrawalTimestamp(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot); + } + + /** + * @dev Converts the withdrawal's slot to an epoch + */ + function getWithdrawalEpoch(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(withdrawalProof.slotRoot) / SLOTS_PER_EPOCH; + } + + /** + * Getters for validator fields: + * 0: pubkey + * 1: withdrawal credentials + * 2: effective balance + * 3: slashed? + * 4: activation elligibility epoch + * 5: activation epoch + * 6: exit epoch + * 7: withdrawable epoch + */ + + /** + * @dev Retrieves a validator's pubkey hash + */ + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return + validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return + validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /** + * @dev Retrieves a validator's effective balance (in gwei) + */ + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /** + * @dev Retrieves a validator's withdrawable epoch + */ + function getWithdrawableEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); + } + + /** + * Getters for withdrawal fields: + * 0: withdrawal index + * 1: validator index + * 2: execution address + * 3: withdrawal amount + */ + + /** + * @dev Retrieves a withdrawal's validator index + */ + function getValidatorIndex(bytes32[] memory withdrawalFields) internal pure returns (uint40) { + return + uint40(Endian.fromLittleEndianUint64(withdrawalFields[WITHDRAWAL_VALIDATOR_INDEX_INDEX])); + } + + /** + * @dev Retrieves a withdrawal's withdrawal amount (in gwei) + */ + function getWithdrawalAmountGwei(bytes32[] memory withdrawalFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(withdrawalFields[WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); + } } diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index ffaf56896..19d713c94 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -36,6 +36,7 @@ import "./EigenPodPausingConstants.sol"; contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, EigenPodPausingConstants { using BytesLib for bytes; using SafeERC20 for IERC20; + using BeaconChainProofs for *; // CONSTANTS + IMMUTABLES // @notice Internal constant used in calculations, since the beacon chain stores balances in Gwei rather than wei @@ -44,9 +45,6 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen /// @notice Maximum "staleness" of a Beacon Chain state root against which `verifyBalanceUpdate` or `verifyWithdrawalCredentials` may be proven. uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; - /// @notice The number of seconds in a slot in the beacon chain - uint256 internal constant SECONDS_PER_SLOT = 12; - /// @notice This is the beacon chain deposit contract IETHPOSDeposit public immutable ethPOS; @@ -167,14 +165,6 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen hasRestaked = true; } - function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { - return _validatorPubkeyHashToInfo[validatorPubkeyHash]; - } - - function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { - return _validatorPubkeyHashToInfo[pubkeyHash].status; - } - /// @notice payable fallback function that receives ether deposited to the eigenpods contract receive() external payable { nonBeaconChainETHBalanceWei += msg.value; @@ -199,55 +189,50 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen BeaconChainProofs.BalanceUpdateProof calldata balanceUpdateProof, bytes32[] calldata validatorFields ) external onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE) { - // ensure that the timestamp being proven against is not "too stale", i.e. that the validator's balance *recently* changed. - require( - oracleTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp, - "EigenPod.verifyBalanceUpdate: specified timestamp is too far in past" - ); - - uint64 validatorBalance = BeaconChainProofs.getBalanceFromBalanceRoot(validatorIndex, balanceUpdateProof.balanceRoot); - - /** - * Reference: - * uint64 validatorWithdrawableEpoch = Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); - * uint64 oracleEpoch = _computeSlotAtTimestamp(oracleTimestamp)) / BeaconChainProofs.SLOTS_PER_EPOCH; - * require(validatorWithdrawableEpoch > oracleEpoch) - * checks that a balance update can only be made before the validator is withdrawable. If this is not checked - * anyone can prove the withdrawn validator's balance as 0 before the validator is able to prove their full withdrawal - */ - if ( - Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]) <= - (_computeSlotAtTimestamp(oracleTimestamp)) / BeaconChainProofs.SLOTS_PER_EPOCH - ) { - require(validatorBalance > 0, "EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn"); - } - bytes32 validatorPubkeyHash = validatorFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX]; - + uint64 validatorBalance = balanceUpdateProof.balanceRoot.getBalanceAtIndex(validatorIndex); + bytes32 validatorPubkeyHash = validatorFields.getPubkeyHash(); ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkeyHash]; - // verify that the validator has been proven to have its withdrawal credentials pointed to this EigenPod, and has not yet been proven to be exited - require(validatorInfo.status == VALIDATOR_STATUS.ACTIVE, "EigenPod.verifyBalanceUpdate: Validator not active"); - // check that the balance update is being made strictly after the previous balance update + // Verify balance update timing: + + // 1. Balance updates should only be performed on "ACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyBalanceUpdate: Validator not active" + ); + + // 2. Balance updates should be more recent than the most recent update require( validatorInfo.mostRecentBalanceUpdateTimestamp < oracleTimestamp, "EigenPod.verifyBalanceUpdate: Validators balance has already been updated for this timestamp" ); - { - // verify ETH validator proof - bytes32 latestBlockRoot = eigenPodManager.getBlockRootAtTimestamp(oracleTimestamp); - - // verify the provided state root against the oracle-provided latest block header - BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot({ - latestBlockRoot: latestBlockRoot, - beaconStateRoot: stateRootProof.beaconStateRoot, - stateRootProof: stateRootProof.proof - }); + // 3. Balance updates should not be "stale" (older than 4.5 hrs) + require( + oracleTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp, + "EigenPod.verifyBalanceUpdate: specified timestamp is too far in past" + ); + + // 4. Balance updates should only be made before a validator is fully withdrawn. + // -- A withdrawable validator may not have withdrawn yet, so we require their balance is nonzero + // -- A fully withdrawn validator should withdraw via verifyAndProcessWithdrawals + if (validatorFields.getWithdrawableEpoch() <= _timestampToEpoch(oracleTimestamp)) { + require( + validatorBalance > 0, + "EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn" + ); } - // verify the provided ValidatorFields against the provided state root, now that it has been proven against the latest block header + // Verify passed-in beaconStateRoot against oracle-provided block root: + BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot({ + latestBlockRoot: eigenPodManager.getBlockRootAtTimestamp(oracleTimestamp), + beaconStateRoot: stateRootProof.beaconStateRoot, + stateRootProof: stateRootProof.proof + }); + + // Verify passed-in validatorFields against verified beaconStateRoot: BeaconChainProofs.verifyValidatorFields({ beaconStateRoot: stateRootProof.beaconStateRoot, validatorFields: validatorFields, @@ -255,7 +240,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen validatorIndex: validatorIndex }); - // verify ETH validators current balance, which is stored in the `balances` container of the beacon state + // Verify passed-in validator balanceRoot against verified beaconStateRoot: BeaconChainProofs.verifyValidatorBalance({ beaconStateRoot: stateRootProof.beaconStateRoot, balanceRoot: balanceUpdateProof.balanceRoot, @@ -263,30 +248,25 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen validatorIndex: validatorIndex }); - // store the current restaked balance in memory, to be checked against later - uint64 currentRestakedBalanceGwei = validatorInfo.restakedBalanceGwei; + // Done with proofs! Now update the validator's balance and send to the EigenPodManager if needed - // deserialize the balance field from the balanceRoot and calculate the effective (pessimistic) restaked balance + uint64 currentRestakedBalanceGwei = validatorInfo.restakedBalanceGwei; uint64 newRestakedBalanceGwei = _calculateRestakedBalanceGwei(validatorBalance); - - // update the balance + + // Update validator balance and timestamp, and save to state: validatorInfo.restakedBalanceGwei = newRestakedBalanceGwei; - - //update the most recent balance update timestamp validatorInfo.mostRecentBalanceUpdateTimestamp = oracleTimestamp; - - // record validatorInfo update in storage _validatorPubkeyHashToInfo[validatorPubkeyHash] = validatorInfo; + // If our new and old balances differ, calculate the delta and send to the EigenPodManager if (newRestakedBalanceGwei != currentRestakedBalanceGwei) { emit ValidatorBalanceUpdated(validatorIndex, oracleTimestamp, newRestakedBalanceGwei); - int256 sharesDelta = _calculateSharesDelta({ - newAmountWei: (newRestakedBalanceGwei * GWEI_TO_WEI), - currentAmountWei: (currentRestakedBalanceGwei * GWEI_TO_WEI) + int256 sharesDeltaGwei = _calculateSharesDelta({ + newAmountGwei: newRestakedBalanceGwei, + previousAmountGwei: currentRestakedBalanceGwei }); - // update shares in strategy manager - eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, sharesDelta); + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, sharesDeltaGwei * int256(GWEI_TO_WEI)); } } @@ -329,21 +309,21 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen validatorFields[i], withdrawalFields[i] ); - withdrawalSummary.amountToSend += verifiedWithdrawal.amountToSend; - withdrawalSummary.sharesDelta += verifiedWithdrawal.sharesDelta; + withdrawalSummary.amountToSendGwei += verifiedWithdrawal.amountToSendGwei; + withdrawalSummary.sharesDeltaGwei += verifiedWithdrawal.sharesDeltaGwei; } // send ETH to the `recipient` via the DelayedWithdrawalRouter, if applicable - if (withdrawalSummary.amountToSend != 0) { - _sendETH_AsDelayedWithdrawal(podOwner, withdrawalSummary.amountToSend); + if (withdrawalSummary.amountToSendGwei != 0) { + _sendETH_AsDelayedWithdrawal(podOwner, withdrawalSummary.amountToSendGwei * GWEI_TO_WEI); } //update podOwner's shares in the strategy manager - if (withdrawalSummary.sharesDelta != 0) { - eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, withdrawalSummary.sharesDelta); + if (withdrawalSummary.sharesDeltaGwei != 0) { + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, withdrawalSummary.sharesDeltaGwei * int256(GWEI_TO_WEI)); } } /******************************************************************************* - EXTERNAL FUNCTIONS CALLABLE BY EIGEN POD OWNER + EXTERNAL FUNCTIONS CALLABLE BY EIGENPOD OWNER *******************************************************************************/ /** @@ -507,7 +487,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen bytes calldata validatorFieldsProof, bytes32[] calldata validatorFields ) internal returns (uint256) { - bytes32 validatorPubkeyHash = validatorFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX]; + bytes32 validatorPubkeyHash = validatorFields.getPubkeyHash(); ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkeyHash]; @@ -517,8 +497,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen ); require( - validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] == - bytes32(_podWithdrawalCredentials()), + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), "EigenPod.verifyCorrectWithdrawalCredentials: Proof is not for this EigenPod" ); /** @@ -530,9 +509,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen * actual validator balance by 0.25 ETH. In EigenLayer, we calculate our own "restaked balance" which is a further pessimistic * view of the validator's effective balance. */ - uint64 validatorEffectiveBalanceGwei = Endian.fromLittleEndianUint64( - validatorFields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX] - ); + uint64 validatorEffectiveBalanceGwei = validatorFields.getEffectiveBalanceGwei(); // verify the provided ValidatorFields against the provided state root, now that it has been proven against the latest block header BeaconChainProofs.verifyValidatorFields({ @@ -576,12 +553,11 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen * This difference in modifier usage is OK, since it is still not possible to `verifyAndProcessWithdrawal` against a slot that occurred * *prior* to the proof provided in the `verifyWithdrawalCredentials` function. */ - proofIsForValidTimestamp(Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot)) + proofIsForValidTimestamp(withdrawalProof.getWithdrawalTimestamp()) returns (VerifiedWithdrawal memory) { - uint64 withdrawalHappenedTimestamp = Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot); - - bytes32 validatorPubkeyHash = validatorFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX]; + uint64 withdrawalTimestamp = withdrawalProof.getWithdrawalTimestamp(); + bytes32 validatorPubkeyHash = validatorFields.getPubkeyHash(); /** * If the validator status is inactive, then withdrawal credentials were never verified for the validator, @@ -593,19 +569,21 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen ); require( - !provenWithdrawal[validatorPubkeyHash][withdrawalHappenedTimestamp], + !provenWithdrawal[validatorPubkeyHash][withdrawalTimestamp], "EigenPod._verifyAndProcessWithdrawal: withdrawal has already been proven for this timestamp" ); - provenWithdrawal[validatorPubkeyHash][withdrawalHappenedTimestamp] = true; + provenWithdrawal[validatorPubkeyHash][withdrawalTimestamp] = true; // Verifying the withdrawal as well as the slot - BeaconChainProofs.verifyWithdrawal({beaconStateRoot: beaconStateRoot, withdrawalFields: withdrawalFields, withdrawalProof: withdrawalProof}); + BeaconChainProofs.verifyWithdrawal({ + beaconStateRoot: beaconStateRoot, + withdrawalFields: withdrawalFields, + withdrawalProof: withdrawalProof + }); { - uint40 validatorIndex = uint40( - Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX]) - ); + uint40 validatorIndex = withdrawalFields.getValidatorIndex(); // Verifying the validator fields, specifically the withdrawable epoch BeaconChainProofs.verifyValidatorFields({ @@ -615,25 +593,18 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen validatorIndex: validatorIndex }); - uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64( - withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX] - ); - + uint64 withdrawalAmountGwei = withdrawalFields.getWithdrawalAmountGwei(); + /** - * if the validator's withdrawable epoch is less than or equal to the slot's epoch, then the validator has fully withdrawn because - * a full withdrawal is only processable after the withdrawable epoch has passed. - * @Note: uint64 withdrawableEpoch = Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); - * @Note:uint64 slot = Endian.fromLittleEndianUint64(withdrawalProof.slotRoot) + * If the withdrawal's epoch comes after the validator's "withdrawable epoch," we know the validator + * has fully withdrawn, and we process this as a full withdrawal. */ - if ( - Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]) <= - (Endian.fromLittleEndianUint64(withdrawalProof.slotRoot)) / BeaconChainProofs.SLOTS_PER_EPOCH - ) { + if (withdrawalProof.getWithdrawalEpoch() >= validatorFields.getWithdrawableEpoch()) { return _processFullWithdrawal( validatorIndex, validatorPubkeyHash, - withdrawalHappenedTimestamp, + withdrawalTimestamp, podOwner, withdrawalAmountGwei, _validatorPubkeyHashToInfo[validatorPubkeyHash] @@ -642,7 +613,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen return _processPartialWithdrawal( validatorIndex, - withdrawalHappenedTimestamp, + withdrawalTimestamp, podOwner, withdrawalAmountGwei ); @@ -653,74 +624,79 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen function _processFullWithdrawal( uint40 validatorIndex, bytes32 validatorPubkeyHash, - uint64 withdrawalHappenedTimestamp, + uint64 withdrawalTimestamp, address recipient, uint64 withdrawalAmountGwei, ValidatorInfo memory validatorInfo ) internal returns (VerifiedWithdrawal memory) { - VerifiedWithdrawal memory verifiedWithdrawal; - uint256 withdrawalAmountWei; - - uint256 currentValidatorRestakedBalanceWei = validatorInfo.restakedBalanceGwei * GWEI_TO_WEI; /** - * If the validator is already withdrawn and additional deposits are made, they will be automatically withdrawn - * in the beacon chain as a full withdrawal. Thus such a validator can prove another full withdrawal, and - * withdraw that ETH via the queuedWithdrawal flow in the strategy manager. + * First, determine withdrawal amounts. We need to know: + * 1. How much can be withdrawn immediately + * 2. How much needs to be withdrawn via the EigenLayer withdrawal queue */ - // if the withdrawal amount is greater than the MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR (i.e. the max amount restaked on EigenLayer, per ETH validator) + + uint64 amountToQueueGwei; + if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { - // then the excess is immediately withdrawable - verifiedWithdrawal.amountToSend = - uint256(withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * - uint256(GWEI_TO_WEI); - // and the extra execution layer ETH in the contract is MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, which must be withdrawn through EigenLayer's normal withdrawal process - withdrawableRestakedExecutionLayerGwei += MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; - withdrawalAmountWei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; + amountToQueueGwei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; } else { - // otherwise, just use the full withdrawal amount to continue to "back" the podOwner's remaining shares in EigenLayer - // (i.e. none is instantly withdrawable) - withdrawableRestakedExecutionLayerGwei += withdrawalAmountGwei; - withdrawalAmountWei = withdrawalAmountGwei * GWEI_TO_WEI; - } - // if the amount being withdrawn is not equal to the current accounted for validator balance, an update must be made - if (currentValidatorRestakedBalanceWei != withdrawalAmountWei) { - verifiedWithdrawal.sharesDelta = _calculateSharesDelta({ - newAmountWei: withdrawalAmountWei, - currentAmountWei: currentValidatorRestakedBalanceWei - }); + amountToQueueGwei = withdrawalAmountGwei; } + /** + * If the withdrawal is for more than the max per-validator balance, we mark + * the max as "withdrawable" via the queue, and withdraw the excess immediately + */ + + VerifiedWithdrawal memory verifiedWithdrawal; + verifiedWithdrawal.amountToSendGwei = uint256(withdrawalAmountGwei - amountToQueueGwei); + withdrawableRestakedExecutionLayerGwei += amountToQueueGwei; + + /** + * Next, calculate the change in number of shares this validator is backing. + * - Anything immediately withdrawn isn't being backed + * - Anything that needs to go through the withdrawal queue is backed + * + * This means that this validator is currently backing `amountToQueueGwei` shares. + */ + + verifiedWithdrawal.sharesDeltaGwei = _calculateSharesDelta({ + newAmountGwei: amountToQueueGwei, + previousAmountGwei: validatorInfo.restakedBalanceGwei + }); + + /** + * Finally, the validator is fully withdrawn. Update their status and place in state: + */ - // now that the validator has been proven to be withdrawn, we can set their restaked balance to 0 validatorInfo.restakedBalanceGwei = 0; validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; - validatorInfo.mostRecentBalanceUpdateTimestamp = withdrawalHappenedTimestamp; - + validatorInfo.mostRecentBalanceUpdateTimestamp = withdrawalTimestamp; _validatorPubkeyHashToInfo[validatorPubkeyHash] = validatorInfo; - emit FullWithdrawalRedeemed(validatorIndex, withdrawalHappenedTimestamp, recipient, withdrawalAmountGwei); + emit FullWithdrawalRedeemed(validatorIndex, withdrawalTimestamp, recipient, withdrawalAmountGwei); return verifiedWithdrawal; } function _processPartialWithdrawal( uint40 validatorIndex, - uint64 withdrawalHappenedTimestamp, + uint64 withdrawalTimestamp, address recipient, uint64 partialWithdrawalAmountGwei ) internal returns (VerifiedWithdrawal memory) { emit PartialWithdrawalRedeemed( validatorIndex, - withdrawalHappenedTimestamp, + withdrawalTimestamp, recipient, partialWithdrawalAmountGwei ); return VerifiedWithdrawal({ - amountToSend: uint256(partialWithdrawalAmountGwei) * uint256(GWEI_TO_WEI), - sharesDelta: 0 + amountToSendGwei: uint256(partialWithdrawalAmountGwei), + sharesDeltaGwei: 0 }); } @@ -756,14 +732,34 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } - function _calculateSharesDelta(uint256 newAmountWei, uint256 currentAmountWei) internal pure returns (int256) { - return (int256(newAmountWei) - int256(currentAmountWei)); + /** + * Calculates delta between two share amounts and returns as an int256 + */ + function _calculateSharesDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int256) { + return + int256(uint256(newAmountGwei)) - int256(uint256(previousAmountGwei)); } - // reference: https://github.com/ethereum/consensus-specs/blob/ce240ca795e257fc83059c4adfd591328c7a7f21/specs/bellatrix/beacon-chain.md#compute_timestamp_at_slot - function _computeSlotAtTimestamp(uint64 timestamp) internal view returns (uint64) { - require(timestamp >= GENESIS_TIME, "EigenPod._computeSlotAtTimestamp: timestamp is before genesis"); - return uint64((timestamp - GENESIS_TIME) / SECONDS_PER_SLOT); + /** + * @dev Converts a timestamp to a beacon chain epoch by calculating the number of + * seconds since genesis, and dividing by seconds per epoch. + * reference: https://github.com/ethereum/consensus-specs/blob/ce240ca795e257fc83059c4adfd591328c7a7f21/specs/bellatrix/beacon-chain.md#compute_timestamp_at_slot + */ + function _timestampToEpoch(uint64 timestamp) internal view returns (uint64) { + require(timestamp >= GENESIS_TIME, "EigenPod._timestampToEpoch: timestamp is before genesis"); + return (timestamp - GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH; + } + + /******************************************************************************* + VIEW FUNCTIONS + *******************************************************************************/ + + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; } diff --git a/src/test/EigenPod.t.sol b/src/test/EigenPod.t.sol index 33bdc8a8e..f53395c6f 100644 --- a/src/test/EigenPod.t.sol +++ b/src/test/EigenPod.t.sol @@ -597,7 +597,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { setJSON("./src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); _proveOverCommittedStake(newPod); - uint64 newValidatorBalance = BeaconChainProofs.getBalanceFromBalanceRoot(uint40(getValidatorIndex()), getBalanceRoot()); + uint64 newValidatorBalance = BeaconChainProofs.getBalanceAtIndex(getBalanceRoot(), uint40(getValidatorIndex())); int256 beaconChainETHShares = eigenPodManager.podOwnerShares(podOwner); require(beaconChainETHShares == int256(_calculateRestakedBalanceGwei(newValidatorBalance) * GWEI_TO_WEI), @@ -739,7 +739,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { uint256 validatorRestakedBalanceAfter = newPod.validatorPubkeyHashToInfo(validatorPubkeyHash).restakedBalanceGwei; - uint64 newValidatorBalance = BeaconChainProofs.getBalanceFromBalanceRoot(uint40(getValidatorIndex()), getBalanceRoot()); + uint64 newValidatorBalance = BeaconChainProofs.getBalanceAtIndex(getBalanceRoot(), uint40(getValidatorIndex())); int256 shareDiff = beaconChainETHBefore - eigenPodManager.podOwnerShares(podOwner); assertTrue(eigenPodManager.podOwnerShares(podOwner) == int256(_calculateRestakedBalanceGwei(newValidatorBalance) * GWEI_TO_WEI), "hysterisis not working"); @@ -769,7 +769,7 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { uint256 validatorRestakedBalanceAfter = newPod.validatorPubkeyHashToInfo(validatorPubkeyHash).restakedBalanceGwei; - uint64 newValidatorBalance = BeaconChainProofs.getBalanceFromBalanceRoot(uint40(getValidatorIndex()), getBalanceRoot()); + uint64 newValidatorBalance = BeaconChainProofs.getBalanceAtIndex(getBalanceRoot(), uint40(getValidatorIndex())); int256 shareDiff = beaconChainETHBefore - eigenPodManager.podOwnerShares(podOwner); assertTrue(eigenPodManager.podOwnerShares(podOwner) == int256(_calculateRestakedBalanceGwei(newValidatorBalance) * GWEI_TO_WEI),