ERC-6538 Stealth Meta-Address Registry
Final · Accounts & wallets · created 2023-01-24
A single shared registry per chain where stealth meta-addresses are registered on-chain and can be looked up by anyone. This lets senders send assets without revealing the recipient's actual address.
Why it is needed
To receive assets at a stealth address defined by ERC-5564, the sender must first know the recipient's stealth meta-address. But if there is no fixed place to find this meta-address, the parties must exchange it separately or depend on an off-chain service. Smart contracts cannot read information that exists only off-chain, so it is hard for services such as ENS or Gnosis Safe to add stealth address features automatically. If each project builds its own storage, the same user's meta-address is scattered across many places and standardization becomes difficult. This standard defines a shared registry deployed once per chain, so that anyone can register and look up meta-addresses in the same place. Registering in the registry also serves as a signal that the registrant is ready to take part in stealth transactions.
What to implement
function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external;: Registers the caller's own stealth meta-address for the given schemeId.function incrementNonce() external;: Increments the caller's nonce by 1, invalidating all delegated registration signatures that were already created.function DOMAIN_SEPARATOR() external view returns (bytes32);: Returns the EIP-712 domain separator used by this contract.function stealthMetaAddressOf(address registrant, uint256 schemeId) external view returns (bytes memory);: Returns the stealth meta-address stored for the given registrant and schemeId.function ERC6538REGISTRY_ENTRY_TYPE_HASH() external view returns (bytes32);: Returns the EIP-712 type hash used by registerKeysOnBehalf.function nonceOf(address registrant) external view returns (uint256);: Returns the current nonce of the given registrant.event NonceIncremented(address indexed registrant, uint256 newNonce);: Emitted with the new nonce value when a registrant increments their nonce.error ERC6538Registry__InvalidSignature();: The error raised when an invalid signature is passed to registerKeysOnBehalf.
In depth
A singleton registry, one per chain
ERC6538Registry MUST be a singleton contract with only one instance per chain. The deployment address is 0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538, and it is deployed with CREATE2 through the deterministic deployer 0x4e59b44847b379578588920ca78fbf26c0b4956c. The salt used is 0x7cac4e512b1768c627c9e711c7a013f1ad0766ef5125c59fb7161dade58da078. The registrant can be identified by an address, an ENS name, or another identifier.
Storage structure and schemeId
The meta-address is stored as bytes in a double mapping of the form stealthMetaAddressOf[registrant][schemeId]. schemeId is a 1-byte integer that distinguishes stealth address schemes, and this standard defines schemeId 1 as the SECP256k1 curve scheme with view tags specified in ERC-5564. ERC-5564 writes a meta-address as st:<shortName>:0x<spendingPubKey>:<viewingPubKey>, but since the chain is already determined by the chain on which the registry is deployed, the stored value is the byte string formed by concatenating the compressed spendingPubKey and viewingPubKey.
Registering directly with registerKeys
registerKeys(schemeId, stealthMetaAddress) stores the meta-address in the caller's (msg.sender) entry for that schemeId and emits the StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress) event. Because it changes only the caller's own entry, there is no separate signature verification, and calling it again overwrites the value, which updates the meta-address.
registerKeysOnBehalf and signature verification
A function that lets a third party register on behalf of a registrant. The signed payload is structured data built from the EIP-712 type hash Erc6538RegistryEntry(uint256 schemeId,bytes stealthMetaAddress,uint256 nonce). It includes the current nonceOf[registrant] value, and the nonce is incremented by 1 on each call. If the signature length is 65 bytes, the signer address is recovered with ecrecover and compared with registrant. If the recovered address is 0 or differs from registrant, the function calls isValidSignature on the registrant contract to verify again as an EIP-1271 signature, and if the return value differs from the selector, it reverts with the ERC6538Registry__InvalidSignature error. In this way both EOA signatures and contract wallet signatures are supported.
Nonce and incrementNonce
nonceOf stores the next nonce to use for each registrant. It increases by 1 on every valid registerKeysOnBehalf call, so the same signature cannot be used twice. A registrant can call incrementNonce directly to raise the nonce, which invalidates all signatures created before that. When called, the NonceIncremented(registrant, newNonce) event is emitted.
Domain separator and chain fork handling
The EIP-712 domain separator is computed from the name "ERC6538Registry", the version "1.0", the chain ID, and the contract address. The chain ID at deployment (INITIAL_CHAIN_ID) and the domain separator (INITIAL_DOMAIN_SEPARATOR) are stored, and DOMAIN_SEPARATOR() returns the stored value if the current block.chainid equals the one at deployment, and recomputes it otherwise. The original code comment states that the domain separator is recomputed if a chain fork occurs.
Why use a central registry
The original text gives four reasons. First, other smart contracts can easily look up and use registered meta-addresses, which ensures interoperability and lets applications such as ENS and Gnosis Safe integrate stealth addresses into their services. Second, fetching a user's meta-address does not need to depend on an off-chain source. Third, registering in this contract is itself a standard declaration of readiness to take part in stealth transactions. Fourth, because it is deployed as a singleton, multiple projects share the same set of meta-addresses, which contributes to standardization.
Things to watch out for
- If a key is leaked, deregister immediately: If a private key is leaked, the registrant must deregister immediately from the stealth key registry. This prevents losing assets that would otherwise arrive later at the compromised account.