Skip to content

Commit 2c90853

Browse files
authored
feat: electra timing fix (#1620)
# v1.6.1 Electra Timing Fix Fixes a bug on EigenPods regarding partial withdrawals for Electra. Please read the [explainer](https://hackmd.io/@rNlehaDSRBydbF_i_McDcw/ByyodLJ9ex) for a detailed description of the bug. **No action is needed and no customer funds are at risk.** ## Release Manager @ypatil12 @nadir-akhtar @gpsanant ## Highlights 🐛 *Bug Fixes* - Update the `EigenPod.requestWithdrawal` function to ensure that validators are pointed to the pod, matching the behavior of `requestConsolidation` 🔧 *Improvements* - Update the `EigenPod.verifyWithdrawalCredentials` function to only accept `beaconTimestamps` that are after the `latestCheckpointTimestamp`. This enables the eigenpod state machine to be easier to be reasoned about **Full Changelog**: v1.6.0...1.6.1
1 parent 7ecc83c commit 2c90853

File tree

12 files changed

+357
-8
lines changed

12 files changed

+357
-8
lines changed

CHANGELOG/CHANGELOG-1.6.1.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# v1.6.1 Electra Timing Fix
2+
3+
Fixes a bug on EigenPods regarding partial withdrawals for Electra. Please read the [explainer](https://hackmd.io/@rNlehaDSRBydbF_i_McDcw/ByyodLJ9ex) for a detailed description of the bug. **No action is needed and no customer funds are at risk.**
4+
5+
## Release Manager
6+
7+
@ypatil12
8+
9+
## Highlights
10+
11+
🐛 *Bug Fixes*
12+
- Update the `EigenPod.requestWithdrawal` function to ensure that validators are pointed to the pod, matching the behavior of `requestConsolidation`
13+
14+
🔧 *Improvements*
15+
- Update the `EigenPod.verifyWithdrawalCredentials` function to only accept `beaconTimestamps` that are after the `latestCheckpointTimestamp`. This enables the eigenpod state machine to be easier to be reasoned about
16+

CHANGELOG/CHANGELOG-1.8.0-all.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# v1.8.0-testnet-final
2+
3+
The below release notes cover the updated version release candidate for multichain and hourglass
4+
5+
# Release Manager
6+
7+
@ypatil12 @eigenmikem @rajath0x
8+
9+
# Multichain
10+
11+
## Highlights
12+
13+
⛔ Breaking Changes
14+
- The leaves of merkle trees used by the `OperatorTableUpdater` and `BN254CertificateVerifier` are now salted. The [`LeafCalculatorMixin`]() is used by:
15+
- `OperatorTableUpdater`: To salt the `operatorTableLeaf` via `calculateOperatorTableLeaf`. This change is also reflected in the [offchain transporter](https://github.com/Layr-Labs/multichain-go/pull/19)
16+
- `BN254CertificateVerifier`: To salt the `operatorInfoLeaf` via `calculateOperatorInfoLeaf`. BN254 OperatorSets MUST update their table calculators to use the new `BN254TableCalculatorBase` in the [`middleware repo`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/dev/src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol)
17+
- Nonsigners in the `BN254CertificateVerifier` are now sorted by operator index. See [PR #1615](https://github.com/layr-labs/eigenlayer-contracts/pull/1615). All offchain aggregators MUST sort nonsigners by operator index
18+
- The `BN254CertificateVerifier` now requires signatures over the `referenceTimestamp` via `calculateCertificateDigest`. See [PR #1610](https://github.com/layr-labs/eigenlayer-contracts/pull/1610)
19+
20+
🛠️ Security Fixes
21+
- The merkle library has been updated to address minor audit issues. No breaking changes. See [PR #1606](https://github.com/layr-labs/eigenlayer-contracts/pull/1606)
22+
- Audit fixes for the `ReleaseManager`. See [PR #1608](https://github.com/layr-labs/eigenlayer-contracts/pull/1608)
23+
24+
🔧 Improvements
25+
- Introspection for operatorSets with active generation reservations: `hasActiveGenerationReservation`. See [PR #1589](https://github.com/layr-labs/eigenlayer-contracts/pull/1589)
26+
- Clearer error messages/natspec:
27+
- [PR #1602](https://github.com/layr-labs/eigenlayer-contracts/pull/1602)
28+
- [PR #1587](https://github.com/layr-labs/eigenlayer-contracts/pull/1587)
29+
- [PR #1585](https://github.com/layr-labs/eigenlayer-contracts/pull/1585)
30+
- [PR #1562](https://github.com/layr-labs/eigenlayer-contracts/pull/1562)
31+
- [PR #1567](https://github.com/layr-labs/eigenlayer-contracts/pull/1567)
32+
- Require `KeyType` to be set when creating a generation reservation. See [PR #1561](https://github.com/layr-labs/eigenlayer-contracts/pull/1561)
33+
34+
🐛 Bug Fixes
35+
- Add pagination for querying active generation reservations. See [PR #1569](https://github.com/layr-labs/eigenlayer-contracts/pull/1569)
36+
- Fix race conditions on offchain table updates. See [PR #1575](https://github.com/layr-labs/eigenlayer-contracts/pull/1575)
37+
- Remove restrictive check on ECDSA certificates required to be confirmed against the latest `referenceTimestamp`. See [PR #1582](https://github.com/layr-labs/eigenlayer-contracts/pull/1582)
38+
39+
## What's Changed
40+
- fix: enforce ordering of nonsigners in bn254CV [PR #1615](https://github.com/layr-labs/eigenlayer-contracts/pull/1615)
41+
- fix: releaseManager audit fixes [PR #1608](https://github.com/layr-labs/eigenlayer-contracts/pull/1608)
42+
- fix: include timestamp with BN254CertificateVerifier certificate generation [PR #1610](https://github.com/layr-labs/eigenlayer-contracts/pull/1610)
43+
- fix(audit): merkle library audit fixes [PR #1606](https://github.com/layr-labs/eigenlayer-contracts/pull/1606)
44+
- chore: clearer error message [PR #1602](https://github.com/layr-labs/eigenlayer-contracts/pull/1602)
45+
- chore: fmt and bindings
46+
- fix: elm-08
47+
- fix: elm-11(2) middleware ref typo
48+
- chore: bindings
49+
- fix: add `hasActiveGenerationReservation` [PR #1589](https://github.com/layr-labs/eigenlayer-contracts/pull/1589)
50+
- fix(h-03): add pagination [PR #1569](https://github.com/layr-labs/eigenlayer-contracts/pull/1569)
51+
- fix(docs): correct hash value in natspec [PR #1587](https://github.com/layr-labs/eigenlayer-contracts/pull/1587)
52+
- fix(h-04): race condition [PR #1575](https://github.com/layr-labs/eigenlayer-contracts/pull/1575)
53+
- docs: multichain natspec [PR #1584](https://github.com/layr-labs/eigenlayer-contracts/pull/1584)
54+
- fix(audit): add salt to Merkle leaf hashing [PR #1580](https://github.com/layr-labs/eigenlayer-contracts/pull/1580)
55+
- fix: check for key upon gen reservation [PR #1561](https://github.com/layr-labs/eigenlayer-contracts/pull/1561)
56+
- fix: remove unused imports [PR #1585](https://github.com/layr-labs/eigenlayer-contracts/pull/1585)
57+
- feat: update generator script [PR #1581](https://github.com/layr-labs/eigenlayer-contracts/pull/1581)
58+
- fix(m-01): remove restrictive check [PR #1582](https://github.com/layr-labs/eigenlayer-contracts/pull/1582)
59+
- fix(I-01): check input lengths [PR #1564](https://github.com/layr-labs/eigenlayer-contracts/pull/1564)
60+
- chore: clean up interfaces [PR #1562](https://github.com/layr-labs/eigenlayer-contracts/pull/1562)
61+
- fix: addressing pr comments [PR #1568](https://github.com/layr-labs/eigenlayer-contracts/pull/1568)
62+
- fix: correct ecdsa message hash check [PR #1563](https://github.com/layr-labs/eigenlayer-contracts/pull/1563)
63+
- fix: Release Manager internal review fixes [PR #1571](https://github.com/layr-labs/eigenlayer-contracts/pull/1571)
64+
- docs: multichain [PR #1567](https://github.com/layr-labs/eigenlayer-contracts/pull/1567)
65+
66+
# Hourglass

audits/M4 Mainnet (PEPE) - Sigma Prime - Jul 2024.pdf renamed to audits/M4 Mainnet (PEPE) - Sigma Prime - Jul 2024 - Updated Aug 2025.pdf

390 KB
Binary file not shown.

docs/core/EigenPod.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ A withdrawal credential proof uses a validator's [`ValidatorIndex`][custom-types
117117
* `exit_epoch`: Initially set to `type(uint64).max`, this value is updated when a validator initiates exit from the beacon chain. **This method requires that a validator has not initiated an exit from the beacon chain.**
118118
* If a validator has been exited prior to calling `verifyWithdrawalCredentials`, their ETH can be accounted for, awarded shares, and/or withdrawn via the checkpoint system (see [Checkpointing Validators](#checkpointing-validators)).
119119

120-
_Note that it is not required to verify your validator's withdrawal credentials_, unless you want to receive shares for ETH on the beacon chain. You may choose to use your `EigenPod` without verifying withdrawal credentials; you will still be able to withdraw yield (or receive shares for yield) via the [checkpoint system](#checkpointing-validators).
120+
_Note that it is not required to verify your validator's withdrawal credentials_, unless you want to receive shares for ETH on the beacon chain. You may choose to use your `EigenPod` without verifying withdrawal credentials; you will still be able to withdraw yield (or receive shares for yield) via the [checkpoint system](#checkpointing-validators). To account for ETH held on the beacon chain and to make execution layer partial withdrawal or full exits, this function *MUST* be called.
121121

122122
*Effects*:
123123
* For each set of unique verified withdrawal credentials:
@@ -135,6 +135,7 @@ _Note that it is not required to verify your validator's withdrawal credentials_
135135
* Input array lengths MUST be equal
136136
* `beaconTimestamp`:
137137
* MUST be greater than `currentCheckpointTimestamp`
138+
* MUST be greater than `latestCheckpointTimestamp`
138139
* MUST be queryable via the [EIP-4788 oracle][eip-4788]. Generally, this means `beaconTimestamp` corresponds to a valid beacon block created within the last 8192 blocks (~27 hours).
139140
* `stateRootProof` MUST verify a `beaconStateRoot` against the `beaconBlockRoot` returned from the EIP-4788 oracle
140141
* For each validator:
@@ -411,6 +412,7 @@ This method allows the pod owner or proof submitter to submit validator withdraw
411412
* "Partial withdrawals" will exit a portion of a validator's balance from the beacon chain, down to 32 ETH. Any amount requested that would bring a validator's balance below 32 ETH is ignored.
412413

413414
In order to initiate a withdrawal request:
415+
* The [`verifyWithdrawalCredentials`](#verifywithdrawalcredentials) function must be called to prove the validator exists within the pod
414416
* The predeploy requires a fee for each request. The current fee for the block can be queried using `getWithdrawalRequestFee`. This should be multiplied for each request in the passed-in `requests` array and provided as `msg.value`. The predeploy updates its fee each block depending on how many withdrawal requests are queued vs how many are processed.
415417
* Note that any unused fee is transferred back to `msg.sender` at the end of this method.
416418
* For partial withdrawals, note that the beacon chain will only process these if the validator has 0x02 withdrawal credentials.
@@ -430,7 +432,8 @@ Note that the beacon chain may "skip" a withdrawal request for many reasons. Thi
430432
* Pause status MUST NOT be set: `PAUSED_WITHDRAWAL_REQUESTS`
431433
* `msg.value` MUST be at least `getWithdrawalRequestFee() * requests.length`
432434
* For each `request` in `requests`:
433-
* `request.pubkey` MUST have a length of 48
435+
* The validator MUST have been proven to the pod
436+
* `request.pubkey` MUST correspond to a validator whose withdrawal credentials are proven to point at the pod (`VALIDATOR_STATUS.ACTIVE`)
434437
* If excess `msg.value` was provided, the transfer of the excess back to `msg.sender` MUST succeed.
435438

436439
---
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.12;
3+
4+
import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol";
5+
import "../Env.sol";
6+
7+
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
8+
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
9+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
10+
11+
/**
12+
* Purpose: use an EOA to deploy all of the new contracts for this upgrade.
13+
*/
14+
contract Deploy is EOADeployer {
15+
using Env for *;
16+
17+
/// forgefmt: disable-next-item
18+
function _runAsEOA() internal override {
19+
vm.startBroadcast();
20+
21+
22+
// We are upgrading 1 contract: EigenPod
23+
deployImpl({
24+
name: type(EigenPod).name,
25+
deployedTo: address(
26+
new EigenPod({
27+
_ethPOS: Env.ethPOS(),
28+
_eigenPodManager: Env.proxy.eigenPodManager(),
29+
_version: Env.deployVersion()
30+
})
31+
)
32+
});
33+
34+
vm.stopBroadcast();
35+
}
36+
37+
function testScript() public virtual {
38+
super.runAsEOA();
39+
40+
_validateNewImplAddresses({areMatching: false});
41+
_validateProxyAdmins();
42+
_validateImplConstructors();
43+
_validateImplsInitialized();
44+
_validateVersion();
45+
}
46+
47+
/// @dev Validate that the `Env.impl` addresses are updated to be distinct from what the proxy
48+
/// admin reports as the current implementation address.
49+
///
50+
/// Note: The upgrade script can call this with `areMatching == true` to check that these impl
51+
/// addresses _are_ matches.
52+
function _validateNewImplAddresses(
53+
bool areMatching
54+
) internal view {
55+
function (bool, string memory) internal pure assertion = areMatching ? _assertTrue : _assertFalse;
56+
57+
assertion(Env.beacon.eigenPod().implementation() == address(Env.impl.eigenPod()), "eigenPod impl failed");
58+
}
59+
60+
/// @dev Ensure each deployed TUP/beacon is owned by the proxyAdmin/executorMultisig
61+
function _validateProxyAdmins() internal view {
62+
assertTrue(Env.beacon.eigenPod().owner() == Env.executorMultisig(), "eigenPod beacon owner incorrect");
63+
}
64+
65+
/// @dev Validate the immutables set in the new implementation constructors
66+
function _validateImplConstructors() internal view {
67+
{
68+
/// EigenPod
69+
EigenPod eigenPod = Env.impl.eigenPod();
70+
assertTrue(eigenPod.ethPOS() == Env.ethPOS(), "ep.ethPOS invalid");
71+
assertTrue(eigenPod.eigenPodManager() == Env.proxy.eigenPodManager(), "ep.epm invalid");
72+
assertTrue(_strEq(eigenPod.version(), Env.deployVersion()), "ep.version failed");
73+
}
74+
}
75+
76+
/// @dev Call initialize on all deployed implementations to ensure initializers are disabled
77+
function _validateImplsInitialized() internal {
78+
bytes memory errInit = "Initializable: contract is already initialized";
79+
80+
/// EigenPod
81+
EigenPod eigenPod = Env.impl.eigenPod();
82+
vm.expectRevert(errInit);
83+
eigenPod.initialize(address(0));
84+
}
85+
86+
function _validateVersion() internal view {
87+
// On future upgrades, just tick the major/minor/patch to validate
88+
string memory expected = Env.deployVersion();
89+
90+
assertEq(Env.impl.eigenPod().version(), expected, "eigenPod version mismatch");
91+
}
92+
93+
/// @dev Query and return `proxyAdmin.getProxyImplementation(proxy)`
94+
function _getProxyImpl(
95+
address proxy
96+
) internal view returns (address) {
97+
return ProxyAdmin(Env.proxyAdmin()).getProxyImplementation(ITransparentUpgradeableProxy(proxy));
98+
}
99+
100+
/// @dev Query and return `proxyAdmin.getProxyAdmin(proxy)`
101+
function _getProxyAdmin(
102+
address proxy
103+
) internal view returns (address) {
104+
return ProxyAdmin(Env.proxyAdmin()).getProxyAdmin(ITransparentUpgradeableProxy(proxy));
105+
}
106+
107+
function _assertTrue(bool b, string memory err) private pure {
108+
assertTrue(b, err);
109+
}
110+
111+
function _assertFalse(bool b, string memory err) private pure {
112+
assertFalse(b, err);
113+
}
114+
115+
function _strEq(string memory a, string memory b) private pure returns (bool) {
116+
return keccak256(bytes(a)) == keccak256(bytes(b));
117+
}
118+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.12;
3+
4+
import {Deploy} from "./1-deployContracts.s.sol";
5+
import "../Env.sol";
6+
7+
import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol";
8+
import {Encode, MultisigCall} from "zeus-templates/utils/Encode.sol";
9+
10+
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
11+
12+
/**
13+
* Purpose:
14+
* * enqueue a multisig transaction which;
15+
* - upgrades EP
16+
* This should be run via the operations multisig.
17+
*/
18+
contract QueueUpgrade is MultisigBuilder, Deploy {
19+
using Env for *;
20+
using Encode for *;
21+
22+
function _runAsMultisig() internal virtual override prank(Env.communityMultisig()) {
23+
bytes memory calldata_to_executor = _getCalldataToExecutor();
24+
25+
(bool success,) = address(Env.executorMultisig()).call(calldata_to_executor);
26+
assertTrue(success, "Upgrade failed");
27+
}
28+
29+
/// @dev Get the calldata to be sent from the timelock to the executor
30+
function _getCalldataToExecutor() internal returns (bytes memory) {
31+
/// forgefmt: disable-next-item
32+
MultisigCall[] storage executorCalls = Encode.newMultisigCalls().append({
33+
to: address(Env.beacon.eigenPod()),
34+
data: Encode.upgradeableBeacon.upgradeTo({
35+
newImpl: address(Env.impl.eigenPod())
36+
})
37+
});
38+
39+
return Encode.gnosisSafe.execTransaction({
40+
from: address(Env.communityMultisig()),
41+
to: Env.multiSendCallOnly(),
42+
op: Encode.Operation.DelegateCall,
43+
data: Encode.multiSend(executorCalls)
44+
});
45+
}
46+
47+
function testScript() public virtual override {
48+
runAsEOA();
49+
50+
execute();
51+
52+
_validateNewImplAddresses({areMatching: true});
53+
_validateProxyAdmins();
54+
_validateProxyConstructors();
55+
}
56+
57+
/// @dev Mirrors the checks done in 1-deployContracts, but now we check each contract's
58+
/// proxy, as the upgrade should mean that each proxy can see these methods/immutables
59+
function _validateProxyConstructors() internal view {
60+
{
61+
UpgradeableBeacon eigenPodBeacon = Env.beacon.eigenPod();
62+
assertTrue(eigenPodBeacon.implementation() == address(Env.impl.eigenPod()), "eigenPodBeacon.impl invalid");
63+
64+
/// EigenPod
65+
EigenPod eigenPod = Env.impl.eigenPod();
66+
assertTrue(eigenPod.ethPOS() == Env.ethPOS(), "ep.ethPOS invalid");
67+
assertTrue(eigenPod.eigenPodManager() == Env.proxy.eigenPodManager(), "ep.epm invalid");
68+
assertEq(eigenPod.version(), Env.deployVersion(), "ep.version failed");
69+
}
70+
}
71+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "moocow-updates",
3+
"from": ">=1.6.0",
4+
"to": "1.6.1",
5+
"phases": [
6+
{
7+
"type": "eoa",
8+
"filename": "1-deployContracts.s.sol"
9+
},
10+
{
11+
"type": "multisig",
12+
"filename": "2-executeUpgrade.s.sol"
13+
}
14+
]
15+
}

src/contracts/interfaces/IEigenPod.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ interface IEigenPodErrors {
7676
error MsgValueNot32ETH();
7777
/// @dev Thrown when provided `beaconTimestamp` is too far in the past.
7878
error BeaconTimestampTooFarInPast();
79+
/// @dev Thrown when provided `beaconTimestamp` is before the last checkpoint
80+
error BeaconTimestampBeforeLatestCheckpoint();
7981
/// @dev Thrown when the pectraForkTimestamp returned from the EigenPodManager is zero
8082
error ForkTimestampZero();
8183
}

src/contracts/pods/EigenPod.sol

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ contract EigenPod is
225225
// on an existing checkpoint.
226226
require(beaconTimestamp > currentCheckpointTimestamp, BeaconTimestampTooFarInPast());
227227

228+
// For sanity, we want to ensure that a newly-verified validator cannot be proven against state
229+
// that has already been checkpointed. This check makes the state transitions easier to reason about.
230+
require(beaconTimestamp > lastCheckpointTimestamp, BeaconTimestampBeforeLatestCheckpoint());
231+
228232
// Verify passed-in `beaconStateRoot` against the beacon block root
229233
// forgefmt: disable-next-item
230234
BeaconChainProofs.verifyStateRoot({
@@ -321,8 +325,7 @@ contract EigenPod is
321325
// Ensure target has verified withdrawal credentials pointed at this pod
322326
bytes32 sourcePubkeyHash = _calcPubkeyHash(request.srcPubkey);
323327
bytes32 targetPubkeyHash = _calcPubkeyHash(request.targetPubkey);
324-
ValidatorInfo memory target = validatorPubkeyHashToInfo(targetPubkeyHash);
325-
require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod());
328+
require(validatorStatus(targetPubkeyHash) == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod());
326329

327330
// Call the predeploy
328331
bytes memory callData = bytes.concat(request.srcPubkey, request.targetPubkey);
@@ -353,6 +356,9 @@ contract EigenPod is
353356
WithdrawalRequest calldata request = requests[i];
354357
bytes32 pubkeyHash = _calcPubkeyHash(request.pubkey);
355358

359+
// Ensure validator has verified withdrawal credentials pointed at this pod
360+
require(validatorStatus(pubkeyHash) == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod());
361+
356362
// Call the predeploy
357363
bytes memory callData = abi.encodePacked(request.pubkey, request.amountGwei);
358364
(bool ok,) = WITHDRAWAL_REQUEST_ADDRESS.call{value: fee}(callData);
@@ -736,7 +742,7 @@ contract EigenPod is
736742
/// @inheritdoc IEigenPod
737743
function validatorStatus(
738744
bytes32 pubkeyHash
739-
) external view returns (VALIDATOR_STATUS) {
745+
) public view returns (VALIDATOR_STATUS) {
740746
return _validatorPubkeyHashToInfo[pubkeyHash].status;
741747
}
742748

0 commit comments

Comments
 (0)