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
Owner-Initiated - Only name owners can migrate their names
Parent-First - A name can only be migrated after its parent has been migrated
Root Nodes Pre-Wrapped - ROOT_NODE and ETH_NODE are wrapped in the new constructor
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
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
}
Set as upgrade target
The current NameWrapper owner sets the new contract: const tx = await oldNameWrapper . setUpgradeContract ( newWrapperAddress )
await tx . wait ()
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 ()
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:
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
Provide migration tools
Create user-friendly tools:
Web interface for migration
Batch migration for multiple names
Status checker for migration progress
Support users during migration
Provide documentation
Offer support channels
Monitor for issues
Assist with troubleshooting
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:
The new wrapper could implement a “downgrade” function
Requires careful design to prevent abuse
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
Verify upgrade contract thoroughly
Audit the new NameWrapper contract
Test all migration paths
Verify permission checks work correctly
Ensure no funds or names can be lost
Prevent unauthorized migrations
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