Skip to content

Commit 2429e08

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 1dce4ed commit 2429e08

23 files changed

+1659
-213
lines changed

src/contracts/interfaces/IEigenPod.sol

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

7173
interface IEigenPodTypes {
@@ -145,6 +147,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents {
145147
) external;
146148

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

150153
/**

src/contracts/interfaces/IEigenPodManager.sol

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

3032
interface IEigenPodManagerEvents {
@@ -54,6 +56,15 @@ interface IEigenPodManagerEvents {
5456
event BeaconChainSlashingFactorDecreased(
5557
address staker, uint64 prevBeaconChainSlashingFactor, uint64 newBeaconChainSlashingFactor
5658
);
59+
60+
/// @notice Emitted when an operator is slashed and shares to be burned are increased
61+
event BurnableETHSharesIncreased(uint256 shares);
62+
63+
/// @notice Emitted when the Pectra fork timestamp is updated
64+
event PectraForkTimestampSet(uint64 newPectraForkTimestamp);
65+
66+
/// @notice Emitted when the proof timestamp setter is updated
67+
event ProofTimestampSetterSet(address newProofTimestampSetter);
5768
}
5869

5970
interface IEigenPodManagerTypes {
@@ -115,6 +126,16 @@ interface IEigenPodManager is
115126
int256 balanceDeltaWei
116127
) external;
117128

129+
/// @notice Sets the address that can set proof timestamps
130+
function setProofTimestampSetter(
131+
address newProofTimestampSetter
132+
) external;
133+
134+
/// @notice Sets the Pectra fork timestamp, only callable by `proofTimestampSetter`
135+
function setPectraForkTimestamp(
136+
uint64 timestamp
137+
) external;
138+
118139
/// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed.
119140
function ownerToPod(
120141
address podOwner
@@ -161,4 +182,11 @@ interface IEigenPodManager is
161182
function beaconChainSlashingFactor(
162183
address staker
163184
) external view returns (uint64);
185+
186+
/// @notice Returns the accumulated amount of beacon chain ETH Strategy shares
187+
function burnableETHShares() external view returns (uint256);
188+
189+
/// @notice Returns the timestamp of the Pectra hard fork
190+
/// @dev Specifically, this returns the timestamp of the first non-missed slot at or after the Pectra hard fork
191+
function pectraForkTimestamp() external view returns (uint64);
164192
}

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
@@ -159,6 +159,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
159159

160160
// Verify `balanceContainerProof` against `beaconBlockRoot`
161161
BeaconChainProofs.verifyBalanceContainer({
162+
proofVersion: _getProofVersion(checkpointTimestamp),
162163
beaconBlockRoot: checkpoint.beaconBlockRoot,
163164
proof: balanceContainerProof
164165
});
@@ -254,6 +255,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
254255
for (uint256 i = 0; i < validatorIndices.length; i++) {
255256
// forgefmt: disable-next-item
256257
totalAmountToBeRestakedWei += _verifyWithdrawalCredentials(
258+
beaconTimestamp,
257259
stateRootProof.beaconStateRoot,
258260
validatorIndices[i],
259261
validatorFieldsProofs[i],
@@ -341,6 +343,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
341343

342344
// Verify Validator container proof against `beaconStateRoot`
343345
BeaconChainProofs.verifyValidatorFields({
346+
proofVersion: _getProofVersion(beaconTimestamp),
344347
beaconStateRoot: stateRootProof.beaconStateRoot,
345348
validatorFields: proof.validatorFields,
346349
validatorFieldsProof: proof.proof,
@@ -378,6 +381,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
378381
}
379382

380383
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
384+
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
381385
function stake(
382386
bytes calldata pubkey,
383387
bytes calldata signature,
@@ -419,6 +423,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
419423
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
420424
*/
421425
function _verifyWithdrawalCredentials(
426+
uint64 beaconTimestamp,
422427
bytes32 beaconStateRoot,
423428
uint40 validatorIndex,
424429
bytes calldata validatorFieldsProof,
@@ -473,7 +478,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
473478

474479
// Ensure the validator's withdrawal credentials are pointed at this pod
475480
require(
476-
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()),
481+
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials())
482+
|| validatorFields.getWithdrawalCredentials() == bytes32(_podCompoundingWithdrawalCredentials()),
477483
WithdrawalCredentialsNotForEigenPod()
478484
);
479485

@@ -484,6 +490,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
484490

485491
// Verify passed-in validatorFields against verified beaconStateRoot:
486492
BeaconChainProofs.verifyValidatorFields({
493+
proofVersion: _getProofVersion(beaconTimestamp),
487494
beaconStateRoot: beaconStateRoot,
488495
validatorFields: validatorFields,
489496
validatorFieldsProof: validatorFieldsProof,
@@ -665,6 +672,10 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
665672
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this));
666673
}
667674

675+
function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) {
676+
return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this));
677+
}
678+
668679
///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec
669680
function _calculateValidatorPubkeyHash(
670681
bytes memory validatorPubkey
@@ -731,4 +742,20 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
731742
require(success && result.length > 0, InvalidEIP4788Response());
732743
return abi.decode(result, (bytes32));
733744
}
745+
746+
/// @notice Returns the PROOF_TYPE depending on the `proofTimestamp` in relation to the fork timestamp.
747+
function _getProofVersion(
748+
uint64 proofTimestamp
749+
) internal view returns (BeaconChainProofs.ProofVersion) {
750+
/// Get the timestamp of the Pectra fork, read from the `EigenPodManager`
751+
/// This returns the timestamp of the first non-missed slot at or after the Pectra hard fork
752+
uint64 forkTimestamp = eigenPodManager.pectraForkTimestamp();
753+
require(forkTimestamp != 0, ForkTimestampZero());
754+
755+
/// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
756+
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
757+
return proofTimestamp <= forkTimestamp
758+
? BeaconChainProofs.ProofVersion.DENEB
759+
: BeaconChainProofs.ProofVersion.PECTRA;
760+
}
734761
}

src/contracts/pods/EigenPodManager.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ contract EigenPodManager is
4444
_;
4545
}
4646

47+
modifier onlyProofTimestampSetter() {
48+
require(msg.sender == proofTimestampSetter, OnlyProofTimestampSetter());
49+
_;
50+
}
51+
4752
constructor(
4853
IETHPOSDeposit _ethPOS,
4954
IBeacon _eigenPodBeacon,
@@ -219,6 +224,28 @@ contract EigenPodManager is
219224
}
220225
}
221226

227+
/// @inheritdoc IShareManager
228+
function increaseBurnableShares(IStrategy, uint256 addedSharesToBurn) external onlyDelegationManager nonReentrant {
229+
burnableETHShares += addedSharesToBurn;
230+
emit BurnableETHSharesIncreased(addedSharesToBurn);
231+
}
232+
233+
/// @notice Sets the address that can set proof timestamps
234+
function setProofTimestampSetter(
235+
address newProofTimestampSetter
236+
) external onlyOwner {
237+
proofTimestampSetter = newProofTimestampSetter;
238+
emit ProofTimestampSetterSet(newProofTimestampSetter);
239+
}
240+
241+
/// @notice Sets the pectra fork timestamp
242+
function setPectraForkTimestamp(
243+
uint64 timestamp
244+
) external onlyProofTimestampSetter {
245+
pectraForkTimestamp = timestamp;
246+
emit PectraForkTimestampSet(timestamp);
247+
}
248+
222249
// INTERNAL FUNCTIONS
223250

224251
function _deployPod() internal returns (IEigenPod) {

src/contracts/pods/EigenPodManagerStorage.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
7878
/// Note: this is specifically updated when the staker's beacon chain balance decreases
7979
mapping(address staker => BeaconChainSlashingFactor) internal _beaconChainSlashingFactor;
8080

81+
/// @notice Returns the amount of `shares` that have been slashed on EigenLayer but not burned yet.
82+
uint256 public burnableETHShares;
83+
84+
/// @notice The address that can set proof timestamps
85+
address public proofTimestampSetter;
86+
87+
/// @notice The timestamp of the Pectra proof
88+
uint64 public pectraForkTimestamp;
89+
8190
constructor(IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, IDelegationManager _delegationManager) {
8291
ethPOS = _ethPOS;
8392
eigenPodBeacon = _eigenPodBeacon;
@@ -89,5 +98,5 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
8998
* variables without shifting down storage in the inheritance chain.
9099
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
91100
*/
92-
uint256[43] private __gap;
101+
uint256[41] private __gap;
93102
}

src/test/harnesses/EigenPodHarness.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ contract EigenPodHarness is EigenPod {
2525
}
2626

2727
function verifyWithdrawalCredentials(
28+
uint64 beaconTimestamp,
2829
bytes32 beaconStateRoot,
2930
uint40 validatorIndex,
3031
bytes calldata validatorFieldsProof,
3132
bytes32[] calldata validatorFields
3233
) public returns (uint256) {
3334
return _verifyWithdrawalCredentials(
35+
beaconTimestamp,
3436
beaconStateRoot,
3537
validatorIndex,
3638
validatorFieldsProof,

0 commit comments

Comments
 (0)