diff --git a/packages/client/src/l2.write.ts b/packages/client/src/l2.write.ts index 90e7e4ed..ec117d1f 100644 --- a/packages/client/src/l2.write.ts +++ b/packages/client/src/l2.write.ts @@ -20,12 +20,7 @@ import { privateKeyToAccount } from 'viem/accounts' import { abi as uAbi } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' import { abi as l1Abi } from '@blockful/contracts/out/L1Resolver.sol/L1Resolver.json' -import { - getRevertErrorData, - getChain, - extractLabelFromName, - extractParentFromName, -} from './client' +import { getRevertErrorData, getChain } from './client' config({ path: process.env.ENV_FILE || '../.env', @@ -58,7 +53,7 @@ const _ = (async () => { throw new Error('RESOLVER_ADDRESS is required') } - const name = normalize('gibi.blockful.eth') + const name = normalize('gibi.arb.eth') const node = namehash(name) const signer = privateKeyToAccount(privateKey as Hex) @@ -112,8 +107,7 @@ const _ = (async () => { functionName: 'register', abi: l1Abi, args: [ - namehash(extractParentFromName(name)), // parent - extractLabelFromName(name), // label + toHex(name), // name signer.address, // owner duration, `0x${'a'.repeat(64)}` as Hex, // secret diff --git a/packages/contracts/script/local/L2ArbitrumResolver.sol b/packages/contracts/script/local/L2ArbitrumResolver.sol index ffba08cc..0323e020 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,9 +82,7 @@ contract L2ArbitrumResolver is Script, ENSHelper { uint256 subdomainPrice = 0.001 ether; uint256 commitTime = 0; SubdomainController subdomainController = new SubdomainController( - address(nameWrapper), - subdomainPrice, - commitTime + address(nameWrapper), subdomainPrice, commitTime ); nameWrapper.setApprovalForAll(address(subdomainController), true); @@ -101,16 +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()}( - namehash("arb.eth"), - "blockful", + name, msg.sender, 31556952000, keccak256("secret"), diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index a47b3c44..dbd5de52 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -103,8 +103,7 @@ contract L1Resolver is /** * Forwards the registering of a subdomain to the L2 contracts - * @param -parentNode namehash of the parent node - * @param -label The name to be registered. + * @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 @@ -115,8 +114,7 @@ contract L1Resolver is * @param -extraData any encoded additional data */ function register( - bytes32, /* parentNode */ - string calldata, /* label */ + bytes calldata, /* name */ address, /* owner */ uint256, /* duration */ bytes32, /* secret */ diff --git a/packages/contracts/src/SubdomainController.sol b/packages/contracts/src/SubdomainController.sol index b9618cbf..9c84d10f 100644 --- a/packages/contracts/src/SubdomainController.sol +++ b/packages/contracts/src/SubdomainController.sol @@ -1,14 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +import "forge-std/console.sol"; + 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; INameWrapper nameWrapper; @@ -24,10 +29,9 @@ contract SubdomainController is OffchainRegister, ENSHelper { } function register( - bytes32 parentNode, - string calldata label, + bytes calldata name, address owner, - uint256 duration, + uint256 duration, bytes32, /* secret */ address resolver, bytes[] calldata data, @@ -39,7 +43,11 @@ contract SubdomainController is OffchainRegister, ENSHelper { payable override { - bytes32 node = keccak256(abi.encodePacked(parentNode, labelhash(label))); + bytes32 node = _getNode(name); + string memory label = _getLabel(name); + + (, uint256 offset) = name.readLabel(0); + bytes32 parentNode = name.namehash(offset); require( nameWrapper.ownerOf(uint256(node)) == address(0), @@ -56,4 +64,36 @@ contract SubdomainController is OffchainRegister, ENSHelper { } } + function _getNode(bytes memory name) private pure returns (bytes32 node) { + return _getNode(name, 0); + } + + function _getNode( + bytes memory name, + uint256 offset + ) + private + pure + returns (bytes32 node) + { + uint256 len = name.readUint8(offset); + node = bytes32(0); + if (len > 0) { + bytes32 label = name.keccak(offset + 1, len); + bytes32 parentNode = _getNode(name, offset + len + 1); + node = keccak256(abi.encodePacked(parentNode, label)); + } + return node; + } + + 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 d1172492..ad25f463 100644 --- a/packages/contracts/src/interfaces/OffchainResolver.sol +++ b/packages/contracts/src/interfaces/OffchainResolver.sol @@ -5,8 +5,7 @@ interface OffchainRegister { /** * Forwards the registering of a domain to the L2 contracts - * @param parentNode namehash of the parent node - * @param label The name to be registered. + * @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 @@ -17,8 +16,7 @@ interface OffchainRegister { * @param extraData any encoded additional data */ function register( - bytes32 parentNode, - string calldata label, + bytes calldata name, address owner, uint256 duration, bytes32 secret, diff --git a/packages/contracts/test/SubdomainController.t.sol b/packages/contracts/test/SubdomainController.t.sol new file mode 100644 index 00000000..91cd3255 --- /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" + ); + } + +}