Skip to main content
The NameWrapper includes a built-in upgrade mechanism that allows for migration to a new version of the contract. This guide covers the upgrade process and requirements.

Upgrade Overview

The NameWrapper has upgrade functionality that allows the contract owner to specify a new NameWrapper contract for migration. This serves as a last resort migration path for users.
Upgrading a name is optional and can only be done by the owner of the name in the original NameWrapper.

How Upgrades Work

Key Principles

  1. Owner-Initiated - Only name owners can migrate their names
  2. Parent-First - A name can only be migrated after its parent has been migrated
  3. Root Nodes Pre-Wrapped - ROOT_NODE and ETH_NODE are wrapped in the new constructor
  4. Preserves State - Ownership and fuse states are maintained during migration

Upgrade Flow

Setting the Upgrade Contract

The NameWrapper owner can specify the upgrade target:
// Set the upgrade contract address
await nameWrapper.setUpgradeContract(newWrapperAddress)
Only the owner of the NameWrapper contract can set the upgrade contract address.

Implementing a New NameWrapper

The upgraded NameWrapper must implement the INameWrapperUpgrade interface:

Required Interface

interface INameWrapperUpgrade {
    function wrapETH2LD(
        string calldata label,
        address wrappedOwner,
        uint16 ownerControlledFuses,
        address resolver
    ) external returns (uint64 expiry);
    
    function setSubnodeRecord(
        bytes32 parentNode,
        string calldata label,
        address owner,
        address resolver,
        uint64 ttl,
        uint32 fuses,
        uint64 expiry
    ) external returns (bytes32 node);
}

Using Existing Functions

  • wrapETH2LD - Can be used as-is from the existing NameWrapper
  • setSubnodeRecord - Needs additional permission check

Additional Permission Check

The setSubnodeRecord function requires an extra check for migrations:
// Check if parent is already wrapped AND caller is old wrapper
require(
    isTokenOwnerOrApproved(parentNode) || 
    (msg.sender == oldWrapperAddress && registrar.ownerOf(parentLabelHash) == address(this)),
    "Unauthorized"
);
Place this check after normal authorization checks to avoid additional gas costs for regular usage.

Migration Process

1

Deploy new NameWrapper

Deploy the upgraded NameWrapper contract with improvements:
// Constructor wraps ROOT_NODE and ETH_NODE by default
constructor(
    ENS _ens,
    BaseRegistrar _registrar,
    IMetadataService _metadataService
) {
    // ... initialization
    // Wrap ROOT_NODE and ETH_NODE
}
2

Set as upgrade target

The current NameWrapper owner sets the new contract:
const tx = await oldNameWrapper.setUpgradeContract(newWrapperAddress)
await tx.wait()
3

Migrate parent names first

Name owners must migrate from parent to child:
// Migrate .eth second-level name
const tx = await oldNameWrapper.upgrade(
    ethers.utils.namehash('example.eth'),
    'example'
)
await tx.wait()
4

Migrate subdomains

After parent migration, subdomain owners can migrate:
// Migrate subdomain
const tx = await oldNameWrapper.upgrade(
    ethers.utils.namehash('sub.example.eth'),
    'sub'
)
await tx.wait()

Function Signature Compatibility

If the new NameWrapper changes function signatures:

Add Compatibility Layer

// Add wrapper functions for old signatures
function wrap(
    bytes32 parentNode,
    bytes calldata name,
    address wrappedOwner,
    address resolver
) external {
    // Translate to new signature
    _wrapWithNewSignature(parentNode, name, wrappedOwner, resolver);
}

function wrapETH2LD(
    string calldata label,
    address wrappedOwner,
    uint16 ownerControlledFuses,
    address resolver
) external returns (uint64 expiry) {
    // Translate to new signature if needed
    return _wrapETH2LDWithNewSignature(label, wrappedOwner, ownerControlledFuses, resolver);
}
This ensures the old NameWrapper can call the new one even if signatures change.

Upgrade Requirements

New NameWrapper Must:

  • ✅ Implement INameWrapperUpgrade interface
  • ✅ Include additional permission check in setSubnodeRecord
  • ✅ Wrap ROOT_NODE and ETH_NODE in constructor
  • ✅ Provide compatibility layer if function signatures change
  • ✅ Maintain existing fuse functionality (or enhance it)
  • ✅ Preserve name ownership and state during migration

Old NameWrapper Must:

  • ✅ Have upgrade contract set by owner
  • ✅ Allow name owners to call upgrade function
  • ✅ Verify parent is migrated before allowing subdomain migration

Migration Validation

After migration, verify:
// Check ownership in new wrapper
const newOwner = await newNameWrapper.ownerOf(namehash('example.eth'))
console.log('New owner:', newOwner)

// Check fuses are preserved
const [owner, fuses, expiry] = await newNameWrapper.getData(namehash('example.eth'))
console.log('Fuses:', fuses)
console.log('Expiry:', expiry)

// Verify in ENS registry
const registryOwner = await registry.owner(namehash('example.eth'))
console.log('Registry owner:', registryOwner) // Should be new wrapper

User Communication

When performing an upgrade:
1

Announce upgrade

Communicate the upgrade to users well in advance:
  • Explain reasons for upgrade
  • Outline new features or fixes
  • Provide timeline for upgrade
  • Detail migration process
2

Provide migration tools

Create user-friendly tools:
  • Web interface for migration
  • Batch migration for multiple names
  • Status checker for migration progress
3

Support users during migration

  • Provide documentation
  • Offer support channels
  • Monitor for issues
  • Assist with troubleshooting
4

Verify completeness

  • Track migration progress
  • Identify unmigrated names
  • Reach out to inactive owners

Rollback Considerations

Upgrades are one-way by default. Once a name is migrated to the new wrapper, it cannot automatically return to the old one.
To support rollback:
  1. The new wrapper could implement a “downgrade” function
  2. Requires careful design to prevent abuse
  3. Should have time limits or governance controls

Testing Upgrades

Fork Testing

Test the upgrade on a mainnet fork:
# Start fork
anvil --fork-url https://mainnet.infura.io/v3/YOUR_KEY

# Deploy new wrapper
bun run hh run scripts/deploy-new-wrapper.ts --network localhost

# Test migration
bun run hh run scripts/test-migration.ts --network localhost

Testnet Deployment

Deploy and test on testnet first:
# Deploy to Sepolia
bun hh --network sepolia deploy

# Test migration with actual users
bun run hh seed --network sepolia test-migration

Security Considerations

  • Audit the new NameWrapper contract
  • Test all migration paths
  • Verify permission checks work correctly
  • Ensure no funds or names can be lost
  • Only name owners can migrate their names
  • Verify parent-first migration enforcement
  • Check for reentrancy vulnerabilities
  • Ensure fuses are preserved or enhanced
  • Prevent fuse state manipulation during migration
  • Verify expiry times are maintained
  • Expired names
  • Names with maximum fuses burned
  • Deeply nested subdomains
  • Concurrent migrations

Example: Complete Upgrade Flow

import { ethers } from 'ethers'
import { namehash } from 'ethers/lib/utils'

// 1. Deploy new NameWrapper
const NewNameWrapper = await ethers.getContractFactory('NameWrapperV2')
const newWrapper = await NewNameWrapper.deploy(
    registryAddress,
    registrarAddress,
    metadataAddress
)
await newWrapper.deployed()

// 2. Set as upgrade target on old wrapper
const oldWrapper = await ethers.getContractAt('NameWrapper', oldWrapperAddress)
const setTx = await oldWrapper.setUpgradeContract(newWrapper.address)
await setTx.wait()

// 3. Migrate a .eth name
const migrateTx = await oldWrapper.upgrade(
    namehash('example.eth'),
    'example'
)
await migrateTx.wait()

// 4. Verify migration
const newOwner = await newWrapper.ownerOf(namehash('example.eth'))
console.log('Migration successful! New owner:', newOwner)

// 5. Migrate subdomain (parent must be migrated first)
const subTx = await oldWrapper.upgrade(
    namehash('sub.example.eth'),
    'sub'
)
await subTx.wait()

Migration Guide

Overall migration and release process

NameWrapper Docs

NameWrapper contract documentation

Deployment

Deploying new contracts

Testing

Testing upgrade procedures

Build docs developers (and LLMs) love