Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
env:
FOUNDRY_PROFILE: ci
BASE_SEPOLIA_RPC_URL: "https://sepolia.base.org"
BASE_MAINNET_RPC_URL: "https://mainnet.base.org"

jobs:
forge-test:
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ auto_detect_remappings = false
[rpc_endpoints]
sepolia="${SEPOLIA_RPC_URL}"
base-sepolia="${BASE_SEPOLIA_RPC_URL}"
base-mainnet="${BASE_MAINNET_RPC_URL}"

[etherscan]
sepolia={url = "https://api-sepolia.etherscan.io/api", key = "${ETHERSCAN_API_KEY}"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,19 @@ 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";
import {AbstractForkSuite} from "./AbstractForkSuite.t.sol";

contract ENSIP19DataMigrations is BaseSepoliaForkBase {
abstract contract AbstractENSIP19DataMigrations is AbstractForkSuite {
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);
RegistrarController legacyRC = RegistrarController(LEGACY_GA_CONTROLLER);
uint256 price = legacyRC.registerPrice(name, 365 days);
vm.deal(user, price);
vm.prank(user);
Expand All @@ -34,56 +32,54 @@ contract ENSIP19DataMigrations is BaseSepoliaForkBase {
name: name,
owner: user,
duration: 365 days,
resolver: BaseSepoliaConstants.LEGACY_L2_RESOLVER,
resolver: 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);
AddrResolver(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
);
L2Resolver(LEGACY_L2_RESOLVER).setRegistrarController(MIGRATION_CONTROLLER);

uint256 coinType = MigrationController(BaseSepoliaConstants.MIGRATION_CONTROLLER).coinType();
uint256 coinType = MigrationController(MIGRATION_CONTROLLER).coinType();

// Pre: ENSIP-11 (coinType) record should be empty
bytes memory beforeBytes = AddrResolver(BaseSepoliaConstants.LEGACY_L2_RESOLVER).addr(node, coinType);
bytes memory beforeBytes = AddrResolver(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);
MigrationController(MIGRATION_CONTROLLER).setBaseForwardAddr(nodes);

// Post: ENSIP-11 (coinType) forward addr set
bytes memory afterBytes = AddrResolver(BaseSepoliaConstants.LEGACY_L2_RESOLVER).addr(node, coinType);
bytes memory afterBytes = AddrResolver(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)
);
// Claim/set old reverse name via legacy flow as the user (persist prank across nested calls)
vm.startPrank(user);
ReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setNameForAddr(user, user, LEGACY_L2_RESOLVER, _fullName(name));
vm.stopPrank();

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)));
// Calculate node under chain-specific reverse parent
bytes32 parent = baseReverseParentNode();
console2.logBytes32(parent);
bytes32 node = keccak256(abi.encodePacked(parent, Sha3.hexAddress(user)));
console2.logBytes32(node);

vm.prank(rrOwner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ 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";
import {AbstractForkSuite} from "./AbstractForkSuite.t.sol";

contract ENSIP19LegacyFlows is BaseSepoliaForkBase {
abstract contract AbstractENSIP19LegacyFlows is AbstractForkSuite {
function test_register_name_on_legacy() public {
string memory name = "forkleg";
bytes32 root = legacyController.rootNode();
Expand Down Expand Up @@ -59,12 +58,13 @@ contract ENSIP19LegacyFlows is BaseSepoliaForkBase {
vm.prank(user);
legacyController.register{value: price}(req);

// Set primary via legacy ReverseRegistrar directly
vm.prank(user);
// Set primary via legacy ReverseRegistrar directly (persist prank across nested calls)
vm.startPrank(user);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why start prank for only one call?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's because the single prank is consumed by the call to _fullName that's happening during argument evaluation (since prank is only good for one external call), hence wrapping the whole expression in startPrank/stopPrank. Otherwise fails with unauthorized error bc caller isn't user.

IReverseRegistrar(LEGACY_REVERSE_REGISTRAR).setNameForAddr(user, user, LEGACY_L2_RESOLVER, _fullName(name));
vm.stopPrank();

// Validate reverse record was set on the legacy resolver
bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE);
bytes32 baseRevNode = _baseReverseNode(user, baseReverseParentNode());
string memory storedName = NameResolver(LEGACY_L2_RESOLVER).name(baseRevNode);
assertEq(keccak256(bytes(storedName)), keccak256(bytes(_fullName(name))), "reverse name not set");

Expand Down Expand Up @@ -93,9 +93,9 @@ contract ENSIP19LegacyFlows is BaseSepoliaForkBase {
legacyController.register{value: price}(req);

// Assert reverse was set by the controller calling the ReverseRegistrar
bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE);
bytes32 baseRevNode = _baseReverseNode(user, baseReverseParentNode());
string memory storedName = NameResolver(LEGACY_L2_RESOLVER).name(baseRevNode);
string memory expectedFull = string.concat(name, legacyController.rootName());
string memory expectedFull = _fullName(name);
assertEq(keccak256(bytes(storedName)), keccak256(bytes(expectedFull)), "reverse name not set by controller");

// Also verify forward resolver/owner as a sanity check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ 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;
import {AbstractForkSuite} from "./AbstractForkSuite.t.sol";

abstract contract AbstractENSIP19NewFlows is AbstractForkSuite {
function test_register_on_new_sets_forward_records_ensip11() public {
string memory name = "forknewfwd";
bytes32 root = legacyController.rootNode();
Expand All @@ -24,7 +21,7 @@ contract ENSIP19NewFlows is BaseSepoliaForkBase {
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));
data[1] = abi.encodeWithSelector(setAddrCointypeSel, node, baseCoinType(), _addressToBytes(user));

UpgradeableRegistrarController.RegisterRequest memory req = UpgradeableRegistrarController.RegisterRequest({
name: name,
Expand All @@ -49,7 +46,7 @@ contract ENSIP19NewFlows is BaseSepoliaForkBase {
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);
bytes memory coinAddr = AddrResolver(UPGRADEABLE_L2_RESOLVER_PROXY).addr(node, baseCoinType());
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");
Expand Down Expand Up @@ -77,7 +74,7 @@ contract ENSIP19NewFlows is BaseSepoliaForkBase {
vm.prank(user);
upgradeableController.register{value: price}(req);

bytes32 baseRevNode = _baseReverseNode(user, BaseSepoliaConstants.BASE_SEPOLIA_REVERSE_NODE);
bytes32 baseRevNode = _baseReverseNode(user, baseReverseParentNode());
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");
Expand All @@ -93,27 +90,20 @@ contract ENSIP19NewFlows is BaseSepoliaForkBase {

function test_set_primary_on_new_writes_both_paths_with_signature() public {
string memory name = "forknewprim";
string memory fullName = string.concat(name, legacyController.rootName());
string memory fullName = _fullName(name);
uint256[] memory coinTypes = new uint256[](1);
coinTypes[0] = BASE_SEPOLIA_COINTYPE;
coinTypes[0] = baseCoinType();
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);
bytes32 baseRevNode = _baseReverseNode(user, baseReverseParentNode());
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)))
}
}
}
119 changes: 119 additions & 0 deletions test/Fork/AbstractForkSuite.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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";

abstract contract AbstractForkSuite is Test {
// Network configuration hooks
function forkAlias() internal pure virtual returns (string memory);

function registry() internal pure virtual returns (address);
function baseRegistrar() internal pure virtual returns (address);
function legacyGaController() internal pure virtual returns (address);
function legacyL2Resolver() internal pure virtual returns (address);
function legacyReverseRegistrar() internal pure virtual returns (address);

function upgradeableControllerProxy() internal pure virtual returns (address);
function upgradeableL2ResolverProxy() internal pure virtual returns (address);

function ensL2ReverseRegistrar() internal pure virtual returns (address);
function l2Owner() internal pure virtual returns (address);
function migrationController() internal pure virtual returns (address);

function baseCoinType() internal pure virtual returns (uint256);
function baseReverseParentNode() internal pure virtual returns (bytes32);

// Actors
uint256 internal userPk;
address internal user;

// Interfaces
RegistrarController internal legacyController;
UpgradeableRegistrarController internal upgradeableController;
NameResolver internal legacyResolver;
IL2ReverseRegistrar internal l2ReverseRegistrar;

// Aliased constants for readability in scenarios
address internal REGISTRY;
address internal BASE_REGISTRAR;
address internal LEGACY_GA_CONTROLLER;
address internal LEGACY_L2_RESOLVER;
address internal LEGACY_REVERSE_REGISTRAR;
address internal UPGRADEABLE_CONTROLLER_PROXY;
address internal UPGRADEABLE_L2_RESOLVER_PROXY;
address internal ENS_L2_REVERSE_REGISTRAR;
address internal L2_OWNER;
address internal MIGRATION_CONTROLLER;

function setUp() public virtual {
vm.createSelectFork(forkAlias());

// Bind constants
REGISTRY = registry();
BASE_REGISTRAR = baseRegistrar();
LEGACY_GA_CONTROLLER = legacyGaController();
LEGACY_L2_RESOLVER = legacyL2Resolver();
LEGACY_REVERSE_REGISTRAR = legacyReverseRegistrar();
UPGRADEABLE_CONTROLLER_PROXY = upgradeableControllerProxy();
UPGRADEABLE_L2_RESOLVER_PROXY = upgradeableL2ResolverProxy();
ENS_L2_REVERSE_REGISTRAR = ensL2ReverseRegistrar();
L2_OWNER = l2Owner();
MIGRATION_CONTROLLER = migrationController();

// 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 view returns (string memory) {
// Use controller-provided root name to avoid hardcoding suffixes
return string.concat(name, legacyController.rootName());
}

function _baseReverseNode(address addr, bytes32 baseReverseParent) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(baseReverseParent, 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);
}

function _addressToBytes(address a) internal pure returns (bytes memory b) {
b = new bytes(20);
assembly {
mstore(add(b, 32), mul(a, exp(256, 12)))
}
}
}
Loading