Standard way to verify a signature when the signing entity is an ENS domain.
Abstract
Externally Owned Accounts (EOAs) can validate signed messages via ecrecover()
and smart contracts can do the same via specifications outlined in EIP-1271, but currently signatures cannot be made by or validated with an ENS domain. We propose a standard way for anyone to verify whether a signature made by an ENS domain is valid. This is possible via a modified signature validation function originally found in EIP-1271: isValidSignature(node, hash)
.
Motivation
Ethereum has, for the first time in history, made it possible for people from around the world to form groups and collectively reach consensus. Such groups are known as Decentralized Autonomous Organizations. DAO is a uniquely Web 3 concept that, in theory, forms a flat organizational tree as each of its members has equal representation and voting power. However, for any organization to efficiently function in practice, structures and hierarchies are often established, most commonly with the aid of ENS apex domains and subdomains if said organization is on-chain.
While we can establish orderly organizational entities using ENS domains, currently we cannot use said domains to sign messages. The fact that signatures are tied to an EOA instead of an ENS domain is a giant hurdle as DAO members are regularly voted in and out of office. Just as nobody should use a personal email address instead of their assigned business email address to sign work-related documents in the Web 2 world, no one should use their own EOA instead of DAO-assigned ENS domain to sign DAO-related messages.
Specification
The key words âMUSTâ, âMUST NOTâ, âREQUIREDâ, âSHALLâ, âSHALL NOTâ, âSHOULDâ, âSHOULD NOTâ, âRECOMMENDEDâ, âMAYâ, and âOPTIONALâ in this document are to be interpreted as described in RFC 2119.
pragma solidity ^0.8.0;
interface IENSIP {
/**
* @dev Should return whether the signature provided is valid for the provided node and hash
* @param node Namehash of the signing ENS node
* @param hash Hash of the data to be signed
*
* MUST return the bytes4 magic value 0xe0c5e6c3 when function passes.
* MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
* MUST allow external calls
*
*/
function isValidSignature(bytes32 node, bytes32 hash)
external
view
returns (bytes4 magicValue);
}
isValidSignature
can call arbitrary methods to validate a given signature.
This function can be implemented by anyone who wishes to maintain an ENS signature registry.
Rationale
Unlike EIP-6066, this proposal enables anyone to validate signatures made by ENS domains that are not wrapped into an NFT. Wrapped ENS domains have their benefits but makes it difficult for domain owners to reassign subdomains, something often done in hierarchical organizations.
We have purposefully decided to not include a signature generation standard in this proposal as it would restrict flexibility of such mechanism, just as EIP-1271 does not enforce a signing standard for smart contracts.
Backwards Compatibility
This ENSIP is incompatible with previous work on signature validation as it does not validate any cryptographically generated signatures. Instead, signature is merely a boolean flag indicating consent. This is consistent with Gnosis Safeâs contract signature implementation.
Reference Implementation
Example implementation of a contract that conforms to ENSIP with a custom signing function:
pragma solidity ^0.8.0;
import "./interfaces/draft-IENSIP.sol";
import "@ensdomains/ens-contracts/contracts/registry/ENS.sol";
contract ENSSignatureRegistry is IENSIP {
bytes4 public constant MAGICVALUE = 0xe0c5e6c3;
bytes4 public constant BADVALUE = 0xffffffff;
ENS public ens;
mapping(bytes32 => mapping(bytes32 => bool)) internal _signatures;
error ENotNodeOwner();
/**
* @dev Checks if the sender owns the signing node
* @param node Namehash of the signing ENS node
*/
modifier onlyNodeOwner(bytes32 node) {
if (ens.owner(node) != msg.sender) revert ENotNodeOwner();
_;
}
function setENS(ENS _ens) external {
ens = _ens;
}
/**
* @dev Should sign the provided hash with ENS node given sender owns said node
* @param node Namehash of the signing ENS node
* @param hash Hash of the data to be signed
*/
function sign(bytes32 node, bytes32 hash) external onlyNodeOwner(node) {
_signatures[node][hash] = true;
}
/**
* @dev Should return whether the signature provided is valid for the provided node and hash
*/
function isValidSignature(bytes32 node, bytes32 hash)
external
view
override
returns (bytes4 magicValue)
{
return _signatures[node][hash] ? MAGICVALUE : BADVALUE;
}
}
Security Considerations
The revokable nature of contract-based signatures carries over to this ENSIP standard. With the ability to reassign subnode owners at will, it is possible for a domain owner to revoke signatures made by their subdomains. Developers and users alike should take this into consideration.
References
- EIP-1271: Standard Signature Validation Method for Contracts: EIPs/eip-1271.md at master · ethereum/EIPs · GitHub
- EIP-6066: Signature Validation Method for NFTs: EIPs/eip-6066.md at nft-sig · boyuanx/EIPs · GitHub
- SignMessageLib from Gnosis Safe: safe-contracts/SignMessageLib.sol at main · safe-global/safe-contracts · GitHub