Overview
ENS v2 uses Rocketh (built on Hardhat Deploy) for managing deployments. The deployment system:
Executes scripts in dependency order
Saves deployment artifacts for reproducibility
Supports network-specific configurations
Enables deployment verification and upgrades
Deployment Architecture
Deployments are organized in the deploy/ directory with numbered prefixes indicating execution order:
deploy/
├── 00_*.ts # Core infrastructure (factories, root)
├── 01_*.ts # Registry implementations and metadata
├── 02_*.ts # Registrars and price oracles
├── 03_*.ts # Advanced registries (wrapper, user)
└── universalResolver/
├── 00_*.ts # Resolver implementation
└── 01_*.ts # Upgradeable proxy
Deployment Order
Phase 0: Core Infrastructure
HCAFactory: Hierarchical Contract Access factory
VerifiableFactory: UUPS proxy factory
RootRegistry: ENS v2 root registry
Resolver implementations (ENSV1, ENSV2, DNS resolvers)
DNSSECGatewayProvider: DNSSEC verification
ETHReverseRegistrar: Reverse name registration
Phase 1: Registry Layer
SimpleRegistryMetadata: Metadata provider
PermissionedResolverImpl: Resolver implementation
UserRegistryImpl: User registry implementation
ETHRegistry: .eth TLD registry
ReverseRegistry: Reverse resolution registry
DNSTLDResolver, DNSAliasResolver: DNS resolvers
Phase 2: Registrars and Oracles
StandardRentPriceOracle: Dynamic pricing
MockTokens: Test ERC20 tokens (USDC, DAI)
ETHReverseResolver: Reverse resolver for ETH addresses
Phase 3: Advanced Features
ETHRegistrar: .eth name registrar with commit-reveal
WrapperRegistryImpl: Wrapper registry for v1 compatibility
Phase 4: Universal Resolver
UniversalResolverV2: Universal resolution implementation
Upgradeable proxy for UniversalResolver
Deployment Scripts
Script Structure
All deployment scripts follow this pattern:
import { artifacts , execute } from "@rocketh" ;
import { ROLES } from "../script/deploy-constants.js" ;
export default execute (
async ({ deploy , execute : write , get , namedAccounts : { deployer , owner } }) => {
// Get dependencies
const dependency = get < ArtifactABI >( "DependencyName" );
// Deploy contract
const contract = await deploy ( "ContractName" , {
account: deployer ,
artifact: artifacts . ContractArtifact ,
args: [
// Constructor arguments
],
});
// Post-deployment setup
await write ( contract , {
account: deployer ,
functionName: "initialize" ,
args: [ /* initialization args */ ],
});
} ,
{
tags: [ "ContractName" , "l1" ] ,
dependencies: [ "DependencyName" ] ,
}
) ;
Example: RootRegistry Deployment
From deploy/00_RootRegistry.ts:
import { artifacts , execute } from "@rocketh" ;
import { ROLES } from "../script/deploy-constants.js" ;
export default execute (
async ({ deploy , get , namedAccounts : { deployer } }) => {
const hcaFactory = get < typeof artifacts . MockHCAFactoryBasic . abi >( "HCAFactory" );
const registryMetadata = get < typeof artifacts . SimpleRegistryMetadata . abi >(
"SimpleRegistryMetadata"
);
await deploy ( "RootRegistry" , {
account: deployer ,
artifact: artifacts . PermissionedRegistry ,
args: [
hcaFactory . address ,
registryMetadata . address ,
deployer ,
ROLES . ALL // Grant all roles to deployer initially
],
});
} ,
{
tags: [ "RootRegistry" , "l1" ] ,
dependencies: [ "HCAFactory" , "RegistryMetadata" ] ,
}
) ;
Example: ETHRegistry with Setup
From deploy/01_ETHRegistry.ts:
import { artifacts , execute } from "@rocketh" ;
import { zeroAddress } from "viem" ;
import { MAX_EXPIRY , ROLES } from "../script/deploy-constants.js" ;
export default execute (
async ({ deploy , execute : write , get , namedAccounts : { deployer } }) => {
const rootRegistry = get < typeof artifacts . PermissionedRegistry . abi >( "RootRegistry" );
const hcaFactory = get < typeof artifacts . MockHCAFactoryBasic . abi >( "HCAFactory" );
const registryMetadata = get < typeof artifacts . SimpleRegistryMetadata . abi >(
"SimpleRegistryMetadata"
);
// Deploy ETH registry
const ethRegistry = await deploy ( "ETHRegistry" , {
account: deployer ,
artifact: artifacts . PermissionedRegistry ,
args: [ hcaFactory . address , registryMetadata . address , deployer , ROLES . ALL ],
});
// Register .eth in root registry
await write ( rootRegistry , {
account: deployer ,
functionName: "register" ,
args: [
"eth" , // label
deployer , // owner
ethRegistry . address , // subregistry
zeroAddress , // resolver (none yet)
0 n , // tokenVersionId
MAX_EXPIRY // never expires
],
});
} ,
{
tags: [ "ETHRegistry" , "l1" ] ,
dependencies: [ "RootRegistry" , "HCAFactory" , "RegistryMetadata" ] ,
}
) ;
Example: ETHRegistrar with Permissions
From deploy/03_ETHRegistrar.ts:
import { artifacts , execute } from "@rocketh" ;
import { ROLES } from "../script/deploy-constants.js" ;
export default execute (
async ({ deploy , execute : write , get , namedAccounts : { deployer , owner } }) => {
const hcaFactory = get < typeof artifacts . MockHCAFactoryBasic . abi >( "HCAFactory" );
const ethRegistry = get < typeof artifacts . PermissionedRegistry . abi >( "ETHRegistry" );
const rentPriceOracle = get < typeof artifacts . IRentPriceOracle . abi >(
"StandardRentPriceOracle"
);
const beneficiary = owner || deployer ;
const SEC_PER_DAY = 86400 n ;
// Deploy ETH registrar
const ethRegistrar = await deploy ( "ETHRegistrar" , {
account: deployer ,
artifact: artifacts . ETHRegistrar ,
args: [
ethRegistry . address ,
hcaFactory . address ,
beneficiary , // funds recipient
60 n , // minCommitmentAge (60 seconds)
SEC_PER_DAY , // maxCommitmentAge (1 day)
28 n * SEC_PER_DAY , // minRegistrationDuration (28 days)
rentPriceOracle . address ,
],
});
// Grant registrar permissions on ETH registry
await write ( ethRegistry , {
functionName: "grantRootRoles" ,
args: [
ROLES . REGISTRY . REGISTRAR | ROLES . REGISTRY . RENEW ,
ethRegistrar . address ,
],
account: deployer ,
});
} ,
{
tags: [ "ETHRegistrar" , "l1" ] ,
dependencies: [ "HCAFactory" , "ETHRegistry" , "StandardRentPriceOracle" ] ,
}
) ;
Deployment Constants
Constants are defined in script/deploy-constants.ts:
// Maximum expiry timestamp (uint64 max)
export const MAX_EXPIRY = ( 1 n << 64 n ) - 1 n ;
// Batch gateway configuration
export const LOCAL_BATCH_GATEWAY_URL = "x-batch-gateway:true" ;
// Role definitions (from EnhancedAccessControl)
export const ROLES = {
ALL: 0x1111111111111111111111111111111111111111111111111111111111111111 n ,
REGISTRY: {
REGISTRAR: 1 n << 0 n ,
REGISTER_RESERVED: 1 n << 4 n ,
SET_PARENT: 1 n << 8 n ,
UNREGISTER: 1 n << 12 n ,
RENEW: 1 n << 16 n ,
SET_SUBREGISTRY: 1 n << 20 n ,
SET_RESOLVER: 1 n << 24 n ,
CAN_TRANSFER: 1 n << 28 n ,
UPGRADE: 1 n << 124 n ,
},
REGISTRAR: {
SET_ORACLE: 1 n << 0 n ,
},
RESOLVER: {
SET_ADDR: 1 n << 0 n ,
SET_TEXT: 1 n << 4 n ,
SET_CONTENTHASH: 1 n << 8 n ,
SET_PUBKEY: 1 n << 12 n ,
SET_ABI: 1 n << 16 n ,
SET_INTERFACE: 1 n << 20 n ,
SET_NAME: 1 n << 24 n ,
SET_ALIAS: 1 n << 28 n ,
CLEAR: 1 n << 32 n ,
UPGRADE: 1 n << 124 n ,
},
// Admin roles (shifted by 128 bits)
ADMIN: {
// ... (generated by adminify() function)
},
};
// Name status constants
export const STATUS = {
AVAILABLE: 0 ,
RESERVED: 1 ,
REGISTERED: 2 ,
};
Network Configuration
Rocketh uses network tags to control which scripts run:
l1 : Layer 1 (Ethereum mainnet/testnets)
l2 : Layer 2 networks
local : Local development networks
use_root : Deploy root contracts (not for mainnet)
allow_unsafe : Allow state manipulation (testing only)
legacy : Deploy ENS v1 contracts
Named Accounts
Accounts are defined per network in the Rocketh config:
accounts : {
deployer : "0x..." , // Deploys all contracts
owner : "0x..." , // Receives ownership after deployment
bridger : "0x..." , // Cross-chain bridge operator
}
For the devnet (from script/setup.ts):
const names = [ "deployer" , "owner" , "bridger" , "user" , "user2" ];
const accounts = Array . from ({ length: extraAccounts }, ( _ , i ) =>
Object . assign ( mnemonicToAccount ( mnemonic , { addressIndex: i }), {
name: names [ i ] ?? `unnamed ${ i } ` ,
})
);
Running Deployments
Local Deployment (Devnet)
The devnet automatically runs all deployments:
cd contracts
bun run devnet
Deployments are saved to deployments/devnet-local/.
Custom Network Deployment
To deploy to a custom network, use Rocketh directly:
# Set environment variables
export DEPLOYER_PRIVATE_KEY = "0x..."
export RPC_URL = "https://..."
# Run deployment
npx rocketh deploy --network < network-nam e >
Network-Specific Deployment
Create a network configuration:
// rocketh.config.ts
import { defineConfig } from "rocketh" ;
export default defineConfig ({
networks: {
sepolia: {
nodeUrl: process . env . SEPOLIA_RPC_URL ,
tags: [ "l1" ],
accounts: {
deployer: process . env . DEPLOYER_ADDRESS ,
owner: process . env . OWNER_ADDRESS ,
},
},
mainnet: {
nodeUrl: process . env . MAINNET_RPC_URL ,
tags: [ "l1" ],
accounts: {
deployer: process . env . DEPLOYER_ADDRESS ,
owner: process . env . OWNER_ADDRESS ,
},
},
} ,
}) ;
Deployment Artifacts
Deployment data is saved in deployments/<network-name>/:
deployments/devnet-local/
├── RootRegistry.json
├── ETHRegistry.json
├── ETHRegistrar.json
├── UniversalResolverV2.json
└── ...
Each artifact contains:
{
"address" : "0x..." ,
"abi" : [ ... ],
"transactionHash" : "0x..." ,
"receipt" : { ... },
"args" : [ ... ],
"bytecode" : "0x..." ,
"deployedBytecode" : "0x..." ,
"metadata" : { ... }
}
Post-Deployment Setup
Verify Contracts
Use the Rocketh verifier plugin:
npx rocketh verify --network < network-nam e >
This submits source code to Etherscan-compatible explorers.
Transfer Ownership
After deployment, transfer ownership from deployer to owner:
// Example: Transfer root registry ownership
rootRegistry. transferOwnership (owner);
// Or revoke deployer roles and grant to owner
rootRegistry. revokeRootRoles (ROLES.ALL, deployer);
rootRegistry. grantRootRoles (ROLES.ALL, owner);
Register TLDs in the root registry:
await rootRegistry . write . register ([
"box" , // TLD label
owner , // owner
boxRegistry , // subregistry address
zeroAddress , // resolver
0 n , // tokenVersionId
MAX_EXPIRY // expiry
]);
Set Up Resolvers
Deploy and configure resolvers:
// Deploy resolver
const resolver = await deployment . deployPermissionedResolver ({
account: deployer ,
admin: owner ,
roles: ROLES . ALL ,
});
// Set resolver for a name
await ethRegistry . write . setResolver ([
tokenId ,
resolver . address
]);
Security Considerations
Critical : Follow these security practices for production deployments.
Private Key Management
Never commit private keys to version control
Use hardware wallets for mainnet deployments
Store keys in environment variables or secure vaults
Use different keys for deployer and owner roles
Role Assignment
Grant minimal roles required for operation
Use the ADMIN role hierarchy for delegation
Transfer root roles from deployer to owner after deployment
Audit all role grants before deployment
Contract Verification
Verify all contracts on Etherscan
Match deployment bytecode with source code
Document constructor arguments
Enable source code viewing
Deployment Checklist
Upgradeable Contracts
Some contracts use UUPS proxies via VerifiableFactory:
Deploying Upgradeable Contracts
import { deployVerifiableProxy } from "./test/integration/fixtures/deployVerifiableProxy.js" ;
const proxy = await deployVerifiableProxy ({
walletClient ,
factoryAddress: verifiableFactory . address ,
implAddress: permissionedResolverImpl . address ,
abi: artifacts . PermissionedResolver . abi ,
functionName: "initialize" ,
args: [ admin , roles ],
salt: 12345 n , // Deterministic deployment
});
Upgrading Contracts
// Deploy new implementation
const newImpl = await deploy ( "PermissionedResolverImplV2" , {
artifact: artifacts . PermissionedResolverV2 ,
});
// Upgrade via UUPS
await proxy . write . upgradeToAndCall ([
newImpl . address ,
"0x" // No initialization data
]);
Only accounts with ROLE_UPGRADE can upgrade contracts. Ensure this role is properly managed.
Environment Variables
Required Variables
# RPC endpoints
MAINNET_RPC_URL = https://...
SEPOLIA_RPC_URL = https://...
# Accounts
DEPLOYER_PRIVATE_KEY = 0x...
DEPLOYER_ADDRESS = 0x...
OWNER_ADDRESS = 0x...
# Optional
ETHERSCAN_API_KEY = ...
BATCH_GATEWAY_URLS = ["https://..." ]
Devnet Variables
# Devnet configuration
ANVIL_IP_ADDR = 0.0.0.0
FOUNDRY_DISABLE_NIGHTLY_WARNING = true
BATCH_GATEWAY_URLS = ["x-batch-gateway:true" ]
Troubleshooting
Deployment Fails with “Nonce too low”
Reset the deployer nonce or wait for pending transactions to confirm.
”Insufficient funds” Error
Ensure the deployer account has enough ETH for:
Contract deployment gas costs
Post-deployment transactions (registration, setup)
Dependency Not Found
Check that dependency tags are correct and that required contracts deployed first:
{
tags : [ "MyContract" ],
dependencies : [ "Dependency1" , "Dependency2" ], // Must deploy first
}
Contract Address Mismatch
Clear old deployments:
rm -rf deployments/ < network-nam e >
Then re-deploy.
Next Steps
Development Setup Configure your environment
Testing Run tests on deployed contracts
Local Devnet Test deployments locally
Contract Reference Learn about deployed contracts