The Ethereum Name Service (ENS) has revolutionized the way we interact with the blockchain by replacing complex addresses with human-readable domain names like "myname.eth". However, ENS faces scalability and cost challenges that hinder its widespread adoption. The External Resolver project offers an innovative solution to overcome these obstacles by combining established patterns such as ERC-3668, EIP-5559, ENSIP-10, and ENSIP-16.
At its core, a "resolver" is a crucial component of ENS that translates human-readable domain names into relevant blockchain information, such as wallet addresses, public keys, and custom records. The "resolution" process is fundamental for making domain names usable in decentralized applications (dApps) and wallets.
The External Resolver takes the concept of resolution further by allowing ENS data to be stored and managed off-chain. This drastically reduces transaction costs, improves network scalability, and enables more advanced features like larger and more complex data records.
This project not only makes ENS more efficient and cost-effective but also opens up a world of possibilities for developers and users, expanding the potential of ENS as a foundational infrastructure for Web3. By providing a comprehensive reference implementation for off-chain storage and management, the External Resolver empowers the community to innovate and build upon the ENS ecosystem.
Contract | Network | Address |
---|---|---|
DatabaseResolver | Ethereum | 0xBF3F57862717099319285c1E2664Cd583f35E333 |
Contract | Network | Address |
---|---|---|
DatabaseResolver | Ethereum | 0xc1D4903Eba794035d2D81D210325b57a95C8a007 |
ArbitrumVerifier | Ethereum | 0x8fc4a214705e3c40032e99f867d964c012bf8efb |
L1Resolver | Ethereum | 0xF0c1d78C73B2fCBF17e1c4DbBBD9df30a9556BB8 |
ENSRegistry | Arbitrum | 0x8d55e297c37993ebbd2e7a8d7688f7e5b35f1b50 |
ReverseRegistrar | Arbitrum | 0xb3c9ff08671bbadddd0436cc46fbfa005c8da0a7 |
BaseRegistrarImplementation | Arbitrum | 0x2C6a113C513fa0fd404abcCE3aC8a4BE16ccb651 |
NameWrapper | Arbitrum | 0xff4f34ac12a84de527cf9e24856fc8d7c42cc379 |
ETHRegistrarController | Arbitrum | 0x263c644d8f5d4bdb44cfab020491ec6fc4ca5271 |
SubdomainController | Arbitrum | 0x41eede073217084a30f6f3bc2c546bda1f08b5ca |
PublicResolver | Arbitrum | 0x0a33f065c9c8f0F5c56BB84b1593631725F0f3af |
The External Resolver consists of three main components, each of them is a self-contained project with its own set of files and logic, ensuring seamless integration and collaboration between them. This modular architecture allows for flexibility and customization, making the External Resolver a versatile solution for various use cases.
The Gateway serves as the bridge between the blockchain and external data sources. It follows the EIP-3668 specification to fetch data from off-chain storage and relays it back to the client. The Gateway ensures secure and efficient communication between the different components of the system.
The smart contracts are the backbone of the External Resolver. They include the L1 Resolver, which redirects requests to external resolvers, the L2 Resolver Contract, which handles the actual resolution of domain names on Layer 2 networks and more. These contracts are designed to be modular and adaptable, allowing for deployment on various EVM-compatible chains.
A smart contract that redirects requests to specified external contract deployed to any EVM compatible protocol.
An L2 contract capable of resolving ENS domains to corresponding addresses and fetching additional information fully compatible with the ENS' Public Resolver but responsible for authentication.
The client acts as the interface between the user and the Blockchain. It handles requests for domain resolution and interacts with the Gateway to retrieve the necessary information.
Sample interaction with the Database Resolver:
try {
await client.simulateContract({
functionName: 'register',
abi: dbAbi,
args: [toHex(name), 300],
account: signer.address,
address: resolverAddr,
})
} catch (err) {
const data = getRevertErrorData(err)
if (data?.errorName === 'StorageHandledByOffChainDatabase') {
const [domain, url, message] = data.args as [
DomainData,
string,
MessageData,
]
await handleDBStorage({ domain, url, message, signer })
} else {
console.error('writing failed: ', { err })
}
}
Sample interaction with the Layer 1 Resolver:
try {
await client.simulateContract({
functionName: 'setText',
abi: l1Abi,
args: [toHex(packetToBytes(name)), 'com.twitter', '@blockful'],
address: resolverAddr,
})
} catch (err) {
const data = getRevertErrorData(err)
if (data?.errorName === 'StorageHandledByL2') {
const [chainId, contractAddress] = data.args as [bigint, `0x${string}`]
await handleL2Storage({
chainId,
l2Url: providerL2,
args: {
functionName: 'setText',
abi: l2Abi,
args: [namehash(name), 'com.twitter', '@blockful'],
address: contractAddress,
account: signer,
},
})
} else if (data) {
console.error('error setting text: ', data.errorName)
} else {
console.error('error setting text: ', { err })
}
}
To run the External Resolver project in its entirety, you'll need to complete the installation process. Since we provide an off-chain resolver solution, it's essential to set up both the database and the Arbitrum Layer 2 environment. This will enable you to run comprehensive end-to-end tests and verify the functionality of the entire project.
anvil
env.example
file to .env
in the root directory.npm install
npm run build
Run a local PostgreSQL instance (no initial data is inserted):
docker-compose up db -d
Deploy the contracts locally:
npm run contracts dev:db
Start the gateway:
npm run gateway dev:db
Write properties to a given domain:
npm run client start:write:db
Request domain properties through the client:
npm run client read
This repository relies on migrations to manage the database schema. To create a new migration, run the following command:
npm run gateway migration:create --name=<migration_name>
To apply the migration, run the following command:
npm run migration:generate -- -n <migration_name>
Deploy the contracts to the local Arbitrum node (follow the Arbitrum's local node setup tutorial):
npm run contracts dev:arb:l2
Gather the contract address from the terminal and add it here so the L1 domain gets resolved by the L2 contract you just deployed.
Start the gateway:
npm run gateway dev:arb
Request domain properties through the client:
npm run client start
Ensure you have the Railway CLI installed.
Install the Railway CLI:
npm i -g @railway/cli
Log in to your Railway account:
railway login
Link the repo to the project:
railway link
Deploy the Gateway:
railway up
npm run contracts deploy:db -- --rpc-url <RPC_URL>
Domain Register and data writing:
register
function on the resolverStorageHandledByDB
revert with the arguments required to call the gateway/{sender}/{data}.json
as specified by the EIP-3668Reading domain properties:
resolver
function on the Universal Resolver passing the reading method in an encoded format as argumentOffchainLookup
revert with the required arguments to call the gateway/{sender}/{data}.json
as specified by the EIP-3668Domain Register:
register
function on the resolver passing the address of the Layer 2 resolver that will be managing the properties of a given domainsetOwner
on the L1 ResolverStorageHandledByL2
revert with the arguments required to call the gatewayThis project aims to significantly enhance the scalability and usability of the Ethereum Name Service through the development of a comprehensive reference codebase. By combining existing patterns and best practices, we aim to lower costs for users and drive increased adoption within the industry. We welcome collaboration and feedback from the community as we progress towards our goals.
We welcome contributions from the community to improve this project. To contribute, please follow these guidelines:
This project is licensed under the MIT License.
Special thanks to the Ethereum Name Service (ENS) community for their contributions and support.