92177 w a medium stale v1 manager snapshot re grants a former manager v2 resolver authority during migration
Submitted on Sep 7th 2026 at 21:26:04 UTC by @LucineT for Audit Competition | ENS
- Report ID: #92177
- 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
Impact
The ENS Manager migration flow can construct and submit a transaction that grants ROLE_SET_RESOLVER to an address that is no longer the name's current V1 Registry manager. The former manager can then change the migrated name's resolver.
This is a direct match for the competition's Critical Manager-flow impact class: the migration assigns resolver control to an attacker/former manager. The proof does not rely on compromising the subgraph, wallet, Hybrid Custody Account, or MigrationHelper. It relies on a stale authority value retained by the Manager app and later materialized as a fresh V2 role grant.
Once the former manager can set the resolver, they can redirect name resolution away from the owner's intended resolver. Any wallet, application, or user relying on that ENS resolution can consequently receive attacker-selected resolution data. The local proof stops after demonstrating the unauthorized role and resolver change; it does not transfer funds or touch a third-party account.
The vulnerable authority decision is inside the in-scope Manager application and its transaction-construction path. The contract fixtures are used only as the poststate oracle that proves the generated Manager batch has a real authority effect; this report does not claim a vulnerability in the out-of-scope ENS smart contracts or subgraph. It also requires no browser-extension defect. The owner's action is the ordinary, prompted Manager migration workflow.
Summary
The migration page obtains V1 name data through useV1Names. A previously current V1 manager, B, is retained in the mounted query result. After V1 Registry ownership changes to the registrant/current owner A, B is no longer the current manager. However, the migration path does not re-read the authoritative V1 Registry owner before constructing the final migration batch.
The stale value crosses the complete transaction-construction path:
useV1Namessupplies the mounted domain snapshot containingB.classifyNamescopiesdomain.owner.idintomanagerAddress.computeMigrationPreflightrefreshes other migration prerequisites but does not freshly bindmanagerAddressto current V1 Registry state.buildAtomicMigrationBatchesemitsgrantRoles(resource, ROLE_SET_RESOLVER, B).- The owner-authorized migration executes the batch.
- V2 poststate contains
ROLE_SET_RESOLVERforB. Bsuccessfully changes the migrated name's resolver.
The decisive negative control changes only the stale/fresh manager read. After query invalidation and a fresh read of A, the same path grants no role to B and rejects B's resolver write.
Root cause in the frozen competition source
The program resources name audited revision 63772fd872af472ced58b009499355f3430c2a86. The official public competition repository's audit-comp-ready branch resolved to 1c9b47f18fcddd2e864dfe385c4171061c9811ae; its public fork base declares that audited upstream revision. The C-ENS-007 mechanism-bearing files are byte-stable from the public fork base through 1c9b47f.
useV1Names.tslines 12–29 obtains the V1 domain snapshot. Its requestedstaleTimeis removed by the shared query-options wrapper, but the mounted result still remains the consumed value until refetch/invalidation.query.tslines 120–147 destructures and does not returnstaleTime.classifyNames.tslines 148–185 promotes the snapshot owner tomanagerAddress.computeMigrationPreflight.tslines 192–259 reuses the classified domains without a fresh authoritative manager read.buildRoleGrantCalls.tslines 8–26 encodes the new V2 role grant tomanagerAddress.buildAtomicMigrationBatches.tslines 608–624 includes the grant in the atomic owner batch and expects it in poststate.
The security invariant that fails is:
manager used for a new V2 authority grant
== current authoritative V1 Registry manager at final plan construction
The application instead binds the recipient to an earlier mounted snapshot.
Preconditions
Bis the V1 Registry manager when the migration page obtains its mounted snapshot.- Before final migration-plan construction, current V1 Registry ownership changes from
BtoA. This state can arise through a genuine registrant reclaim; the local source also shows that a current manager can callsetOwner(node, A). - The mounted query is not invalidated/refetched before the plan captures
managerAddress. Aproceeds with the Manager-generated migration batch.
No malicious wallet, compromised dependency, private key theft, subgraph write, or privileged test hook is required.
Expected result
Immediately before final batch construction, Manager must bind every authority recipient to current authoritative V1 chain state. If the current manager differs from the earlier snapshot, the plan must be rebuilt or rejected. A former manager must not receive a new V2 role.
Actual result
The stale snapshot value is converted into new V2 ROLE_SET_RESOLVER authority. The former manager can exercise that authority and change the migrated name's resolver. A fresh-read control prevents both effects.
Why the owner's signature does not remove the defect
The security-sensitive recipient is chosen by the application before the wallet prompt. The transaction is owner-authorized, but the application has constructed the role recipient from obsolete authority state. The issue is therefore the wrong transaction argument and authority recipient, not signature bypass. A wallet signature does not make stale application-selected authority current.
Distinction from public known issues
- Public QA-01 covers transfer-role combinations that cause later functional errors and explicitly leaves a demonstrated unintended authority consequence as a new finding. C-ENS-007 demonstrates a new V2 role grant and successful resolver write during migration.
- ENS contracts-v2 issue #418 concerns a V2 delegate role that already exists and survives a later transfer. C-ENS-007 has a different root and transition: a revoked V1 manager is used by stale Manager state to create fresh V2 authority during migration.
A finite search of the competition's published known-issue list and official ENS issues found no exact public match for this stale V1 snapshot -> fresh V2 authority grant root cause.
Source and deployment corroboration
- The mechanism-bearing files are byte-stable from the public fork base through
audit-comp-readycommit1c9b47f.... - The served
app.ens.devmigration chunks observed on2026-09-02contained themanagerAddresspath,grantRolessurface, pinned helper address, and expected helper runtime hash. - At Sepolia block
11621448, the pinned helper runtime hash matched the exact runtime invariant embedded in the competition source and served bundle. - The app-pinned helper is expressly not source-verified in the competition code; therefore the local PoC binds the published/frozen app source and exact PR #402 contract artifacts rather than claiming a vendor-signed reproducible production build that the project has not published.
Current source/version receipt SHA-256: 4aea78825b0f5bb51265289f2b1fd64a7dbf4a0fa44fe9d8c3f161de83845b76.
Recommended remediation
- Immediately before building the migration batch, read the current V1 Registry owner for every name from authoritative chain state.
- Derive
managerAddressonly from that fresh result. - Bind the recipient, role, block reference, and name into the preflight/plan digest.
- Display the exact role recipient before signature.
- Reject and rebuild if current ownership changes before submission.
- Verify poststate against the freshly bound manager, never the earlier mounted snapshot.
- Add the positive stale-read test and fresh-read negative control as permanent regression tests.
Proof of Concept
Step-by-step proof of concept
PoC A — mounted Manager path through TransactionManager and local EVM
Attachment: c-ens-007.continuous-cache-manager-evm.security-repro.test.tsx
SHA-256: 1af8d6cafcfb78d79fe6351a8c14a9cf06128fb9596223d21762946f420976b7
Place the attachment at:
apps/manager/src/features/migration/service/
From apps/manager, run:
pnpm exec vitest run src/features/migration/service/c-ens-007.continuous-cache-manager-evm.security-repro.test.tsx --reporter=verbose
The first test performs both the positive and fresh-read control:
- Mount the real
useV1Nameshook whileBis the V1 manager. - Observe cached manager
B. - Perform a genuine V1 Registry state transition so the current manager becomes
A. - Without invalidating the mounted query, execute exact
classifyNamesandbuildAtomicMigrationBatches. - Submit the generated owner batch through the real app TransactionManager/XState path to the local EVM.
- Assert that
BreceivesROLE_SET_RESOLVER. - Execute a resolver change as
Band assert success. - Invalidate/refetch and repeat with current manager
A. - Assert that
Breceives no role and thatB's resolver write is rejected.
The second test preserves the same stale recipient through the exact PR #402 StandaloneHCAFactory, StandaloneSingleOwnerHCA.executeByOwner, MigrationHelper, UnlockedMigrationController, and local PermissionedRegistry artifacts in the same mounted-app process.
Fresh run on 2026-09-07:
Test Files 1 passed (1)
Tests 2 passed (2)
Exit code 0
Duration 25.81s
Observed transaction states:
c-ens-007-stale-manager-positive pending -> success
c-ens-007-fresh-manager-negative pending -> success
c-ens-007-all-real-pr402-positive pending -> success
The Node test emits an expected IndexedDB unavailable warning and uses the transaction manager's fallback storage path. Both poststate oracles complete and the suite passes.
PoC B — exact PR #402 HCA/MigrationHelper discriminator
Attachment: c-ens-007-real-hca-pr402.t.sol
SHA-256: f171113262e889de05bf3bad72a776e8ee8579acd386f836f86eb258a595dc9c
This preserved network-disabled Foundry discriminator uses ENS contracts commit 0717b0b6f1867f43078f19115dd5dfa4f35426ab and varies only the manager read:
[PASS] test_staleRead_realFactoryAndNexusHCA_regrantsBAndPermitsResolverWrite()
[PASS] test_freshRead_realFactoryAndNexusHCA_doesNotGrantB()
Suite result: ok. 2 passed; 0 failed; 0 skipped
The production migration route used by this source is the wallet-owner executeByOwner path. Adding a session/default-executor signature would test a different path and is therefore intentionally excluded.