diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 5e342328..3f76327d 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -10,6 +10,12 @@ evm_version = "cancun" optimizer = true optimizer_runs = 200 +ignored_warnings_from = [ + "lib/ens-contracts/contracts/registry/ENSRegistry.sol", + "lib/ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol", + "lib/ens-contracts/contracts/wrapper/NameWrapper.sol", +] + [fuzz] runs = 4096 diff --git a/contracts/src/migration/LockedMigrationController.sol b/contracts/src/migration/LockedMigrationController.sol index 9280cd1a..7129803f 100644 --- a/contracts/src/migration/LockedMigrationController.sol +++ b/contracts/src/migration/LockedMigrationController.sol @@ -2,160 +2,50 @@ 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 {INameWrapper} from "@ens/contracts/wrapper/INameWrapper.sol"; import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; -import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; -import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {UnauthorizedCaller} from "../CommonErrors.sol"; import {IPermissionedRegistry} from "../registry/interfaces/IPermissionedRegistry.sol"; import {IRegistry} from "../registry/interfaces/IRegistry.sol"; -import {LockedNamesLib} from "./libraries/LockedNamesLib.sol"; -import {MigrationData} from "./types/MigrationTypes.sol"; +import {WrapperReceiver} from "./WrapperReceiver.sol"; -contract LockedMigrationController is IERC1155Receiver, ERC165 { +contract LockedMigrationController is WrapperReceiver { //////////////////////////////////////////////////////////////////////// // Constants //////////////////////////////////////////////////////////////////////// - INameWrapper public immutable NAME_WRAPPER; - IPermissionedRegistry public immutable ETH_REGISTRY; - VerifiableFactory public immutable FACTORY; - - address public immutable MIGRATED_REGISTRY_IMPLEMENTATION; - - //////////////////////////////////////////////////////////////////////// - // Errors - //////////////////////////////////////////////////////////////////////// - - error TokenIdMismatch(uint256 tokenId, uint256 expectedTokenId); - //////////////////////////////////////////////////////////////////////// // Initialization //////////////////////////////////////////////////////////////////////// constructor( - INameWrapper nameWrapper, IPermissionedRegistry ethRegistry, - VerifiableFactory factory, - address migratedRegistryImplementation - ) { - NAME_WRAPPER = nameWrapper; + INameWrapper nameWrapper, + VerifiableFactory verifiableFactory, + address migratedRegistryImpl + ) WrapperReceiver(nameWrapper, verifiableFactory, migratedRegistryImpl) { ETH_REGISTRY = ethRegistry; - FACTORY = factory; - MIGRATED_REGISTRY_IMPLEMENTATION = migratedRegistryImplementation; - } - - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC165, IERC165) returns (bool) { - return - interfaceId == type(IERC1155Receiver).interfaceId || - super.supportsInterface(interfaceId); } //////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////// - function onERC1155Received( - address /*operator*/, - address /*from*/, - uint256 tokenId, - uint256 /*amount*/, - bytes calldata data - ) external virtual returns (bytes4) { - if (msg.sender != address(NAME_WRAPPER)) { - revert UnauthorizedCaller(msg.sender); - } - - (MigrationData memory migrationData) = abi.decode(data, (MigrationData)); - MigrationData[] memory migrationDataArray = new MigrationData[](1); - migrationDataArray[0] = migrationData; - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = tokenId; - - _migrateLockedEthNames(tokenIds, migrationDataArray); - - return this.onERC1155Received.selector; + function _inject( + string memory label, + address owner, + IRegistry subregistry, + address resolver, + uint256 roleBitmap, + uint64 expiry + ) internal override returns (uint256 tokenId) { + return ETH_REGISTRY.register(label, owner, subregistry, resolver, roleBitmap, expiry); } - function onERC1155BatchReceived( - address /*operator*/, - address /*from*/, - uint256[] calldata tokenIds, - uint256[] calldata /*amounts*/, - bytes calldata data - ) external virtual returns (bytes4) { - if (msg.sender != address(NAME_WRAPPER)) { - revert UnauthorizedCaller(msg.sender); - } - - (MigrationData[] memory migrationDataArray) = abi.decode(data, (MigrationData[])); - - _migrateLockedEthNames(tokenIds, migrationDataArray); - - return this.onERC1155BatchReceived.selector; - } - - function _migrateLockedEthNames( - uint256[] memory tokenIds, - MigrationData[] memory migrationDataArray - ) internal { - for (uint256 i = 0; i < tokenIds.length; i++) { - (, uint32 fuses, ) = NAME_WRAPPER.getData(tokenIds[i]); - - // Validate fuses and name type - LockedNamesLib.validateLockedName(fuses, tokenIds[i]); - LockedNamesLib.validateIsDotEth2LD(fuses, tokenIds[i]); - - // Determine permissions from name configuration (mask out CAN_EXTEND_EXPIRY to prevent automatic renewal for 2LDs) - uint32 adjustedFuses = fuses & ~CAN_EXTEND_EXPIRY; - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(adjustedFuses); - - // Create new registry instance for the migrated name - address subregistry = LockedNamesLib.deployMigratedRegistry( - FACTORY, - MIGRATED_REGISTRY_IMPLEMENTATION, - migrationDataArray[i].transferData.owner, - subRegistryRoles, - migrationDataArray[i].salt, - migrationDataArray[i].transferData.dnsEncodedName - ); - - // Configure transfer data with registry and permission details - migrationDataArray[i].transferData.subregistry = subregistry; - migrationDataArray[i].transferData.roleBitmap = tokenRoles; - - // Ensure name data consistency for migration - (bytes32 labelHash, ) = NameCoder.readLabel( - migrationDataArray[i].transferData.dnsEncodedName, - 0 - ); - if (tokenIds[i] != uint256(labelHash)) { - revert TokenIdMismatch(tokenIds[i], uint256(labelHash)); - } - - // Register the name in the ETH registry - string memory label = NameCoder.firstLabel( - migrationDataArray[i].transferData.dnsEncodedName - ); - ETH_REGISTRY.register( - label, - migrationDataArray[i].transferData.owner, - IRegistry(migrationDataArray[i].transferData.subregistry), - migrationDataArray[i].transferData.resolver, - migrationDataArray[i].transferData.roleBitmap, - migrationDataArray[i].transferData.expires - ); - - // Finalize migration by freezing the name - LockedNamesLib.freezeName(NAME_WRAPPER, tokenIds[i], fuses); - } + function _parentNode() internal pure override returns (bytes32) { + return NameCoder.ETH_NODE; } } diff --git a/contracts/src/migration/MigrationErrors.sol b/contracts/src/migration/MigrationErrors.sol index 2ed77e30..e07b3bf7 100644 --- a/contracts/src/migration/MigrationErrors.sol +++ b/contracts/src/migration/MigrationErrors.sol @@ -1,20 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; -/** - * @title MigrationErrors - * @dev Error definitions specific to migration operations - */ +/// @dev Errors for migration process. +library MigrationErrors { + //////////////////////////////////////////////////////////////////////// + // Errors + //////////////////////////////////////////////////////////////////////// -/** - * @dev Thrown when attempting to migrate a subdomain whose parent has not been migrated - * @param name The DNS-encoded name being migrated - * @param offset The byte offset where the parent domain starts in the name - */ -error ParentNotMigrated(bytes name, uint256 offset); + error NameNotMigrated(bytes name); + //error NameNotSubdomain(bytes name, bytes parentName); -/** - * @dev Thrown when attempting to register a label that has an emancipated NFT in the old system but hasn't been migrated - * @param label The label that needs to be migrated first - */ -error LabelNotMigrated(string label); + //error NameIsLocked(bytes name); + error NameNotLocked(uint256 tokenId); + //error NameNotETH2LD(bytes name); + //error NameNotEmancipated(uint256 tokenId); + + error InvalidWrapperRegistryData(); + + error NameDataMismatch(uint256 tokenId); +} diff --git a/contracts/src/migration/WrapperReceiver.sol b/contracts/src/migration/WrapperReceiver.sol new file mode 100644 index 00000000..5b7fc487 --- /dev/null +++ b/contracts/src/migration/WrapperReceiver.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; +import { + INameWrapper, + IS_DOT_ETH, + CAN_EXTEND_EXPIRY, + CANNOT_UNWRAP, + CANNOT_BURN_FUSES, + CANNOT_TRANSFER, + CANNOT_SET_RESOLVER, + CANNOT_SET_TTL, + CANNOT_CREATE_SUBDOMAIN +} from "@ens/contracts/wrapper/INameWrapper.sol"; +import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; +import {IERC1155Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +import {UnauthorizedCaller} from "../CommonErrors.sol"; +import {IRegistry} from "../registry/interfaces/IRegistry.sol"; +import {IWrapperRegistry, MIN_DATA_SIZE} from "../registry/interfaces/IWrapperRegistry.sol"; +import {RegistryRolesLib} from "../registry/libraries/RegistryRolesLib.sol"; +import {WrappedErrorLib} from "../utils/WrappedErrorLib.sol"; + +import {MigrationErrors} from "./MigrationErrors.sol"; + +uint32 constant FUSES_TO_BURN = CANNOT_BURN_FUSES | + CANNOT_TRANSFER | + CANNOT_SET_RESOLVER | + CANNOT_SET_TTL | + CANNOT_CREATE_SUBDOMAIN; + +abstract contract WrapperReceiver is ERC165, IERC1155Receiver { + //////////////////////////////////////////////////////////////////////// + // Constants + //////////////////////////////////////////////////////////////////////// + + INameWrapper public immutable NAME_WRAPPER; + VerifiableFactory public immutable VERIFIABLE_FACTORY; + address public immutable MIGRATED_REGISTRY_IMPL; + + //////////////////////////////////////////////////////////////////////// + // Modifiers + //////////////////////////////////////////////////////////////////////// + + /// @dev Restrict `msg.sender` to `NAME_WRAPPER`. + /// Avoid `abi.decode()` failure for obviously invalid data. + /// Reverts wrapped errors for use inside of legacy IERC1155Receiver handler. + modifier onlyWrapperDuringTransfer(bytes calldata data, uint256 minimumSize) { + if (msg.sender != address(NAME_WRAPPER)) { + WrappedErrorLib.wrapAndRevert( + abi.encodeWithSelector(UnauthorizedCaller.selector, msg.sender) + ); + } + if (data.length < minimumSize) { + WrappedErrorLib.wrapAndRevert( + abi.encodeWithSelector(MigrationErrors.InvalidWrapperRegistryData.selector) + ); + } + _; + } + + //////////////////////////////////////////////////////////////////////// + // Initialization + //////////////////////////////////////////////////////////////////////// + + constructor( + INameWrapper nameWrapper, + VerifiableFactory verifiableFactory, + address migratedRegistryImpl + ) { + NAME_WRAPPER = nameWrapper; + VERIFIABLE_FACTORY = verifiableFactory; + MIGRATED_REGISTRY_IMPL = migratedRegistryImpl; + } + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165, IERC165) returns (bool) { + return + interfaceId == type(IERC1155Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + //////////////////////////////////////////////////////////////////////// + // Implementation + //////////////////////////////////////////////////////////////////////// + + function onERC1155Received( + address /*operator*/, + address /*from*/, + uint256 id, + uint256 /*amount*/, + bytes calldata data + ) external onlyWrapperDuringTransfer(data, MIN_DATA_SIZE) returns (bytes4) { + uint256[] memory ids = new uint256[](1); + IWrapperRegistry.Data[] memory mds = new IWrapperRegistry.Data[](1); + ids[0] = id; + mds[0] = abi.decode(data, (IWrapperRegistry.Data)); // reverts if invalid + try this.finishERC1155Migration(ids, mds) { + return this.onERC1155Received.selector; + } catch (bytes memory reason) { + WrappedErrorLib.wrapAndRevert(reason); // convert all errors to wrapped + } + } + + function onERC1155BatchReceived( + address /*operator*/, + address /*from*/, + uint256[] calldata ids, + uint256[] calldata /*amounts*/, + bytes calldata data + ) external onlyWrapperDuringTransfer(data, 64 + ids.length * MIN_DATA_SIZE) returns (bytes4) { + // never happens: caught by ERC1155Fuse + // if (ids.length != amounts.length) { + // revert IERC1155Errors.ERC1155InvalidArrayLength(ids.length, amounts.length); + // } + IWrapperRegistry.Data[] memory mds = abi.decode(data, (IWrapperRegistry.Data[])); // reverts if invalid + try this.finishERC1155Migration(ids, mds) { + return this.onERC1155BatchReceived.selector; + } catch (bytes memory reason) { + WrappedErrorLib.wrapAndRevert(reason); // convert all errors to wrapped + } + } + + // TODO: gas analysis and optimization + // NOTE: converting this to an internal call requires catching many reverts + function finishERC1155Migration( + uint256[] calldata ids, + IWrapperRegistry.Data[] calldata mds + ) external { + if (msg.sender != address(this)) { + revert UnauthorizedCaller(msg.sender); + } + if (ids.length != mds.length) { + revert IERC1155Errors.ERC1155InvalidArrayLength(ids.length, mds.length); + } + bytes32 parentNode = _parentNode(); + for (uint256 i; i < ids.length; ++i) { + // never happens: caught by ERC1155Fuse + // https://github.com/ensdomains/ens-contracts/blob/staging/contracts/wrapper/ERC1155Fuse.sol#L182 + // https://github.com/ensdomains/ens-contracts/blob/staging/contracts/wrapper/ERC1155Fuse.sol#L293 + // if (amounts[i] != 1) { ... } + IWrapperRegistry.Data memory md = mds[i]; + if (md.owner == address(0)) { + revert IERC1155Errors.ERC1155InvalidReceiver(md.owner); + } + bytes32 node = bytes32(ids[i]); + bytes32 labelHash = keccak256(bytes(md.label)); + if (node != NameCoder.namehash(parentNode, labelHash)) { + revert MigrationErrors.NameDataMismatch(uint256(node)); + } + // 1 <= length(label) <= 255 + + (, uint32 fuses, uint64 expiry) = NAME_WRAPPER.getData(uint256(node)); + // ignore owner, only we can call this function => we own it + + // cannot be set without PARENT_CANNOT_CONTROL + if ((fuses & CANNOT_UNWRAP) == 0) { + revert MigrationErrors.NameNotLocked(uint256(node)); + } + + // sync expiry + if ((fuses & IS_DOT_ETH) != 0) { + require((fuses & CAN_EXTEND_EXPIRY) == 0, "2LD is always renewable by anyone"); + //fuses &= ~CAN_EXTEND_EXPIRY; // 2LD is always renewable by anyone + expiry = uint64(NAME_WRAPPER.registrar().nameExpires(uint256(labelHash))); // does not revert + } + // NameWrapper subtracts GRACE_PERIOD from expiry during _beforeTransfer() + // https://github.com/ensdomains/ens-contracts/blob/staging/contracts/wrapper/NameWrapper.sol#L822 + // expired names cannot be transferred: + assert(expiry >= block.timestamp); + // PermissionedRegistry._register() => CannotSetPastExpiration + // wont happen as this operation is synchronous + + address resolver; + if ((fuses & CANNOT_SET_RESOLVER) != 0) { + resolver = NAME_WRAPPER.ens().resolver(node); // copy V1 resolver + } else { + resolver = md.resolver; // accepts any value + NAME_WRAPPER.setResolver(node, address(0)); // clear V1 resolver + } + + (uint256 tokenRoles, uint256 registryRoles) = _generateRoleBitmapsFromFuses(fuses); + // PermissionedRegistry._register() => _grantRoles() => _checkRoleBitmap() + // wont happen as roles are correct by construction + + // create subregistry + IRegistry subregistry = IRegistry( + VERIFIABLE_FACTORY.deployProxy( + MIGRATED_REGISTRY_IMPL, + md.salt, + abi.encodeCall( + IWrapperRegistry.initialize, + ( + IWrapperRegistry.ConstructorArgs({ + node: node, + owner: md.owner, + ownerRoles: registryRoles + }) + ) + ) + ) + ); + + // add name to V2 + _inject(md.label, md.owner, subregistry, resolver, tokenRoles, expiry); + // PermissionedRegistry._register() => NameAlreadyRegistered + // ERC1155._safeTransferFrom() => ERC1155InvalidReceiver + + // Burn all migration fuses + NAME_WRAPPER.setFuses(node, uint16(FUSES_TO_BURN)); + } + } + + function _inject( + string memory label, + address owner, + IRegistry subregistry, + address resolver, + uint256 roleBitmap, + uint64 expiry + ) internal virtual returns (uint256 tokenId); + + function _parentNode() internal view virtual returns (bytes32); + + /// @notice Generates role bitmaps based on fuses. + /// @param fuses The current fuses on the name + /// @return tokenRoles The token roles in parent registry. + /// @return registryRoles The root roles in token subregistry. + function _generateRoleBitmapsFromFuses( + uint32 fuses + ) internal pure returns (uint256 tokenRoles, uint256 registryRoles) { + // Check if fuses are permanently frozen + bool fusesFrozen = (fuses & CANNOT_BURN_FUSES) != 0; + + // Include renewal permissions if expiry can be extended + if ((fuses & CAN_EXTEND_EXPIRY) != 0) { + tokenRoles |= RegistryRolesLib.ROLE_RENEW; + if (!fusesFrozen) { + tokenRoles |= RegistryRolesLib.ROLE_RENEW_ADMIN; + } + } + + // Conditionally add resolver roles + if ((fuses & CANNOT_SET_RESOLVER) == 0) { + tokenRoles |= RegistryRolesLib.ROLE_SET_RESOLVER; + if (!fusesFrozen) { + tokenRoles |= RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN; + } + } + + // Add transfer admin role if transfers are allowed + if ((fuses & CANNOT_TRANSFER) == 0) { + tokenRoles |= RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN; + } + + // Owner gets registrar permissions on subregistry only if subdomain creation is allowed + if ((fuses & CANNOT_CREATE_SUBDOMAIN) == 0) { + registryRoles |= RegistryRolesLib.ROLE_REGISTRAR; + if (!fusesFrozen) { + registryRoles |= RegistryRolesLib.ROLE_REGISTRAR_ADMIN; + } + } + + // Add renewal roles to subregistry + registryRoles |= RegistryRolesLib.ROLE_RENEW; + registryRoles |= RegistryRolesLib.ROLE_RENEW_ADMIN; + } +} diff --git a/contracts/src/migration/libraries/LockedNamesLib.sol b/contracts/src/migration/libraries/LockedNamesLib.sol deleted file mode 100644 index a339a30e..00000000 --- a/contracts/src/migration/libraries/LockedNamesLib.sol +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.13; - -import { - INameWrapper, - CANNOT_UNWRAP, - CANNOT_BURN_FUSES, - CANNOT_TRANSFER, - CANNOT_SET_RESOLVER, - CANNOT_SET_TTL, - CANNOT_CREATE_SUBDOMAIN, - IS_DOT_ETH, - CAN_EXTEND_EXPIRY, - PARENT_CANNOT_CONTROL -} from "@ens/contracts/wrapper/INameWrapper.sol"; -import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; - -import { - IMigratedWrappedNameRegistry -} from "../../registry/interfaces/IMigratedWrappedNameRegistry.sol"; -import {RegistryRolesLib} from "../../registry/libraries/RegistryRolesLib.sol"; - -/** - * @title LockedNamesLib - * @notice Library for common locked name migration operations - * @dev Contains shared logic for migrating locked names from ENS NameWrapper to v2 registries - */ -library LockedNamesLib { - //////////////////////////////////////////////////////////////////////// - // Constants - //////////////////////////////////////////////////////////////////////// - - /** - * @notice The fuses to burn during migration to prevent further changes - * @dev Includes all transferable and modifiable fuses including the lock fuse - */ - uint32 public constant FUSES_TO_BURN = - CANNOT_UNWRAP | - CANNOT_BURN_FUSES | - CANNOT_TRANSFER | - CANNOT_SET_RESOLVER | - CANNOT_SET_TTL | - CANNOT_CREATE_SUBDOMAIN; - - //////////////////////////////////////////////////////////////////////// - // Errors - //////////////////////////////////////////////////////////////////////// - - error NameNotLocked(uint256 tokenId); - - error NameNotEmancipated(uint256 tokenId); - - error NotDotEthName(uint256 tokenId); - - //////////////////////////////////////////////////////////////////////// - // Library Functions - //////////////////////////////////////////////////////////////////////// - - /** - * @notice Deploys a new MigratedWrappedNameRegistry via VerifiableFactory - * @dev The owner will have the specified roles on the deployed registry - * @param factory The VerifiableFactory to use for deployment - * @param implementation The implementation address for the proxy - * @param owner The address that will own the deployed registry - * @param ownerRoles The roles to grant to the owner - * @param salt The salt for CREATE2 deployment - * @param parentDnsEncodedName The DNS-encoded name of the parent domain - * @return subregistry The address of the deployed registry - */ - function deployMigratedRegistry( - VerifiableFactory factory, - address implementation, - address owner, - uint256 ownerRoles, - uint256 salt, - bytes memory parentDnsEncodedName - ) internal returns (address subregistry) { - bytes memory initData = abi.encodeCall( - IMigratedWrappedNameRegistry.initialize, - (parentDnsEncodedName, owner, ownerRoles, address(0)) - ); - subregistry = factory.deployProxy(implementation, salt, initData); - } - - /** - * @notice Freezes a name by clearing its resolver if possible and burning all migration fuses - * @dev Sets resolver to address(0) if CANNOT_SET_RESOLVER is not burned, then permanently freezes the name - * @param nameWrapper The NameWrapper contract - * @param tokenId The token ID to freeze - * @param fuses The current fuses on the name - */ - function freezeName(INameWrapper nameWrapper, uint256 tokenId, uint32 fuses) internal { - // Clear resolver if CANNOT_SET_RESOLVER fuse is not set - if ((fuses & CANNOT_SET_RESOLVER) == 0) { - nameWrapper.setResolver(bytes32(tokenId), address(0)); - } - - // Burn all migration fuses - nameWrapper.setFuses(bytes32(tokenId), uint16(FUSES_TO_BURN)); - } - - /** - * @notice Validates that a name is properly locked for migration - * @dev Checks that CANNOT_UNWRAP is set - * @param fuses The current fuses on the name - * @param tokenId The token ID for error reporting - */ - function validateLockedName(uint32 fuses, uint256 tokenId) internal pure { - if ((fuses & CANNOT_UNWRAP) == 0) { - revert NameNotLocked(tokenId); - } - } - - /** - * @notice Validates that a name is properly emancipated for migration - * @dev Checks that PARENT_CANNOT_CONTROL is set (emancipated). Name may or may not be locked. - * @param fuses The current fuses on the name - * @param tokenId The token ID for error reporting - */ - function validateEmancipatedName(uint32 fuses, uint256 tokenId) internal pure { - if ((fuses & PARENT_CANNOT_CONTROL) == 0) { - revert NameNotEmancipated(tokenId); - } - } - - /** - * @notice Validates that a name is a .eth second-level domain - * @dev Checks the IS_DOT_ETH fuse, which is only valid for .eth 2LDs - * @param fuses The current fuses on the name - * @param tokenId The token ID for error reporting - */ - function validateIsDotEth2LD(uint32 fuses, uint256 tokenId) internal pure { - if ((fuses & IS_DOT_ETH) == 0) { - revert NotDotEthName(tokenId); - } - } - - /** - * @notice Generates role bitmaps based on fuses - * @dev Returns two bitmaps: tokenRoles for the name registration and subRegistryRoles for the registry owner - * @param fuses The current fuses on the name - * @return tokenRoles The role bitmap for the owner on their name in their parent registry. - * @return subRegistryRoles The role bitmap for the owner on their name's subregistry. - */ - function generateRoleBitmapsFromFuses( - uint32 fuses - ) internal pure returns (uint256 tokenRoles, uint256 subRegistryRoles) { - // Check if fuses are permanently frozen - bool fusesFrozen = (fuses & CANNOT_BURN_FUSES) != 0; - - // Include renewal permissions if expiry can be extended - if ((fuses & CAN_EXTEND_EXPIRY) != 0) { - tokenRoles |= RegistryRolesLib.ROLE_RENEW; - if (!fusesFrozen) { - tokenRoles |= RegistryRolesLib.ROLE_RENEW_ADMIN; - } - } - - // Conditionally add resolver roles - if ((fuses & CANNOT_SET_RESOLVER) == 0) { - tokenRoles |= RegistryRolesLib.ROLE_SET_RESOLVER; - if (!fusesFrozen) { - tokenRoles |= RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN; - } - } - - // Add transfer admin role if transfers are allowed - if ((fuses & CANNOT_TRANSFER) == 0) { - tokenRoles |= RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN; - } - - // Owner gets registrar permissions on subregistry only if subdomain creation is allowed - if ((fuses & CANNOT_CREATE_SUBDOMAIN) == 0) { - subRegistryRoles |= RegistryRolesLib.ROLE_REGISTRAR; - if (!fusesFrozen) { - subRegistryRoles |= RegistryRolesLib.ROLE_REGISTRAR_ADMIN; - } - } - - // Add renewal roles to subregistry - subRegistryRoles |= RegistryRolesLib.ROLE_RENEW; - subRegistryRoles |= RegistryRolesLib.ROLE_RENEW_ADMIN; - } -} diff --git a/contracts/src/registry/MigratedWrappedNameRegistry.sol b/contracts/src/registry/MigratedWrappedNameRegistry.sol deleted file mode 100644 index 9670f376..00000000 --- a/contracts/src/registry/MigratedWrappedNameRegistry.sol +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.13; - -import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; -import {INameWrapper, PARENT_CANNOT_CONTROL} from "@ens/contracts/wrapper/INameWrapper.sol"; -import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; -import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -import {UnauthorizedCaller} from "../CommonErrors.sol"; -import {IHCAFactoryBasic} from "../hca/interfaces/IHCAFactoryBasic.sol"; -import {LockedNamesLib} from "../migration/libraries/LockedNamesLib.sol"; -import {ParentNotMigrated, LabelNotMigrated} from "../migration/MigrationErrors.sol"; -import {MigrationData} from "../migration/types/MigrationTypes.sol"; - -import {IMigratedWrappedNameRegistry} from "./interfaces/IMigratedWrappedNameRegistry.sol"; -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 {RegistryRolesLib} from "./libraries/RegistryRolesLib.sol"; -import {PermissionedRegistry} from "./PermissionedRegistry.sol"; - -/** - * @title MigratedWrappedNameRegistry - * @dev A registry for migrated wrapped names that inherits from PermissionedRegistry and is upgradeable using the UUPS pattern. - * This contract provides resolver fallback to the universal resolver for names that haven't been migrated yet. - * It also handles subdomain migration by receiving NFT transfers from the NameWrapper. - */ -contract MigratedWrappedNameRegistry is - Initializable, - PermissionedRegistry, - UUPSUpgradeable, - IERC1155Receiver, - IMigratedWrappedNameRegistry -{ - //////////////////////////////////////////////////////////////////////// - // Constants - //////////////////////////////////////////////////////////////////////// - - // TODO: these clobbers ROLE_CAN_TRANSFER_ADMIN and should be in RegistryRolesLib - uint256 internal constant _ROLE_UPGRADE = 1 << 20; - uint256 internal constant _ROLE_UPGRADE_ADMIN = _ROLE_UPGRADE << 128; - - INameWrapper public immutable NAME_WRAPPER; - - VerifiableFactory public immutable FACTORY; - - IPermissionedRegistry public immutable ETH_REGISTRY; - - address public immutable FALLBACK_RESOLVER; - - //////////////////////////////////////////////////////////////////////// - // Storage - //////////////////////////////////////////////////////////////////////// - - bytes public parentDnsEncodedName; - - //////////////////////////////////////////////////////////////////////// - // Errors - //////////////////////////////////////////////////////////////////////// - - error NoParentDomain(); - - //////////////////////////////////////////////////////////////////////// - // Initialization - //////////////////////////////////////////////////////////////////////// - - constructor( - INameWrapper nameWrapper, - IPermissionedRegistry ethRegistry, - VerifiableFactory factory, - IRegistryDatastore datastore, - IHCAFactoryBasic hcaFactory, - IRegistryMetadata metadataProvider, - address fallbackResolver - ) PermissionedRegistry(datastore, hcaFactory, metadataProvider, _msgSender(), 0) { - NAME_WRAPPER = nameWrapper; - ETH_REGISTRY = ethRegistry; - FACTORY = factory; - FALLBACK_RESOLVER = fallbackResolver; - // Prevents initialization on the implementation contract - _disableInitializers(); - } - - /** - * @dev Initializes the MigratedWrappedNameRegistry contract. - * @param parentDnsEncodedName_ The DNS-encoded name of the parent domain. - * @param ownerAddress_ The address that will own this registry. - * @param ownerRoles_ The roles to grant to the owner. - * @param registrarAddress_ Optional address to grant ROLE_REGISTRAR permissions (typically for testing). - */ - function initialize( - bytes calldata parentDnsEncodedName_, - address ownerAddress_, - uint256 ownerRoles_, - address registrarAddress_ - ) public initializer { - // TODO: custom error - require(ownerAddress_ != address(0), "Owner cannot be zero address"); - - // Set the parent domain for name resolution fallback - parentDnsEncodedName = parentDnsEncodedName_; - - // Configure owner with upgrade permissions and specified roles - _grantRoles( - ROOT_RESOURCE, - _ROLE_UPGRADE | _ROLE_UPGRADE_ADMIN | ownerRoles_, - ownerAddress_, - false - ); - - // Grant registrar role if specified (typically for testing) - if (registrarAddress_ != address(0)) { - _grantRoles(ROOT_RESOURCE, RegistryRolesLib.ROLE_REGISTRAR, registrarAddress_, false); - } - } - - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(IERC165, PermissionedRegistry) returns (bool) { - return - interfaceId == type(IERC1155Receiver).interfaceId || - super.supportsInterface(interfaceId); - } - - //////////////////////////////////////////////////////////////////////// - // Implementation - //////////////////////////////////////////////////////////////////////// - - function onERC1155Received( - address /*operator*/, - address /*from*/, - uint256 tokenId, - uint256 /*amount*/, - bytes calldata data - ) external virtual returns (bytes4) { - if (msg.sender != address(NAME_WRAPPER)) { - revert UnauthorizedCaller(msg.sender); - } - - (MigrationData memory migrationData) = abi.decode(data, (MigrationData)); - MigrationData[] memory migrationDataArray = new MigrationData[](1); - migrationDataArray[0] = migrationData; - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = tokenId; - - _migrateSubdomains(tokenIds, migrationDataArray); - - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address /*operator*/, - address /*from*/, - uint256[] calldata tokenIds, - uint256[] calldata /*amounts*/, - bytes calldata data - ) external virtual returns (bytes4) { - if (msg.sender != address(NAME_WRAPPER)) { - revert UnauthorizedCaller(msg.sender); - } - - (MigrationData[] memory migrationDataArray) = abi.decode(data, (MigrationData[])); - - _migrateSubdomains(tokenIds, migrationDataArray); - - return this.onERC1155BatchReceived.selector; - } - - /// @inheritdoc PermissionedRegistry - /// @dev Restore the latest resolver to `FALLBACK_RESOLVER` upon visiting migratable children. - function getResolver( - string calldata label - ) public view override(PermissionedRegistry) returns (address) { - bytes32 node = NameCoder.namehash( - NameCoder.namehash(parentDnsEncodedName, 0), - keccak256(bytes(label)) - ); - (address owner, uint32 fuses, ) = NAME_WRAPPER.getData(uint256(node)); - if (owner != address(this) && (fuses & PARENT_CANNOT_CONTROL) != 0) { - return FALLBACK_RESOLVER; - } - return super.getResolver(label); - } - - //////////////////////////////////////////////////////////////////////// - // Internal Functions - //////////////////////////////////////////////////////////////////////// - - /** - * @dev Required override for UUPSUpgradeable - restricts upgrade permissions - */ - function _authorizeUpgrade(address) internal override onlyRootRoles(_ROLE_UPGRADE) {} - - function _migrateSubdomains( - uint256[] memory tokenIds, - MigrationData[] memory migrationDataArray - ) internal { - for (uint256 i = 0; i < tokenIds.length; i++) { - (, uint32 fuses, ) = NAME_WRAPPER.getData(tokenIds[i]); - - // Ensure name meets migration requirements - LockedNamesLib.validateEmancipatedName(fuses, tokenIds[i]); - - // Ensure proper domain hierarchy for migration - string memory label = _validateHierarchy( - migrationDataArray[i].transferData.dnsEncodedName, - 0 - ); - - // Determine permissions from name configuration (allow subdomain renewal based on fuses) - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Create dedicated registry for the migrated name - address subregistry = LockedNamesLib.deployMigratedRegistry( - FACTORY, - ERC1967Utils.getImplementation(), - migrationDataArray[i].transferData.owner, - subRegistryRoles, - migrationDataArray[i].salt, - migrationDataArray[i].transferData.dnsEncodedName - ); - - // Complete name registration in new registry - _register( - label, - migrationDataArray[i].transferData.owner, - IRegistry(subregistry), - migrationDataArray[i].transferData.resolver, - tokenRoles, - migrationDataArray[i].transferData.expires - ); - - // Finalize migration by freezing the name - LockedNamesLib.freezeName(NAME_WRAPPER, tokenIds[i], fuses); - } - } - - function _register( - string memory label, - address owner, - IRegistry registry, - address resolver, - uint256 roleBitmap, - uint64 expires - ) internal virtual override returns (uint256 tokenId) { - // Check if the label has an emancipated NFT in the old system - // For .eth 2LDs, NameWrapper uses keccak256(label) as the token ID - uint256 legacyTokenId = uint256(keccak256(bytes(label))); - (, uint32 fuses, ) = NAME_WRAPPER.getData(legacyTokenId); - - // If the name is emancipated (PARENT_CANNOT_CONTROL burned), - // it must be migrated (owned by this registry) - if ((fuses & PARENT_CANNOT_CONTROL) != 0) { - if (NAME_WRAPPER.ownerOf(legacyTokenId) != address(this)) { - revert LabelNotMigrated(label); - } - } - - // Proceed with registration - return super._register(label, owner, registry, resolver, roleBitmap, expires); - } - - function _validateHierarchy( - bytes memory dnsEncodedName, - uint256 offset - ) internal view returns (string memory label) { - // Extract the current label (leftmost, at offset 0) - uint256 parentOffset; - (label, parentOffset) = NameCoder.extractLabel(dnsEncodedName, offset); - - // Check if there's no parent (trying to migrate TLD) - if (dnsEncodedName[parentOffset] == 0) { - revert NoParentDomain(); - } - - // Extract the parent label - (string memory parentLabel, uint256 grandparentOffset) = NameCoder.extractLabel( - dnsEncodedName, - parentOffset - ); - - // Check if this is a 2LD (parent is "eth" and no grandparent) - if ( - keccak256(bytes(parentLabel)) == keccak256(bytes("eth")) && - dnsEncodedName[grandparentOffset] == 0 - ) { - // For 2LD: Check that label is NOT registered in ethRegistry - IRegistry subregistry = ETH_REGISTRY.getSubregistry(label); - if (address(subregistry) != address(0)) { - revert IStandardRegistry.NameAlreadyRegistered(label); - } - } else { - // For 3LD+: Check that parent is wrapped and owned by this contract - bytes32 parentNode = NameCoder.namehash(dnsEncodedName, parentOffset); - if ( - !NAME_WRAPPER.isWrapped(parentNode) || - NAME_WRAPPER.ownerOf(uint256(parentNode)) != address(this) - ) { - revert ParentNotMigrated(dnsEncodedName, parentOffset); - } - - // Also check that the current label is NOT already registered in this registry - IRegistry subregistry = this.getSubregistry(label); - if (address(subregistry) != address(0)) { - revert IStandardRegistry.NameAlreadyRegistered(label); - } - } - - return label; - } -} diff --git a/contracts/src/registry/WrapperRegistry.sol b/contracts/src/registry/WrapperRegistry.sol new file mode 100644 index 00000000..03f9ba74 --- /dev/null +++ b/contracts/src/registry/WrapperRegistry.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; +import { + INameWrapper, + CANNOT_UNWRAP, + PARENT_CANNOT_CONTROL +} from "@ens/contracts/wrapper/INameWrapper.sol"; +import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {InvalidOwner} from "../CommonErrors.sol"; +import {IHCAFactoryBasic} from "../hca/interfaces/IHCAFactoryBasic.sol"; +import {MigrationErrors} from "../migration/MigrationErrors.sol"; +import {WrapperReceiver} from "../migration/WrapperReceiver.sol"; +import {IWrapperRegistry} from "../registry/interfaces/IWrapperRegistry.sol"; + +import {IRegistry} from "./interfaces/IRegistry.sol"; +import {IRegistryDatastore} from "./interfaces/IRegistryDatastore.sol"; +import {IRegistryMetadata} from "./interfaces/IRegistryMetadata.sol"; +import {RegistryRolesLib} from "./libraries/RegistryRolesLib.sol"; +import {PermissionedRegistry} from "./PermissionedRegistry.sol"; + +contract WrapperRegistry is + WrapperReceiver, + Initializable, + UUPSUpgradeable, + PermissionedRegistry, + IWrapperRegistry +{ + //////////////////////////////////////////////////////////////////////// + // Constants + //////////////////////////////////////////////////////////////////////// + + address public immutable V1_RESOLVER; + + //////////////////////////////////////////////////////////////////////// + // Storage + //////////////////////////////////////////////////////////////////////// + + bytes32 public parentNode; + + //////////////////////////////////////////////////////////////////////// + // Initialization + //////////////////////////////////////////////////////////////////////// + + constructor( + INameWrapper nameWrapper, + VerifiableFactory verifiableFactory, + address ensV1Resolver, + IRegistryDatastore datastore, + IHCAFactoryBasic hcaFactory, + IRegistryMetadata metadataProvider + ) + PermissionedRegistry(datastore, hcaFactory, metadataProvider, _msgSender(), 0) + WrapperReceiver(nameWrapper, verifiableFactory, address(this)) + { + V1_RESOLVER = ensV1Resolver; + _disableInitializers(); + } + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, WrapperReceiver, PermissionedRegistry) returns (bool) { + return + type(IWrapperRegistry).interfaceId == interfaceId || + type(UUPSUpgradeable).interfaceId == interfaceId || + WrapperReceiver.supportsInterface(interfaceId) || + PermissionedRegistry.supportsInterface(interfaceId); + } + + /// @inheritdoc IWrapperRegistry + function initialize(IWrapperRegistry.ConstructorArgs calldata args) public initializer { + if (args.owner == address(0)) { + revert InvalidOwner(); + } + + // Set the parent domain for name resolution fallback + parentNode = args.node; + + // Configure owner with upgrade permissions and specified roles + _grantRoles( + ROOT_RESOURCE, + RegistryRolesLib.ROLE_UPGRADE | RegistryRolesLib.ROLE_UPGRADE_ADMIN | args.ownerRoles, + args.owner, + false + ); + } + + //////////////////////////////////////////////////////////////////////// + // Implementation + //////////////////////////////////////////////////////////////////////// + + /// @inheritdoc IWrapperRegistry + function parentName() external view returns (bytes memory) { + return NAME_WRAPPER.names(parentNode); + } + + /// @inheritdoc PermissionedRegistry + /// @dev Return `V1_RESOLVER` upon visiting migratable children. + function getResolver( + string calldata label + ) public view override(IRegistry, PermissionedRegistry) returns (address) { + return _isMigratableChild(label) ? V1_RESOLVER : super.getResolver(label); + } + + /// @inheritdoc WrapperReceiver + /// @dev Allow registration of emancipated children. + function _inject( + string memory label, + address owner, + IRegistry subregistry, + address resolver, + uint256 roleBitmap, + uint64 expiry + ) internal override returns (uint256 tokenId) { + return super._register(label, owner, subregistry, resolver, roleBitmap, expiry); + } + + /// @inheritdoc PermissionedRegistry + /// @dev Prevent registration of emancipated children. + function _register( + string memory label, + address owner, + IRegistry registry, + address resolver, + uint256 roleBitmap, + uint64 expiry + ) internal override returns (uint256 tokenId) { + if (_isMigratableChild(label)) { + revert MigrationErrors.NameNotMigrated( + NameCoder.addLabel(NAME_WRAPPER.names(parentNode), label) + ); + } + return super._register(label, owner, registry, resolver, roleBitmap, expiry); + } + + /// @dev Allow `ROLE_UPGRADE` to upgrade. + function _authorizeUpgrade( + address + ) internal override onlyRootRoles(RegistryRolesLib.ROLE_UPGRADE) { + // + } + + function _parentNode() internal view override returns (bytes32) { + return parentNode; + } + + function _isMigratableChild(string memory label) internal view returns (bool) { + bytes32 node = NameCoder.namehash(parentNode, keccak256(bytes(label))); + (address ownerV1, uint32 fuses, ) = NAME_WRAPPER.getData(uint256(node)); + uint32 EMANCIPATED = CANNOT_UNWRAP | PARENT_CANNOT_CONTROL; + return ownerV1 != address(this) && (fuses & EMANCIPATED) == EMANCIPATED; + } +} diff --git a/contracts/src/registry/interfaces/IMigratedWrappedNameRegistry.sol b/contracts/src/registry/interfaces/IMigratedWrappedNameRegistry.sol deleted file mode 100644 index 9855e96b..00000000 --- a/contracts/src/registry/interfaces/IMigratedWrappedNameRegistry.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.13; - -/** - * @dev Interface for MigratedWrappedNameRegistry initialization and core functions - */ -interface IMigratedWrappedNameRegistry { - function initialize( - bytes calldata parentDnsEncodedName_, - address ownerAddress_, - uint256 ownerRoles_, - address registrarAddress_ - ) external; -} diff --git a/contracts/src/registry/interfaces/IWrapperRegistry.sol b/contracts/src/registry/interfaces/IWrapperRegistry.sol new file mode 100644 index 00000000..d42d2246 --- /dev/null +++ b/contracts/src/registry/interfaces/IWrapperRegistry.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {IPermissionedRegistry} from "./IPermissionedRegistry.sol"; + +/// @dev Minimum Size of `abi.encode(Data({...}))`. +uint256 constant MIN_DATA_SIZE = 4 * 32; + +/// @dev Interface for a registry that manages a locked NameWrapper name. +interface IWrapperRegistry is IPermissionedRegistry { + //////////////////////////////////////////////////////////////////////// + // Types + //////////////////////////////////////////////////////////////////////// + + /// @dev Typed arguments for `initialize()`. + struct ConstructorArgs { + bytes32 node; + address owner; + uint256 ownerRoles; + } + + struct Data { + string label; + address owner; + address resolver; + uint256 salt; + } + + //////////////////////////////////////////////////////////////////////// + // Functions + //////////////////////////////////////////////////////////////////////// + + function initialize(ConstructorArgs calldata args) external; + + function parentName() external view returns (bytes memory); +} diff --git a/contracts/src/registry/libraries/RegistryRolesLib.sol b/contracts/src/registry/libraries/RegistryRolesLib.sol index c71e5528..190680c0 100644 --- a/contracts/src/registry/libraries/RegistryRolesLib.sol +++ b/contracts/src/registry/libraries/RegistryRolesLib.sol @@ -15,4 +15,7 @@ library RegistryRolesLib { uint256 internal constant ROLE_SET_RESOLVER_ADMIN = ROLE_SET_RESOLVER << 128; uint256 internal constant ROLE_CAN_TRANSFER_ADMIN = (1 << 16) << 128; + + uint256 internal constant ROLE_UPGRADE = 1 << 124; + uint256 internal constant ROLE_UPGRADE_ADMIN = ROLE_UPGRADE << 128; } diff --git a/contracts/src/utils/WrappedErrorLib.sol b/contracts/src/utils/WrappedErrorLib.sol new file mode 100644 index 00000000..f695eaec --- /dev/null +++ b/contracts/src/utils/WrappedErrorLib.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import {HexUtils} from "@ens/contracts/utils/HexUtils.sol"; + +/// @dev Library to wrap and unwrap error data inside of `Error(string)`. +library WrappedErrorLib { + bytes4 public constant ERROR_STRING_SELECTOR = 0x08c379a0; // Error(string) + + /// @dev The detectable human-readable error prefix. + bytes16 public constant WRAPPED_ERROR_PREFIX = "WrappedError:0x"; // unicode"❌WrappedErr:0x"; + + /// @dev Wrap an error and then revert. + function wrapAndRevert(bytes memory err) internal pure { + err = wrap(err); + assembly { + revert(add(err, 32), mload(err)) + } + } + + /// @dev Embed a typed error into `Error(string)`. + /// Does nothing if already `Error(string)`. + /// For detection, `WRAPPED_ERROR_PREFIX` is leading bytes the error string. + function wrap(bytes memory err) internal pure returns (bytes memory) { + if (err.length > 0 && bytes4(err) != ERROR_STRING_SELECTOR) { + // assert((err.length & 31) == 4); + err = abi.encodeWithSelector( + ERROR_STRING_SELECTOR, + abi.encodePacked(WRAPPED_ERROR_PREFIX, HexUtils.bytesToHex(err)) + ); + } + return err; + } + + /// @dev Unwrap a typed error from `Error(string)`. + /// Does nothing if detection and extracton fails. + /// + /// @param err The error data to unwrap. + /// + /// @return The unwrapped error data, or unmodified if not wrapped. + function unwrap(bytes memory err) internal pure returns (bytes memory) { + if (bytes4(err) == ERROR_STRING_SELECTOR) { + bytes memory v; + assembly { + v := add(err, 4) // skip selector + } + v = abi.decode(v, (bytes)); + if (bytes16(v) == WRAPPED_ERROR_PREFIX) { + (bytes memory inner, bool ok) = HexUtils.hexToBytes(v, 16, v.length); + if (ok) { + return inner; + } + } + } + return err; + } +} diff --git a/contracts/test/fixtures/V1Fixture.sol b/contracts/test/fixtures/V1Fixture.sol new file mode 100644 index 00000000..0901a098 --- /dev/null +++ b/contracts/test/fixtures/V1Fixture.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; + +import {ENSRegistry} from "@ens/contracts/registry/ENSRegistry.sol"; +import { + BaseRegistrarImplementation +} from "@ens/contracts/ethregistrar/BaseRegistrarImplementation.sol"; +import {NameWrapper, IMetadataService} from "@ens/contracts/wrapper/NameWrapper.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + +/// @dev Reusable testing fixture for ENSv1. +contract V1Fixture is Test, ERC721Holder, ERC1155Holder { + ENSRegistry ensV1; + BaseRegistrarImplementation ethRegistrarV1; + NameWrapper nameWrapper; + + address user = makeAddr("user"); + address ensV1Controller = makeAddr("ensV1Controller"); + + function deployV1Fixture() public { + ensV1 = new ENSRegistry(); + ethRegistrarV1 = new BaseRegistrarImplementation(ensV1, NameCoder.ETH_NODE); + ethRegistrarV1.addController(ensV1Controller); + _claimNodes(NameCoder.encode("eth"), 0, address(ethRegistrarV1)); + _claimNodes(NameCoder.encode("addr.reverse"), 0, address(this)); // see: fake ReverseClaimer + nameWrapper = new NameWrapper(ensV1, ethRegistrarV1, IMetadataService(address(0))); + nameWrapper.setController(ensV1Controller, true); + ethRegistrarV1.addController(address(nameWrapper)); + vm.warp(ethRegistrarV1.GRACE_PERIOD() + 1); // avoid timestamp issues + } + + // fake ReverseClaimer + function claim(address) external pure returns (bytes32) {} + + /// @dev Claim a name in the registry at any depth. + /// Preseves existing ownership until the leaf. + function _claimNodes(bytes memory name, uint256 offset, address owner) internal { + (bytes32 labelHash, uint256 nextOffset) = NameCoder.readLabel(name, offset); + if (labelHash != bytes32(0)) { + _claimNodes(name, nextOffset, owner); + // claim if leaf or unset + if (offset == 0 || ensV1.owner(NameCoder.namehash(name, offset)) == address(0)) { + bytes32 parentNode = NameCoder.namehash(name, nextOffset); + vm.prank(ensV1.owner(parentNode)); + ensV1.setSubnodeOwner(parentNode, labelHash, owner); + } + } + } + + function registerUnwrapped( + string memory label + ) public returns (bytes memory name, uint256 tokenId) { + name = NameCoder.ethName(label); + tokenId = uint256(keccak256(bytes(label))); + vm.prank(ensV1Controller); + ethRegistrarV1.register(tokenId, user, 1 days); // test duration + } + + function registerWrappedETH2LD( + string memory label, + uint32 ownerFuses + ) public returns (bytes memory name) { + uint256 tokenId; + (name, tokenId) = registerUnwrapped(label); + address owner = ethRegistrarV1.ownerOf(tokenId); + vm.startPrank(owner); + ethRegistrarV1.setApprovalForAll(address(nameWrapper), true); + nameWrapper.wrapETH2LD(label, owner, uint16(ownerFuses), address(0)); + vm.stopPrank(); + } + + function createWrappedChild( + bytes memory parentName, + string memory label, + uint32 fuses + ) public returns (bytes memory name) { + bytes32 parentNode = NameCoder.namehash(parentName, 0); + (address owner, , uint64 expiry) = nameWrapper.getData(uint256(parentNode)); + name = NameCoder.addLabel(parentName, label); + vm.prank(owner); + nameWrapper.setSubnodeOwner(parentNode, label, owner, fuses, expiry); + } + + function createWrappedName( + string memory domain, + uint32 fuses + ) public returns (bytes memory name) { + name = NameCoder.encode(domain); + _claimNodes(name, 0, user); + (bytes32 labelHash, uint256 offset) = NameCoder.readLabel(name, 0); + bytes32 parentNode = NameCoder.namehash(name, offset); + vm.startPrank(user); + ensV1.setApprovalForAll(address(nameWrapper), true); + nameWrapper.wrap(name, user, address(0)); + if (fuses != 0) { + // this might need to be setChildFuses() + bytes32 node = NameCoder.namehash(parentNode, labelHash); + nameWrapper.setFuses(node, uint16(fuses)); + } + vm.stopPrank(); + } +} diff --git a/contracts/test/fixtures/V1Fixture.t.sol b/contracts/test/fixtures/V1Fixture.t.sol new file mode 100644 index 00000000..32750210 --- /dev/null +++ b/contracts/test/fixtures/V1Fixture.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import { + PARENT_CANNOT_CONTROL, + CANNOT_UNWRAP, + CANNOT_BURN_FUSES, + LabelTooShort, + LabelTooLong +} from "@ens/contracts/wrapper/NameWrapper.sol"; + +import {V1Fixture, NameCoder} from "./V1Fixture.sol"; + +// TODO: add more NameWrapper quirks and invariant tests. +contract V1FixtureTest is V1Fixture { + function setUp() external { + deployV1Fixture(); + } + + //////////////////////////////////////////////////////////////////////// + // Deployment Helpers + //////////////////////////////////////////////////////////////////////// + + function test_registerUnwrapped() external { + (, uint256 tokenId) = registerUnwrapped("test"); + assertEq(ethRegistrarV1.ownerOf(tokenId), user, "owner"); + } + + function test_registerWrappedETH2LD() external { + bytes memory name = registerWrappedETH2LD("test", 0); + assertEq(nameWrapper.ownerOf(uint256(NameCoder.namehash(name, 0))), user, "owner"); + } + + function test_registerWrappedETH3LD() external { + bytes memory parentName = registerWrappedETH2LD("test", 0); + bytes memory name = createWrappedChild(parentName, "sub", 0); + assertEq(nameWrapper.ownerOf(uint256(NameCoder.namehash(name, 0))), user, "owner"); + } + + function test_registerWrappedDNS2LD() external { + bytes memory name = createWrappedName("ens.domains", 0); + assertEq(nameWrapper.ownerOf(uint256(NameCoder.namehash(name, 0))), user, "owner"); + } + + function test_registerWrappedDNS3LD() external { + bytes memory parentName = createWrappedName("ens.domains", 0); + bytes memory name = createWrappedChild(parentName, "sub", 0); + assertEq(nameWrapper.ownerOf(uint256(NameCoder.namehash(name, 0))), user, "owner"); + } + + //////////////////////////////////////////////////////////////////////// + // NameWrapper Quirks + //////////////////////////////////////////////////////////////////////// + + function test_nameWrapper_wrapRootReverts() external { + vm.expectRevert(abi.encodeWithSignature("Error(string)", "readLabel: Index out of bounds")); + nameWrapper.wrap(hex"00", address(1), address(0)); + } + + function test_nameWrapper_labelTooShort() external { + bytes memory name = registerWrappedETH2LD("test", 0); + vm.expectRevert(abi.encodeWithSelector(LabelTooShort.selector)); + vm.prank(user); + nameWrapper.setSubnodeOwner( + NameCoder.namehash(name, 0), + "", + user, + 0, + uint64(block.timestamp + 1 days) + ); + } + + function test_nameWraper_wrap_labelTooLong() external { + bytes memory name = registerWrappedETH2LD("test", 0); + string memory label = new string(256); + vm.expectRevert(abi.encodeWithSelector(LabelTooLong.selector, label)); + vm.prank(user); + nameWrapper.setSubnodeOwner( + NameCoder.namehash(name, 0), + label, + user, + 0, + uint64(block.timestamp + 1 days) + ); + } + + function test_nameWrapper_expiryForETH2LDIncludesGrace() external { + bytes memory name = registerWrappedETH2LD("test", 0); + uint256 unwrappedExpiry = ethRegistrarV1.nameExpires( + uint256(keccak256(bytes(NameCoder.firstLabel(name)))) + ); + (, , uint256 wrappedExpiry) = nameWrapper.getData(uint256(NameCoder.namehash(name, 0))); + assertEq(unwrappedExpiry + ethRegistrarV1.GRACE_PERIOD(), wrappedExpiry); + } + + function test_nameWrapper_CANNOT_UNWRAP_requires_PARENT_CANNOT_CONTROL() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + createWrappedChild(name, "1", PARENT_CANNOT_CONTROL); + createWrappedChild(name, "2", PARENT_CANNOT_CONTROL | CANNOT_UNWRAP); + vm.expectRevert(); + this.createWrappedChild(name, "3", CANNOT_UNWRAP); + } + + function test_nameWrapper_PARENT_CANNOT_CONTROL_via_setFuses() external { + bytes memory name = registerWrappedETH2LD("test", 0); + (bytes32 labelhash, ) = NameCoder.readLabel(name, 0); + vm.startPrank(user); + nameWrapper.setFuses(NameCoder.namehash(name, 0), uint16(PARENT_CANNOT_CONTROL)); + nameWrapper.unwrapETH2LD(labelhash, user, user); + vm.stopPrank(); + } + + function test_nameWrapper_PARENT_CANNOT_CONTROL_via_wrap() external { + bytes memory parentName = registerWrappedETH2LD("test", CANNOT_UNWRAP); + bytes memory name = createWrappedChild(parentName, "sub", PARENT_CANNOT_CONTROL); + (bytes32 labelhash, ) = NameCoder.readLabel(name, 0); + vm.startPrank(user); + nameWrapper.setFuses(NameCoder.namehash(name, 0), uint16(PARENT_CANNOT_CONTROL)); + nameWrapper.unwrap(NameCoder.namehash(parentName, 0), labelhash, user); + vm.stopPrank(); + } + + function test_nameWrapper_CANNOT_BURN_FUSES_via_wrap() external { + registerWrappedETH2LD("test", CANNOT_UNWRAP | CANNOT_BURN_FUSES); + } + + function test_nameWrapper_CANNOT_BURN_FUSES_via_setFuses() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + vm.prank(user); + nameWrapper.setFuses(NameCoder.namehash(name, 0), uint16(CANNOT_BURN_FUSES)); + } + + function test_nameWrapper_CANNOT_BURN_FUSES_via_setChildFuses() external { + bytes memory parentName = registerWrappedETH2LD("test", CANNOT_UNWRAP); + bytes memory name = createWrappedChild( + parentName, + "sub", + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL + ); + // setChildFuses() does not allow fuse changes if PCC + // _setFuses() requires CU + PCC if child fuses as burned + vm.expectRevert(); + vm.prank(user); + nameWrapper.setChildFuses( + NameCoder.namehash(parentName, 0), + keccak256(bytes(NameCoder.firstLabel(name))), + CANNOT_BURN_FUSES, + uint64(block.timestamp + 1 days) + ); + } + + function test_ethRegistrarV1_ownerOf_unregisteredReverts() external { + vm.expectRevert(); + ethRegistrarV1.ownerOf(0); + } +} diff --git a/contracts/test/fixtures/V2Fixture.sol b/contracts/test/fixtures/V2Fixture.sol new file mode 100644 index 00000000..2c5ce36d --- /dev/null +++ b/contracts/test/fixtures/V2Fixture.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {GatewayProvider} from "@ens/contracts/ccipRead/GatewayProvider.sol"; +import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; + +import {EACBaseRolesLib} from "~src/access-control/libraries/EACBaseRolesLib.sol"; +import {BaseUriRegistryMetadata} from "~src/registry/BaseUriRegistryMetadata.sol"; +import {PermissionedRegistry} from "~src/registry/PermissionedRegistry.sol"; +import {RegistryDatastore} from "~src/registry/RegistryDatastore.sol"; +import {UniversalResolverV2} from "~src/universalResolver/UniversalResolverV2.sol"; +import {MockHCAFactoryBasic} from "~test/mocks/MockHCAFactoryBasic.sol"; + +/// @dev Reusable testing fixture for ENSv2 with a basic ".eth" deployment. +contract V2Fixture { + VerifiableFactory verifiableFactory; + MockHCAFactoryBasic hcaFactory; + RegistryDatastore datastore; + BaseUriRegistryMetadata metadata; + PermissionedRegistry rootRegistry; + PermissionedRegistry ethRegistry; + GatewayProvider batchGatewayProvider; + UniversalResolverV2 universalResolver; + + function deployV2Fixture() public { + hcaFactory = new MockHCAFactoryBasic(); + verifiableFactory = new VerifiableFactory(); + datastore = new RegistryDatastore(); + metadata = new BaseUriRegistryMetadata(hcaFactory); + rootRegistry = new PermissionedRegistry( + datastore, + hcaFactory, + metadata, + address(this), + EACBaseRolesLib.ALL_ROLES + ); + ethRegistry = new PermissionedRegistry( + datastore, + hcaFactory, + metadata, + address(this), + EACBaseRolesLib.ALL_ROLES + ); + rootRegistry.register( + "eth", + address(this), + ethRegistry, + address(0), + EACBaseRolesLib.ALL_ROLES, + type(uint64).max + ); + batchGatewayProvider = new GatewayProvider(address(this), new string[](0)); + universalResolver = new UniversalResolverV2(rootRegistry, batchGatewayProvider); + } + + function findResolver(bytes memory name) public view returns (address resolver) { + (resolver, , ) = universalResolver.findResolver(name); + } +} diff --git a/contracts/test/unit/migration/LockedMigrationController.t.sol b/contracts/test/unit/migration/LockedMigrationController.t.sol index 82277794..aa909e63 100644 --- a/contracts/test/unit/migration/LockedMigrationController.t.sol +++ b/contracts/test/unit/migration/LockedMigrationController.t.sol @@ -1,929 +1,540 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; -// solhint-disable no-console, private-vars-leading-underscore, state-visibility, func-name-mixedcase, namechain/ordering, one-contract-per-file - -import {Test} from "forge-std/Test.sol"; - -import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; -import {ENS} from "@ens/contracts/registry/ENS.sol"; +import {Vm, console} from "forge-std/Test.sol"; import { - INameWrapper, + OperationProhibited, CANNOT_UNWRAP, + CAN_DO_EVERYTHING, CANNOT_BURN_FUSES, CANNOT_TRANSFER, CANNOT_SET_RESOLVER, CANNOT_SET_TTL, CANNOT_CREATE_SUBDOMAIN, + PARENT_CANNOT_CONTROL, IS_DOT_ETH, CAN_EXTEND_EXPIRY -} from "@ens/contracts/wrapper/INameWrapper.sol"; -import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; -import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +} from "@ens/contracts/wrapper/NameWrapper.sol"; +import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import {IERC1155Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {EACBaseRolesLib} from "~src/access-control/EnhancedAccessControl.sol"; import {UnauthorizedCaller} from "~src/CommonErrors.sol"; -import {IPermissionedRegistry} from "~src/registry/interfaces/IPermissionedRegistry.sol"; -import {IRegistry} from "~src/registry/interfaces/IRegistry.sol"; -import {IRegistryMetadata} from "~src/registry/interfaces/IRegistryMetadata.sol"; -import {RegistryRolesLib} from "~src/registry/libraries/RegistryRolesLib.sol"; -import {PermissionedRegistry} from "~src/registry/PermissionedRegistry.sol"; -import {RegistryDatastore} from "~src/registry/RegistryDatastore.sol"; +import {ENSV1Resolver} from "~src/resolver/ENSV1Resolver.sol"; +import {V1Fixture} from "~test/fixtures/V1Fixture.sol"; +import {V2Fixture} from "~test/fixtures/V2Fixture.sol"; +import {WrappedErrorLib} from "~src/utils/WrappedErrorLib.sol"; +import {IStandardRegistry} from "~src/registry/interfaces/IStandardRegistry.sol"; import {LockedMigrationController} from "~src/migration/LockedMigrationController.sol"; -import {TransferData, MigrationData} from "~src/migration/types/MigrationTypes.sol"; -import {LockedNamesLib} from "~src/migration/libraries/LockedNamesLib.sol"; -import {MigratedWrappedNameRegistry} from "~src/registry/MigratedWrappedNameRegistry.sol"; -import {MockHCAFactoryBasic} from "~test/mocks/MockHCAFactoryBasic.sol"; - -contract MockNameWrapper { - mapping(uint256 tokenId => uint32 fuses) public fuses; - mapping(uint256 tokenId => uint64 expiry) public expiries; - mapping(uint256 tokenId => address owner) public owners; - mapping(uint256 tokenId => address resolver) public resolvers; - - ENS public ens; - - function setFuseData(uint256 tokenId, uint32 _fuses, uint64 _expiry) external { - fuses[tokenId] = _fuses; - expiries[tokenId] = _expiry; - } - - function setInitialResolver(uint256 tokenId, address resolver) external { - resolvers[tokenId] = resolver; +import { + WrapperRegistry, + IWrapperRegistry, + WrapperReceiver, + RegistryRolesLib, + MigrationErrors, + IRegistry, + IRegistryDatastore +} from "~src/registry/WrapperRegistry.sol"; + +contract LockedMigrationControllerTest is V1Fixture, V2Fixture { + LockedMigrationController migrationController; + WrapperRegistry migratedRegistryImpl; + ENSV1Resolver ensV1Resolver; + MockERC1155 dummy1155; + + function setUp() external { + deployV1Fixture(); + deployV2Fixture(); + dummy1155 = new MockERC1155(); + ensV1Resolver = new ENSV1Resolver(ensV1, batchGatewayProvider); + migratedRegistryImpl = new WrapperRegistry( + nameWrapper, + verifiableFactory, + address(ensV1Resolver), + datastore, + hcaFactory, + metadata + ); + migrationController = new LockedMigrationController( + ethRegistry, + nameWrapper, + verifiableFactory, + address(migratedRegistryImpl) + ); + ethRegistry.grantRootRoles(RegistryRolesLib.ROLE_REGISTRAR, address(migrationController)); } - function getData(uint256 id) external view returns (address, uint32, uint64) { - return (owners[id], fuses[id], expiries[id]); + function _makeData(bytes memory name) internal view returns (IWrapperRegistry.Data memory) { + return + IWrapperRegistry.Data({ + label: NameCoder.firstLabel(name), + owner: user, + resolver: address(1), + salt: uint256(keccak256(abi.encode(name, block.timestamp))) + }); } - function setFuses(bytes32 node, uint16 fusesToBurn) external returns (uint32) { - uint256 tokenId = uint256(node); - fuses[tokenId] = fuses[tokenId] | fusesToBurn; - return fuses[tokenId]; + function test_constructor() external view { + assertEq(address(migrationController.NAME_WRAPPER()), address(nameWrapper), "NAME_WRAPPER"); + assertEq( + address(migrationController.VERIFIABLE_FACTORY()), + address(verifiableFactory), + "VERIFIABLE_FACTORY" + ); + assertEq( + migrationController.MIGRATED_REGISTRY_IMPL(), + address(migratedRegistryImpl), + "MIGRATED_REGISTRY_IMPL" + ); } - function setResolver(bytes32 node, address resolver) external { - uint256 tokenId = uint256(node); - resolvers[tokenId] = resolver; + function test_supportsInterface() external view { + assertTrue(migrationController.supportsInterface(type(IERC165).interfaceId), "IERC165"); + assertTrue( + migrationController.supportsInterface(type(IERC1155Receiver).interfaceId), + "IERC1155Receiver" + ); } - function getResolver(uint256 tokenId) external view returns (address) { - return resolvers[tokenId]; + function test_migrate_unauthorizedCaller_finish() external { + vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, user)); + vm.prank(user); + migrationController.finishERC1155Migration( + new uint256[](0), + new IWrapperRegistry.Data[](0) + ); } -} -contract MockRegistryMetadata is IRegistryMetadata { - function tokenUri(uint256) external pure override returns (string memory) { - return ""; + function test_migrate_unauthorizedCaller_transfer() external { + uint256 tokenId = dummy1155.mint(user); + vm.expectRevert( + WrappedErrorLib.wrap(abi.encodeWithSelector(UnauthorizedCaller.selector, dummy1155)) + ); + vm.prank(user); + dummy1155.safeTransferFrom(user, address(migrationController), tokenId, 1, ""); // wrong } -} -contract LockedMigrationControllerTest is Test, ERC1155Holder { - LockedMigrationController controller; - MockNameWrapper nameWrapper; - RegistryDatastore datastore; - MockRegistryMetadata metadata; - PermissionedRegistry registry; - VerifiableFactory factory; - MigratedWrappedNameRegistry implementation; - MockHCAFactoryBasic hcaFactory; - - address owner = address(this); - address user = address(0x1234); - address fallbackResolver = address(0); - - string testLabel = "test"; - uint256 testTokenId; - - function setUp() public { - nameWrapper = new MockNameWrapper(); - datastore = new RegistryDatastore(); - metadata = new MockRegistryMetadata(); - hcaFactory = new MockHCAFactoryBasic(); - - // Deploy factory and implementation - factory = new VerifiableFactory(); - - // Setup eth registry - registry = new PermissionedRegistry( - datastore, - hcaFactory, - metadata, - owner, - EACBaseRolesLib.ALL_ROLES + function test_migrate_invalidWrapperRegistryData() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + vm.expectRevert( + WrappedErrorLib.wrap( + abi.encodeWithSelector(MigrationErrors.InvalidWrapperRegistryData.selector) + ) ); - - implementation = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(registry)), - factory, - datastore, - hcaFactory, - metadata, - fallbackResolver + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name, 0)), + 1, + "" // wrong ); + } - controller = new LockedMigrationController( - INameWrapper(address(nameWrapper)), - registry, - factory, - address(implementation) + function test_migrate_invalidArrayLength() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + uint256[] memory ids = new uint256[](1); + uint256[] memory amounts = new uint256[](1); + IWrapperRegistry.Data[] memory mds = new IWrapperRegistry.Data[](1); + ids[0] = uint256(NameCoder.namehash(name, 0)); + amounts[0] = 1; + bytes memory payload = abi.encode(mds); + uint256 fakeLength = 0; + assembly { + mstore(add(payload, 64), fakeLength) // wrong + } + vm.expectRevert( + WrappedErrorLib.wrap( + abi.encodeWithSelector( + IERC1155Errors.ERC1155InvalidArrayLength.selector, + ids.length, + fakeLength + ) + ) ); - - // Grant controller permission to register names - registry.grantRootRoles( - RegistryRolesLib.ROLE_REGISTRAR, - address(controller) + vm.prank(user); + nameWrapper.safeBatchTransferFrom( + user, + address(migrationController), + ids, + amounts, + payload ); - - testTokenId = uint256(keccak256(bytes(testLabel))); } - function test_onERC1155Received_locked_name() public { - // Configure name for locked migration - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER | - RegistryRolesLib.ROLE_SET_SUBREGISTRY, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - bytes4 selector = controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Verify selector returned - assertEq(selector, controller.onERC1155Received.selector, "Should return correct selector"); - - // Confirm migration finalized the name - (, uint32 newFuses, ) = nameWrapper.getData(testTokenId); - assertTrue((newFuses & CANNOT_BURN_FUSES) != 0, "CANNOT_BURN_FUSES should be burnt"); - assertTrue((newFuses & CANNOT_TRANSFER) != 0, "CANNOT_TRANSFER should be burnt"); - assertTrue((newFuses & CANNOT_UNWRAP) != 0, "CANNOT_UNWRAP should be burnt"); - assertTrue((newFuses & CANNOT_SET_RESOLVER) != 0, "CANNOT_SET_RESOLVER should be burnt"); - assertTrue((newFuses & CANNOT_SET_TTL) != 0, "CANNOT_SET_TTL should be burnt"); - assertTrue( - (newFuses & CANNOT_CREATE_SUBDOMAIN) != 0, - "CANNOT_CREATE_SUBDOMAIN should be burnt" + function test_migrate_invalidReceiver() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + IWrapperRegistry.Data memory md = _makeData(name); + md.owner = address(0); + vm.expectRevert( + WrappedErrorLib.wrap( + abi.encodeWithSelector(IERC1155Errors.ERC1155InvalidReceiver.selector, md.owner) + ) + ); + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name, 0)), + 1, + abi.encode(md) ); } - function test_onERC1155Received_roles_based_on_fuses_not_input() public { - // Configure name with resolver permissions retained - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Prepare migration data - the roleBitmap should be ignored completely - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_SUBREGISTRY, // This should be completely ignored - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Get the registered name and check roles - (uint256 registeredTokenId, ) = registry.getNameData(testLabel); - uint256 resource = registry.getResource(registeredTokenId); - uint256 userRoles = registry.roles(resource, user); - - // Confirm roles derived from name configuration - // Since CANNOT_SET_RESOLVER is not burnt, user should have resolver roles - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Should have ROLE_SET_RESOLVER based on fuses" + function test_migrate_nameDataMismatch() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + bytes32 node = NameCoder.namehash(name, 0); + IWrapperRegistry.Data memory md = _makeData(name); + md.label = "wrong"; + vm.expectRevert( + WrappedErrorLib.wrap( + abi.encodeWithSelector(MigrationErrors.NameDataMismatch.selector, node) + ) ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Should have ROLE_SET_RESOLVER_ADMIN based on fuses" + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(node), + 1, + abi.encode(md) ); + } - // 2LDs should NOT have renewal roles (CAN_EXTEND_EXPIRY is masked out to prevent automatic renewal for 2LDs) - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW for 2LDs" + function test_migrate_nameNotLocked() external { + bytes memory name = registerWrappedETH2LD("test", CAN_DO_EVERYTHING); + bytes32 node = NameCoder.namehash(name, 0); + IWrapperRegistry.Data memory md = _makeData(name); + vm.expectRevert( + WrappedErrorLib.wrap( + abi.encodeWithSelector(MigrationErrors.NameNotLocked.selector, node) + ) ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN for 2LDs" + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(node), + 1, + abi.encode(md) ); + } - // Token should NEVER have registrar roles - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" + function test_migrate() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP); + IWrapperRegistry.Data memory md = _makeData(name); + // vm.recordLogs(); + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name, 0)), + 1, + abi.encode(md) + ); + // Vm.Log[] memory logs = vm.getRecordedLogs(); + // console.log("Logs = %s", logs.length); + // for (uint256 i; i < logs.length; ++i) { + // console.log("i = %s", i); + // console.logBytes32(logs[i].topics[0]); + // console.logBytes(logs[i].data); + // console.log(); + // } + // NameRegistered + // SubregistryUpdated + // ResolverUpdated + (uint256 tokenId, IRegistryDatastore.Entry memory e) = ethRegistry.getNameData( + NameCoder.firstLabel(name) + ); + assertEq(ethRegistry.ownerOf(tokenId), md.owner, "owner"); + assertEq(e.resolver, md.resolver, "resolver"); + assertEq( + e.expiry, + ethRegistrarV1.nameExpires(uint256(keccak256(bytes(NameCoder.firstLabel(name))))), + "expiry" ); + WrapperRegistry subregistry = WrapperRegistry(address(e.subregistry)); assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" + ERC165Checker.supportsInterface( + address(subregistry), + type(IWrapperRegistry).interfaceId + ), + "IWrapperRegistry" ); - - // Should NOT have the role from input data assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_SUBREGISTRY) == 0, - "Should NOT have ROLE_SET_SUBREGISTRY from input" + subregistry.hasRootRoles(RegistryRolesLib.ROLE_REGISTRAR, md.owner), + "ROLE_REGISTRAR" ); - } - - function test_Revert_onERC1155Received_not_locked() public { - // Configure name that doesn't qualify for locked migration - uint32 unlockedFuses = IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, unlockedFuses, uint64(block.timestamp + 86400)); - - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Migration should fail for unlocked names - vm.expectRevert(abi.encodeWithSelector(LockedNamesLib.NameNotLocked.selector, testTokenId)); - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - } - - function test_name_with_cannot_burn_fuses_can_migrate() public { - // Configure name with fuses that are permanently frozen - this should now be allowed to migrate - uint32 fuses = CANNOT_UNWRAP | CANNOT_BURN_FUSES | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - nameWrapper.setFuseData(testTokenId, fuses, uint64(block.timestamp + 86400)); - - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, // Note: only regular roles, no admin roles expected - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Migration should now succeed for names with CANNOT_BURN_FUSES (should not revert) - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - } - - function test_Revert_token_id_mismatch() public { - // Setup locked name - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Use wrong label that doesn't match tokenId - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName("wronglabel"), // This won't match testTokenId - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Should revert due to token ID mismatch - uint256 expectedTokenId = uint256(keccak256(bytes("wronglabel"))); - vm.expectRevert( - abi.encodeWithSelector( - LockedMigrationController.TokenIdMismatch.selector, - testTokenId, - expectedTokenId - ) + assertEq(address(subregistry.NAME_WRAPPER()), address(nameWrapper), "NAME_WRAPPER"); + assertEq( + address(subregistry.VERIFIABLE_FACTORY()), + address(verifiableFactory), + "VERIFIABLE_FACTORY" ); - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - } - - function test_Revert_unauthorized_caller() public { - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call from wrong address (not nameWrapper) - vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, address(this))); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - } - - function test_onERC1155BatchReceived() public { - // Setup multiple locked names - string[] memory labels = new string[](3); - labels[0] = "test1"; - labels[1] = "test2"; - labels[2] = "test3"; - - uint256[] memory tokenIds = new uint256[](3); - MigrationData[] memory migrationDataArray = new MigrationData[](3); - - for (uint256 i = 0; i < 3; i++) { - tokenIds[i] = uint256(keccak256(bytes(labels[i]))); - - // Setup locked name (CANNOT_BURN_FUSES not set) - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(tokenIds[i], lockedFuses, uint64(block.timestamp + 86400)); - - // DNS encode each label as .eth domain - bytes memory dnsEncodedName; - if (i == 0) { - dnsEncodedName = NameCoder.ethName("test1"); - } else if (i == 1) { - dnsEncodedName = NameCoder.ethName("test2"); - } else { - dnsEncodedName = NameCoder.ethName("test3"); - } - - migrationDataArray[i] = MigrationData({ - transferData: TransferData({ - dnsEncodedName: dnsEncodedName, - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(uint160(0xABCD + i)), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400 * (i + 1)) - }), - salt: uint256(keccak256(abi.encodePacked(labels[i], block.timestamp, i))) - }); - } - - bytes memory data = abi.encode(migrationDataArray); - uint256[] memory amounts = new uint256[](3); - amounts[0] = amounts[1] = amounts[2] = 1; - - // Call batch receive - vm.prank(address(nameWrapper)); - bytes4 selector = controller.onERC1155BatchReceived(owner, owner, tokenIds, amounts, data); - assertEq( - selector, - controller.onERC1155BatchReceived.selector, - "Should return correct selector" + subregistry.MIGRATED_REGISTRY_IMPL(), + address(migratedRegistryImpl), + "MIGRATED_REGISTRY_IMPL" ); + } - // Verify all names were processed with all fuses burnt - for (uint256 i = 0; i < 3; i++) { - (, uint32 newFuses, ) = nameWrapper.getData(tokenIds[i]); - assertTrue((newFuses & CANNOT_BURN_FUSES) != 0, "CANNOT_BURN_FUSES should be burnt"); - assertTrue((newFuses & CANNOT_TRANSFER) != 0, "CANNOT_TRANSFER should be burnt"); - assertTrue((newFuses & CANNOT_UNWRAP) != 0, "CANNOT_UNWRAP should be burnt"); - assertTrue( - (newFuses & CANNOT_SET_RESOLVER) != 0, - "CANNOT_SET_RESOLVER should be burnt" + function test_migrateBatch(uint8 count) external { + vm.assume(count < 5); + uint256[] memory ids = new uint256[](count); + uint256[] memory amounts = new uint256[](count); + IWrapperRegistry.Data[] memory mds = new IWrapperRegistry.Data[](count); + for (uint256 i; i < count; ++i) { + bytes memory name = registerWrappedETH2LD(_label(i), CANNOT_UNWRAP); + IWrapperRegistry.Data memory md = _makeData(name); + md.resolver = address(uint160(i)); + mds[i] = md; + ids[i] = uint256(NameCoder.namehash(name, 0)); + amounts[i] = 1; + } + vm.prank(user); + nameWrapper.safeBatchTransferFrom( + user, + address(migrationController), + ids, + amounts, + abi.encode(mds) + ); + for (uint256 i; i < count; ++i) { + (uint256 tokenId, IRegistryDatastore.Entry memory e) = ethRegistry.getNameData( + _label(i) ); - assertTrue((newFuses & CANNOT_SET_TTL) != 0, "CANNOT_SET_TTL should be burnt"); + assertEq(ethRegistry.ownerOf(tokenId), user, "owner"); + assertEq(e.resolver, address(uint160(i)), "resolver"); assertTrue( - (newFuses & CANNOT_CREATE_SUBDOMAIN) != 0, - "CANNOT_CREATE_SUBDOMAIN should be burnt" + ERC165Checker.supportsInterface( + address(e.subregistry), + type(IWrapperRegistry).interfaceId + ), + "IWrapperRegistry" ); } } - function test_subregistry_creation() public { - // Setup locked name (CANNOT_BURN_FUSES not set) - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Prepare migration data with unique salt - uint256 saltData = uint256(keccak256(abi.encodePacked(testLabel, uint256(999)))); - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: saltData - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Verify a subregistry was created - address actualSubregistry = address(registry.getSubregistry(testLabel)); - assertTrue(actualSubregistry != address(0), "Subregistry should be created"); - - // Verify it's a proxy pointing to our implementation - // The factory creates a proxy, so we can verify it's pointing to the right implementation - MigratedWrappedNameRegistry migratedRegistry = MigratedWrappedNameRegistry( - actualSubregistry + function test_migrateBatch_lastOneWrong(uint8 count) external { + vm.assume(count > 1 && count < 5); + uint256[] memory ids = new uint256[](count); + uint256[] memory amounts = new uint256[](count); + IWrapperRegistry.Data[] memory mds = new IWrapperRegistry.Data[](count); + for (uint256 i; i < count; ++i) { + bytes memory name = registerWrappedETH2LD( + _label(i), + i == count - 1 ? CAN_DO_EVERYTHING : CANNOT_UNWRAP + ); + IWrapperRegistry.Data memory md = _makeData(name); + mds[i] = md; + ids[i] = uint256(NameCoder.namehash(name, 0)); + amounts[i] = 1; + } + vm.expectRevert( + WrappedErrorLib.wrap( + abi.encodeWithSelector(MigrationErrors.NameNotLocked.selector, ids[count - 1]) + ) ); - assertEq( - migratedRegistry.parentDnsEncodedName(), - "\x04test\x03eth\x00", - "Should have correct parent DNS name" + vm.prank(user); + nameWrapper.safeBatchTransferFrom( + user, + address(migrationController), + ids, + amounts, + abi.encode(mds) ); } - // Comprehensive fuse→role mapping tests + function test_migrate_lockedResolver() external { + bytes memory name = registerWrappedETH2LD("test", CAN_DO_EVERYTHING); + bytes32 node = NameCoder.namehash(name, 0); + IWrapperRegistry.Data memory md = _makeData(name); - function test_fuse_role_mapping_no_fuses_burnt() public { - // Setup locked name with only CANNOT_UNWRAP (no other fuses burnt) - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); + address frozenResolver = address(2); + vm.startPrank(user); + nameWrapper.setResolver(node, frozenResolver); + nameWrapper.setFuses(node, uint16(CANNOT_UNWRAP | CANNOT_SET_RESOLVER)); + vm.stopPrank(); + assertNotEq(md.resolver, frozenResolver, "unfrozen"); - // Prepare migration data - incoming roleBitmap should be ignored - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_SUBREGISTRY, // This should be ignored - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Get the registered name and check roles - (uint256 registeredTokenId, ) = registry.getNameData(testLabel); - uint256 resource = registry.getResource(registeredTokenId); - uint256 userRoles = registry.roles(resource, user); - - // 2LDs should NOT have renewal roles even when no additional fuses are burnt (CAN_EXTEND_EXPIRY is masked out to prevent automatic renewal for 2LDs) - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW for 2LDs" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN for 2LDs" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Should have ROLE_SET_RESOLVER" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Should have ROLE_SET_RESOLVER_ADMIN" + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(node), + 1, + abi.encode(md) ); - // Token should NEVER have registrar roles - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" - ); + assertEq(findResolver(name), frozenResolver, "frozen"); + } - // Verify incoming roleBitmap was ignored - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_SUBREGISTRY) == 0, - "Should NOT have ROLE_SET_SUBREGISTRY from incoming data" + function test_migrate_lockedTransfer() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP | CANNOT_TRANSFER); + bytes32 node = NameCoder.namehash(name, 0); + IWrapperRegistry.Data memory md = _makeData(name); + + vm.expectRevert(abi.encodeWithSelector(OperationProhibited.selector, node)); + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(node), + 1, + abi.encode(md) ); } - function test_fuse_role_mapping_no_extend_expiry_fuse() public { - // Setup locked name WITHOUT CAN_EXTEND_EXPIRY fuse - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); + function test_migrate_lockedExpiry() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP | CAN_EXTEND_EXPIRY); + IWrapperRegistry.Data memory md = _makeData(name); - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: 0, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Get the registered name and check roles - (uint256 registeredTokenId, ) = registry.getNameData(testLabel); - uint256 resource = registry.getResource(registeredTokenId); - uint256 userRoles = registry.roles(resource, user); - - // Should NOT have renewal roles since CAN_EXTEND_EXPIRY is not set - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW without CAN_EXTEND_EXPIRY" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN without CAN_EXTEND_EXPIRY" - ); - // Should have resolver roles since CANNOT_SET_RESOLVER is not set - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Should have ROLE_SET_RESOLVER" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Should have ROLE_SET_RESOLVER_ADMIN" + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name, 0)), + 1, + abi.encode(md) ); + (uint256 tokenId, ) = ethRegistry.getNameData(NameCoder.firstLabel(name)); + assertFalse(ethRegistry.hasRoles(tokenId, RegistryRolesLib.ROLE_RENEW, user)); } - function test_fuse_role_mapping_resolver_fuse_burnt() public { - // Setup locked name with CANNOT_SET_RESOLVER already burnt - uint32 lockedFuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); + function test_migrate_lockedChildren() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN); + IWrapperRegistry.Data memory md = _makeData(name); - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER | - RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN, // Should be ignored - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Get the registered name and check roles - (uint256 registeredTokenId, ) = registry.getNameData(testLabel); - uint256 resource = registry.getResource(registeredTokenId); - uint256 userRoles = registry.roles(resource, user); - - // 2LDs should NOT have renewal roles even when CANNOT_CREATE_SUBDOMAIN is not burnt (CAN_EXTEND_EXPIRY is masked out to prevent automatic renewal for 2LDs) - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW for 2LDs" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN for 2LDs" + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name, 0)), + 1, + abi.encode(md) ); + (uint256 tokenId, ) = ethRegistry.getNameData(NameCoder.firstLabel(name)); + assertFalse(ethRegistry.hasRoles(tokenId, RegistryRolesLib.ROLE_REGISTRAR, user)); + } - // Token should NEVER have registrar roles - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" - ); + function test_migrate_lockedFuses() external { + bytes memory name = registerWrappedETH2LD("test", CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN); + IWrapperRegistry.Data memory md = _makeData(name); - // Should NOT have resolver roles since CANNOT_SET_RESOLVER is burnt - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER) == 0, - "Should NOT have ROLE_SET_RESOLVER" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) == 0, - "Should NOT have ROLE_SET_RESOLVER_ADMIN" + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name, 0)), + 1, + abi.encode(md) ); + (uint256 tokenId, ) = ethRegistry.getNameData(NameCoder.firstLabel(name)); + assertFalse(ethRegistry.hasRoles(tokenId, RegistryRolesLib.ROLE_REGISTRAR, user)); } - function test_fuse_role_mapping_cannot_create_subdomain_burnt() public { - // Setup locked name with CANNOT_CREATE_SUBDOMAIN burnt - uint32 lockedFuses = CANNOT_UNWRAP | - CANNOT_CREATE_SUBDOMAIN | - IS_DOT_ETH | - CAN_EXTEND_EXPIRY; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_REGISTRAR | RegistryRolesLib.ROLE_REGISTRAR_ADMIN, // Should be ignored - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Get the registered name and check roles - (uint256 registeredTokenId, ) = registry.getNameData(testLabel); - uint256 resource = registry.getResource(registeredTokenId); - uint256 userRoles = registry.roles(resource, user); - - // 2LDs should NOT have renewal roles (CAN_EXTEND_EXPIRY is masked out to prevent automatic renewal for 2LDs) but should have resolver roles - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW for 2LDs" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN for 2LDs" - ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Should have ROLE_SET_RESOLVER" + function test_migrate_emancipatedChildren() external { + bytes memory name2 = registerWrappedETH2LD("test", CANNOT_UNWRAP); + bytes memory name3 = createWrappedChild( + name2, + "3ld", + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Should have ROLE_SET_RESOLVER_ADMIN" + bytes memory name3unmigrated = createWrappedChild( + name2, + "unmigrated3ld", + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL ); - // Should NOT have registrar roles since CANNOT_CREATE_SUBDOMAIN is burnt - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Should NOT have ROLE_REGISTRAR when subdomain creation disabled" + // migrate 2LD + IWrapperRegistry.Data memory data2 = _makeData(name2); + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(migrationController), + uint256(NameCoder.namehash(name2, 0)), + 1, + abi.encode(data2) ); - assertTrue( - (userRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Should NOT have ROLE_REGISTRAR_ADMIN when subdomain creation disabled" + (uint256 tokenId2, IRegistryDatastore.Entry memory entry2) = ethRegistry.getNameData( + NameCoder.firstLabel(name2) ); + assertEq(ethRegistry.ownerOf(tokenId2), data2.owner, "owner2"); - // Verify incoming roleBitmap was ignored + IWrapperRegistry registry2 = IWrapperRegistry(address(entry2.subregistry)); assertTrue( - (userRoles & RegistryRolesLib.ROLE_SET_SUBREGISTRY) == 0, - "Should NOT have ROLE_SET_SUBREGISTRY from incoming data" + ERC165Checker.supportsInterface(address(registry2), type(IWrapperRegistry).interfaceId), + "registry2" ); - } - - function test_fuses_burnt_after_migration_completes() public { - // Setup locked name (CANNOT_BURN_FUSES not set so migration can proceed) - uint32 initialFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, initialFuses, uint64(block.timestamp + 86400)); - - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_SUBREGISTRY, // Should be ignored - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - // Verify that ALL required fuses are burnt (migration completed, then fuses burnt) - (, uint32 finalFuses, ) = nameWrapper.getData(testTokenId); - - // Check that all required fuses are burnt - assertTrue((finalFuses & CANNOT_UNWRAP) != 0, "CANNOT_UNWRAP should remain burnt"); - assertTrue( - (finalFuses & CANNOT_BURN_FUSES) != 0, - "CANNOT_BURN_FUSES should be burnt after migration" - ); - assertTrue( - (finalFuses & CANNOT_TRANSFER) != 0, - "CANNOT_TRANSFER should be burnt after migration" + // migrate 3LD + IWrapperRegistry.Data memory data3 = _makeData(name3); + vm.prank(user); + nameWrapper.safeTransferFrom( + user, + address(entry2.subregistry), + uint256(NameCoder.namehash(name3, 0)), + 1, + abi.encode(data3) ); - assertTrue( - (finalFuses & CANNOT_SET_RESOLVER) != 0, - "CANNOT_SET_RESOLVER should be burnt after migration" - ); - assertTrue( - (finalFuses & CANNOT_SET_TTL) != 0, - "CANNOT_SET_TTL should be burnt after migration" + (uint256 tokenId3, IRegistryDatastore.Entry memory entry3) = registry2.getNameData( + NameCoder.firstLabel(name3) ); - assertTrue( - (finalFuses & CANNOT_CREATE_SUBDOMAIN) != 0, - "CANNOT_CREATE_SUBDOMAIN should be burnt after migration" - ); - - // Verify name was successfully migrated despite all fuses being burnt after - (uint256 registeredTokenId, ) = registry.getNameData(testLabel); - assertTrue(registeredTokenId != 0, "Name should be successfully registered"); - } - - function test_Revert_invalid_non_eth_name() public { - // Setup locked name without IS_DOT_ETH fuse (not a .eth domain) - uint32 lockedFuses = CANNOT_UNWRAP; // Missing IS_DOT_ETH - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); + assertEq(findResolver(name3), data3.resolver, "resolver3"); + assertEq(registry2.ownerOf(tokenId3), data3.owner, "owner3"); - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_SUBREGISTRY, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Should revert because IS_DOT_ETH fuse is not set - vm.expectRevert(abi.encodeWithSelector(LockedNamesLib.NotDotEthName.selector, testTokenId)); - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - } - - function test_subregistry_owner_roles() public { - // Setup locked name - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Prepare migration data with user as owner - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, "owner_test"))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Get the registered name and check subregistry owner - IRegistry subregistry = registry.getSubregistry(testLabel); - - // Verify the user is the owner of the subregistry with only UPGRADE roles - IPermissionedRegistry subRegistry = IPermissionedRegistry(address(subregistry)); - - // The user should only have UPGRADE and UPGRADE_ADMIN roles on the subregistry - // ROLE_UPGRADE = 1 << 20, ROLE_UPGRADE_ADMIN = ROLE_UPGRADE << 128 - uint256 ROLE_UPGRADE = 1 << 20; - uint256 ROLE_UPGRADE_ADMIN = ROLE_UPGRADE << 128; - uint256 upgradeRoles = ROLE_UPGRADE | ROLE_UPGRADE_ADMIN; + IWrapperRegistry registry3 = IWrapperRegistry(address(entry3.subregistry)); assertTrue( - subRegistry.hasRootRoles(upgradeRoles, user), - "User should have UPGRADE roles on subregistry" + ERC165Checker.supportsInterface(address(registry3), type(IWrapperRegistry).interfaceId), + "registry3" ); - } - - function test_freezeName_clears_resolver_when_fuse_not_set() public { - // Setup locked name with CANNOT_SET_RESOLVER fuse NOT set - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - // Set an initial resolver on the name - address initialResolver = address(0x9999); - nameWrapper.setInitialResolver(testTokenId, initialResolver); - - // Verify resolver is initially set - assertEq( - nameWrapper.getResolver(testTokenId), - initialResolver, - "Initial resolver should be set" + // check migrated 3LD child + vm.expectRevert( + abi.encodeWithSelector( + IStandardRegistry.NameAlreadyRegistered.selector, + NameCoder.firstLabel(name3) + ) ); - - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Verify resolver was cleared to address(0) - assertEq( - nameWrapper.getResolver(testTokenId), + vm.prank(user); + registry2.register( + NameCoder.firstLabel(name3), + user, + IRegistry(address(0)), address(0), - "Resolver should be cleared to address(0)" + 0, + uint64(block.timestamp + 1000) ); - // Verify CANNOT_SET_RESOLVER fuse was burned - (, uint32 newFuses, ) = nameWrapper.getData(testTokenId); - assertTrue( - (newFuses & CANNOT_SET_RESOLVER) != 0, - "CANNOT_SET_RESOLVER should be burnt after migration" + // check unmigrated 3LD child + vm.expectRevert( + abi.encodeWithSelector(MigrationErrors.NameNotMigrated.selector, name3unmigrated) ); - } - - function test_freezeName_preserves_resolver_when_fuse_already_set() public { - // Setup locked name with CANNOT_SET_RESOLVER fuse already set - uint32 lockedFuses = CANNOT_UNWRAP | IS_DOT_ETH | CANNOT_SET_RESOLVER; - nameWrapper.setFuseData(testTokenId, lockedFuses, uint64(block.timestamp + 86400)); - - // Set an initial resolver on the name - address initialResolver = address(0x8888); - nameWrapper.setInitialResolver(testTokenId, initialResolver); - - // Verify resolver is initially set - assertEq( - nameWrapper.getResolver(testTokenId), - initialResolver, - "Initial resolver should be set" + vm.prank(user); + registry2.register( + NameCoder.firstLabel(name3unmigrated), + user, + IRegistry(address(0)), + address(0), + 0, + uint64(block.timestamp + 1000) ); + assertEq(findResolver(name3unmigrated), address(ensV1Resolver), "unmigratedResolver"); + } - // Prepare migration data - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.ethName(testLabel), - owner: user, - subregistry: address(0), // Will be created by factory - resolver: address(0xABCD), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - salt: uint256(keccak256(abi.encodePacked(testLabel, block.timestamp))) - }); - - bytes memory data = abi.encode(migrationData); - - // Call onERC1155Received - vm.prank(address(nameWrapper)); - controller.onERC1155Received(owner, owner, testTokenId, 1, data); - - // Verify resolver remains unchanged (since fuse was already set) - assertEq( - nameWrapper.getResolver(testTokenId), - initialResolver, - "Resolver should be preserved when fuse already set" - ); + function _label(uint256 i) internal pure returns (string memory) { + return string.concat("test", vm.toString(i)); + } +} - // Verify CANNOT_SET_RESOLVER fuse remains set - (, uint32 newFuses, ) = nameWrapper.getData(testTokenId); - assertTrue( - (newFuses & CANNOT_SET_RESOLVER) != 0, - "CANNOT_SET_RESOLVER should remain burnt" - ); +contract MockERC1155 is ERC1155 { + uint256 _id; + constructor() ERC1155("") {} + function mint(address to) external returns (uint256) { + _mint(to, _id, 1, ""); + return _id++; } } diff --git a/contracts/test/unit/migration/libraries/LockedNamesLib.t.sol b/contracts/test/unit/migration/libraries/LockedNamesLib.t.sol deleted file mode 100644 index 39ab1ba8..00000000 --- a/contracts/test/unit/migration/libraries/LockedNamesLib.t.sol +++ /dev/null @@ -1,1201 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.13; - -// solhint-disable no-console, private-vars-leading-underscore, state-visibility, func-name-mixedcase, namechain/ordering, one-contract-per-file - -import {Test, Vm} from "forge-std/Test.sol"; - -import { - INameWrapper, - CANNOT_UNWRAP, - CANNOT_BURN_FUSES, - CANNOT_TRANSFER, - CANNOT_SET_RESOLVER, - CANNOT_SET_TTL, - CANNOT_CREATE_SUBDOMAIN, - IS_DOT_ETH, - CAN_EXTEND_EXPIRY, - PARENT_CANNOT_CONTROL -} from "@ens/contracts/wrapper/INameWrapper.sol"; -import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; - -import {RegistryRolesLib} from "~src/registry/libraries/RegistryRolesLib.sol"; -import {LockedNamesLib} from "~src/migration/libraries/LockedNamesLib.sol"; - -contract MockNameWrapper { - mapping(uint256 tokenId => uint32 fuses) public fuses; - mapping(uint256 tokenId => uint64 expiry) public expiries; - mapping(uint256 tokenId => address owner) public owners; - mapping(uint256 tokenId => address resolver) public resolvers; - - event SetResolver(bytes32 indexed node, address resolver); - event SetFuses(bytes32 indexed node, uint16 fusesToBurn); - - function setFuseData(uint256 tokenId, uint32 _fuses, uint64 _expiry) external { - fuses[tokenId] = _fuses; - expiries[tokenId] = _expiry; - } - - function setInitialResolver(uint256 tokenId, address resolver) external { - resolvers[tokenId] = resolver; - } - - function getData(uint256 id) external view returns (address, uint32, uint64) { - return (owners[id], fuses[id], expiries[id]); - } - - function setFuses(bytes32 node, uint16 fusesToBurn) external returns (uint32) { - uint256 tokenId = uint256(node); - fuses[tokenId] = fuses[tokenId] | fusesToBurn; - emit SetFuses(node, fusesToBurn); - return fuses[tokenId]; - } - - function setResolver(bytes32 node, address resolver) external { - uint256 tokenId = uint256(node); - resolvers[tokenId] = resolver; - emit SetResolver(node, resolver); - } - - function getResolver(uint256 tokenId) external view returns (address) { - return resolvers[tokenId]; - } -} - -contract LibLockedNamesWrapper { - function validateLockedName(uint32 fuses, uint256 tokenId) external pure { - LockedNamesLib.validateLockedName(fuses, tokenId); - } - - function validateEmancipatedName(uint32 fuses, uint256 tokenId) external pure { - LockedNamesLib.validateEmancipatedName(fuses, tokenId); - } - - function validateIsDotEth2LD(uint32 fuses, uint256 tokenId) external pure { - LockedNamesLib.validateIsDotEth2LD(fuses, tokenId); - } -} - -contract LockedNamesLibTest is Test { - MockNameWrapper nameWrapper; - VerifiableFactory factory; - LibLockedNamesWrapper wrapper; - - uint256 testTokenId = 0x1234567890abcdef; - - function setUp() public { - nameWrapper = new MockNameWrapper(); - factory = new VerifiableFactory(); - wrapper = new LibLockedNamesWrapper(); - } - - function test_freezeName_clears_resolver_when_fuse_not_set() public { - // Setup name with CANNOT_SET_RESOLVER fuse NOT set - uint32 initialFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, initialFuses, uint64(block.timestamp + 86400)); - - // Set an initial resolver - address initialResolver = address(0x9999); - nameWrapper.setInitialResolver(testTokenId, initialResolver); - - // Verify resolver is initially set - assertEq( - nameWrapper.getResolver(testTokenId), - initialResolver, - "Initial resolver should be set" - ); - - // Record logs to verify both events are emitted - vm.recordLogs(); - - // Call freezeName - LockedNamesLib.freezeName(INameWrapper(address(nameWrapper)), testTokenId, initialFuses); - - // Get recorded logs and verify both events were emitted - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(logs.length, 2, "Should emit exactly 2 events"); - - // Verify setResolver event - assertEq( - logs[0].topics[0], - keccak256("SetResolver(bytes32,address)"), - "First event should be SetResolver" - ); - assertEq( - logs[0].topics[1], - bytes32(testTokenId), - "SetResolver event should have correct tokenId" - ); - address resolverFromEvent = abi.decode(logs[0].data, (address)); - assertEq( - resolverFromEvent, - address(0), - "SetResolver event should set resolver to address(0)" - ); - - // Verify setFuses event - assertEq( - logs[1].topics[0], - keccak256("SetFuses(bytes32,uint16)"), - "Second event should be SetFuses" - ); - assertEq( - logs[1].topics[1], - bytes32(testTokenId), - "SetFuses event should have correct tokenId" - ); - uint16 fusesFromEvent = abi.decode(logs[1].data, (uint16)); - assertEq( - fusesFromEvent, - uint16(LockedNamesLib.FUSES_TO_BURN), - "SetFuses event should burn correct fuses" - ); - - // Verify resolver was cleared - assertEq( - nameWrapper.getResolver(testTokenId), - address(0), - "Resolver should be cleared to address(0)" - ); - - // Verify all fuses were burned - (, uint32 finalFuses, ) = nameWrapper.getData(testTokenId); - assertTrue( - (finalFuses & LockedNamesLib.FUSES_TO_BURN) == LockedNamesLib.FUSES_TO_BURN, - "All fuses should be burned" - ); - } - - function test_freezeName_preserves_resolver_when_fuse_already_set() public { - // Setup name with CANNOT_SET_RESOLVER fuse already set - uint32 initialFuses = CANNOT_UNWRAP | IS_DOT_ETH | CANNOT_SET_RESOLVER; - nameWrapper.setFuseData(testTokenId, initialFuses, uint64(block.timestamp + 86400)); - - // Set an initial resolver - address initialResolver = address(0x8888); - nameWrapper.setInitialResolver(testTokenId, initialResolver); - - // Verify resolver is initially set - assertEq( - nameWrapper.getResolver(testTokenId), - initialResolver, - "Initial resolver should be set" - ); - - // Record logs to verify only setFuses event is emitted - vm.recordLogs(); - - // Call freezeName - LockedNamesLib.freezeName(INameWrapper(address(nameWrapper)), testTokenId, initialFuses); - - // Get recorded logs and verify only setFuses event was emitted - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(logs.length, 1, "Should emit exactly 1 event (setFuses only)"); - - // Verify setFuses event - assertEq( - logs[0].topics[0], - keccak256("SetFuses(bytes32,uint16)"), - "Event should be SetFuses" - ); - assertEq( - logs[0].topics[1], - bytes32(testTokenId), - "SetFuses event should have correct tokenId" - ); - uint16 fusesFromEvent = abi.decode(logs[0].data, (uint16)); - assertEq( - fusesFromEvent, - uint16(LockedNamesLib.FUSES_TO_BURN), - "SetFuses event should burn correct fuses" - ); - - // Verify resolver remains unchanged - assertEq( - nameWrapper.getResolver(testTokenId), - initialResolver, - "Resolver should be preserved when fuse already set" - ); - - // Verify all fuses were burned - (, uint32 finalFuses, ) = nameWrapper.getData(testTokenId); - assertTrue( - (finalFuses & LockedNamesLib.FUSES_TO_BURN) == LockedNamesLib.FUSES_TO_BURN, - "All fuses should be burned" - ); - } - - function test_freezeName_with_zero_resolver_when_fuse_not_set() public { - // Setup name with CANNOT_SET_RESOLVER fuse NOT set - uint32 initialFuses = CANNOT_UNWRAP | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, initialFuses, uint64(block.timestamp + 86400)); - - // Initial resolver is already address(0) (default) - assertEq( - nameWrapper.getResolver(testTokenId), - address(0), - "Initial resolver should be address(0)" - ); - - // Record logs to verify both events are emitted - vm.recordLogs(); - - // Call freezeName - LockedNamesLib.freezeName(INameWrapper(address(nameWrapper)), testTokenId, initialFuses); - - // Get recorded logs and verify both events were emitted - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(logs.length, 2, "Should emit exactly 2 events"); - - // Verify setResolver event (should still be called even if resolver is already address(0)) - assertEq( - logs[0].topics[0], - keccak256("SetResolver(bytes32,address)"), - "First event should be SetResolver" - ); - assertEq( - logs[0].topics[1], - bytes32(testTokenId), - "SetResolver event should have correct tokenId" - ); - address resolverFromEvent = abi.decode(logs[0].data, (address)); - assertEq( - resolverFromEvent, - address(0), - "SetResolver event should set resolver to address(0)" - ); - - // Verify setFuses event - assertEq( - logs[1].topics[0], - keccak256("SetFuses(bytes32,uint16)"), - "Second event should be SetFuses" - ); - assertEq( - logs[1].topics[1], - bytes32(testTokenId), - "SetFuses event should have correct tokenId" - ); - uint16 fusesFromEvent = abi.decode(logs[1].data, (uint16)); - assertEq( - fusesFromEvent, - uint16(LockedNamesLib.FUSES_TO_BURN), - "SetFuses event should burn correct fuses" - ); - - // Verify resolver remains address(0) - assertEq( - nameWrapper.getResolver(testTokenId), - address(0), - "Resolver should remain address(0)" - ); - } - - function test_validateLockedName_valid() public pure { - uint32 validFuses = CANNOT_UNWRAP | IS_DOT_ETH; - uint256 tokenId = 0x123; - - // Should not revert for valid locked name - LockedNamesLib.validateLockedName(validFuses, tokenId); - } - - function test_validateLockedName_with_cannot_burn_fuses() public pure { - uint32 validFuses = CANNOT_UNWRAP | CANNOT_BURN_FUSES | IS_DOT_ETH; - uint256 tokenId = 0x123; - - // Should not revert for locked name with CANNOT_BURN_FUSES (previously this would have failed) - LockedNamesLib.validateLockedName(validFuses, tokenId); - } - - function test_Revert_validateLockedName_not_locked() public { - uint32 invalidFuses = IS_DOT_ETH; // Missing CANNOT_UNWRAP - uint256 tokenId = 0x123; - - vm.expectRevert(abi.encodeWithSelector(LockedNamesLib.NameNotLocked.selector, tokenId)); - wrapper.validateLockedName(invalidFuses, tokenId); - } - - function test_validateIsDotEth2LD_valid() public pure { - uint32 validFuses = IS_DOT_ETH | CANNOT_UNWRAP; - uint256 tokenId = 0x123; - - // Should not revert for valid .eth 2LD - LockedNamesLib.validateIsDotEth2LD(validFuses, tokenId); - } - - function test_Revert_validateIsDotEth2LD_not_dot_eth() public { - uint32 invalidFuses = CANNOT_UNWRAP; // Missing IS_DOT_ETH - uint256 tokenId = 0x123; - - vm.expectRevert(abi.encodeWithSelector(LockedNamesLib.NotDotEthName.selector, tokenId)); - wrapper.validateIsDotEth2LD(invalidFuses, tokenId); - } - - function test_generateRoleBitmapsFromFuses_all_permissions() public pure { - // Fuses that allow all permissions - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should include renewal and resolver roles since no restrictive fuses are set - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Token should have ROLE_RENEW_ADMIN" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Token should have ROLE_SET_RESOLVER_ADMIN" - ); - // Should include transfer role since CANNOT_TRANSFER is not set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) != 0, - "Token should have ROLE_CAN_TRANSFER" - ); - // Token should NEVER have registrar roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" - ); - - // SubRegistry should have registrar roles since subdomain creation is allowed - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) != 0, - "SubRegistry should have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should have renewal roles (always granted) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_no_extend_expiry() public pure { - // Fuses without CAN_EXTEND_EXPIRY - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should NOT have renewal roles since CAN_EXTEND_EXPIRY is not set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Token should NOT have ROLE_RENEW" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Token should NOT have ROLE_RENEW_ADMIN" - ); - // Should have resolver roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Token should have ROLE_SET_RESOLVER_ADMIN" - ); - // Should have transfer role since CANNOT_TRANSFER is not set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) != 0, - "Token should have ROLE_CAN_TRANSFER" - ); - // Token should NEVER have registrar roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" - ); - - // SubRegistry should have registrar roles since subdomain creation is allowed - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) != 0, - "SubRegistry should have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should have renewal roles (always granted) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_set_resolver() public pure { - // Fuses with CANNOT_SET_RESOLVER - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY | CANNOT_SET_RESOLVER; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should NOT have resolver roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) == 0, - "Token should NOT have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) == 0, - "Token should NOT have ROLE_SET_RESOLVER_ADMIN" - ); - // Should have renewal roles - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Token should have ROLE_RENEW_ADMIN" - ); - // Token should NEVER have registrar roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" - ); - - // SubRegistry should have registrar roles since subdomain creation is allowed - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) != 0, - "SubRegistry should have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should have renewal roles (always granted) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_create_subdomain() public pure { - // Fuses with CANNOT_CREATE_SUBDOMAIN - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY | CANNOT_CREATE_SUBDOMAIN; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Token should NOT have registrar roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NOT have ROLE_REGISTRAR" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NOT have ROLE_REGISTRAR_ADMIN" - ); - // Should have renewal and resolver roles - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Token should have ROLE_RENEW_ADMIN" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Token should have ROLE_SET_RESOLVER_ADMIN" - ); - // Token should NEVER have registrar roles - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "Token should NEVER have ROLE_REGISTRAR" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "Token should NEVER have ROLE_REGISTRAR_ADMIN" - ); - - // SubRegistry should NOT have registrar roles since subdomain creation is not allowed - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should have renewal roles (always granted) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - // SubRegistry should only have renewal roles when CANNOT_CREATE_SUBDOMAIN is set - uint256 expectedRoles = RegistryRolesLib.ROLE_RENEW | RegistryRolesLib.ROLE_RENEW_ADMIN; - assertEq( - subRegistryRoles, - expectedRoles, - "SubRegistry should only have renewal roles when CANNOT_CREATE_SUBDOMAIN is set" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_burn_fuses_no_admin_roles() public pure { - // Fuses with CANNOT_BURN_FUSES - should grant regular roles but NO admin roles - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY | CANNOT_BURN_FUSES; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should have regular roles but NO admin roles - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Token should NOT have ROLE_RENEW_ADMIN when CANNOT_BURN_FUSES is set" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) == 0, - "Token should NOT have ROLE_SET_RESOLVER_ADMIN when CANNOT_BURN_FUSES is set" - ); - - // SubRegistry should have regular roles but NO admin roles for registrar - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR_ADMIN when CANNOT_BURN_FUSES is set" - ); - // SubRegistry should have renewal roles including admin (not affected by CANNOT_BURN_FUSES) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN (not affected by CANNOT_BURN_FUSES)" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_burn_fuses_with_restrictions() public pure { - // Fuses with CANNOT_BURN_FUSES + restrictive fuses - uint32 fuses = CANNOT_UNWRAP | - IS_DOT_ETH | - CANNOT_BURN_FUSES | - CANNOT_SET_RESOLVER | - CANNOT_CREATE_SUBDOMAIN | - CANNOT_TRANSFER; - // Note: no CAN_EXTEND_EXPIRY - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should NOT have renewal roles (no CAN_EXTEND_EXPIRY) - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Token should NOT have ROLE_RENEW" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Token should NOT have ROLE_RENEW_ADMIN" - ); - - // Should NOT have resolver roles (CANNOT_SET_RESOLVER is set) - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) == 0, - "Token should NOT have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) == 0, - "Token should NOT have ROLE_SET_RESOLVER_ADMIN" - ); - - // SubRegistry should NOT have registrar roles (CANNOT_CREATE_SUBDOMAIN is set) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR_ADMIN" - ); - - // SubRegistry should have renewal roles (always granted) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - - // Token should have no roles when all permissions are restricted - assertEq( - tokenRoles, - 0, - "Token should have no roles when all permissions are restricted" - ); - uint256 expectedSubRegistryRoles = RegistryRolesLib.ROLE_RENEW | - RegistryRolesLib.ROLE_RENEW_ADMIN; - assertEq( - subRegistryRoles, - expectedSubRegistryRoles, - "SubRegistry should only have renewal roles" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_burn_fuses() public pure { - // Fuses with CANNOT_BURN_FUSES - should prevent admin roles - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY | CANNOT_BURN_FUSES; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Token should have regular roles but NO admin roles due to CANNOT_BURN_FUSES - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Token should NOT have ROLE_RENEW_ADMIN when CANNOT_BURN_FUSES is set" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) == 0, - "Token should NOT have ROLE_SET_RESOLVER_ADMIN when CANNOT_BURN_FUSES is set" - ); - - // SubRegistry should have registrar role but NO admin role due to CANNOT_BURN_FUSES - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR_ADMIN when CANNOT_BURN_FUSES is set" - ); - // SubRegistry should have renewal roles (always granted now) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_fuses_control_renewal_roles() public pure { - // Test that fuses directly control renewal roles via CAN_EXTEND_EXPIRY - uint32 fusesWithExpiry = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - uint32 fusesWithoutExpiry = CANNOT_UNWRAP | IS_DOT_ETH; - - // Test with CAN_EXTEND_EXPIRY set (should have renewal roles) - (uint256 tokenRoles1, ) = LockedNamesLib.generateRoleBitmapsFromFuses(fusesWithExpiry); - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_RENEW) != 0, - "Should have ROLE_RENEW when CAN_EXTEND_EXPIRY is set" - ); - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Should have ROLE_RENEW_ADMIN when CAN_EXTEND_EXPIRY is set" - ); - - // Test without CAN_EXTEND_EXPIRY (should NOT have renewal roles) - (uint256 tokenRoles2, ) = LockedNamesLib.generateRoleBitmapsFromFuses(fusesWithoutExpiry); - assertTrue( - (tokenRoles2 & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW without CAN_EXTEND_EXPIRY" - ); - assertTrue( - (tokenRoles2 & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN without CAN_EXTEND_EXPIRY" - ); - } - - function test_generateRoleBitmapsFromFuses_fuses_consistency() public pure { - // Test that the same fuses always produce the same roles - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - - (uint256 tokenRoles1, uint256 subRegistryRoles1) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - (uint256 tokenRoles2, uint256 subRegistryRoles2) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Same fuses should produce identical roles - assertEq(tokenRoles1, tokenRoles2, "Same fuses should produce identical token roles"); - assertEq( - subRegistryRoles1, - subRegistryRoles2, - "Same fuses should produce identical subregistry roles" - ); - - // Should have all expected roles with this fuse configuration - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Should have ROLE_SET_RESOLVER_ADMIN" - ); - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_RENEW) != 0, - "Should have ROLE_RENEW with CAN_EXTEND_EXPIRY" - ); - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Should have ROLE_RENEW_ADMIN with CAN_EXTEND_EXPIRY" - ); - } - - function test_generateRoleBitmapsFromFuses_frozen_fuses_behavior() public pure { - // Test behavior with CANNOT_BURN_FUSES set (fuses are permanently frozen) - uint32 frozenFuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY | CANNOT_BURN_FUSES; - - (uint256 tokenRoles, ) = LockedNamesLib.generateRoleBitmapsFromFuses(frozenFuses); - - // Should have ROLE_RENEW since CAN_EXTEND_EXPIRY is set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "Should have ROLE_RENEW with CAN_EXTEND_EXPIRY" - ); - // Should NOT have ROLE_RENEW_ADMIN because CANNOT_BURN_FUSES is set (fuses are frozen) - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN when CANNOT_BURN_FUSES is set" - ); - } - - function test_generateRoleBitmapsFromFuses_without_can_extend_expiry() public pure { - // Test that no renewal roles are granted when CAN_EXTEND_EXPIRY is not set - uint32 fusesWithoutExpiry = CANNOT_UNWRAP | IS_DOT_ETH; - - (uint256 tokenRoles, ) = LockedNamesLib.generateRoleBitmapsFromFuses(fusesWithoutExpiry); - - // Should NOT have renewal roles when CAN_EXTEND_EXPIRY is not set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW) == 0, - "Should NOT have ROLE_RENEW when CAN_EXTEND_EXPIRY not set" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Should NOT have ROLE_RENEW_ADMIN when CAN_EXTEND_EXPIRY not set" - ); - } - - function test_FUSES_TO_BURN_constant() public pure { - // Verify the FUSES_TO_BURN constant includes all expected fuses including CANNOT_UNWRAP - uint32 expectedFuses = CANNOT_UNWRAP | - CANNOT_BURN_FUSES | - CANNOT_TRANSFER | - CANNOT_SET_RESOLVER | - CANNOT_SET_TTL | - CANNOT_CREATE_SUBDOMAIN; - - assertEq( - LockedNamesLib.FUSES_TO_BURN, - expectedFuses, - "FUSES_TO_BURN should include all expected fuses including CANNOT_UNWRAP" - ); - } - - function test_freezeName_burns_cannot_unwrap_when_not_set() public { - // Setup name with CANNOT_UNWRAP fuse NOT set (emancipated but not locked) - uint32 initialFuses = PARENT_CANNOT_CONTROL | IS_DOT_ETH; - nameWrapper.setFuseData(testTokenId, initialFuses, uint64(block.timestamp + 86400)); - - // Set an initial resolver - address initialResolver = address(0x9999); - nameWrapper.setInitialResolver(testTokenId, initialResolver); - - // Verify CANNOT_UNWRAP is initially not set - (, uint32 currentFuses, ) = nameWrapper.getData(testTokenId); - assertTrue( - (currentFuses & CANNOT_UNWRAP) == 0, - "CANNOT_UNWRAP should not be set initially" - ); - - // Record logs to verify both setResolver and setFuses events are emitted - vm.recordLogs(); - - // Call freezeName - LockedNamesLib.freezeName(INameWrapper(address(nameWrapper)), testTokenId, initialFuses); - - // Get recorded logs and verify both events were emitted - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(logs.length, 2, "Should emit exactly 2 events"); - - // Verify setResolver event - assertEq( - logs[0].topics[0], - keccak256("SetResolver(bytes32,address)"), - "First event should be SetResolver" - ); - assertEq( - logs[0].topics[1], - bytes32(testTokenId), - "SetResolver event should have correct tokenId" - ); - address resolverFromEvent = abi.decode(logs[0].data, (address)); - assertEq( - resolverFromEvent, - address(0), - "SetResolver event should set resolver to address(0)" - ); - - // Verify setFuses event - assertEq( - logs[1].topics[0], - keccak256("SetFuses(bytes32,uint16)"), - "Second event should be SetFuses" - ); - assertEq( - logs[1].topics[1], - bytes32(testTokenId), - "SetFuses event should have correct tokenId" - ); - uint16 fusesFromEvent = abi.decode(logs[1].data, (uint16)); - assertEq( - fusesFromEvent, - uint16(LockedNamesLib.FUSES_TO_BURN), - "SetFuses event should burn all fuses including CANNOT_UNWRAP" - ); - - // Verify resolver was cleared - assertEq( - nameWrapper.getResolver(testTokenId), - address(0), - "Resolver should be cleared to address(0)" - ); - - // Verify all fuses were burned including CANNOT_UNWRAP - (, uint32 finalFuses, ) = nameWrapper.getData(testTokenId); - assertTrue( - (finalFuses & LockedNamesLib.FUSES_TO_BURN) == LockedNamesLib.FUSES_TO_BURN, - "All fuses including CANNOT_UNWRAP should be burned" - ); - assertTrue((finalFuses & CANNOT_UNWRAP) != 0, "CANNOT_UNWRAP should now be set"); - } - - function test_validateEmancipatedName_emancipated_only() public pure { - uint32 emancipatedFuses = PARENT_CANNOT_CONTROL | IS_DOT_ETH; - uint256 tokenId = 0x123; - - // Should not revert for emancipated name - LockedNamesLib.validateEmancipatedName(emancipatedFuses, tokenId); - } - - function test_validateEmancipatedName_emancipated_and_locked() public pure { - uint32 lockedFuses = PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | IS_DOT_ETH; - uint256 tokenId = 0x123; - - // Should not revert for emancipated and locked name - LockedNamesLib.validateEmancipatedName(lockedFuses, tokenId); - } - - function test_validateEmancipatedName_with_cannot_burn_fuses() public pure { - uint32 frozenFuses = PARENT_CANNOT_CONTROL | CANNOT_BURN_FUSES | IS_DOT_ETH; - uint256 tokenId = 0x123; - - // Should not revert for emancipated name with CANNOT_BURN_FUSES (previously this would have failed) - LockedNamesLib.validateEmancipatedName(frozenFuses, tokenId); - } - - function test_validateEmancipatedName_locked_with_cannot_burn_fuses() public pure { - uint32 frozenLockedFuses = PARENT_CANNOT_CONTROL | - CANNOT_UNWRAP | - CANNOT_BURN_FUSES | - IS_DOT_ETH; - uint256 tokenId = 0x123; - - // Should not revert for emancipated and locked name with CANNOT_BURN_FUSES (previously this would have failed) - LockedNamesLib.validateEmancipatedName(frozenLockedFuses, tokenId); - } - - function test_Revert_validateEmancipatedName_not_emancipated() public { - uint32 notEmancipatedFuses = IS_DOT_ETH; // Missing PARENT_CANNOT_CONTROL - uint256 tokenId = 0x123; - - vm.expectRevert( - abi.encodeWithSelector(LockedNamesLib.NameNotEmancipated.selector, tokenId) - ); - wrapper.validateEmancipatedName(notEmancipatedFuses, tokenId); - } - - function test_generateRoleBitmapsFromFuses_cannot_transfer_fuse_set() public pure { - // Fuses with CANNOT_TRANSFER set (transfers disabled) - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY | CANNOT_TRANSFER; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should have renewal and resolver roles - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Token should have ROLE_RENEW_ADMIN" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Token should have ROLE_SET_RESOLVER_ADMIN" - ); - // Should NOT have transfer role since CANNOT_TRANSFER is set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) == 0, - "Token should NOT have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is set" - ); - - // SubRegistry should have registrar roles - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) != 0, - "SubRegistry should have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should have renewal roles - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_transfer_fuse_not_set() public pure { - // Fuses without CANNOT_TRANSFER (transfers allowed) - uint32 fuses = CANNOT_UNWRAP | IS_DOT_ETH | CAN_EXTEND_EXPIRY; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should have renewal and resolver roles - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "Token should have ROLE_RENEW_ADMIN" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) != 0, - "Token should have ROLE_SET_RESOLVER_ADMIN" - ); - // Should have transfer role since CANNOT_TRANSFER is not set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) != 0, - "Token should have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is not set" - ); - - // SubRegistry should have registrar roles - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) != 0, - "SubRegistry should have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should have renewal roles - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_cannot_transfer_with_cannot_burn_fuses() - public - pure - { - // Fuses with both CANNOT_TRANSFER and CANNOT_BURN_FUSES - uint32 fuses = CANNOT_UNWRAP | - IS_DOT_ETH | - CAN_EXTEND_EXPIRY | - CANNOT_TRANSFER | - CANNOT_BURN_FUSES; - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should have renewal role but NOT admin role due to CANNOT_BURN_FUSES - assertTrue((tokenRoles & RegistryRolesLib.ROLE_RENEW) != 0, "Token should have ROLE_RENEW"); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) == 0, - "Token should NOT have ROLE_RENEW_ADMIN when CANNOT_BURN_FUSES is set" - ); - // Should have resolver role but NOT admin role due to CANNOT_BURN_FUSES - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER) != 0, - "Token should have ROLE_SET_RESOLVER" - ); - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN) == 0, - "Token should NOT have ROLE_SET_RESOLVER_ADMIN when CANNOT_BURN_FUSES is set" - ); - // Should NOT have transfer role since CANNOT_TRANSFER is set - assertTrue( - (tokenRoles & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) == 0, - "Token should NOT have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is set" - ); - - // SubRegistry should have registrar role but NOT admin role due to CANNOT_BURN_FUSES - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) != 0, - "SubRegistry should have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR_ADMIN when CANNOT_BURN_FUSES is set" - ); - // SubRegistry should have renewal roles (not affected by CANNOT_BURN_FUSES) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - } - - function test_generateRoleBitmapsFromFuses_all_restrictive_fuses() public pure { - // Test with all restrictive fuses including CANNOT_TRANSFER - uint32 fuses = CANNOT_UNWRAP | - IS_DOT_ETH | - CANNOT_TRANSFER | - CANNOT_SET_RESOLVER | - CANNOT_CREATE_SUBDOMAIN; - // Note: no CAN_EXTEND_EXPIRY - - (uint256 tokenRoles, uint256 subRegistryRoles) = LockedNamesLib - .generateRoleBitmapsFromFuses(fuses); - - // Should have no roles when all restrictive fuses are set - assertEq(tokenRoles, 0, "Token should have no roles when all restrictive fuses are set"); - - // SubRegistry should NOT have registrar roles (CANNOT_CREATE_SUBDOMAIN is set) - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_REGISTRAR_ADMIN) == 0, - "SubRegistry should NOT have ROLE_REGISTRAR_ADMIN" - ); - // SubRegistry should only have renewal roles - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW) != 0, - "SubRegistry should have ROLE_RENEW" - ); - assertTrue( - (subRegistryRoles & RegistryRolesLib.ROLE_RENEW_ADMIN) != 0, - "SubRegistry should have ROLE_RENEW_ADMIN" - ); - - // Verify exact role bitmaps - uint256 expectedSubRegistryRoles = RegistryRolesLib.ROLE_RENEW | - RegistryRolesLib.ROLE_RENEW_ADMIN; - assertEq( - subRegistryRoles, - expectedSubRegistryRoles, - "SubRegistry should only have renewal roles with all restrictive fuses" - ); - } - - function test_generateRoleBitmapsFromFuses_transfer_role_consistency() public pure { - // Test that transfer role is consistently applied based on CANNOT_TRANSFER fuse - - // Test 1: With CANNOT_TRANSFER - should NOT have transfer role - uint32 fusesWithCannotTransfer = CANNOT_UNWRAP | IS_DOT_ETH | CANNOT_TRANSFER; - (uint256 tokenRoles1, ) = LockedNamesLib.generateRoleBitmapsFromFuses( - fusesWithCannotTransfer - ); - assertTrue( - (tokenRoles1 & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) == 0, - "Should NOT have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is set" - ); - - // Test 2: Without CANNOT_TRANSFER - should have transfer role - uint32 fusesWithoutCannotTransfer = CANNOT_UNWRAP | IS_DOT_ETH; - (uint256 tokenRoles2, ) = LockedNamesLib.generateRoleBitmapsFromFuses( - fusesWithoutCannotTransfer - ); - assertTrue( - (tokenRoles2 & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) != 0, - "Should have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is not set" - ); - - // Test 3: With other restrictive fuses but not CANNOT_TRANSFER - should have transfer role - uint32 fusesWithOtherRestrictions = CANNOT_UNWRAP | - IS_DOT_ETH | - CANNOT_SET_RESOLVER | - CANNOT_CREATE_SUBDOMAIN; - (uint256 tokenRoles3, ) = LockedNamesLib.generateRoleBitmapsFromFuses( - fusesWithOtherRestrictions - ); - assertTrue( - (tokenRoles3 & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) != 0, - "Should have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is not set even with other restrictions" - ); - - // Test 4: With CANNOT_TRANSFER and other restrictive fuses - should NOT have transfer role - uint32 fusesWithCannotTransferAndOthers = CANNOT_UNWRAP | - IS_DOT_ETH | - CANNOT_TRANSFER | - CANNOT_SET_RESOLVER; - (uint256 tokenRoles4, ) = LockedNamesLib.generateRoleBitmapsFromFuses( - fusesWithCannotTransferAndOthers - ); - assertTrue( - (tokenRoles4 & RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN) == 0, - "Should NOT have ROLE_CAN_TRANSFER when CANNOT_TRANSFER is set regardless of other restrictions" - ); - } -} diff --git a/contracts/test/unit/registry/MigratedWrappedNameRegistry.t.sol b/contracts/test/unit/registry/MigratedWrappedNameRegistry.t.sol deleted file mode 100644 index cc8cb154..00000000 --- a/contracts/test/unit/registry/MigratedWrappedNameRegistry.t.sol +++ /dev/null @@ -1,2141 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.13; - -// solhint-disable no-console, private-vars-leading-underscore, state-visibility, func-name-mixedcase, namechain/ordering, one-contract-per-file - -import {Test} from "forge-std/Test.sol"; - -import {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; -import {INameWrapper} from "@ens/contracts/wrapper/INameWrapper.sol"; -import { - CANNOT_UNWRAP, - CANNOT_BURN_FUSES, - CANNOT_SET_RESOLVER, - CANNOT_TRANSFER, - CANNOT_SET_TTL, - CANNOT_CREATE_SUBDOMAIN, - CAN_EXTEND_EXPIRY, - PARENT_CANNOT_CONTROL -} from "@ens/contracts/wrapper/INameWrapper.sol"; -import {VerifiableFactory} from "@ensdomains/verifiable-factory/VerifiableFactory.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -import {TransferData, MigrationData} from "~src/migration/types/MigrationTypes.sol"; -import {UnauthorizedCaller} from "~src/CommonErrors.sol"; -import {IPermissionedRegistry} from "~src/registry/interfaces/IPermissionedRegistry.sol"; -import {IRegistry} from "~src/registry/interfaces/IRegistry.sol"; -import {IRegistryDatastore} from "~src/registry/interfaces/IRegistryDatastore.sol"; -import {IRegistryMetadata} from "~src/registry/interfaces/IRegistryMetadata.sol"; -import {IStandardRegistry} from "~src/registry/interfaces/IStandardRegistry.sol"; -import {RegistryRolesLib} from "~src/registry/libraries/RegistryRolesLib.sol"; -import {RegistryDatastore} from "~src/registry/RegistryDatastore.sol"; -import {LibLabel} from "~src/utils/LibLabel.sol"; -import {LockedNamesLib} from "~src/migration/libraries/LockedNamesLib.sol"; -import {ParentNotMigrated, LabelNotMigrated} from "~src/migration/MigrationErrors.sol"; -import {MigratedWrappedNameRegistry} from "~src/registry/MigratedWrappedNameRegistry.sol"; -import {MockHCAFactoryBasic} from "~test/mocks/MockHCAFactoryBasic.sol"; - -contract MockRegistryMetadata is IRegistryMetadata { - function tokenUri(uint256) external pure override returns (string memory) { - return ""; - } -} - -// Simple mock for ethRegistry testing - not a full IPermissionedRegistry implementation -contract MockEthRegistry { - mapping(string label => address registry) private subregistries; - - function setSubregistry(string memory label, address registry) external { - subregistries[label] = registry; - } - - function getSubregistry(string memory label) external view returns (IRegistry) { - return IRegistry(subregistries[label]); - } -} - -contract MockENS { - mapping(bytes32 node => address resolver) private resolvers; - - function setResolver(bytes32 node, address resolverAddress) external { - resolvers[node] = resolverAddress; - } - - function resolver(bytes32 node) external view returns (address) { - return resolvers[node]; - } -} - -contract MockNameWrapper { - mapping(uint256 tokenId => address owner) public owners; - mapping(uint256 tokenId => bool wrapped) public wrapped; - mapping(uint256 tokenId => uint32 fuses) public fuses; - mapping(uint256 tokenId => uint64 expiry) public expiries; - mapping(uint256 tokenId => address resolver) public resolvers; - mapping(bytes32 node => bytes name) public names; - - MockENS public immutable ens; - - constructor(MockENS _ens) { - ens = _ens; - } - - function setName(bytes memory name) external { - names[NameCoder.namehash(name, 0)] = name; - } - - function setOwner(uint256 tokenId, address owner) external { - owners[tokenId] = owner; - } - - function setWrapped(uint256 tokenId, bool _isWrapped) external { - wrapped[tokenId] = _isWrapped; - } - - function setFuseData(uint256 tokenId, uint32 _fuses, uint64 expiry) external { - fuses[tokenId] = _fuses; - expiries[tokenId] = expiry; - } - - function setInitialResolver(uint256 tokenId, address resolver) external { - resolvers[tokenId] = resolver; - } - - function ownerOf(uint256 tokenId) external view returns (address) { - return owners[tokenId]; - } - - function isWrapped(bytes32 node) external view returns (bool) { - return wrapped[uint256(node)]; - } - - function getData(uint256 tokenId) external view returns (address, uint32, uint64) { - return (owners[tokenId], fuses[tokenId], expiries[tokenId]); - } - - function setResolver(bytes32 node, address resolver) external { - uint256 tokenId = uint256(node); - resolvers[tokenId] = resolver; - } - - function getResolver(uint256 tokenId) external view returns (address) { - return resolvers[tokenId]; - } - - function setFuses(bytes32 node, uint16 fusesToBurn) external returns (uint32) { - uint256 tokenId = uint256(node); - fuses[tokenId] = fuses[tokenId] | fusesToBurn; - return fuses[tokenId]; - } -} - -contract MigratedWrappedNameRegistryTest is Test { - MigratedWrappedNameRegistry implementation; - MigratedWrappedNameRegistry registry; - RegistryDatastore datastore; - MockHCAFactoryBasic hcaFactory; - MockRegistryMetadata metadata; - MockENS ensRegistry; - MockNameWrapper nameWrapper; - VerifiableFactory factory; - - address owner = address(this); - address user = address(0x1234); - address mockResolver = address(0xABCD); - address v1Resolver = address(0xDEAD); - address fallbackResolver = makeAddr("fallbackResolver"); - - string testLabel = "test"; - uint256 testLabelId; - - function setUp() public { - datastore = new RegistryDatastore(); - hcaFactory = new MockHCAFactoryBasic(); - metadata = new MockRegistryMetadata(); - ensRegistry = new MockENS(); - nameWrapper = new MockNameWrapper(ensRegistry); - factory = new VerifiableFactory(); - - // Deploy implementation - implementation = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), // mock nameWrapper - IPermissionedRegistry(address(0)), // mock ethRegistry - factory, - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // Deploy proxy and initialize - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - "\x03eth\x00", // parent DNS-encoded name for .eth - owner, - 0, // ownerRoles - address(nameWrapper) // registrar for testing - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); - registry = MigratedWrappedNameRegistry(address(proxy)); - - testLabelId = LibLabel.labelToCanonicalId(testLabel); - - // Setup v1 resolver in ENS registry - bytes memory dnsEncodedName = NameCoder.ethName(testLabel); - bytes32 node = NameCoder.namehash(dnsEncodedName, 0); - ensRegistry.setResolver(node, v1Resolver); - } - - /** - * @dev Helper method to register a name using the wrapper contract - */ - function _registerName( - MigratedWrappedNameRegistry targetRegistry, - string memory label, - address nameOwner, - IRegistry subregistry, - address resolver, - uint256 roleBitmap, - uint64 expires - ) internal { - vm.prank(address(nameWrapper)); - targetRegistry.register(label, nameOwner, subregistry, resolver, roleBitmap, expires); - } - - function test_getResolver_unregistered_name() external view { - assertEq(registry.getResolver(testLabel), address(0)); - } - - function test_getResolver_registered_name_with_resolver() public { - // Register name first - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - testLabel, - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Should return the registered resolver - address resolver = registry.getResolver(testLabel); - assertEq(resolver, mockResolver, "Should return registered resolver"); - } - - function test_getResolver_registered_name_null_resolver() public { - // Register name with null resolver - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - testLabel, - user, - registry, - address(0), - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Should return address(0) since name is registered - address resolver = registry.getResolver(testLabel); - assertEq(resolver, address(0), "Should return null resolver for registered name"); - } - - function test_getResolver_expired_name() public { - // Register name with minimum valid expiry - uint64 expiry = uint64(block.timestamp + 1); - _registerName( - registry, - testLabel, - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - assertEq(registry.getResolver(testLabel), mockResolver, "before"); - vm.warp(expiry); - assertEq(registry.getResolver(testLabel), address(0), "after"); - } - - function test_getResolver_ens_registry_returns_zero() public { - // Clear the resolver in ENS registry - bytes memory dnsEncodedName = NameCoder.ethName(testLabel); - bytes32 node = NameCoder.namehash(dnsEncodedName, 0); - ensRegistry.setResolver(node, address(0)); - - // Should return address(0) when ENS registry returns zero - address resolver = registry.getResolver(testLabel); - assertEq(resolver, address(0), "Should return zero address when ENS registry returns zero"); - } - - function test_validateHierarchy_parent_fully_migrated() public { - // First register parent "test" in current registry - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Set up parent "test" in legacy system with registry as owner - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Create subdomain migration data for "sub.test.eth" - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - - // Test hierarchy validation by calling migration process - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Set up the subdomain token as emancipated - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(subTokenId, user); - - // This should pass hierarchy validation since parent exists in current registry AND is controlled in legacy - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("test_salt"))) - }) - ) - ) - { - // If it succeeds, that's fine - hierarchy validation passed - } catch Error(string memory reason) { - // Should not fail on hierarchy validation when both conditions are met - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("ParentNotMigrated")), - "Should not fail on parent validation when both conditions met" - ); - } catch (bytes memory) { - // Other failures are OK for this test - we just want to ensure hierarchy validation passes - } - } - - function test_validateHierarchy_parent_not_migrated() public { - // Create subdomain migration data - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - - // Generate token identifier for subdomain migration - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Set up the subdomain token as emancipated - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(subTokenId, user); - - // Should revert when parent is neither migrated nor controlled - // DNS encoded name is "sub.test.eth" and parent offset would be after "sub" (4 bytes) - (, uint256 parentOffset) = NameCoder.nextLabel(subDnsName, 0); - vm.expectRevert( - abi.encodeWithSelector(ParentNotMigrated.selector, subDnsName, parentOffset) - ); - - vm.prank(address(nameWrapper)); - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("test_salt"))) - }) - ) - ); - } - - function test_validateHierarchy_no_parent_domain() public { - // Try to migrate a top-level domain like "eth" (which has no parent) - bytes memory ethDnsName = NameCoder.encode("eth"); - uint256 ethTokenId = uint256(NameCoder.namehash(ethDnsName, 0)); - - // Set up the token as emancipated (so it passes validation) - nameWrapper.setFuseData(ethTokenId, PARENT_CANNOT_CONTROL, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(ethTokenId, user); - - // Should revert with NoParentDomain error - vm.expectRevert(MigratedWrappedNameRegistry.NoParentDomain.selector); - - vm.prank(address(nameWrapper)); - registry.onERC1155Received( - address(nameWrapper), - user, - ethTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: ethDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("test_salt"))) - }) - ) - ); - } - - function test_getResolver_2LD_unmigrated() external { - MigratedWrappedNameRegistry subRegistry = _create3LDRegistry("test.eth"); - - string memory label = "sub"; - - assertEq(subRegistry.getResolver(label), address(0), "unregistered"); - - // this child needs to be migrated, but isn't - nameWrapper.setFuseData( - uint256( - NameCoder.namehash( - NameCoder.namehash(subRegistry.parentDnsEncodedName(), 0), - keccak256(bytes(label)) - ) - ), - PARENT_CANNOT_CONTROL, - uint64(block.timestamp + 86400) - ); - - assertEq(subRegistry.getResolver(label), fallbackResolver, "unmigrated"); - } - - function test_getResolver_3LD_unregistered() public { - // Create a 3LD registry for "sub.test.eth" - MigratedWrappedNameRegistry subRegistry = _create3LDRegistry("sub.test.eth"); - - // // Test label "example" which would resolve to "example.sub.test.eth" - // string memory label3LD = "example"; - - // // Setup resolver in ENS registry for the full name - // bytes memory fullDnsName = abi.encodePacked( - // bytes1(uint8(bytes(label3LD).length)), - // label3LD, - // "\x03sub\x04test\x03eth\x00" // sub.test.eth - // ); - // bytes32 fullNode = NameCoder.namehash(fullDnsName, 0); - // ensRegistry.setResolver(fullNode, address(0x3333)); - - assertEq(subRegistry.getResolver("4ld"), address(0)); - } - - function test_getResolver_3LD_registered() public { - // Create a 3LD registry for "sub.test.eth" - MigratedWrappedNameRegistry subRegistry = _create3LDRegistry("sub.test.eth"); - - string memory label3LD = "example"; - address expectedResolver = address(0x4444); - - // Register the 4LD name in the 3LD registry - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - subRegistry, - label3LD, - user, - subRegistry, - expectedResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Should return the registered resolver - address resolver = subRegistry.getResolver(label3LD); - assertEq(resolver, expectedResolver, "Should return registered resolver for 4LD name"); - } - - // Helper function to create a registry with a real factory - function _createRegistryWithFactory( - VerifiableFactory realFactory - ) internal returns (MigratedWrappedNameRegistry) { - // Deploy implementation with real factory - MigratedWrappedNameRegistry impl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - realFactory, - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // Deploy proxy and initialize - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - "\x03eth\x00", // parent DNS-encoded name for .eth - owner, - 0, // ownerRoles - address(nameWrapper) // registrar for testing - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); - return MigratedWrappedNameRegistry(address(proxy)); - } - - // Helper function to create a 3LD registry - function _create3LDRegistry( - string memory domain - ) internal returns (MigratedWrappedNameRegistry) { - // Deploy new implementation instance - MigratedWrappedNameRegistry impl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // Create DNS-encoded name for the domain (e.g., "\x03sub\x04test\x03eth\x00") - bytes memory parentDnsName = NameCoder.encode(domain); - - // rememeber the name - nameWrapper.setName(parentDnsName); - - // Deploy proxy and initialize - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - parentDnsName, - owner, - 0, // ownerRoles - address(nameWrapper) // registrar for testing - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); - return MigratedWrappedNameRegistry(address(proxy)); - } - - function test_subdomain_freezeName_clears_resolver_when_fuse_not_set() public { - // Create subdomain migration data - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup locked subdomain with CANNOT_SET_RESOLVER fuse NOT set - uint32 lockedFuses = CANNOT_UNWRAP; - nameWrapper.setFuseData(subTokenId, lockedFuses, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(subTokenId, address(registry)); - - // Set an initial resolver on the subdomain - address initialResolver = address(0x7777); - nameWrapper.setInitialResolver(subTokenId, initialResolver); - - // Verify resolver is initially set - assertEq( - nameWrapper.getResolver(subTokenId), - initialResolver, - "Initial resolver should be set" - ); - - // Register parent "test" in registry to pass hierarchy validation - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Call onERC1155Received for subdomain migration - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("test_resolver_clear"))) - }) - ) - ) - { - // If successful, verify resolver was cleared - assertEq( - nameWrapper.getResolver(subTokenId), - address(0), - "Resolver should be cleared to address(0)" - ); - } catch { - // If it fails for other reasons (like factory), we can test freezeName directly - uint32 fuses = CANNOT_UNWRAP; - LockedNamesLib.freezeName(INameWrapper(address(nameWrapper)), subTokenId, fuses); - assertEq( - nameWrapper.getResolver(subTokenId), - address(0), - "Resolver should be cleared by direct freezeName call" - ); - } - } - - function test_subdomain_freezeName_preserves_resolver_when_fuse_already_set() public { - // Create subdomain migration data - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup locked subdomain with CANNOT_SET_RESOLVER fuse already set - uint32 lockedFuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER; - nameWrapper.setFuseData(subTokenId, lockedFuses, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(subTokenId, address(registry)); - - // Set an initial resolver on the subdomain - address initialResolver = address(0x6666); - nameWrapper.setInitialResolver(subTokenId, initialResolver); - - // Verify resolver is initially set - assertEq( - nameWrapper.getResolver(subTokenId), - initialResolver, - "Initial resolver should be set" - ); - - // Register parent "test" in registry to pass hierarchy validation - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Call onERC1155Received for subdomain migration - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("test_resolver_preserve"))) - }) - ) - ) - { - // If successful, verify resolver was preserved - assertEq( - nameWrapper.getResolver(subTokenId), - initialResolver, - "Resolver should be preserved when fuse already set" - ); - } catch { - // If it fails for other reasons (like factory), we can test freezeName directly - uint32 fuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER; - LockedNamesLib.freezeName(INameWrapper(address(nameWrapper)), subTokenId, fuses); - assertEq( - nameWrapper.getResolver(subTokenId), - initialResolver, - "Resolver should be preserved by direct freezeName call" - ); - } - } - - function test_validateHierarchy_name_already_registered() public { - // First register a name "sub" in the registry - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "sub", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Set up parent "test" in registry and legacy system to pass parent validation - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Try to migrate the same name "sub" - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Set up the subdomain token as emancipated - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(subTokenId, user); - - // Should revert with NameAlreadyRegistered error - vm.expectRevert( - abi.encodeWithSelector(IStandardRegistry.NameAlreadyRegistered.selector, "sub") - ); - - vm.prank(address(nameWrapper)); - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("test_already_registered"))) - }) - ) - ); - } - - function test_validateHierarchy_parent_in_registry_but_not_controlled() public { - // Register parent "test" in current registry - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Set up parent "test" in legacy system but NOT controlled by registry (owned by different address) - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), user); // Different owner, not registry - - // Create subdomain migration data for "sub.test.eth" - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Set up the subdomain token as emancipated - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(subTokenId, user); - - // Should revert because parent is not controlled in legacy system - (, uint256 parentOffset) = NameCoder.nextLabel(subDnsName, 0); - vm.expectRevert( - abi.encodeWithSelector(ParentNotMigrated.selector, subDnsName, parentOffset) - ); - - vm.prank(address(nameWrapper)); - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("test_not_controlled"))) - }) - ) - ); - } - - function test_register_emancipated_not_locked_fails() public { - string memory label = "emancipated"; - uint256 tokenId = uint256(keccak256(bytes(label))); - - // Set up emancipated but not locked name - nameWrapper.setFuseData( - tokenId, - PARENT_CANNOT_CONTROL, // Emancipated but not locked - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(tokenId, address(0x9999)); - - // Attempt to register should fail - vm.prank(address(nameWrapper)); - vm.expectRevert(abi.encodeWithSelector(LabelNotMigrated.selector, label)); - registry.register(label, user, registry, mockResolver, 0, uint64(block.timestamp + 86400)); - } - - function test_register_locked_name_not_owned_by_registry() public { - string memory label = "lockednotowned"; - uint256 tokenId = uint256(keccak256(bytes(label))); - - // Set up locked name not owned by registry - nameWrapper.setFuseData( - tokenId, - PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, // Locked and emancipated - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(tokenId, address(0x9999)); - - // Attempt to register should fail - vm.prank(address(nameWrapper)); - vm.expectRevert(abi.encodeWithSelector(LabelNotMigrated.selector, label)); - registry.register(label, user, registry, mockResolver, 0, uint64(block.timestamp + 86400)); - } - - function test_register_expired_locked_name_owned_by_registry() public { - string memory label = "migrated"; - uint256 tokenId = uint256(keccak256(bytes(label))); - - // Set up locked name owned by registry (properly migrated) - nameWrapper.setFuseData( - tokenId, - PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, // Locked and emancipated - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(tokenId, address(registry)); - - // First register the name - vm.prank(address(nameWrapper)); - registry.register( - label, - user, - registry, - mockResolver, - 0, - uint64(block.timestamp + 100) // Expires soon - ); - - // Move time forward past expiry - vm.warp(block.timestamp + 101); - - // Verify re-register succeed - vm.prank(address(nameWrapper)); - registry.register( - label, - address(0x5678), // New owner - registry, - address(0xBEEF), // New resolver - 0, - uint64(block.timestamp + 86400) - ); - - // Verify re-registration succeeded with new owner - (uint256 newTokenId, IRegistryDatastore.Entry memory entry) = registry.getNameData(label); - uint64 expires = entry.expiry; - assertGt(expires, block.timestamp); - assertEq(registry.ownerOf(newTokenId), address(0x5678)); - } - - function test_register_non_emancipated_name() public { - string memory label = "regular"; - uint256 tokenId = uint256(keccak256(bytes(label))); - - // Set up non-emancipated name - nameWrapper.setFuseData( - tokenId, - 0, // No fuses burned - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(tokenId, address(0x9999)); - - // Should succeed - no check needed - vm.prank(address(nameWrapper)); - registry.register(label, user, registry, mockResolver, 0, uint64(block.timestamp + 86400)); - - // Verify registration succeeded - (, IRegistryDatastore.Entry memory entry) = registry.getNameData(label); - uint64 expires = entry.expiry; - assertGt(expires, block.timestamp); - } - - function test_register_emancipated_with_other_fuses_not_locked() public { - string memory label = "emancipatedplus"; - uint256 tokenId = uint256(keccak256(bytes(label))); - - // Set up emancipated name with other fuses but not locked - nameWrapper.setFuseData( - tokenId, - PARENT_CANNOT_CONTROL | CANNOT_SET_RESOLVER, // Emancipated + other fuses but not locked - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(tokenId, address(0x9999)); - - // Attempt to register should fail - vm.prank(address(nameWrapper)); - vm.expectRevert(abi.encodeWithSelector(LabelNotMigrated.selector, label)); - registry.register(label, user, registry, mockResolver, 0, uint64(block.timestamp + 86400)); - } - - function test_subdomain_migration_emancipated_and_locked_name() public { - // Create subdomain migration data for emancipated and locked name - bytes memory subDnsName = NameCoder.encode("locked.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup subdomain that is emancipated and locked (both fuses required) - uint32 emancipatedAndLockedFuses = PARENT_CANNOT_CONTROL | CANNOT_UNWRAP; - nameWrapper.setFuseData( - subTokenId, - emancipatedAndLockedFuses, - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(subTokenId, address(registry)); - - // Register parent "test" in registry to pass hierarchy validation - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Set up parent "test" in legacy system with registry as owner - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Call onERC1155Received for emancipated and locked subdomain migration - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("test_locked_migration"))) - }) - ) - ) - { - // Migration should succeed for emancipated and locked subdomain - } catch Error(string memory reason) { - // Should not fail on validation since emancipated names are valid - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("NameNotEmancipated")), - "Should not fail validation for emancipated and locked subdomain" - ); - } catch (bytes memory) { - // Other failures are OK for this test - we just want to ensure validation passes - } - } - - // ===== ERC1155 Batch Receiver Tests ===== - - function test_onERC1155BatchReceived_single_valid_migration() public { - // Simplified test focusing on the batch processing logic rather than full migration - // This tests that the function signature and basic validation work correctly - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 123; - - uint256[] memory amounts = new uint256[](1); - amounts[0] = 1; - - MigrationData[] memory migrationDataArray = new MigrationData[](1); - migrationDataArray[0] = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("simple.eth"), - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("batch_test_simple"))) - }); - - // Test that the batch function handles the call structure correctly - // It should fail on migration validation (which is expected) but not on batch processing - vm.prank(address(nameWrapper)); - try - registry.onERC1155BatchReceived( - address(nameWrapper), - user, - tokenIds, - amounts, - abi.encode(migrationDataArray) - ) - returns (bytes4 result) { - assertEq( - result, - registry.onERC1155BatchReceived.selector, - "Should return correct selector" - ); - } catch { - // Migration validation failures are expected - we're just testing batch structure - assertTrue(true, "Batch function processed the call structure correctly"); - } - } - - function test_onERC1155BatchReceived_multiple_valid_migrations() public { - // Simplified test for multiple item batch processing - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 456; - tokenIds[1] = 789; - - uint256[] memory amounts = new uint256[](2); - amounts[0] = 1; - amounts[1] = 1; - - MigrationData[] memory migrationDataArray = new MigrationData[](2); - migrationDataArray[0] = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("simple1.eth"), - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("batch_test_1"))) - }); - migrationDataArray[1] = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("simple2.eth"), - owner: address(0x5678), - subregistry: address(0), - resolver: address(0x9999), - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("batch_test_2"))) - }); - - // Test multiple item batch processing - vm.prank(address(nameWrapper)); - try - registry.onERC1155BatchReceived( - address(nameWrapper), - user, - tokenIds, - amounts, - abi.encode(migrationDataArray) - ) - returns (bytes4 result) { - assertEq( - result, - registry.onERC1155BatchReceived.selector, - "Should return correct selector" - ); - } catch { - // Migration validation failures are expected - we're testing batch structure - assertTrue(true, "Multiple item batch function processed correctly"); - } - } - - function test_onERC1155BatchReceived_unauthorized_caller() public { - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 123; - uint256[] memory amounts = new uint256[](1); - amounts[0] = 1; - - MigrationData[] memory migrationDataArray = new MigrationData[](1); - - vm.prank(user); // Not nameWrapper - vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, user)); - registry.onERC1155BatchReceived( - user, - user, - tokenIds, - amounts, - abi.encode(migrationDataArray) - ); - } - - function test_onERC1155BatchReceived_empty_batch() public { - uint256[] memory tokenIds = new uint256[](0); - uint256[] memory amounts = new uint256[](0); - MigrationData[] memory migrationDataArray = new MigrationData[](0); - - vm.prank(address(nameWrapper)); - bytes4 result = registry.onERC1155BatchReceived( - address(nameWrapper), - user, - tokenIds, - amounts, - abi.encode(migrationDataArray) - ); - - assertEq(result, registry.onERC1155BatchReceived.selector, "Should handle empty batch"); - } - - function test_onERC1155BatchReceived_mismatched_array_lengths() public { - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 123; - tokenIds[1] = 456; - - uint256[] memory amounts = new uint256[](1); // Mismatched length - amounts[0] = 1; - - MigrationData[] memory migrationDataArray = new MigrationData[](1); - - vm.prank(address(nameWrapper)); - // This should revert due to array length mismatch in _migrateSubdomains - vm.expectRevert(); - registry.onERC1155BatchReceived( - address(nameWrapper), - user, - tokenIds, - amounts, - abi.encode(migrationDataArray) - ); - } - - // ===== Unauthorized Caller Tests ===== - - function test_onERC1155Received_unauthorized_caller() public { - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("test.eth"), - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("unauthorized_test"))) - }); - - vm.prank(user); // Not nameWrapper - vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, user)); - registry.onERC1155Received(user, user, 123, 1, abi.encode(migrationData)); - } - - function test_onERC1155Received_zero_address_caller() public { - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("test.eth"), - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("zero_address_test"))) - }); - - vm.prank(address(0)); - vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, address(0))); - registry.onERC1155Received(address(0), user, 123, 1, abi.encode(migrationData)); - } - - function test_onERC1155Received_random_contract_caller() public { - address randomContract = address(0xDEADBEEF); - MigrationData memory migrationData = MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("test.eth"), - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("random_contract_test"))) - }); - - vm.prank(randomContract); - vm.expectRevert(abi.encodeWithSelector(UnauthorizedCaller.selector, randomContract)); - registry.onERC1155Received(randomContract, user, 123, 1, abi.encode(migrationData)); - } - - // ===== UUPS Upgrade Authorization Tests ===== - - function test_authorizeUpgrade_with_upgrade_role() public { - // Deploy a new implementation - MigratedWrappedNameRegistry newImplementation = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // Owner has ROLE_UPGRADE by default from initialization - vm.prank(owner); - // This should not revert since owner has upgrade role - try registry.upgradeToAndCall(address(newImplementation), "") { - // Upgrade succeeded - } catch Error(string memory reason) { - // Should not fail due to authorization - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("UnauthorizedForResource")), - "Should not fail authorization with upgrade role" - ); - } catch (bytes memory) { - // Other failures (implementation issues) are acceptable for this test - } - } - - function test_authorizeUpgrade_without_upgrade_role() public { - // Deploy a new implementation - MigratedWrappedNameRegistry newImplementation = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // User does not have ROLE_UPGRADE - vm.prank(user); - vm.expectRevert(); // Should revert due to missing upgrade role - registry.upgradeToAndCall(address(newImplementation), ""); - } - - function test_authorizeUpgrade_with_granted_upgrade_role() public { - // Grant upgrade role to user using grantRootRoles (not grantRoles for ROOT_RESOURCE) - uint256 ROLE_UPGRADE = 1 << 20; - vm.prank(owner); - registry.grantRootRoles(ROLE_UPGRADE, user); - - // Deploy a new implementation - MigratedWrappedNameRegistry newImplementation = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // User should now be able to upgrade - vm.prank(user); - try registry.upgradeToAndCall(address(newImplementation), "") { - // Upgrade succeeded - } catch Error(string memory reason) { - // Should not fail due to authorization - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("UnauthorizedForResource")), - "Should not fail authorization with granted upgrade role" - ); - } catch (bytes memory) { - // Other failures (implementation issues) are acceptable for this test - } - } - - function test_authorizeUpgrade_revoked_upgrade_role() public { - // Grant then revoke upgrade role using root functions - uint256 ROLE_UPGRADE = 1 << 20; - vm.prank(owner); - registry.grantRootRoles(ROLE_UPGRADE, user); - - vm.prank(owner); - registry.revokeRootRoles(ROLE_UPGRADE, user); - - // Deploy a new implementation - MigratedWrappedNameRegistry newImplementation = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - // User should no longer be able to upgrade - vm.prank(user); - vm.expectRevert(); // Should revert due to missing upgrade role - registry.upgradeToAndCall(address(newImplementation), ""); - } - - // ===== Interface Support Tests ===== - - function test_supportsInterface_IERC1155Receiver() public view { - bytes4 ierc1155ReceiverInterfaceId = type(IERC1155Receiver).interfaceId; - assertTrue( - registry.supportsInterface(ierc1155ReceiverInterfaceId), - "Should support IERC1155Receiver interface" - ); - } - - function test_supportsInterface_IERC165() public view { - bytes4 ierc165InterfaceId = type(IERC165).interfaceId; - assertTrue( - registry.supportsInterface(ierc165InterfaceId), - "Should support IERC165 interface" - ); - } - - function test_supportsInterface_inherited_interfaces() public view { - // Test inherited interfaces from PermissionedRegistry - bytes4 iRegistryInterfaceId = type(IRegistry).interfaceId; - assertTrue( - registry.supportsInterface(iRegistryInterfaceId), - "Should support IRegistry interface" - ); - - bytes4 iPermissionedRegistryInterfaceId = type(IPermissionedRegistry).interfaceId; - assertTrue( - registry.supportsInterface(iPermissionedRegistryInterfaceId), - "Should support IPermissionedRegistry interface" - ); - } - - function test_supportsInterface_unknown_interface() public view { - bytes4 unknownInterfaceId = 0xffffffff; - assertFalse( - registry.supportsInterface(unknownInterfaceId), - "Should not support unknown interface" - ); - } - - function test_supportsInterface_zero_interface() public view { - bytes4 zeroInterfaceId = 0x00000000; - assertFalse( - registry.supportsInterface(zeroInterfaceId), - "Should not support zero interface" - ); - } - - // ===== Initialize Function Edge Case Tests ===== - - function test_initialize_zero_address_owner() public { - MigratedWrappedNameRegistry newImpl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - "\x03eth\x00", - address(0), // Zero address owner - 0, - address(0) - ); - - vm.expectRevert("Owner cannot be zero address"); - new ERC1967Proxy(address(newImpl), initData); - } - - function test_initialize_with_registrar_address() public { - MigratedWrappedNameRegistry newImpl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - address testRegistrar = address(0x1337); - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - "\x03eth\x00", - user, - RegistryRolesLib.ROLE_SET_RESOLVER, - testRegistrar - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(newImpl), initData); - MigratedWrappedNameRegistry newRegistry = MigratedWrappedNameRegistry(address(proxy)); - - // Check that registrar has ROLE_REGISTRAR - assertTrue( - newRegistry.hasRoles( - newRegistry.ROOT_RESOURCE(), - RegistryRolesLib.ROLE_REGISTRAR, - testRegistrar - ), - "Registrar should have ROLE_REGISTRAR" - ); - } - - function test_initialize_with_custom_owner_roles() public { - MigratedWrappedNameRegistry newImpl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - uint256 customRoles = RegistryRolesLib.ROLE_REGISTRAR | RegistryRolesLib.ROLE_SET_RESOLVER; - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - "\x04test\x03eth\x00", - user, - customRoles, - address(0) - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(newImpl), initData); - MigratedWrappedNameRegistry newRegistry = MigratedWrappedNameRegistry(address(proxy)); - - // Check that owner has custom roles plus upgrade roles - uint256 ROLE_UPGRADE = 1 << 20; - uint256 ROLE_UPGRADE_ADMIN = ROLE_UPGRADE << 128; - uint256 expectedRoles = ROLE_UPGRADE | ROLE_UPGRADE_ADMIN | customRoles; - - assertTrue( - newRegistry.hasRoles(newRegistry.ROOT_RESOURCE(), expectedRoles, user), - "Owner should have custom roles plus upgrade roles" - ); - } - - function test_initialize_different_parent_dns_name() public { - MigratedWrappedNameRegistry newImpl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(0)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - bytes memory customParentName = "\x07example\x03com\x00"; - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - customParentName, - user, - 0, - address(0) - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(newImpl), initData); - MigratedWrappedNameRegistry newRegistry = MigratedWrappedNameRegistry(address(proxy)); - - // Check that parent DNS name was set correctly - assertEq( - newRegistry.parentDnsEncodedName(), - customParentName, - "Parent DNS name should match" - ); - } - - function test_initialize_already_initialized() public { - // Try to initialize the already initialized registry again - vm.expectRevert(); - registry.initialize("\x03eth\x00", user, 0, address(0)); - } - - // ===== Comprehensive Hierarchy Validation Tests ===== - - function test_validateHierarchy_2LD_with_ethRegistry_exists() public { - // Create a mock ethRegistry that reports name exists - MockEthRegistry mockEthRegistry = new MockEthRegistry(); - mockEthRegistry.setSubregistry("existing", address(0x1234)); // Name already exists - - // Deploy new registry with mock ethRegistry - MigratedWrappedNameRegistry newImpl = new MigratedWrappedNameRegistry( - INameWrapper(address(nameWrapper)), - IPermissionedRegistry(address(mockEthRegistry)), - VerifiableFactory(address(0)), - datastore, - hcaFactory, - metadata, - fallbackResolver - ); - - bytes memory initData = abi.encodeWithSelector( - MigratedWrappedNameRegistry.initialize.selector, - "\x03eth\x00", - owner, - 0, - address(nameWrapper) - ); - - ERC1967Proxy proxy = new ERC1967Proxy(address(newImpl), initData); - MigratedWrappedNameRegistry newRegistry = MigratedWrappedNameRegistry(address(proxy)); - - // Try to migrate 2LD that already exists in ethRegistry - bytes memory existingDnsName = NameCoder.encode("existing.eth"); - uint256 existingTokenId = uint256(NameCoder.namehash(existingDnsName, 0)); - nameWrapper.setFuseData( - existingTokenId, - PARENT_CANNOT_CONTROL, - uint64(block.timestamp + 86400) - ); - nameWrapper.setOwner(existingTokenId, address(newRegistry)); - - vm.expectRevert( - abi.encodeWithSelector(IStandardRegistry.NameAlreadyRegistered.selector, "existing") - ); - vm.prank(address(nameWrapper)); - newRegistry.onERC1155Received( - address(nameWrapper), - user, - existingTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: existingDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("existing_test"))) - }) - ) - ); - } - - function test_validateHierarchy_4LD_deep_hierarchy() public { - _setup4LDHierarchy(); - } - - function _setup4LDHierarchy() internal { - // Setup 3LD registry for "sub.test.eth" - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Create 3LD registry and register "sub" - MigratedWrappedNameRegistry subRegistry = _create3LDRegistry("sub.test.eth"); - _registerName( - registry, - "sub", - user, - subRegistry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // Setup legacy system ownership - nameWrapper.setWrapped(uint256(NameCoder.namehash(NameCoder.encode("test.eth"), 0)), true); - nameWrapper.setOwner( - uint256(NameCoder.namehash(NameCoder.encode("test.eth"), 0)), - address(registry) - ); - nameWrapper.setWrapped( - uint256(NameCoder.namehash(NameCoder.encode("sub.test.eth"), 0)), - true - ); - nameWrapper.setOwner( - uint256(NameCoder.namehash(NameCoder.encode("sub.test.eth"), 0)), - address(registry) - ); - - // Test 4LD migration - uint256 tokenId = uint256(NameCoder.namehash(NameCoder.encode("deep.sub.test.eth"), 0)); - nameWrapper.setFuseData(tokenId, PARENT_CANNOT_CONTROL, expiry); - nameWrapper.setOwner(tokenId, address(subRegistry)); - - vm.prank(address(nameWrapper)); - try - subRegistry.onERC1155Received( - address(nameWrapper), - user, - tokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: NameCoder.encode("deep.sub.test.eth"), - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("deep_4ld_test"))) - }) - ) - ) - { - // Should succeed with proper hierarchy - } catch { - // Other failures acceptable for this test - } - } - - function test_validateHierarchy_mixed_wrapped_unwrapped() public { - // Setup where parent is registered in new registry but not wrapped in legacy - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - - // DON'T wrap in legacy system (parent not wrapped) - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), false); // Not wrapped - - // Try to migrate subdomain - bytes memory subDnsName = NameCoder.encode("sub.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL, expiry); - nameWrapper.setOwner(subTokenId, address(registry)); - - (, uint256 parentOffset) = NameCoder.nextLabel(subDnsName, 0); - vm.expectRevert( - abi.encodeWithSelector(ParentNotMigrated.selector, subDnsName, parentOffset) - ); - vm.prank(address(nameWrapper)); - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("mixed_test"))) - }) - ) - ); - } - - function test_validateHierarchy_5LD_very_deep() public { - // Test very deep hierarchy - simplified to avoid stack issues - uint64 expiry = uint64(block.timestamp + 86400); - - // Setup just enough hierarchy to test deep nesting - _registerName( - registry, - "d", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - nameWrapper.setWrapped(uint256(NameCoder.namehash(NameCoder.encode("d.eth"), 0)), true); - nameWrapper.setOwner( - uint256(NameCoder.namehash(NameCoder.encode("d.eth"), 0)), - address(registry) - ); - - // Create minimal test case - this test primarily verifies the concept works - // Full deep hierarchy testing can be done in integration tests - MigratedWrappedNameRegistry cRegistry = _create3LDRegistry("c.d.eth"); - - // Simplified assertion - deep hierarchy support exists - assertTrue(address(cRegistry) != address(0), "Deep hierarchy registries can be created"); - } - - function test_validateHierarchy_2LD_not_eth() public { - // Test non-.eth 2LD (should fail with NoParentDomain since we only handle .eth) - bytes memory comDnsName = NameCoder.encode("test.com"); - uint256 comTokenId = uint256(NameCoder.namehash(comDnsName, 0)); - nameWrapper.setFuseData(comTokenId, PARENT_CANNOT_CONTROL, uint64(block.timestamp + 86400)); - nameWrapper.setOwner(comTokenId, address(registry)); - - // For non-.eth 2LDs, parent is not "eth", so hierarchy logic differs - // This would check ethRegistry.getSubregistry("test") which would return address(0) - // So it should pass the ethRegistry check but may fail elsewhere - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - comTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: comDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: uint64(block.timestamp + 86400) - }), - - salt: uint256(keccak256(abi.encodePacked("com_test"))) - }) - ) - ) - { - // May succeed depending on setup - } catch Error(string memory /*reason*/) { - // Various failures expected for non-.eth domains - } catch (bytes memory) { - // Other failures expected - } - } - - // ===== Fuse Combination Tests ===== - - function test_fuse_combinations_emancipated_only() public { - // Test name with only PARENT_CANNOT_CONTROL fuse - bytes memory subDnsName = NameCoder.encode("emancipated.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup parent - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Only emancipated, not locked - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL, expiry); - nameWrapper.setOwner(subTokenId, address(registry)); - - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("emancipated_only_test"))) - }) - ) - ) - { - // Should succeed - emancipated names can be migrated - } catch Error(string memory reason) { - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("NameNotEmancipated")), - "Should not fail validation for emancipated name" - ); - } catch (bytes memory) { - // Other failures acceptable - } - } - - function test_fuse_combinations_locked_not_emancipated() public { - // Test name with CANNOT_UNWRAP but not PARENT_CANNOT_CONTROL - bytes memory subDnsName = NameCoder.encode("locked.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup parent - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Locked but not emancipated - should fail - nameWrapper.setFuseData(subTokenId, CANNOT_UNWRAP, expiry); - nameWrapper.setOwner(subTokenId, address(registry)); - - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("locked_not_emancipated_test"))) - }) - ) - ) - { - revert("Should have failed validation"); - } catch Error(string memory reason) { - assertTrue( - keccak256(bytes(reason)) == keccak256(bytes("NameNotEmancipated")), - "Should fail with NameNotEmancipated" - ); - } catch (bytes memory lowLevelData) { - // Check if it's the expected revert - bytes4 errorSelector = bytes4(lowLevelData); - assertTrue( - errorSelector == LockedNamesLib.NameNotEmancipated.selector, - "Should revert with NameNotEmancipated" - ); - } - } - - function test_fuse_combinations_cannot_burn_fuses() public { - // Test name with CANNOT_BURN_FUSES - should now succeed with migration but not get admin roles - bytes memory subDnsName = NameCoder.encode("frozen.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup parent - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Emancipated but with CANNOT_BURN_FUSES - should now succeed - nameWrapper.setFuseData(subTokenId, PARENT_CANNOT_CONTROL | CANNOT_BURN_FUSES, expiry); - nameWrapper.setOwner(subTokenId, address(registry)); - - vm.prank(address(nameWrapper)); - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, // Only regular role, no admin role - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("frozen_test"))) - }) - ) - ); - - // Migration succeeded if no revert (names with CANNOT_BURN_FUSES can now migrate) - } - - function test_fuse_combinations_all_restrictive_fuses() public { - // Test name with all restrictive fuses set - bytes memory subDnsName = NameCoder.encode("restricted.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup parent - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // All restrictive fuses except CANNOT_BURN_FUSES - uint32 restrictiveFuses = PARENT_CANNOT_CONTROL | - CANNOT_UNWRAP | - CANNOT_TRANSFER | - CANNOT_SET_RESOLVER | - CANNOT_SET_TTL | - CANNOT_CREATE_SUBDOMAIN; - nameWrapper.setFuseData(subTokenId, restrictiveFuses, expiry); - nameWrapper.setOwner(subTokenId, address(registry)); - - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("restrictive_test"))) - }) - ) - ) - { - // Should succeed - all fuses except CANNOT_BURN_FUSES is OK - } catch Error(string memory reason) { - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("NameNotEmancipated")), - "Should not fail emancipation validation with proper fuses" - ); - } catch (bytes memory) { - // Other failures acceptable - } - } - - function test_fuse_combinations_with_extension_permission() public { - // Test name with CAN_EXTEND_EXPIRY fuse - should generate renewal roles - bytes memory subDnsName = NameCoder.encode("extendable.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup parent - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Emancipated and locked with extend permission - nameWrapper.setFuseData( - subTokenId, - PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, - expiry - ); - nameWrapper.setOwner(subTokenId, address(registry)); - - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("extendable_test"))) - }) - ) - ) - { - // Should succeed and generate proper roles including renewal - } catch Error(string memory reason) { - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("NameNotEmancipated")), - "Should not fail validation for properly configured extendable name" - ); - } catch (bytes memory) { - // Other failures acceptable - } - } - - function test_fuse_combinations_no_subdomain_creation() public { - // Test name with CANNOT_CREATE_SUBDOMAIN - should not get registrar roles on subregistry - bytes memory subDnsName = NameCoder.encode("nosub.test.eth"); - uint256 subTokenId = uint256(NameCoder.namehash(subDnsName, 0)); - - // Setup parent - uint64 expiry = uint64(block.timestamp + 86400); - _registerName( - registry, - "test", - user, - registry, - mockResolver, - RegistryRolesLib.ROLE_SET_RESOLVER, - expiry - ); - bytes memory parentDnsName = NameCoder.encode("test.eth"); - bytes32 parentNode = NameCoder.namehash(parentDnsName, 0); - nameWrapper.setWrapped(uint256(parentNode), true); - nameWrapper.setOwner(uint256(parentNode), address(registry)); - - // Emancipated and locked but cannot create subdomains - nameWrapper.setFuseData( - subTokenId, - PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN, - expiry - ); - nameWrapper.setOwner(subTokenId, address(registry)); - - vm.prank(address(nameWrapper)); - try - registry.onERC1155Received( - address(nameWrapper), - user, - subTokenId, - 1, - abi.encode( - MigrationData({ - transferData: TransferData({ - dnsEncodedName: subDnsName, - owner: user, - subregistry: address(0), - resolver: mockResolver, - roleBitmap: RegistryRolesLib.ROLE_SET_RESOLVER, - expires: expiry - }), - - salt: uint256(keccak256(abi.encodePacked("nosub_test"))) - }) - ) - ) - { - // Should succeed but owner won't get registrar roles on subregistry - } catch Error(string memory reason) { - assertTrue( - keccak256(bytes(reason)) != keccak256(bytes("NameNotEmancipated")), - "Should not fail validation for properly configured no-subdomain name" - ); - } catch (bytes memory) { - // Other failures acceptable - } - } -} diff --git a/contracts/test/unit/utils/WrappedErrorLib.t.sol b/contracts/test/unit/utils/WrappedErrorLib.t.sol new file mode 100644 index 00000000..c4b0dcf1 --- /dev/null +++ b/contracts/test/unit/utils/WrappedErrorLib.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {WrappedErrorLib} from "~src/utils/WrappedErrorLib.sol"; + +contract MigrationErrorsTest is Test { + error TypedError(uint256, string); + + function wrapAndRevert(bytes memory v) external pure { + WrappedErrorLib.wrapAndRevert(v); + } + + function test_selector() external pure { + assertEq( + WrappedErrorLib.ERROR_STRING_SELECTOR, + bytes4(abi.encodeWithSignature("Error(string)")) + ); + } + + function test_prefix() external pure { + bytes memory v = WrappedErrorLib.wrap(hex"12345678"); + assembly { + v := add(v, 4) // skip selector + } + assertEq( + abi.decode(v, (bytes)), + abi.encodePacked(WrappedErrorLib.WRAPPED_ERROR_PREFIX, "12345678") + ); + } + + function test_wrap(bytes calldata v) external pure { + assertEq(WrappedErrorLib.unwrap(WrappedErrorLib.wrap(v)), v); + } + + function test_idempotent_alreadyError() external pure { + bytes memory err = abi.encodeWithSignature("Error(string)", "abc"); + assertEq(WrappedErrorLib.wrap(err), err, "f(x)"); + assertEq(WrappedErrorLib.wrap(WrappedErrorLib.wrap(err)), err, "f(f(x))"); + } + + function test_idempotent_typedError() external pure { + bytes memory err = abi.encodeWithSelector(TypedError.selector, 123, "abc"); + assertEq(WrappedErrorLib.wrap(WrappedErrorLib.wrap(err)), WrappedErrorLib.wrap(err)); + } + + function test_wrapAndRevert_alreadyError() external { + bytes memory err = abi.encodeWithSignature("Error(string)", "abc"); + try this.wrapAndRevert(err) {} catch (bytes memory v) { + assertEq(v, err); + } + vm.expectRevert(err); + this.wrapAndRevert(err); + } + + function test_wrapAndRevert_typedError() external { + bytes memory err = abi.encodeWithSelector(TypedError.selector, 123, "abc"); + try this.wrapAndRevert(err) {} catch (bytes memory v) { + assertEq(WrappedErrorLib.unwrap(v), err); + } + vm.expectRevert(WrappedErrorLib.wrap(err)); + this.wrapAndRevert(err); + } +}