diff --git a/Makefile b/Makefile index 2bc5de0..57d2467 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ script_dir := packages/contracts/script deploy_dir := $(script_dir)/deploy PRIVATE_KEY?=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -L2_PRIVATE_KEY?=0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 ETHERSCAN_API_KEY?=VEJ7GISNRKFUESRPC4W4D3ZEM2P9B4J6C4 .PHONY: deploy-arb @@ -34,7 +33,7 @@ deploy-arb-resolver: --rpc-url arb_sepolia \ --broadcast \ -vvv \ - --private-key $(L2_PRIVATE_KEY) \ + --private-key $(PRIVATE_KEY) \ --verify; @@ -47,7 +46,7 @@ deploy-arb-full: --rpc-url arb_sepolia \ --broadcast \ -vvv \ - --private-key $(L2_PRIVATE_KEY) \ + --private-key $(PRIVATE_KEY) \ --verify \ && \ ) true; \ diff --git a/README.md b/README.md index b05b6f7..e272a14 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,28 @@ This project not only makes ENS more efficient and cost-effective but also opens - **Increase Usability**: Make ENS more user-friendly and accessible. - **Reference implementation**: Create a reference on how to implement off-chain storage and management. +## Deployments + +### Mainnet + +| Contract | Network | Address | +| ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | +| DatabaseResolver | Ethereum | [0xBF3F57862717099319285c1E2664Cd583f35E333](https://etherscan.io/address/0xBF3F57862717099319285c1E2664Cd583f35E333) | + +### Sepolia + +| Contract | Network | Address | +| --------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | +| ArbitrumVerifier | Ethereum | [0x8fc4a214705e3c40032e99f867d964c012bf8efb](https://sepolia.etherscan.io/address/0x8fc4a214705e3c40032e99f867d964c012bf8efb) | +| L1Resolver | Ethereum | [0xF0c1d78C73B2fCBF17e1c4DbBBD9df30a9556BB8](https://sepolia.etherscan.io/address/0xF0c1d78C73B2fCBF17e1c4DbBBD9df30a9556BB8) | +| ENSRegistry | Arbitrum | [0x8d55e297c37993ebbd2e7a8d7688f7e5b35f1b50](https://sepolia.arbiscan.io/address/0x8d55e297c37993ebbd2e7a8d7688f7e5b35f1b50) | +| ReverseRegistrar | Arbitrum | [0xb3c9ff08671bbadddd0436cc46fbfa005c8da0a7](https://sepolia.arbiscan.io/address/0xb3c9ff08671bbadddd0436cc46fbfa005c8da0a7) | +| BaseRegistrarImplementation | Arbitrum | [0x41eedE073217084A30f6f3Bc2c546BDa1F08b5ca](https://sepolia.arbiscan.io/address/0x41eedE073217084A30f6f3Bc2c546BDa1F08b5ca) | +| NameWrapper | Arbitrum | [0xff4f34ac12a84de527cf9e24856fc8d7c42cc379](https://sepolia.arbiscan.io/address/0xff4f34ac12a84de527cf9e24856fc8d7c42cc379) | +| ETHRegistrarController | Arbitrum | [0x263c644d8f5d4bdb44cfab020491ec6fc4ca5271](https://sepolia.arbiscan.io/address/0x263c644d8f5d4bdb44cfab020491ec6fc4ca5271) | +| SubdomainController | Arbitrum | [0x41eede073217084a30f6f3bc2c546bda1f08b5ca](https://sepolia.arbiscan.io/address/0x41eede073217084a30f6f3bc2c546bda1f08b5ca) | +| PublicResolver | Arbitrum | [0x0a33f065c9c8f0F5c56BB84b1593631725F0f3af](https://sepolia.arbiscan.io/address/0x0a33f065c9c8f0F5c56BB84b1593631725F0f3af) | + ## Components 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. diff --git a/ensips/ccip-write/ccip-write.md b/ensips/ccip-write/ccip-write.md index 68e5fb7..a4b4db2 100644 --- a/ensips/ccip-write/ccip-write.md +++ b/ensips/ccip-write/ccip-write.md @@ -47,7 +47,7 @@ The function has the following signature: ```solidity function registerParams( - bytes memory name, + bytes calldata name, uint256 duration ) external @@ -89,7 +89,8 @@ Aiming to integrate with the already existing interface of domain registration, ```solidity function register( - string calldata name, + bytes32 parentNode, + string calldata label, address owner, uint256 duration, bytes32 secret, @@ -103,7 +104,8 @@ function register( Parameters: -- `name`: DNS-encoded name to be registered +- `parentNode`: namehash of the parent domain +- `label`: DNS-encoded name to be registered - `owner`: subdomain owner's address - `duration`: the duration in miliseconds of the registration - `secret`: random seed to be used for commit/reveal @@ -278,7 +280,8 @@ interface OffchainRegister { /** * Forwards the registering of a domain to the L2 contracts - * @param name The DNS-encoded name to resolve. + * @param parentNode namehash of the parent domain + * @param label The DNS-encoded name to resolve. * @param owner Owner of the domain * @param duration duration The duration in seconds of the registration. * @param resolver The address of the resolver to set for this name. @@ -287,7 +290,8 @@ interface OffchainRegister { * @param extraData any encoded additional data */ function register( - string calldata name, + bytes32 parentNode, + string calldata label, address owner, uint256 duration, bytes32 secret, @@ -336,10 +340,10 @@ interface OffchainMulticallable { interface OffchainCommitable { - /** - * @notice produces the commit hash from the register calldata - * @returns the hash of the commit to be used - */ + /** + * @notice produces the commit hash from the register calldata + * @returns the hash of the commit to be used + */ function makeCommitment( string calldata name, address owner, diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 1550149..7d39f10 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -73,7 +73,7 @@ export async function handleDBStorage({ }) } -export function getChain(chainId: number) { +export function getChain(chainId: number): chains.Chain | undefined { return [ ...Object.values(chains), defineChain({ @@ -92,3 +92,15 @@ export function getChain(chainId: number) { }), ].find((chain) => chain.id === chainId) } + +// gather the first part of the domain (e.g. floripa.blockful.eth -> floripa) +export function extractLabelFromName(name: string): string { + const [, label] = /^(\w+)/.exec(name) || [] + return label +} + +// gather the last part of the domain (e.g. floripa.blockful.eth -> blockful.eth) +export function extractParentFromName(name: string): string { + const [, parent] = /\w*\.(.*)$/.exec(name) || [] + return parent +} diff --git a/packages/client/src/l2.write.ts b/packages/client/src/l2.write.ts index 8f855bb..b03ffcd 100644 --- a/packages/client/src/l2.write.ts +++ b/packages/client/src/l2.write.ts @@ -27,48 +27,50 @@ config({ }) let { - UNIVERSAL_RESOLVER_ADDRESS: resolver, - L2_RESOLVER_ADDRESS: l2Resolver, + UNIVERSAL_RESOLVER_ADDRESS: universalResolver, + RESOLVER_ADDRESS: resolver, CHAIN_ID: chainId = '31337', RPC_URL: provider = 'http://127.0.0.1:8545/', L2_RPC_URL: providerL2 = 'http://127.0.0.1:8547', - PRIVATE_KEY: - privateKey = '0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659', // local arbitrum PK + PRIVATE_KEY: privateKey, } = process.env const chain = getChain(parseInt(chainId)) -console.log(`Connecting to ${chain?.name}.`) +if (!chain) { + throw new Error('Chain not found') +} const client = createPublicClient({ chain, transport: http(provider), }).extend(walletActions) +console.log(`Connecting to ${chain?.name}.`) // eslint-disable-next-line const _ = (async () => { - if (!l2Resolver) { - throw new Error('L2_RESOLVER_ADDRESS is required') + if (!resolver) { + throw new Error('RESOLVER_ADDRESS is required') } - const publicAddress = normalize('lucas.arb.eth') - const node = namehash(publicAddress) + const name = normalize('gibi.arb.eth') + const encodedName = toHex(packetToBytes(name)) + const node = namehash(name) const signer = privateKeyToAccount(privateKey as Hex) - if (!resolver) { - resolver = getChainContractAddress({ + if (!universalResolver) { + universalResolver = getChainContractAddress({ chain: client.chain, contract: 'ensUniversalResolver', }) } const [resolverAddr] = (await client.readContract({ - address: resolver as Hex, + address: universalResolver as Hex, functionName: 'findResolver', abi: uAbi, - args: [toHex(packetToBytes(publicAddress))], + args: [encodedName], })) as Hash[] - const name = extractLabelFromName(publicAddress) const duration = 31556952000n // SUBDOMAIN PRICING @@ -105,13 +107,13 @@ const _ = (async () => { functionName: 'register', abi: l1Abi, args: [ - name, + encodedName, // name signer.address, // owner duration, `0x${'a'.repeat(64)}` as Hex, // secret - l2Resolver, // resolver - data, // calldata - false, // primaryName + resolver, + data, // records calldata + false, // reverseRecord 0, // fuses `0x${'a'.repeat(64)}` as Hex, // extraData ], @@ -124,33 +126,28 @@ const _ = (async () => { await client.simulateContract(calldata) } catch (err) { const data = getRevertErrorData(err) - if (data?.errorName === 'StorageHandledByL2') { - const [chainId, contractAddress] = data.args as [bigint, `0x${string}`] - - const l2Client = createPublicClient({ - chain: getChain(Number(chainId)), - transport: http(providerL2), - }).extend(walletActions) - - try { - const { request } = await l2Client.simulateContract({ - ...calldata, - address: contractAddress, - }) - await l2Client.writeContract(request) - } catch (err) { - console.log('error while trying to make the request: ', { err }) + switch (data?.errorName) { + case 'StorageHandledByL2': { + const [chainId, contractAddress] = data.args as [bigint, `0x${string}`] + + const l2Client = createPublicClient({ + chain: getChain(Number(chainId)), + transport: http(providerL2), + }).extend(walletActions) + + try { + const { request } = await l2Client.simulateContract({ + ...calldata, + address: contractAddress, + }) + await l2Client.writeContract(request) + } catch (err) { + console.log('error while trying to make the request: ', { err }) + } + return } - } else if (data) { - console.error('error registering domain: ', data.errorName) - } else { - console.error('error registering domain: ', { err }) + default: + console.error('error registering domain: ', { err }) } } })() - -// gather the first part of the domain (e.g. floripa.blockful.eth -> floripa) -function extractLabelFromName(name: string): string { - const [, label] = /^(\w+)/.exec(name) || [] - return label -} diff --git a/packages/contracts/script/deploy/SubdomainController.sol b/packages/contracts/script/deploy/SubdomainController.sol index 7b114fb..09ba590 100644 --- a/packages/contracts/script/deploy/SubdomainController.sol +++ b/packages/contracts/script/deploy/SubdomainController.sol @@ -18,8 +18,9 @@ contract SubdomainControllerScript is DeployHelper, ENSHelper { vm.startBroadcast(); uint256 subdomainPrice = 0.001 ether; + uint256 commitTime = 0; SubdomainController subdomainController = new SubdomainController( - namehash("arb.eth"), address(nameWrapper), subdomainPrice, 0 + address(nameWrapper), subdomainPrice, commitTime ); nameWrapper.setApprovalForAll(address(subdomainController), true); diff --git a/packages/contracts/script/local/L2ArbitrumResolver.sol b/packages/contracts/script/local/L2ArbitrumResolver.sol index 69b6990..0323e02 100644 --- a/packages/contracts/script/local/L2ArbitrumResolver.sol +++ b/packages/contracts/script/local/L2ArbitrumResolver.sol @@ -21,6 +21,7 @@ import {StaticMetadataService} from "@ens-contracts/wrapper/StaticMetadataService.sol"; import {IMetadataService} from "@ens-contracts/wrapper/IMetadataService.sol"; import {PublicResolver} from "@ens-contracts/resolvers/PublicResolver.sol"; +import {NameEncoder} from "@ens-contracts/utils/NameEncoder.sol"; import {ENSHelper} from "../ENSHelper.sol"; import {SubdomainController} from "../../src/SubdomainController.sol"; @@ -81,10 +82,7 @@ contract L2ArbitrumResolver is Script, ENSHelper { uint256 subdomainPrice = 0.001 ether; uint256 commitTime = 0; SubdomainController subdomainController = new SubdomainController( - namehash("arb.eth"), - address(nameWrapper), - subdomainPrice, - commitTime + address(nameWrapper), subdomainPrice, commitTime ); nameWrapper.setApprovalForAll(address(subdomainController), true); @@ -102,15 +100,16 @@ contract L2ArbitrumResolver is Script, ENSHelper { "arb", msg.sender, 31556952000, address(arbResolver), 1 ); + (bytes memory name, bytes32 node) = + NameEncoder.dnsEncodeName("blockful.arb.eth"); + bytes[] memory data = new bytes[](1); data[0] = abi.encodeWithSelector( - TextResolver.setText.selector, - namehash("blockful.arb.eth"), - "com.twitter", - "@blockful" + TextResolver.setText.selector, node, "com.twitter", "@blockful" ); + subdomainController.register{value: subdomainController.price()}( - "blockful", + name, msg.sender, 31556952000, keccak256("secret"), diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index d2e1b04..dbd5de5 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -103,16 +103,18 @@ contract L1Resolver is /** * Forwards the registering of a subdomain to the L2 contracts - * @param -name The DNS-encoded name to resolve. + * @param -name The DNS-encoded name to be registered. * @param -owner Owner of the domain * @param -duration duration The duration in seconds of the registration. + * @param -secret The secret to be used for the registration based on commit/reveal * @param -resolver The address of the resolver to set for this name. * @param -data Multicallable data bytes for setting records in the associated resolver upon reigstration. + * @param -reverseRecord Whether this name is the primary name * @param -fuses The fuses to set for this name. * @param -extraData any encoded additional data */ function register( - string calldata, /* name */ + bytes calldata, /* name */ address, /* owner */ uint256, /* duration */ bytes32, /* secret */ @@ -137,7 +139,7 @@ contract L1Resolver is * @return extraData any given structure in an ABI encoded format */ function registerParams( - bytes memory, /* name */ + bytes calldata, /* name */ uint256 /* duration */ ) external diff --git a/packages/contracts/src/SubdomainController.sol b/packages/contracts/src/SubdomainController.sol index 87e22e7..c6bd0a5 100644 --- a/packages/contracts/src/SubdomainController.sol +++ b/packages/contracts/src/SubdomainController.sol @@ -3,31 +3,31 @@ pragma solidity ^0.8.17; import {INameWrapper} from "@ens-contracts/wrapper/INameWrapper.sol"; import {Resolver} from "@ens-contracts/resolvers/Resolver.sol"; +import {BytesUtils} from "@ens-contracts/utils/BytesUtils.sol"; import {ENSHelper} from "../script/ENSHelper.sol"; import {OffchainRegister} from "./interfaces/OffchainResolver.sol"; contract SubdomainController is OffchainRegister, ENSHelper { + using BytesUtils for bytes; + uint256 public price; uint256 public commitTime; - bytes32 public baseNode; INameWrapper nameWrapper; constructor( - bytes32 _baseNode, address _nameWrapperAddress, uint256 _price, uint256 _commitTime ) { commitTime = _commitTime; - baseNode = _baseNode; price = _price; nameWrapper = INameWrapper(_nameWrapperAddress); } function register( - string calldata name, + bytes calldata name, address owner, uint256 duration, bytes32, /* secret */ @@ -41,22 +41,35 @@ contract SubdomainController is OffchainRegister, ENSHelper { payable override { - bytes32 nodehash = - keccak256(abi.encodePacked(baseNode, labelhash(name))); + bytes32 node = name.namehash(0); + string memory label = _getLabel(name); + + (, uint256 offset) = name.readLabel(0); + bytes32 parentNode = name.namehash(offset); require( - nameWrapper.ownerOf(uint256(nodehash)) == address(0), + nameWrapper.ownerOf(uint256(node)) == address(0), "domain already registered" ); require(msg.value >= price, "insufficient funds"); nameWrapper.setSubnodeRecord( - baseNode, name, owner, resolver, 0, fuses, uint64(duration) + parentNode, label, owner, resolver, 0, fuses, uint64(duration) ); if (data.length > 0) { - Resolver(resolver).multicallWithNodeCheck(nodehash, data); + Resolver(resolver).multicallWithNodeCheck(node, data); } } + function _getLabel(bytes calldata name) + private + pure + returns (string memory) + { + uint256 labelLength = uint256(uint8(name[0])); + if (labelLength == 0) return ""; + return string(name[1:labelLength + 1]); + } + } diff --git a/packages/contracts/src/interfaces/OffchainResolver.sol b/packages/contracts/src/interfaces/OffchainResolver.sol index c9768e2..ad25f46 100644 --- a/packages/contracts/src/interfaces/OffchainResolver.sol +++ b/packages/contracts/src/interfaces/OffchainResolver.sol @@ -5,16 +5,18 @@ interface OffchainRegister { /** * Forwards the registering of a domain to the L2 contracts - * @param name The DNS-encoded name to resolve. + * @param name DNS-encoded name to be registered. * @param owner Owner of the domain * @param duration duration The duration in seconds of the registration. + * @param secret The secret to be used for the registration based on commit/reveal * @param resolver The address of the resolver to set for this name. * @param data Multicallable data bytes for setting records in the associated resolver upon reigstration. + * @param reverseRecord Whether this name is the primary name * @param fuses The fuses to set for this name. * @param extraData any encoded additional data */ function register( - string calldata name, + bytes calldata name, address owner, uint256 duration, bytes32 secret, @@ -40,7 +42,7 @@ interface OffchainRegisterParams { * @return extraData any given structure in an ABI encoded format */ function registerParams( - bytes memory name, + bytes calldata name, uint256 duration ) external diff --git a/packages/contracts/test/SubdomainController.t.sol b/packages/contracts/test/SubdomainController.t.sol new file mode 100644 index 0000000..91cd325 --- /dev/null +++ b/packages/contracts/test/SubdomainController.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Test, console} from "forge-std/Test.sol"; + +import {DummyOffchainResolver} from + "@ens-contracts/test/mocks/DummyOffchainResolver.sol"; +import {NameEncoder} from "@ens-contracts/utils/NameEncoder.sol"; +import {Multicallable} from "@ens-contracts/resolvers/Multicallable.sol"; + +import "../src/SubdomainController.sol"; +import {ENSHelper} from "../script/ENSHelper.sol"; + +contract DummyNameWrapper { + + mapping(bytes32 => address) public owners; + + function ownerOf(uint256 id) public view returns (address) { + return owners[bytes32(id)]; + } + + function setSubnodeRecord( + bytes32 parentNode, + string memory label, + address owner, + address, /* resolver */ + uint64, /* ttl */ + uint32, /* fuses */ + uint64 /* expiry */ + ) + public + returns (bytes32 node) + { + node = keccak256(abi.encodePacked(parentNode, keccak256(bytes(label)))); + owners[node] = owner; + return node; + } + +} + +contract DummyResolver is DummyOffchainResolver, Multicallable { + + mapping(bytes32 => mapping(string => string)) private records; + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(DummyOffchainResolver, Multicallable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function setText( + bytes32 node, + string calldata key, + string calldata value + ) + external + { + records[node][key] = value; + } + + function text( + bytes32 node, + string calldata key + ) + external + view + returns (string memory) + { + return records[node][key]; + } + +} + +contract SubdomainControllerTest is Test, ENSHelper { + + SubdomainController public controller; + DummyNameWrapper public nameWrapper; + DummyResolver public resolver; + + uint256 constant PRICE = 0.1 ether; + uint256 constant COMMIT_TIME = 1 days; + + function setUp() public { + nameWrapper = new DummyNameWrapper(); + resolver = new DummyResolver(); + controller = + new SubdomainController(address(nameWrapper), PRICE, COMMIT_TIME); + } + + function testRegister() public { + (bytes memory name, bytes32 node) = + NameEncoder.dnsEncodeName("newdomain.blockful.eth"); + address owner = address(0x123); + uint256 duration = 365 days; + bytes32 secret = bytes32(0); + bytes[] memory data = new bytes[](0); + + vm.expectCall( + address(nameWrapper), + abi.encodeWithSelector( + DummyNameWrapper.setSubnodeRecord.selector, + namehash("blockful.eth"), + "newdomain", + owner, + address(resolver), + 0, + 0, + duration + ) + ); + + vm.deal(address(this), PRICE); + controller.register{value: PRICE}( + name, + owner, + duration, + secret, + address(resolver), + data, + false, + 0, + abi.encode("") + ); + + assertEq( + nameWrapper.ownerOf(uint256(node)), + owner, + "Owner should be set correctly" + ); + } + + function testRegisterInsufficientFunds() public { + (bytes memory name,) = + NameEncoder.dnsEncodeName("newdomain.blockful.eth"); + address owner = address(0x123); + uint256 duration = 365 days; + bytes32 secret = bytes32(0); + bytes[] memory data = new bytes[](0); + + vm.expectRevert("insufficient funds"); + + controller.register{value: PRICE - 1}( + name, + owner, + duration, + secret, + address(resolver), + data, + false, + 0, + abi.encode("") + ); + } + + function testRegisterAlreadyRegistered() public { + (bytes memory name,) = + NameEncoder.dnsEncodeName("existingdomain.blockful.eth"); + address owner = address(0x123); + uint256 duration = 365 days; + bytes32 secret = bytes32(0); + bytes[] memory data = new bytes[](0); + + // Simulate that the domain is already registered + nameWrapper.setSubnodeRecord( + namehash("blockful.eth"), + "existingdomain", + owner, + address(0), + 0, + 0, + 0 + ); + + vm.expectRevert("domain already registered"); + + vm.deal(address(this), PRICE); + controller.register{value: PRICE}( + name, + owner, + duration, + secret, + address(resolver), + data, + false, + 0, + abi.encode("") + ); + } + + function testRegisterWithResolverData() public { + (bytes memory name, bytes32 node) = + NameEncoder.dnsEncodeName("newdomain.blockful.eth"); + address owner = address(0x123); + uint256 duration = 365 days; + bytes32 secret = bytes32(0); + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector( + DummyResolver.setText.selector, node, "key", "value" + ); + + vm.expectCall( + address(nameWrapper), + abi.encodeWithSelector( + DummyNameWrapper.setSubnodeRecord.selector, + namehash("blockful.eth"), + "newdomain", + owner, + address(resolver), + 0, + 0, + duration + ) + ); + + vm.expectCall( + address(resolver), + abi.encodeWithSelector( + Multicallable.multicallWithNodeCheck.selector, node, data + ) + ); + + vm.deal(address(this), PRICE); + controller.register{value: PRICE}( + name, + owner, + duration, + secret, + address(resolver), + data, + false, + 0, + abi.encode("") + ); + + assertEq( + nameWrapper.ownerOf(uint256(node)), + owner, + "Owner should be set correctly" + ); + + // Verify that the text record was saved correctly + assertEq( + resolver.text(node, "key"), + "value", + "Text record should be set correctly" + ); + } + +}