Skip to content
12 changes: 12 additions & 0 deletions src/L2/interface/IL2ReverseRegistrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
30 changes: 30 additions & 0 deletions test/Fork/BaseSepoliaConstants.sol
Original file line number Diff line number Diff line change
@@ -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;
}
123 changes: 123 additions & 0 deletions test/Fork/BaseSepoliaForkBase.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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);

// Ensure legacy resolver authorizes the configured legacy reverse registrar
// and ensure the reverse parent node is owned by that registrar so claims succeed.
// 1) Ensure ENS owner(BASE_SEPOLIA_REVERSE_NODE) == LEGACY_REVERSE_REGISTRAR
bytes32 parentNode = BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE;
address currentOwner = ENS(REGISTRY).owner(parentNode);
if (currentOwner != LEGACY_REVERSE_REGISTRAR) {
vm.prank(currentOwner);
ENS(REGISTRY).setOwner(parentNode, LEGACY_REVERSE_REGISTRAR);
}
// 2) Ensure RegistrarController uses the configured legacy reverse registrar
try legacyController.reverseRegistrar() returns (IReverseRegistrar currentLegacyRR) {
if (address(currentLegacyRR) != LEGACY_REVERSE_REGISTRAR) {
address rcOwner = legacyController.owner();
vm.prank(rcOwner);
legacyController.setReverseRegistrar(IReverseRegistrar(LEGACY_REVERSE_REGISTRAR));
}
} catch {}
// 3) Approve RegistrarController as a controller on the ReverseRegistrar so it can set names on behalf of users
try ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setControllerApproval(LEGACY_GA_CONTROLLER, true) {}
catch {
address rrOwner = ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).owner();
vm.prank(rrOwner);
ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setControllerApproval(LEGACY_GA_CONTROLLER, true);
}
// 4) Approve upgradeable controller proxy
try ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setControllerApproval(UPGRADEABLE_CONTROLLER_PROXY, true) {}
catch {
address rrOwner2 = ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).owner();
vm.prank(rrOwner2);
ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setControllerApproval(UPGRADEABLE_CONTROLLER_PROXY, true);
}
}

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);
}
}
96 changes: 96 additions & 0 deletions test/Fork/ENSIP19DataMigrations.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
106 changes: 106 additions & 0 deletions test/Fork/ENSIP19LegacyFlows.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading