This specification defines a standardized way of storing and retrieving an entity’s stealth meta-address, by extending ERC-5564. An entity may register their stealth meta-address directly. A third party can also register on behalf of an entity using a valid EIP-712 or EIP-1271 signature. Once registered, the stealth meta-address for the entity can be retrieved by any smart contract or user. One can use the stealth meta-address with generateStealthAddress specified in ERC-5564 to send assets to the generated stealth address without revealing the entity’s address.
Motivation
The standardization of stealth address generation holds the potential to greatly enhance the privacy capabilities of Ethereum by enabling the recipient of a transfer to remain anonymous when receiving an asset. By introducing a central smart contract for users to store their stealth meta-addresses, EOAs and contracts can programmatically engage in stealth interactions using a variety of stealth address schemes.
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.
This contract defines an ERC6538Registry that stores the stealth meta-address for entities. These entities may be identified by an address, ENS name, or other identifier. This MUST be a singleton contract, with one instance per chain.
The contract is specified below. A one byte integer is used to identify the stealth address scheme. This integer is used to differentiate between different stealth address schemes. This ERC outlines schemeId 1 as the SECP256k1 curve cryptographic scheme with view tags, as specified in ERC-5564.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity0.8.23;/// @notice `ERC6538Registry` contract to map accounts to their stealth meta-address. See
/// [ERC-6538](https://eips.ethereum.org/EIPS/eip-6538) to learn more.
contractERC6538Registry{/// @notice Emitted when an invalid signature is provided to `registerKeysOnBehalf`.
errorERC6538Registry__InvalidSignature();/// @notice Next nonce expected from `user` to use when signing for `registerKeysOnBehalf`.
/// @dev `registrant` may be a standard 160-bit address or any other identifier.
/// @dev `schemeId` is an integer identifier for the stealth address scheme.
mapping(addressregistrant=>mapping(uint256schemeId=>bytes))publicstealthMetaAddressOf;/// @notice A nonce used to ensure a signature can only be used once.
/// @dev `registrant` is the user address.
/// @dev `nonce` will be incremented after each valid `registerKeysOnBehalf` call.
mapping(addressregistrant=>uint256)publicnonceOf;/// @notice The EIP-712 type hash used in `registerKeysOnBehalf`.
bytes32publicconstantERC6538REGISTRY_ENTRY_TYPE_HASH=keccak256("Erc6538RegistryEntry(uint256 schemeId,bytes stealthMetaAddress,uint256 nonce)");/// @notice The chain ID where this contract is initially deployed.
uint256internalimmutableINITIAL_CHAIN_ID;/// @notice The domain separator used in this contract.
bytes32internalimmutableINITIAL_DOMAIN_SEPARATOR;/// @notice Emitted when a registrant updates their stealth meta-address.
/// @param registrant The account that registered the stealth meta-address.
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
/// secp256k1, as specified in ERC-5564.
/// @param stealthMetaAddress The stealth meta-address.
/// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) bases the format for stealth
/// meta-addresses on [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) and specifies them as:
/// st:<shortName>:0x<spendingPubKey>:<viewingPubKey>
/// The chain (`shortName`) is implicit based on the chain the `ERC6538Registry` is deployed on,
/// therefore this `stealthMetaAddress` is just the compressed `spendingPubKey` and
/// `viewingPubKey` concatenated.
eventStealthMetaAddressSet(addressindexedregistrant,uint256indexedschemeId,bytesstealthMetaAddress);/// @notice Emitted when a registrant increments their nonce.
/// @param registrant The account that incremented the nonce.
/// @param newNonce The new nonce value.
eventNonceIncremented(addressindexedregistrant,uint256newNonce);constructor(){INITIAL_CHAIN_ID=block.chainid;INITIAL_DOMAIN_SEPARATOR=_computeDomainSeparator();}/// @notice Sets the caller's stealth meta-address for the given scheme ID.
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
/// secp256k1, as specified in ERC-5564.
/// @param stealthMetaAddress The stealth meta-address to register.
functionregisterKeys(uint256schemeId,bytescalldatastealthMetaAddress)external{stealthMetaAddressOf[msg.sender][schemeId]=stealthMetaAddress;emitStealthMetaAddressSet(msg.sender,schemeId,stealthMetaAddress);}/// @notice Sets the `registrant`'s stealth meta-address for the given scheme ID.
/// @param registrant Address of the registrant.
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
/// secp256k1, as specified in ERC-5564.
/// @param signature A signature from the `registrant` authorizing the registration.
/// @param stealthMetaAddress The stealth meta-address to register.
/// @dev Supports both EOA signatures and EIP-1271 signatures.
/// @dev Reverts if the signature is invalid.
functionregisterKeysOnBehalf(addressregistrant,uint256schemeId,bytesmemorysignature,bytescalldatastealthMetaAddress)external{bytes32dataHash;addressrecoveredAddress;unchecked{dataHash=keccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR(),keccak256(abi.encode(ERC6538REGISTRY_ENTRY_TYPE_HASH,schemeId,keccak256(stealthMetaAddress),nonceOf[registrant]++))));}if(signature.length==65){bytes32r;bytes32s;uint8v;assembly("memory-safe"){r:=mload(add(signature,0x20))s:=mload(add(signature,0x40))v:=byte(0,mload(add(signature,0x60)))}recoveredAddress=ecrecover(dataHash,v,r,s);}if(((recoveredAddress==address(0)||recoveredAddress!=registrant)&&(IERC1271(registrant).isValidSignature(dataHash,signature)!=IERC1271.isValidSignature.selector)))revertERC6538Registry__InvalidSignature();stealthMetaAddressOf[registrant][schemeId]=stealthMetaAddress;emitStealthMetaAddressSet(registrant,schemeId,stealthMetaAddress);}/// @notice Increments the nonce of the sender to invalidate existing signatures.
functionincrementNonce()external{unchecked{nonceOf[msg.sender]++;}emitNonceIncremented(msg.sender,nonceOf[msg.sender]);}/// @notice Returns the domain separator used in this contract.
/// @dev The domain separator is re-computed if there's a chain fork.
functionDOMAIN_SEPARATOR()publicviewreturns(bytes32){returnblock.chainid==INITIAL_CHAIN_ID?INITIAL_DOMAIN_SEPARATOR:_computeDomainSeparator();}/// @notice Computes the domain separator for this contract.
function_computeDomainSeparator()internalviewreturns(bytes32){returnkeccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),keccak256("ERC6538Registry"),keccak256("1.0"),block.chainid,address(this)));}}/// @notice Interface of the ERC1271 standard signature validation method for contracts as defined
/// in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271].
interfaceIERC1271{/// @notice Should return whether the signature provided is valid for the provided data
/// @param hash Hash of the data to be signed
/// @param signature Signature byte array associated with _data
functionisValidSignature(bytes32hash,bytesmemorysignature)externalviewreturns(bytes4magicValue);}
The interface for this contract is defined below:
// SPDX-License-Identifier: CC0-1.0
pragmasolidity0.8.23;/// @dev Interface for calling the `ERC6538Registry` contract to map accounts to their stealth
/// meta-address. See [ERC-6538](https://eips.ethereum.org/EIPS/eip-6538) to learn more.
interfaceIERC6538Registry{/// @notice Emitted when an invalid signature is provided to `registerKeysOnBehalf`.
errorERC6538Registry__InvalidSignature();/// @dev Emitted when a registrant updates their stealth meta-address.
/// @param registrant The account that registered the stealth meta-address.
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
/// secp256k1, as specified in ERC-5564.
/// @param stealthMetaAddress The stealth meta-address.
/// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) bases the format for stealth
/// meta-addresses on [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) and specifies them as:
/// st:<shortName>:0x<spendingPubKey>:<viewingPubKey>
/// The chain (`shortName`) is implicit based on the chain the `ERC6538Registry` is deployed on,
/// therefore this `stealthMetaAddress` is just the `spendingPubKey` and `viewingPubKey`
/// concatenated.
eventStealthMetaAddressSet(addressindexedregistrant,uint256indexedschemeId,bytesstealthMetaAddress);/// @notice Emitted when a registrant increments their nonce.
/// @param registrant The account that incremented the nonce.
/// @param newNonce The new nonce value.
eventNonceIncremented(addressindexedregistrant,uint256newNonce);/// @notice Sets the caller's stealth meta-address for the given scheme ID.
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
/// secp256k1, as specified in ERC-5564.
/// @param stealthMetaAddress The stealth meta-address to register.
functionregisterKeys(uint256schemeId,bytescalldatastealthMetaAddress)external;/// @notice Sets the `registrant`'s stealth meta-address for the given scheme ID.
/// @param registrant Address of the registrant.
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
/// secp256k1, as specified in ERC-5564.
/// @param signature A signature from the `registrant` authorizing the registration.
/// @param stealthMetaAddress The stealth meta-address to register.
/// @dev Supports both EOA signatures and EIP-1271 signatures.
/// @dev Reverts if the signature is invalid.
functionregisterKeysOnBehalf(addressregistrant,uint256schemeId,bytesmemorysignature,bytescalldatastealthMetaAddress)external;/// @notice Increments the nonce of the sender to invalidate existing signatures.
functionincrementNonce()external;/// @notice Returns the domain separator used in this contract.
functionDOMAIN_SEPARATOR()externalviewreturns(bytes32);/// @notice Returns the stealth meta-address for the given `registrant` and `schemeId`.
functionstealthMetaAddressOf(addressregistrant,uint256schemeId)externalviewreturns(bytesmemory);/// @notice Returns the EIP-712 type hash used in `registerKeysOnBehalf`.
functionERC6538REGISTRY_ENTRY_TYPE_HASH()externalviewreturns(bytes32);/// @notice Returns the nonce of the given `registrant`.
functionnonceOf(addressregistrant)externalviewreturns(uint256);}
Deployment Method
The ERC6538Registry contract is deployed at 0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538 using CREATE2 via the deterministic deployer at 0x4e59b44847b379578588920ca78fbf26c0b4956c with a salt of 0x7cac4e512b1768c627c9e711c7a013f1ad0766ef5125c59fb7161dade58da078.
Rationale
Having a central smart contract for registering stealth meta-addresses has several benefits:
It guarantees interoperability with other smart contracts, as they can easily retrieve and utilize the registered stealth meta-addresses. This enables applications such as ENS or Gnosis Safe to use that information and integrate stealth addresses into their services.
It ensures that users are not dependent on off-chain sources to retrieve a user’s stealth meta-address.
Registration of a stealth meta-address in this contract provides a standard way for users to communicate that they’re ready to participate in stealth interactions.
By deploying the registry as a singleton contract, multiple projects can access the same set of stealth meta-addresses, contributing to improved standardization.
Backwards Compatibility
This EIP is fully backward compatible.
Reference Implementation
You can find an implementation of the ERC6538Registry contract here and the interface IERC6538Registry.solhere.
Security Considerations
In the event of a compromised private key, the registrant should promptly un-register from the stealth key registry to prevent loss of future funds sent to the compromised account.