Skip to content

Commit 5e227b1

Browse files
committed
feat: pectra compatibility (#1053)
Updates checkpoint proof system to be Pectra compatible. The breaking change to EigenPods is the `BeaconState` container increasing to have 37 fields, which results in the tree height to be > 5. We need to solve for the following cases: - Prevent deneb proofs from being submitted to pectra blocks - Ensure that the PECTRA_FORK_TIMESTAMP is the first timestamp at or after the pectra hard fork for which there is a non missed slot To do this, here is the upgrade process: 1. Pause checkpoint starting & credential proofs 2. Upgrade after fork is hit 3. Run script to detect the first timestamp at or after the pectra hard fork for which there is a non missed slot 4. Set pectra fork timestamp to the first timestamp at which there is a pectra block header 5. Unpause - Updated balance container and validator container proofs to pass in a proof timestamp & pectra fork timestamp to check against which tree height to use for the beacon state - Modify storing variables in memory to handle stack too deep errors - Note that since the 4788 oracle returns the PARENT beacon block root, our check against the pectra fork timestamp returns the previous tree length for proofs that are <= `pectraForkTimestamp` - Post pectra, we can upgrade the EigenPod to deprecate the fork timestamp case handling once all in progress pre-Pectra checkpoints have been completed - [x] Unit Tests - [x] Integration Tests simulating upgrade - [x] Mekong Deployment - [x] Update Integration Test User to use validators >32 ETH
1 parent 585bfb4 commit 5e227b1

27 files changed

+1152
-246
lines changed

src/contracts/interfaces/IEigenPod.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ interface IEigenPodErrors {
6767
error MsgValueNot32ETH();
6868
/// @dev Thrown when provided `beaconTimestamp` is too far in the past.
6969
error BeaconTimestampTooFarInPast();
70+
/// @dev Thrown when the pectraForkTimestamp returned from the EigenPodManager is zero
71+
error ForkTimestampZero();
7072
}
7173

7274
interface IEigenPodTypes {
@@ -146,6 +148,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin {
146148
) external;
147149

148150
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
151+
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
149152
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
150153

151154
/**

src/contracts/interfaces/IEigenPodManager.sol

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ interface IEigenPodManagerErrors {
2626
/// @dev Thrown when the pods shares are negative and a beacon chain balance update is attempted.
2727
/// The podOwner should complete legacy withdrawal first.
2828
error LegacyWithdrawalsNotCompleted();
29+
/// @dev Thrown when caller is not the proof timestamp setter
30+
error OnlyProofTimestampSetter();
2931
}
3032

3133
interface IEigenPodManagerEvents {
@@ -58,6 +60,12 @@ interface IEigenPodManagerEvents {
5860

5961
/// @notice Emitted when an operator is slashed and shares to be burned are increased
6062
event BurnableETHSharesIncreased(uint256 shares);
63+
64+
/// @notice Emitted when the Pectra fork timestamp is updated
65+
event PectraForkTimestampSet(uint64 newPectraForkTimestamp);
66+
67+
/// @notice Emitted when the proof timestamp setter is updated
68+
event ProofTimestampSetterSet(address newProofTimestampSetter);
6169
}
6270

6371
interface IEigenPodManagerTypes {
@@ -122,6 +130,16 @@ interface IEigenPodManager is
122130
int256 balanceDeltaWei
123131
) external;
124132

133+
/// @notice Sets the address that can set proof timestamps
134+
function setProofTimestampSetter(
135+
address newProofTimestampSetter
136+
) external;
137+
138+
/// @notice Sets the Pectra fork timestamp, only callable by `proofTimestampSetter`
139+
function setPectraForkTimestamp(
140+
uint64 timestamp
141+
) external;
142+
125143
/// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed.
126144
function ownerToPod(
127145
address podOwner
@@ -171,4 +189,8 @@ interface IEigenPodManager is
171189

172190
/// @notice Returns the accumulated amount of beacon chain ETH Strategy shares
173191
function burnableETHShares() external view returns (uint256);
192+
193+
/// @notice Returns the timestamp of the Pectra hard fork
194+
/// @dev Specifically, this returns the timestamp of the first non-missed slot at or after the Pectra hard fork
195+
function pectraForkTimestamp() external view returns (uint64);
174196
}

src/contracts/libraries/BeaconChainProofs.sol

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ library BeaconChainProofs {
2828
/// | HEIGHT: VALIDATOR_TREE_HEIGHT
2929
/// individual validators
3030
uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3;
31-
uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5;
31+
uint256 internal constant DENEB_BEACON_STATE_TREE_HEIGHT = 5;
32+
uint256 internal constant PECTRA_BEACON_STATE_TREE_HEIGHT = 6;
3233
uint256 internal constant BALANCE_TREE_HEIGHT = 38;
3334
uint256 internal constant VALIDATOR_TREE_HEIGHT = 40;
3435

@@ -71,6 +72,12 @@ library BeaconChainProofs {
7172
uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max;
7273
bytes8 internal constant UINT64_MASK = 0xffffffffffffffff;
7374

75+
/// @notice The beacon chain version to validate against
76+
enum ProofVersion {
77+
DENEB,
78+
PECTRA
79+
}
80+
7481
/// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root
7582
struct StateRootProof {
7683
bytes32 beaconStateRoot;
@@ -134,17 +141,20 @@ library BeaconChainProofs {
134141
/// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot`
135142
/// @param validatorIndex the validator's unique index
136143
function verifyValidatorFields(
144+
ProofVersion proofVersion,
137145
bytes32 beaconStateRoot,
138146
bytes32[] calldata validatorFields,
139147
bytes calldata validatorFieldsProof,
140148
uint40 validatorIndex
141149
) internal view {
142150
require(validatorFields.length == VALIDATOR_FIELDS_LENGTH, InvalidValidatorFieldsLength());
143151

152+
uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofVersion);
153+
144154
/// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for
145155
/// this container includes hashing the root of the validator tree with the length of the validator list
146156
require(
147-
validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT),
157+
validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + beaconStateTreeHeight),
148158
InvalidProofLength()
149159
);
150160

@@ -185,10 +195,15 @@ library BeaconChainProofs {
185195
/// against the same balance container root.
186196
/// @param beaconBlockRoot merkle root of the beacon block
187197
/// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot`
188-
function verifyBalanceContainer(bytes32 beaconBlockRoot, BalanceContainerProof calldata proof) internal view {
198+
function verifyBalanceContainer(
199+
ProofVersion proofVersion,
200+
bytes32 beaconBlockRoot,
201+
BalanceContainerProof calldata proof
202+
) internal view {
203+
uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofVersion);
204+
189205
require(
190-
proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT),
191-
InvalidProofLength()
206+
proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + beaconStateTreeHeight), InvalidProofLength()
192207
);
193208

194209
/// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees:
@@ -197,7 +212,7 @@ library BeaconChainProofs {
197212
/// -- beaconStateRoot
198213
/// | HEIGHT: BEACON_STATE_TREE_HEIGHT
199214
/// ---- balancesContainerRoot
200-
uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX;
215+
uint256 index = (STATE_ROOT_INDEX << (beaconStateTreeHeight)) | BALANCE_CONTAINER_INDEX;
201216

202217
require(
203218
Merkle.verifyInclusionSha256({
@@ -312,4 +327,12 @@ library BeaconChainProofs {
312327
) internal pure returns (uint64) {
313328
return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]);
314329
}
330+
331+
/// @dev We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
332+
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
333+
function getBeaconStateTreeHeight(
334+
ProofVersion proofVersion
335+
) internal pure returns (uint256) {
336+
return proofVersion == ProofVersion.DENEB ? DENEB_BEACON_STATE_TREE_HEIGHT : PECTRA_BEACON_STATE_TREE_HEIGHT;
337+
}
315338
}

src/contracts/pods/EigenPod.sol

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ contract EigenPod is
172172

173173
// Verify `balanceContainerProof` against `beaconBlockRoot`
174174
BeaconChainProofs.verifyBalanceContainer({
175+
proofVersion: _getProofVersion(checkpointTimestamp),
175176
beaconBlockRoot: checkpoint.beaconBlockRoot,
176177
proof: balanceContainerProof
177178
});
@@ -267,6 +268,7 @@ contract EigenPod is
267268
for (uint256 i = 0; i < validatorIndices.length; i++) {
268269
// forgefmt: disable-next-item
269270
totalAmountToBeRestakedWei += _verifyWithdrawalCredentials(
271+
beaconTimestamp,
270272
stateRootProof.beaconStateRoot,
271273
validatorIndices[i],
272274
validatorFieldsProofs[i],
@@ -354,6 +356,7 @@ contract EigenPod is
354356

355357
// Verify Validator container proof against `beaconStateRoot`
356358
BeaconChainProofs.verifyValidatorFields({
359+
proofVersion: _getProofVersion(beaconTimestamp),
357360
beaconStateRoot: stateRootProof.beaconStateRoot,
358361
validatorFields: proof.validatorFields,
359362
validatorFieldsProof: proof.proof,
@@ -391,6 +394,7 @@ contract EigenPod is
391394
}
392395

393396
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
397+
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
394398
function stake(
395399
bytes calldata pubkey,
396400
bytes calldata signature,
@@ -432,6 +436,7 @@ contract EigenPod is
432436
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
433437
*/
434438
function _verifyWithdrawalCredentials(
439+
uint64 beaconTimestamp,
435440
bytes32 beaconStateRoot,
436441
uint40 validatorIndex,
437442
bytes calldata validatorFieldsProof,
@@ -486,7 +491,8 @@ contract EigenPod is
486491

487492
// Ensure the validator's withdrawal credentials are pointed at this pod
488493
require(
489-
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()),
494+
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials())
495+
|| validatorFields.getWithdrawalCredentials() == bytes32(_podCompoundingWithdrawalCredentials()),
490496
WithdrawalCredentialsNotForEigenPod()
491497
);
492498

@@ -497,6 +503,7 @@ contract EigenPod is
497503

498504
// Verify passed-in validatorFields against verified beaconStateRoot:
499505
BeaconChainProofs.verifyValidatorFields({
506+
proofVersion: _getProofVersion(beaconTimestamp),
500507
beaconStateRoot: beaconStateRoot,
501508
validatorFields: validatorFields,
502509
validatorFieldsProof: validatorFieldsProof,
@@ -678,6 +685,10 @@ contract EigenPod is
678685
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this));
679686
}
680687

688+
function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) {
689+
return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this));
690+
}
691+
681692
///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec
682693
function _calculateValidatorPubkeyHash(
683694
bytes memory validatorPubkey
@@ -744,4 +755,20 @@ contract EigenPod is
744755
require(success && result.length > 0, InvalidEIP4788Response());
745756
return abi.decode(result, (bytes32));
746757
}
758+
759+
/// @notice Returns the PROOF_TYPE depending on the `proofTimestamp` in relation to the fork timestamp.
760+
function _getProofVersion(
761+
uint64 proofTimestamp
762+
) internal view returns (BeaconChainProofs.ProofVersion) {
763+
/// Get the timestamp of the Pectra fork, read from the `EigenPodManager`
764+
/// This returns the timestamp of the first non-missed slot at or after the Pectra hard fork
765+
uint64 forkTimestamp = eigenPodManager.pectraForkTimestamp();
766+
require(forkTimestamp != 0, ForkTimestampZero());
767+
768+
/// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
769+
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
770+
return proofTimestamp <= forkTimestamp
771+
? BeaconChainProofs.ProofVersion.DENEB
772+
: BeaconChainProofs.ProofVersion.PECTRA;
773+
}
747774
}

src/contracts/pods/EigenPodManager.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ contract EigenPodManager is
4646
_;
4747
}
4848

49+
modifier onlyProofTimestampSetter() {
50+
require(msg.sender == proofTimestampSetter, OnlyProofTimestampSetter());
51+
_;
52+
}
53+
4954
constructor(
5055
IETHPOSDeposit _ethPOS,
5156
IBeacon _eigenPodBeacon,
@@ -231,6 +236,22 @@ contract EigenPodManager is
231236
emit BurnableETHSharesIncreased(addedSharesToBurn);
232237
}
233238

239+
/// @notice Sets the address that can set proof timestamps
240+
function setProofTimestampSetter(
241+
address newProofTimestampSetter
242+
) external onlyOwner {
243+
proofTimestampSetter = newProofTimestampSetter;
244+
emit ProofTimestampSetterSet(newProofTimestampSetter);
245+
}
246+
247+
/// @notice Sets the pectra fork timestamp
248+
function setPectraForkTimestamp(
249+
uint64 timestamp
250+
) external onlyProofTimestampSetter {
251+
pectraForkTimestamp = timestamp;
252+
emit PectraForkTimestampSet(timestamp);
253+
}
254+
234255
// INTERNAL FUNCTIONS
235256

236257
function _deployPod() internal returns (IEigenPod) {

src/contracts/pods/EigenPodManagerStorage.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
9191
/// @notice Returns the amount of `shares` that have been slashed on EigenLayer but not burned yet.
9292
uint256 public burnableETHShares;
9393

94+
/// @notice The address that can set proof timestamps
95+
address public proofTimestampSetter;
96+
97+
/// @notice The timestamp of the Pectra proof
98+
uint64 public pectraForkTimestamp;
99+
94100
constructor(IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, IDelegationManager _delegationManager) {
95101
ethPOS = _ethPOS;
96102
eigenPodBeacon = _eigenPodBeacon;
@@ -102,5 +108,5 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
102108
* variables without shifting down storage in the inheritance chain.
103109
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
104110
*/
105-
uint256[42] private __gap;
111+
uint256[41] private __gap;
106112
}

src/test/harnesses/EigenPodHarness.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ contract EigenPodHarness is EigenPod {
1818
}
1919

2020
function verifyWithdrawalCredentials(
21+
uint64 beaconTimestamp,
2122
bytes32 beaconStateRoot,
2223
uint40 validatorIndex,
2324
bytes calldata validatorFieldsProof,
2425
bytes32[] calldata validatorFields
2526
) public returns (uint) {
26-
return _verifyWithdrawalCredentials(beaconStateRoot, validatorIndex, validatorFieldsProof, validatorFields);
27+
return _verifyWithdrawalCredentials(beaconTimestamp, beaconStateRoot, validatorIndex, validatorFieldsProof, validatorFields);
2728
}
2829

2930
function setValidatorStatus(bytes32 pkhash, VALIDATOR_STATUS status) public {

src/test/integration/IntegrationDeployer.t.sol

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
213213
cheats.roll(10_000);
214214
timeMachine = new TimeMachine();
215215
beaconChain = new BeaconChainMock(eigenPodManager, BEACON_GENESIS_TIME);
216+
217+
// Set the `pectraForkTimestamp` on the EigenPodManager. Use pectra state
218+
cheats.startPrank(executorMultisig);
219+
eigenPodManager.setProofTimestampSetter(executorMultisig);
220+
eigenPodManager.setPectraForkTimestamp(BEACON_GENESIS_TIME);
221+
cheats.stopPrank();
216222
}
217223

218224
/// Parse existing contracts from mainnet
@@ -256,7 +262,15 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
256262

257263
// Since we haven't done the slashing upgrade on mainnet yet, upgrade mainnet contracts
258264
// prior to test. `isUpgraded` is true by default, but is set to false in `UpgradeTest.t.sol`
259-
if (isUpgraded) _upgradeMainnetContracts();
265+
if (isUpgraded) {
266+
_upgradeMainnetContracts();
267+
268+
// Set the `pectraForkTimestamp` on the EigenPodManager. Use pectra state
269+
cheats.startPrank(executorMultisig);
270+
eigenPodManager.setProofTimestampSetter(executorMultisig);
271+
eigenPodManager.setPectraForkTimestamp(BEACON_GENESIS_TIME);
272+
cheats.stopPrank();
273+
}
260274
}
261275

262276
function _upgradeMainnetContracts() public virtual {
@@ -619,7 +633,8 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
619633

620634
if (strategy == BEACONCHAIN_ETH_STRAT) {
621635
// Award the user with a random amount of ETH
622-
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 5)
636+
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 2080)
637+
uint amount = 32 ether * _randUint({min: 1, max: 65});
623638
balance = 32 ether * _randUint({min: 1, max: 5});
624639
cheats.deal(address(user), balance);
625640
} else {

src/test/integration/UpgradeTest.t.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.27;
33

44
import "src/test/integration/IntegrationDeployer.t.sol";
55
import "src/test/integration/IntegrationChecks.t.sol";
6+
import "src/test/integration/mocks/BeaconChainMock_Deneb.t.sol";
67

78
abstract contract UpgradeTest is IntegrationCheckUtils {
89
/// Only run upgrade tests on mainnet forks
@@ -12,6 +13,11 @@ abstract contract UpgradeTest is IntegrationCheckUtils {
1213
} else {
1314
isUpgraded = false;
1415
super.setUp();
16+
17+
// Use Deneb Beacon Chain Mock as Pectra state is not live on mainnet
18+
beaconChain = BeaconChainMock(
19+
new BeaconChainMock_DenebForkable(eigenPodManager, BEACON_GENESIS_TIME)
20+
);
1521
}
1622
}
1723

@@ -23,6 +29,7 @@ abstract contract UpgradeTest is IntegrationCheckUtils {
2329
emit log("_upgradeEigenLayerContracts: upgrading mainnet to slashing");
2430

2531
_upgradeMainnetContracts();
32+
_handlePectraFork();
2633

2734
// Bump block.timestamp forward to allow verifyWC proofs for migrated pods
2835
emit log("advancing block time to start of next epoch:");
@@ -34,4 +41,18 @@ abstract contract UpgradeTest is IntegrationCheckUtils {
3441
isUpgraded = true;
3542
emit log("_upgradeEigenLayerContracts: slashing upgrade complete");
3643
}
44+
45+
// Set the fork timestamp sufficiently in the future to keep using Deneb proofs
46+
// `Prooftra.t.sol` will handle the Deneb -> Pectra transition
47+
function _handlePectraFork() internal {
48+
// 1. Set proof timestamp setter to operations multisig
49+
cheats.prank(eigenPodManager.owner());
50+
eigenPodManager.setProofTimestampSetter(address(operationsMultisig));
51+
52+
// 2. Set Proof timestamp
53+
cheats.prank(eigenPodManager.proofTimestampSetter());
54+
eigenPodManager.setPectraForkTimestamp(
55+
type(uint64).max
56+
);
57+
}
3758
}

0 commit comments

Comments
 (0)