Skip to content

fix(enm): block validator creation against a retired pod [audit finding 4, MED] - #488

Merged
0xpanicError merged 2 commits into
pankaj/feat/non-eigenpod-withdrawal-credentialsfrom
fix/enm-block-creation-against-retired-pod
Aug 11, 2026
Merged

fix(enm): block validator creation against a retired pod [audit finding 4, MED]#488
0xpanicError merged 2 commits into
pankaj/feat/non-eigenpod-withdrawal-credentialsfrom
fix/enm-block-creation-against-retired-pod

Conversation

@0xpanicError

@0xpanicError 0xpanicError commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Finding 4 (MED) — new validators can be created against a retired pod

disablePod never clears ownerToPod, so _credentialTarget keeps returning the (now disabled) pod for a retired node. All three StakingManager creation paths resolve credentials through withdrawalCredentialTarget, so they would bake 0x02 credentials pointing at the dead pod — and such a validator can never verifyWithdrawalCredentials or checkpoint, stranding its 32 ETH. PR #485 also removed the getEigenPod() == 0 guard from registerBeaconValidators, so nothing else blocked it.

Fix

withdrawalCredentialTarget (the validated resolver used only by the creation paths) reverts PodRetired when the target pod reports restakingDisabled(). The request paths use _credentialTarget directly and are intentionally untouched, so validators already live on a since-retired pod keep working. try/catch leaves pre-v1.14 pods working as before.

Tests

  • New regression test test_credentialTarget_revertsForRetiredPod.
  • non-eigenpod-credentials: 77 passing; non-eigenpod-validator-lifecycle: 54 passing.

Evidence

Reproduced on a Tenderly fork with real EL v1.14.0 (rehearsal test T3): after disabling a node's pod, the resolver returned the disabled pod and baked 0x02 creds pointing at it.

Base: pankaj/feat/non-eigenpod-withdrawal-credentials.

🤖 Generated with Claude Code


Note

Medium Risk
Touches the credential resolver used by all validator creation paths and guards against stranded ETH, but the change is a small defensive check with a regression test and does not alter request/exit flows.

Overview
Prevents new validators from being created against a retired EigenPod, which would strand 32 ETH behind a dead pod that can never verify credentials or checkpoint.

withdrawalCredentialTarget (used by all three StakingManager creation paths) now reverts PodRetired when the target pod reports restakingDisabled(). Request paths still use _credentialTarget directly, so existing validators on a since-retired pod keep working. A try/catch preserves pre-v1.14 pod behavior.

Reviewed by Cursor Bugbot for commit f219f9b. Bugbot is set up for automated code reviews on this repo. Configure here.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

disablePod does not clear ownerToPod, so the resolver keeps returning a
retired pod as the credential target. All three StakingManager creation paths
would then bake 0x02 credentials pointing at a dead pod, and the resulting
validator could never verify credentials or checkpoint - stranding its 32 ETH.
The PR also removed the getEigenPod()==0 guard from registerBeaconValidators,
so nothing else blocked this.

Make the validated withdrawalCredentialTarget resolver revert PodRetired when
the pod reports restakingDisabled(). The check lives in the resolver used only
by creation paths (the request paths call _credentialTarget directly), so
validators already live on a since-retired pod are unaffected. try/catch keeps
pre-v1.14 pods working.

Finding 4 (MED) from the PR #485 pre-audit review; the retired-pod resolver
output was reproduced on a Tenderly fork with real EigenLayer v1.14.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

…wal-credentials' into fix/enm-block-creation-against-retired-pod

# Conflicts:
#	src/staking/interfaces/IEtherFiNodesManager.sol
@0xpanicError
0xpanicError merged commit 95abc75 into pankaj/feat/non-eigenpod-withdrawal-credentials Aug 11, 2026
2 checks passed
0xpanicError added a commit that referenced this pull request Aug 11, 2026
Two follow-ups from Cursor Bugbot on the merged fixes:

Revert finding-3 change (#487): extending the MixedNodeRequest guard to
disabled pods enforced membership via etherFiNodeFromPubkeyHash, which does not
contain all legacy validators. A legitimate same-pod consolidation batch of
legacy validators - the actual migration case - could revert MixedNodeRequest
on the very retirement path it targets. Restore the pod-less-only guard and
drop the _requiresBatchMembershipCheck helper.

Scope finding-4 check (#488): the PodRetired revert lived in
withdrawalCredentialTarget, which confirmAndFundBeaconValidators also calls, so
disabling a pod between the 1 ETH create and the 31 ETH top-up stranded the
validator at 1 ETH. Resolve the target directly in the top-up path (same
pod/node address regardless of retirement); the checked resolver still guards
initial creation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0xpanicError added a commit that referenced this pull request Aug 11, 2026
Two follow-ups from Cursor Bugbot on the merged fixes:

Revert finding-3 change (#487): extending the MixedNodeRequest guard to
disabled pods enforced membership via etherFiNodeFromPubkeyHash, which does not
contain all legacy validators. A legitimate same-pod consolidation batch of
legacy validators - the actual migration case - could revert MixedNodeRequest
on the very retirement path it targets. Restore the pod-less-only guard and
drop the _requiresBatchMembershipCheck helper.

Scope finding-4 check (#488): the PodRetired revert lived in
withdrawalCredentialTarget, which confirmAndFundBeaconValidators also calls, so
disabling a pod between the 1 ETH create and the 31 ETH top-up stranded the
validator at 1 ETH. Resolve the target directly in the top-up path (same
pod/node address regardless of retirement); the checked resolver still guards
initial creation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@pankajjagtapp pankajjagtapp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants