Skip to content

Port latest ENSv2 deployment to .dos - #1

Merged
JOY (JOY) merged 108 commits into
dosfrom
codex/ensv2-dos-port
Aug 4, 2026
Merged

Port latest ENSv2 deployment to .dos#1
JOY (JOY) merged 108 commits into
dosfrom
codex/ensv2-dos-port

Conversation

@JOY

Copy link
Copy Markdown

Summary

  • base the DOS fork on upstream ENSv2 commit 48b3e2d39513b9dd32ef1850877a29009bc807b9
  • add a .dos registrar that preserves the audited upstream registration logic
  • add a greenfield DOS Chain deployment profile for chain ID 3939 with WDOS payments, .dos forward resolution, DOS reverse resolution, and resolver proxy support
  • add integration coverage for registration, renewal, payment scaling, forward resolution, and reverse resolution

Validation

  • Solgrid lint and format: passed
  • Forge targeted suite: 8 passed
  • Forge full suite: 898 passed
  • Hardhat compile on Node 24.1.0: passed
  • Vitest: 308 passed, 1 suite skipped by upstream
  • TypeScript tsc --noEmit: passed
  • Actual forge script --broadcast against local Anvil chain 3939: passed, followed by on-chain ownership, role, oracle ratio, registry, and reverse namespace checks
  • Independent code review: approved with no Critical, Important, or Minor findings

Deployment boundary

This PR changes contract source only and does not deploy to DOS Chain. The existing BENS indexer in DOS-Chain uses the legacy registrar ABI and event layout. It must be updated and validated in a separate cross-repo change before these contracts are deployed to any shared environment.

…nsdomains#273)

- moved `event EnhancedAccessControl.EACRolesChanged` before callbacks
- moved `event PermissionedRegistry.TokenRegenerated` before `_mint()`
- added tests
* added `test_unwrapped_safeTransferFrom_unauthorizedCaller()`
* added `test_unwrapped_nameDataMismatch()`
* added `test_MIN_DATA_SIZE()`
* updated `test_wrapped_migrate()`
* updated `test_unwrapped_invalidData()`
* updated `test_wrapped_invalidData()`
* updated `test_migrate_invalidData()`
- restored *"next resource while expired"* logic from ensdomains#245
- added more tests
…sdomains#281)

- added `PermissionedRegistry._register(..., bool checkRoles)`
- changed `PermissionedRegistry.register()` to call `_register(..., true)`
- changed `WrapperRegistry._inject()` to call `_register(..., false)`
- removed questionable function specifiers
)

* fixed ResolverProfileRewriterLib.replaceNode()
    - avoids writing if out of bounds
* added tests for malicious calldata
- updated `PermissionedResolver`
     * changed `resource(0, 0)` to be `ROOT_RESOURCE`
     * changed `grantTextRoles(name, key, account)` to `authorizeTextRoles(name, key, account, grant)`
     * changed `grantAddrRoles(name, coinType, account)` to `authorizeAddrRoles(name, coinType, account, grant)`
     * disabled `revokeRoles()`  
     * changed `grantNameRoles(name, roles, account)` to `authorizeNameRoles(name, roles, account, grant)`
     * added `IDataResolver` support
     * added `authorizeDataRoles(name, key, account)`
- added tests
- minor simplification of `EAC._get{Settable|Revokable}Roles()`
- removed `StorageTester.sol` and test
- added configurable `--chainId` to devnet
- changed default to `31337`
- changed e2e tests to use `--chainId 1`
…y-upgrade-gate

fix: gate wrapper registry upgrades
…strar-inception

fix: advance reverse registrar inception on direct writes
- changed `PermissionedRegistry._update()` to check roles after `super._update()`
    * transfers roles immediately after each `ROLE_CAN_TRANSFER_ADMIN` check
    * added tests
- added `uint256[256] private __gap` to `EnhancedAccessControl`
- added `uint256[256] private __gap` to `PermissionedRegistry`
…able-factory

chore: update verifiable factory
* feat: add reverse registrar hca adapters

* fix: mirror reverse namespace at root

* chore: migrate post-audit linting

* chore: simplify solgrid scripts

* chore: split solgrid format check

* chore: make format script ci-aware

* chore: split format check script

* chore: relax constructor notice lint

* chore: lint test solidity files

* chore: remove redundant constructor notices

* Add `RegistryCreated` event (ensdomains#258)

- added `event IRegistryEvents.RegistryCreated()`
- modified `PermissionedRegistry` to emit event during constructor
- modified `UserRegistry` to emit event during `initialize()`
- modified `WrapperRegistry` to emit event during `initialize()`
- moved `WrapperRegistry` upgrade role logic to `_subregistryRoleBitmapFromFuses()`

* Tag migrations onchain (ensdomains#287)

- changed `PermissionedRegistry` to assign token `ROLE_WAS_RESERVED` when registered via reservation

* fix: assign reverse namespace to owner

* fix: align reverse adapter section headers

* test: split reverse hca adapter suites

* style: format reverse hca adapters

* Add `LabelStore` (ensdomains#260)

* added `ILabelStore`
    - added `event Label(bytes32 labelHash, string label)`
* added `LabelStore`
    - added `setLabel(label)` and `getLabel(anyId) returns (label)`
* updated `PermissionedRegistry`, `UserRegistry`, and related constructors

* Add `Graveyard` (ensdomains#266)

- added Graveyard and tests
    * has no ENSv2 dependencies
    * added `clear(names[])`
- moved some common migration logic to `LibMigration`
- changed `UnlockedMigrationController` to transfer unwrapped ERC-721 to `Graveyard`
- changed `UnlockedMigrationController` to unwrap unlocked ERC-1155 to `Graveyard`
- changed `LockedMigrationController` to unwrap detached ERC-1155 to `Graveyard`
- changed `LockedMigrationController` to transfer locked ERC-1155 to `Graveyard`

* Add ENSIP-24 to `DNSTXTResolver` (ensdomains#285)

* Change `PermissionedRegistry.renew()` to allow revive from root (ensdomains#282)

* Refactor `ETHRegistrar` and add `ETHRenewerV1` (ensdomains#286)

* refactored `StandardRentPriceOracle` and tests
     - fully immutable except payment tokens
     - uses EAC
     - separate role for `disablePaymentToken()`
     - changed to pricing via components
     - added some helpers
         * `getLength(label): number`
         * `getPaymentTokenRatio(paymentToken): (numer, denom)`
         * `applyDiscount(x, duration): % of x`
         * `convertUnits(x, paymentToken): amount`
         * `getBasePrice(label, duration): cost`
     - changed discount to cliffs
* changed `IETHRegistrar`
     - split renew functions into `IETHRenewer`
     - added `getRegisterPrice(...): (base, premium)` — relative to current state
* added `IETHRegistrar`
     - added `GRACE_PERIOD(): seconds` — specific to the renewer
     - added `getRemainingGracePeriod(label): seconds`
     - added `getRenewPrice(...): amount` — relative to current state
* refactored `ETHRegistrar` and tests
     - uses `Ownable`
     - implements `IETHRegistrar` and `IETHRenewer`
     - `renew()` only does registrations w/grace
     - no ENSv1 references
* added `ETHRenewerV1` and tests
     - uses `Ownable`
     - implements `IETHRenewer`
     - `renew()` only does reservations with grace and syncs ENSv1 `BaseRegistrar`
     - `syncWrapper(labels[])` — sync wrapped with unwrapped (trustless)
     - `transferRegistrarOwnership(newOwner)` — like security controller
* changed `PermissionedRegistry`
     - `register()` of `RESERVED` allows past expiry
     - added tests
* added postlaunch e2e tests
* removed `IERC165` from `IEnhancedAccessControl`
* removed `IRegistry` from `PermissionedRegistry`
* removed `HCA` from `MockERC20`
* renamed `ETH_REGISTRAR_V1` to `BASE_REGISTRAR`
* `StandardRegistrar.sol` constants are mirrored in `deploy-constants.ts`
* `deploy/StandardPriceOracle.ts` prints a discount table
* updated deploy scripts

* Add `PublicResolverV2` (ensdomains#263)

- added `IAddressSet`
- added `PermissionedAddressSet` and tests — manages a set of addresses with EAC
- added `PublicResolverV2` which is like `PublicResolver` except `isAuthorized()` uses ENSv2
- added `PublicResolverV2.t.sol`
- added tests to `LockedMigrationController.t.sol`
- added deploy scripts
    * include `PublicResolverV1`  in set on devnet
- fixed e2e test for whitelisted PublicResolver

* feat(premigration): include v1 grace-period names with 62-day default (ensdomains#302)

* test(premigration): align e2e expectations with relaxed past-expiry guard (ensdomains#303)

PR ensdomains#286 relaxed PermissionedRegistry._register so that reservations
(owner == address(0)) only revert when expiry == 0; past expiries are
allowed. PR ensdomains#302 widened pre-migration eligibility to include names
inside v1's 90-day grace period, but the rewritten e2e tests still
asserted the old "past v2 expiry => failure" behavior.

Update three test expectations so just-expired-but-still-claimable
names are counted as successful reservations rather than failures.

* Add `IUniversalResolverV2` (ensdomains#267)

- added `IUniversalResolverV2`
- updated `UniversalResolverV2`
     * restored `findParentRegistry()`
     * added `IUniversalResolverV2`
- added `UniversalResolverV2.t.sol`

* Add `ITokenizedRegistry`, `ITemporalRegistry`, and `IOwnedRegistry` (ensdomains#262)

- added `ITokenizedRegistry` — `findTokenId(label): token`
- added `ITemporalRegistry` — `findExpiry(label): expiry`
- added `IOwnedRegistry` — `findOwner(label): owner`
- changed `IStandardRegistry` to inherit them

* Refactor Registry Metadata (ensdomains#253)

- removed `IRegistryMetadata`
- removed `MetadataMixin`
- removed `BaseUriRegistryMetadata`
- removed `SimpleRegistryMetadata`
- removed metadata from all constructors
- added `IRegistryURIRenderer`
- changed `PermissionedRegistry`
    * added `ROLE_SET_URI` and `ROLE_SET_URI_ADMIN`
    * added `setURI(uri, renderer)`
    * added `event URIChanged(uri, renderer, sender)`
    * added `uri(tokenId)` — uses renderer if it exists

* feat(devnet): add mainnet-fork mode to runDevnet (ensdomains#305)

* feat(devnet): add mainnet-fork mode to runDevnet

Introduces `--forkUrl` (and optional `--forkBlock`) so the devnet can boot
against a forked mainnet with canonical v1 contracts preserved and v2
contracts deployed fresh on top.

- Pre-populate `deployments/devnet-<chainId>/` with canonical v1
  `{address, abi}` entries from `lib/ens-contracts/deployments/mainnet/`
  so `rocketh.get(name)` resolves to mainnet addresses without redeploys.
- Mirror the canonical `.chain` header so rocketh's chainId/genesisHash
  validation passes against the forked node.
- Enable anvil `autoImpersonate` and fund the DAO multisig plus every live
  `.owner()` of the relevant Ownable v1 contracts via `anvil_setBalance`,
  letting deploy scripts that issue `account: owner` writes succeed.
- Skip the `lib/ens-contracts/deploy` scripts list on fork; v2 scripts
  still run.
- Branch `activateV2` to (a) sign as the DAO via autoImpersonate, (b) skip
  `LegacyETHRegistrarController` removal (doesn't exist on canonical
  mainnet), (c) grant `accounts.deployer` registrar-controller rights so
  test harnesses can drive v1.BaseRegistrar from the mnemonic, and (d)
  renounce NameWrapper ownership only when still non-zero.
- Skip `setupEnsDotEth` on fork to avoid colliding with real subdomains.
- Omit anvil `--auto-impersonate false` in non-fork mode (prool encodes
  `false` as a positional arg that trips anvil's subcommand parser).

* feat(devnet): auto-activate v2 + /deployments + env-var fallbacks + --quiet

Four ergonomics fixes to the fork-mode devnet that previously forced
downstream consumers (morticia's mainnet-fork e2e runner) to paper
over gaps in setup:

1. Auto-invoke env.activateV2() when --forkUrl is set. Previously
   runDevnet returned activateV2 from the env object but never
   called it, so on a fresh fork the v2 Graveyard / ETHRenewerV1
   weren't BaseRegistrar controllers and the BaseRegistrar's owner
   was still the ENS DAO multisig — every clear() simulation hit
   ONLY_CONTROLLER. Gating to --forkUrl preserves the existing
   greenfield-devnet behaviour (its consumers grant controller roles
   from test-side setup).

2. Env-var fallbacks for the two fork-mode flags: --forkUrl
   <- FORK_URL, --forkBlock <- FORK_BLOCK. Matches the pattern used
   by every downstream consumer.

3. GET /deployments on the healthcheck server returns a JSON map of
   { rocketh deployment name -> checksummed address } plus chainId,
   so consumers can discover addresses (Graveyard, ETHRenewerV1,
   migration controllers, etc.) without parsing
   deployments/devnet-{chainId}/*.json files directly.

4. --quiet flag (or DEVNET_QUIET=1) suppresses the two console.table
   blocks that print named-accounts and contract-addresses. The
   'Ready! <Xms>' line and healthcheck-listening message are
   retained. Cleaner output when runDevnet is consumed as a
   subprocess.

Smoke-tested standalone with FORK_URL=publicnode + DEVNET_QUIET=1:
the new /deployments endpoint returns the v2 graveyard,
BaseRegistrar.controllers(graveyard) returns true post-boot, and
BaseRegistrar.owner() is ETHRenewerV1.

* fix(devnet): remove LegacyETHRegistrarController on mainnet-fork activateV2

* Add `UniversalResolver.findOwner(name)` (ensdomains#308)

* added `LibRegistry.findOwner(root, name offset): owner`
* added `UniversalResolver.findOwner(name): owner`
* updated `PublicResolverV2.authorize()` to use `findOwner()`

* Fix `foundry.lock` (ensdomains#310)

* Add Encoded Label Support to `Graveyard` (ensdomains#309)

* updated `Graveyard.clear()` to process encoded labels using modified encoding

* Add `ExtendedDNSResolver` Support to `DNSTXTResolver` (ensdomains#311)

* update `DNSTXTResolver` to support `ExtendedDNSResolver` context format: just `addr(60)`

* Add `MigrationHelper` (ensdomains#261)

* Bump `ens-contracts` and remove `ICompositeResolver.requiresOffchain()` (ensdomains#314)

* Fix: `_isMigratableChild()` allows ABANDONED and returns false once expiry is non-zero (ensdomains#313)

* feat: require hca interaction

* Add `MigrationHelper` deploy script (ensdomains#317)

* Remove `ExtendedDNSResolver` artifact (ensdomains#316)

- removed `ExtendedDNSResolver_53f64de872aad627467a34836be1e2b63713a438.json`
- remapped `dnsname` to `dnstxt` 
   * `DNSTXTResolver`  is backwards-compatible with `ExtendedDNSResolver` context
    * fixes bug with `addr(coinType)` response on `ExtendedDNSResolver` deployment

* feat: hardcode deferred hca implementation

* style: format hca tests

* refactor: clean up hca factory interface

* refactor: remove nexus hca proxy dependency

* refactor: use yul hca proxy bytecode

* Improve pre migration csv parsing (ensdomains#319)

* feat: update hca interaction flow

* feat: expose deferred hca implementation

* Add `IContractNamer` (ensdomains#297)

- added `IContractNamer`
- added `AccountNamerLib`
- updated `L2ReverseRegistrar`
    * changed `setNameForOwnableWithSignature` to `setNameForContractWithSignature`
    * replaced `_ownsContract()` and `modifier authorized()` with `AccountNamerLib`
    * added `IContractNamer` support
- updated `ReverseRegistrarHCAAdapter`
    * added `claimForContract(contract, resolver)`
- updated `DefaultReverseRegistrarHCAAdapter`
    * added `setNameForContract(contract, name)`

- added `ContractNamer` as shared upgradeable namer
- added `DelegatedContractNamer` mixin
- updated `PermissionedRegistry`, `PermissionedAddressSet`, and `StableRentPriceOracle`
    * added `ROLE_CAN_NAME` and `ROLE_CAN_NAME_ADMIN`
    * implemented `IContractNamer` with `hasRootRoles(ROLE_CAN_NAME, account)`
- updated non-`Ownable/EAC` core contracts to use `DelegatedContractNamer`

- cleaned `foundry.lock`
- updated readme
- added tests

* feat: record hca on account creation

* perf: compute hca account address

* test: simplify reverse registrar hca fixture

* fix: restore reverse mirror deploy

* fix: update hca factory interface selector

* fix: restore contract namer deploy

* fix: avoid spaced hca coverage name

* chore: revert coverage script change

* fix: skip mainnet hca owner setup

* chore: allow manual docker image branch builds

* chore: quote github actions trigger key

* Add test for emancipation concern (ensdomains#327)

* Finish Contract Naming (ensdomains#328)

- named `ReverseRegistrarHCAAdapter`
- named `DefaultReverseRegistrarHCAAdapter`
- named `MigrationHelper`
- added upgrade test for `ContractNamer`

* Emit `ParentUpdated` during `WrapperRegistry.initialize()` (ensdomains#324)

* Change `WrapperRegistry` to use "virtual owner" (ensdomains#331)

- changed `WrapperRegistry` to have at most one account with admin
    * roles granted to "virtual owner" — actual owner is parent registry
    * `_msgSender()` remaps from token owner to "virtual owner"
- changed `WrapperRegistry` to respect frozen `NameWrapper` fuses for `ROLE_RENEW`, `ROLE_UPGRADE`, and `ROLE_CAN_NAME` (like `ROLE_REGISTRAR`)

* Utilize `PermissionRegistry.setApprovalForAll()` in `EAC` (ensdomains#333)

- changed `EnhancedAccessControl`
    * added virtual `_getRoles(resource, account): roles` which permits logic <ins>before</ins> permission checks
    * added private `_effectiveRoles(resource, account)` which is root + resource
    * refactored to utilize those functions
- changed `PermissionedRegistry`
    * overrode `_getRoles()` to account for token expiry and approval 
    * simplified `_getSettableRoles()` and `_getRevokableRoles()` since `_checkedRoles()` already handles expiry
- changed `WrapperRegistry`
    * removed `_msgSender()` hack
    * removed `IContractNamer` override
    * overrode `_getRoles()` to account for virtual owner — applies <ins>after</ins> expiry and approval checks, so approvals are not virtualized and do not survive transfer

---

* `EAC` reverts use the caller address
* `PermissionRegistry.roles()` returns the same roles for the token owner AND virtual owner AND approved
* `test_revokeRoles_whileReserved()` returns false instead of reverting
* `test_revokeRoles_whileExpired()` returns false instead of reverting

* Remove HCA (ensdomains#332)

- removed all instances of HCA
- replaced all `Context._msgSender()` with `msg.sender`
- simplified reverse adapters
- added `ERC1155Singleton._checkApproved()`
- simplified `WrapperRegistry._getRoles()`

* add permit support

* cleanup

* derp

* More `PermissionedRegistry` Tests (ensdomains#334)

- added more `PermissionedRegistry` tests
- added more `WrapperRegistry` tests

* Add `setters[]` to `PermissionedResolver.initialize()` (Redo) (ensdomains#336)

- added `bytes[] calldata setters` to `initialize()`
- removed `InvalidOwner` revert

* fix(ci): repair codecov pr uploads

* fix(migration): preserve CAN_EXTEND_EXPIRY when migrating emancipated names [NM-0919] (ensdomains#343)

* fix(migration): preserve CAN_EXTEND_EXPIRY when migrating emancipated names

Grant ROLE_RENEW and ROLE_RENEW_ADMIN to the owner when an emancipated
(non-locked) wrapped name with the CAN_EXTEND_EXPIRY fuse is migrated,
so the v1 self-renewal right carries over to v2. Previously the fixed
registration bitmap silently dropped it, leaving only the parent owner
able to renew the name.

* style(migration): apply solgrid formatting

* Fix `ETHRenewerV1.syncWrapper()` juggle (ensdomains#344)

- bumped `lib/ens-contracts` (deploy fix)
- changed `ETHRenewerV1`
     * takes `NameWapper` instead of `BaseRegistrar`
     * juggles `NameWrapper` instead of `WrappedController`
     * fixed `syncWrapper()` test
- added test to `migration.test.ts` for `syncWrapper()`

* Fix `RegistryRolesLib` comments (ensdomains#345)

* Improved Coverage (ensdomains#339)

- changed from `365.25 days` to `365 days` for pricing
- canonicalized `ROLE_CAN_NAME` for `IContractNamer` (some were `ROLE_SET_NAME`)
- changed `Graveyard`
     * fixed `clear()` logic for locked parent 
     * added `error NameRequiresPreimage()`
- changed `ERC1155Singleton`
     * fixed `balanceOf(address(0), *) == 1`
- improved test coverage: `97.88%` &rarr; `99.92%`

* Fix `WrapperRegistry` prevents revive if `CANNOT_CREATE_SUBDOMAIN` (ensdomains#346)

* poc

* test: cover WrapperRegistry revive success path

Add positive-path coverage for WrapperRegistry._canRevive: when
CANNOT_CREATE_SUBDOMAIN is not burned, a root role holder can revive an
expired child. This exercises super._canRevive, closing the codecov patch
gap that was failing CI.

* ci: make per-flag codecov patch status informational

Per-flag patch statuses fail on cross-cutting changes when a flag's test
suite includes the changed files in its report but does not exercise the
changed lines. Mark per-flag patch as informational so it reports without
blocking; the aggregate patch gate still enforces coverage.

---------

Co-authored-by: Ramesh Nair <ram@hiddentao.com>

* fix typo (ensdomains#348)

* Add comment (ensdomains#349)

* Prevent `Graveyard.clear()` on unregistered .eth (ensdomains#351)

- check `nameExpires()` before `register()`
- added tests

* Bump verifiable-factory (ensdomains#352)

* Bump verifiable-factory

* Align foundry.lock with bumped verifiable-factory

foundry.lock still pinned lib/verifiable-factory to 04ec76f (old
two-argument verifyContract API) while the submodule gitlink and
UserRegistry.t.sol now require 5ef7b1a (one-argument API). Setups that
hydrate Foundry deps from the lock file would restore the old revision
and fail to compile. Update the lock entry to match the gitlink.

* Fix `PermissionRegistry._getSettableRoles()` when expired (ensdomains#353)

- added `getOwner(anyId): owner`
- refactored `findOwner(label): owner` to use `getOwner(...)`
- changed `_getSettableRoles(...)` to check if registered
- fixed `_randomRoleBitmap(...)` for better fuzzing — a typical iteration wouldn't set a single bit and `(true, true)` would always set 2+ bits
- avoid checking `PUBLIC_RESOLVER_SET.includes(...)` if `resolver` is unset
- avoid checking `isApprovedForAll()` if `owner == account`

* Add `ETHRenewerV1.setRegistrarResolver()` (ensdomains#355)

In ENSv2, `ETHRenewer` <ins>must</ins> own the ENSv1 `BaseRegistrar` to perform the controller juggle, see: `syncWrapper()`.

`BaseRegistrar.setResolver()` uses `onlyOwner` instead of `onlyController` so only the singular owner can call this function.

Effectively, `ETHRenewerV1` is a `RegistrarSecurityController` without any `BaseRegistrar` controller management (since ENSv1 is disabled.)

---

`ETHRenewerV1` already has `transferRegistrarOwnership()`, this PR adds `setRegistrarResolver()`, which makes changing the ENSv1 resolver for "eth" simple.  Without this change, the following steps would be required:
1. `ETHRenewerV1.transferRegistrarOwnership(DAO)`
1. `BaseRegistrar.setResolver(<resolver>)`
1. `BaseRegistrar.transferOwnership(ETHRenewerV1)`

---

**Usage**: the ENSv1 "eth" resolver should be set to the `ENSV2Resolver` at launch, and cleared (via this function) once the initial migration period is over (TBD).

* feat: migration/sepolia deploy updates (core) (ensdomains#342)

* feat: sepolia deploy snapshot (core)

* style: format solidity

* fix: read hardhat coverage inputs from build info

* fix: repair canonical parent on ethregistry reruns

* feat: add migration rehearsal harness

* fix: add v1 controller grant for testnet premigration registrar

* chore: track sepolia deployment records

* fix(migration): address Codex review feedback on PR ensdomains#342

- TestnetV1PremigrationRegistrar: apply resolver records while the registrar still owns the node, then hand off ownership (ensdomains#5); set reverse records for the registrant rather than the caller (ensdomains#3); add the continuity bonus to every v2 reservation (ensdomains#7).

- Deploy the testnet registrar as a controller of both reverse registrars so the owner-targeted reverse records are authorized (#1).

- Reverse-registrar adapters resolve their v1 contracts via getV1 (ensdomains#9).

- Treat sepolia-dev as sepolia for the known top URP and v1 deployment lookups (ensdomains#6, ensdomains#11).

- Run the v1 root-TLD mirror before DNSTLDResolver so root TLDs keep their v1 fallback (ensdomains#12).

* fix(migration): preserve network v1 owner default; revoke suffix roles on failure

Address outstanding Codex review feedback on PR ensdomains#342:

- resolveMigrationSigners no longer defaults v1Owner to the migration
  owner when --v1-owner is omitted. Leaving it unset lets the deploy
  logic fall back to the network-configured v1 owner, so v1-owner
  transactions (and the deferred JSONL `from`) target the real v1 owner
  on sepolia/mainnet instead of the migration/Hardhat account.
- registerSuffixesViaBatchRegistrar wraps the batch loop in try/finally
  so the temporary REGISTRAR|RENEW root roles are always revoked, even
  if a suffix batch registration reverts partway through.

* test(e2e): gate live external DNS resolution tests behind env flag

The taytems.xyz / raffy.xyz resolution cases resolve real third-party DNS
records through the live dnssec-oracle.ens.domains CCIP gateway, so they
fail in CI whenever that gateway or those external records are unavailable
(independent of any contract change). Gate them behind RUN_LIVE_DNS_TESTS
so CI stays deterministic; the onchain .ens.eth DNS cases still run.

* fix(migration): require an output file when deferring v1-owner transactions

Deferring v1-owner transactions without an output file silently dropped the
calldata: only a one-line summary was logged, the JSONL append was skipped,
and a fake-success receipt was returned, so phase-1 could appear to succeed
while v1-owner actions were neither broadcast nor saved for execute-owner-txs.
Fail fast when deferring is requested without a file so the calldata is always
persisted for replay.

* fix(premigration): adopt post-audit bonus-days reservation model

Replace the continuity-expiry pre-migration model with the audited
post-audit version: each v2 reservation expiry is the name's v1 expiry
plus a configurable --bonus-period-days (default 62), gated on the v1
90-day grace period. Drops the --min-expiry-days and
--skip-existing-reservations flags and the hardcoded continuity bonus.

Reconcile the feat-only consumers to the new API:
- migration.ts: imports, inline reserve/verify logic, phase 2/4 wiring,
  and premigration run/verify CLI options switch to --bonus-period-days
- premigration-run Hardhat task and plugin options
- migration.md phases 2 & 4

Retain --account (impersonation signer) in preMigration.ts since the
fork rehearsal harness drives pre-migration via impersonated accounts;
it is orthogonal to the expiry model.

* fix(premigration): support multicall on non-mainnet chains

resolveChain now returns viem's sepolia chain for chain id 11155111 and
attaches the canonical Multicall3 address to synthesized custom chains, so
preMigration's batched multicall no longer fails with
ChainDoesNotSupportContract on Sepolia and other non-mainnet networks.

Also document running pre-migration against a Sepolia fork (including the
fresh-deployer requirement for EIP-7702-delegated default accounts) and
ignore the local .dev scratch directory.

* feat(migration): support fork-full rehearsal against already-migrated chains

Make the phased migration fork rehearsal work against a chain where a
prior v1->v2 hand-off has already happened (e.g. current Sepolia), while
still running the full from-scratch rehearsal on a pristine chain
(mainnet today, or a repeat mainnet run after a bug-fix redeploy).

- Detect a completed v1 hand-off from on-chain controller state and skip
  the live v1 registration smokes when v1 registration is already
  disabled; a pristine chain still exercises them.
- Impersonate the v1 owner during the in-process phase-1 deploy so the
  resolver hand-off write has a signer on the fork.
- Derive the URP cut-over baseline from the canonical top proxy's live
  implementation instead of a static deployment reference, so the managed
  proxy is seeded transparently and verification matches current state.
- Pass the Graveyard address through from the in-memory deploy instead of
  reading it from disk, so phase 8 works without saved deployments.
- Consolidate the repeated fork chain setup into a helper.

* feat(migration): support live re-migration and address review feedback

Add the ability to re-run the migration on a chain that has already been
migrated once (important for the live migration, not just fork rehearsals),
and resolve the open Codex review threads on PR ensdomains#342.

Live re-migration:
- Reclaim v1 BaseRegistrar ownership from a prior deployment's ETHRenewerV1
  back to the v1 owner (via the prior renewer's transferRegistrarOwnership,
  signed by its own owner) so the EOA-signed handoff phases can run again.
  Wired into the fork rehearsal before phase 8 and exposed as a standalone
  `phase reclaim-v1-registrar-ownership` command for live operators.

Review feedback:
- Align the testnet premigration registrar's reservation expiry with the
  bonus-days verifier by dropping the stray extra second.
- Route the v1 reverse/default-reverse adapter controller grants through the
  v1 owner instead of the v2 admin.
- Only attach an owner/urManager private key during phase deploy when it
  controls the resolved account, so an explicit or DAO owner is not replaced
  by the deployer key; mirror the fix for the Hardhat deploy-v2 task.
- Keep the free-mint mock payment tokens off mainnet and whitelist real
  mainnet USDC/DAI in the rent price oracle instead.

Docs:
- Document the post-migration auto-detection and the narrowed
  `--calldata-only` scope; refine the universal resolver setup notes.

* fix(migration): handle real payment tokens and clean-testnet owner key

Address two Codex review findings that follow from the mainnet token and
phase-deploy owner-key changes:

- Only attach an owner/urManager/v1Owner private key in the clean-testnet
  path when it controls the resolved account, so an explicit owner is not
  signed for by the deployer key (mirrors the phase deploy-v2 fix).
- Make the mock payment token optional: gate the paid v2-registrar smokes
  on a mintable token and verify the phase-9 role grant directly where
  none exists (mainnet, which whitelists real USDC/DAI), instead of
  assuming a free-mint mock.

* refactor(migration): reorder phases and authorize v1 renewer early

Restructure the v1 → v2 migration from nine phases to seven so unmigrated
names stay renewable throughout the migration window and public resolution
flips only once everything else is live:

- authorize ETHRenewerV1 as a v1 controller early (new phase 4) instead of
  at final hand-off, splitting it from the BaseRegistrar ownership transfer
- run the final pre-migration sync afterwards (phase 5) so renewed expiries
  are picked up
- bundle the v2-controller steps (disable BatchRegistrar, authorize handoff
  controllers, transfer v1 ownership to ETHRenewerV1, enable v2 ETHRegistrar)
  into phase 6
- move the Universal Resolver cutover to phase 7, run last
- deploy the reverse-registrar adapters as part of phase 1 and tag their
  deploy scripts with migration:phase1:deploy-v2
- reclaim v1 registrar ownership before the early renewer authorization on
  re-migration runs

Update docs/migration.md and console phase labels to match.

* feat(migration): add --fresh flag to redeploy v2 over an archived namespace

phase deploy-v2 is idempotent against its deployment namespace, so re-running
into a populated deployments/<env>/ reuses the existing contracts instead of
deploying fresh. Add an opt-in --fresh flag that archives the current namespace
to <env>-<YYYYMMDD>-r<N> (date from the archived set's recorded deploy time,
with an auto-incrementing revision) and deploys fresh into the clean namespace.
--fresh implies --save-deployments and works for both sepolia and mainnet.

Record a .deployment.json metadata file in each saved namespace capturing the
original deploy time; the archiver reads it back (falling back to the latest
.migrations.json timestamp) to name the archived folder.

Track deployments/README.md and document the namespace layout, the --fresh
workflow, the git-tracking model, and the v1 override layer.

* docs(migration): add live Sepolia deployment runbook

Document the end-to-end fresh Sepolia deployment using phase deploy-v2 --fresh
followed by phases 2-7, including the deferred v1-owner transaction flow. Explain
the three signer keys (DEPLOYER_KEY, SEPOLIA_V1_OWNER_KEY, SEPOLIA_TOP_URP_OWNER_KEY)
and which roles and phase commands each one covers, plus the renewal-gap and
pre-migration caveats and the mainnet multisig difference.

* test(e2e): gate flaky live-mainnet v1 verification behind opt-in flag

The two preMigration verification tests that read live mainnet state were
hardcoded to a free public RPC and flaked in CI on request timeouts. Gate them
behind RUN_LIVE_MAINNET_TESTS (matching the existing RUN_SEPOLIA_FORK_MIGRATION_TEST
and RUN_LIVE_DNS_TESTS conventions) and make the RPC configurable via
MAINNET_RPC_URL. The input-validation cases, which make no network calls, keep
running unconditionally.

* fix(migration): skip env signer fallback when impersonated account differs

runPreMigrationCommand unconditionally resolved the batch signer from the
env key chain even when the caller passed an impersonated account, then
forwarded both --private-key and --account to preMigration (which prefers
the key). When the env deployer key did not control the BatchRegistrar
owner, the batch run signed as the wrong address and onlyOwner reverted
despite the account being impersonated.

Only consult the env fallback key when no account was supplied, or when it
actually controls that account; otherwise forward the impersonated account
alone. Add a focused test covering each signer-resolution branch.

* refactor(migration): make phase deploy-v2 fresh by default with --resume

Replace the deploy-v2 --fresh / --save-deployments flags with fresh-as-default
behavior plus a --resume flag to continue an interrupted deploy into the
existing namespace. Drop the hardcoded phase-1 transaction-count figure from
the docs.

* feat(migration): reuse existing intermediate URP instead of deploying a new top URP

Treat the top URP and intermediate (managed) URP as pre-existing fixtures
and reuse them where they already exist. A fresh v2 deployment on a network
whose top URP already fronts an intermediate URP we administer now performs
a single on-chain mutation — upgrading the intermediate URP to the new
implementation — leaving the externally-administered top URP untouched.

- add KNOWN_INTERMEDIATE_URP map and adopt it in the managed-URP deploy step,
  falling back to deploy-fresh on bootstrap networks
- stop deploying a fresh top URP (adopt the canonical address only; clean-testnet
  still builds its own self-owned stack)
- skip the top-URP setup steps when the top URP already serves an implementation
  or already fronts the intermediate URP
- make switchTopUrpToManaged idempotent and skip the switch in the cutover when
  reuse is already in place
- wire securityCouncil/urManager to the intermediate URP admin per network
- extract a shared knownProxyNetworkName helper
- update universal resolver and migration docs for the reuse vs bootstrap split

* feat(migration): commit fresh sepolia v2 deployment + fix live deploy signing

Deploy the fresh v2 contract set into deployments/sepolia/ and track all
sepolia* namespaces (live + archived) so real deploys are committed.

Fixes surfaced during the live Sepolia run:
- loadDotEnv: strip inline `#` comments and surrounding whitespace from
  unquoted values (an inline comment previously corrupted private keys);
  preserve `#` inside quoted values.
- phase deploy-v2: wire the v1Owner account to a local key when one controls
  it, so a keyless v1Owner sharing the deployer address can no longer shadow
  the deployer's signer and force node-side signing that a remote RPC rejects.
- docs: add reclaim-v1-registrar-ownership to the Phase 1 runbook (before the
  deferred-tx replay on an already-migrated chain) and the CLI reference, plus
  deployer/.env hygiene notes.

* chore(deployments): track real deploy namespaces by default

Invert the deployments .gitignore from allow-list to deny-list: track live and
dated-archive namespaces by default and ignore only the local rehearsal/runtime
ones (*-fork, *-clean-*). The previous blanket `deployments/*` ignore caused
editors to dim the committed sets even though they were force-tracked.

* fix(migration): address Codex review findings (mainnet deploy + signer/CSV correctness)

Make the migration tooling mainnet-capable and fix signer/CSV correctness issues
flagged in PR review.

Mainnet deploy:
- Reverse-registrar adapters: drop the mainnet early-return so the v1-owner
  setController grant is recorded as a deferred owner tx instead of being silently
  skipped.
- Defer owner/DAO writes (not just v1-owner) during deploy when owner differs from
  the deployer, so PublicResolverSet.approve is replayable on mainnet.
- Populate the real mainnet wrapper-aware PublicResolverV3/V4 addresses.

Signer handling:
- set-v1-reverse-default-resolver: resolve the v1-owner key (not the deployer),
  add a phase CLI command with --calldata-only and an owner guard.
- phase deploy-v2: honour an explicit --deployer instead of overriding it with
  DEPLOYER_KEY.
- clean-testnet phase-0 deployV1: only attach a key that controls v1Owner.
- Standalone fork-full/clean-testnet CLIs: hydrate env signer keys on the
  non-state-control path and detect Tenderly virtual RPCs (shared helper).

CSV / verify:
- Drop TheGraph rows with no decodable labelName before export.
- Verify only asserts the premigration fallback resolver for RESERVED names.
- Normalize the label-column header case-insensitively (throwing when absent)
  and CSV-escape labels when rewriting the premigration CSV.
- Reclaim skips the live RegistrarSecurityController instead of treating it as a
  prior renewer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1WiTqnbW7bLbqejYkyBN1

* feat(migration): add contract source-code verification script

Add a dedicated verify script that submits deployed contracts to Etherscan
and Sourcify via @rocketh/verifier. The verifier rebuilds the solc
standard-JSON input from each artifact's recorded metadata, which needs the
literal source content of every file; hardhat-compiled artifacts embed it but
forge-compiled ones record only hashes and URLs. The script backfills missing
content from disk (checked against the recorded hash) before submitting, so a
contract verifies regardless of which compiler produced its artifact.

Wire up verify / verify:sepolia / verify:mainnet npm scripts and document the
step in the migration runbook.

* docs(migration): tighten and correct migration guides

- update phased-deploy role-grant reference from phase 9 to phase 6
- condense premigration and prepareMigration docs, trimming verbose sections
- document fresh-by-default deploy behaviour for phase deploy-v2
- align README usage examples with current flow

* fix(migration): correct stale phase-number reference in comment

The reclaimV1RegistrarOwnership comment referenced non-existent
'controller phases (8 and 9)'; the migration has only phases 1-7.
Reword to describe the owner-signed v1 controller steps conceptually.

* refactor(migration): extract owner-gated write helpers

Collapse the repeated owner-read + key-guard + impersonate + wallet +
write + receipt skeleton into resolveOwnerGatedWallet / sendOwnerGatedWrite
(owner()-gated) and sendAdminWrite (account passed in). Apply across the v1
registrar/reverse/renewer writes, the URP switches, and the registrar
enable/disable. The disable-v1-registrars loop now shares the same wallet
resolver, gaining the key/owner guard it previously lacked.

* refactor(migration): dedupe orchestrator signer and smoke blocks

Compute the v1-owner signer once (v1OwnerSigner) and spread it at the four
controller-change phases instead of repeating the impersonate/key ternary.
Collapse the three v1 smoke register+assert pairs into registerSmokeV1 /
assertSmokeV1Owner closures over the shared context.

* refactor(migration): add registry/role read helpers

Extract resolveRegistry (explicit address or deployment artifact) and
readHasRegistrarRoles, replacing the duplicated registry/abi resolution and
hasRootRoles reads in the v2 registrar enable/disable/verify paths and
removing the registry! non-null assertions.

* refactor(migration): reduce CLI handler boilerplate

Add withNetworkRpc (parse network + resolve rpc url) and route all 24
action handlers through it, dropping the repeated requireRpcUrl call.
Add v1OwnerKeyFromEnv for the v1-owner key env fallback and addV1OwnerWriteOptions
for the shared private-key/impersonate-owner/calldata-only option trio,
applied across the v1-owner-gated phase commands.

* refactor(migration): share deploy provider/chain preamble and address logging

Extract resolveDeployProviderAndChain (provider shim, chain-id override,
chain resolution) and logDeployedAddresses, replacing the byte-for-byte
identical preambles and print loops in deployV1 and deployV2.

* fix(migration): make upgrade-managed-urp idempotent

When reusing a managed URP that already fronts the target UniversalResolverV2
(e.g. a deterministic redeploy lands the same implementation address), the
proxy reverts upgradeTo with SameImplementation. Skip the upgrade when the
managed URP is already at the target implementation, mirroring the existing
'top URP already fronts managed URP' guard in switchTopUrpToManaged. Surfaced
by a fork full sepolia rehearsal re-run against an already-migrated namespace.

---------

Co-authored-by: Ramesh Nair <ram@hiddentao.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(migration): auto-generate per-network address docs; note Sepolia URP rollback (ensdomains#361)

* docs(readme): note temporary Sepolia intermediate-URP rollback

Record that the intermediate URP has been repointed from the fresh
deployments/sepolia stack to the previous sepolia-official-v1-20260525-r2
UniversalResolverV2, restoring v1-name resolution through the public
entrypoint, with the revert command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYSpMNUyiWpnmB1FSJUErH

* feat(migration): auto-generate contract address docs per network

Add a shared generator that renders deployment namespaces as Etherscan-linked
markdown tables under docs/addresses/<network>.md. Wire it into the end of
phase deploy-v2 (persisted deploys only) and expose a standalone
`bun run docs:addresses` CLI. Link the tables from the README and move the
temporary managed-URP operational note alongside them. Generate the current
Sepolia table.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* patch coverage below 90% is shown but never fails (ensdomains#364)

* Add `WrapperRegistry._getRoles()` support for virtual owner approvals (ensdomains#356)

- changed `WrapperRegistry._getRoles()` to check `parent.isApprovedForAll()`

* Revert "Add `WrapperRegistry._getRoles()` support for virtual owner approvals (ensdomains#356)"

This reverts commit 39457d8.

---------

Co-authored-by: tate <tate@ens.domains>
Co-authored-by: Ram <ram@hiddentao.com>
Co-authored-by: v1rtl <hi@v1rtl.site>
Co-authored-by: TateB <yo@taytems.xyz>
Co-authored-by: Makoto Inoue <2630+makoto@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JOY
JOY (JOY) merged commit 9bb45b0 into dos Aug 4, 2026
6 checks passed
@JOY
JOY (JOY) deleted the codex/ensv2-dos-port branch August 4, 2026 13:53
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.

6 participants