92909 w a medium migration reuses uncleared resolver state and activates a revoked delegate s attacker address
Submitted on Sep 13th 2026 at 06:26:37 UTC by @Ailenia for Audit Competition | ENS
- Report ID: #92909
- Report Type: Websites & Apps
- Report severity: Medium
- Target: https://github.com/immunefi-team/audit-comp-ens/tree/audit-comp-ready/apps/manager
- Impacts:
- Hijacking or bricking an ENS name during a Manager flow (registration, v1 to v2 migration, or transfer), such as: - a registration or migration that assigns the name, its ownership, registry, or resolver to an attacker - redirecting the recipient of a name transfer - leaving a name permanently inaccessible after the flow
Description
Brief/Intro
Manager migrates a resolverless V1 name to the victim's existing deterministic PermissionedResolver without clearing or validating that name's destination records. A delegate previously authorized only for that exact name can leave an attacker address in the resolver, be fully revoked before migration, and have the dormant address become authoritative when Manager reattaches the resolver. The completed V2 name is owned by the victim but resolves to the attacker. I assess this as a Medium instance of the project-authored impact "Hijacking or bricking an ENS name during a Manager flow" because prior exact-name trust is required and the owner can recover after detection.
Vulnerability Details
The affected asset is audit-comp-ens/apps/manager at commit 1c9b47f18fcddd2e864dfe385c4171061c9811ae.
A fresh resolverless V1 name is routed to an existing shared resolver
Manager's in-scope migration service receives an unlocked V1 name with no resolver as to-owned-permres; the intended behavior is asserted by audit-comp-ens/apps/manager/src/features/migration/service/classifyNames.test.ts:121-126. This classification is normal context, not the alleged defect.
audit-comp-ens/apps/manager/src/features/migration/service/computeMigrationPreflight.ts:71-101,202-228 derives the HCA's standard deterministic resolver and calls checkDeterministicMigrationResolverReadiness(). The address is the CREATE2 result for the configured VerifiableFactory, its proxy logic, the HCA as deployer, and computeResolverSalt(HCA). If that exact resolver is already deployed and verified, Manager reuses it. The readiness checks in audit-comp-ens/apps/manager/src/features/migration/service/migrationInvariants.ts:278-355 verify factory provenance, implementation, and HCA/wallet roles, but not records stored for the destination name node.
Source-record absence is incorrectly treated as destination-record safety
fetchProfilesForNames() in audit-comp-ens/apps/manager/src/features/migration/service/buildMigrationPlan.ts:67-86 excludes every name whose v1ResolverAddress is null. Manager has no profile to replay when the live V1 Registry correctly reports resolver zero.
Nevertheless, buildMigrationPlan() at lines 317-380 selects the existing deterministic resolver and creates the batch. profileForName() in audit-comp-ens/apps/manager/src/features/migration/service/buildAtomicMigrationBatches.ts:435-455 returns null because no profile exists. buildNameExecution() at lines 459-565 then attaches the existing resolver in Migration.Data but emits no clearRecords(node) or safe address overwrite before migration.
The generated verification is also blind to this state. audit-comp-ens/apps/manager/src/features/migration/service/verifyAtomicMigrationBatch.ts:181-355 checks the resolver implementation, resolver roles, V2 owner, attached resolver, and only the profile entries explicitly generated by the plan. It never checks that an address is absent when the source profile is absent. The exact poisoned state is accepted as a confirmed migration.
Supported exploit sequence
The attack requires only a normal deterministic HCA resolver and a historical role scoped to the exact victim-owned name:
- The victim owns the V1 name
future-name.eth, uses their factory-certified deterministic HCA resolverR, and grants attacker A only name-scopedROLE_SET_ADDRforfuture-name.eth. - A uses that authorized role to store A's ETH address at
namehash(future-name.eth)inR. - The victim revokes A's exact-name role and detaches the V1 resolver to stop serving the poisoned address before migration. A no longer has any write authority; the PoC proves a fresh
setAddrreverts. The live V1 Registry now has resolver zero and no source profile to migrate. - The victim starts a fresh, ordinary Manager V1-to-V2 migration. There is no stale V1 owner or resolver input. Manager verifies and reuses the correct deterministic resolver
R, but does not invalidate the name's existing state inR. - The official migration attaches
Rto the V2 name. The victim remains the correct owner, yet effective resolution immediately returns A's dormant address. Manager's verifier reports success without reading that address. - The victim can overwrite the address after independently detecting the hijack, but resolution remains attacker-controlled until that repair.
The official resolver and migration contracts behave as designed and are used only as reachability oracles. The in-scope root cause is Manager's unsafe composition of a fresh resolverless source with a reused stateful destination.
Impact Details
I chose the impact "Hijacking or bricking an ENS name during a Manager flow (registration, v1 to v2 migration, or transfer)." The PoC completes the Manager-shaped migration with the expected owner and expected resolver, but the migrated name resolves to the revoked delegate's attacker address.
The attacker previously held only the exact-name address role and chooses the sleeper address while authorized; after revocation, the attacker cannot write again and merely waits for the public migration. The victim's migration is a normal flow and requires no stale cache or timing race. However, the victim must previously have trusted the delegate, retains resolver administration, and can recover with a direct record write after detection. I do not claim ownership theft, permanent bricking, or a separate payment by another user.
Existing Safeguards and triage FAQ
- "The address was once authorized." Historical permission to write while the V1 name used
Ris not authority to reactivate that record after the role is revoked, the resolver is detached, and Manager migrates a fresh resolverless source. The flaw is not that revocation retroactively erases records; it is that Manager attaches a reused stateful destination while treating source absence as a clean destination. - "Manager verifies the resolver." It verifies that
Ris the correct factory-certified HCA resolver and that the victim/HCA hold roles. Those facts are true in the exploit. Provenance does not establish that this name's stored records are clean. - "The V1 indexer may be stale." The exploit needs no stale value. The production test supplies the current state: resolver null and no profile keys. Fresh V1 revalidation produces the same vulnerable plan.
- "The owner can recover." Recovery is unilateral but reactive. It requires detecting an attacker address after Manager has declared the migration successful. This bounds severity but does not prevent the completed resolver hijack.
Known Issue and Duplicate Boundaries
Another one I reported before which is now confirmed was a stale-manager report snapshots the V1 owner and later grants a revoked manager ROLE_SET_RESOLVER. The stale-custom-resolver report snapshots a V1 resolver pointer and reinstalls the removed contract. This report has neither stale field: the source resolver is freshly and correctly null, no role is granted, and the correct victim-controlled resolver is attached. Re-reading V1 owner or resolver fixes those reports but cannot detect this poisoned destination; clearing or reconciling destination records fixes this report.
Known Issue QA-01 covers role combinations that cause errors and expressly leaves demonstrated unintended authority as new. This flow does not error. Known Issue WEB-806 concerns migrated subname records not being visible, not activation of an attacker record on a resolverless source.
Remediation
Before attaching an already-deployed PermissionedResolver for every to-owned-permres name, invalidate the destination node in the same atomic HCA execution and then replay only the intended live V1 profile. For ordinary records, call clearRecords(node) before migrate/profile replay. Independently reject or remove any alias state because aliases may not share the ordinary record version.
If Manager cannot prove the destination is clean, fail closed and require explicit review rather than interpreting a missing source profile as an empty destination. Add verification expectations for the effective ETH address and every supported record class, including expected absence. Add a regression test with an exact-name delegate who writes, is revoked, and whose V1 resolver is detached before migration; the migrated result must not expose the historical value.
References
audit-comp-ens/apps/manager/src/features/migration/service/classifyNames.test.ts:121-126audit-comp-ens/apps/manager/src/features/migration/service/computeMigrationPreflight.ts:71-101,202-228audit-comp-ens/apps/manager/src/features/migration/service/migrationInvariants.ts:278-355audit-comp-ens/apps/manager/src/features/migration/service/buildMigrationPlan.ts:67-86,317-380audit-comp-ens/apps/manager/src/features/migration/service/buildAtomicMigrationBatches.ts:435-565audit-comp-ens/apps/manager/src/features/migration/service/verifyAtomicMigrationBatch.ts:181-400
Link to Proof of Concept
https://gist.github.com/niafreu/1eb62c8a513196e4db9d863a5edfb738
Proof of Concept
Proof of Concept
My PoC is available at https://gist.github.com/niafreu/1eb62c8a513196e4db9d863a5edfb738. It combines a production Manager plan/verifier test with official ENS v2 resolver and migration contracts.
Prerequisites
- Node.js, Corepack/pnpm, Git, and Foundry.
immunefi-team/audit-comp-ensat1c9b47f18fcddd2e864dfe385c4171061c9811ae, with dependencies installed.ensdomains/contracts-v2at48b3e2d39513b9dd32ef1850877a29009bc807b9, with submodules initialized.
Setup
git clone https://github.com/immunefi-team/audit-comp-ens.git audit-comp-ens
git -C audit-comp-ens checkout 1c9b47f18fcddd2e864dfe385c4171061c9811ae
corepack pnpm --dir audit-comp-ens install --frozen-lockfile
git clone --recurse-submodules https://github.com/ensdomains/contracts-v2.git contracts-v2
git -C contracts-v2 checkout 48b3e2d39513b9dd32ef1850877a29009bc807b9
git -C contracts-v2 submodule update --init --recursive
Save the five attached files in one poc directory, then run:
ENS_REPO_DIR="$PWD/audit-comp-ens" \
CONTRACTS_V2_DIR="$PWD/contracts-v2" \
bash poc/run-poc.sh
Expected output
manager-plan.test.ts > production Manager reuse of a resolver with dormant destination records > attaches the existing resolver without clearing the name node or checking record absence
Test Files 1 passed (1)
Tests 1 passed (1)
[PASS] test_clearDestinationNodeBeforeAttachmentPreventsActivation()
[PASS] test_revokedDelegatePreseedBecomesLiveWhenManagerAttachesExistingResolver()
Suite result: ok. 2 passed; 0 failed; 0 skipped