Contract Naming

I’ve had a new idea for contract naming that I’d like to discuss. Some prior ideas were discussed in this github PR. New idea discussed here and expanded below:


interface IContractName {
    function contractName() external view returns (string memory);
}

On L1, a contract can name itself by implementing IContractName. The detection method can be ERC-165 or a blind call with a valid response. The addr.reverse resolver can implement this check. This dodges the ownership requirement.

On L2 (or any contract that interfaces with the crosschain reverse registrars), since we want names stored in one contract for read simplicity (so the name needs stored into the registrar), the only option currently is to implement owner().

However, a similar mechanism like L1 would work: there can be a function on the registrar that anyone can call, and if the address supports IContractName, it copies that name accordingly.

In both situations, the requirement would be the response must be onchain.

I believe this is significantly simpler than ReverseClaimer (current solution) and ReverseNamer (prior suggestion).


Example usage:

contract MyContract {
     string public contractName = "mycontract.eth";
}

// if deployed on L1
// 1. setAddr(`mycontract.eth`, 60, <MyContract>);
// 2. named!

// if deployed on L2 
// 1. setAddr(`mycontract.eth`, <chain>, <MyContract>);
// 2a. L2ReverseRegistrar.syncContractName(<MyContract>); // called by anyone
// 2b. L2ReverseRegistrar.syncContractName(); // called by contract
// 3. named!

An alternative option:

contract MyContract {
     address public contractNamer = 0x51050ec063d393217B436747617aD1C2285Aeeee;
}

As Ownable is often not the right construction. In ENSv2, many contracts use EAC which doesn’t have a single owner, however typically there is someone who would be responsible for naming, eg. DAO names core ENS contracts.

There’s also a more generalized contractNamer()-like interface:

interface IContractNameable {
    function isContractNamer(address account) external view returns (bool);
}

The advantage being you could have an EAC role like ROLE_NAMER and implement the following:

contract MyContract is EnhancedAccessContract, IContractNameable {
    function isContractNamer(address account) external view returns (bool) {
        return hasRootRoles(ROLE_NAMER, account);
    }
}

Ownable would use:

contract MyContract is Ownable, IContractNameable {
    function isContractNamer(address account) external view returns (bool) {
        return account == owner();
    }
}

tl;dr

  • Reduce complexity so it’s simple to name your contract.
  • Modify addr.reverse resolver to ask mainnet contracts for their name directly.
  • Add alternative to Ownable for L2ReverseRegistrar.
4 Likes

Cool to see. We’re supportive of any mechanisms that can simplify naming for contracts.

Having another option that doesn’t require Ownable and simplifies the implementation seems like a useful step forward.

Longer term, woud still love to see a standard that doesn’t require any custom code in the contract, but this is definitely useful.

We can use ERC-8049: Contract-Level Onchain Metadata and then just define a key. They key could be “ens_name”.

Would this approach alleviate the need for what @raffy is proposing?

If new approaches for primary naming are to be established for contracts, its beneficial if we can keep the messaging simple and ideally land on a "preferred approach” from within the ENS community.

Then perhaps we can try to get more teams accross it.

Using ERC-8049 the interface would be:


interface IERC8049 {

   /// @noticenotice Get contract metadata value for a key.
   function getContractMetadata(string calldata key) external view returns (bytes memory);

   /// @notice Emitted when contract metadata is updated.
   event ContractMetadataUpdated(string indexed indexedKey, string key, bytes value);
}

and, the function call would look like:

getContractMetadata(“ens_name”);

IContactName is basically the same thing as INameResolver without bytes32 node but can be written as string public contractName = "mycontract.eth" which is brain-dead simple and has no dependencies.

I believe contract naming is just <contractAddress> → <name>. ENSv1 dogfooded resolvers for {addr}.addr.reverse but I see no value in custom reverse resolvers and would prefer a simpler construction (like the crosschain registrars.)


Ultimately, I’m proposing (2) separate things: first, a simplification of the naming process:

  • on L1, when you reverse(<contractAddress>, 60) and {contactAddress}.addr.reverse has no resolver, then resolver(addr.reverse) invokes IContractName(<contractAddress>).contractName() (or INameResolver(<contractAddress>).name(…)) and uses that value.
    • This removes the need to claim {addr}.addr.reverse
    • This removes the need to create a resolver to store name
    • This removes the need for delegation for immutable names
  • on L2 since the contract is on a different chain, your contract would implement the same getter but instead of the resolver calling your contract, anyone could call ReverseRegistrar.syncName(<contractAddress>) which copies the name from the contract to the registrar, for which the crosschain reverse resolvers can already fetch and prove, and reverse(<contractAddress>, <supportedL2CoinType>) work as expected.

Second, the actual convention for naming the contract, which has 2 styles but not sure what’s the best:

  1. The contract has a name: IContractName or INameResolver
  2. The contract has a namer: Ownable(1 account) or IContractNamer(multiple accounts)

My take on IERC8049 support would be that you implement the following:

function contractName() external view returns (string memory) {
     return getContractMetadata("ens_name"); 
     // or whatever key makes sense, eg. "eth.ens.name"
}
1 Like

Of the possibilities, I think the following is the best:

contract MyContract {
     string public contractPrimaryName = "mycontract.eth";
}

and L2ReverseRegistrar.syncName(<contractAddress>)

  • minimal code to support naming (import not necessary)
  • same technique works on L1/L2
  • contracts can decide their own logic for adjusting contractPrimaryName

Note: changed to contractPrimaryName since contractName is not unique enough.
Other suggestions welcome: ensContractName? myENSName?


EAC contracts would likely use something like:

function contractPrimaryName() onlyRootRoles(...) external view returns (string memory) {
    return ...;
}

Most core ENSv2 contracts would use:

public string contractPrimaryName = "thing.ens.eth";

To implement:

  • on L1: addr.reverse resolver gains the ability to call contractPrimaryName directly on contracts when reached during reverse resolution
  • on L2: L2ReverseRegstrar gains syncName() to import contractPrimaryName directly from contract

Fine with me. Can you make a PR for:

3 Likes

Yeah, I can update when PR is reviewed.

  • could be changed to IENSIP2X?
  • currently using ensContractName

I think this should make contract naming very simple!

1 Like