Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

ENSIP-28: ENS Name Owned Accounts

Authors: premm.eth, workemon.eth, raffy.eth

Created: August 13, 2026

Status: draft

Abstract

This ENSIP lets an ENS name list more than one account per chain, for example a Safe or a token bound account, and requires each listed account to prove that it consents to the association. The proof is a signature over an EIP-712 typed message, stored as a data record alongside the listing. It covers EOAs, deployed smart accounts (via ERC-1271), and token bound accounts (ERC-6551).

Motivation

ENS names often represent a user identity, for both human users and AI agents. A user frequently controls more than one account on a chain and wants those accounts associated with a single ENS name. ENSIP-9 resolves one address per chain, which suits a payment address but not an identity that spans several accounts.

An association is only meaningful if both sides agree to it. A record written by the name owner alone is a claim about someone else's account, and anyone who controls a name can name any address in it. This ENSIP therefore defines both halves: how a name lists its accounts, and how each account proves that it consents to being listed.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Account Data Records

An ENS name lists its accounts on a chain as an ENSIP-24 data record on its own node under the reserved key:

accounts[<chain-id>]

where <chain-id> is the EVM chain ID as an unsigned base-10 integer without leading zeros.

The value MUST be the accounts encoded as consecutive 32-byte words, each holding an address in its low 20 bytes:

abi.encode(address account1, address account2, ...)

The number of accounts is the length of the value divided by 32. Accounts MAY appear in any order.

A name MAY additionally publish the chains it has accounts on under the reserved key:

accounts-chains

whose value MUST be abi.encode(uint256 chainId1, uint256 chainId2, ...). Clients MAY instead discover keys by indexing DataChanged events, defined in ENSIP-24.

Proof Data Records

The proof for an account listed under accounts[<chain-id>] is stored on the same node under the reserved key:

accounts[<chain-id>][<account>]

with the same <chain-id> as the listing it proves, and where <account> is 0x followed by the lowercase hexadecimal representation of the listed account's address bytes. The value MUST be:

abi.encode(uint256 expiresAt, bytes signature)

Each account listed in accounts[<chain-id>] MUST have a record at its key. An account whose record is absent or empty is not verified.

expiresAt is a Unix timestamp in seconds and MUST be non-zero. The proof is invalid at and after that time, and is valid for at most the two years preceding it. Until a proof lapses, an account cannot withdraw consent unilaterally, and removal rests with the name owner. Signers SHOULD choose the shortest window that suits the association.

signature is a signature over the message defined below. It MUST NOT be empty. A listed account MUST therefore be able to produce an ECDSA signature or to validate one under ERC-1271. A contract account MUST be deployed before it can be listed. A token bound account is deployed through the ERC-6551 registry's createAccount.

Consent Message

The signature MUST be over the following EIP-712 typed data:

domain = {
    name:    "ENS Name Owned Accounts",
    version: "1",
    chainId: <the chain the name's records are on>
}
 
struct Consent {
    bytes32 node;           // namehash of the listing ENS name
    uint256 accountChainId; // the chain the listed account is on
    address account;        // the listed account
    uint256 expiresAt;      // must equal the record's expiresAt
}

The Consent type hash is 0x8dff32c14409d396587044aa7663b77f60b1d374ec13710f3f0acdc3a810a94d. The domain type hash, over EIP712Domain(string name,string version,uint256 chainId), is 0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e, which gives these domain separators:

ChainchainIdDomain separator
Ethereum Mainnet10xefbd5fd6831eeaf1b039217878898cb25c5afb12a1789321e1e4b870d56bdab0
Sepolia111551110x0e8de794261382cbb6e04b05bebc36d7e32e3461ee7f992d81fb7236a1419cd6

The domain's chainId is the chain the name's records are on. It binds a proof to one ENS deployment, so a proof signed against a name in one deployment does not verify against the same name in another.

The domain contains no verifyingContract. These proofs are not verified by any one contract, so there is nothing for it to name, and its absence lets a proof survive a migration of the name's records to a new resolver.

Verification

To determine a name's accounts on a chain, a client:

  1. Reads accounts[<chain-id>] and decodes it as consecutive 32-byte words, each holding an address in its low 20 bytes.
  2. For each listed account, reads accounts[<chain-id>][<account>] and decodes it as (uint256 expiresAt, bytes signature). An account whose record is absent or empty is not verified.
  3. Rejects the record if expiresAt is zero, if signature is empty, if the current time is at or past expiresAt, or if expiresAt is more than two years after the current time. A verifier running on chain uses block.timestamp as the current time.
  4. Reconstructs the Consent struct, taking node from the name being resolved, accountChainId from the record key, account from the listed account, and expiresAt from the record. Computes the EIP-712 digest, taking the domain chainId from the chain the name's records were resolved from, and validates signature for the listed account on the chain named by <chain-id>. Validation MUST be attempted in this order:
    1. If the account has code, validate it by calling isValidSignature (ERC-1271) on account. Validation succeeds only if the call returns the ERC-1271 magic value 0x1626ba7e. A revert, or any other return value, is a failure of that account's verification and MUST NOT affect any other account.
    2. Otherwise, validate it by ecrecover. Validation succeeds only if the recovered address equals account and is not the zero address.

Each account is verified independently. An account that fails any step is not an owned account of the name, and clients MUST NOT present it as one. Because the struct is reconstructed from context, a proof copied to another name, chain, account, or ENS deployment fails verification.

Relation to Address Records

The name's address records under ENSIP-1 and ENSIP-9 continue to resolve its primary accounts. Listed accounts are data records, not address records.

Meaning of a Verified Owned Account

A listed account becomes a verified owned account of the name when its proof verifies. Both sides have then acted: the name owner wrote the listing, and the account authorised a signature consenting to it.

An owned account is not a payment address. Clients and explorers MUST NOT display the ENS name in place of the account address, and clients SHOULD NOT send funds to an owned account. The name's address records remain its payment addresses.

Ethereum Example

The name agent.example.eth lists two accounts on Ethereum mainnet, chain ID 1:

data(namehash("agent.example.eth"), "accounts[1]") =
  0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045
    000000000000000000000000b8c2c29ee19d8307cb7255e1cd9cbde883a267d5

The proofs for those accounts, both expiring at 1798761600 (2027-01-01):

data(namehash("agent.example.eth"),
     "accounts[1][0xd8da6bf26964af9d7eed9e03e53415d37aa96045]")
  = abi.encode(uint256(1798761600), 0x...)
 
data(namehash("agent.example.eth"),
     "accounts[1][0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5]")
  = abi.encode(uint256(1798761600), 0x...)

Rationale

  • One signature scheme for all signing accounts. EIP-712 signatures verify by ecrecover for EOAs and via ERC-1271 for contract accounts, including ERC-6551 token bound accounts, which are required to implement ERC-1271 and by default treat the holder of the bound NFT as a valid signer. Standard library functions already implement this cascade, for example OpenZeppelin's SignatureChecker, so clients need one code path.
  • The account is bound inside the struct. ERC-1271 only answers whether a digest is validly signed for a given account, and one signing key often controls several accounts. Without account in the struct, a consent signed for one account would verify for every account under the same signer. Binding accountChainId likewise stops a proof for a contract at a given address on one chain from verifying the same address on another.
  • Expiry is what makes consent withdrawable. A signature cannot be unsigned, so a required expiresAt is what gives an account a way out: it stops renewing and the listing lapses within a window of at most two years. ERC-1271 proofs additionally stop verifying when the account rotates its signers, which for an ERC-6551 account means a proof lapses when the owning token is transferred.

Backwards Compatibility

This ENSIP requires no change to existing resolution standards and introduces no new resolver profile method.

Security Considerations

  • Proofs survive a transfer of the ENS name, so an owned account consented to the name, not to its current owner. Bounded validity windows mitigate this.
  • A proof shows that the account's signer consented to the association, not that the name and the account are controlled by the same party. Two cooperating parties can produce the same records.
  • An account delegated under EIP-7702 has code, so it is verified through its delegate under ERC-1271. Such an account can be listed only while its delegate implements ERC-1271, and changing the delegate can stop existing proofs from verifying.
  • A token bound account validates signatures against the current holder of its bound token, which it can read only on its own chain. An account bound to a token on another chain cannot be listed.

Copyright

Copyright and related rights waived via CC0.