Skip to content
Draft
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
99 changes: 99 additions & 0 deletions contracts/reverseRegistrar/ReverseNamer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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 MAINNET = address(0); // TODO: ENSv2 addr.reverse registrar

Choose a reason for hiding this comment

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

need to add TESTNET or is it same as MAINNET address? since we have distinct MAINNET_ROLLUP and TESTNET_ROLLUP

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this will mean all mainnets (mainnet/sepolia/...).

I speculatively designed ETHReverseResolver as the v2 replacement for addr.reverse that uses the same tech as the rollups (independent registry + wildcard resolver) but I think we're going with a proper claimable {addr}.addr.reverse that uses the v2 design, but it should be a fixed address deployment and the same on those chains. And If not, you're correct.

address constant MAINNET_ROLLUP =
0x0000000000D8e504002cC26E3Ec46D81971C1664;
address constant TESTNET_ROLLUP =
0x00000BeEF055f7934784D6d81b6BC86665630dbA;

function registrarFromChain(
uint256 chainId
) internal pure returns (address) {
if (isMainnet(chainId)) {
return MAINNET;
} else if (isMainnetRollup(chainId)) {
return MAINNET_ROLLUP;
} else if (isTestnetRollup(chainId)) {
return TESTNET_ROLLUP;
} else {

Choose a reason for hiding this comment

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

on line 10 - MAINNET = address(0) so is the else case

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
}
}

/// @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 {

Choose a reason for hiding this comment

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

NotOwnable in case of NamedOnce makes sense as no one can later name the contract but in case of NameableBy doesnt make much sense to it.

NameableBy can be seen in a contract with Ownable as an extra delegate who can name contract but still if contract has Owner he can revoke the delegation. I might be wrong but limiting NameableBy to only non ownable contracts doesn't make much sense to me

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I wasn't sure about this design. NameableBy could be removed. My thinking was you want something like Ownable but without implementing Ownable which at a glance implies the contract is mutable.

NameableBy + Ownable would give you 2 accounts that can change the name, but maybe that should be allowed.

I was thinking of it more like a trust curve:

  1. NamedOnce — set once
  2. NameableBy — set later, revoke it / delegate to someone to name it
  3. NameableBy — delegate to someone, change whenever
  4. Ownable — change whenever, but shares with other Ownable responsibilities

address public nameOwner;

constructor(address owner, string memory primary) {
nameOwner = owner;
if (bytes(primary).length > 0) {
ReverseNamer.setName(primary);
}
}

/// @notice Set the name owner.
/// @dev Use `address(0)` to revoke ownership.
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);
}
}
16 changes: 16 additions & 0 deletions contracts/test/mocks/MockReverseNamer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//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);
}
}

// import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
// import {NamedOnce} from "../../reverseRegistrar/ReverseNamer.sol";
// contract MockNamedOnce is Ownable, NamedOnce("") {} // compile-time error if Ownable
141 changes: 141 additions & 0 deletions test/reverseRegistrar/TestReverseNamer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import hre from 'hardhat'
import { type Address, getAddress, zeroAddress } from 'viem'
import { readdirSync, readFileSync } from 'node: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('NamedOnce', [
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',
)
})
})
})