diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 297aabc1..286ea331 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ on: env: FOUNDRY_PROFILE: ci + BASE_SEPOLIA_RPC_URL: "https://sepolia.base.org" jobs: forge-test: diff --git a/src/L2/interface/IL2ReverseRegistrar.sol b/src/L2/interface/IL2ReverseRegistrar.sol index 18211ede..e913f998 100644 --- a/src/L2/interface/IL2ReverseRegistrar.sol +++ b/src/L2/interface/IL2ReverseRegistrar.sol @@ -15,6 +15,12 @@ interface IL2ReverseRegistrar { /// @param name The name to set. function setNameForAddr(address addr, string memory name) external; + /// @notice Returns the name for an address. + /// + /// @param addr The address to get the name for. + /// @return The name for the address. + function nameForAddr(address addr) external view returns (string memory); + /// @notice Sets the `nameForAddr()` record for the addr provided account using a signature. /// /// @param addr The address to set the name for. @@ -46,4 +52,10 @@ interface IL2ReverseRegistrar { uint256[] memory coinTypes, bytes memory signature ) external; + + /// @notice Migrates the names from the old reverse resolver to the new one. + /// Only callable by the owner. + /// + /// @param addresses The addresses to migrate. + function batchSetName(address[] calldata addresses) external; } diff --git a/test/Fork/BaseSepoliaConstants.sol b/test/Fork/BaseSepoliaConstants.sol new file mode 100644 index 00000000..6fcc454c --- /dev/null +++ b/test/Fork/BaseSepoliaConstants.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +library BaseSepolia { + // ENS / Basenames addresses on Base Sepolia + address constant REGISTRY = 0x1493b2567056c2181630115660963E13A8E32735; + address constant BASE_REGISTRAR = 0xA0c70ec36c010B55E3C434D6c6EbEEC50c705794; + address constant LEGACY_GA_CONTROLLER = 0x49aE3cC2e3AA768B1e5654f5D3C6002144A59581; + address constant LEGACY_L2_RESOLVER = 0x6533C94869D28fAA8dF77cc63f9e2b2D6Cf77eBA; + // ReverseRegistrar with correct reverse node configured for Base Sepolia + address constant LEGACY_REVERSE_REGISTRAR = 0x876eF94ce0773052a2f81921E70FF25a5e76841f; + // Old reverse registrar with incorrect reverse node configured for Base Sepolia + // address constant LEGACY_REVERSE_REGISTRAR = 0xa0A8401ECF248a9375a0a71C4dedc263dA18dCd7; + + address constant UPGRADEABLE_CONTROLLER_PROXY = 0x82c858CDF64b3D893Fe54962680edFDDC37e94C8; + address constant UPGRADEABLE_L2_RESOLVER_PROXY = 0x85C87e548091f204C2d0350b39ce1874f02197c6; + + // ENS L2 Reverse Registrar (ENS-managed) on Base Sepolia + address constant ENS_L2_REVERSE_REGISTRAR = 0x00000BeEF055f7934784D6d81b6BC86665630dbA; + + // Ops / controllers + address constant L2_OWNER = 0xdEC57186e5dB11CcFbb4C932b8f11bD86171CB9D; + address constant MIGRATION_CONTROLLER = 0xE8A87034a06425476F2bD6fD14EA038332Cc5e10; + + // ENSIP-11 Base Sepolia cointype + uint256 constant BASE_SEPOLIA_COINTYPE = 2147568180; + + // ENSIP-19 Base Sepolia reverse parent node: namehash("80014a34.reverse") + bytes32 constant BASE_SEPOLIA_REVERSE_NODE = 0x9831acb91a733dba6ffe6c6e872dd546b8c24e2dbd225f3616a8c670cbbd8b8a; +} diff --git a/test/Fork/BaseSepoliaForkBase.t.sol b/test/Fork/BaseSepoliaForkBase.t.sol new file mode 100644 index 00000000..b9d3223e --- /dev/null +++ b/test/Fork/BaseSepoliaForkBase.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {NameResolver} from "ens-contracts/resolvers/profiles/NameResolver.sol"; + +import {RegistrarController} from "src/L2/RegistrarController.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {IL2ReverseRegistrar} from "src/L2/interface/IL2ReverseRegistrar.sol"; +import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; +import {Sha3} from "src/lib/Sha3.sol"; +import {BASE_ETH_NODE} from "src/util/Constants.sol"; + +import {BaseSepolia as BaseSepoliaConstants} from "test/Fork/BaseSepoliaConstants.sol"; +import {L2Resolver} from "src/L2/L2Resolver.sol"; +import {ReverseRegistrar} from "src/L2/ReverseRegistrar.sol"; + +contract BaseSepoliaForkBase is Test { + // RPC alias must be configured in foundry.toml as `base-sepolia`. + string internal constant FORK_ALIAS = "base-sepolia"; + + // Addresses from constants + address internal constant REGISTRY = BaseSepoliaConstants.REGISTRY; + address internal constant BASE_REGISTRAR = BaseSepoliaConstants.BASE_REGISTRAR; + address internal constant LEGACY_GA_CONTROLLER = BaseSepoliaConstants.LEGACY_GA_CONTROLLER; + address internal constant LEGACY_L2_RESOLVER = BaseSepoliaConstants.LEGACY_L2_RESOLVER; + address internal constant LEGACY_REVERSE_REGISTRAR = BaseSepoliaConstants.LEGACY_REVERSE_REGISTRAR; + + address internal constant UPGRADEABLE_CONTROLLER_PROXY = BaseSepoliaConstants.UPGRADEABLE_CONTROLLER_PROXY; + address internal constant UPGRADEABLE_L2_RESOLVER_PROXY = BaseSepoliaConstants.UPGRADEABLE_L2_RESOLVER_PROXY; + + // ENS L2 Reverse Registrar (Base Sepolia) + address internal constant ENS_L2_REVERSE_REGISTRAR = BaseSepoliaConstants.ENS_L2_REVERSE_REGISTRAR; + + // Owners / ops + address internal constant L2_OWNER = BaseSepoliaConstants.L2_OWNER; + + // Actors + uint256 internal userPk; + address internal user; + + // Interfaces + RegistrarController internal legacyController; + UpgradeableRegistrarController internal upgradeableController; + NameResolver internal legacyResolver; + IL2ReverseRegistrar internal l2ReverseRegistrar; + + function setUp() public virtual { + vm.createSelectFork(FORK_ALIAS); + + // Create a deterministic EOA we control for signing + userPk = uint256(keccak256("basenames.fork.user")); + user = vm.addr(userPk); + + legacyController = RegistrarController(LEGACY_GA_CONTROLLER); + upgradeableController = UpgradeableRegistrarController(UPGRADEABLE_CONTROLLER_PROXY); + legacyResolver = NameResolver(LEGACY_L2_RESOLVER); + l2ReverseRegistrar = IL2ReverseRegistrar(ENS_L2_REVERSE_REGISTRAR); + } + + function _labelFor(string memory name) internal pure returns (bytes32) { + return keccak256(bytes(name)); + } + + function _nodeFor(string memory name) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(BASE_ETH_NODE, _labelFor(name))); + } + + function _fullName(string memory name) internal pure returns (string memory) { + return string.concat(name, ".base.eth"); + } + + function _baseReverseNode(address addr, bytes32 baseReverseParentNode) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(baseReverseParentNode, Sha3.hexAddress(addr))); + } + + // Build a signature for ENS L2 Reverse Registrar setNameForAddrWithSignature, EIP-191 style + function _buildL2ReverseSignature(string memory fullName, uint256[] memory coinTypes, uint256 expiry) + internal + view + returns (bytes memory) + { + bytes4 selector = IL2ReverseRegistrar.setNameForAddrWithSignature.selector; + bytes32 inner = + keccak256(abi.encodePacked(ENS_L2_REVERSE_REGISTRAR, selector, user, expiry, fullName, coinTypes)); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inner)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, digest); + return abi.encodePacked(r, s, v); + } +} diff --git a/test/Fork/ENSIP19DataMigrations.sol b/test/Fork/ENSIP19DataMigrations.sol new file mode 100644 index 00000000..7831902b --- /dev/null +++ b/test/Fork/ENSIP19DataMigrations.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {NameResolver} from "ens-contracts/resolvers/profiles/NameResolver.sol"; +import {NameEncoder} from "ens-contracts/utils/NameEncoder.sol"; +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {AddrResolver} from "ens-contracts/resolvers/profiles/AddrResolver.sol"; +import {console2} from "forge-std/console2.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {RegistrarController} from "src/L2/RegistrarController.sol"; +import {ReverseRegistrar} from "src/L2/ReverseRegistrar.sol"; +import {L2Resolver} from "src/L2/L2Resolver.sol"; +import {BASE_REVERSE_NODE} from "src/util/Constants.sol"; +import {MigrationController} from "src/L2/MigrationController.sol"; +import {Sha3} from "src/lib/Sha3.sol"; + +import {BaseSepoliaForkBase} from "./BaseSepoliaForkBase.t.sol"; +import {BaseSepolia as BaseSepoliaConstants} from "test/Fork/BaseSepoliaConstants.sol"; + +contract ENSIP19DataMigrations is BaseSepoliaForkBase { + function test_migration_controller_setBaseForwardAddr() public { + string memory name = "migratefwd"; + bytes32 root = legacyController.rootNode(); + bytes32 node = keccak256(abi.encodePacked(root, _labelFor(name))); + + // Register a name with legacy resolver + RegistrarController legacyRC = RegistrarController(BaseSepoliaConstants.LEGACY_GA_CONTROLLER); + uint256 price = legacyRC.registerPrice(name, 365 days); + vm.deal(user, price); + vm.prank(user); + legacyRC.register{value: price}( + RegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: 365 days, + resolver: BaseSepoliaConstants.LEGACY_L2_RESOLVER, + data: new bytes[](0), + reverseRecord: false + }) + ); + + // Set a legacy EVM addr record (ETH_COINTYPE) on the resolver so there is something to migrate + vm.prank(user); + AddrResolver(BaseSepoliaConstants.LEGACY_L2_RESOLVER).setAddr(node, user); + + // Configure MigrationController as registrar controller on the resolver (as L2 owner) + vm.prank(L2_OWNER); + L2Resolver(BaseSepoliaConstants.LEGACY_L2_RESOLVER).setRegistrarController( + BaseSepoliaConstants.MIGRATION_CONTROLLER + ); + + uint256 coinType = MigrationController(BaseSepoliaConstants.MIGRATION_CONTROLLER).coinType(); + + // Pre: ENSIP-11 (coinType) record should be empty + bytes memory beforeBytes = AddrResolver(BaseSepoliaConstants.LEGACY_L2_RESOLVER).addr(node, coinType); + assertEq(beforeBytes.length, 0, "pre: ensip-11 addr already set"); + + // Call MigrationController as owner (l2_owner_address) + bytes32[] memory nodes = new bytes32[](1); + nodes[0] = node; + vm.prank(L2_OWNER); + MigrationController(BaseSepoliaConstants.MIGRATION_CONTROLLER).setBaseForwardAddr(nodes); + + // Post: ENSIP-11 (coinType) forward addr set + bytes memory afterBytes = AddrResolver(BaseSepoliaConstants.LEGACY_L2_RESOLVER).addr(node, coinType); + assertGt(afterBytes.length, 0, "post: ensip-11 addr not set"); + } + + function test_l2_reverse_registrar_with_migration_batchSetName() public { + string memory name = "migraterev"; + + // Claim/set old reverse name via legacy flow + vm.prank(user); + ReverseRegistrar(BaseSepoliaConstants.LEGACY_REVERSE_REGISTRAR).setNameForAddr( + user, user, BaseSepoliaConstants.LEGACY_L2_RESOLVER, _fullName(name) + ); + + address rrOwner = Ownable(ENS_L2_REVERSE_REGISTRAR).owner(); + + address[] memory addrs = new address[](1); + addrs[0] = user; + + (, bytes32 calculatedBaseReverseNode) = NameEncoder.dnsEncodeName("80014a34.reverse"); + console2.logBytes32(calculatedBaseReverseNode); + bytes32 node = keccak256(abi.encodePacked(calculatedBaseReverseNode, Sha3.hexAddress(user))); + console2.logBytes32(node); + + vm.prank(rrOwner); + l2ReverseRegistrar.batchSetName(addrs); + + // Assert L2 reverse registrar stored the migrated name + string memory l2Name = l2ReverseRegistrar.nameForAddr(user); + assertEq(keccak256(bytes(l2Name)), keccak256(bytes(_fullName(name))), "l2 reverse name not migrated"); + } +} diff --git a/test/Fork/ENSIP19LegacyFlows.t.sol b/test/Fork/ENSIP19LegacyFlows.t.sol new file mode 100644 index 00000000..47ed3776 --- /dev/null +++ b/test/Fork/ENSIP19LegacyFlows.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {NameResolver} from "ens-contracts/resolvers/profiles/NameResolver.sol"; + +import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; +import {RegistrarController} from "src/L2/RegistrarController.sol"; + +import {BaseSepoliaForkBase} from "./BaseSepoliaForkBase.t.sol"; +import {BaseSepolia as BaseSepoliaConstants} from "./BaseSepoliaConstants.sol"; + +contract ENSIP19LegacyFlows is BaseSepoliaForkBase { + function test_register_name_on_legacy() public { + string memory name = "forkleg"; + bytes32 root = legacyController.rootNode(); + bytes32 node = keccak256(abi.encodePacked(root, _labelFor(name))); + + RegistrarController.RegisterRequest memory req = RegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: 365 days, + resolver: LEGACY_L2_RESOLVER, + data: new bytes[](0), + reverseRecord: false + }); + + uint256 price = legacyController.registerPrice(name, req.duration); + + vm.deal(user, price); + vm.startPrank(user); + legacyController.register{value: price}(req); + vm.stopPrank(); + + // Assert resolver set on registry and owner assigned + ENS ens = ENS(REGISTRY); + address ownerNow = ens.owner(node); + address resolverNow = ens.resolver(node); + assertEq(ownerNow, user, "legacy owner"); + assertEq(resolverNow, LEGACY_L2_RESOLVER, "legacy resolver"); + } + + function test_set_primary_name_on_legacy() public { + string memory name = "forkprimary"; + bytes32 root = legacyController.rootNode(); + bytes32 node = keccak256(abi.encodePacked(root, _labelFor(name))); + + // First register the name with a resolver and no reverse + RegistrarController.RegisterRequest memory req = RegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: 365 days, + resolver: LEGACY_L2_RESOLVER, + data: new bytes[](0), + reverseRecord: false + }); + uint256 price = legacyController.registerPrice(name, req.duration); + vm.deal(user, price); + vm.prank(user); + legacyController.register{value: price}(req); + + // Set primary via legacy ReverseRegistrar directly + vm.prank(user); + IReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setNameForAddr(user, user, LEGACY_L2_RESOLVER, _fullName(name)); + + // Validate reverse record was set on the legacy resolver + bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE); + string memory storedName = NameResolver(LEGACY_L2_RESOLVER).name(baseRevNode); + assertEq(keccak256(bytes(storedName)), keccak256(bytes(_fullName(name))), "reverse name not set"); + + // Forward resolver unchanged + ENS ens = ENS(REGISTRY); + assertEq(ens.resolver(node), LEGACY_L2_RESOLVER, "resolver unchanged"); + } + + function test_register_with_reverse_sets_primary_via_controller() public { + string memory name = "forklegrev"; + bytes32 root = legacyController.rootNode(); + bytes32 node = keccak256(abi.encodePacked(root, _labelFor(name))); + + RegistrarController.RegisterRequest memory req = RegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: 365 days, + resolver: LEGACY_L2_RESOLVER, + data: new bytes[](0), + reverseRecord: true + }); + + uint256 price = legacyController.registerPrice(name, req.duration); + vm.deal(user, price); + vm.prank(user); + legacyController.register{value: price}(req); + + // Assert reverse was set by the controller calling the ReverseRegistrar + bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE); + string memory storedName = NameResolver(LEGACY_L2_RESOLVER).name(baseRevNode); + string memory expectedFull = string.concat(name, legacyController.rootName()); + assertEq(keccak256(bytes(storedName)), keccak256(bytes(expectedFull)), "reverse name not set by controller"); + + // Also verify forward resolver/owner as a sanity check + ENS ens = ENS(REGISTRY); + assertEq(ens.owner(node), user); + assertEq(ens.resolver(node), LEGACY_L2_RESOLVER); + } +} diff --git a/test/Fork/ENSIP19NewFlows.sol b/test/Fork/ENSIP19NewFlows.sol new file mode 100644 index 00000000..21433c16 --- /dev/null +++ b/test/Fork/ENSIP19NewFlows.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {AddrResolver} from "ens-contracts/resolvers/profiles/AddrResolver.sol"; +import {NameResolver} from "ens-contracts/resolvers/profiles/NameResolver.sol"; + +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; + +import {BaseSepoliaForkBase} from "./BaseSepoliaForkBase.t.sol"; +import {BaseSepolia as BaseSepoliaConstants} from "./BaseSepoliaConstants.sol"; + +contract ENSIP19NewFlows is BaseSepoliaForkBase { + uint256 internal constant BASE_SEPOLIA_COINTYPE = 2147568180; + + function test_register_on_new_sets_forward_records_ensip11() public { + string memory name = "forknewfwd"; + bytes32 root = legacyController.rootNode(); + bytes32 node = keccak256(abi.encodePacked(root, keccak256(bytes(name)))); + + bytes[] memory data = new bytes[](2); + // setAddr(bytes32,address) + bytes4 setAddrDefaultSel = bytes4(keccak256("setAddr(bytes32,address)")); + data[0] = abi.encodeWithSelector(setAddrDefaultSel, node, user); + // setAddr(bytes32,uint256,bytes) + bytes4 setAddrCointypeSel = bytes4(keccak256("setAddr(bytes32,uint256,bytes)")); + data[1] = abi.encodeWithSelector(setAddrCointypeSel, node, BASE_SEPOLIA_COINTYPE, _addressToBytes(user)); + + UpgradeableRegistrarController.RegisterRequest memory req = UpgradeableRegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: 365 days, + resolver: UPGRADEABLE_L2_RESOLVER_PROXY, + data: data, + reverseRecord: false, + coinTypes: new uint256[](0), + signatureExpiry: 0, + signature: bytes("") + }); + + uint256 price = upgradeableController.registerPrice(name, req.duration); + vm.deal(user, price); + vm.prank(user); + upgradeableController.register{value: price}(req); + + ENS ens = ENS(REGISTRY); + address resolverNow = ens.resolver(node); + address ownerNow = ens.owner(node); + assertEq(resolverNow, UPGRADEABLE_L2_RESOLVER_PROXY, "resolver should be upgradeable L2 resolver"); + assertEq(ownerNow, user, "owner should be user"); + + bytes memory coinAddr = AddrResolver(UPGRADEABLE_L2_RESOLVER_PROXY).addr(node, BASE_SEPOLIA_COINTYPE); + assertEq(coinAddr.length, 20, "ensip-11 addr length"); + assertEq(address(bytes20(coinAddr)), user, "ensip-11 addr matches user"); + assertEq(AddrResolver(UPGRADEABLE_L2_RESOLVER_PROXY).addr(node), user, "default addr matches user"); + } + + function test_register_with_reverse_on_new_sets_only_legacy_reverse_no_signature() public { + string memory name = "forknewrev"; + bytes32 root = legacyController.rootNode(); + bytes32 node = keccak256(abi.encodePacked(root, keccak256(bytes(name)))); + + UpgradeableRegistrarController.RegisterRequest memory req = UpgradeableRegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: 365 days, + resolver: UPGRADEABLE_L2_RESOLVER_PROXY, + data: new bytes[](0), + reverseRecord: true, + coinTypes: new uint256[](0), + signatureExpiry: 0, + signature: bytes("") + }); + + uint256 price = upgradeableController.registerPrice(name, req.duration); + vm.deal(user, price); + vm.prank(user); + upgradeableController.register{value: price}(req); + + bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE); + string memory storedName = NameResolver(LEGACY_L2_RESOLVER).name(baseRevNode); + string memory expectedFull = string.concat(name, legacyController.rootName()); + assertEq(keccak256(bytes(storedName)), keccak256(bytes(expectedFull)), "legacy reverse name not set"); + + ENS ens = ENS(REGISTRY); + assertEq(ens.resolver(node), UPGRADEABLE_L2_RESOLVER_PROXY); + assertEq(ens.owner(node), user); + + // L2 reverse should NOT be set without signature + string memory l2Name = l2ReverseRegistrar.nameForAddr(user); + assertTrue(keccak256(bytes(l2Name)) != keccak256(bytes(expectedFull)), "l2 reverse should not be set"); + } + + function test_set_primary_on_new_writes_both_paths_with_signature() public { + string memory name = "forknewprim"; + string memory fullName = string.concat(name, legacyController.rootName()); + uint256[] memory coinTypes = new uint256[](1); + coinTypes[0] = BASE_SEPOLIA_COINTYPE; + uint256 expiry = block.timestamp + 30 minutes; + bytes memory signature = _buildL2ReverseSignature(fullName, coinTypes, expiry); + + vm.prank(user); + upgradeableController.setReverseRecord(name, expiry, coinTypes, signature); + + bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE); + string memory storedLegacy = NameResolver(LEGACY_L2_RESOLVER).name(baseRevNode); + assertEq(keccak256(bytes(storedLegacy)), keccak256(bytes(fullName)), "legacy reverse not set"); + + string memory l2Name = l2ReverseRegistrar.nameForAddr(user); + assertEq(keccak256(bytes(l2Name)), keccak256(bytes(fullName)), "l2 reverse not set"); + } + + function _addressToBytes(address a) internal pure returns (bytes memory b) { + b = new bytes(20); + assembly { + mstore(add(b, 32), mul(a, exp(256, 12))) + } + } +}