From 470d4024cd7b058ae4784d73f22e930f9cf7cb9d Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 13 Oct 2025 00:38:22 -0400 Subject: [PATCH 01/10] backport from #145 --- .../{02_ETHRegistry.ts => 01_ETHRegistry.ts} | 24 +- ...ETHTLDResolver.ts => 04_ETHTLDResolver.ts} | 23 +- contracts/deploy/l1/05_SetETHTLDResolver.ts | 22 + contracts/foundry.lock | 2 +- contracts/lib/ens-contracts | 2 +- contracts/remappings.txt | 4 +- .../src/L1/bridge/L1BridgeController.sol | 4 +- .../migration/L1LockedMigrationController.sol | 11 +- .../L1UnlockedMigrationController.sol | 9 +- .../registry/MigratedWrappedNameRegistry.sol | 4 +- contracts/src/L1/resolver/ETHTLDResolver.sol | 346 +++--- .../src/L2/bridge/L2BridgeController.sol | 5 +- .../src/common/bridge/EjectionController.sol | 3 +- contracts/src/common/utils/LibLabel.sol | 40 - contracts/src/common/utils/LibMem.sol | 42 + .../libraries/LibRegistry.sol | 22 +- contracts/test/Remappings.t.sol | 19 + .../test/integration/bridge/BridgeTest.t.sol | 4 +- .../integration/fixtures/deployV1Fixture.ts | 6 + .../integration/l1/ETHTLDResolver.test.ts | 19 +- contracts/test/integration/l1/dns/rr.ts | 6 +- contracts/test/mocks/MockBridgeController.sol | 15 + .../unit/L1/bridge/L1BridgeController.t.sol | 43 +- .../L1LockedMigrationController.t.sol | 40 +- .../L1UnlockedMigrationController.t.sol | 9 +- .../MigratedWrappedNameRegistry.t.sol | 8 +- .../unit/L2/bridge/L2BridgeController.t.sol | 9 +- .../unit/common/bridge/BridgeMessages.t.sol | 7 +- .../bridge/libraries/BridgeEncoderLib.t.sol | 11 +- .../test/unit/common/utils/LibLabel.t.sol | 1097 +---------------- .../libraries/LibRegistry.sol | 34 +- contracts/test/utils/utils.ts | 6 +- 32 files changed, 428 insertions(+), 1468 deletions(-) rename contracts/deploy/l1/{02_ETHRegistry.ts => 01_ETHRegistry.ts} (66%) rename contracts/deploy/l1/{01_ETHTLDResolver.ts => 04_ETHTLDResolver.ts} (84%) create mode 100644 contracts/deploy/l1/05_SetETHTLDResolver.ts create mode 100644 contracts/src/common/utils/LibMem.sol create mode 100644 contracts/test/Remappings.t.sol create mode 100644 contracts/test/mocks/MockBridgeController.sol diff --git a/contracts/deploy/l1/02_ETHRegistry.ts b/contracts/deploy/l1/01_ETHRegistry.ts similarity index 66% rename from contracts/deploy/l1/02_ETHRegistry.ts rename to contracts/deploy/l1/01_ETHRegistry.ts index 1e6ff90b..010f2539 100644 --- a/contracts/deploy/l1/02_ETHRegistry.ts +++ b/contracts/deploy/l1/01_ETHRegistry.ts @@ -1,7 +1,8 @@ import { artifacts, execute } from "@rocketh"; -import { MAX_EXPIRY, ROLES } from "../constants.js"; +import { MAX_EXPIRY, ROLES } from "../constants.ts"; +import { zeroAddress } from "viem"; - // TODO: ownership +// TODO: ownership export default execute( async ({ deploy, execute: write, get, namedAccounts: { deployer } }) => { const rootRegistry = @@ -14,9 +15,6 @@ export default execute( (typeof artifacts.SimpleRegistryMetadata)["abi"] >("SimpleRegistryMetadata"); - const ethTLDResolver = - get<(typeof artifacts.ETHTLDResolver)["abi"]>("ETHTLDResolver"); - const ethRegistry = await deploy("ETHRegistry", { account: deployer, artifact: artifacts.PermissionedRegistry, @@ -31,23 +29,11 @@ export default execute( await write(rootRegistry, { account: deployer, functionName: "register", - args: [ - "eth", - deployer, - ethRegistry.address, - ethTLDResolver.address, - 0n, - MAX_EXPIRY, - ], + args: ["eth", deployer, ethRegistry.address, zeroAddress, 0n, MAX_EXPIRY], }); }, { tags: ["ETHRegistry", "l1"], - dependencies: [ - "RootRegistry", - "RegistryDatastore", - "RegistryMetadata", - "ETHTLDResolver", - ], + dependencies: ["RootRegistry", "RegistryDatastore", "RegistryMetadata"], }, ); diff --git a/contracts/deploy/l1/01_ETHTLDResolver.ts b/contracts/deploy/l1/04_ETHTLDResolver.ts similarity index 84% rename from contracts/deploy/l1/01_ETHTLDResolver.ts rename to contracts/deploy/l1/04_ETHTLDResolver.ts index a135fc0e..17636e5d 100755 --- a/contracts/deploy/l1/01_ETHTLDResolver.ts +++ b/contracts/deploy/l1/04_ETHTLDResolver.ts @@ -1,10 +1,5 @@ import { artifacts, execute } from "@rocketh"; -import { - type RpcLog, - encodeFunctionData, - parseEventLogs, - zeroAddress, -} from "viem"; +import { type RpcLog, encodeFunctionData, parseEventLogs } from "viem"; export default execute( async ( @@ -13,13 +8,16 @@ export default execute( ) => { if (!args?.l2Deploy) throw new Error("expected L2 deployment"); - const ensRegistryV1 = - get<(typeof artifacts.ENSRegistry)["abi"]>("ENSRegistry"); + const nameWrapper = + get<(typeof artifacts.NameWrapper)["abi"]>("NameWrapper"); const batchGatewayProvider = get<(typeof artifacts.GatewayProvider)["abi"]>( "BatchGatewayProvider", ); + const bridgeController = + get<(typeof artifacts.L1BridgeController)["abi"]>("BridgeController"); + const verifiableFactory = get<(typeof artifacts.VerifiableFactory)["abi"]>("VerifiableFactory"); @@ -62,9 +60,9 @@ export default execute( account: deployer, artifact: artifacts.ETHTLDResolver, args: [ - ensRegistryV1.address, + nameWrapper.address, batchGatewayProvider.address, - zeroAddress, // burnAddressV1 + bridgeController.address, ethSelfResolver.address, args.verifierAddress, args.l2Deploy.deployments.RegistryDatastore.address, @@ -81,10 +79,11 @@ export default execute( { tags: ["ETHTLDResolver", "l1"], dependencies: [ + "NameWrapper", + "BatchGatewayProvider", "VerifiableFactory", "DedicatedResolver", - "BaseRegistrarImplementation", // "ENSRegistry" - "BatchGatewayProvider", + "BridgeController", ], }, ); diff --git a/contracts/deploy/l1/05_SetETHTLDResolver.ts b/contracts/deploy/l1/05_SetETHTLDResolver.ts new file mode 100644 index 00000000..a43dc4eb --- /dev/null +++ b/contracts/deploy/l1/05_SetETHTLDResolver.ts @@ -0,0 +1,22 @@ +import { artifacts, execute } from "@rocketh"; +import { labelToCanonicalId } from "../../test/utils/utils.ts"; + +export default execute( + async ({ execute: write, get, namedAccounts: { deployer } }) => { + const rootRegistry = + get<(typeof artifacts.PermissionedRegistry)["abi"]>("RootRegistry"); + + const ethTLDResolver = + get<(typeof artifacts.ETHTLDResolver)["abi"]>("ETHTLDResolver"); + + await write(rootRegistry, { + account: deployer, + functionName: "setResolver", + args: [labelToCanonicalId("eth"), ethTLDResolver.address], + }); + }, + { + tags: ["SetETHTLDResolver", "l1"], + dependencies: ["RootRegistry", "ETHTLDResolver"], + }, +); diff --git a/contracts/foundry.lock b/contracts/foundry.lock index 5d0940f7..80a4ede5 100644 --- a/contracts/foundry.lock +++ b/contracts/foundry.lock @@ -3,7 +3,7 @@ "rev": "82cc81935de1d1a82e021cf1030d902c5248982b" }, "lib/ens-contracts": { - "rev": "143fd904fade60cae1b7862a7d998f434063a345" + "rev": "04cf79fb11d5351a4a5a52412b2e4819f990394d" }, "lib/forge-std": { "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505" diff --git a/contracts/lib/ens-contracts b/contracts/lib/ens-contracts index 143fd904..04cf79fb 160000 --- a/contracts/lib/ens-contracts +++ b/contracts/lib/ens-contracts @@ -1 +1 @@ -Subproject commit 143fd904fade60cae1b7862a7d998f434063a345 +Subproject commit 04cf79fb11d5351a4a5a52412b2e4819f990394d diff --git a/contracts/remappings.txt b/contracts/remappings.txt index c075aa85..46054223 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,14 +1,16 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ @ens/contracts/=lib/ens-contracts/contracts/ +@ens/contracts/utils/LibMem/=src/common/utils/ @ensdomains/buffer/=lib/buffer/ @unruggable/gateways/=lib/unruggable-gateways/ @ensdomains/verifiable-factory/=lib/verifiable-factory/src/ forge-std/=lib/forge-std/src/ +lib/ens-contracts/contracts/utils/LibMem/=src/common/utils/ lib/ens-contracts/:@openzeppelin/contracts=lib/openzeppelin-contracts-v4/contracts lib/ens-contracts/:@openzeppelin/contracts-v5=lib/openzeppelin-contracts/contracts lib/ens-contracts/:@ensdomains/solsha1/contracts=lib/solsha1/contracts lib/ens-contracts/:@unruggable/gateways/=lib/unruggable-gateways/contracts test/mocks/v1/:@openzeppelin/contracts=lib/openzeppelin-contracts-v4/contracts ~src/=src/ -~test/=test/ \ No newline at end of file +~test/=test/ diff --git a/contracts/src/L1/bridge/L1BridgeController.sol b/contracts/src/L1/bridge/L1BridgeController.sol index 169d8a3a..2dce64b9 100644 --- a/contracts/src/L1/bridge/L1BridgeController.sol +++ b/contracts/src/L1/bridge/L1BridgeController.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + import {EjectionController} from "../../common/bridge/EjectionController.sol"; import {IBridge} from "../../common/bridge/interfaces/IBridge.sol"; import {BridgeEncoderLib} from "../../common/bridge/libraries/BridgeEncoderLib.sol"; @@ -53,7 +55,7 @@ contract L1BridgeController is EjectionController { function completeEjectionToL1( TransferData memory transferData ) external virtual onlyRootRoles(BridgeRolesLib.ROLE_EJECTOR) returns (uint256 tokenId) { - string memory label = LibLabel.extractLabel(transferData.dnsEncodedName); + string memory label = NameCoder.firstLabel(transferData.dnsEncodedName); tokenId = REGISTRY.register( label, diff --git a/contracts/src/L1/migration/L1LockedMigrationController.sol b/contracts/src/L1/migration/L1LockedMigrationController.sol index 92148314..fe790a69 100644 --- a/contracts/src/L1/migration/L1LockedMigrationController.sol +++ b/contracts/src/L1/migration/L1LockedMigrationController.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {INameWrapper, CAN_EXTEND_EXPIRY} from "@ens/contracts/wrapper/INameWrapper.sol"; import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; @@ -132,12 +133,12 @@ contract L1LockedMigrationController is IERC1155Receiver, ERC165 { migrationDataArray[i].transferData.roleBitmap = tokenRoles; // Ensure name data consistency for migration - string memory label = LibLabel.extractLabel( - migrationDataArray[i].transferData.dnsEncodedName + (bytes32 labelHash, ) = NameCoder.readLabel( + migrationDataArray[i].transferData.dnsEncodedName, + 0 ); - uint256 expectedTokenId = uint256(keccak256(bytes(label))); - if (tokenIds[i] != expectedTokenId) { - revert TokenIdMismatch(tokenIds[i], expectedTokenId); + if (tokenIds[i] != uint256(labelHash)) { + revert TokenIdMismatch(tokenIds[i], uint256(labelHash)); } // Process the locked name migration through bridge diff --git a/contracts/src/L1/migration/L1UnlockedMigrationController.sol b/contracts/src/L1/migration/L1UnlockedMigrationController.sol index 67dfe4e2..c3eafc25 100644 --- a/contracts/src/L1/migration/L1UnlockedMigrationController.sol +++ b/contracts/src/L1/migration/L1UnlockedMigrationController.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {INameWrapper, CANNOT_UNWRAP} from "@ens/contracts/wrapper/INameWrapper.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; @@ -9,7 +10,6 @@ import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC16 import {BridgeEncoderLib} from "../../common/bridge/libraries/BridgeEncoderLib.sol"; import {MigrationData} from "../../common/bridge/types/TransferData.sol"; import {UnauthorizedCaller} from "../../common/CommonErrors.sol"; -import {LibLabel} from "../../common/utils/LibLabel.sol"; import {L1BridgeController} from "../bridge/L1BridgeController.sol"; /** @@ -166,10 +166,9 @@ contract L1UnlockedMigrationController is IERC1155Receiver, IERC721Receiver, ERC */ function _migrateNameViaBridge(uint256 tokenId, MigrationData memory migrationData) internal { // Validate that tokenId matches the label hash - string memory label = LibLabel.extractLabel(migrationData.transferData.dnsEncodedName); - uint256 expectedTokenId = uint256(keccak256(bytes(label))); - if (tokenId != expectedTokenId) { - revert TokenIdMismatch(tokenId, expectedTokenId); + (bytes32 labelHash, ) = NameCoder.readLabel(migrationData.transferData.dnsEncodedName, 0); + if (tokenId != uint256(labelHash)) { + revert TokenIdMismatch(tokenId, uint256(labelHash)); } // Handle L1 migration by setting up the name locally diff --git a/contracts/src/L1/registry/MigratedWrappedNameRegistry.sol b/contracts/src/L1/registry/MigratedWrappedNameRegistry.sol index e0c0b354..305adc16 100644 --- a/contracts/src/L1/registry/MigratedWrappedNameRegistry.sol +++ b/contracts/src/L1/registry/MigratedWrappedNameRegistry.sol @@ -286,7 +286,7 @@ contract MigratedWrappedNameRegistry is ) internal view returns (string memory label) { // Extract the current label (leftmost, at offset 0) uint256 parentOffset; - (label, parentOffset) = LibLabel.extractLabel(dnsEncodedName, offset); + (label, parentOffset) = NameCoder.extractLabel(dnsEncodedName, offset); // Check if there's no parent (trying to migrate TLD) if (dnsEncodedName[parentOffset] == 0) { @@ -294,7 +294,7 @@ contract MigratedWrappedNameRegistry is } // Extract the parent label - (string memory parentLabel, uint256 grandparentOffset) = LibLabel.extractLabel( + (string memory parentLabel, uint256 grandparentOffset) = NameCoder.extractLabel( dnsEncodedName, parentOffset ); diff --git a/contracts/src/L1/resolver/ETHTLDResolver.sol b/contracts/src/L1/resolver/ETHTLDResolver.sol index 6c3f2f84..7bb14889 100755 --- a/contracts/src/L1/resolver/ETHTLDResolver.sol +++ b/contracts/src/L1/resolver/ETHTLDResolver.sol @@ -16,15 +16,13 @@ import {INameResolver} from "@ens/contracts/resolvers/profiles/INameResolver.sol import {IPubkeyResolver} from "@ens/contracts/resolvers/profiles/IPubkeyResolver.sol"; import {ITextResolver} from "@ens/contracts/resolvers/profiles/ITextResolver.sol"; import {ResolverFeatures} from "@ens/contracts/resolvers/ResolverFeatures.sol"; -import { - RegistryUtils as RegistryUtilsV1, - ENS -} from "@ens/contracts/universalResolver/RegistryUtils.sol"; +import {RegistryUtils} from "@ens/contracts/universalResolver/RegistryUtils.sol"; import {ResolverCaller} from "@ens/contracts/universalResolver/ResolverCaller.sol"; import {BytesUtils} from "@ens/contracts/utils/BytesUtils.sol"; import {ENSIP19, COIN_TYPE_ETH, COIN_TYPE_DEFAULT} from "@ens/contracts/utils/ENSIP19.sol"; import {IERC7996} from "@ens/contracts/utils/IERC7996.sol"; import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; +import {INameWrapper} from "@ens/contracts/wrapper/INameWrapper.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {GatewayFetcher} from "@unruggable/gateways/contracts/GatewayFetcher.sol"; @@ -34,26 +32,23 @@ import { } from "@unruggable/gateways/contracts/GatewayFetchTarget.sol"; import {GatewayRequest, EvalFlag} from "@unruggable/gateways/contracts/GatewayRequest.sol"; -import {IRegistryResolver} from "../../common/resolver/interfaces/IRegistryResolver.sol"; +import {BridgeRolesLib} from "../../common/bridge/libraries/BridgeRolesLib.sol"; import { DedicatedResolverLayoutLib } from "../../common/resolver/libraries/DedicatedResolverLayoutLib.sol"; import {LibLabel} from "../../common/utils/LibLabel.sol"; - -/// @dev The namehash of "eth". -bytes32 constant ETH_NODE = keccak256(abi.encode(bytes32(0), keccak256("eth"))); +import {L1BridgeController} from "../bridge/L1BridgeController.sol"; /// @notice Resolver that performs ".eth" resolutions for Namechain (via gateway) or V1 (via fallback). /// -/// Mainnet ".eth" resolutions do not reach this resolver unless there are no resolvers set. +/// Mainnet ".eth" resolutions do not reach this resolver unless there are no resolvers set. /// -/// 1. If there is an active V1 registration, resolve using Universal Resolver for V1. -/// 2. Otherwise, resolve using Namechain. -/// 3. If no resolver is found, reverts `UnreachableName`. +/// 1. If there is an active V1 registration, resolve using V1 Registry. +/// 2. Otherwise, resolve using Namechain. +/// 3. If no resolver is found, reverts `UnreachableName`. contract ETHTLDResolver is IExtendedResolver, IERC7996, - IRegistryResolver, GatewayFetchTarget, ResolverCaller, Ownable, @@ -71,11 +66,11 @@ contract ETHTLDResolver is /// @dev `GatewayRequest` exit code which indicates no resolver was found. uint8 private constant _EXIT_CODE_NO_RESOLVER = 2; - ENS public immutable REGISTRY_V1; + INameWrapper public immutable NAME_WRAPPER; IBaseRegistrar public immutable ETH_REGISTRAR_V1; - address public immutable BURN_ADDRESS_V1; + L1BridgeController public immutable L1_BRIDGE_CONTROLLER; address public immutable NAMECHAIN_DATASTORE; @@ -105,20 +100,20 @@ contract ETHTLDResolver is //////////////////////////////////////////////////////////////////////// constructor( - ENS registryV1_, - IGatewayProvider batchGatewayProvider_, - address burnAddressV1_, + INameWrapper nameWrapper, + IGatewayProvider batchGatewayProvider, + L1BridgeController l1BridgeController, address ethResolver_, IGatewayVerifier namechainVerifier_, - address namechainDatastore_, - address namechainEthRegistry_ + address namechainDatastore, + address namechainEthRegistry ) Ownable(msg.sender) CCIPReader(DEFAULT_UNSAFE_CALL_GAS) { - REGISTRY_V1 = registryV1_; - ETH_REGISTRAR_V1 = IBaseRegistrar(registryV1_.owner(ETH_NODE)); - BATCH_GATEWAY_PROVIDER = batchGatewayProvider_; - BURN_ADDRESS_V1 = burnAddressV1_; - NAMECHAIN_DATASTORE = namechainDatastore_; - NAMECHAIN_ETH_REGISTRY = namechainEthRegistry_; + NAME_WRAPPER = nameWrapper; + ETH_REGISTRAR_V1 = IBaseRegistrar(nameWrapper.ens().owner(NameCoder.ETH_NODE)); + BATCH_GATEWAY_PROVIDER = batchGatewayProvider; + L1_BRIDGE_CONTROLLER = l1BridgeController; + NAMECHAIN_DATASTORE = namechainDatastore; + NAMECHAIN_ETH_REGISTRY = namechainEthRegistry; ethResolver = ethResolver_; namechainVerifier = namechainVerifier_; @@ -131,7 +126,6 @@ contract ETHTLDResolver is return type(IExtendedResolver).interfaceId == interfaceId || type(IERC7996).interfaceId == interfaceId || - type(IRegistryResolver).interfaceId == interfaceId || super.supportsInterface(interfaceId); } @@ -156,15 +150,64 @@ contract ETHTLDResolver is ethResolver = resolver; } - /// @notice Same as `resolveWithRegistry()` but starts at "eth". + /// @notice Resolve `name` with the Namechain registry. + /// Checks Mainnet V1 before resolving on Namechain. function resolve( bytes calldata name, bytes calldata data - ) external view returns (bytes memory) { - return resolveWithRegistry(NAMECHAIN_ETH_REGISTRY, ETH_NODE, name, data); + ) external view returns (bytes memory result) { + (address resolver, bool offchain) = _determineResolver(name); + if (offchain) { + bytes[] memory calls; + bool multi = bytes4(data) == IMulticallable.multicall.selector; + if (multi) { + calls = abi.decode(data[4:], (bytes[])); + } else { + calls = new bytes[](1); + calls[0] = data; + } + result = _resolveNamechain(name, multi, calls); + } else { + callResolver(resolver, name, data, BATCH_GATEWAY_PROVIDER.gateways()); + } + } + + /// @dev Return `true` if `name` does not exist onchain. + function isResolverOffchain(bytes memory name) external view returns (bool offchain) { + (, offchain) = _determineResolver(name); + } + + /// @notice Determine underlying resolver and location for `name`. + /// @dev This function executes over multiple steps. + /// + /// @param name The DNS-encoded name. + /// + /// @return resolver The resolver address. + /// @return offchain If `true`, `resolver` is on Namechain, otherwise on Mainnet. + function getResolver( + bytes memory name + ) external view returns (address resolver, bool offchain) { + (resolver, offchain) = _determineResolver(name); + if (offchain) { + fetch( + namechainVerifier, + _createRequest(0, name), + this.getResolverCallback.selector // ==> step 2 + ); + } + } + + /// @notice CCIP-Read callback for `getResolver()`. + function getResolverCallback( + bytes[] calldata values, + uint8 /*exitCode*/, + bytes calldata /*extraData*/ + ) external pure returns (address resolver, bool offchain) { + resolver = abi.decode(values[1], (address)); + offchain = true; } - /// @dev CCIP-Read callback for `resolve()` from calling `namechainVerifier`. + /// @notice CCIP-Read callback for `resolve()`. /// /// @param values The outputs for `GatewayRequest`. /// @param exitCode The exit code for `GatewayRequest`. @@ -176,136 +219,95 @@ contract ETHTLDResolver is uint8 exitCode, bytes calldata extraData ) external pure returns (bytes memory) { - State memory state = abi.decode(extraData, (State)); + (bytes memory name, bool multi, bytes[] memory m) = abi.decode( + extraData, + (bytes, bool, bytes[]) + ); if (exitCode == _EXIT_CODE_NO_RESOLVER) { - revert UnreachableName(state.name); + revert UnreachableName(name); } - bytes memory defaultAddress = values[state.data.length]; // stored at end - if (state.multi) { - for (uint256 i; i < state.data.length; ++i) { - state.data[i] = _prepareResponse(state.data[i], values[i], defaultAddress); + bytes memory defaultAddress = values[m.length]; // stored at end + if (multi) { + for (uint256 i; i < m.length; ++i) { + m[i] = _prepareResponse(m[i], values[i], defaultAddress); } - return abi.encode(state.data); + return abi.encode(m); } else { - return _prepareResponse(state.data[0], values[0], defaultAddress); + return _prepareResponse(m[0], values[0], defaultAddress); } } /// @dev Determine if actively registered on V1. + /// /// @param labelHash The labelhash of the "eth" 2LD. + /// /// @return `true` if the registration is active. - function isActiveRegistrationV1(uint256 labelHash) public view returns (bool) { - // TODO: add final migration logic + function isActiveRegistrationV1(bytes32 labelHash) public view returns (bool) { return - ETH_REGISTRAR_V1.nameExpires(labelHash) >= block.timestamp && - ETH_REGISTRAR_V1.ownerOf(labelHash) != BURN_ADDRESS_V1; - } - - /// @notice Resolve `name` with the Namechain registry corresponding to `nodeSuffix`. - /// If `nodeSuffix` is "eth", checks Mainnet V1 before resolving on Namechain. - /// @inheritdoc IRegistryResolver - function resolveWithRegistry( - address parentRegistry, - bytes32 nodeSuffix, - bytes calldata name, - bytes calldata data - ) public view returns (bytes memory) { - (bool matched, , uint256 prevOffset, uint256 offset) = NameCoder.matchSuffix( - name, - 0, - nodeSuffix - ); - if (!matched) { - revert UnreachableName(name); - } - if (nodeSuffix == ETH_NODE) { - if (offset == prevOffset) { - callResolver(ethResolver, name, data, BATCH_GATEWAY_PROVIDER.gateways()); - } - (bytes32 labelHash, ) = NameCoder.readLabel(name, prevOffset); - if (isActiveRegistrationV1(uint256(labelHash))) { - (address resolver, , ) = RegistryUtilsV1.findResolver(REGISTRY_V1, name, 0); - callResolver(resolver, name, data, BATCH_GATEWAY_PROVIDER.gateways()); - } - } - bytes[] memory calls; - bool multi = bytes4(data) == IMulticallable.multicall.selector; - if (multi) { - calls = abi.decode(data[4:], (bytes[])); - } else { - calls = new bytes[](1); - calls[0] = data; - } - return _resolveNamechain(State(parentRegistry, name, offset, multi, calls)); + ETH_REGISTRAR_V1.nameExpires(uint256(labelHash)) >= block.timestamp && + !L1_BRIDGE_CONTROLLER.hasRootRoles( + BridgeRolesLib.ROLE_EJECTOR, + ETH_REGISTRAR_V1.ownerOf(uint256(labelHash)) + ); } - // solhint-disable namechain/ordering - /// @dev State of Namechain resolution. - struct State { - address registry; // starting parent registry - bytes name; - uint256 nameLength; // name[:nameLength] are the labels to resolve - bool multi; // true if multicall - bytes[] data; - } - // solhint-enable namechain/ordering + //////////////////////////////////////////////////////////////////////// + // Internal Functions + //////////////////////////////////////////////////////////////////////// - // solhint-disable private-vars-leading-underscore - /// @notice Resolve `state.name[:state.nameLength]` on Namechain starting at `state.registry`. + /// @dev Create `GatewayRequest` for registry traversal. /// - /// @dev This function executes over multiple steps. + /// @param outputs The number of outputs for the request. + /// @param name The DNS-encoded .eth name to resolve. /// - /// `GatewayRequest` walkthrough: - /// * The stack is loaded with labelhashes: - /// * "sub.vitalik" → `["sub", "vitalik"]`. - /// * `output[0]` is set to the Namechain "eth" registry. - /// * A traversal program is pushed onto the stack. - /// * `evalLoop(flags, count)` pops the program and executes it `count` times, - /// consuming one labelhash from the stack and passing it to the program in a separate context. - /// * The default `count` is the full stack. - /// * If `EvalFlag.STOP_ON_FAILURE`, the loop terminates when the program throws. - /// * Unless `EvalFlag.KEEP_ARGS`, `count` stack arguments are consumed, even when the loop terminates early. - /// * Before the program executes: - /// * The target is `namechainDatastore`. - /// * The slot is `SLOT_RD_ENTRIES`. - /// * The stack is `[labelhash]`. - /// * `output[0]` is the parent registry address. - /// * `output[1]` is the latest resolver address. - /// * `pushOutput(0)` adds the `registry` to the stack. - /// * The stack is `[labelHash, registry]`. - /// * `req.setSlot(SLOT_RD_ENTRIES).follow().follow()` ↔ `entries[registry][labelHash]`. - /// * `follow()` does a pop and uses the value as a mapping key. - /// * The program terminates if the next registry is expired. - /// * `output[1]` contains the resolver if one is set. - /// * The program terminates if the next registry is unset. - /// * `output[0]` contains the next registry in the chain. + /// `GatewayRequest` walkthrough: + /// * The stack is loaded with labelhashes: + /// * "sub.vitalik" → `["sub", "vitalik"]`. + /// * `output[0]` is set to the Namechain "eth" registry. + /// * A traversal program is pushed onto the stack. + /// * `evalLoop(flags, count)` pops the program and executes it `count` times, + /// consuming one labelhash from the stack and passing it to the program in a separate context. + /// * The default `count` is the full stack. + /// * If `EvalFlag.STOP_ON_FAILURE`, the loop terminates when the program throws. + /// * Unless `EvalFlag.KEEP_ARGS`, `count` stack arguments are consumed, even when the loop terminates early. + /// * Before the program executes: + /// * The target is `namechainDatastore`. + /// * The slot is `SLOT_RD_ENTRIES`. + /// * The stack is `[labelhash]`. + /// * `output[0]` is the parent registry address. + /// * `output[1]` is the latest resolver address. + /// * `pushOutput(0)` adds the `registry` to the stack. + /// * The stack is `[labelHash, registry]`. + /// * `req.setSlot(SLOT_RD_ENTRIES).follow().follow()` ↔ `entries[registry][labelHash]`. + /// * `follow()` does a pop and uses the value as a mapping key. + /// * The program terminates if the next registry is expired. + /// * `output[1]` contains the resolver if one is set. + /// * The program terminates if the next registry is unset. + /// * `output[0]` contains the next registry in the chain. /// - /// Pseudocode: - /// ``` - /// registry = - /// resolver = null - /// for label of name.slice(-length).split('.').reverse() - /// (reg, res) = datastore.getSubregistry(reg, label) - /// if (expired) break - /// if (res) resolver = res - /// if (!reg) break - /// registry = reg - /// ``` - function _resolveNamechain(State memory state) public view returns (bytes memory) { - // output[ 0] = registry - // output[ 1] = last non-zero resolver - // output[-1] = default address - uint8 max = uint8(state.data.length); - GatewayRequest memory req = GatewayFetcher.newRequest(max < 2 ? 2 : max + 1); - { - uint256 offset; - while (offset < state.nameLength) { - bytes32 labelHash; - (labelHash, offset) = NameCoder.readLabel(state.name, offset); - req.push(LibLabel.getCanonicalId(uint256(labelHash))); - } + /// Pseudocode: + /// ``` + /// registry = + /// resolver = null + /// for label of name.slice(-length).split('.').reverse() + /// (reg, res) = datastore.getSubregistry(reg, label) + /// if (expired) break + /// if (res) resolver = res + /// if (!reg) break + /// registry = reg + /// ``` + function _createRequest( + uint8 outputs, + bytes memory name + ) internal view returns (GatewayRequest memory req) { + req = GatewayFetcher.newRequest(outputs < 2 ? 2 : outputs); + uint256 offset; + while (offset < name.length - 5) { + bytes32 labelHash; // ^ "3eth0".length = 5 + (labelHash, offset) = NameCoder.readLabel(name, offset); + req.push(LibLabel.getCanonicalId(uint256(labelHash))); } - req.push(state.registry).setOutput(0); // starting point + req.push(NAMECHAIN_ETH_REGISTRY).setOutput(0); // starting point req.setTarget(NAMECHAIN_DATASTORE); req.setSlot(_SLOT_RD_ENTRIES); { @@ -326,11 +328,26 @@ contract ETHTLDResolver is req.push(cmd); } req.evalLoop(EvalFlag.STOP_ON_FAILURE | EvalFlag.KEEP_ARGS); // outputs = [registry, resolver] + } + + /// @notice Resolve `name` on Namechain. + /// + /// @dev This function executes over multiple steps. + function _resolveNamechain( + bytes memory name, + bool multi, + bytes[] memory m + ) internal view returns (bytes memory) { + // output[ 0] = registry + // output[ 1] = last non-zero resolver + // output[-1] = default address + uint8 max = uint8(m.length); + GatewayRequest memory req = _createRequest(max + 1, name); req.pushOutput(1).requireNonzero(_EXIT_CODE_NO_RESOLVER).target(); // target resolver req.push(bytes("")).dup().setOutput(0).setOutput(1); // clear outputs uint8 errorCount; // number of errors - for (uint8 i; i < state.data.length; ++i) { - bytes memory v = state.data[i]; + for (uint8 i; i < m.length; ++i) { + bytes memory v = m[i]; bytes4 selector = bytes4(v); // NOTE: "node check" is NOT performed: // if (v.length < 36 || BytesUtils.readBytes32(v, 4) != node) { @@ -399,19 +416,16 @@ contract ETHTLDResolver is continue; } else { ++errorCount; - state.data[i] = abi.encodeWithSelector( - UnsupportedResolverProfile.selector, - selector - ); + m[i] = abi.encodeWithSelector(UnsupportedResolverProfile.selector, selector); continue; } req.setOutput(i); } if (errorCount == max) { - if (state.multi) { - return abi.encode(state.data); // all calls failed + if (multi) { + return abi.encode(m); // all calls failed } else { - bytes memory v = state.data[0]; + bytes memory v = m[0]; assembly { revert(add(v, 32), mload(v)) // revert with the call that failed } @@ -428,15 +442,33 @@ contract ETHTLDResolver is namechainVerifier, req, this.resolveNamechainCallback.selector, // ==> step 2 - abi.encode(state), + abi.encode(name, multi, m), new string[](0) ); } - // solhint-enable private-vars-leading-underscore - //////////////////////////////////////////////////////////////////////// - // Internal Functions - //////////////////////////////////////////////////////////////////////// + /// @dev Determine underlying resolver and location for `name`. + function _determineResolver( + bytes memory name + ) internal view returns (address resolver, bool offchain) { + (bool matched, , uint256 prevOffset, uint256 offset) = NameCoder.matchSuffix( + name, + 0, + NameCoder.ETH_NODE + ); + if (!matched) { + revert UnreachableName(name); + } + if (offset == prevOffset) { + return (ethResolver, false); + } + (bytes32 labelHash, ) = NameCoder.readLabel(name, prevOffset); + if (isActiveRegistrationV1(labelHash)) { + (resolver, , ) = RegistryUtils.findResolver(NAME_WRAPPER.ens(), name, 0); + return (resolver, false); + } + return (address(0), true); + } /// @dev Prepare response based on the request. /// diff --git a/contracts/src/L2/bridge/L2BridgeController.sol b/contracts/src/L2/bridge/L2BridgeController.sol index 8d6ce625..2f7e1312 100644 --- a/contracts/src/L2/bridge/L2BridgeController.sol +++ b/contracts/src/L2/bridge/L2BridgeController.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + import {EjectionController} from "../../common/bridge/EjectionController.sol"; import {IBridge} from "../../common/bridge/interfaces/IBridge.sol"; import {BridgeEncoderLib} from "../../common/bridge/libraries/BridgeEncoderLib.sol"; @@ -12,7 +14,6 @@ import {IRegistry} from "../../common/registry/interfaces/IRegistry.sol"; import {IRegistryDatastore} from "../../common/registry/interfaces/IRegistryDatastore.sol"; import {ITokenObserver} from "../../common/registry/interfaces/ITokenObserver.sol"; import {RegistryRolesLib} from "../../common/registry/libraries/RegistryRolesLib.sol"; -import {LibLabel} from "../../common/utils/LibLabel.sol"; /** * @title L2BridgeController @@ -65,7 +66,7 @@ contract L2BridgeController is EjectionController, ITokenObserver { function completeEjectionToL2( TransferData memory transferData ) external virtual onlyRootRoles(BridgeRolesLib.ROLE_EJECTOR) { - string memory label = LibLabel.extractLabel(transferData.dnsEncodedName); + string memory label = NameCoder.firstLabel(transferData.dnsEncodedName); (uint256 tokenId, ) = REGISTRY.getNameData(label); // owner should be the bridge controller diff --git a/contracts/src/common/bridge/EjectionController.sol b/contracts/src/common/bridge/EjectionController.sol index 5339b391..a667d8f8 100644 --- a/contracts/src/common/bridge/EjectionController.sol +++ b/contracts/src/common/bridge/EjectionController.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; @@ -139,7 +140,7 @@ abstract contract EjectionController is IERC1155Receiver, ERC165, EnhancedAccess uint256 tokenId, bytes memory dnsEncodedName ) internal pure { - string memory label = LibLabel.extractLabel(dnsEncodedName); + string memory label = NameCoder.firstLabel(dnsEncodedName); if (LibLabel.labelToCanonicalId(label) != LibLabel.getCanonicalId(tokenId)) { revert InvalidLabel(tokenId, label); } diff --git a/contracts/src/common/utils/LibLabel.sol b/contracts/src/common/utils/LibLabel.sol index 08c1cf6d..68ad4eba 100644 --- a/contracts/src/common/utils/LibLabel.sol +++ b/contracts/src/common/utils/LibLabel.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; -import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; - library LibLabel { /// @dev Convert a label to canonical id. /// @@ -21,42 +19,4 @@ library LibLabel { function getCanonicalId(uint256 id) internal pure returns (uint256) { return id ^ uint32(id); } - - /// @dev DNS encodes a label as a .eth second-level domain. - /// - /// @param label The label to encode (e.g., "test" becomes "\x04test\x03eth\x00"). - /// - /// @return The DNS-encoded name. - function dnsEncodeEthLabel(string memory label) internal pure returns (bytes memory) { - return abi.encodePacked(bytes1(uint8(bytes(label).length)), label, "\x03eth\x00"); - } - - /// @dev Extracts a label from a DNS-encoded name at the given offset. - /// - /// @param dnsEncodedName The DNS-encoded name to extract from. - /// @param offset The offset in the DNS-encoded name to start reading from. - /// - /// @return label The extracted label as a string. - /// @return nextOffset The offset to the next label in the DNS-encoded name. - function extractLabel( - bytes memory dnsEncodedName, - uint256 offset - ) internal pure returns (string memory label, uint256 nextOffset) { - (, uint256 _nextOffset, uint8 size, ) = NameCoder.readLabel(dnsEncodedName, offset, false); - nextOffset = _nextOffset; - label = new string(size); - assembly { - mcopy(add(label, 32), add(add(dnsEncodedName, 33), offset), size) - } - } - - /// @dev Extracts the first label from a DNS-encoded name. - /// - /// @param dnsEncodedName The DNS-encoded name to extract from. - /// - /// @return The extracted label as a string. - function extractLabel(bytes memory dnsEncodedName) internal pure returns (string memory) { - (string memory label, ) = extractLabel(dnsEncodedName, 0); - return label; - } } diff --git a/contracts/src/common/utils/LibMem.sol b/contracts/src/common/utils/LibMem.sol new file mode 100644 index 00000000..0b9694f6 --- /dev/null +++ b/contracts/src/common/utils/LibMem.sol @@ -0,0 +1,42 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.25; + +// solhint-disable no-inline-assembly + +library LibMem { + bool public constant REMAPPED = true; + + /// @dev Copy `mem[src:src+len]` to `mem[dst:dst+len]`. + /// Equivalent to `mcopy()`. + /// + /// @param src The source memory offset. + /// @param dst The destination memory offset. + /// @param len The number of bytes to copy. + function copy(uint256 dst, uint256 src, uint256 len) internal pure { + assembly ("memory-safe") { + mcopy(dst, src, len) + } + } + + /// @dev Convert bytes to a memory offset. + /// + /// @param v The bytes to convert. + /// + /// @return ret The corresponding memory offset. + function ptr(bytes memory v) internal pure returns (uint256 ret) { + assembly ("memory-safe") { + ret := add(v, 32) + } + } + + /// @dev Read word at memory offset. + /// + /// @param src The memory offset. + /// + /// @return ret The read word. + function load(uint256 src) internal pure returns (uint256 ret) { + assembly ("memory-safe") { + ret := mload(src) + } + } +} diff --git a/contracts/src/universalResolver/libraries/LibRegistry.sol b/contracts/src/universalResolver/libraries/LibRegistry.sol index ffc4aaef..7696b809 100644 --- a/contracts/src/universalResolver/libraries/LibRegistry.sol +++ b/contracts/src/universalResolver/libraries/LibRegistry.sol @@ -34,7 +34,7 @@ library LibRegistry { (exactRegistry, resolver, node, resolverOffset) = findResolver(rootRegistry, name, next); // if there was a parent registry... if (address(exactRegistry) != address(0)) { - string memory label = readLabel(name, offset); + (string memory label, ) = NameCoder.extractLabel(name, offset); // remember the resolver (if it exists) address res = exactRegistry.getResolver(label); if (res != address(0)) { @@ -63,7 +63,7 @@ library LibRegistry { } IRegistry parent = findExactRegistry(rootRegistry, name, next); if (address(parent) != address(0)) { - string memory label = readLabel(name, offset); + (string memory label, ) = NameCoder.extractLabel(name, offset); exactRegistry = parent.getSubregistry(label); } } @@ -84,22 +84,4 @@ library LibRegistry { parentRegistry = findExactRegistry(rootRegistry, name, next); } } - - /// @dev Read label at offset from a DNS-encoded name. - /// eg. `readLabel("\x03abc\x00", 0) = "abc"`. - /// - /// @param name The DNS-encoded name. - /// @param offset The offset into `name`. - /// - /// @return label The label. - function readLabel( - bytes memory name, - uint256 offset - ) internal pure returns (string memory label) { - (uint8 size, ) = NameCoder.nextLabel(name, offset); - label = new string(size); - assembly { - mcopy(add(label, 32), add(add(name, 33), offset), size) - } - } } diff --git a/contracts/test/Remappings.t.sol b/contracts/test/Remappings.t.sol new file mode 100644 index 00000000..38488f46 --- /dev/null +++ b/contracts/test/Remappings.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {LibMem} from "~src/common/utils/LibMem.sol"; + +/// @dev Ensure remappings.txt is applied correctly. +/// +/// Note: Forge does not recompile changes to file remappings as expected. +/// You must induce a recompile via clean or file change. +/// +/// 1. Check UnsafeCopyLib is remapped to get `mcopy`. +/// +contract RemappingsTest is Test { + function test_LibMem() external pure { + assertTrue(LibMem.REMAPPED); + } +} diff --git a/contracts/test/integration/bridge/BridgeTest.t.sol b/contracts/test/integration/bridge/BridgeTest.t.sol index a2a09cf8..27923fbc 100644 --- a/contracts/test/integration/bridge/BridgeTest.t.sol +++ b/contracts/test/integration/bridge/BridgeTest.t.sol @@ -5,6 +5,8 @@ pragma solidity >=0.8.13; import {Test} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + import { EnhancedAccessControl, EACBaseRolesLib @@ -95,7 +97,7 @@ contract BridgeTest is Test, EnhancedAccessControl { ); TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel("premiumname"), + dnsEncodedName: NameCoder.ethName("premiumname"), owner: user2, subregistry: address(0x123), resolver: address(0x456), diff --git a/contracts/test/integration/fixtures/deployV1Fixture.ts b/contracts/test/integration/fixtures/deployV1Fixture.ts index c054fef0..09bff774 100755 --- a/contracts/test/integration/fixtures/deployV1Fixture.ts +++ b/contracts/test/integration/fixtures/deployV1Fixture.ts @@ -64,6 +64,11 @@ export async function deployV1Fixture( labelhash("eth"), ethRegistrar.address, ]); + const nameWrapper = await network.viem.deployContract("NameWrapper", [ + ensRegistry.address, + ethRegistrar.address, + zeroAddress, // IMetadataService + ]); return { network, publicClient, @@ -74,6 +79,7 @@ export async function deployV1Fixture( publicResolver, batchGatewayProvider, universalResolver, + nameWrapper, setupName, }; // clobbers registry ownership up to name diff --git a/contracts/test/integration/l1/ETHTLDResolver.test.ts b/contracts/test/integration/l1/ETHTLDResolver.test.ts index ca0ffbc2..b60afee6 100755 --- a/contracts/test/integration/l1/ETHTLDResolver.test.ts +++ b/contracts/test/integration/l1/ETHTLDResolver.test.ts @@ -90,13 +90,15 @@ async function fixture() { libs: { GatewayVM }, }); const ethResolver = await mainnetV2.deployDedicatedResolver(); - const burnAddressV1 = "0x000000000000000000000000000000000000FadE"; + const mockBridgeController = await chain1.viem.deployContract( + "MockBridgeController", + ); const ethTLDResolver = await chain1.viem.deployContract( "ETHTLDResolver", [ - mainnetV1.ensRegistry.address, + mainnetV1.nameWrapper.address, mainnetV1.batchGatewayProvider.address, - burnAddressV1, + mockBridgeController.address, ethResolver.address, verifierAddress, namechain.datastore.address, @@ -112,7 +114,7 @@ async function fixture() { ethTLDResolver, ethResolver, mainnetV1, - burnAddressV1, + mockBridgeController, mainnetV2, namechain, gateway, @@ -146,12 +148,7 @@ describe("ETHTLDResolver", () => { shouldSupportInterfaces({ contract: () => loadFixture().then((F) => F.ethTLDResolver), - interfaces: [ - "IERC165", - "IERC7996", - "IExtendedResolver", - "IRegistryResolver", - ], + interfaces: ["IERC165", "IERC7996", "IExtendedResolver"], }); shouldSupportFeatures({ @@ -313,7 +310,7 @@ describe("ETHTLDResolver", () => { const tokenId = BigInt(labelhash(getLabelAt(kp.name, -2))); await F.mainnetV1.ethRegistrar.write.safeTransferFrom([ F.mainnetV1.walletClient.account.address, - F.burnAddressV1, + F.mockBridgeController.address, tokenId, ]); const available = await F.mainnetV1.ethRegistrar.read.available([ diff --git a/contracts/test/integration/l1/dns/rr.ts b/contracts/test/integration/l1/dns/rr.ts index 7ae0cfe0..202ac43e 100644 --- a/contracts/test/integration/l1/dns/rr.ts +++ b/contracts/test/integration/l1/dns/rr.ts @@ -1,5 +1,5 @@ -import { type ByteArray, concat, stringToBytes, toHex } from "viem"; -import { packetToBytes } from "../../../utils/utils.js"; +import { type ByteArray, concat, hexToBytes, stringToBytes, toHex } from "viem"; +import { dnsEncodeName } from "../../../utils/utils.ts"; export type RR = { name: ByteArray; @@ -34,7 +34,7 @@ export function encodeRRs(rr: RR[]) { export function makeTXT(name: string, txt: string): RR { return { - name: packetToBytes(name), + name: hexToBytes(dnsEncodeName(name)), class: 1, // CLASS_INET type: 16, // QTYPE_TXT data: encodeTXT(txt), diff --git a/contracts/test/mocks/MockBridgeController.sol b/contracts/test/mocks/MockBridgeController.sol new file mode 100644 index 00000000..4d8fb620 --- /dev/null +++ b/contracts/test/mocks/MockBridgeController.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; + +// used by ETHTLDResolver.test.ts +contract MockBridgeController is ERC721Holder, ERC1155Holder { + function hasRootRoles(uint256, address account) external view returns (bool) { + // emulate L1BridgeController + // appear as authorized ejector + // but we hold the burned tokens + return account == address(this); + } +} diff --git a/contracts/test/unit/L1/bridge/L1BridgeController.t.sol b/contracts/test/unit/L1/bridge/L1BridgeController.t.sol index ce72a98a..f43812c7 100644 --- a/contracts/test/unit/L1/bridge/L1BridgeController.t.sol +++ b/contracts/test/unit/L1/bridge/L1BridgeController.t.sol @@ -5,6 +5,7 @@ pragma solidity >=0.8.13; import {Test, Vm} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import { @@ -128,7 +129,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // Create ejection data for this specific label TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label), + dnsEncodedName: NameCoder.ethName(label), owner: address(1), subregistry: address(2), resolver: address(3), @@ -160,7 +161,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // Create a locked name (without any subregistry roles) TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label), + dnsEncodedName: NameCoder.ethName(label), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -230,7 +231,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // Create a name with only admin role via bridge migration TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label), + dnsEncodedName: NameCoder.ethName(label), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -311,7 +312,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { uint256 roleBitmap ) internal pure returns (bytes memory) { TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label), + dnsEncodedName: NameCoder.ethName(label), owner: l2Owner, subregistry: l2Subregistry, resolver: l2Resolver, @@ -345,7 +346,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { for (uint256 i = 0; i < l2Owners.length; i++) { transferDataArray[i] = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(labels[i]), + dnsEncodedName: NameCoder.ethName(labels[i]), owner: l2Owners[i], subregistry: l2Subregistries[i], resolver: l2Resolvers[i], @@ -392,7 +393,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_eject_from_namechain_unlocked() public { uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -413,7 +414,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { RegistryRolesLib.ROLE_SET_SUBREGISTRY; address subregistry = address(0x1234); TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: subregistry, resolver: MOCK_RESOLVER, @@ -443,7 +444,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -478,7 +479,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // First register the name uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -500,7 +501,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_updateExpiration() public { uint64 expiryTime = uint64(block.timestamp) + 100; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -530,7 +531,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_updateExpiration_emits_event() public { uint64 expiryTime = uint64(block.timestamp) + 100; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -570,7 +571,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_Revert_updateExpiration_expired_name() public { uint64 expiryTime = uint64(block.timestamp) + 100; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -593,7 +594,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_Revert_updateExpiration_reduce_expiry() public { uint64 initialExpiry = uint64(block.timestamp) + 200; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -980,7 +981,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { for (uint256 i = 0; i < l2Owners.length; i++) { transferDataArray[i] = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(labels[i]), + dnsEncodedName: NameCoder.ethName(labels[i]), owner: l2Owners[i], subregistry: l2Subregistries[i], resolver: l2Resolvers[i], @@ -995,7 +996,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_Revert_completeEjectionToL1_not_bridge() public { uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -1018,7 +1019,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_Revert_syncRenewal_not_bridge() public { uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -1047,7 +1048,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { function test_completeEjectionToL1() public { uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -1084,7 +1085,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // First, migrate a locked name (locked names don't have ROLE_SET_SUBREGISTRY) uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -1120,7 +1121,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // First, migrate a locked name (locked names don't have ROLE_SET_SUBREGISTRY) uint64 expiryTime = uint64(block.timestamp) + 86400; TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(this), subregistry: address(registry), resolver: MOCK_RESOLVER, @@ -1154,7 +1155,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { // Create batch transfer data TransferData[] memory transferDataArray = new TransferData[](2); transferDataArray[0] = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: address(1), subregistry: address(2), resolver: address(3), @@ -1162,7 +1163,7 @@ contract L1BridgeControllerTest is Test, ERC1155Holder, EnhancedAccessControl { expires: uint64(block.timestamp + 86400) }); transferDataArray[1] = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel("test2"), + dnsEncodedName: NameCoder.ethName("test2"), owner: address(1), subregistry: address(2), resolver: address(3), diff --git a/contracts/test/unit/L1/migration/L1LockedMigrationController.t.sol b/contracts/test/unit/L1/migration/L1LockedMigrationController.t.sol index 9bb43db2..a62e7689 100644 --- a/contracts/test/unit/L1/migration/L1LockedMigrationController.t.sol +++ b/contracts/test/unit/L1/migration/L1LockedMigrationController.t.sol @@ -5,6 +5,7 @@ pragma solidity >=0.8.13; import {Test} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {ENS} from "@ens/contracts/registry/ENS.sol"; import { INameWrapper, @@ -30,7 +31,6 @@ import {IRegistry} from "~src/common/registry/interfaces/IRegistry.sol"; import {IRegistryMetadata} from "~src/common/registry/interfaces/IRegistryMetadata.sol"; import {RegistryRolesLib} from "~src/common/registry/libraries/RegistryRolesLib.sol"; import {RegistryDatastore} from "~src/common/registry/RegistryDatastore.sol"; -import {LibLabel} from "~src/common/utils/LibLabel.sol"; import {L1BridgeController} from "~src/L1/bridge/L1BridgeController.sol"; import {L1LockedMigrationController} from "~src/L1/migration/L1LockedMigrationController.sol"; import {LockedNamesLib} from "~src/L1/migration/libraries/LockedNamesLib.sol"; @@ -164,7 +164,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -206,7 +206,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data - the roleBitmap should be ignored completely MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -273,7 +273,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -299,7 +299,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -325,7 +325,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Use wrong label that doesn't match tokenId MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel("wronglabel"), // This won't match testTokenId + dnsEncodedName: NameCoder.ethName("wronglabel"), // This won't match testTokenId owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -354,7 +354,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { function test_Revert_unauthorized_caller() public { MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -392,11 +392,11 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // DNS encode each label as .eth domain bytes memory dnsEncodedName; if (i == 0) { - dnsEncodedName = LibLabel.dnsEncodeEthLabel("test1"); + dnsEncodedName = NameCoder.ethName("test1"); } else if (i == 1) { - dnsEncodedName = LibLabel.dnsEncodeEthLabel("test2"); + dnsEncodedName = NameCoder.ethName("test2"); } else { - dnsEncodedName = LibLabel.dnsEncodeEthLabel("test3"); + dnsEncodedName = NameCoder.ethName("test3"); } migrationDataArray[i] = MigrationData({ @@ -454,7 +454,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { uint256 saltData = uint256(keccak256(abi.encodePacked(testLabel, uint256(999)))); MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -497,7 +497,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data - incoming roleBitmap should be ignored MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -562,7 +562,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -612,7 +612,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -677,7 +677,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -742,7 +742,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -798,7 +798,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -825,7 +825,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data with user as owner MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -878,7 +878,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), @@ -929,7 +929,7 @@ contract L1LockedMigrationControllerTest is Test, ERC1155Holder { // Prepare migration data MigrationData memory migrationData = MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: user, subregistry: address(0), // Will be created by factory resolver: address(0xABCD), diff --git a/contracts/test/unit/L1/migration/L1UnlockedMigrationController.t.sol b/contracts/test/unit/L1/migration/L1UnlockedMigrationController.t.sol index 87f9ae5d..5a02c73d 100644 --- a/contracts/test/unit/L1/migration/L1UnlockedMigrationController.t.sol +++ b/contracts/test/unit/L1/migration/L1UnlockedMigrationController.t.sol @@ -5,6 +5,7 @@ pragma solidity >=0.8.13; import {Test, Vm} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {INameWrapper, CANNOT_UNWRAP} from "@ens/contracts/wrapper/INameWrapper.sol"; import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; @@ -127,7 +128,7 @@ contract L1UnlockedMigrationControllerTest is Test, ERC1155Holder, ERC721Holder return MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label), + dnsEncodedName: NameCoder.ethName(label), owner: address(0), subregistry: address(0), resolver: address(0), @@ -149,7 +150,7 @@ contract L1UnlockedMigrationControllerTest is Test, ERC1155Holder, ERC721Holder return MigrationData({ transferData: TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label), + dnsEncodedName: NameCoder.ethName(label), owner: address(0x1111), subregistry: address(0x2222), resolver: address(0x3333), @@ -188,7 +189,7 @@ contract L1UnlockedMigrationControllerTest is Test, ERC1155Holder, ERC721Holder (TransferData memory decodedTransferData) = BridgeEncoderLib.decodeEjection( message ); - string memory decodedLabel = LibLabel.extractLabel( + string memory decodedLabel = NameCoder.firstLabel( decodedTransferData.dnsEncodedName ); if (keccak256(bytes(decodedLabel)) == keccak256(bytes(expectedLabel))) { @@ -232,7 +233,7 @@ contract L1UnlockedMigrationControllerTest is Test, ERC1155Holder, ERC721Holder (TransferData memory decodedTransferData) = BridgeEncoderLib.decodeEjection( message ); - string memory decodedLabel = LibLabel.extractLabel( + string memory decodedLabel = NameCoder.firstLabel( decodedTransferData.dnsEncodedName ); uint256 emittedTokenId = uint256(keccak256(bytes(decodedLabel))); diff --git a/contracts/test/unit/L1/registry/MigratedWrappedNameRegistry.t.sol b/contracts/test/unit/L1/registry/MigratedWrappedNameRegistry.t.sol index c5109909..2e3635f3 100644 --- a/contracts/test/unit/L1/registry/MigratedWrappedNameRegistry.t.sol +++ b/contracts/test/unit/L1/registry/MigratedWrappedNameRegistry.t.sol @@ -174,7 +174,7 @@ contract MigratedWrappedNameRegistryTest is Test { testLabelId = LibLabel.labelToCanonicalId(testLabel); // Setup v1 resolver in ENS registry - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel(testLabel); + bytes memory dnsEncodedName = NameCoder.ethName(testLabel); bytes32 node = NameCoder.namehash(dnsEncodedName, 0); ensRegistry.setResolver(node, v1Resolver); } @@ -260,7 +260,7 @@ contract MigratedWrappedNameRegistryTest is Test { function test_getResolver_ens_registry_returns_zero() public { // Clear the resolver in ENS registry - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel(testLabel); + bytes memory dnsEncodedName = NameCoder.ethName(testLabel); bytes32 node = NameCoder.namehash(dnsEncodedName, 0); ensRegistry.setResolver(node, address(0)); @@ -274,8 +274,8 @@ contract MigratedWrappedNameRegistryTest is Test { string memory label2 = "bar"; // Setup different resolvers in ENS registry - bytes32 node1 = NameCoder.namehash(LibLabel.dnsEncodeEthLabel(label1), 0); - bytes32 node2 = NameCoder.namehash(LibLabel.dnsEncodeEthLabel(label2), 0); + bytes32 node1 = NameCoder.namehash(NameCoder.ethName(label1), 0); + bytes32 node2 = NameCoder.namehash(NameCoder.ethName(label2), 0); ensRegistry.setResolver(node1, address(0x1111)); ensRegistry.setResolver(node2, address(0x2222)); diff --git a/contracts/test/unit/L2/bridge/L2BridgeController.t.sol b/contracts/test/unit/L2/bridge/L2BridgeController.t.sol index 27d0c7b2..698ffc17 100644 --- a/contracts/test/unit/L2/bridge/L2BridgeController.t.sol +++ b/contracts/test/unit/L2/bridge/L2BridgeController.t.sol @@ -5,6 +5,7 @@ pragma solidity >=0.8.13; import {Test, Vm} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; @@ -121,7 +122,7 @@ contract TestL2BridgeController is Test, ERC1155Holder { uint256 roleBitmap ) internal pure returns (bytes memory) { TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(nameLabel), + dnsEncodedName: NameCoder.ethName(nameLabel), owner: _owner, subregistry: subregistry, resolver: _resolver, @@ -237,7 +238,7 @@ contract TestL2BridgeController is Test, ERC1155Holder { uint256 differentRoles = RegistryRolesLib.ROLE_RENEW | RegistryRolesLib.ROLE_REGISTRAR; vm.recordLogs(); TransferData memory migrationData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(label2), + dnsEncodedName: NameCoder.ethName(label2), owner: l2Owner, subregistry: l2Subregistry, resolver: l2Resolver, @@ -312,7 +313,7 @@ contract TestL2BridgeController is Test, ERC1155Holder { vm.expectRevert(abi.encodeWithSelector(L2BridgeController.NotTokenOwner.selector, tokenId)); // Call the external method which should revert TransferData memory migrationData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: l2Owner, subregistry: l2Subregistry, resolver: l2Resolver, @@ -327,7 +328,7 @@ contract TestL2BridgeController is Test, ERC1155Holder { function test_Revert_completeEjectionToL2_not_bridge() public { // Try to call completeEjectionToL2 directly (without proper role) TransferData memory transferData = TransferData({ - dnsEncodedName: LibLabel.dnsEncodeEthLabel(testLabel), + dnsEncodedName: NameCoder.ethName(testLabel), owner: l2Owner, subregistry: l2Subregistry, resolver: l2Resolver, diff --git a/contracts/test/unit/common/bridge/BridgeMessages.t.sol b/contracts/test/unit/common/bridge/BridgeMessages.t.sol index 90d07726..ab2574ff 100644 --- a/contracts/test/unit/common/bridge/BridgeMessages.t.sol +++ b/contracts/test/unit/common/bridge/BridgeMessages.t.sol @@ -5,6 +5,8 @@ pragma solidity >=0.8.13; import {Test, Vm} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + import {EACBaseRolesLib} from "~src/common/access-control/EnhancedAccessControl.sol"; import {BridgeMessageType} from "~src/common/bridge/interfaces/IBridge.sol"; import {BridgeEncoderLib} from "~src/common/bridge/libraries/BridgeEncoderLib.sol"; @@ -13,7 +15,6 @@ import {TransferData} from "~src/common/bridge/types/TransferData.sol"; import {IRegistryMetadata} from "~src/common/registry/interfaces/IRegistryMetadata.sol"; import {PermissionedRegistry} from "~src/common/registry/PermissionedRegistry.sol"; import {RegistryDatastore} from "~src/common/registry/RegistryDatastore.sol"; -import {LibLabel} from "~src/common/utils/LibLabel.sol"; import {L1BridgeController} from "~src/L1/bridge/L1BridgeController.sol"; import {L2BridgeController} from "~src/L2/bridge/L2BridgeController.sol"; import {MockBridgeBase} from "~test/mocks/MockBridgeBase.sol"; @@ -77,7 +78,7 @@ contract BridgeMessagesTest is Test { } function test_encodeDecodeEjection() public view { - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel(testLabel); + bytes memory dnsEncodedName = NameCoder.ethName(testLabel); TransferData memory transferData = TransferData({ dnsEncodedName: dnsEncodedName, owner: testOwner, @@ -200,7 +201,7 @@ contract BridgeMessagesTest is Test { } function test_l2Bridge_sendMessage_ejection() public { - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel(testLabel); + bytes memory dnsEncodedName = NameCoder.ethName(testLabel); bytes memory ejectionMessage = BridgeEncoderLib.encodeEjection( TransferData({ dnsEncodedName: dnsEncodedName, diff --git a/contracts/test/unit/common/bridge/libraries/BridgeEncoderLib.t.sol b/contracts/test/unit/common/bridge/libraries/BridgeEncoderLib.t.sol index 3a08a1e8..9f01d6ab 100644 --- a/contracts/test/unit/common/bridge/libraries/BridgeEncoderLib.t.sol +++ b/contracts/test/unit/common/bridge/libraries/BridgeEncoderLib.t.sol @@ -5,10 +5,11 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + import {BridgeMessageType} from "~src/common/bridge/interfaces/IBridge.sol"; import {BridgeEncoderLib} from "~src/common/bridge/libraries/BridgeEncoderLib.sol"; import {TransferData} from "~src/common/bridge/types/TransferData.sol"; -import {LibLabel} from "~src/common/utils/LibLabel.sol"; // Wrapper contract to properly test library errors contract BridgeEncoderWrapper { @@ -31,7 +32,7 @@ contract BridgeEncoderLibTest is Test { } function testEncodeEjection() public view { - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel("test"); + bytes memory dnsEncodedName = NameCoder.ethName("test"); TransferData memory transferData = TransferData({ dnsEncodedName: dnsEncodedName, owner: address(0x123), @@ -79,7 +80,7 @@ contract BridgeEncoderLibTest is Test { function testDecodeEjectionInvalidMessageType() public { // Create a message with wrong message type but correct structure // to test the custom error (not ABI decoding error) - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel("test"); + bytes memory dnsEncodedName = NameCoder.ethName("test"); TransferData memory transferData = TransferData({ dnsEncodedName: dnsEncodedName, owner: address(0x123), @@ -118,7 +119,7 @@ contract BridgeEncoderLibTest is Test { } function testGetMessageType() public view { - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel("test"); + bytes memory dnsEncodedName = NameCoder.ethName("test"); TransferData memory transferData = TransferData({ dnsEncodedName: dnsEncodedName, owner: address(0x123), @@ -143,7 +144,7 @@ contract BridgeEncoderLibTest is Test { function testEncodingStructure() public view { // Test that the new encoding structure works correctly - bytes memory dnsEncodedName = LibLabel.dnsEncodeEthLabel("structuretest"); + bytes memory dnsEncodedName = NameCoder.ethName("structuretest"); TransferData memory transferData = TransferData({ dnsEncodedName: dnsEncodedName, owner: address(0x999), diff --git a/contracts/test/unit/common/utils/LibLabel.t.sol b/contracts/test/unit/common/utils/LibLabel.t.sol index 13a8ff6b..713f5bdd 100644 --- a/contracts/test/unit/common/utils/LibLabel.t.sol +++ b/contracts/test/unit/common/utils/LibLabel.t.sol @@ -7,1096 +7,19 @@ import {Test} from "forge-std/Test.sol"; import {LibLabel} from "~src/common/utils/LibLabel.sol"; -// Wrapper contract to properly test library functions -contract LibLabelWrapper { - function labelToCanonicalId(string memory label) external pure returns (uint256) { - return LibLabel.labelToCanonicalId(label); - } - - function getCanonicalId(uint256 id) external pure returns (uint256) { - return LibLabel.getCanonicalId(id); - } - - function dnsEncodeEthLabel(string memory label) external pure returns (bytes memory) { - return LibLabel.dnsEncodeEthLabel(label); - } - - function extractLabel( - bytes memory dnsEncodedName, - uint256 offset - ) external pure returns (string memory label, uint256 nextOffset) { - return LibLabel.extractLabel(dnsEncodedName, offset); - } - - function extractLabel(bytes memory dnsEncodedName) external pure returns (string memory) { - return LibLabel.extractLabel(dnsEncodedName); - } -} - contract LibLabelTest is Test { - LibLabelWrapper _wrapper; - - function setUp() public { - _wrapper = new LibLabelWrapper(); - } - - // Test labelToCanonicalId function - function test_labelToCanonicalId_BasicLabels() public view { - // Test common labels - uint256 testId = _wrapper.labelToCanonicalId("test"); - uint256 expectedHash = uint256(keccak256(bytes("test"))); - uint256 expected = expectedHash ^ uint32(expectedHash); - assertEq(testId, expected); - - uint256 aliceId = _wrapper.labelToCanonicalId("alice"); - expectedHash = uint256(keccak256(bytes("alice"))); - expected = expectedHash ^ uint32(expectedHash); - assertEq(aliceId, expected); - } - - function test_labelToCanonicalId_EmptyString() public view { - uint256 emptyId = _wrapper.labelToCanonicalId(""); - uint256 expectedHash = uint256(keccak256(bytes(""))); - uint256 expected = expectedHash ^ uint32(expectedHash); - assertEq(emptyId, expected); - } - - function test_labelToCanonicalId_SpecialCharacters() public view { - uint256 specialId = _wrapper.labelToCanonicalId("test-name_123"); - uint256 expectedHash = uint256(keccak256(bytes("test-name_123"))); - uint256 expected = expectedHash ^ uint32(expectedHash); - assertEq(specialId, expected); - } - - function test_labelToCanonicalId_UnicodeCharacters() public view { - uint256 unicodeId = _wrapper.labelToCanonicalId(unicode"tëst"); - uint256 expectedHash = uint256(keccak256(bytes(unicode"tëst"))); - uint256 expected = expectedHash ^ uint32(expectedHash); - assertEq(unicodeId, expected); - } - - function testFuzz_labelToCanonicalId(string memory label) public view { - uint256 canonicalId = _wrapper.labelToCanonicalId(label); - uint256 expectedHash = uint256(keccak256(bytes(label))); - uint256 expected = expectedHash ^ uint32(expectedHash); - assertEq(canonicalId, expected); - } - - // Test getCanonicalId function - function test_getCanonicalId_BasicIds() public view { - uint256 id1 = 0x123456789abcdef0; - uint256 canonical1 = _wrapper.getCanonicalId(id1); - uint256 expected1 = id1 ^ uint32(id1); - assertEq(canonical1, expected1); - - uint256 id2 = 0xffffffffffffffff; - uint256 canonical2 = _wrapper.getCanonicalId(id2); - uint256 expected2 = id2 ^ uint32(id2); - assertEq(canonical2, expected2); - } - - function test_getCanonicalId_ZeroId() public view { - uint256 canonical = _wrapper.getCanonicalId(0); - assertEq(canonical, 0); - } - - function test_getCanonicalId_MaxId() public view { - uint256 maxId = type(uint256).max; - uint256 canonical = _wrapper.getCanonicalId(maxId); - uint256 expected = maxId ^ uint32(maxId); - assertEq(canonical, expected); - } - - function test_getCanonicalId_Properties() public view { - uint256 id = 0x123456789abcdef0; - uint256 canonical = _wrapper.getCanonicalId(id); - - // Verify the canonical ID follows the expected formula: id ^ uint32(id) - assertEq(canonical, id ^ uint32(id)); - - // Test with a value where lower 32 bits are zero - uint256 idZeroLower = 0x1234567800000000; - uint256 canonicalZero = _wrapper.getCanonicalId(idZeroLower); - assertEq(canonicalZero, idZeroLower); // Should be unchanged since uint32(id) = 0 - } - - function testFuzz_getCanonicalId(uint256 id) public view { - uint256 canonical = _wrapper.getCanonicalId(id); - uint256 expected = id ^ uint32(id); - assertEq(canonical, expected); + function testFuzz_getCanonicalId(uint256 id) external pure { + uint256 canonicalId = LibLabel.getCanonicalId(id); + assertEq(canonicalId, id ^ uint32(id), "xor"); + assertEq(canonicalId, id - uint32(id), "sub"); + assertEq(canonicalId, id & ~uint256(0xffffffff), "and"); + assertEq(canonicalId, (id >> 32) << 32, "shift"); } - // Test dnsEncodeEthLabel function - function test_dnsEncodeEthLabel_BasicLabels() public view { - bytes memory encoded = _wrapper.dnsEncodeEthLabel("test"); - bytes memory expected = abi.encodePacked( - bytes1(uint8(4)), // length of "test" - "test", - "\x03eth\x00" + function testFuzz_labelToCanonicalId(string memory label) external pure { + assertEq( + LibLabel.labelToCanonicalId(label), + LibLabel.getCanonicalId(uint256(keccak256(bytes(label)))) ); - assertEq(encoded, expected); - } - - function test_dnsEncodeEthLabel_EmptyString() public view { - bytes memory encoded = _wrapper.dnsEncodeEthLabel(""); - bytes memory expected = abi.encodePacked( - bytes1(uint8(0)), // length of "" - "", - "\x03eth\x00" - ); - assertEq(encoded, expected); - } - - function test_dnsEncodeEthLabel_SingleCharacter() public view { - bytes memory encoded = _wrapper.dnsEncodeEthLabel("a"); - bytes memory expected = abi.encodePacked( - bytes1(uint8(1)), // length of "a" - "a", - "\x03eth\x00" - ); - assertEq(encoded, expected); - } - - function test_dnsEncodeEthLabel_LongLabel() public view { - string memory longLabel = "verylonglabelnamethatshouldstillwork"; - bytes memory encoded = _wrapper.dnsEncodeEthLabel(longLabel); - bytes memory expected = abi.encodePacked( - bytes1(uint8(bytes(longLabel).length)), - longLabel, - "\x03eth\x00" - ); - assertEq(encoded, expected); - } - - function test_dnsEncodeEthLabel_SpecialCharacters() public view { - bytes memory encoded = _wrapper.dnsEncodeEthLabel("test-name_123"); - bytes memory expected = abi.encodePacked( - bytes1(uint8(13)), // length of "test-name_123" - "test-name_123", - "\x03eth\x00" - ); - assertEq(encoded, expected); - } - - function testFuzz_dnsEncodeEthLabel(string memory label) public view { - // Skip labels that are too long (DNS has 63 byte limit per label) - vm.assume(bytes(label).length <= 63); - - bytes memory encoded = _wrapper.dnsEncodeEthLabel(label); - bytes memory expected = abi.encodePacked( - bytes1(uint8(bytes(label).length)), - label, - "\x03eth\x00" - ); - assertEq(encoded, expected); - } - - // Test extractLabel function with offset - function test_extractLabel_WithOffset_BasicCase() public view { - // Use the DNS encoding function to generate test input - bytes memory dnsTestName = _wrapper.dnsEncodeEthLabel("test"); - - (string memory label, uint256 nextOffset) = _wrapper.extractLabel(dnsTestName, 0); - assertEq(label, "test"); - assertEq(nextOffset, 5); // 1 (length) + 4 (label) = 5 - } - - function test_extractLabel_WithOffset_SecondLabel() public view { - // Use the DNS encoding function to generate test input - bytes memory dnsName = _wrapper.dnsEncodeEthLabel("test"); - - // Extract the second label (eth) from the DNS encoded name - (string memory label, uint256 nextOffset) = _wrapper.extractLabel(dnsName, 5); - assertEq(label, "eth"); - assertEq(nextOffset, 9); // 5 + 1 (length) + 3 (label) = 9 - } - - function test_extractLabel_WithOffset_SingleCharLabel() public view { - // Use the DNS encoding function to generate single character test input - bytes memory dnsSingleName = _wrapper.dnsEncodeEthLabel("a"); - - (string memory label, uint256 nextOffset) = _wrapper.extractLabel(dnsSingleName, 0); - assertEq(label, "a"); - assertEq(nextOffset, 2); // 1 (length) + 1 (label) = 2 - } - - // Test extractLabel function without offset (convenience function) - function test_extractLabel_WithoutOffset_BasicCase() public view { - // Use the DNS encoding function to generate test input - bytes memory dnsTestName = _wrapper.dnsEncodeEthLabel("test"); - - string memory label = _wrapper.extractLabel(dnsTestName); - assertEq(label, "test"); - } - - function test_extractLabel_WithoutOffset_SingleChar() public view { - // Use the DNS encoding function to generate single character test input - bytes memory dnsSingleName = _wrapper.dnsEncodeEthLabel("x"); - - string memory label = _wrapper.extractLabel(dnsSingleName); - assertEq(label, "x"); - } - - // Integration tests combining multiple functions - function test_integration_LabelToCanonicalIdAndBack() public view { - string memory originalLabel = "testlabel"; - uint256 canonicalId = _wrapper.labelToCanonicalId(originalLabel); - - // Verify that the canonical ID is different from the raw hash - uint256 rawHash = uint256(keccak256(bytes(originalLabel))); - assertTrue(canonicalId != rawHash); - - // Verify the canonical ID follows the expected formula - assertEq(canonicalId, rawHash ^ uint32(rawHash)); - } - - function test_integration_DnsEncodeAndExtract() public view { - string memory originalLabel = "mytest"; - bytes memory encoded = _wrapper.dnsEncodeEthLabel(originalLabel); - string memory extracted = _wrapper.extractLabel(encoded); - - assertEq(extracted, originalLabel); - } - - function test_integration_MultipleLabelsExtraction() public view { - // Use the DNS encoding function to generate test input - bytes memory dnsName = _wrapper.dnsEncodeEthLabel("alice"); - - // Extract first label - (string memory label1, uint256 offset1) = _wrapper.extractLabel(dnsName, 0); - assertEq(label1, "alice"); - - // Extract second label (eth) from the DNS encoded name - (string memory label2, ) = _wrapper.extractLabel(dnsName, offset1); - assertEq(label2, "eth"); - } - - // Edge cases and error conditions - function test_edge_MaxLengthLabel() public view { - // Create a 63-byte label (max DNS label length) - string memory maxLabel = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk"; - require(bytes(maxLabel).length == 63, "Test setup error"); - - uint256 canonicalId = _wrapper.labelToCanonicalId(maxLabel); - uint256 expectedHash = uint256(keccak256(bytes(maxLabel))); - assertEq(canonicalId, expectedHash ^ uint32(expectedHash)); - - bytes memory encoded = _wrapper.dnsEncodeEthLabel(maxLabel); - string memory extracted = _wrapper.extractLabel(encoded); - assertEq(extracted, maxLabel); - } - - function test_edge_CanonicalIdConsistency() public view { - // Test that the same input always produces the same canonical ID - string memory label = "consistency"; - uint256 id1 = _wrapper.labelToCanonicalId(label); - uint256 id2 = _wrapper.labelToCanonicalId(label); - assertEq(id1, id2); - - uint256 canonical1 = _wrapper.getCanonicalId(12345); - uint256 canonical2 = _wrapper.getCanonicalId(12345); - assertEq(canonical1, canonical2); - } - - function test_edge_DifferentLabelsProduceDifferentIds() public view { - uint256 id1 = _wrapper.labelToCanonicalId("test1"); - uint256 id2 = _wrapper.labelToCanonicalId("test2"); - assertTrue(id1 != id2); - } - - // Additional edge cases for dnsEncodeEthLabel based on TODO comment - function test_dnsEncodeEthLabel_ScamLabelsWithDots() public view { - // Test labels that contain dots (potential scam labels) - bytes memory encoded = _wrapper.dnsEncodeEthLabel("a.b"); - bytes memory expected = abi.encodePacked( - bytes1(uint8(3)), // length of "a.b" - "a.b", - "\x03eth\x00" - ); - assertEq(encoded, expected); - - // Test more complex scam label - bytes memory encoded2 = _wrapper.dnsEncodeEthLabel("fake.uniswap"); - bytes memory expected2 = abi.encodePacked( - bytes1(uint8(12)), // length of "fake.uniswap" - "fake.uniswap", - "\x03eth\x00" - ); - assertEq(encoded2, expected2); - } - - function test_dnsEncodeEthLabel_LongLabels() public view { - // Test label longer than 255 bytes (DNS limit) - string memory longLabel = "a"; - for (uint i = 0; i < 8; i++) { - longLabel = string(abi.encodePacked(longLabel, longLabel)); // Double each time - } - // This creates a label of 256 characters - require(bytes(longLabel).length == 256, "Test setup error: expected 256 bytes"); - - bytes memory encoded = _wrapper.dnsEncodeEthLabel(longLabel); - bytes memory expected = abi.encodePacked( - bytes1(uint8(0)), // Length overflows to 0 when > 255 - longLabel, - "\x03eth\x00" - ); - assertEq(encoded, expected); - } - - function test_dnsEncodeEthLabel_HashedLabelFormat() public view { - // Test label that looks like a hashed label format [64 hex chars] - string - memory hashedLabel = "[af2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc]"; - bytes memory encoded = _wrapper.dnsEncodeEthLabel(hashedLabel); - - // Verify the actual encoded result - bytes memory expected = abi.encodePacked( - bytes1(uint8(bytes(hashedLabel).length)), // Actual length of the string - hashedLabel, - "\x03eth\x00" - ); - assertEq(encoded, expected); - - // Verify the length byte is correct - assertEq(uint8(encoded[0]), bytes(hashedLabel).length); - - // Test malformed hashed label (wrong length) - string - memory malformedHashed = "[af2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103]"; - bytes memory encoded2 = _wrapper.dnsEncodeEthLabel(malformedHashed); - bytes memory expected2 = abi.encodePacked( - bytes1(uint8(bytes(malformedHashed).length)), // Actual length - malformedHashed, - "\x03eth\x00" - ); - assertEq(encoded2, expected2); - } - - function test_dnsEncodeEthLabel_UnicodeAndEmoji() public view { - // Test Unicode characters - string memory unicodeLabel = unicode"tëst-ñämé"; - bytes memory encoded = _wrapper.dnsEncodeEthLabel(unicodeLabel); - bytes memory expected = abi.encodePacked( - bytes1(uint8(bytes(unicodeLabel).length)), - unicodeLabel, - "\x03eth\x00" - ); - assertEq(encoded, expected); - - // Test emoji - string memory emojiLabel = unicode"🚀test🌟"; - bytes memory encoded2 = _wrapper.dnsEncodeEthLabel(emojiLabel); - bytes memory expected2 = abi.encodePacked( - bytes1(uint8(bytes(emojiLabel).length)), - emojiLabel, - "\x03eth\x00" - ); - assertEq(encoded2, expected2); - } - - function test_dnsEncodeEthLabel_ControlCharacters() public view { - // Test labels with control characters - string memory controlLabel = "test\x01\x02\x03"; - bytes memory encoded = _wrapper.dnsEncodeEthLabel(controlLabel); - bytes memory expected = abi.encodePacked( - bytes1(uint8(bytes(controlLabel).length)), - controlLabel, - "\x03eth\x00" - ); - assertEq(encoded, expected); - - // Test with null byte - string memory nullLabel = "test\x00null"; - bytes memory encoded2 = _wrapper.dnsEncodeEthLabel(nullLabel); - bytes memory expected2 = abi.encodePacked( - bytes1(uint8(bytes(nullLabel).length)), - nullLabel, - "\x03eth\x00" - ); - assertEq(encoded2, expected2); - } - - function test_dnsEncodeEthLabel_SpecialDNSCharacters() public view { - // Test with DNS reserved characters - string memory dnsChars = "test\\label"; - bytes memory encoded = _wrapper.dnsEncodeEthLabel(dnsChars); - bytes memory expected = abi.encodePacked( - bytes1(uint8(bytes(dnsChars).length)), - dnsChars, - "\x03eth\x00" - ); - assertEq(encoded, expected); - - // Test with spaces and other special chars - string memory spacesLabel = "test label with spaces"; - bytes memory encoded2 = _wrapper.dnsEncodeEthLabel(spacesLabel); - bytes memory expected2 = abi.encodePacked( - bytes1(uint8(bytes(spacesLabel).length)), - spacesLabel, - "\x03eth\x00" - ); - assertEq(encoded2, expected2); - } - - // Enhanced extractLabel testing with malformed inputs and boundary conditions - function test_extractLabel_EmptyDNSName() public view { - // Test with just the terminator byte - bytes memory emptyDns = hex"00"; - - // This should extract an empty string, not revert - string memory extracted = _wrapper.extractLabel(emptyDns); - assertEq(extracted, ""); - } - - function test_extractLabel_MalformedDNSName() public { - // Test with incomplete DNS name (missing terminator) - bytes memory incompleteDns = hex"04746573"; // "04tes" without "t" and terminator - - vm.expectRevert(); - _wrapper.extractLabel(incompleteDns); - - // Test with length byte larger than remaining data - bytes memory oversizedLength = hex"0574657374"; // says 5 bytes but only has "test" (4 bytes) - - vm.expectRevert(); - _wrapper.extractLabel(oversizedLength); - } - - function test_extractLabel_BoundaryOffsets() public { - bytes memory dnsName = _wrapper.dnsEncodeEthLabel("test"); - - // Test extracting at exact boundary - (string memory ethLabel, uint256 nextOffset) = _wrapper.extractLabel(dnsName, 5); - assertEq(ethLabel, "eth"); - assertEq(nextOffset, 9); - - // Test extracting at the last valid position (terminator) - this should work but return empty - (string memory termLabel, uint256 termOffset) = _wrapper.extractLabel(dnsName, 9); - assertEq(termLabel, ""); // Should return empty string for terminator - assertEq(termOffset, 10); // Should advance by 1 - - // Test extracting beyond the end - vm.expectRevert(); - _wrapper.extractLabel(dnsName, 15); // Beyond end of data - } - - function test_extractLabel_ZeroLengthLabel() public { - // Note: Empty string DNS encoding creates a special case that NameCoder.extractLabel cannot handle - // This is a limitation of the underlying ENS NameCoder library - // The encoded result 0x000365746800 represents: zero-length-label + "eth" + terminator - // But extractLabel expects different formatting for zero-length labels - - // Test that we get the expected encoding format - bytes memory emptyEncoded = _wrapper.dnsEncodeEthLabel(""); - assertEq(emptyEncoded, hex"000365746800"); // Verify the encoding format - - // This is a known limitation - extractLabel cannot handle this specific encoding - vm.expectRevert(); - _wrapper.extractLabel(emptyEncoded); - } - - function test_extractLabel_LongLabel() public view { - // Test with maximum valid DNS label (63 bytes) - string memory maxLabel = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk"; - require(bytes(maxLabel).length == 63, "Test setup error"); - - bytes memory encoded = _wrapper.dnsEncodeEthLabel(maxLabel); - string memory extracted = _wrapper.extractLabel(encoded); - assertEq(extracted, maxLabel); - - // Test extraction with offset - (string memory label, uint256 nextOffset) = _wrapper.extractLabel(encoded, 0); - assertEq(label, maxLabel); - assertEq(nextOffset, 64); // 1 (length) + 63 (label) - } - - function test_extractLabel_ConsecutiveLabels() public view { - // Create a more complex DNS name manually: "\x04test\x05alice\x03bob\x00" - bytes memory complexDns = abi.encodePacked( - bytes1(uint8(4)), - "test", - bytes1(uint8(5)), - "alice", - bytes1(uint8(3)), - "bob", - bytes1(uint8(0)) - ); - - // Extract all labels sequentially - (string memory label1, uint256 offset1) = _wrapper.extractLabel(complexDns, 0); - assertEq(label1, "test"); - - (string memory label2, uint256 offset2) = _wrapper.extractLabel(complexDns, offset1); - assertEq(label2, "alice"); - - (string memory label3, uint256 offset3) = _wrapper.extractLabel(complexDns, offset2); - assertEq(label3, "bob"); - - // Verify final offset points to terminator - assertEq(offset3, complexDns.length - 1); - } - - function test_extractLabel_BinaryData() public view { - // Test with binary data that might confuse string extraction - bytes memory binaryLabel = hex"deadbeef1234"; - bytes memory dnsEncoded = abi.encodePacked( - bytes1(uint8(binaryLabel.length)), - binaryLabel, - "\x03eth\x00" - ); - - (string memory extracted, ) = _wrapper.extractLabel(dnsEncoded, 0); - // The extracted string should contain the binary data as bytes - assertEq(bytes(extracted), binaryLabel); - } - - function test_extractLabel_MaxOffsetEdgeCases() public view { - bytes memory dnsName = _wrapper.dnsEncodeEthLabel("test"); - uint256 maxValidOffset = 5; // Start of "eth" label - - // Test at maximum valid offset - (string memory label, uint256 nextOffset) = _wrapper.extractLabel(dnsName, maxValidOffset); - assertEq(label, "eth"); - assertEq(nextOffset, 9); - - // Test at terminator position - should work but return empty - (string memory termLabel, ) = _wrapper.extractLabel(dnsName, 9); - assertEq(termLabel, ""); - } - - // Comprehensive getCanonicalId tests including idempotency and bit patterns - function test_getCanonicalId_Idempotency() public view { - uint256 id = 0x123456789abcdef0; - uint256 canonical1 = _wrapper.getCanonicalId(id); - uint256 canonical2 = _wrapper.getCanonicalId(canonical1); - - // Applying getCanonicalId twice should give the same result as applying it once - // This tests: getCanonicalId(getCanonicalId(x)) == getCanonicalId(x) - assertEq(canonical1, canonical2); - - // Test with different values - uint256 id2 = 0xdeadbeefcafebabe; - uint256 canonical3 = _wrapper.getCanonicalId(id2); - uint256 canonical4 = _wrapper.getCanonicalId(canonical3); - assertEq(canonical3, canonical4); - } - - function test_getCanonicalId_SpecificBitPatterns() public view { - // Test with all lower 32 bits set - uint256 lowerBitsSet = 0x123456780ffffffff; - uint256 canonical1 = _wrapper.getCanonicalId(lowerBitsSet); - uint256 expected1 = lowerBitsSet ^ 0xffffffff; - assertEq(canonical1, expected1); - - // Test with alternating bit pattern - uint256 alternating = 0xaaaaaaaaaaaaaaaa; - uint256 canonical2 = _wrapper.getCanonicalId(alternating); - uint256 expected2 = alternating ^ 0xaaaaaaaa; - assertEq(canonical2, expected2); - - // Test with only high bits set (lower 32 bits are zero) - uint256 highBitsOnly = 0xffffffff00000000; - uint256 canonical3 = _wrapper.getCanonicalId(highBitsOnly); - assertEq(canonical3, highBitsOnly); // Should be unchanged since uint32(id) = 0 - } - - function test_getCanonicalId_PowersOfTwo() public view { - // Test with powers of 2 - for (uint8 i = 0; i < 32; i++) { - uint256 powerOf2 = 1 << i; - uint256 canonical = _wrapper.getCanonicalId(powerOf2); - uint256 expected = powerOf2 ^ uint32(powerOf2); - assertEq(canonical, expected); - } - - // Test with higher powers of 2 (beyond 32 bits) - for (uint8 i = 32; i < 64; i++) { - uint256 powerOf2 = 1 << i; - uint256 canonical = _wrapper.getCanonicalId(powerOf2); - // Since uint32(powerOf2) = 0 for powers > 2^32, canonical should equal original - assertEq(canonical, powerOf2); - } - } - - function test_getCanonicalId_SymmetricProperties() public view { - // Test that the XOR operation creates symmetry - uint256 id = 0x123456789abcdef0; - uint256 canonical = _wrapper.getCanonicalId(id); - - // Verify the bit manipulation works correctly - uint32 lower32 = uint32(id); - uint256 expectedResult = id ^ uint256(lower32); - assertEq(canonical, expectedResult); - - // Test edge case where lower 32 bits create interesting patterns - uint256 testId = 0x123456789abcdef0; - uint256 result = _wrapper.getCanonicalId(testId); - - // Manually calculate expected result - uint32 expectedLower32 = uint32(testId); // 0x9abcdef0 - uint256 expectedCanonical = testId ^ expectedLower32; - assertEq(result, expectedCanonical); - } - - function test_getCanonicalId_EdgeCaseBitPatterns() public view { - // Test when lower 32 bits are all 1s - uint256 allOnes = type(uint256).max; // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - uint256 canonical1 = _wrapper.getCanonicalId(allOnes); - uint256 expected1 = allOnes ^ 0xffffffff; - assertEq(canonical1, expected1); - - // Test when lower 32 bits spell out specific patterns - uint256 deadbeef = 0x123456789abcdeadbeef1234; - uint256 canonical2 = _wrapper.getCanonicalId(deadbeef); - uint256 expected2 = deadbeef ^ 0xbeef1234; - assertEq(canonical2, expected2); - - // Test with minimal bit differences - uint256 minimal1 = 0x8000000000000001; - uint256 minimal2 = 0x8000000000000000; - uint256 canonical3 = _wrapper.getCanonicalId(minimal1); - uint256 canonical4 = _wrapper.getCanonicalId(minimal2); - - // They should differ by exactly the XOR of their lower 32 bits - assertEq(canonical3, minimal1 ^ 1); - assertEq(canonical4, minimal2 ^ 0); - } - - function test_getCanonicalId_DistributionProperties() public view { - // Test that different inputs produce different outputs - // We need values with different upper bits AND lower bits to ensure different results - uint256[] memory testValues = new uint256[](5); - testValues[0] = 0x123456781; // Different upper + lower bits: 1 - testValues[1] = 0x234567892; // Different upper + lower bits: 2 - testValues[2] = 0x345678900; // Different upper + lower bits: 0 - testValues[3] = 0x45678901F; // Different upper + lower bits: 31 - testValues[4] = 0x5678901FF; // Different upper + lower bits: 255 - - uint256[] memory canonicalValues = new uint256[](5); - for (uint i = 0; i < testValues.length; i++) { - canonicalValues[i] = _wrapper.getCanonicalId(testValues[i]); - - // Verify the formula works: canonical = original ^ uint32(original) - uint256 expected = testValues[i] ^ uint32(testValues[i]); - assertEq(canonicalValues[i], expected); - } - - // Ensure all canonical values are different - for (uint i = 0; i < canonicalValues.length; i++) { - for (uint j = i + 1; j < canonicalValues.length; j++) { - assertTrue(canonicalValues[i] != canonicalValues[j]); - } - } - } - - function test_getCanonicalId_ConsistencyWithLabelToCanonicalId() public view { - // Test that getCanonicalId is consistent with labelToCanonicalId - string memory testLabel = "testlabel"; - uint256 fromLabel = _wrapper.labelToCanonicalId(testLabel); - - // Calculate what labelToCanonicalId should produce - uint256 rawHash = uint256(keccak256(bytes(testLabel))); - uint256 fromHash = _wrapper.getCanonicalId(rawHash); - - assertEq(fromLabel, fromHash); - - // Test with multiple labels - string[] memory labels = new string[](3); - labels[0] = "alice"; - labels[1] = "bob"; - labels[2] = "charlie"; - - for (uint i = 0; i < labels.length; i++) { - uint256 labelCanonical = _wrapper.labelToCanonicalId(labels[i]); - uint256 hashCanonical = _wrapper.getCanonicalId(uint256(keccak256(bytes(labels[i])))); - assertEq(labelCanonical, hashCanonical); - } - } - - function testFuzz_getCanonicalId_Idempotency(uint256 id) public view { - uint256 canonical1 = _wrapper.getCanonicalId(id); - uint256 canonical2 = _wrapper.getCanonicalId(canonical1); - assertEq(canonical1, canonical2); - } - - function testFuzz_getCanonicalId_Formula(uint256 id) public view { - uint256 canonical = _wrapper.getCanonicalId(id); - uint256 expected = id ^ uint32(id); - assertEq(canonical, expected); - } - - // Integration tests and cross-function verification - function test_integration_CompleteRoundTrip() public view { - string memory originalLabel = "complex-test-label"; - - // Step 1: Convert label to canonical ID - uint256 canonicalId = _wrapper.labelToCanonicalId(originalLabel); - - // Step 2: Encode the label as DNS - bytes memory dnsEncoded = _wrapper.dnsEncodeEthLabel(originalLabel); - - // Step 3: Extract the label back - string memory extractedLabel = _wrapper.extractLabel(dnsEncoded); - - // Step 4: Convert extracted label back to canonical ID - uint256 extractedCanonicalId = _wrapper.labelToCanonicalId(extractedLabel); - - // Verify round-trip integrity - assertEq(extractedLabel, originalLabel); - assertEq(extractedCanonicalId, canonicalId); - - // Verify canonical ID is idempotent - uint256 reappliedCanonical = _wrapper.getCanonicalId(canonicalId); - assertEq(reappliedCanonical, canonicalId); - } - - function test_integration_MultipleLabelsRoundTrip() public view { - string[] memory labels = new string[](4); - labels[0] = "alice"; - labels[1] = "bob"; - labels[2] = "charlie"; - labels[3] = unicode"tëst-ñämé"; - - for (uint i = 0; i < labels.length; i++) { - // Encode and extract - bytes memory encoded = _wrapper.dnsEncodeEthLabel(labels[i]); - string memory extracted = _wrapper.extractLabel(encoded); - assertEq(extracted, labels[i]); - - // Verify canonical IDs match - uint256 originalId = _wrapper.labelToCanonicalId(labels[i]); - uint256 extractedId = _wrapper.labelToCanonicalId(extracted); - assertEq(originalId, extractedId); - } - } - - function test_integration_CanonicalIdUniqueness() public view { - // Test that different labels produce different canonical IDs - string[] memory uniqueLabels = new string[](6); - uniqueLabels[0] = "test"; - uniqueLabels[1] = "Test"; // Different case - uniqueLabels[2] = "test1"; - uniqueLabels[3] = "test "; // With space - uniqueLabels[4] = unicode"tëst"; // With accent - uniqueLabels[5] = ""; // Empty - - uint256[] memory canonicalIds = new uint256[](uniqueLabels.length); - for (uint i = 0; i < uniqueLabels.length; i++) { - canonicalIds[i] = _wrapper.labelToCanonicalId(uniqueLabels[i]); - } - - // Verify all canonical IDs are unique - for (uint i = 0; i < canonicalIds.length; i++) { - for (uint j = i + 1; j < canonicalIds.length; j++) { - assertTrue(canonicalIds[i] != canonicalIds[j]); - } - } - } - - function test_integration_RealWorldENSNames() public view { - // Test with real-world-style ENS names - string[] memory realWorldLabels = new string[](5); - realWorldLabels[0] = "vitalik"; - realWorldLabels[1] = "uniswap"; - realWorldLabels[2] = "compound"; - realWorldLabels[3] = "1inch"; - realWorldLabels[4] = "opensea"; - - for (uint i = 0; i < realWorldLabels.length; i++) { - // Test complete workflow - uint256 canonicalId = _wrapper.labelToCanonicalId(realWorldLabels[i]); - bytes memory dnsEncoded = _wrapper.dnsEncodeEthLabel(realWorldLabels[i]); - string memory extracted = _wrapper.extractLabel(dnsEncoded); - - // Verify round-trip - assertEq(extracted, realWorldLabels[i]); - uint256 roundTripId = _wrapper.labelToCanonicalId(extracted); - assertEq(roundTripId, canonicalId); - - // Verify canonical ID properties - uint256 rawHash = uint256(keccak256(bytes(realWorldLabels[i]))); - uint256 expectedCanonical = _wrapper.getCanonicalId(rawHash); - assertEq(canonicalId, expectedCanonical); - } - } - - function test_integration_ChainedExtraction() public view { - // Create a complex DNS name with multiple labels - bytes memory complexDns = abi.encodePacked( - bytes1(uint8(3)), - "www", - bytes1(uint8(7)), - "example", - bytes1(uint8(3)), - "com", - bytes1(uint8(0)) - ); - - // Extract labels in sequence and verify each step - (string memory label1, uint256 offset1) = _wrapper.extractLabel(complexDns, 0); - assertEq(label1, "www"); - assertEq(offset1, 4); // 1 + 3 - - (string memory label2, uint256 offset2) = _wrapper.extractLabel(complexDns, offset1); - assertEq(label2, "example"); - assertEq(offset2, 12); // 4 + 1 + 7 - - (string memory label3, uint256 offset3) = _wrapper.extractLabel(complexDns, offset2); - assertEq(label3, "com"); - assertEq(offset3, 16); // 12 + 1 + 3 - - // Verify we're at the terminator - assertEq(offset3, complexDns.length - 1); - - // Test canonical IDs for each extracted label - uint256 canonicalWww = _wrapper.labelToCanonicalId(label1); - uint256 canonicalExample = _wrapper.labelToCanonicalId(label2); - uint256 canonicalCom = _wrapper.labelToCanonicalId(label3); - - // Verify they're all different - assertTrue(canonicalWww != canonicalExample); - assertTrue(canonicalExample != canonicalCom); - assertTrue(canonicalWww != canonicalCom); - } - - function test_integration_EdgeCaseCombinations() public view { - // Test combinations of edge cases (excluding empty string due to NameCoder limitation) - string[] memory edgeCases = new string[](3); - edgeCases[0] = "a"; // Single char - edgeCases[1] = "fake.domain"; // Scam-style - edgeCases[2] = unicode"🎉party🎊"; // Emoji - - for (uint i = 0; i < edgeCases.length; i++) { - // Full workflow test - uint256 canonicalId = _wrapper.labelToCanonicalId(edgeCases[i]); - bytes memory dnsEncoded = _wrapper.dnsEncodeEthLabel(edgeCases[i]); - string memory extracted = _wrapper.extractLabel(dnsEncoded); - - // Verify consistency - assertEq(extracted, edgeCases[i]); - assertEq(_wrapper.labelToCanonicalId(extracted), canonicalId); - - // Test idempotency of canonical ID - assertEq(_wrapper.getCanonicalId(canonicalId), canonicalId); - } - - // Test empty string separately (known limitation) - uint256 emptyCanonicalId = _wrapper.labelToCanonicalId(""); - bytes memory emptyDnsEncoded = _wrapper.dnsEncodeEthLabel(""); - - // Verify the empty string produces the expected DNS encoding - assertEq(emptyDnsEncoded, hex"000365746800"); - - // Verify canonical ID works for empty strings - assertEq(_wrapper.getCanonicalId(emptyCanonicalId), emptyCanonicalId); - - // Note: extractLabel cannot handle empty string DNS encoding due to NameCoder limitation - } - - function test_integration_ConsistencyAcrossEncodings() public view { - // Test that the same logical label produces consistent results regardless of how it's processed - string memory testLabel = "consistency-test"; - - // Method 1: Direct canonical ID - uint256 directCanonical = _wrapper.labelToCanonicalId(testLabel); - - // Method 2: Via DNS encoding and extraction - bytes memory dnsEncoded = _wrapper.dnsEncodeEthLabel(testLabel); - string memory extracted = _wrapper.extractLabel(dnsEncoded); - uint256 indirectCanonical = _wrapper.labelToCanonicalId(extracted); - - // Method 3: Via getCanonicalId of hash - uint256 rawHash = uint256(keccak256(bytes(testLabel))); - uint256 hashCanonical = _wrapper.getCanonicalId(rawHash); - - // All methods should produce the same result - assertEq(directCanonical, indirectCanonical); - assertEq(indirectCanonical, hashCanonical); - - // Verify the extracted label matches original - assertEq(extracted, testLabel); - } - - function testFuzz_integration_RoundTripConsistency(string memory label) public view { - // Skip extremely long labels to avoid gas issues and empty strings which have special behavior - vm.assume(bytes(label).length <= 100); - vm.assume(bytes(label).length > 0); // Skip empty strings - - // Test round-trip consistency - uint256 originalCanonical = _wrapper.labelToCanonicalId(label); - bytes memory dnsEncoded = _wrapper.dnsEncodeEthLabel(label); - string memory extracted = _wrapper.extractLabel(dnsEncoded); - uint256 extractedCanonical = _wrapper.labelToCanonicalId(extracted); - - assertEq(extracted, label); - assertEq(extractedCanonical, originalCanonical); - - // Test canonical ID idempotency - assertEq(_wrapper.getCanonicalId(originalCanonical), originalCanonical); - } - - // Error condition and boundary testing - function test_error_ExtractLabelWithCorruptedData() public { - // Test with completely invalid data - bytes memory invalidData = hex"ffffffffffffffff"; - vm.expectRevert(); - _wrapper.extractLabel(invalidData); - - // Test with partial data that looks valid but isn't - bytes memory partialData = hex"0474657374"; // Says "test" (4 bytes) but no terminator - vm.expectRevert(); - _wrapper.extractLabel(partialData); - } - - function test_error_ExtractLabelBeyondBounds() public { - bytes memory validDns = _wrapper.dnsEncodeEthLabel("test"); - - // Try to extract at invalid offsets - vm.expectRevert(); - _wrapper.extractLabel(validDns, 100); // Way beyond end - - vm.expectRevert(); - _wrapper.extractLabel(validDns, validDns.length); // Exactly at end (invalid) - - vm.expectRevert(); - _wrapper.extractLabel(validDns, validDns.length + 1); // Beyond end - } - - function test_boundary_MaximumValidInputs() public view { - // Test with maximum valid DNS label size (63 bytes) - string memory maxSizeLabel = ""; - for (uint i = 0; i < 63; i++) { - maxSizeLabel = string(abi.encodePacked(maxSizeLabel, "a")); - } - require(bytes(maxSizeLabel).length == 63, "Test setup error"); - - // Should work without issues - bytes memory encoded = _wrapper.dnsEncodeEthLabel(maxSizeLabel); - string memory extracted = _wrapper.extractLabel(encoded); - assertEq(extracted, maxSizeLabel); - - uint256 canonicalId = _wrapper.labelToCanonicalId(maxSizeLabel); - assertEq(_wrapper.getCanonicalId(canonicalId), canonicalId); - } - - function test_boundary_EmptyAndNullInputs() public view { - // Test empty string handled separately as it has special behavior - // Empty strings create DNS names with just .eth suffix - - // Test string with null bytes - string memory nullString = string(abi.encodePacked("test", bytes1(0), "null")); - bytes memory nullEncoded = _wrapper.dnsEncodeEthLabel(nullString); - string memory nullExtracted = _wrapper.extractLabel(nullEncoded); - assertEq(nullExtracted, nullString); - - uint256 nullCanonical = _wrapper.labelToCanonicalId(nullString); - assertEq(_wrapper.getCanonicalId(nullCanonical), nullCanonical); - } - - function test_boundary_OffsetEdgeCases() public view { - bytes memory dnsName = _wrapper.dnsEncodeEthLabel("boundary"); - - // Test offset at the start of each component - (string memory label1, uint256 offset1) = _wrapper.extractLabel(dnsName, 0); - assertEq(label1, "boundary"); - - (string memory label2, uint256 offset2) = _wrapper.extractLabel(dnsName, offset1); - assertEq(label2, "eth"); - - // offset2 should now be at the terminator position - assertEq(offset2, dnsName.length - 1); - - // Trying to extract at the terminator should return empty string - (string memory emptyLabel, ) = _wrapper.extractLabel(dnsName, offset2); - assertEq(emptyLabel, ""); - } - - function test_boundary_LargeCanonicalIds() public view { - // Test with very large uint256 values - uint256 maxValue = type(uint256).max; - uint256 maxCanonical = _wrapper.getCanonicalId(maxValue); - uint256 expectedMax = maxValue ^ uint32(maxValue); - assertEq(maxCanonical, expectedMax); - - // Test idempotency with max value - assertEq(_wrapper.getCanonicalId(maxCanonical), maxCanonical); - - // Test with large power of 2 - uint256 largePowerOf2 = 1 << 255; - uint256 largeCanonical = _wrapper.getCanonicalId(largePowerOf2); - assertEq(largeCanonical, largePowerOf2); // Should be unchanged since lower 32 bits are 0 - } - - function test_boundary_DNSEncodingLimits() public view { - // Test the exact boundary where length byte would overflow - string memory boundary255 = ""; - for (uint i = 0; i < 255; i++) { - boundary255 = string(abi.encodePacked(boundary255, "x")); - } - - bytes memory encoded255 = _wrapper.dnsEncodeEthLabel(boundary255); - // The length byte should be 255 (0xFF) - assertEq(uint8(encoded255[0]), 255); - - // Test with 256 bytes (overflows to 0) - string memory overflow256 = string(abi.encodePacked(boundary255, "y")); - bytes memory encodedOverflow = _wrapper.dnsEncodeEthLabel(overflow256); - // The length byte should overflow to 0 - assertEq(uint8(encodedOverflow[0]), 0); - } - - function test_boundary_ConsecutiveExtractions() public view { - // Test extracting many labels in sequence without errors - bytes memory multiLabelDns = abi.encodePacked( - bytes1(uint8(1)), - "a", - bytes1(uint8(1)), - "b", - bytes1(uint8(1)), - "c", - bytes1(uint8(1)), - "d", - bytes1(uint8(1)), - "e", - bytes1(uint8(0)) - ); - - string[] memory expectedLabels = new string[](5); - expectedLabels[0] = "a"; - expectedLabels[1] = "b"; - expectedLabels[2] = "c"; - expectedLabels[3] = "d"; - expectedLabels[4] = "e"; - - uint256 currentOffset = 0; - for (uint i = 0; i < expectedLabels.length; i++) { - (string memory extractedLabel, uint256 nextOffset) = _wrapper.extractLabel( - multiLabelDns, - currentOffset - ); - assertEq(extractedLabel, expectedLabels[i]); - currentOffset = nextOffset; - } - - // Should be at terminator now - assertEq(currentOffset, multiLabelDns.length - 1); - } - - function testFuzz_boundary_ValidOffsets(uint256 seed) public view { - // Generate a deterministic but varied DNS name - string memory label = string(abi.encodePacked("test", seed % 1000)); - bytes memory dnsEncoded = _wrapper.dnsEncodeEthLabel(label); - - // Only test valid offsets - uint256 validOffset = seed % 2 == 0 ? 0 : uint256(bytes(label).length) + 1; - - if (validOffset < dnsEncoded.length - 1) { - // Should not revert for valid offsets - try _wrapper.extractLabel(dnsEncoded, validOffset) returns (string memory, uint256) { - // Success expected for valid offsets - } catch { - // This might happen for some edge cases, which is acceptable - } - } } } diff --git a/contracts/test/unit/universalResolver/libraries/LibRegistry.sol b/contracts/test/unit/universalResolver/libraries/LibRegistry.sol index 400a62bd..338789d4 100755 --- a/contracts/test/unit/universalResolver/libraries/LibRegistry.sol +++ b/contracts/test/unit/universalResolver/libraries/LibRegistry.sol @@ -45,43 +45,11 @@ contract LibRegistryTest is Test, ERC1155Holder { ); } - function setUp() public { + function setUp() external { datastore = new RegistryDatastore(); rootRegistry = _createRegistry(); } - function test_readLabel_root() external pure { - bytes memory name = NameCoder.encode(""); - assertEq(LibRegistry.readLabel(name, 0), ""); - } - function test_readLabel_eth() external pure { - bytes memory name = NameCoder.encode("eth"); - assertEq(LibRegistry.readLabel(name, 0), "eth"); - assertEq(LibRegistry.readLabel(name, 4), ""); // 3eth - } - function test_readLabel_test_eth() external pure { - bytes memory name = NameCoder.encode("test.eth"); - assertEq(LibRegistry.readLabel(name, 0), "test"); - assertEq(LibRegistry.readLabel(name, 5), "eth"); // 4test - assertEq(LibRegistry.readLabel(name, 9), ""); // 4test3eth - } - - function _readLabel(bytes memory name, uint256 offset) public pure returns (string memory) { - return LibRegistry.readLabel(name, offset); - } - function test_Revert_readLabel_invalidOffset() external { - vm.expectRevert(); - this._readLabel("", 1); - } - function test_Revert_readLabel_invalidEncoding() external { - vm.expectRevert(); - this._readLabel("0x01", 0); - } - function test_revert_readLabel_junkAtEnd() external { - vm.expectRevert(); - this._readLabel("0x0000", 0); - } - function _expectFindResolver( bytes memory name, uint256 resolverOffset, diff --git a/contracts/test/utils/utils.ts b/contracts/test/utils/utils.ts index f1c481f6..45658c94 100644 --- a/contracts/test/utils/utils.ts +++ b/contracts/test/utils/utils.ts @@ -6,11 +6,7 @@ import { labelhash } from "viem"; // | undefined // | false; -export { - dnsEncodeName, - encodeLabelhash, - packetToBytes, -} from "../../lib/ens-contracts/test/fixtures/dnsEncodeName.js"; +export { dnsEncodeName } from "../../lib/ens-contracts/test/fixtures/dnsEncodeName.js"; // export function packetToBytes(packet: string) { // const m = splitName(packet).flatMap(s => { From 38016e7a961663eb3c1414fd6b444a84749606f7 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 13 Oct 2025 00:43:00 -0400 Subject: [PATCH 02/10] fix lint --- contracts/src/L1/migration/L1LockedMigrationController.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/L1/migration/L1LockedMigrationController.sol b/contracts/src/L1/migration/L1LockedMigrationController.sol index fe790a69..6da84935 100644 --- a/contracts/src/L1/migration/L1LockedMigrationController.sol +++ b/contracts/src/L1/migration/L1LockedMigrationController.sol @@ -9,7 +9,6 @@ import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC16 import {MigrationData} from "../../common/bridge/types/TransferData.sol"; import {UnauthorizedCaller} from "../../common/CommonErrors.sol"; -import {LibLabel} from "../../common/utils/LibLabel.sol"; import {L1BridgeController} from "../bridge/L1BridgeController.sol"; import {LockedNamesLib} from "./libraries/LockedNamesLib.sol"; From 54a8326b73eaec02143fe68f765211037a848d99 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 13 Oct 2025 15:21:36 -0400 Subject: [PATCH 03/10] add IComposite, add tests --- contracts/deploy/l1/01_ETHRegistry.ts | 2 +- contracts/script/setup.ts | 3 +- contracts/src/L1/resolver/ETHTLDResolver.sol | 28 +++--- .../interfaces/ICompositeExtendedResolver.sol | 22 +++++ .../integration/fixtures/deployV1Fixture.ts | 10 +- .../integration/fixtures/deployV2Fixture.ts | 4 +- .../integration/l1/ETHTLDResolver.test.ts | 95 ++++++++++++++++++- contracts/test/utils/utils.ts | 4 +- 8 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol diff --git a/contracts/deploy/l1/01_ETHRegistry.ts b/contracts/deploy/l1/01_ETHRegistry.ts index 010f2539..c5e50412 100644 --- a/contracts/deploy/l1/01_ETHRegistry.ts +++ b/contracts/deploy/l1/01_ETHRegistry.ts @@ -1,5 +1,5 @@ import { artifacts, execute } from "@rocketh"; -import { MAX_EXPIRY, ROLES } from "../constants.ts"; +import { MAX_EXPIRY, ROLES } from "../constants.js"; import { zeroAddress } from "viem"; // TODO: ownership diff --git a/contracts/script/setup.ts b/contracts/script/setup.ts index 63dd088c..10d49f92 100644 --- a/contracts/script/setup.ts +++ b/contracts/script/setup.ts @@ -102,6 +102,7 @@ const l2Contracts = { } as const satisfies DeployedArtifacts; export type CrossChainSnapshot = () => Promise; +export type CrossChainClient = ReturnType; export type CrossChainEnvironment = Awaited< ReturnType >; @@ -109,8 +110,6 @@ export type CrossChainEnvironment = Awaited< export type L1Deployment = ChainDeployment; export type L2Deployment = ChainDeployment; -export type CrossChainClient = ReturnType; - function ansi(c: any, s: any) { return `\x1b[${c}m${s}\x1b[0m`; } diff --git a/contracts/src/L1/resolver/ETHTLDResolver.sol b/contracts/src/L1/resolver/ETHTLDResolver.sol index 7bb14889..b5003a16 100755 --- a/contracts/src/L1/resolver/ETHTLDResolver.sol +++ b/contracts/src/L1/resolver/ETHTLDResolver.sol @@ -33,6 +33,9 @@ import { import {GatewayRequest, EvalFlag} from "@unruggable/gateways/contracts/GatewayRequest.sol"; import {BridgeRolesLib} from "../../common/bridge/libraries/BridgeRolesLib.sol"; +import { + ICompositeExtendedResolver +} from "../../common/resolver/interfaces/ICompositeExtendedResolver.sol"; import { DedicatedResolverLayoutLib } from "../../common/resolver/libraries/DedicatedResolverLayoutLib.sol"; @@ -47,7 +50,7 @@ import {L1BridgeController} from "../bridge/L1BridgeController.sol"; /// 2. Otherwise, resolve using Namechain. /// 3. If no resolver is found, reverts `UnreachableName`. contract ETHTLDResolver is - IExtendedResolver, + ICompositeExtendedResolver, IERC7996, GatewayFetchTarget, ResolverCaller, @@ -109,7 +112,7 @@ contract ETHTLDResolver is address namechainEthRegistry ) Ownable(msg.sender) CCIPReader(DEFAULT_UNSAFE_CALL_GAS) { NAME_WRAPPER = nameWrapper; - ETH_REGISTRAR_V1 = IBaseRegistrar(nameWrapper.ens().owner(NameCoder.ETH_NODE)); + ETH_REGISTRAR_V1 = nameWrapper.registrar(); BATCH_GATEWAY_PROVIDER = batchGatewayProvider; L1_BRIDGE_CONTROLLER = l1BridgeController; NAMECHAIN_DATASTORE = namechainDatastore; @@ -125,6 +128,7 @@ contract ETHTLDResolver is ) public view virtual override(ERC165) returns (bool) { return type(IExtendedResolver).interfaceId == interfaceId || + type(ICompositeExtendedResolver).interfaceId == interfaceId || type(IERC7996).interfaceId == interfaceId || super.supportsInterface(interfaceId); } @@ -172,20 +176,20 @@ contract ETHTLDResolver is } } - /// @dev Return `true` if `name` does not exist onchain. - function isResolverOffchain(bytes memory name) external view returns (bool offchain) { + /// @inheritdoc ICompositeExtendedResolver + function isResolverOffchain(bytes calldata name) external view returns (bool offchain) { (, offchain) = _determineResolver(name); } - /// @notice Determine underlying resolver and location for `name`. + /// @inheritdoc ICompositeExtendedResolver /// @dev This function executes over multiple steps. - /// - /// @param name The DNS-encoded name. - /// - /// @return resolver The resolver address. - /// @return offchain If `true`, `resolver` is on Namechain, otherwise on Mainnet. + /// * `getResolver("eth") = (ethResolver, false)` + /// * `getResolver()` reverts + /// * `getResolver() = (, false)` + /// * `getResolver() = (, true)` + /// * `getResolver(*.eth) = (address(0), true)` function getResolver( - bytes memory name + bytes calldata name ) external view returns (address resolver, bool offchain) { (resolver, offchain) = _determineResolver(name); if (offchain) { @@ -203,7 +207,7 @@ contract ETHTLDResolver is uint8 /*exitCode*/, bytes calldata /*extraData*/ ) external pure returns (address resolver, bool offchain) { - resolver = abi.decode(values[1], (address)); + resolver = address(uint160(uint256(bytes32(values[1])))); offchain = true; } diff --git a/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol b/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol new file mode 100644 index 00000000..6612388c --- /dev/null +++ b/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {IExtendedResolver} from "@ens/contracts/resolvers/profiles/IExtendedResolver.sol"; + +interface ICompositeExtendedResolver is IExtendedResolver { + /// @notice Fetch the underlying resolver for `name`. + /// Callers should enable EIP-3668. + /// + /// @param name The DNS-encoded name. + /// + /// @return resolver The underlying resolver address. + /// @return offchain `true` if required offchain data. + function getResolver(bytes memory name) external view returns (address resolver, bool offchain); + + /// @notice Determine if resolving `name` requires offchain data. + /// + /// @param name The DNS-encoded name. + /// + /// @return `true` if requires offchain data. + function isResolverOffchain(bytes calldata name) external view returns (bool); +} diff --git a/contracts/test/integration/fixtures/deployV1Fixture.ts b/contracts/test/integration/fixtures/deployV1Fixture.ts index 09bff774..079e0bad 100755 --- a/contracts/test/integration/fixtures/deployV1Fixture.ts +++ b/contracts/test/integration/fixtures/deployV1Fixture.ts @@ -1,5 +1,11 @@ import type { NetworkConnection } from "hardhat/types/network"; -import { type Address, labelhash, namehash, zeroAddress } from "viem"; +import { + type Address, + getAddress, + labelhash, + namehash, + zeroAddress, +} from "viem"; import { splitName } from "../../utils/utils.js"; import { LOCAL_BATCH_GATEWAY_URL } from "../../../deploy/constants.js"; @@ -111,6 +117,6 @@ export async function deployV1Fixture( } // set resolver on leaf await ensRegistry.write.setResolver([namehash(name), resolverAddress]); - return { labels }; + return { name, labels, resolverAddress: getAddress(resolverAddress) }; } } diff --git a/contracts/test/integration/fixtures/deployV2Fixture.ts b/contracts/test/integration/fixtures/deployV2Fixture.ts index 4258761b..10280f0a 100644 --- a/contracts/test/integration/fixtures/deployV2Fixture.ts +++ b/contracts/test/integration/fixtures/deployV2Fixture.ts @@ -1,5 +1,5 @@ import type { NetworkConnection } from "hardhat/types/network"; -import { type Address, labelhash, zeroAddress } from "viem"; +import { type Address, getAddress, labelhash, zeroAddress } from "viem"; import { LOCAL_BATCH_GATEWAY_URL, ROLES } from "../../../deploy/constants.js"; import { splitName } from "../../utils/utils.js"; import { deployVerifiableProxy } from "./deployVerifiableProxy.js"; @@ -166,6 +166,7 @@ export async function deployV2Fixture( // parentRegistry == registries[1] // dedicatedResolver? == !resolverAddress return { + name, labels, tokenId, parentRegistry, @@ -182,6 +183,7 @@ export async function deployV2Fixture( dedicatedResolver: dedicatedResolver as resolver_ extends false ? NonNullable : undefined, + resolverAddress: getAddress(resolverAddress), }; } } diff --git a/contracts/test/integration/l1/ETHTLDResolver.test.ts b/contracts/test/integration/l1/ETHTLDResolver.test.ts index b60afee6..161b3ec3 100755 --- a/contracts/test/integration/l1/ETHTLDResolver.test.ts +++ b/contracts/test/integration/l1/ETHTLDResolver.test.ts @@ -13,6 +13,7 @@ import { namehash, parseAbi, toHex, + zeroAddress, } from "viem"; import { afterAll, afterEach, describe, expect, it } from "vitest"; @@ -21,9 +22,9 @@ import { UncheckedRollup } from "../../../lib/unruggable-gateways/src/UncheckedR import { expectVar } from "../../utils/expectVar.js"; import { injectRPCCounter } from "../../utils/hardhat-counter.js"; import { + type KnownProfile, COIN_TYPE_DEFAULT, COIN_TYPE_ETH, - type KnownProfile, PROFILE_ABI, bundleCalls, makeResolutions, @@ -129,7 +130,7 @@ const loadFixture = async () => { const dummySelector = "0x12345678"; const testAddress = "0x8000000000000000000000000000000000000001"; -const testNames = ["test.eth", "a.b.c.test.eth"]; +const testNames = ["test.eth", "a.b.c.test.eth"] as const; describe("ETHTLDResolver", () => { const rpcs: Record = {}; @@ -206,7 +207,7 @@ describe("ETHTLDResolver", () => { }); }); - describe("eth", () => { + describe("", () => { it("ethResolver", async () => { const F = await loadFixture(); const address = await F.ethTLDResolver.read.ethResolver(); @@ -238,6 +239,94 @@ describe("ETHTLDResolver", () => { }); }); + describe("introspection", () => { + it("", async () => { + const F = await loadFixture(); + const resolverAddress = await F.ethTLDResolver.read.ethResolver(); + const dns = dnsEncodeName("eth"); + await expect( + F.ethTLDResolver.read.isResolverOffchain([dns]), + "isResolverOffchain", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([resolverAddress, false]); + }); + it("not .eth", async () => { + const F = await loadFixture(); + const dns = dnsEncodeName("com"); + await expect(F.ethTLDResolver.read.isResolverOffchain([dns])) + .toBeRevertedWithCustomError("UnreachableName") + .withArgs([dns]); + // await expect( + // F.ethTLDResolver.read.getResolver([dnsEncodeName("eth")]), + // "getResolver", + // ).resolves.toStrictEqual([resolverAddress, false]); + }); + it("dne", async () => { + const F = await loadFixture(); + const name = testNames[0]; + const dns = dnsEncodeName(name); + await sync(); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([ + keccak256(toHex(getLabelAt(name))), + ]), + "isActiveRegistrationV1", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.isResolverOffchain([dns]), + "isResolverOffchain", + ).resolves.toStrictEqual(true); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([zeroAddress, true]); + }); + it("Mainnet V1", async () => { + const F = await loadFixture(); + const name = testNames[0]; + const dns = dnsEncodeName(name); + const { resolverAddress } = await F.mainnetV1.setupName({ name }); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([ + keccak256(toHex(getLabelAt(name))), + ]), + "isActiveRegistrationV1", + ).resolves.toStrictEqual(true); + await expect( + F.ethTLDResolver.read.isResolverOffchain([dns]), + "isResolverOffchain", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([resolverAddress, false]); + }); + it("Namechain", async () => { + const F = await loadFixture(); + const name = testNames[0]; + const dns = dnsEncodeName(name); + const { resolverAddress } = await F.namechain.setupName({ name }); + await sync(); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([ + keccak256(toHex(getLabelAt(name))), + ]), + "isActiveRegistrationV1", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.isResolverOffchain([dns]), + "isResolverOffchain", + ).resolves.toStrictEqual(true); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([resolverAddress, true]); + }); + }); + describe("unregistered", () => { for (const name of testNames) { it(name, async () => { diff --git a/contracts/test/utils/utils.ts b/contracts/test/utils/utils.ts index 45658c94..a8bfe91f 100644 --- a/contracts/test/utils/utils.ts +++ b/contracts/test/utils/utils.ts @@ -53,9 +53,9 @@ export function getParentName(name: string) { return i == -1 ? "" : name.slice(i + 1); } -// "a.b.c" 0 => "a" +// "a.b.c" 0 => "a" aka firstLabel() // -1 => "c" // 5 => "" -export function getLabelAt(name: string, index: number) { +export function getLabelAt(name: string, index = 0) { return splitName(name).at(index) ?? ""; } From 10fae3fe1ab3988104164500bd0b8e7b6166a007 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 13 Oct 2025 15:27:28 -0400 Subject: [PATCH 04/10] add missing test --- contracts/test/integration/l1/ETHTLDResolver.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/test/integration/l1/ETHTLDResolver.test.ts b/contracts/test/integration/l1/ETHTLDResolver.test.ts index 161b3ec3..ee93b3d6 100755 --- a/contracts/test/integration/l1/ETHTLDResolver.test.ts +++ b/contracts/test/integration/l1/ETHTLDResolver.test.ts @@ -259,10 +259,9 @@ describe("ETHTLDResolver", () => { await expect(F.ethTLDResolver.read.isResolverOffchain([dns])) .toBeRevertedWithCustomError("UnreachableName") .withArgs([dns]); - // await expect( - // F.ethTLDResolver.read.getResolver([dnsEncodeName("eth")]), - // "getResolver", - // ).resolves.toStrictEqual([resolverAddress, false]); + await expect(F.ethTLDResolver.read.getResolver([dns])) + .toBeRevertedWithCustomError("UnreachableName") + .withArgs([dns]); }); it("dne", async () => { const F = await loadFixture(); From 23a5d0014ae7116138e23efce692615d38e00af5 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Tue, 14 Oct 2025 16:40:42 -0400 Subject: [PATCH 05/10] bump ens-contracts --- contracts/foundry.lock | 2 +- contracts/lib/ens-contracts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/foundry.lock b/contracts/foundry.lock index 80a4ede5..cceb8ded 100644 --- a/contracts/foundry.lock +++ b/contracts/foundry.lock @@ -3,7 +3,7 @@ "rev": "82cc81935de1d1a82e021cf1030d902c5248982b" }, "lib/ens-contracts": { - "rev": "04cf79fb11d5351a4a5a52412b2e4819f990394d" + "rev": "05c3412f4e242df54d293124dfb35d310264699f" }, "lib/forge-std": { "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505" diff --git a/contracts/lib/ens-contracts b/contracts/lib/ens-contracts index 04cf79fb..05c3412f 160000 --- a/contracts/lib/ens-contracts +++ b/contracts/lib/ens-contracts @@ -1 +1 @@ -Subproject commit 04cf79fb11d5351a4a5a52412b2e4819f990394d +Subproject commit 05c3412f4e242df54d293124dfb35d310264699f From 4ae169fc33cf5ebcda93ca9d90bb609f4139e581 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Fri, 17 Oct 2025 19:48:17 -0700 Subject: [PATCH 06/10] fix generated/artifacts issue --- contracts/script/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/script/setup.ts b/contracts/script/setup.ts index 10d49f92..afa4c84b 100644 --- a/contracts/script/setup.ts +++ b/contracts/script/setup.ts @@ -252,6 +252,7 @@ export async function setupCrossChainEnvironment({ } try { console.log("Deploying ENSv2..."); + await patchArtifactsV1(); const names = ["deployer", "owner", "bridger", "user", "user2"]; extraAccounts += names.length; @@ -375,7 +376,6 @@ export async function setupCrossChainEnvironment({ const tags = [tag, "local"]; const scripts = [`deploy/${tag}`, "deploy/shared"]; if (isL1) { - await patchArtifactsV1(); scripts.unshift("lib/ens-contracts/deploy"); tags.push("use_root"); // deploy root contracts tags.push("allow_unsafe"); // tate hacks From b0e7cbb2ddca34e0c5e6d09218a25a20efb6bfe5 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Sun, 19 Oct 2025 20:27:33 -0700 Subject: [PATCH 07/10] add inflight logic, change burn(), add IUnruggableResolver --- contracts/deploy/l1/04_ETHTLDResolver.ts | 7 +- contracts/foundry.lock | 2 +- contracts/foundry.toml | 2 - contracts/lib/ens-contracts | 2 +- .../src/L1/bridge/L1BridgeController.sol | 4 +- contracts/src/L1/resolver/ETHTLDResolver.sol | 115 ++++++++-- .../common/registry/PermissionedRegistry.sol | 56 ++--- .../registry/interfaces/IStandardRegistry.sol | 3 +- .../interfaces/ICompositeExtendedResolver.sol | 4 +- .../interfaces/IUnruggableResolver.sol | 18 ++ .../integration/fixtures/deployV1Fixture.ts | 21 +- .../integration/l1/ETHTLDResolver.test.ts | 209 +++++++++++++++--- .../registry/PermissionedRegistry.t.sol | 10 +- contracts/test/utils/utils.ts | 22 -- 14 files changed, 347 insertions(+), 128 deletions(-) create mode 100644 contracts/src/common/resolver/interfaces/IUnruggableResolver.sol diff --git a/contracts/deploy/l1/04_ETHTLDResolver.ts b/contracts/deploy/l1/04_ETHTLDResolver.ts index 17636e5d..a51bdecb 100755 --- a/contracts/deploy/l1/04_ETHTLDResolver.ts +++ b/contracts/deploy/l1/04_ETHTLDResolver.ts @@ -15,6 +15,9 @@ export default execute( "BatchGatewayProvider", ); + const ethRegistry = + get<(typeof artifacts.PermissionedRegistry)["abi"]>("ETHRegistry"); + const bridgeController = get<(typeof artifacts.L1BridgeController)["abi"]>("BridgeController"); @@ -62,6 +65,7 @@ export default execute( args: [ nameWrapper.address, batchGatewayProvider.address, + ethRegistry.address, bridgeController.address, ethSelfResolver.address, args.verifierAddress, @@ -81,9 +85,10 @@ export default execute( dependencies: [ "NameWrapper", "BatchGatewayProvider", + "ETHRegistry", + "BridgeController", "VerifiableFactory", "DedicatedResolver", - "BridgeController", ], }, ); diff --git a/contracts/foundry.lock b/contracts/foundry.lock index cceb8ded..c0e45cdd 100644 --- a/contracts/foundry.lock +++ b/contracts/foundry.lock @@ -3,7 +3,7 @@ "rev": "82cc81935de1d1a82e021cf1030d902c5248982b" }, "lib/ens-contracts": { - "rev": "05c3412f4e242df54d293124dfb35d310264699f" + "rev": "a392133a641f7a88dd8d08616e1113e1b6d0416a" }, "lib/forge-std": { "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505" diff --git a/contracts/foundry.toml b/contracts/foundry.toml index a691b54a..5e342328 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -15,5 +15,3 @@ runs = 4096 [lint] lint_on_build = false - -# forge i ensdomains/ens-contracts@branch=feature-fet-1938/fixed-deploy-scripts diff --git a/contracts/lib/ens-contracts b/contracts/lib/ens-contracts index 05c3412f..a392133a 160000 --- a/contracts/lib/ens-contracts +++ b/contracts/lib/ens-contracts @@ -1 +1 @@ -Subproject commit 05c3412f4e242df54d293124dfb35d310264699f +Subproject commit a392133a641f7a88dd8d08616e1113e1b6d0416a diff --git a/contracts/src/L1/bridge/L1BridgeController.sol b/contracts/src/L1/bridge/L1BridgeController.sol index 2dce64b9..6af933b8 100644 --- a/contracts/src/L1/bridge/L1BridgeController.sol +++ b/contracts/src/L1/bridge/L1BridgeController.sol @@ -115,8 +115,8 @@ contract L1BridgeController is EjectionController { // check that the label matches the token id _assertTokenIdMatchesLabel(tokenId, transferData.dnsEncodedName); - // burn the token - REGISTRY.burn(tokenId); + // burn the token but keep the registry/resolver + REGISTRY.burn(tokenId, true); // send the message to the bridge BRIDGE.sendMessage(BridgeEncoderLib.encodeEjection(transferData)); diff --git a/contracts/src/L1/resolver/ETHTLDResolver.sol b/contracts/src/L1/resolver/ETHTLDResolver.sol index b5003a16..dc82558e 100755 --- a/contracts/src/L1/resolver/ETHTLDResolver.sol +++ b/contracts/src/L1/resolver/ETHTLDResolver.sol @@ -33,13 +33,18 @@ import { import {GatewayRequest, EvalFlag} from "@unruggable/gateways/contracts/GatewayRequest.sol"; import {BridgeRolesLib} from "../../common/bridge/libraries/BridgeRolesLib.sol"; +import {IPermissionedRegistry} from "../../common/registry/interfaces/IPermissionedRegistry.sol"; +import {IRegistry} from "../../common/registry/interfaces/IRegistry.sol"; +import {IRegistryDatastore} from "../../common/registry/interfaces/IRegistryDatastore.sol"; import { ICompositeExtendedResolver } from "../../common/resolver/interfaces/ICompositeExtendedResolver.sol"; +import {IUnruggableResolver} from "../../common/resolver/interfaces/IUnruggableResolver.sol"; import { DedicatedResolverLayoutLib } from "../../common/resolver/libraries/DedicatedResolverLayoutLib.sol"; import {LibLabel} from "../../common/utils/LibLabel.sol"; +import {LibRegistry} from "../../universalResolver/libraries/LibRegistry.sol"; import {L1BridgeController} from "../bridge/L1BridgeController.sol"; /// @notice Resolver that performs ".eth" resolutions for Namechain (via gateway) or V1 (via fallback). @@ -51,6 +56,7 @@ import {L1BridgeController} from "../bridge/L1BridgeController.sol"; /// 3. If no resolver is found, reverts `UnreachableName`. contract ETHTLDResolver is ICompositeExtendedResolver, + IUnruggableResolver, IERC7996, GatewayFetchTarget, ResolverCaller, @@ -59,6 +65,17 @@ contract ETHTLDResolver is { using GatewayFetcher for GatewayRequest; + //////////////////////////////////////////////////////////////////////// + // Types + //////////////////////////////////////////////////////////////////////// + + enum NameState { + NULL, + POST_MIGRATION, + POST_EJECTION, + NAMECHAIN + } + //////////////////////////////////////////////////////////////////////// // Constants //////////////////////////////////////////////////////////////////////// @@ -73,6 +90,8 @@ contract ETHTLDResolver is IBaseRegistrar public immutable ETH_REGISTRAR_V1; + IPermissionedRegistry public immutable ETH_REGISTRY; + L1BridgeController public immutable L1_BRIDGE_CONTROLLER; address public immutable NAMECHAIN_DATASTORE; @@ -105,6 +124,7 @@ contract ETHTLDResolver is constructor( INameWrapper nameWrapper, IGatewayProvider batchGatewayProvider, + IPermissionedRegistry ethRegistry, L1BridgeController l1BridgeController, address ethResolver_, IGatewayVerifier namechainVerifier_, @@ -114,6 +134,7 @@ contract ETHTLDResolver is NAME_WRAPPER = nameWrapper; ETH_REGISTRAR_V1 = nameWrapper.registrar(); BATCH_GATEWAY_PROVIDER = batchGatewayProvider; + ETH_REGISTRY = ethRegistry; L1_BRIDGE_CONTROLLER = l1BridgeController; NAMECHAIN_DATASTORE = namechainDatastore; NAMECHAIN_ETH_REGISTRY = namechainEthRegistry; @@ -129,6 +150,7 @@ contract ETHTLDResolver is return type(IExtendedResolver).interfaceId == interfaceId || type(ICompositeExtendedResolver).interfaceId == interfaceId || + type(IUnruggableResolver).interfaceId == interfaceId || type(IERC7996).interfaceId == interfaceId || super.supportsInterface(interfaceId); } @@ -160,7 +182,7 @@ contract ETHTLDResolver is bytes calldata name, bytes calldata data ) external view returns (bytes memory result) { - (address resolver, bool offchain) = _determineResolver(name); + (address resolver, bool offchain) = _determineMainnetResolver(name); if (offchain) { bytes[] memory calls; bool multi = bytes4(data) == IMulticallable.multicall.selector; @@ -176,9 +198,20 @@ contract ETHTLDResolver is } } + /// @inheritdoc IUnruggableResolver + function unruggableGateway() + external + view + returns (uint256 coinType, IGatewayVerifier verifier, string[] memory gateways) + { + coinType = 0x80eeeeee; // TODO: namechain coinType + verifier = namechainVerifier; + // gateways = namechainVerifier.gatewayURLs(); + } + /// @inheritdoc ICompositeExtendedResolver function isResolverOffchain(bytes calldata name) external view returns (bool offchain) { - (, offchain) = _determineResolver(name); + (, offchain) = _determineMainnetResolver(name); } /// @inheritdoc ICompositeExtendedResolver @@ -191,12 +224,14 @@ contract ETHTLDResolver is function getResolver( bytes calldata name ) external view returns (address resolver, bool offchain) { - (resolver, offchain) = _determineResolver(name); + (resolver, offchain) = _determineMainnetResolver(name); if (offchain) { fetch( namechainVerifier, _createRequest(0, name), - this.getResolverCallback.selector // ==> step 2 + this.getResolverCallback.selector, // ==> step 2, + name, + new string[](0) ); } } @@ -205,10 +240,15 @@ contract ETHTLDResolver is function getResolverCallback( bytes[] calldata values, uint8 /*exitCode*/, - bytes calldata /*extraData*/ - ) external pure returns (address resolver, bool offchain) { - resolver = address(uint160(uint256(bytes32(values[1])))); - offchain = true; + bytes calldata name + ) external view returns (address resolver, bool offchain) { + NameState state = _nameStateFrom(values[1]); + if (state == NameState.NAMECHAIN) { + resolver = address(uint160(uint256(bytes32(values[1])))); + offchain = true; + } else { + resolver = _determineInflightResolver(name, state); + } } /// @notice CCIP-Read callback for `resolve()`. @@ -222,13 +262,22 @@ contract ETHTLDResolver is bytes[] calldata values, uint8 exitCode, bytes calldata extraData - ) external pure returns (bytes memory) { + ) external view returns (bytes memory) { (bytes memory name, bool multi, bytes[] memory m) = abi.decode( extraData, (bytes, bool, bytes[]) ); if (exitCode == _EXIT_CODE_NO_RESOLVER) { - revert UnreachableName(name); + address resolver = _determineInflightResolver(name, _nameStateFrom(values[1])); + if (address(resolver) == address(0)) { + revert UnreachableName(name); + } + callResolver( + resolver, + name, + multi ? abi.encodeCall(IMulticallable.multicall, (m)) : m[0], + BATCH_GATEWAY_PROVIDER.gateways() + ); } bytes memory defaultAddress = values[m.length]; // stored at end if (multi) { @@ -347,7 +396,10 @@ contract ETHTLDResolver is // output[-1] = default address uint8 max = uint8(m.length); GatewayRequest memory req = _createRequest(max + 1, name); - req.pushOutput(1).requireNonzero(_EXIT_CODE_NO_RESOLVER).target(); // target resolver + req.pushOutput(1).push(uint256(type(NameState).max)).gt().assertNonzero( + _EXIT_CODE_NO_RESOLVER + ); // is this a real resolver? + req.pushOutput(1).target(); // target resolver req.push(bytes("")).dup().setOutput(0).setOutput(1); // clear outputs uint8 errorCount; // number of errors for (uint8 i; i < m.length; ++i) { @@ -430,7 +482,7 @@ contract ETHTLDResolver is return abi.encode(m); // all calls failed } else { bytes memory v = m[0]; - assembly { + assembly ("memory-safe") { revert(add(v, 32), mload(v)) // revert with the call that failed } } @@ -452,7 +504,7 @@ contract ETHTLDResolver is } /// @dev Determine underlying resolver and location for `name`. - function _determineResolver( + function _determineMainnetResolver( bytes memory name ) internal view returns (address resolver, bool offchain) { (bool matched, , uint256 prevOffset, uint256 offset) = NameCoder.matchSuffix( @@ -474,6 +526,41 @@ contract ETHTLDResolver is return (address(0), true); } + /// @dev Determine underlying resolver while `name` is inflight to Namechain. + function _determineInflightResolver( + bytes memory name, + NameState state + ) internal view returns (address resolver) { + if (state == NameState.POST_MIGRATION) { + (resolver, , ) = RegistryUtils.findResolver(NAME_WRAPPER.ens(), name, 0); + } else if (state == NameState.POST_EJECTION) { + (, , uint256 offset, ) = NameCoder.matchSuffix(name, 0, NameCoder.ETH_NODE); + (string memory label, ) = NameCoder.extractLabel(name, offset); + (, IRegistryDatastore.Entry memory entry) = ETH_REGISTRY.getNameData(label); + if (entry.subregistry != address(0)) { + bytes memory prefix = BytesUtils.substring(name, 0, offset + 1); + prefix[offset] = 0; + (, resolver, , ) = LibRegistry.findResolver( + IRegistry(entry.subregistry), + prefix, + 0 + ); + } + if (resolver == address(0)) { + resolver = entry.resolver; + } + } + if (resolver == address(this)) { + resolver = address(0); + } + } + + /// @dev Map an abi-encoded address from `GatewayRequest` to a `NameState`. + function _nameStateFrom(bytes memory value) internal pure returns (NameState) { + uint256 i = uint256(bytes32(value)); + return i > uint256(type(NameState).max) ? NameState.NAMECHAIN : NameState(i); + } + /// @dev Prepare response based on the request. /// /// @param data The original request (or error). @@ -511,7 +598,7 @@ contract ETHTLDResolver is } else if (selector == IABIResolver.ABI.selector) { uint256 contentType; if (value.length > 0) { - assembly { + assembly ("memory-safe") { let ptr := add(value, 32) contentType := mload(ptr) // extract contentType from first word mstore(ptr, sub(mload(value), 32)) // reduce length diff --git a/contracts/src/common/registry/PermissionedRegistry.sol b/contracts/src/common/registry/PermissionedRegistry.sol index b7f598c8..7171e2ad 100644 --- a/contracts/src/common/registry/PermissionedRegistry.sol +++ b/contracts/src/common/registry/PermissionedRegistry.sol @@ -14,6 +14,7 @@ import {IPermissionedRegistry} from "./interfaces/IPermissionedRegistry.sol"; import {IRegistry} from "./interfaces/IRegistry.sol"; import {IRegistryDatastore} from "./interfaces/IRegistryDatastore.sol"; import {IRegistryMetadata} from "./interfaces/IRegistryMetadata.sol"; +import {IStandardRegistry} from "./interfaces/IStandardRegistry.sol"; import {ITokenObserver} from "./interfaces/ITokenObserver.sol"; import {RegistryRolesLib} from "./libraries/RegistryRolesLib.sol"; import {MetadataMixin} from "./MetadataMixin.sol"; @@ -67,31 +68,21 @@ contract PermissionedRegistry is // Implementation //////////////////////////////////////////////////////////////////////// - /** - * @dev Burn a name. - * This will destroy the name and remove it from the registry. - * - * @param tokenId The token ID of the name to relinquish. - */ + /// @inheritdoc IStandardRegistry function burn( - uint256 tokenId + uint256 tokenId, + bool retain ) external override onlyNonExpiredTokenRoles(tokenId, RegistryRolesLib.ROLE_BURN) { _burn(ownerOf(tokenId), tokenId, 1); - (IRegistryDatastore.Entry memory entry, ) = _getEntry(tokenId); - _setEntry( - tokenId, - IRegistryDatastore.Entry({ - subregistry: address(0), - expiry: 0, - tokenVersionId: entry.tokenVersionId, - resolver: address(0), - eacVersionId: entry.eacVersionId - }) - ); - emit SubregistryUpdate(tokenId, address(0)); - emit ResolverUpdate(tokenId, address(0)); - + entry.expiry = 0; + if (!retain) { + entry.resolver = address(0); + entry.subregistry = address(0); + emit SubregistryUpdate(tokenId, address(0)); + emit ResolverUpdate(tokenId, address(0)); + } + _setEntry(tokenId, entry); emit NameBurned(tokenId, msg.sender); } @@ -99,17 +90,7 @@ contract PermissionedRegistry is uint256 tokenId, IRegistry registry ) external override onlyNonExpiredTokenRoles(tokenId, RegistryRolesLib.ROLE_SET_SUBREGISTRY) { - (IRegistryDatastore.Entry memory entry, ) = _getEntry(tokenId); - _setEntry( - tokenId, - IRegistryDatastore.Entry({ - subregistry: address(registry), - expiry: entry.expiry, - tokenVersionId: entry.tokenVersionId, - resolver: entry.resolver, - eacVersionId: entry.eacVersionId - }) - ); + DATASTORE.setSubregistry(LibLabel.getCanonicalId(tokenId), address(registry)); emit SubregistryUpdate(tokenId, address(registry)); } @@ -175,15 +156,8 @@ contract PermissionedRegistry is if (expires < entry.expiry) { revert CannotReduceExpiration(entry.expiry, expires); } - - IRegistryDatastore.Entry memory newEntry = IRegistryDatastore.Entry({ - subregistry: entry.subregistry, - expiry: expires, - tokenVersionId: entry.tokenVersionId, - resolver: entry.resolver, - eacVersionId: entry.eacVersionId - }); - _setEntry(tokenId, newEntry); + entry.expiry = expires; + _setEntry(tokenId, entry); emit SubregistryUpdate(tokenId, entry.subregistry); ITokenObserver observer = tokenObservers[tokenId]; diff --git a/contracts/src/common/registry/interfaces/IStandardRegistry.sol b/contracts/src/common/registry/interfaces/IStandardRegistry.sol index a6532b5e..f99de9c6 100644 --- a/contracts/src/common/registry/interfaces/IStandardRegistry.sol +++ b/contracts/src/common/registry/interfaces/IStandardRegistry.sol @@ -69,8 +69,9 @@ interface IStandardRegistry is IRegistry { /** * @dev Burns a name. * @param tokenId The token ID of the name to burn. + * @param retain If `true`, leaves registry and resolver unmodified. */ - function burn(uint256 tokenId) external; + function burn(uint256 tokenId, bool retain) external; /** * @dev Sets a name. diff --git a/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol b/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol index 6612388c..b5b517c2 100644 --- a/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol +++ b/contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol @@ -3,6 +3,8 @@ pragma solidity >=0.8.13; import {IExtendedResolver} from "@ens/contracts/resolvers/profiles/IExtendedResolver.sol"; +/// @notice A resolver that is composed of multiple resolvers. +/// @dev Interface selector: `0xf686ea10` interface ICompositeExtendedResolver is IExtendedResolver { /// @notice Fetch the underlying resolver for `name`. /// Callers should enable EIP-3668. @@ -10,7 +12,7 @@ interface ICompositeExtendedResolver is IExtendedResolver { /// @param name The DNS-encoded name. /// /// @return resolver The underlying resolver address. - /// @return offchain `true` if required offchain data. + /// @return offchain `true` if `resolver` is offchain. function getResolver(bytes memory name) external view returns (address resolver, bool offchain); /// @notice Determine if resolving `name` requires offchain data. diff --git a/contracts/src/common/resolver/interfaces/IUnruggableResolver.sol b/contracts/src/common/resolver/interfaces/IUnruggableResolver.sol new file mode 100644 index 00000000..db7d4590 --- /dev/null +++ b/contracts/src/common/resolver/interfaces/IUnruggableResolver.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {IGatewayVerifier} from "@unruggable/gateways/contracts/GatewayFetchTarget.sol"; + +/// @notice A resolver that uses an Unruggable gateway. +/// @dev Interface selector: `0xe4b2bbef` +interface IUnruggableResolver { + /// @notice Expose Unruggable gateway parameters. + /// + /// @return coinType The source rollup coin type. + /// @return verifier The Unruggable Verifier contract. + /// @return gatewayURLs The gateways used by `verifier`. + function unruggableGateway() + external + view + returns (uint256 coinType, IGatewayVerifier verifier, string[] memory gatewayURLs); +} diff --git a/contracts/test/integration/fixtures/deployV1Fixture.ts b/contracts/test/integration/fixtures/deployV1Fixture.ts index 079e0bad..8a551bf8 100755 --- a/contracts/test/integration/fixtures/deployV1Fixture.ts +++ b/contracts/test/integration/fixtures/deployV1Fixture.ts @@ -1,5 +1,6 @@ import type { NetworkConnection } from "hardhat/types/network"; import { + Account, type Address, getAddress, labelhash, @@ -93,30 +94,34 @@ export async function deployV1Fixture( async function setupName({ name, resolverAddress = publicResolver.address, + account = walletClient.account, }: { name: string; resolverAddress?: Address; + account?: Account; }) { + resolverAddress = getAddress(resolverAddress); // fix checksum const labels = splitName(name); let i = labels.length; if (name.endsWith(".eth")) { await ethRegistrar.write.register([ BigInt(labelhash(labels[(i -= 2)])), - walletClient.account.address, + account.address, (1n << 64n) - 1n, ]); } while (i > 0) { const parent = labels.slice(i).join("."); const child = labels[--i]; - await ensRegistry.write.setSubnodeOwner([ - namehash(parent), - labelhash(child), - walletClient.account.address, - ]); + await ensRegistry.write.setSubnodeOwner( + [namehash(parent), labelhash(child), account.address], + { account }, + ); } // set resolver on leaf - await ensRegistry.write.setResolver([namehash(name), resolverAddress]); - return { name, labels, resolverAddress: getAddress(resolverAddress) }; + await ensRegistry.write.setResolver([namehash(name), resolverAddress], { + account, + }); + return { name, labels, resolverAddress }; } } diff --git a/contracts/test/integration/l1/ETHTLDResolver.test.ts b/contracts/test/integration/l1/ETHTLDResolver.test.ts index ee93b3d6..5b341558 100755 --- a/contracts/test/integration/l1/ETHTLDResolver.test.ts +++ b/contracts/test/integration/l1/ETHTLDResolver.test.ts @@ -99,6 +99,7 @@ async function fixture() { [ mainnetV1.nameWrapper.address, mainnetV1.batchGatewayProvider.address, + mainnetV2.ethRegistry.address, mockBridgeController.address, ethResolver.address, verifierAddress, @@ -149,7 +150,13 @@ describe("ETHTLDResolver", () => { shouldSupportInterfaces({ contract: () => loadFixture().then((F) => F.ethTLDResolver), - interfaces: ["IERC165", "IERC7996", "IExtendedResolver"], + interfaces: [ + "IERC165", + "IERC7996", + "IExtendedResolver", + "ICompositeExtendedResolver", + "IUnruggableResolver", + ], }); shouldSupportFeatures({ @@ -205,6 +212,14 @@ describe("ETHTLDResolver", () => { const address = await F.ethTLDResolver.read.namechainVerifier(); expectVar({ address }).toEqualAddress(testAddress); }); + it("unruggableGateway", async () => { + const F = await loadFixture(); + const [coinType, verifier, gatewayURLs] = + await F.ethTLDResolver.read.unruggableGateway(); + expectVar({ coinType }).toStrictEqual(0x80eeeeeen); + expectVar({ verifier }).toStrictEqual(F.verifierAddress); + expectVar({ gatewayURLs }).toStrictEqual([]); // using verifier defaults + }); }); describe("", () => { @@ -270,7 +285,7 @@ describe("ETHTLDResolver", () => { await sync(); await expect( F.ethTLDResolver.read.isActiveRegistrationV1([ - keccak256(toHex(getLabelAt(name))), + labelhash(getLabelAt(name)), ]), "isActiveRegistrationV1", ).resolves.toStrictEqual(false); @@ -281,7 +296,7 @@ describe("ETHTLDResolver", () => { await expect( F.ethTLDResolver.read.getResolver([dns]), "getResolver", - ).resolves.toStrictEqual([zeroAddress, true]); + ).resolves.toStrictEqual([zeroAddress, false]); }); it("Mainnet V1", async () => { const F = await loadFixture(); @@ -290,7 +305,7 @@ describe("ETHTLDResolver", () => { const { resolverAddress } = await F.mainnetV1.setupName({ name }); await expect( F.ethTLDResolver.read.isActiveRegistrationV1([ - keccak256(toHex(getLabelAt(name))), + labelhash(getLabelAt(name)), ]), "isActiveRegistrationV1", ).resolves.toStrictEqual(true); @@ -311,7 +326,7 @@ describe("ETHTLDResolver", () => { await sync(); await expect( F.ethTLDResolver.read.isActiveRegistrationV1([ - keccak256(toHex(getLabelAt(name))), + labelhash(getLabelAt(name)), ]), "isActiveRegistrationV1", ).resolves.toStrictEqual(false); @@ -362,7 +377,7 @@ describe("ETHTLDResolver", () => { } }); - describe("still registered on V1", () => { + describe("on V1", () => { for (const name of testNames) { it(name, async () => { const F = await loadFixture(); @@ -385,7 +400,90 @@ describe("ETHTLDResolver", () => { } }); - describe("migrated from V1", () => { + describe("migrating to L1", () => { + for (const name of testNames) { + it(name, async () => { + const F = await loadFixture(); + const kp: KnownProfile = { + name, + addresses: [{ coinType: COIN_TYPE_ETH, value: testAddress }], + }; + const [res] = makeResolutions(kp); + await F.mainnetV1.setupName(kp); + await F.mainnetV1.publicResolver.write.multicall([[res.write]]); + const label2LD = getLabelAt(name, -2); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([labelhash(label2LD)]), + "active.before", + ).resolves.toStrictEqual(true); + // "burn" for post-migration state + await F.mainnetV1.ethRegistrar.write.safeTransferFrom([ + F.mainnetV1.walletClient.account.address, + F.mockBridgeController.address, + BigInt(labelhash(label2LD)), + ]); + // setup post-migration state + const { dedicatedResolver } = await F.mainnetV2.setupName({ name }); + await dedicatedResolver.write.multicall([[res.writeDedicated]]); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([labelhash(label2LD)]), + "active.after", + ).resolves.toStrictEqual(false); + const [answer, resolver] = + await F.mainnetV2.universalResolver.read.resolve([ + dnsEncodeName(kp.name), + res.call, + ]); + expectVar({ resolver }).toEqualAddress(dedicatedResolver.address); + res.expect(answer); + }); + } + }); + + describe("migrating to L2", () => { + for (const name of testNames) { + it(name, async () => { + const F = await loadFixture(); + const kp: KnownProfile = { + name, + addresses: [{ coinType: COIN_TYPE_ETH, value: testAddress }], + }; + const [res] = makeResolutions(kp); + await F.mainnetV1.setupName(kp); + await F.mainnetV1.publicResolver.write.multicall([[res.write]]); + const label2LD = getLabelAt(name, -2); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([labelhash(label2LD)]), + "active.before", + ).resolves.toStrictEqual(true); + // "burn" for post-migration state + await F.mainnetV1.ethRegistrar.write.safeTransferFrom([ + F.mainnetV1.walletClient.account.address, + F.mockBridgeController.address, + BigInt(labelhash(label2LD)), + ]); + // setup faux premigrated state + await F.namechain.setupName({ + name: `${label2LD}.eth`, + resolverAddress: toHex(1, { size: 20 }), + }); + await sync(); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([labelhash(label2LD)]), + "active.after", + ).resolves.toStrictEqual(false); + const [answer, resolver] = + await F.mainnetV2.universalResolver.read.resolve([ + dnsEncodeName(kp.name), + res.call, + ]); + expectVar({ resolver }).toEqualAddress(F.ethTLDResolver.address); + res.expect(answer); + }); + } + }); + + describe("on L2", () => { for (const name of testNames) { it(name, async () => { const F = await loadFixture(); @@ -419,7 +517,7 @@ describe("ETHTLDResolver", () => { } }); - describe("ejected from Namechain", () => { + describe("ejecting L1 -> L2", () => { for (const name of testNames) { it(name, async () => { const F = await loadFixture(); @@ -430,19 +528,30 @@ describe("ETHTLDResolver", () => { const [res] = makeResolutions(kp); const { dedicatedResolver } = await F.mainnetV2.setupName(kp); await dedicatedResolver.write.multicall([[res.writeDedicated]]); + // "burn" for post-ejection state + const label2LD = getLabelAt(name, -2); + const [tokenId] = await F.mainnetV2.ethRegistry.read.getNameData([ + label2LD, + ]); + await F.mainnetV2.ethRegistry.write.burn([tokenId, true]); + // setup faux ejected state + await F.namechain.setupName({ + name: `${label2LD}.eth`, + resolverAddress: toHex(2, { size: 20 }), + }); await sync(); const [answer, resolver] = await F.mainnetV2.universalResolver.read.resolve([ dnsEncodeName(kp.name), res.call, ]); - expectVar({ resolver }).toEqualAddress(dedicatedResolver.address); + expectVar({ resolver }).toEqualAddress(F.ethTLDResolver.address); res.expect(answer); }); } }); - describe("registered on Namechain", () => { + describe("on L1", () => { for (const name of testNames) { it(name, async () => { const F = await loadFixture(); @@ -451,7 +560,7 @@ describe("ETHTLDResolver", () => { addresses: [{ coinType: COIN_TYPE_ETH, value: testAddress }], }; const [res] = makeResolutions(kp); - const { dedicatedResolver } = await F.namechain.setupName(kp); + const { dedicatedResolver } = await F.mainnetV2.setupName(kp); await dedicatedResolver.write.multicall([[res.writeDedicated]]); await sync(); const [answer, resolver] = @@ -459,13 +568,58 @@ describe("ETHTLDResolver", () => { dnsEncodeName(kp.name), res.call, ]); - expectVar({ resolver }).toEqualAddress(F.ethTLDResolver.address); + expectVar({ resolver }).toEqualAddress(dedicatedResolver.address); + res.expect(answer); + }); + } + }); + + describe("expired on L1", () => { + for (const name of testNames) { + it(name, async () => { + const F = await loadFixture(); + const kp: KnownProfile = { + name, + addresses: [{ coinType: COIN_TYPE_ETH, value: testAddress }], + }; + const interval = 1000n; + //await sync(); + const { timestamp } = await F.mainnetV2.publicClient.getBlock(); + const [res] = makeResolutions(kp); + const { dedicatedResolver } = await F.mainnetV2.setupName({ + name: kp.name, + expiry: timestamp + interval, + }); + await dedicatedResolver.write.multicall([[res.writeDedicated]]); + //await sync(); + const [answer, resolver] = + await F.mainnetV2.universalResolver.read.resolve([ + dnsEncodeName(kp.name), + res.call, + ]); + expectVar({ resolver }).toEqualAddress(dedicatedResolver.address); res.expect(answer); + await chain1.networkHelpers.mine(2, { interval }); // wait for the name to expire + await sync(); // expired => checks namechain + await expect( + F.mainnetV2.universalResolver.read.resolve([ + dnsEncodeName(kp.name), + res.call, + ]), + ) + .toBeRevertedWithCustomError("ResolverError") + .withArgs([ + encodeErrorResult({ + abi: F.ethTLDResolver.abi, + errorName: "UnreachableName", + args: [dnsEncodeName(kp.name)], + }), + ]); }); } }); - describe("expired", () => { + describe("expired on L2", () => { for (const name of testNames) { it(name, async () => { const F = await loadFixture(); @@ -491,22 +645,19 @@ describe("ETHTLDResolver", () => { await chain2.networkHelpers.mine(2, { interval }); // wait for the name to expire await sync(); await expect( - F.ethTLDResolver.read.resolve([dnsEncodeName(kp.name), res.call]), - ).toBeRevertedWithCustomError("UnreachableName"); - // await expect( - // F.mainnetV2.universalResolver.read.resolve([ - // dnsEncodeName(kp.name), - // res.call, - // ]), - // ) - // .toBeRevertedWithCustomError("ResolverError") - // .withArgs( - // encodeErrorResult({ - // abi: F.ETHTLDResolver.abi, - // errorName: "UnreachableName", - // args: [dnsEncodeName(kp.name)], - // }), - // ); + F.mainnetV2.universalResolver.read.resolve([ + dnsEncodeName(kp.name), + res.call, + ]), + ) + .toBeRevertedWithCustomError("ResolverError") + .withArgs([ + encodeErrorResult({ + abi: F.ethTLDResolver.abi, + errorName: "UnreachableName", + args: [dnsEncodeName(kp.name)], + }), + ]); }); } }); diff --git a/contracts/test/unit/common/registry/PermissionedRegistry.t.sol b/contracts/test/unit/common/registry/PermissionedRegistry.t.sol index 6430f106..96b357fd 100644 --- a/contracts/test/unit/common/registry/PermissionedRegistry.t.sol +++ b/contracts/test/unit/common/registry/PermissionedRegistry.t.sol @@ -545,7 +545,7 @@ contract PermissionedRegistryTest is Test, ERC1155Holder { roleBitmap, uint64(block.timestamp) + 86400 ); - registry.burn(tokenId); + registry.burn(tokenId, false); vm.assertEq(registry.ownerOf(tokenId), address(0), "owner"); vm.assertEq(address(registry.getSubregistry("test2")), address(0), "registry"); vm.assertEq(registry.latestOwnerOf(tokenId), address(0), "latest"); // does not survive burn @@ -579,7 +579,7 @@ contract PermissionedRegistryTest is Test, ERC1155Holder { ); vm.prank(user1); - registry.burn(tokenId); + registry.burn(tokenId, false); // Verify roles are revoked after burning assertFalse( @@ -610,7 +610,7 @@ contract PermissionedRegistryTest is Test, ERC1155Holder { ); vm.recordLogs(); - registry.burn(tokenId); + registry.burn(tokenId, false); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 6); @@ -639,7 +639,7 @@ contract PermissionedRegistryTest is Test, ERC1155Holder { ) ); vm.prank(address(2)); - registry.burn(tokenId); + registry.burn(tokenId, false); vm.assertEq(registry.ownerOf(tokenId), address(1)); vm.assertEq(address(registry.getSubregistry("test2")), address(registry)); @@ -2723,7 +2723,7 @@ contract PermissionedRegistryTest is Test, ERC1155Holder { // Burn should succeed even without ROLE_CAN_TRANSFER_ADMIN vm.prank(user1); - registry.burn(tokenId); + registry.burn(tokenId, false); assertEq(registry.ownerOf(tokenId), address(0)); } diff --git a/contracts/test/utils/utils.ts b/contracts/test/utils/utils.ts index a8bfe91f..c694773d 100644 --- a/contracts/test/utils/utils.ts +++ b/contracts/test/utils/utils.ts @@ -8,16 +8,6 @@ import { labelhash } from "viem"; export { dnsEncodeName } from "../../lib/ens-contracts/test/fixtures/dnsEncodeName.js"; -// export function packetToBytes(packet: string) { -// const m = splitName(packet).flatMap(s => { -// let v = stringToBytes(s); -// if (v.length > 255) v = stringToBytes(`[${labelhash(s).slice(2)}]`); -// return [Uint8Array.of(v.length), v]; -// }); -// m.push(Uint8Array.of(0)); -// return concat(m); -// } - // see: NameUtils.labelToCanonicalId() export function labelToCanonicalId(label: string) { return getCanonicalId(BigInt(labelhash(label))); @@ -28,18 +18,6 @@ export function getCanonicalId(id: bigint) { return id ^ BigInt.asUintN(32, id); } -// export function dnsEncodeName(name: string) { -// return bytesToHex(packetToBytes(name)); -// } - -// export const labelhashUint256 = (label: string): bigint => { -// return BigInt(labelhash(label)); -// }; - -// export const namehashUint256 = (name: string): bigint => { -// return BigInt(namehash(name)); -// }; - // "" => [] // "a.b.c" => ["a", "b", "c"] export function splitName(name: string): string[] { From 0b1b18f19354596f221020fce989ef68c68ef515 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 20 Oct 2025 18:49:33 -0700 Subject: [PATCH 08/10] Add `findRegistries()` (#157) * added UR.findRegistries() * added LibRegistry.findRegistries() and tests --- .../universalResolver/UniversalResolverV2.sol | 13 +++++++ .../libraries/LibRegistry.sol | 35 +++++++++++++++++++ .../fixtures/deployV2Fixture.test.ts | 20 +++++++++-- .../fixtures/deployVerifiableProxy.ts | 2 +- .../{LibRegistry.sol => LibRegistry.t.sol} | 34 +++++++++++------- 5 files changed, 89 insertions(+), 15 deletions(-) rename contracts/test/unit/universalResolver/libraries/{LibRegistry.sol => LibRegistry.t.sol} (82%) diff --git a/contracts/src/universalResolver/UniversalResolverV2.sol b/contracts/src/universalResolver/UniversalResolverV2.sol index 46952404..dd633d9b 100644 --- a/contracts/src/universalResolver/UniversalResolverV2.sol +++ b/contracts/src/universalResolver/UniversalResolverV2.sol @@ -18,6 +18,19 @@ contract UniversalResolverV2 is AbstractUniversalResolver { ROOT_REGISTRY = root; } + /// @notice Find all registries in the ancestry of `name`. + /// * `findRegistries("") = []` + /// * `findRegistries("eth") = [, ]` + /// * `findRegistries("nick.eth") = [, , ]` + /// * `findRegistries("sub.nick.eth") = [null, , , ]` + /// + /// @param name The DNS-encoded name. + /// + /// @return Array of registries in label-order. + function findRegistries(bytes calldata name) external view returns (IRegistry[] memory) { + return LibRegistry.findRegistries(ROOT_REGISTRY, name, 0); + } + /// @inheritdoc AbstractUniversalResolver function findResolver( bytes memory name diff --git a/contracts/src/universalResolver/libraries/LibRegistry.sol b/contracts/src/universalResolver/libraries/LibRegistry.sol index 7696b809..607c6deb 100644 --- a/contracts/src/universalResolver/libraries/LibRegistry.sol +++ b/contracts/src/universalResolver/libraries/LibRegistry.sol @@ -84,4 +84,39 @@ library LibRegistry { parentRegistry = findExactRegistry(rootRegistry, name, next); } } + + /// @notice Find all registries in the ancestry of `name`. + /// + /// @param rootRegistry The root ENS registry. + /// @param name The DNS-encoded name. + /// @param offset The offset into `name` to begin the search. + /// + /// @return registries Array of registries in label-order. + function findRegistries( + IRegistry rootRegistry, + bytes memory name, + uint256 offset + ) internal view returns (IRegistry[] memory registries) { + registries = new IRegistry[](1 + NameCoder.countLabels(name, offset)); + registries[registries.length - 1] = rootRegistry; + _findRegistries(name, offset, registries, 0); + } + + /// @dev Recursive function for building ancestory. + function _findRegistries( + bytes memory name, + uint256 offset, + IRegistry[] memory registries, + uint256 index + ) private view returns (IRegistry registry) { + (string memory label, uint256 nextOffset) = NameCoder.extractLabel(name, offset); + if (bytes(label).length == 0) { + return registries[registries.length - 1]; + } + registry = _findRegistries(name, nextOffset, registries, index + 1); + if (address(registry) != address(0)) { + registry = registry.getSubregistry(label); + registries[index] = registry; + } + } } diff --git a/contracts/test/integration/fixtures/deployV2Fixture.test.ts b/contracts/test/integration/fixtures/deployV2Fixture.test.ts index 2a24ef31..e8c4f03f 100644 --- a/contracts/test/integration/fixtures/deployV2Fixture.test.ts +++ b/contracts/test/integration/fixtures/deployV2Fixture.test.ts @@ -1,9 +1,9 @@ import hre from "hardhat"; -import { type Address, zeroAddress } from "viem"; +import { type Address, getAddress, zeroAddress } from "viem"; import { describe, expect, it } from "vitest"; import { expectVar } from "../../utils/expectVar.js"; -import { labelToCanonicalId } from "../../utils/utils.js"; +import { dnsEncodeName, labelToCanonicalId } from "../../utils/utils.js"; import { deployV2Fixture } from "./deployV2Fixture.js"; import { ROLES } from "../../../deploy/constants.js"; @@ -75,6 +75,22 @@ describe("deployV2Fixture", () => { expectVar({ dedicatedResolver }).toBeUndefined(); }); + it("setupName() matches findRegistries()", async () => { + const F = await loadFixture(); + const name = "a.b.c.d"; + const { registries } = await F.setupName({ + name, + resolverAddress: zeroAddress, + }); + const regs1 = registries.map((x) => + x ? getAddress(x.address) : zeroAddress, + ); + const regs2 = await F.universalResolver.read.findRegistries([ + dnsEncodeName(name), + ]); + expect(regs1).toStrictEqual(regs2); + }); + it("overlapping names", async () => { const F = await loadFixture(); await F.setupName({ name: "test.eth" }); diff --git a/contracts/test/integration/fixtures/deployVerifiableProxy.ts b/contracts/test/integration/fixtures/deployVerifiableProxy.ts index 2069fa2e..1220dced 100755 --- a/contracts/test/integration/fixtures/deployVerifiableProxy.ts +++ b/contracts/test/integration/fixtures/deployVerifiableProxy.ts @@ -49,7 +49,7 @@ export async function deployVerifiableProxy({ const receipt = await waitForTransactionReceipt(walletClient, { hash }); const [log] = parseEventLogs({ abi: verifiableFactoryAbi, - eventName: 'ProxyDeployed', + eventName: "ProxyDeployed", logs: receipt.logs, }); return getContract({ diff --git a/contracts/test/unit/universalResolver/libraries/LibRegistry.sol b/contracts/test/unit/universalResolver/libraries/LibRegistry.t.sol similarity index 82% rename from contracts/test/unit/universalResolver/libraries/LibRegistry.sol rename to contracts/test/unit/universalResolver/libraries/LibRegistry.t.sol index 338789d4..70595385 100755 --- a/contracts/test/unit/universalResolver/libraries/LibRegistry.sol +++ b/contracts/test/unit/universalResolver/libraries/LibRegistry.t.sol @@ -6,6 +6,7 @@ pragma solidity >=0.8.13; import {Test} from "forge-std/Test.sol"; import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {EACBaseRolesLib} from "~src/common/access-control/EnhancedAccessControl.sol"; import { @@ -50,35 +51,44 @@ contract LibRegistryTest is Test, ERC1155Holder { rootRegistry = _createRegistry(); } - function _expectFindResolver( + function _expectFind( bytes memory name, uint256 resolverOffset, address parentRegistry, IRegistry[] memory registries ) internal view { - (, address resolver, bytes32 node, uint256 offset) = LibRegistry.findResolver( + (, address resolver, bytes32 node, uint256 resolverOffset_) = LibRegistry.findResolver( rootRegistry, name, 0 ); assertEq(resolver, resolverAddress, "resolver"); assertEq(node, NameCoder.namehash(name, 0), "node"); - assertEq(offset, resolverOffset, "offset"); + assertEq(resolverOffset_, resolverOffset, "offset"); assertEq( address(LibRegistry.findParentRegistry(rootRegistry, name, 0)), parentRegistry, "parent" ); - uint256 i; - for (offset = 0; i < registries.length; i++) { + IRegistry[] memory regs = LibRegistry.findRegistries(rootRegistry, name, 0); + assertEq(registries.length, regs.length, "count"); + for (uint256 i; i < regs.length; ++i) { + assertEq( + address(registries[i]), + address(regs[i]), + string.concat("registry[", Strings.toString(i), "]") + ); + } + uint256 offset; + for (uint256 i; i < registries.length; ++i) { assertEq( address(LibRegistry.findExactRegistry(rootRegistry, name, offset)), address(registries[i]), - "exact" + string.concat("exact[", Strings.toString(i), "]") ); - (, offset) = NameCoder.readLabel(name, offset); + (, offset) = NameCoder.nextLabel(name, offset); } - assertEq(i, registries.length, "count"); + assertEq(offset, name.length, "length"); } function test_findResolver_eth() external { @@ -101,7 +111,7 @@ contract LibRegistryTest is Test, ERC1155Holder { IRegistry[] memory v = new IRegistry[](2); v[0] = ethRegistry; v[1] = rootRegistry; - _expectFindResolver(name, 0, address(rootRegistry), v); + _expectFind(name, 0, address(rootRegistry), v); } function test_findResolver_resolverOnParent() external { @@ -120,7 +130,7 @@ contract LibRegistryTest is Test, ERC1155Holder { v[0] = testRegistry; v[1] = ethRegistry; v[2] = rootRegistry; - _expectFindResolver(name, 0, address(ethRegistry), v); + _expectFind(name, 0, address(ethRegistry), v); } function test_findResolver_resolverOnRoot() external { @@ -139,7 +149,7 @@ contract LibRegistryTest is Test, ERC1155Holder { v[1] = testRegistry; v[2] = ethRegistry; v[3] = rootRegistry; - _expectFindResolver(name, 9, address(testRegistry), v); // 3sub4test + _expectFind(name, 9, address(testRegistry), v); // 3sub4test } function test_findResolver_virtual() external { @@ -158,6 +168,6 @@ contract LibRegistryTest is Test, ERC1155Holder { v[2] = testRegistry; v[3] = ethRegistry; v[4] = rootRegistry; - _expectFindResolver(name, 10, address(0), v); // 1a2bb4test + _expectFind(name, 10, address(0), v); // 1a2bb4test } } From 3b4538ff2ac37e463abe5ed4ab96264a859c5710 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 20 Oct 2025 20:13:20 -0700 Subject: [PATCH 09/10] bump ens-contracts and urg, moved interfaces, fixed TooManyProofs tests, use ens-contracts interfaces tool --- contracts/foundry.lock | 4 +- contracts/lib/ens-contracts | 2 +- contracts/lib/unruggable-gateways | 2 +- contracts/package.json | 2 +- contracts/script/interfaces.ts | 67 ----- contracts/src/L1/resolver/ETHTLDResolver.sol | 39 ++- .../interfaces/ICompositeExtendedResolver.sol | 24 -- .../interfaces/IUnruggableResolver.sol | 18 -- .../integration/l1/ETHTLDResolver.test.ts | 282 ++++++++++-------- 9 files changed, 177 insertions(+), 263 deletions(-) delete mode 100644 contracts/script/interfaces.ts delete mode 100644 contracts/src/common/resolver/interfaces/ICompositeExtendedResolver.sol delete mode 100644 contracts/src/common/resolver/interfaces/IUnruggableResolver.sol diff --git a/contracts/foundry.lock b/contracts/foundry.lock index c0e45cdd..e65acf76 100644 --- a/contracts/foundry.lock +++ b/contracts/foundry.lock @@ -3,7 +3,7 @@ "rev": "82cc81935de1d1a82e021cf1030d902c5248982b" }, "lib/ens-contracts": { - "rev": "a392133a641f7a88dd8d08616e1113e1b6d0416a" + "rev": "a54e5cf2857ab597011d115b9b1183c7a83bd307" }, "lib/forge-std": { "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505" @@ -21,7 +21,7 @@ "rev": "c4fbe97cf5e8c1b8d607001588fd23abb5bfb923" }, "lib/unruggable-gateways": { - "rev": "4c246c98d322fcf12654138e63fcd4bea3f31f7b" + "rev": "84d0a07772618a293abf3f778f4caabd51ca8ff5" }, "lib/verifiable-factory": { "rev": "c47c0e61ce03b3ab5891a3b743287b54aee9f021" diff --git a/contracts/lib/ens-contracts b/contracts/lib/ens-contracts index a392133a..a54e5cf2 160000 --- a/contracts/lib/ens-contracts +++ b/contracts/lib/ens-contracts @@ -1 +1 @@ -Subproject commit a392133a641f7a88dd8d08616e1113e1b6d0416a +Subproject commit a54e5cf2857ab597011d115b9b1183c7a83bd307 diff --git a/contracts/lib/unruggable-gateways b/contracts/lib/unruggable-gateways index 4c246c98..84d0a077 160000 --- a/contracts/lib/unruggable-gateways +++ b/contracts/lib/unruggable-gateways @@ -1 +1 @@ -Subproject commit 4c246c98d322fcf12654138e63fcd4bea3f31f7b +Subproject commit 84d0a07772618a293abf3f778f4caabd51ca8ff5 diff --git a/contracts/package.json b/contracts/package.json index ca917c3c..1f477f5c 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -48,7 +48,7 @@ "test:hardhat": "bun run compile:hardhat && vitest run", "test:e2e": "bun run compile:hardhat --quiet && bun test ./test/e2e/", "test": "bun run test:forge && bun run test:hardhat", - "interfaces": "bun run compile:hardhat --quiet && bun ./script/interfaces.ts", + "interfaces": "bun run compile:hardhat --quiet && bun ./lib/ens-contracts/scripts/interfaces.ts", "coverage:forge": "mkdir -p coverage/ && forge coverage --report lcov --report-file coverage/forge.lcov", "coverage:hardhat": "hardhat compile --coverage && COVERAGE=1 vitest run", "coverage:reports": "bun ./script/coverage.ts", diff --git a/contracts/script/interfaces.ts b/contracts/script/interfaces.ts deleted file mode 100644 index e1c5a374..00000000 --- a/contracts/script/interfaces.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type Abi, isHex, toFunctionSelector, toHex } from 'viem' -import artifacts from '../generated/artifacts.js' - -// $ bun interfaces # all -// $ bun interfaces Ens # by name (ignores case) -// $ bun interfaces 0x9061b923 # by selector -// $ bun interfaces Ens 0x9061b923 # mixture of names/selectors -// $ bun interfaces ... --json # export as JSON - -const ifaces = Object.values(artifacts) - .filter((x) => x.bytecode === '0x') - .map((x) => ({ - interfaceId: getInterfaceId(x.abi), - name: x.contractName, - file: x.sourceName, - })) - .sort((a, b) => a.file.localeCompare(b.file)) - -const UNKNOWN = '???' - -let output: (x: any) => void = console.table -const qs = process.argv.slice(2).filter((x) => { - if (x === '--json') { - output = (x) => { - console.log() - console.log(JSON.stringify(x, null, ' ')) - } - } else { - return true - } -}) -if (qs.length) { - output( - qs.map((q) => { - if (isHex(q) && q.length === 10) { - return ( - ifaces.find((x) => same(x.interfaceId, q)) ?? { - interfaceId: q, - name: UNKNOWN, - } - ) - } else { - return ( - ifaces.find((x) => same(x.name, q)) ?? { - interfaceId: UNKNOWN, - name: q, - } - ) - } - }), - ) -} else { - output(ifaces) -} - -function same(a: string, b: string) { - return !a.localeCompare(b, undefined, { sensitivity: 'base' }) -} - -function getInterfaceId(abi: Abi) { - return toHex( - abi - .filter((item) => item.type === 'function') - .reduce((a, x) => a ^ BigInt(toFunctionSelector(x)), 0n), - { size: 4 }, - ) -} diff --git a/contracts/src/L1/resolver/ETHTLDResolver.sol b/contracts/src/L1/resolver/ETHTLDResolver.sol index dc82558e..9d597c1a 100755 --- a/contracts/src/L1/resolver/ETHTLDResolver.sol +++ b/contracts/src/L1/resolver/ETHTLDResolver.sol @@ -8,6 +8,9 @@ import {IMulticallable} from "@ens/contracts/resolvers/IMulticallable.sol"; import {IABIResolver} from "@ens/contracts/resolvers/profiles/IABIResolver.sol"; import {IAddressResolver} from "@ens/contracts/resolvers/profiles/IAddressResolver.sol"; import {IAddrResolver} from "@ens/contracts/resolvers/profiles/IAddrResolver.sol"; +import { + ICompositeExtendedResolver +} from "@ens/contracts/resolvers/profiles/ICompositeExtendedResolver.sol"; import {IContentHashResolver} from "@ens/contracts/resolvers/profiles/IContentHashResolver.sol"; import {IExtendedResolver} from "@ens/contracts/resolvers/profiles/IExtendedResolver.sol"; import {IHasAddressResolver} from "@ens/contracts/resolvers/profiles/IHasAddressResolver.sol"; @@ -15,6 +18,7 @@ import {IInterfaceResolver} from "@ens/contracts/resolvers/profiles/IInterfaceRe import {INameResolver} from "@ens/contracts/resolvers/profiles/INameResolver.sol"; import {IPubkeyResolver} from "@ens/contracts/resolvers/profiles/IPubkeyResolver.sol"; import {ITextResolver} from "@ens/contracts/resolvers/profiles/ITextResolver.sol"; +import {IVerifiableResolver} from "@ens/contracts/resolvers/profiles/IVerifiableResolver.sol"; import {ResolverFeatures} from "@ens/contracts/resolvers/ResolverFeatures.sol"; import {RegistryUtils} from "@ens/contracts/universalResolver/RegistryUtils.sol"; import {ResolverCaller} from "@ens/contracts/universalResolver/ResolverCaller.sol"; @@ -36,10 +40,6 @@ import {BridgeRolesLib} from "../../common/bridge/libraries/BridgeRolesLib.sol"; import {IPermissionedRegistry} from "../../common/registry/interfaces/IPermissionedRegistry.sol"; import {IRegistry} from "../../common/registry/interfaces/IRegistry.sol"; import {IRegistryDatastore} from "../../common/registry/interfaces/IRegistryDatastore.sol"; -import { - ICompositeExtendedResolver -} from "../../common/resolver/interfaces/ICompositeExtendedResolver.sol"; -import {IUnruggableResolver} from "../../common/resolver/interfaces/IUnruggableResolver.sol"; import { DedicatedResolverLayoutLib } from "../../common/resolver/libraries/DedicatedResolverLayoutLib.sol"; @@ -56,7 +56,7 @@ import {L1BridgeController} from "../bridge/L1BridgeController.sol"; /// 3. If no resolver is found, reverts `UnreachableName`. contract ETHTLDResolver is ICompositeExtendedResolver, - IUnruggableResolver, + IVerifiableResolver, IERC7996, GatewayFetchTarget, ResolverCaller, @@ -70,10 +70,9 @@ contract ETHTLDResolver is //////////////////////////////////////////////////////////////////////// enum NameState { - NULL, + NAMECHAIN, POST_MIGRATION, - POST_EJECTION, - NAMECHAIN + POST_EJECTION } //////////////////////////////////////////////////////////////////////// @@ -150,7 +149,7 @@ contract ETHTLDResolver is return type(IExtendedResolver).interfaceId == interfaceId || type(ICompositeExtendedResolver).interfaceId == interfaceId || - type(IUnruggableResolver).interfaceId == interfaceId || + type(IVerifiableResolver).interfaceId == interfaceId || type(IERC7996).interfaceId == interfaceId || super.supportsInterface(interfaceId); } @@ -198,19 +197,19 @@ contract ETHTLDResolver is } } - /// @inheritdoc IUnruggableResolver - function unruggableGateway() - external - view - returns (uint256 coinType, IGatewayVerifier verifier, string[] memory gateways) - { - coinType = 0x80eeeeee; // TODO: namechain coinType - verifier = namechainVerifier; - // gateways = namechainVerifier.gatewayURLs(); + /// @inheritdoc IVerifiableResolver + function verifierMetadata( + bytes calldata name + ) external view returns (address verifier, string[] memory gateways) { + (, bool offchain) = _determineMainnetResolver(name); + if (offchain) { + verifier = address(namechainVerifier); + gateways = namechainVerifier.gatewayURLs(); + } } /// @inheritdoc ICompositeExtendedResolver - function isResolverOffchain(bytes calldata name) external view returns (bool offchain) { + function requiresOffchain(bytes calldata name) external view returns (bool offchain) { (, offchain) = _determineMainnetResolver(name); } @@ -220,7 +219,7 @@ contract ETHTLDResolver is /// * `getResolver()` reverts /// * `getResolver() = (, false)` /// * `getResolver() = (, true)` - /// * `getResolver(*.eth) = (address(0), true)` + /// * `getResolver(=0.8.13; - -import {IExtendedResolver} from "@ens/contracts/resolvers/profiles/IExtendedResolver.sol"; - -/// @notice A resolver that is composed of multiple resolvers. -/// @dev Interface selector: `0xf686ea10` -interface ICompositeExtendedResolver is IExtendedResolver { - /// @notice Fetch the underlying resolver for `name`. - /// Callers should enable EIP-3668. - /// - /// @param name The DNS-encoded name. - /// - /// @return resolver The underlying resolver address. - /// @return offchain `true` if `resolver` is offchain. - function getResolver(bytes memory name) external view returns (address resolver, bool offchain); - - /// @notice Determine if resolving `name` requires offchain data. - /// - /// @param name The DNS-encoded name. - /// - /// @return `true` if requires offchain data. - function isResolverOffchain(bytes calldata name) external view returns (bool); -} diff --git a/contracts/src/common/resolver/interfaces/IUnruggableResolver.sol b/contracts/src/common/resolver/interfaces/IUnruggableResolver.sol deleted file mode 100644 index db7d4590..00000000 --- a/contracts/src/common/resolver/interfaces/IUnruggableResolver.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.13; - -import {IGatewayVerifier} from "@unruggable/gateways/contracts/GatewayFetchTarget.sol"; - -/// @notice A resolver that uses an Unruggable gateway. -/// @dev Interface selector: `0xe4b2bbef` -interface IUnruggableResolver { - /// @notice Expose Unruggable gateway parameters. - /// - /// @return coinType The source rollup coin type. - /// @return verifier The Unruggable Verifier contract. - /// @return gatewayURLs The gateways used by `verifier`. - function unruggableGateway() - external - view - returns (uint256 coinType, IGatewayVerifier verifier, string[] memory gatewayURLs); -} diff --git a/contracts/test/integration/l1/ETHTLDResolver.test.ts b/contracts/test/integration/l1/ETHTLDResolver.test.ts index 5b341558..cfd1a71f 100755 --- a/contracts/test/integration/l1/ETHTLDResolver.test.ts +++ b/contracts/test/integration/l1/ETHTLDResolver.test.ts @@ -85,9 +85,10 @@ async function fixture() { const hooksAddress = await deployArtifact(mainnetV2.walletClient, { file: urgArtifact("UncheckedVerifierHooks"), }); + const verifierGateways = [ccip.endpoint]; const verifierAddress = await deployArtifact(mainnetV2.walletClient, { file: urgArtifact("UncheckedVerifier"), - args: [[ccip.endpoint], 0, hooksAddress], + args: [verifierGateways, 0, hooksAddress], libs: { GatewayVM }, }); const ethResolver = await mainnetV2.deployDedicatedResolver(); @@ -121,6 +122,7 @@ async function fixture() { namechain, gateway, verifierAddress, + verifierGateways, } as const; } @@ -131,7 +133,7 @@ const loadFixture = async () => { const dummySelector = "0x12345678"; const testAddress = "0x8000000000000000000000000000000000000001"; -const testNames = ["test.eth", "a.b.c.test.eth"] as const; +const ethNames = ["test.eth", "a.b.c.test.eth"] as const; describe("ETHTLDResolver", () => { const rpcs: Record = {}; @@ -155,7 +157,7 @@ describe("ETHTLDResolver", () => { "IERC7996", "IExtendedResolver", "ICompositeExtendedResolver", - "IUnruggableResolver", + "IVerifiableResolver", ], }); @@ -212,13 +214,13 @@ describe("ETHTLDResolver", () => { const address = await F.ethTLDResolver.read.namechainVerifier(); expectVar({ address }).toEqualAddress(testAddress); }); - it("unruggableGateway", async () => { + it("verifierMetadata", async () => { const F = await loadFixture(); - const [coinType, verifier, gatewayURLs] = - await F.ethTLDResolver.read.unruggableGateway(); - expectVar({ coinType }).toStrictEqual(0x80eeeeeen); + const [verifier, gateways] = await F.ethTLDResolver.read.verifierMetadata( + [dnsEncodeName("any.eth")], + ); expectVar({ verifier }).toStrictEqual(F.verifierAddress); - expectVar({ gatewayURLs }).toStrictEqual([]); // using verifier defaults + expectVar({ gateways }).toStrictEqual(F.verifierGateways); }); }); @@ -260,8 +262,8 @@ describe("ETHTLDResolver", () => { const resolverAddress = await F.ethTLDResolver.read.ethResolver(); const dns = dnsEncodeName("eth"); await expect( - F.ethTLDResolver.read.isResolverOffchain([dns]), - "isResolverOffchain", + F.ethTLDResolver.read.requiresOffchain([dns]), + "requiresOffchain", ).resolves.toStrictEqual(false); await expect( F.ethTLDResolver.read.getResolver([dns]), @@ -271,78 +273,87 @@ describe("ETHTLDResolver", () => { it("not .eth", async () => { const F = await loadFixture(); const dns = dnsEncodeName("com"); - await expect(F.ethTLDResolver.read.isResolverOffchain([dns])) + await expect(F.ethTLDResolver.read.requiresOffchain([dns])) .toBeRevertedWithCustomError("UnreachableName") .withArgs([dns]); await expect(F.ethTLDResolver.read.getResolver([dns])) .toBeRevertedWithCustomError("UnreachableName") .withArgs([dns]); }); - it("dne", async () => { - const F = await loadFixture(); - const name = testNames[0]; - const dns = dnsEncodeName(name); - await sync(); - await expect( - F.ethTLDResolver.read.isActiveRegistrationV1([ - labelhash(getLabelAt(name)), - ]), - "isActiveRegistrationV1", - ).resolves.toStrictEqual(false); - await expect( - F.ethTLDResolver.read.isResolverOffchain([dns]), - "isResolverOffchain", - ).resolves.toStrictEqual(true); - await expect( - F.ethTLDResolver.read.getResolver([dns]), - "getResolver", - ).resolves.toStrictEqual([zeroAddress, false]); + describe("unregistered", () => { + for (const name of ethNames) { + it(name, async () => { + const F = await loadFixture(); + const dns = dnsEncodeName(name); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([ + labelhash(getLabelAt(name, -2)), + ]), + "isActiveRegistrationV1", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.requiresOffchain([dns]), + "requiresOffchain", + ).resolves.toStrictEqual(true); + await sync(); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([zeroAddress, true]); + }); + } }); - it("Mainnet V1", async () => { - const F = await loadFixture(); - const name = testNames[0]; - const dns = dnsEncodeName(name); - const { resolverAddress } = await F.mainnetV1.setupName({ name }); - await expect( - F.ethTLDResolver.read.isActiveRegistrationV1([ - labelhash(getLabelAt(name)), - ]), - "isActiveRegistrationV1", - ).resolves.toStrictEqual(true); - await expect( - F.ethTLDResolver.read.isResolverOffchain([dns]), - "isResolverOffchain", - ).resolves.toStrictEqual(false); - await expect( - F.ethTLDResolver.read.getResolver([dns]), - "getResolver", - ).resolves.toStrictEqual([resolverAddress, false]); + describe("Mainnet V1", () => { + for (const name of ethNames) { + it(name, async () => { + const F = await loadFixture(); + const dns = dnsEncodeName(name); + const { resolverAddress } = await F.mainnetV1.setupName({ name }); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([ + labelhash(getLabelAt(name, -2)), + ]), + "isActiveRegistrationV1", + ).resolves.toStrictEqual(true); + await expect( + F.ethTLDResolver.read.requiresOffchain([dns]), + "requiresOffchain", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([resolverAddress, false]); + }); + } }); - it("Namechain", async () => { - const F = await loadFixture(); - const name = testNames[0]; - const dns = dnsEncodeName(name); - const { resolverAddress } = await F.namechain.setupName({ name }); - await sync(); - await expect( - F.ethTLDResolver.read.isActiveRegistrationV1([ - labelhash(getLabelAt(name)), - ]), - "isActiveRegistrationV1", - ).resolves.toStrictEqual(false); - await expect( - F.ethTLDResolver.read.isResolverOffchain([dns]), - "isResolverOffchain", - ).resolves.toStrictEqual(true); - await expect( - F.ethTLDResolver.read.getResolver([dns]), - "getResolver", - ).resolves.toStrictEqual([resolverAddress, true]); + describe("Namechain", () => { + for (const name of ethNames) { + it(name, async () => { + const F = await loadFixture(); + const dns = dnsEncodeName(name); + const { resolverAddress } = await F.namechain.setupName({ name }); + await sync(); + await expect( + F.ethTLDResolver.read.isActiveRegistrationV1([ + labelhash(getLabelAt(name)), + ]), + "isActiveRegistrationV1", + ).resolves.toStrictEqual(false); + await expect( + F.ethTLDResolver.read.requiresOffchain([dns]), + "requiresOffchain", + ).resolves.toStrictEqual(true); + await expect( + F.ethTLDResolver.read.getResolver([dns]), + "getResolver", + ).resolves.toStrictEqual([resolverAddress, true]); + }); + } }); }); describe("unregistered", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const [res] = makeResolutions({ @@ -378,7 +389,7 @@ describe("ETHTLDResolver", () => { }); describe("on V1", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -401,7 +412,7 @@ describe("ETHTLDResolver", () => { }); describe("migrating to L1", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -441,7 +452,7 @@ describe("ETHTLDResolver", () => { }); describe("migrating to L2", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -484,7 +495,7 @@ describe("ETHTLDResolver", () => { }); describe("on L2", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -518,7 +529,7 @@ describe("ETHTLDResolver", () => { }); describe("ejecting L1 -> L2", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -552,7 +563,7 @@ describe("ETHTLDResolver", () => { }); describe("on L1", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -575,7 +586,7 @@ describe("ETHTLDResolver", () => { }); describe("expired on L1", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -620,7 +631,7 @@ describe("ETHTLDResolver", () => { }); describe("expired on L2", () => { - for (const name of testNames) { + for (const name of ethNames) { it(name, async () => { const F = await loadFixture(); const kp: KnownProfile = { @@ -664,8 +675,8 @@ describe("ETHTLDResolver", () => { describe("profile support", () => { const kp: KnownProfile = { - name: testNames[0], - primary: { value: testNames[0] }, + name: ethNames[0], + primary: { value: ethNames[0] }, addresses: [ { coinType: COIN_TYPE_ETH, value: testAddress }, { coinType: COIN_TYPE_DEFAULT, value: testAddress }, @@ -717,7 +728,7 @@ describe("ETHTLDResolver", () => { it("hasAddr()", async () => { const F = await loadFixture(); const kp: KnownProfile = { - name: testNames[0], + name: ethNames[0], hasAddresses: [ { coinType: COIN_TYPE_ETH, exists: false }, { coinType: COIN_TYPE_DEFAULT, exists: true }, @@ -740,7 +751,7 @@ describe("ETHTLDResolver", () => { it("addr() w/fallback", async () => { const F = await loadFixture(); const kp: KnownProfile = { - name: testNames[0], + name: ethNames[0], addresses: [ { coinType: COIN_TYPE_ETH, value: testAddress }, { coinType: COIN_TYPE_DEFAULT, value: testAddress }, @@ -760,7 +771,7 @@ describe("ETHTLDResolver", () => { }); describe("ABI()", () => { const kp: KnownProfile = { - name: testNames[0], + name: ethNames[0], abis: [ { contentType: 0n, value: "0x" }, { contentType: 1n << 0n, value: "0x11" }, @@ -821,44 +832,48 @@ describe("ETHTLDResolver", () => { }); } }); - it("too many calls", async () => { - const F = await loadFixture(); - const max = 10; - const kp: KnownProfile = { - name: testNames[0], - addresses: [{ coinType: COIN_TYPE_ETH, value: testAddress }], // 1 proof - }; - try { - F.gateway.rollup.configure = (c) => { - c.prover.maxUniqueProofs = 1 + max; - }; - const [call] = makeResolutions(kp); - const { dedicatedResolver } = await F.namechain.setupName(kp); - await F.namechain.walletClient.sendTransaction({ - to: dedicatedResolver.address, - data: call.writeDedicated, + describe("too many calls", () => { + for (const name of ethNames) { + it(name, async () => { + const F = await loadFixture(); + const max = 10; + const kp: KnownProfile = { + name, + addresses: [{ coinType: COIN_TYPE_ETH, value: testAddress }], // 1 proof + }; + try { + F.gateway.rollup.configure = (c) => { + c.prover.maxUniqueProofs = max + splitName(name).length * 2; // see: limits + }; + const [call] = makeResolutions(kp); + const { dedicatedResolver } = await F.namechain.setupName(kp); + await F.namechain.walletClient.sendTransaction({ + to: dedicatedResolver.address, + data: call.writeDedicated, + }); + await sync(); + const calls = Array.from({ length: max }, () => call); + const bundle = bundleCalls(calls); + const answer = await F.ethTLDResolver.read.resolve([ + dnsEncodeName(kp.name), + bundle.call, + ]); + bundle.expect(answer); + await expect( + F.ethTLDResolver.read.resolve([ + dnsEncodeName(kp.name), + bundleCalls([...calls, call]).call, // one additional proof + ]), + ).toBeRevertedWithCustomError("TooManyProofs"); + } finally { + F.gateway.rollup.configure = undefined; + } }); - await sync(); - const calls = Array.from({ length: max }, () => call); - const bundle = bundleCalls(calls); - const answer = await F.ethTLDResolver.read.resolve([ - dnsEncodeName(kp.name), - bundle.call, - ]); - bundle.expect(answer); - // TODO: UncheckedRollup doesn't respect maxUniqueProofs - // TODO: fix after Urg adds callback error propagation - // await expect(F.ethTLDResolver.read.resolve([ - // dnsEncodeName(kp.name), - // bundleCalls([...calls, call]).call, - // ])).toBeReverted(); - } finally { - F.gateway.rollup.configure = undefined; } }); it("every multicall failed", async () => { const kp: KnownProfile = { - name: testNames[0], + name: ethNames[0], errors: Array.from({ length: 2 }, (_, i) => { const call = toHex(i, { size: 4 }); return { @@ -888,7 +903,7 @@ describe("ETHTLDResolver", () => { // bytes-like: text(*), addr(*), contenthash() // complex: ABI() - for (const name of testNames) { + for (const name of ethNames) { // see: RegistryDatastore.test.ts // to target exact resolver of "x.y.eth" // 1. AccountProof(registry) @@ -906,11 +921,8 @@ describe("ETHTLDResolver", () => { const { dedicatedResolver } = await F.namechain.setupName({ name }); F.gateway.rollup.configure = (c) => { c.prover.maxUniqueProofs = maxProofs; - c.prover.maxProvableBytes = maxProofs << 5; }; - await run(0); - await expect(run(1)).rejects.toThrow(); - async function run(extra: number) { + for (let extra of [0, 1]) { const kp: KnownProfile = { name, contenthash: { @@ -925,9 +937,17 @@ describe("ETHTLDResolver", () => { data: res.writeDedicated, }); await sync(); - return F.mainnetV2.universalResolver.read - .resolve([dnsEncodeName(kp.name), res.call]) - .then(([answer]) => res.expect(answer)); + const promise = F.ethTLDResolver.read.resolve([ + dnsEncodeName(kp.name), + res.call, + ]); + if (extra) { + await expect(promise).toBeRevertedWithCustomError( + "TooManyProofs", + ); + } else { + res.expect(await promise); + } } }); @@ -936,14 +956,11 @@ describe("ETHTLDResolver", () => { await F.namechain.setupName({ name }); F.gateway.rollup.configure = (c) => { c.prover.maxUniqueProofs = maxProofs; - c.prover.maxProvableBytes = maxProofs << 5; c.prover.maxStackSize = Infinity; }; - await run(0); - await expect(run(1)).rejects.toThrow(); - async function run(extra: number) { + for (let extra of [0, 1]) { await sync(); - return F.mainnetV2.universalResolver.read.resolve([ + const promise = F.ethTLDResolver.read.resolve([ dnsEncodeName(name), encodeFunctionData({ abi: PROFILE_ABI, @@ -954,6 +971,13 @@ describe("ETHTLDResolver", () => { ], }), ]); + if (extra) { + await expect(promise).toBeRevertedWithCustomError( + "TooManyProofs", + ); + } else { + await promise; + } } }); }); From 11bb2c0ef1bb9c39537dade9090c4d30e0b641c2 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Mon, 20 Oct 2025 20:26:22 -0700 Subject: [PATCH 10/10] few more interfaces --- contracts/hardhat.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 0ba3bf67..ca6bf90c 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -36,6 +36,8 @@ const config = { "./lib/verifiable-factory/src/", "./lib/ens-contracts/contracts/", "./lib/openzeppelin-contracts/contracts/utils/introspection/", + "./lib/openzeppelin-contracts/contracts/token/ERC721", + "./lib/openzeppelin-contracts/contracts/token/ERC1155/", ], }, },