diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 5f354e7cd..9e1dfcd89 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -145,6 +145,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { ) 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; /** @@ -351,4 +352,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { function getParentBlockRoot( uint64 timestamp ) external view returns (bytes32); + + /// @notice Returns the timestamp of the Pectra fork, read from the `EigenPodManager` contract + function getPectraForkTimestamp() external view returns (uint64); } diff --git a/src/contracts/interfaces/IEigenPodManager.sol b/src/contracts/interfaces/IEigenPodManager.sol index baf574cde..fe9d1ac0b 100644 --- a/src/contracts/interfaces/IEigenPodManager.sol +++ b/src/contracts/interfaces/IEigenPodManager.sol @@ -25,6 +25,8 @@ interface IEigenPodManagerErrors { /// @dev Thrown when the pods shares are negative and a beacon chain balance update is attempted. /// The podOwner should complete legacy withdrawal first. error LegacyWithdrawalsNotCompleted(); + /// @dev Thrown when caller is not the proof timestamp setter + error OnlyProofTimestampSetter(); } interface IEigenPodManagerEvents { @@ -57,6 +59,12 @@ interface IEigenPodManagerEvents { /// @notice Emitted when an operator is slashed and shares to be burned are increased event BurnableETHSharesIncreased(uint256 shares); + + /// @notice Emitted when the Pectra fork timestamp is updated + event PectraForkTimestampSet(uint64 newPectraForkTimestamp); + + /// @notice Emitted when the proof timestamp setter is updated + event ProofTimestampSetterSet(address newProofTimestampSetter); } interface IEigenPodManagerTypes { @@ -120,6 +128,16 @@ interface IEigenPodManager is int256 balanceDeltaWei ) external; + /// @notice Sets the address that can set proof timestamps + function setProofTimestampSetter( + address newProofTimestampSetter + ) external; + + /// @notice Sets the Pectra fork timestamp, only callable by `proofTimestampSetter` + function setPectraForkTimestamp( + uint64 timestamp + ) external; + /// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed. function ownerToPod( address podOwner @@ -169,4 +187,8 @@ interface IEigenPodManager is /// @notice Returns the accumulated amount of beacon chain ETH Strategy shares function burnableETHShares() external view returns (uint256); + + /// @notice Returns the timestamp of the Pectra hard fork + /// @dev Specifically, this returns the timestamp of the first non-missed slot at or after the Pectra hard fork + function pectraForkTimestamp() external view returns (uint64); } diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index 7e176653b..42512e139 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -28,7 +28,8 @@ library BeaconChainProofs { /// | HEIGHT: VALIDATOR_TREE_HEIGHT /// individual validators uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; - uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant DENEB_BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant PECTRA_BEACON_STATE_TREE_HEIGHT = 6; uint256 internal constant BALANCE_TREE_HEIGHT = 38; uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; @@ -134,6 +135,8 @@ library BeaconChainProofs { /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` /// @param validatorIndex the validator's unique index function verifyValidatorFields( + uint64 proofTimestamp, + uint64 pectraForkTimestamp, bytes32 beaconStateRoot, bytes32[] calldata validatorFields, bytes calldata validatorFieldsProof, @@ -141,10 +144,12 @@ library BeaconChainProofs { ) internal view { require(validatorFields.length == VALIDATOR_FIELDS_LENGTH, InvalidValidatorFieldsLength()); + uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofTimestamp, pectraForkTimestamp); + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for /// this container includes hashing the root of the validator tree with the length of the validator list require( - validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + beaconStateTreeHeight), InvalidProofLength() ); @@ -185,10 +190,16 @@ library BeaconChainProofs { /// against the same balance container root. /// @param beaconBlockRoot merkle root of the beacon block /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` - function verifyBalanceContainer(bytes32 beaconBlockRoot, BalanceContainerProof calldata proof) internal view { + function verifyBalanceContainer( + uint64 proofTimestamp, + uint64 pectraForkTimestamp, + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofTimestamp, pectraForkTimestamp); + require( - proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), - InvalidProofLength() + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + beaconStateTreeHeight), InvalidProofLength() ); /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: @@ -197,7 +208,7 @@ library BeaconChainProofs { /// -- beaconStateRoot /// | HEIGHT: BEACON_STATE_TREE_HEIGHT /// ---- balancesContainerRoot - uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + uint256 index = (STATE_ROOT_INDEX << (beaconStateTreeHeight)) | BALANCE_CONTAINER_INDEX; require( Merkle.verifyInclusionSha256({ @@ -312,4 +323,13 @@ library BeaconChainProofs { ) internal pure returns (uint64) { return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); } + + /// @dev 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. + function getBeaconStateTreeHeight( + uint64 proofTimestamp, + uint64 pectraForkTimestamp + ) internal pure returns (uint256) { + return proofTimestamp <= pectraForkTimestamp ? DENEB_BEACON_STATE_TREE_HEIGHT : PECTRA_BEACON_STATE_TREE_HEIGHT; + } } diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 1066b2fbf..61f48644e 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -159,6 +159,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // Verify `balanceContainerProof` against `beaconBlockRoot` BeaconChainProofs.verifyBalanceContainer({ + proofTimestamp: checkpointTimestamp, + pectraForkTimestamp: getPectraForkTimestamp(), beaconBlockRoot: checkpoint.beaconBlockRoot, proof: balanceContainerProof }); @@ -254,6 +256,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC for (uint256 i = 0; i < validatorIndices.length; i++) { // forgefmt: disable-next-item totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + beaconTimestamp, stateRootProof.beaconStateRoot, validatorIndices[i], validatorFieldsProofs[i], @@ -341,6 +344,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // Verify Validator container proof against `beaconStateRoot` BeaconChainProofs.verifyValidatorFields({ + proofTimestamp: beaconTimestamp, + pectraForkTimestamp: getPectraForkTimestamp(), beaconStateRoot: stateRootProof.beaconStateRoot, validatorFields: proof.validatorFields, validatorFieldsProof: proof.proof, @@ -378,6 +383,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC } /// @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, @@ -419,13 +425,13 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC * @param validatorFields are the fields of the "Validator Container", refer to consensus specs */ function _verifyWithdrawalCredentials( + uint64 beaconTimestamp, bytes32 beaconStateRoot, uint40 validatorIndex, bytes calldata validatorFieldsProof, bytes32[] calldata validatorFields ) internal returns (uint256) { - bytes32 pubkeyHash = validatorFields.getPubkeyHash(); - ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorFields.getPubkeyHash()]; // Withdrawal credential proofs should only be processed for "INACTIVE" validators require(validatorInfo.status == VALIDATOR_STATUS.INACTIVE, CredentialsAlreadyVerified()); @@ -473,7 +479,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // Ensure the validator's withdrawal credentials are pointed at this pod require( - validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()) + || validatorFields.getWithdrawalCredentials() == bytes32(_podCompoundingWithdrawalCredentials()), WithdrawalCredentialsNotForEigenPod() ); @@ -484,6 +491,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // Verify passed-in validatorFields against verified beaconStateRoot: BeaconChainProofs.verifyValidatorFields({ + proofTimestamp: beaconTimestamp, + pectraForkTimestamp: getPectraForkTimestamp(), beaconStateRoot: beaconStateRoot, validatorFields: validatorFields, validatorFieldsProof: validatorFieldsProof, @@ -499,7 +508,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; // Proofs complete - create the validator in state - _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + _validatorPubkeyHashToInfo[validatorFields.getPubkeyHash()] = ValidatorInfo({ validatorIndex: validatorIndex, restakedBalanceGwei: restakedBalanceGwei, lastCheckpointedAt: lastCheckpointedAt, @@ -665,6 +674,10 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } + function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this)); + } + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec function _calculateValidatorPubkeyHash( bytes memory validatorPubkey @@ -731,4 +744,10 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC require(success && result.length > 0, InvalidEIP4788Response()); return abi.decode(result, (bytes32)); } + + /// @notice Returns the timestamp of the Pectra fork, read from the `EigenPodManager` contract + /// @dev Specifically, this returns the timestamp of the first non-missed slot at or after the Pectra hard fork + function getPectraForkTimestamp() public view returns (uint64) { + return eigenPodManager.pectraForkTimestamp(); + } } diff --git a/src/contracts/pods/EigenPodManager.sol b/src/contracts/pods/EigenPodManager.sol index 697b8d0f7..43d07cc1c 100644 --- a/src/contracts/pods/EigenPodManager.sol +++ b/src/contracts/pods/EigenPodManager.sol @@ -44,6 +44,11 @@ contract EigenPodManager is _; } + modifier onlyProofTimestampSetter() { + require(msg.sender == proofTimestampSetter, OnlyProofTimestampSetter()); + _; + } + constructor( IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, @@ -223,6 +228,22 @@ contract EigenPodManager is emit BurnableETHSharesIncreased(addedSharesToBurn); } + /// @notice Sets the address that can set proof timestamps + function setProofTimestampSetter( + address newProofTimestampSetter + ) external onlyOwner { + proofTimestampSetter = newProofTimestampSetter; + emit ProofTimestampSetterSet(newProofTimestampSetter); + } + + /// @notice Sets the pectra fork timestamp + function setPectraForkTimestamp( + uint64 timestamp + ) external onlyProofTimestampSetter { + pectraForkTimestamp = timestamp; + emit PectraForkTimestampSet(timestamp); + } + // INTERNAL FUNCTIONS function _deployPod() internal returns (IEigenPod) { diff --git a/src/contracts/pods/EigenPodManagerStorage.sol b/src/contracts/pods/EigenPodManagerStorage.sol index acc676fe4..edfaeb972 100644 --- a/src/contracts/pods/EigenPodManagerStorage.sol +++ b/src/contracts/pods/EigenPodManagerStorage.sol @@ -91,6 +91,12 @@ abstract contract EigenPodManagerStorage is IEigenPodManager { /// @notice Returns the amount of `shares` that have been slashed on EigenLayer but not burned yet. uint256 public burnableETHShares; + /// @notice The address that can set proof timestamps + address public proofTimestampSetter; + + /// @notice The timestamp of the Pectra proof + uint64 public pectraForkTimestamp; + constructor(IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, IDelegationManager _delegationManager) { ethPOS = _ethPOS; eigenPodBeacon = _eigenPodBeacon; @@ -102,5 +108,5 @@ abstract contract EigenPodManagerStorage is IEigenPodManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[42] private __gap; + uint256[41] private __gap; } diff --git a/src/test/harnesses/EigenPodHarness.sol b/src/test/harnesses/EigenPodHarness.sol index 0be5e4a54..f6678d3a7 100644 --- a/src/test/harnesses/EigenPodHarness.sol +++ b/src/test/harnesses/EigenPodHarness.sol @@ -25,12 +25,14 @@ contract EigenPodHarness is EigenPod { } function verifyWithdrawalCredentials( + uint64 beaconTimestamp, bytes32 beaconStateRoot, uint40 validatorIndex, bytes calldata validatorFieldsProof, bytes32[] calldata validatorFields ) public returns (uint256) { return _verifyWithdrawalCredentials( + beaconTimestamp, beaconStateRoot, validatorIndex, validatorFieldsProof, diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index 5f4fdfcc4..8274a5d1f 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -39,6 +39,14 @@ struct StaleBalanceProofs { BeaconChainProofs.ValidatorProof validatorProof; } +/// @notice A Pectra Beacon Chain Mock Contract. For testing upgrades, use BeaconChainMock_Upgradeable +/// @notice This mock assumed the following +/** + * @notice A Semi-Compatible Pectra Beacon Chain Mock Contract. For Testing Upgrades to Pectra use BeaconChainMock_Upgradeable + * @dev This mock assumes the following: + * - Ceiling is 64 ETH, at which sweeps will be triggered + * - No support for consolidations or any execution layer triggerable actions (exits, partial withdrawals) + */ contract BeaconChainMock is Logger { using StdStyle for *; using print for *; @@ -61,18 +69,19 @@ contract BeaconChainMock is Logger { uint64 public constant SLASH_AMOUNT_GWEI = 10; /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): + /// @dev Non-constant values will change with the Pectra hard fork - // see https://eth2book.info/capella/part3/containers/state/#beaconstate - uint constant BEACON_STATE_FIELDS = 32; + // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate + uint BEACON_STATE_FIELDS = 37; // see https://eth2book.info/capella/part3/containers/blocks/#beaconblock uint constant BEACON_BLOCK_FIELDS = 5; uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; - uint immutable VAL_FIELDS_PROOF_LEN = 32 * ( - (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.BEACON_STATE_TREE_HEIGHT + uint VAL_FIELDS_PROOF_LEN = 32 * ( + (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT ); - uint immutable BALANCE_CONTAINER_PROOF_LEN = 32 * ( - BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.BEACON_STATE_TREE_HEIGHT + uint BALANCE_CONTAINER_PROOF_LEN = 32 * ( + BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT ); uint immutable BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); @@ -142,7 +151,7 @@ contract BeaconChainMock is Logger { } } - function NAME() public pure override returns (string memory) { + function NAME() public pure virtual override returns (string memory) { return "BeaconChain"; } @@ -245,7 +254,7 @@ contract BeaconChainMock is Logger { /// @dev Move forward one epoch on the beacon chain, taking care of important epoch processing: /// - Award ALL validators CONSENSUS_REWARD_AMOUNT - /// - Withdraw any balance over 32 ETH + /// - Withdraw any balance over 64 ETH /// - Withdraw any balance for exited validators /// - Effective balances updated (NOTE: we do not use hysteresis!) /// - Move time forward one epoch @@ -254,7 +263,7 @@ contract BeaconChainMock is Logger { /// /// Note: /// - DOES generate consensus rewards for ALL non-exited validators - /// - DOES withdraw in excess of 32 ETH / if validator is exited + /// - DOES withdraw in excess of 64 ETH / if validator is exited function advanceEpoch() public { print.method("advanceEpoch"); _generateRewards(); @@ -268,7 +277,7 @@ contract BeaconChainMock is Logger { /// /// Note: /// - does NOT generate consensus rewards - /// - DOES withdraw in excess of 32 ETH / if validator is exited + /// - DOES withdraw in excess of 64 ETH / if validator is exited function advanceEpoch_NoRewards() public { print.method("advanceEpoch_NoRewards"); _withdrawExcess(); @@ -276,12 +285,12 @@ contract BeaconChainMock is Logger { } /// @dev Like `advanceEpoch`, but explicitly does NOT withdraw if balances - /// are over 32 ETH. This exists to support tests that check share increases solely + /// are over 64 ETH. This exists to support tests that check share increases solely /// due to beacon chain balance changes. /// /// Note: /// - DOES generate consensus rewards for ALL non-exited validators - /// - does NOT withdraw in excess of 32 ETH + /// - does NOT withdraw in excess of 64 ETH /// - does NOT withdraw if validator is exited function advanceEpoch_NoWithdraw() public { print.method("advanceEpoch_NoWithdraw"); @@ -310,7 +319,7 @@ contract BeaconChainMock is Logger { console.log(" - Generated rewards for %s of %s validators.", totalRewarded, validators.length); } - /// @dev Iterate over all validators. If the validator has > 32 ETH current balance + /// @dev Iterate over all validators. If the validator has > 64 ETH current balance /// OR is exited, withdraw the excess to the validator's withdrawal address. function _withdrawExcess() internal { uint totalExcessWei; @@ -325,15 +334,15 @@ contract BeaconChainMock is Logger { // If the validator has exited, withdraw any existing balance // - // If the validator has > 32 ether, withdraw anything over that + // If the validator has > 64 ether, withdraw anything over that if (v.exitEpoch != BeaconChainProofs.FAR_FUTURE_EPOCH) { if (balanceWei == 0) continue; excessBalanceWei = balanceWei; newBalanceGwei = 0; - } else if (balanceWei > 32 ether) { - excessBalanceWei = balanceWei - 32 ether; - newBalanceGwei = 32 gwei; + } else if (balanceWei > 64 ether) { + excessBalanceWei = balanceWei - 64 ether; + newBalanceGwei = 64 gwei; } // Send ETH to withdrawal address @@ -348,7 +357,7 @@ contract BeaconChainMock is Logger { console.log("- Withdrew excess balance:", totalExcessWei.asGwei()); } - function _advanceEpoch() public { + function _advanceEpoch() public virtual { cheats.pauseTracing(); // Update effective balances for each validator @@ -356,10 +365,10 @@ contract BeaconChainMock is Logger { Validator storage v = validators[i]; if (v.isDummy) continue; // don't process dummy validators - // Get current balance and trim anything over 32 ether + // Get current balance and trim anything over 64 ether uint64 balanceGwei = _currentBalanceGwei(uint40(i)); - if (balanceGwei > 32 gwei) { - balanceGwei = 32 gwei; + if (balanceGwei > 64 gwei) { + balanceGwei = 64 gwei; } v.effectiveBalanceGwei = balanceGwei; @@ -409,7 +418,7 @@ contract BeaconChainMock is Logger { // Build merkle tree for BeaconState bytes32 beaconStateRoot = _buildMerkleTree({ leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), - treeHeight: BeaconChainProofs.BEACON_STATE_TREE_HEIGHT, + treeHeight: BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT, tree: trees[curTimestamp].stateTree }); // console.log("-- beacon state root", beaconStateRoot); @@ -589,13 +598,13 @@ contract BeaconChainMock is Logger { }); } - function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal { + function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal virtual { bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); bytes32 curNode = balanceContainerRoot; uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; uint depth = 0; - for (uint i = 0; i < BeaconChainProofs.BEACON_STATE_TREE_HEIGHT; i++) { + for (uint i = 0; i < BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; i++) { bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; // proof[j] = sibling; @@ -631,7 +640,7 @@ contract BeaconChainMock is Logger { }); } - function _genCredentialProofs() internal { + function _genCredentialProofs() internal virtual { mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp]; // Calculate credential proofs for each validator @@ -661,7 +670,7 @@ contract BeaconChainMock is Logger { // Validator container root -> beacon state root for ( uint j = depth; - j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.BEACON_STATE_TREE_HEIGHT; + j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; j++ ) { bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; @@ -746,7 +755,7 @@ contract BeaconChainMock is Logger { 0 : ((validators.length - 1) / 4) + 1; } - function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal pure returns (bytes32[] memory) { + function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal virtual view returns (bytes32[] memory) { bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS); // Pre-populate leaves with dummy values so sibling/parent tracking is correct @@ -1008,4 +1017,4 @@ contract BeaconChainMock is Logger { function isActive(uint40 validatorIndex) public view returns (bool) { return validators[validatorIndex].exitEpoch == BeaconChainProofs.FAR_FUTURE_EPOCH; } -} +} \ No newline at end of file diff --git a/src/test/integration/mocks/BeaconChainMock_Pectra.t.sol b/src/test/integration/mocks/BeaconChainMock_Pectra.t.sol new file mode 100644 index 000000000..f6ed10e13 --- /dev/null +++ b/src/test/integration/mocks/BeaconChainMock_Pectra.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/mocks/BeaconChainMock.t.sol"; + +/// @notice A backwards-compatible BeaconChain Mock that updates containers & proofs for the Pectra upgrade +contract BeaconChainMock_PectraForkable is BeaconChainMock { + using StdStyle for *; + using print for *; + + // Denotes whether the beacon chain has been forked to Pectra + bool isPectra; + + // The timestamp of the Pectra hard fork + uint64 public pectraForkTimestamp; + + constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) BeaconChainMock(_eigenPodManager, _genesisTime) { + /// DENEB SPECIFIC CONSTANTS (PROOF LENGTHS, FIELD SIZES): + // see https://eth2book.info/capella/part3/containers/state/#beaconstate + BEACON_STATE_FIELDS = 32; + + VAL_FIELDS_PROOF_LEN = 32 * ( + (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT + ); + + BALANCE_CONTAINER_PROOF_LEN = 32 * ( + BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT + ); + } + + function NAME() public pure override returns (string memory) { + return "BeaconChain_PectraForkable"; + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + function _advanceEpoch() public override { + cheats.pauseTracing(); + + // Update effective balances for each validator + for (uint i = 0; i < validators.length; i++) { + Validator storage v = validators[i]; + if (v.isDummy) continue; // don't process dummy validators + + // Get current balance and trim anything over 64 ether + uint64 balanceGwei = _currentBalanceGwei(uint40(i)); + if (balanceGwei > 64 gwei) { + balanceGwei = 64 gwei; + } + + v.effectiveBalanceGwei = balanceGwei; + } + + // console.log(" Updated effective balances...".dim()); + // console.log(" timestamp:", block.timestamp); + // console.log(" epoch:", currentEpoch()); + + uint64 curEpoch = currentEpoch(); + cheats.warp(_nextEpochStartTimestamp(curEpoch)); + curTimestamp = uint64(block.timestamp); + + // console.log(" Jumping to next epoch...".dim()); + // console.log(" timestamp:", block.timestamp); + // console.log(" epoch:", currentEpoch()); + + // console.log(" Building beacon state trees...".dim()); + + // Log total number of validators and number being processed for the first time + if (validators.length > 0) { + lastIndexProcessed = validators.length - 1; + } else { + // generate an empty root if we don't have any validators + EIP_4788_ORACLE.setBlockRoot(curTimestamp, keccak256("")); + + // console.log("-- no validators; added empty block root"); + return; + } + + // Build merkle tree for validators + bytes32 validatorsRoot = _buildMerkleTree({ + leaves: _getValidatorLeaves(), + treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1, + tree: trees[curTimestamp].validatorTree + }); + // console.log("-- validator container root", validatorsRoot); + + // Build merkle tree for current balances + bytes32 balanceContainerRoot = _buildMerkleTree({ + leaves: _getBalanceLeaves(), + treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1, + tree: trees[curTimestamp].balancesTree + }); + // console.log("-- balances container root", balanceContainerRoot); + + // Build merkle tree for BeaconState + bytes32 beaconStateRoot = _buildMerkleTree({ + leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), + treeHeight: getBeaconStateTreeHeight(), + tree: trees[curTimestamp].stateTree + }); + // console.log("-- beacon state root", beaconStateRoot); + + // Build merkle tree for BeaconBlock + bytes32 beaconBlockRoot = _buildMerkleTree({ + leaves: _getBeaconBlockLeaves(beaconStateRoot), + treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT, + tree: trees[curTimestamp].blockTree + }); + + + // console.log("-- beacon block root", cheats.toString(beaconBlockRoot)); + + // Push new block root to oracle + EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot); + + // Pre-generate proofs to pass to EigenPod methods + _genStateRootProof(beaconStateRoot); + _genBalanceContainerProof(balanceContainerRoot); + _genCredentialProofs(); + _genBalanceProofs(); + + cheats.resumeTracing(); + } + + function _genCredentialProofs() internal override { + mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp]; + + // Calculate credential proofs for each validator + for (uint i = 0; i < validators.length; i++) { + + bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); + bytes32[] memory validatorFields = _getValidatorFields(uint40(i)); + bytes32 curNode = Merkle.merkleizeSha256(validatorFields); + + // Validator fields leaf -> validator container root + uint depth = 0; + for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { + bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore( + add(proof, add(32, mul(32, j))), + sibling + ) + } + + curNode = trees[curTimestamp].validatorTree.parents[curNode]; + depth++; + } + + // Validator container root -> beacon state root + for ( + uint j = depth; + j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + getBeaconStateTreeHeight(); + j++ + ) { + bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore( + add(proof, add(32, mul(32, j))), + sibling + ) + } + + curNode = trees[curTimestamp].stateTree.parents[curNode]; + depth++; + } + + vfProofs[uint40(i)].validatorFields = validatorFields; + vfProofs[uint40(i)].validatorFieldsProof = proof; + } + } + + function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal override { + bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); + bytes32 curNode = balanceContainerRoot; + + uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; + uint depth = 0; + for (uint i = 0; i < getBeaconStateTreeHeight(); i++) { + bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore( + add(proof, add(32, mul(32, i))), + sibling + ) + } + + curNode = trees[curTimestamp].stateTree.parents[curNode]; + depth++; + } + + for (uint i = depth; i < totalHeight; i++) { + bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore( + add(proof, add(32, mul(32, i))), + sibling + ) + } + + curNode = trees[curTimestamp].blockTree.parents[curNode]; + depth++; + } + + balanceContainerProofs[curTimestamp] = BeaconChainProofs.BalanceContainerProof({ + balanceContainerRoot: balanceContainerRoot, + proof: proof + }); + } + + function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal override view returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS); + + // Pre-populate leaves with dummy values so sibling/parent tracking is correct + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = bytes32(i + 1); + } + + // Place validatorsRoot and balancesRoot into tree + leaves[BeaconChainProofs.VALIDATOR_CONTAINER_INDEX] = validatorsRoot; + leaves[BeaconChainProofs.BALANCE_CONTAINER_INDEX] = balancesRoot; + return leaves; + } + + /// @notice Forks the beacon chain to Pectra + /// @dev Test battery should warp to the fork timestamp after calling this method + function forkToPectra(uint64 _pectraForkTimestamp) public { + // https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate + BEACON_STATE_FIELDS = 37; + + VAL_FIELDS_PROOF_LEN = 32 * ( + (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT + ); + BALANCE_CONTAINER_PROOF_LEN = 32 * ( + BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT + ); + + isPectra = true; + + cheats.warp(_pectraForkTimestamp); + pectraForkTimestamp = _pectraForkTimestamp; + + } + + function getBeaconStateTreeHeight() public view returns (uint) { + return isPectra ? BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT : BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT; + } +} \ No newline at end of file diff --git a/src/test/integration/tests/upgrade/Prooftra.t.sol b/src/test/integration/tests/upgrade/Prooftra.t.sol new file mode 100644 index 000000000..72a600eb0 --- /dev/null +++ b/src/test/integration/tests/upgrade/Prooftra.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/UpgradeTest.t.sol"; +import "src/test/integration/mocks/BeaconChainMock_Pectra.t.sol"; +contract Integration_Upgrade_Pectra is UpgradeTest, EigenPodPausingConstants { + + // Using Holeksy's Fork Timestamp for Testing Purposes + uint64 constant PECTRA_FORK_TIMESTAMP = 1739352768; + + function _init() internal override { + _configAssetTypes(HOLDS_ETH); + _configUserTypes(DEFAULT); + + // Set beacon chain mock + beaconChain = BeaconChainMock( + new BeaconChainMock_PectraForkable(eigenPodManager, BEACON_GENESIS_TIME) + ); + } + + function test_Upgrade_VerifyWC_StartCP_CompleteCP(uint24 _rand) public rand(_rand) { + // 1. Pause, Fork, and Upgrade + _pauseForkAndUpgrade(PECTRA_FORK_TIMESTAMP); + + // 2. Set Pectra Fork Timestamp & unpause + _setTimestampAndUnpause(); + + // 3. Initialize Staker + (User staker, ,) = _newRandomStaker(); + (uint40[] memory validators, uint64 beaconBalanceGwei) = staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + + // 4. Verify Withdrawal Credentials + staker.verifyWithdrawalCredentials(validators); + check_VerifyWC_State(staker, validators, beaconBalanceGwei); + + // 4. Start Checkpoint + staker.startCheckpoint(); + check_StartCheckpoint_State(staker); + + // 5. Complete Checkpoint + staker.completeCheckpoint(); + check_CompleteCheckpoint_State(staker); + } + + function test_VerifyWC_StartCP_Fork_CompleteCP(uint24 _rand) public rand(_rand) { + // Initialize state + (User staker, ,) = _newRandomStaker(); + (uint40[] memory validators, ) = staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + + // 1. Verify validators' withdrawal credentials + staker.verifyWithdrawalCredentials(validators); + + // 2. Start a checkpoint + staker.startCheckpoint(); + + // 3. Pause, Fork, and Upgrade + _pauseForkAndUpgrade(PECTRA_FORK_TIMESTAMP); + + // 4. Set Pectra Fork Timestamp & unpause + _setTimestampAndUnpause(); + + // 5. Complete in progress checkpoint + staker.completeCheckpoint(); + check_CompleteCheckpoint_State(staker); + } + + function test_VerifyWC_Fork_EarnToPod_StartCP_CompleteCP(uint24 _rand) public rand(_rand) { + // Initialize state + (User staker, ,) = _newRandomStaker(); + (uint40[] memory validators, ) = staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + + // 1. Verify validators' withdrawal credentials + staker.verifyWithdrawalCredentials(validators); + + // 2. Fork to Pectra + BeaconChainMock_PectraForkable(address(beaconChain)).forkToPectra(PECTRA_FORK_TIMESTAMP); + + // 3. Upgrade EigenPodManager & EigenPod + _upgradeEigenLayerContracts(); + + // 4. Advance epoch, generating consensus rewards and withdrawing anything over 64 ETH + beaconChain.advanceEpoch(); + uint64 expectedWithdrawnGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); + + // 5. Start a checkpoint + staker.startCheckpoint(); + check_StartCheckpoint_WithPodBalance_State(staker, expectedWithdrawnGwei); + + // 6. Complete in progress checkpoint + staker.completeCheckpoint(); + check_CompleteCheckpoint_WithPodBalance_State(staker, expectedWithdrawnGwei); + } + + function _pauseForkAndUpgrade(uint64 pectraForkTimestamp) internal { + // 1. Pause starting checkpoint, completing, and credential proofs + cheats.prank(pauserMultisig); + eigenPodManager.pause( + 2 ** PAUSED_START_CHECKPOINT | + 2 ** PAUSED_EIGENPODS_VERIFY_CREDENTIALS + ); + + // 2. Fork to Pectra + BeaconChainMock_PectraForkable(address(beaconChain)).forkToPectra(pectraForkTimestamp); + + // 3. Upgrade EigenPodManager & EigenPod + _upgradeEigenLayerContracts(); + + // 4. Set proof timestamp setter to operations multisig + cheats.prank(eigenPodManager.owner()); + eigenPodManager.setProofTimestampSetter(address(operationsMultisig)); + } + + function _setTimestampAndUnpause() internal { + // 1. Set Timestamp + cheats.startPrank(eigenPodManager.proofTimestampSetter()); + eigenPodManager.setPectraForkTimestamp( + BeaconChainMock_PectraForkable(address(beaconChain)).pectraForkTimestamp() + ); + cheats.stopPrank(); + + // 2. Randomly warp to just after the fork timestamp + // If we do not warp, proofs will be against deneb state + if (_randBool()) { + // If we warp, proofs will be against electra state + cheats.warp(block.timestamp + 1); + } + + // 3. Unpause + cheats.prank(eigenLayerPauserReg.unpauser()); + eigenPodManager.unpause(0); + } +} \ No newline at end of file diff --git a/src/test/mocks/EigenPodManagerMock.sol b/src/test/mocks/EigenPodManagerMock.sol index ecf602870..855aaa1bc 100644 --- a/src/test/mocks/EigenPodManagerMock.sol +++ b/src/test/mocks/EigenPodManagerMock.sol @@ -74,4 +74,8 @@ contract EigenPodManagerMock is Test, Pausable { BeaconChainSlashingFactor memory bsf = _beaconChainSlashingFactor[staker]; return bsf.isSet ? bsf.slashingFactor : WAD; } + + function pectraForkTimestamp() external pure returns (uint64) { + return 0; + } } \ No newline at end of file diff --git a/src/test/mocks/EigenPodMock.sol b/src/test/mocks/EigenPodMock.sol index d1e12199f..e5f62f5b4 100644 --- a/src/test/mocks/EigenPodMock.sol +++ b/src/test/mocks/EigenPodMock.sol @@ -102,4 +102,6 @@ contract EigenPodMock is IEigenPod, Test { /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method /// will revert. function getParentBlockRoot(uint64 timestamp) external view returns (bytes32) {} + + function getPectraForkTimestamp() external view returns (uint64) {} } \ No newline at end of file diff --git a/src/test/unit/EigenPodManagerUnit.t.sol b/src/test/unit/EigenPodManagerUnit.t.sol index f1286f031..1972ceb2c 100644 --- a/src/test/unit/EigenPodManagerUnit.t.sol +++ b/src/test/unit/EigenPodManagerUnit.t.sol @@ -151,6 +151,57 @@ contract EigenPodManagerUnitTests_CreationTests is EigenPodManagerUnitTests { } } +contract EigenPodManagerUnitTests_ProofTimestampSetterTests is EigenPodManagerUnitTests { + + function testFuzz_setProofTimestampSetter_revert_notOwner(address notOwner) public filterFuzzedAddressInputs(notOwner) { + cheats.assume(notOwner != initialOwner); + cheats.prank(notOwner); + cheats.expectRevert("Ownable: caller is not the owner"); + eigenPodManager.setProofTimestampSetter(address(1)); + } + + function test_setProofTimestampSetter() public { + address newSetter = address(1); + cheats.expectEmit(true, true, true, true); + emit ProofTimestampSetterSet(newSetter); + + cheats.prank(initialOwner); + eigenPodManager.setProofTimestampSetter(newSetter); + + assertEq(eigenPodManager.proofTimestampSetter(), newSetter, "Proof timestamp setter not set correctly"); + } + + function test_setPectraForkTimestamp_revert_notSetter(address notSetter) public filterFuzzedAddressInputs(notSetter) { + // First set a proof timestamp setter + address setter = address(1); + cheats.prank(initialOwner); + eigenPodManager.setProofTimestampSetter(setter); + + // Try to set timestamp from non-setter address + cheats.assume(notSetter != setter); + cheats.prank(notSetter); + cheats.expectRevert(IEigenPodManagerErrors.OnlyProofTimestampSetter.selector); + eigenPodManager.setPectraForkTimestamp(1); + } + + function test_setPectraForkTimestamp() public { + // First set a proof timestamp setter + address setter = address(1); + cheats.prank(initialOwner); + eigenPodManager.setProofTimestampSetter(setter); + + // Set new timestamp + uint64 newTimestamp = 1; + cheats.expectEmit(true, true, true, true); + emit PectraForkTimestampSet(newTimestamp); + + cheats.prank(setter); + eigenPodManager.setPectraForkTimestamp(newTimestamp); + + assertEq(eigenPodManager.pectraForkTimestamp(), newTimestamp, "Pectra fork timestamp not set correctly"); + } +} + contract EigenPodManagerUnitTests_StakeTests is EigenPodManagerUnitTests { function test_stake_podAlreadyDeployed() deployPodForStaker(defaultStaker) public { diff --git a/src/test/unit/EigenPodUnit.t.sol b/src/test/unit/EigenPodUnit.t.sol index 002ff9419..735bd4cc9 100644 --- a/src/test/unit/EigenPodUnit.t.sol +++ b/src/test/unit/EigenPodUnit.t.sol @@ -675,6 +675,7 @@ contract EigenPodUnitTests_verifyWithdrawalCredentials is EigenPodUnitTests, Pro /// @notice beaconTimestamp must be after the current checkpoint function testFuzz_revert_beaconTimestampInvalid(uint256 rand) public { + rand = 10; cheats.warp(10 days); (EigenPodUser staker,) = _newEigenPodStaker({ rand: rand }); // Ensure we have more than one validator (_newEigenPodStaker allocates a nonzero amt of eth) @@ -1783,6 +1784,7 @@ contract EigenPodUnitTests_proofParsingTests is EigenPodHarnessSetup, ProofParsi oracleTimestamp = uint64(block.timestamp); eigenPodHarness.verifyWithdrawalCredentials( + oracleTimestamp, beaconStateRoot, validatorIndex, validatorFieldsProof, diff --git a/src/test/utils/EigenPodUser.t.sol b/src/test/utils/EigenPodUser.t.sol index d0f2e80f1..5d1e92905 100644 --- a/src/test/utils/EigenPodUser.t.sol +++ b/src/test/utils/EigenPodUser.t.sol @@ -118,6 +118,7 @@ contract EigenPodUser is Logger { *******************************************************************************/ /// @dev Uses any ETH held by the User to start validators on the beacon chain + /// @dev Creates validators with either: 32 or 64 ETH /// @return A list of created validator indices /// @return The amount of wei sent to the beacon chain /// Note: If the user does not have enough ETH to start a validator, this method reverts @@ -125,49 +126,69 @@ contract EigenPodUser is Logger { /// withdrawal credential proofs are generated for each validator. function _startValidators() internal returns (uint40[] memory, uint) { uint balanceWei = address(this).balance; + uint numValidators = 0; + + // Get maximum possible number of validators. Add 1 to account for a validator with < 32 ETH + uint maxValidators = balanceWei / 32 ether; + uint40[] memory newValidators = new uint40[](maxValidators + 1); + + // Create validators with 32 or 64 ETH until we can't create more + while (balanceWei >= 32 ether) { + uint validatorEth = uint(keccak256(abi.encodePacked( + block.timestamp, + balanceWei, + numValidators + ))) % 2 == 0 ? 32 ether : 64 ether; + + // If we don't have enough ETH for 64, use 32 + if (balanceWei < 64 ether) { + validatorEth = 32 ether; + } + + // Create the validator + bytes memory withdrawalCredentials = validatorEth == 32 ether ? + _podWithdrawalCredentials() : _podCompoundingWithdrawalCredentials(); + + uint40 validatorIndex = beaconChain.newValidator{ + value: validatorEth + }(withdrawalCredentials); + - // Number of full validators: balance / 32 ETH - uint numValidators = balanceWei / 32 ether; - balanceWei -= (numValidators * 32 ether); + newValidators[numValidators] = validatorIndex; + validators.push(validatorIndex); + + balanceWei -= validatorEth; + numValidators++; + } // If we still have at least 1 ETH left over, we can create another (non-full) validator // Note that in the mock beacon chain this validator will generate rewards like any other. // The main point is to ensure pods are able to handle validators that have less than 32 ETH - uint lastValidatorBalance; - uint totalValidators = numValidators; if (balanceWei >= 1 ether) { - lastValidatorBalance = balanceWei - (balanceWei % 1 gwei); + uint lastValidatorBalance = balanceWei - (balanceWei % 1 gwei); balanceWei -= lastValidatorBalance; - totalValidators++; - } - - require(totalValidators != 0, "startValidators: not enough ETH to start a validator"); - uint40[] memory newValidators = new uint40[](totalValidators); - uint totalBeaconBalance = address(this).balance - balanceWei; - console.log("- creating new validators", newValidators.length); - console.log("- depositing balance to beacon chain (wei)", totalBeaconBalance); - - // Create each of the full validators - for (uint i = 0; i < numValidators; i++) { uint40 validatorIndex = beaconChain.newValidator{ - value: 32 ether + value: lastValidatorBalance }(_podWithdrawalCredentials()); - newValidators[i] = validatorIndex; + newValidators[numValidators] = validatorIndex; validators.push(validatorIndex); - } - // If we had a remainder, create the final, non-full validator - if (totalValidators == numValidators + 1) { - uint40 validatorIndex = beaconChain.newValidator{ - value: lastValidatorBalance - }(_podWithdrawalCredentials()); + numValidators++; + } - newValidators[newValidators.length - 1] = validatorIndex; - validators.push(validatorIndex); + // Resize the array to actual number of validators created + assembly { + mstore(newValidators, numValidators) } + require(numValidators != 0, "startValidators: not enough ETH to start a validator"); + uint totalBeaconBalance = address(this).balance - balanceWei; + + console.log("- created new validators", newValidators.length); + console.log("- deposited balance to beacon chain (wei)", totalBeaconBalance); + // Advance forward one epoch and generate withdrawal and balance proofs for each validator beaconChain.advanceEpoch_NoRewards(); @@ -224,6 +245,10 @@ contract EigenPodUser is Logger { return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(pod)); } + function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(pod)); + } + function getActiveValidators() public view returns (uint40[] memory) { uint40[] memory activeValidators = new uint40[](validators.length);