From 07b6df254ed20867c4eb02e66a2d82bb6db6532d Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Tue, 9 Sep 2025 17:09:22 -0400 Subject: [PATCH 1/3] first idea --- contracts/reverseRegistrar/ReverseNamer.sol | 83 +++++++++++ contracts/test/mocks/MockReverseNamer.sol | 18 +++ .../reverseRegistrar/TestReverseNamer.test.ts | 141 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 contracts/reverseRegistrar/ReverseNamer.sol create mode 100644 contracts/test/mocks/MockReverseNamer.sol create mode 100644 test/reverseRegistrar/TestReverseNamer.test.ts diff --git a/contracts/reverseRegistrar/ReverseNamer.sol b/contracts/reverseRegistrar/ReverseNamer.sol new file mode 100644 index 000000000..d3796876d --- /dev/null +++ b/contracts/reverseRegistrar/ReverseNamer.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IReverseRegistrarStub { + function setName(string memory name) external; +} + +/// @notice https://docs.ens.domains/ensip/19/ +library ReverseNamer { + address constant REVERSE_REGISTRAR_MAINNET = address(0); // TODO: ENSv2 addr.reverse registrar + address constant REVERSE_REGISTRAR_MAINNET_ROLLUP = + 0x0000000000D8e504002cC26E3Ec46D81971C1664; + address constant REVERSE_REGISTRAR_TESTNET_ROLLUP = + 0x00000BeEF055f7934784D6d81b6BC86665630dbA; + + function registrarFromChain( + uint256 chainId + ) internal pure returns (address) { + if (isMainnet(chainId)) { + return REVERSE_REGISTRAR_MAINNET; + } else if (isMainnetRollup(chainId)) { + return REVERSE_REGISTRAR_MAINNET_ROLLUP; + } else if (isTestnetRollup(chainId)) { + return REVERSE_REGISTRAR_TESTNET_ROLLUP; + } else { + return address(0); + } + } + + function setName(string memory primary) internal { + address registrar = registrarFromChain(block.chainid); + if (registrar != address(0)) { + IReverseRegistrarStub(registrar).setName(primary); + } + } + + function isMainnet(uint256 chainId) internal pure returns (bool) { + return + chainId == 1 || // mainnet + chainId == 17000 || // holesky + chainId == 560048 || // hoodi + chainId == 11155111; // sepolia + } + + function isMainnetRollup(uint256 chainId) internal pure returns (bool) { + return + chainId == 10 || // optimism + chainId == 8453 || // base + chainId == 42161 || // arb1 + chainId == 59144 || // linea + chainId == 534352; // scroll + } + + function isTestnetRollup(uint256 chainId) internal pure returns (bool) { + return + chainId == 59141 || // linea-sepolia + chainId == 84532 || // base-sepolia + chainId == 421614 || // arb1-sepolia + chainId == 534351 || // scroll-sepolia + chainId == 11155420; // optimism-sepolia + } +} + +contract NameableBy { + address public nameOwner; + + constructor(address owner, string memory primary) { + nameOwner = owner; + if (bytes(primary).length > 0) { + ReverseNamer.setName(primary); + } + } + + function setNameOwner(address owner) public { + require(msg.sender == nameOwner); + nameOwner = owner; + } + + function setName(string memory primary) public { + require(msg.sender == nameOwner); + ReverseNamer.setName(primary); + } +} diff --git a/contracts/test/mocks/MockReverseNamer.sol b/contracts/test/mocks/MockReverseNamer.sol new file mode 100644 index 000000000..f37acc18a --- /dev/null +++ b/contracts/test/mocks/MockReverseNamer.sol @@ -0,0 +1,18 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ReverseNamer} from "../../reverseRegistrar/ReverseNamer.sol"; + +contract MockReverseNamer { + function registrarFromChain( + uint256 chainId + ) external pure returns (address) { + return ReverseNamer.registrarFromChain(chainId); + } +} + +contract MockNamedOnce { + constructor(string memory primary) { + ReverseNamer.setName(primary); + } +} diff --git a/test/reverseRegistrar/TestReverseNamer.test.ts b/test/reverseRegistrar/TestReverseNamer.test.ts new file mode 100644 index 000000000..01a7ceb71 --- /dev/null +++ b/test/reverseRegistrar/TestReverseNamer.test.ts @@ -0,0 +1,141 @@ +import hre from 'hardhat' +import { type Address, getAddress, zeroAddress } from 'viem' +import { readdirSync, readFileSync } from 'fs' +import { coinTypeFromChain } from '../fixtures/ensip19.js' + +const REVERSE_REGISTRAR_MAINNET_ROLLUP = + '0x0000000000D8e504002cC26E3Ec46D81971C1664' + +const connection = await hre.network.connect({ + override: { + chainId: 8453, // appear like an L2 + }, +}) + +async function fixture() { + const [owner] = await connection.viem.getWalletClients() + const publicClient = await connection.viem.getPublicClient() + const reverseNamer = await connection.viem.deployContract('MockReverseNamer') + const l2ReverseRegistrar0 = await connection.viem.deployContract( + 'L2ReverseRegistrar', + [coinTypeFromChain(connection.networkConfig.chainId!)], + ) + await connection.networkHelpers.setCode( + REVERSE_REGISTRAR_MAINNET_ROLLUP, + (await publicClient.getCode(l2ReverseRegistrar0))!, + ) + const l2ReverseRegistrar = await connection.viem.getContractAt( + 'L2ReverseRegistrar', + REVERSE_REGISTRAR_MAINNET_ROLLUP, + ) + return { + owner, + reverseNamer, + l2ReverseRegistrar, + } +} + +const PRIMARY = 'mycontract.eth' + +describe('ReverseNamer', () => { + describe('registryFromChain', () => { + const dir = new URL('../../deployments/', import.meta.url) + for (const chainName of readdirSync(dir)) { + try { + const chainId = BigInt( + readFileSync(new URL(`./${chainName}/.chainId`, dir), { + encoding: 'utf8', + }), + ) + const deploy = JSON.parse( + readFileSync(new URL(`./${chainName}/L2ReverseRegistrar.json`, dir), { + encoding: 'utf8', + }), + ) as { address: Address } + it(chainName, async () => { + const F = await connection.networkHelpers.loadFixture(fixture) + await expect( + F.reverseNamer.read.registrarFromChain([chainId]), + ).resolves.toStrictEqual(deploy.address) + }) + } catch (err) {} + } + }) + + it('NamedOnce', async () => { + const F = await connection.networkHelpers.loadFixture(fixture) + const contract = await connection.viem.deployContract('MockNamedOnce', [ + PRIMARY, + ]) + await expect( + F.l2ReverseRegistrar.read.nameForAddr([contract.address]), + ).resolves.toStrictEqual(PRIMARY) + }) + + describe('NameableBy', () => { + it('w/o primary', async () => { + const F = await connection.networkHelpers.loadFixture(fixture) + const contract = await connection.viem.deployContract('NameableBy', [ + F.owner.account.address, + '', + ]) + await expect( + F.l2ReverseRegistrar.read.nameForAddr([contract.address]), + 'primary', + ).resolves.toStrictEqual('') + await expect(contract.read.nameOwner()).resolves.toStrictEqual( + getAddress(F.owner.account.address), + ) + }) + + it('w/primary', async () => { + const F = await connection.networkHelpers.loadFixture(fixture) + const contract = await connection.viem.deployContract('NameableBy', [ + F.owner.account.address, + PRIMARY, + ]) + await expect( + F.l2ReverseRegistrar.read.nameForAddr([contract.address]), + 'primary', + ).resolves.toStrictEqual(PRIMARY) + }) + + it('setName', async () => { + const F = await connection.networkHelpers.loadFixture(fixture) + const contract = await connection.viem.deployContract('NameableBy', [ + F.owner.account.address, + PRIMARY, + ]) + const primary = 'new-name' + await contract.write.setName([primary]) + await expect( + F.l2ReverseRegistrar.read.nameForAddr([contract.address]), + ).resolves.toStrictEqual(primary) + }) + + it('disown', async () => { + const F = await connection.networkHelpers.loadFixture(fixture) + const contract = await connection.viem.deployContract('NameableBy', [ + F.owner.account.address, + PRIMARY, + ]) + await contract.write.setNameOwner([zeroAddress]) + await expect(contract.read.nameOwner()).resolves.toStrictEqual( + zeroAddress, + ) + }) + + it('not owner', async () => { + const contract = await connection.viem.deployContract('NameableBy', [ + zeroAddress, + PRIMARY, + ]) + await expect(contract.write.setNameOwner([zeroAddress])).rejects.toThrow( + 'reverted without a reason', + ) + await expect(contract.write.setName([''])).rejects.toThrow( + 'reverted without a reason', + ) + }) + }) +}) From 885f1347d1f6da39ada7ba6ff95c5d72725a7f76 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Tue, 9 Sep 2025 17:25:51 -0400 Subject: [PATCH 2/3] restore NamedOnce, add NotOwnable --- contracts/reverseRegistrar/ReverseNamer.sol | 18 +++++++++++++++++- contracts/test/mocks/MockReverseNamer.sol | 9 ++++----- test/reverseRegistrar/TestReverseNamer.test.ts | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/contracts/reverseRegistrar/ReverseNamer.sol b/contracts/reverseRegistrar/ReverseNamer.sol index d3796876d..0010aaff7 100644 --- a/contracts/reverseRegistrar/ReverseNamer.sol +++ b/contracts/reverseRegistrar/ReverseNamer.sol @@ -61,7 +61,20 @@ library ReverseNamer { } } -contract NameableBy { +/// @dev Compile-time guard to prevent being Ownable. +contract NotOwnable { + function owner() internal pure {} +} + +/// @notice Mixin for naming a contract once. +contract NamedOnce is NotOwnable { + constructor(string memory primary) { + ReverseNamer.setName(primary); + } +} + +/// @notice Mixin for delegated contract naming. +contract NameableBy is NotOwnable { address public nameOwner; constructor(address owner, string memory primary) { @@ -71,11 +84,14 @@ contract NameableBy { } } + /// @notice Change the namer. + /// @dev Use `address(0)` to revoke. function setNameOwner(address owner) public { require(msg.sender == nameOwner); nameOwner = owner; } + /// @notice Set contract primary name. function setName(string memory primary) public { require(msg.sender == nameOwner); ReverseNamer.setName(primary); diff --git a/contracts/test/mocks/MockReverseNamer.sol b/contracts/test/mocks/MockReverseNamer.sol index f37acc18a..88f80efb6 100644 --- a/contracts/test/mocks/MockReverseNamer.sol +++ b/contracts/test/mocks/MockReverseNamer.sol @@ -11,8 +11,7 @@ contract MockReverseNamer { } } -contract MockNamedOnce { - constructor(string memory primary) { - ReverseNamer.setName(primary); - } -} +// // PoC for compile-time error if Ownable +// import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +// import {NamedOnce} from "../../reverseRegistrar/ReverseNamer.sol"; +// contract MockNamedOnce is Ownable, NamedOnce("") {} diff --git a/test/reverseRegistrar/TestReverseNamer.test.ts b/test/reverseRegistrar/TestReverseNamer.test.ts index 01a7ceb71..9e3e4cc15 100644 --- a/test/reverseRegistrar/TestReverseNamer.test.ts +++ b/test/reverseRegistrar/TestReverseNamer.test.ts @@ -64,7 +64,7 @@ describe('ReverseNamer', () => { it('NamedOnce', async () => { const F = await connection.networkHelpers.loadFixture(fixture) - const contract = await connection.viem.deployContract('MockNamedOnce', [ + const contract = await connection.viem.deployContract('NamedOnce', [ PRIMARY, ]) await expect( From 2cf0dffa5d2773b3086cea47964ac7d1a66414e1 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Tue, 9 Sep 2025 17:31:38 -0400 Subject: [PATCH 3/3] minor --- contracts/reverseRegistrar/ReverseNamer.sol | 16 ++++++++-------- contracts/test/mocks/MockReverseNamer.sol | 3 +-- test/reverseRegistrar/TestReverseNamer.test.ts | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/reverseRegistrar/ReverseNamer.sol b/contracts/reverseRegistrar/ReverseNamer.sol index 0010aaff7..5798337f4 100644 --- a/contracts/reverseRegistrar/ReverseNamer.sol +++ b/contracts/reverseRegistrar/ReverseNamer.sol @@ -7,21 +7,21 @@ interface IReverseRegistrarStub { /// @notice https://docs.ens.domains/ensip/19/ library ReverseNamer { - address constant REVERSE_REGISTRAR_MAINNET = address(0); // TODO: ENSv2 addr.reverse registrar - address constant REVERSE_REGISTRAR_MAINNET_ROLLUP = + address constant MAINNET = address(0); // TODO: ENSv2 addr.reverse registrar + address constant MAINNET_ROLLUP = 0x0000000000D8e504002cC26E3Ec46D81971C1664; - address constant REVERSE_REGISTRAR_TESTNET_ROLLUP = + address constant TESTNET_ROLLUP = 0x00000BeEF055f7934784D6d81b6BC86665630dbA; function registrarFromChain( uint256 chainId ) internal pure returns (address) { if (isMainnet(chainId)) { - return REVERSE_REGISTRAR_MAINNET; + return MAINNET; } else if (isMainnetRollup(chainId)) { - return REVERSE_REGISTRAR_MAINNET_ROLLUP; + return MAINNET_ROLLUP; } else if (isTestnetRollup(chainId)) { - return REVERSE_REGISTRAR_TESTNET_ROLLUP; + return TESTNET_ROLLUP; } else { return address(0); } @@ -84,8 +84,8 @@ contract NameableBy is NotOwnable { } } - /// @notice Change the namer. - /// @dev Use `address(0)` to revoke. + /// @notice Set the name owner. + /// @dev Use `address(0)` to revoke ownership. function setNameOwner(address owner) public { require(msg.sender == nameOwner); nameOwner = owner; diff --git a/contracts/test/mocks/MockReverseNamer.sol b/contracts/test/mocks/MockReverseNamer.sol index 88f80efb6..b94ce965b 100644 --- a/contracts/test/mocks/MockReverseNamer.sol +++ b/contracts/test/mocks/MockReverseNamer.sol @@ -11,7 +11,6 @@ contract MockReverseNamer { } } -// // PoC for compile-time error if Ownable // import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; // import {NamedOnce} from "../../reverseRegistrar/ReverseNamer.sol"; -// contract MockNamedOnce is Ownable, NamedOnce("") {} +// contract MockNamedOnce is Ownable, NamedOnce("") {} // compile-time error if Ownable diff --git a/test/reverseRegistrar/TestReverseNamer.test.ts b/test/reverseRegistrar/TestReverseNamer.test.ts index 9e3e4cc15..21d98acb6 100644 --- a/test/reverseRegistrar/TestReverseNamer.test.ts +++ b/test/reverseRegistrar/TestReverseNamer.test.ts @@ -1,6 +1,6 @@ import hre from 'hardhat' import { type Address, getAddress, zeroAddress } from 'viem' -import { readdirSync, readFileSync } from 'fs' +import { readdirSync, readFileSync } from 'node:fs' import { coinTypeFromChain } from '../fixtures/ensip19.js' const REVERSE_REGISTRAR_MAINNET_ROLLUP =