Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-6538: Stealth Meta-Address Registry

A canonical contract for entities to register stealth meta-addresses directly or through a third party using signatures.

Authors Matt Solomon (@mds1), Toni Wahrstätter (@nerolation), Ben DiFrancesco (@apbendi), Vitalik Buterin (@vbuterin), Gary Ghayrat (@garyghayrat)
Created 2023-01-24
Requires EIP-712, EIP-1271, EIP-5564

Abstract

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 scehemes.

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
pragma solidity 0.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.
contract ERC6538Registry {
  /// @notice Emitted when an invalid signature is provided to `registerKeysOnBehalf`.
  error ERC6538Registry__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(address registrant => mapping(uint256 schemeId => bytes)) public stealthMetaAddressOf;

  /// @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(address registrant => uint256) public nonceOf;

  /// @notice The EIP-712 type hash used in `registerKeysOnBehalf`.
  bytes32 public constant ERC6538REGISTRY_ENTRY_TYPE_HASH =
    keccak256("Erc6538RegistryEntry(uint256 schemeId,bytes stealthMetaAddress,uint256 nonce)");

  /// @notice The chain ID where this contract is initially deployed.
  uint256 internal immutable INITIAL_CHAIN_ID;

  /// @notice The domain separator used in this contract.
  bytes32 internal immutable INITIAL_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.
  event StealthMetaAddressSet(
    address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress
  );

  /// @notice Emitted when a registrant increments their nonce.
  /// @param registrant The account that incremented the nonce.
  /// @param newNonce The new nonce value.
  event NonceIncremented(address indexed registrant, uint256 newNonce);

  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.
  function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external {
    stealthMetaAddressOf[msg.sender][schemeId] = stealthMetaAddress;
    emit StealthMetaAddressSet(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.
  function registerKeysOnBehalf(
    address registrant,
    uint256 schemeId,
    bytes memory signature,
    bytes calldata stealthMetaAddress
  ) external {
    bytes32 dataHash;
    address recoveredAddress;

    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) {
      bytes32 r;
      bytes32 s;
      uint8 v;
      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
          )
      )
    ) revert ERC6538Registry__InvalidSignature();

    stealthMetaAddressOf[registrant][schemeId] = stealthMetaAddress;
    emit StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress);
  }

  /// @notice Increments the nonce of the sender to invalidate existing signatures.
  function incrementNonce() external {
    unchecked {
      nonceOf[msg.sender]++;
    }
    emit NonceIncremented(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.
  function DOMAIN_SEPARATOR() public view returns (bytes32) {
    return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator();
  }

  /// @notice Computes the domain separator for this contract.
  function _computeDomainSeparator() internal view returns (bytes32) {
    return keccak256(
      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].
interface IERC1271 {
  /// @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
  function isValidSignature(bytes32 hash, bytes memory signature)
    external
    view
    returns (bytes4 magicValue);
}

The interface for this contract is defined below:

// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.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.
interface IERC6538Registry {
  /// @notice Emitted when an invalid signature is provided to `registerKeysOnBehalf`.
  error ERC6538Registry__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.
  event StealthMetaAddressSet(
    address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress
  );

  /// @notice Emitted when a registrant increments their nonce.
  /// @param registrant The account that incremented the nonce.
  /// @param newNonce The new nonce value.
  event NonceIncremented(address indexed registrant, uint256 newNonce);

  /// @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.
  function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) 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.
  function registerKeysOnBehalf(
    address registrant,
    uint256 schemeId,
    bytes memory signature,
    bytes calldata stealthMetaAddress
  ) external;

  /// @notice Increments the nonce of the sender to invalidate existing signatures.
  function incrementNonce() external;

  /// @notice Returns the domain separator used in this contract.
  function DOMAIN_SEPARATOR() external view returns (bytes32);

  /// @notice Returns the stealth meta-address for the given `registrant` and `schemeId`.
  function stealthMetaAddressOf(address registrant, uint256 schemeId)
    external
    view
    returns (bytes memory);

  /// @notice Returns the EIP-712 type hash used in `registerKeysOnBehalf`.
  function ERC6538REGISTRY_ENTRY_TYPE_HASH() external view returns (bytes32);

  /// @notice Returns the nonce of the given `registrant`.
  function nonceOf(address registrant) external view returns (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:

  1. 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.

  2. It ensures that users are not dependent on off-chain sources to retrieve a user’s stealth meta-address.

  3. 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.

  4. 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.sol here.

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.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Matt Solomon (@mds1), Toni Wahrstätter (@nerolation), Ben DiFrancesco (@apbendi), Vitalik Buterin (@vbuterin), Gary Ghayrat (@garyghayrat), "ERC-6538: Stealth Meta-Address Registry [DRAFT]," Ethereum Improvement Proposals, no. 6538, January 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6538.