Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7897: Wallet-Linked Services for Smart Accounts

Define a registry for modular services linked to ERC-4337 wallets.

Authors Francesco Sullo (@sullof)
Created 2024-04-15
Discussion Link https://ethereum-magicians.org/t/generalized-wallet-linked-services-for-erc-4337-wallets/23028
Requires EIP-165, EIP-1167, EIP-4337, EIP-6551, EIP-7656

Abstract

This proposal defines a registry for generic services linked to smart accounts, with a special focus on ERC-4337 wallets, where services are contracts extending a wallet’s functionality, owned by the wallet itself. It leverages ERC-1167 minimal proxies and deterministic addressing to enable permissionless innovation while maintaining backward compatibility with existing ERC-4337 wallets. To reach its goal, it takes the concept introduced with ERC-6551 and ERC-7656 standards that work for NFTs, and applies it to wallets.

Motivation

ERC-4337 (Account Abstraction) introduces programmable smart accounts. Existing proposals to extend wallet functionalities (e.g., ERC-6900) focus on internal modules. This proposal generalizes the concept of service binding, allowing any ERC-4337 wallet to attach external services (e.g., recovery, automation, compliance) without requiring changes to the wallet’s core logic.

By enabling modular, non-invasive extensions, this standard fosters an open ecosystem of wallet-linked services while ensuring backward compatibility with existing ERC-4337 wallets.

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.

Registry Interface

The interface IERC7897Registry is defined as follows:

interface IERC7897Registry {
    /**
     * @notice Emitted when a wallet-linked service is successfully deployed.
       * @param deployedService The address of the deployed contract
       * @param serviceImplementation The address of the implementation contract
       * @param salt The salt used for the CREATE2 operation
       * @param chainId The chain ID where the contract is deployed
       * @param wallet The address of the ERC-4337 wallet
       */
    event ServiceDeployed(
        address deployedService,
        address indexed serviceImplementation,
        bytes32 salt,
        uint256 chainId,
        address indexed wallet
    );
    
    /**
     * @notice Thrown when the CREATE2 operation fails to deploy the contract.
       */
    error DeployFailed();
    
    /**
     * @notice Deploys a wallet-linked service for an ERC-4337 wallet.  
       * If the service already exists, returns its address without calling CREATE2.  
       * @param serviceImplementation The address of the implementation contract  
       * @param salt The salt used for the CREATE2 operation  
       * @param wallet The address of the ERC-4337 wallet  
       * Emits a {ServiceDeployed} event.  
       * @return service The address of the wallet-linked service  
       */
    function deployService(
        address serviceImplementation,
        bytes32 salt,
        address wallet
    ) external returns (address service);
    
    /**
     * @notice Computes the expected wallet-linked service address for an ERC-4337 wallet  
       * without deploying it.  
       * @param serviceImplementation The address of the implementation contract  
       * @param salt The salt used for the CREATE2 operation  
       * @param chainId The chain ID where the service would be deployed  
       * @param wallet The address of the ERC-4337 wallet  
       * @return service The computed address of the wallet-linked service  
       */
    function serviceAddress(
        address serviceImplementation,
        bytes32 salt,
        uint256 chainId,
        address wallet
    ) external view returns (address service);
}

Deployment Requirements

The registry MUST deploy each wallet-linked service as an ERC-1167 minimal proxy with immutable constant data appended to the bytecode.

The deployed bytecode of each wallet-linked service MUST have the following structure:

ERC-1167 Header                      (10 bytes)
<serviceImplementation (address)>    (20 bytes)
ERC-1167 Footer                      (15 bytes)
<salt (bytes32)>                     (32 bytes)
<chainId (uint256)>                  (32 bytes)
<wallet (address)>                   (20 bytes)

Any contract created using an ERC7897Registry SHOULD implement the IERC7897Service interface:

interface IERC7897Service {
  /**
  * @notice Returns the wallet linked to the contract
  * @return chainId The chainId of the wallet
  * @return wallet The address of the [ERC-4337](/EIPS/eip-4337) wallet
  */
  function wallet() external view returns (uint256 chainId, address wallet);
}

Access Control

Services SHOULD implement access control to restrict critical operations to the wallet owner. For example:

function owner() public view returns (address) {
  (, address wallet) = IERC7897Service(address(this)).wallet();
  return wallet;
}

modifier onlyOwner() {
  require(msg.sender == owner(), "Unauthorized");
  _;
}

Rationale

The technical foundation of ERC-7897 centers on the extension and generalization of contract types that can be associated with ERC-4337 wallets. Key decisions include:

  • Flexibility: Enables any ERC-4337 wallet to attach external services without modifying its core logic.

  • Permissionless Innovation: Developers can deploy services for any wallet, fostering an open ecosystem.

  • Backward Compatibility: Works with existing ERC-4337 wallets, including Safe, Argent, and Biconomy.

  • Deterministic Addressing: Uses CREATE2 + salt/chainId/wallet for predictable service deployments.

Reference Implementation

// This implementation is a variation of the ERC6551Registry contract written by Jayden Windle @jaydenwindle and Vectorized @vectorized
 
contract ERC7897Registry is IERC7897Registry {
  function deployService(
    address serviceImplementation,
    bytes32 salt,
    address wallet
  ) external override returns (address) {
    // solhint-disable-next-line no-inline-assembly
    assembly {
    // Memory Layout:
    // ----
    // 0x00   0xff                           (1 byte)
    // 0x01   registry (address)             (20 bytes)
    // 0x15   salt (bytes32)                 (32 bytes)
    // 0x35   Bytecode Hash (bytes32)        (32 bytes)
    // ----
    // 0x55   ERC-1167 Constructor + Header  (20 bytes)
    // 0x69   implementation (address)       (20 bytes)
    // 0x5D   ERC-1167 Footer                (15 bytes)
    // 0x8C   salt (uint256)                 (32 bytes)
    // 0xAC   chainId (uint256)              (32 bytes)
    // 0xCC   wallet (address)               (20 bytes)

    // Copy bytecode + constant data to memory
      mstore(0x8c, salt) // salt
      mstore(0xac, chainid()) // chainId
      mstore(0xcc, wallet) // wallet address (20 bytes)
      mstore(0x6c, 0x5af43d82803e903d91602b57fd5bf3) // ERC-1167 footer
      mstore(0x5d, serviceImplementation) // implementation
      mstore(0x49, 0x3d60ad80600a3d3981f3363d3d373d3d3d363d73) // ERC-1167 constructor + header

    // Copy create2 computation data to memory
      mstore8(0x00, 0xff) // 0xFF
      mstore(0x35, keccak256(0x55, 0x8b)) // keccak256(bytecode) - 0x8b = 139 bytes
      mstore(0x01, shl(96, address())) // registry address
      mstore(0x15, salt) // salt

    // Compute service address
      let computed := keccak256(0x00, 0x55)

    // If the service has not yet been deployed
      if iszero(extcodesize(computed)) {
      // Deploy service contract
        let deployed := create2(0, 0x55, 0x8b, salt) // 0x8b = 139 bytes

      // Revert if the deployment fails
        if iszero(deployed) {
          mstore(0x00, 0xd786d393) // `DeployFailed()`
          revert(0x1c, 0x04)
        }

      // Emit the ServiceDeployed event
        mstore(0x00, deployed) // deployedService
        mstore(0x20, serviceImplementation) // serviceImplementation
        mstore(0x40, salt) // salt
        mstore(0x60, chainid()) // chainId
        mstore(0x80, wallet) // wallet

        log4(
          0x00, // Start of data
          0xa0, // Data length (160 bytes: deployed + implementation + salt + chainId + wallet)
          0x2f82bd0c129ea2d065cf394fb7760031982c6278372c89e1a059f2478ddf4763, // Event signature hash
          deployed, // indexed deployedService
          serviceImplementation, // indexed serviceImplementation
          salt, // salt
          chainid(), // chainId
          wallet // indexed wallet
        )

      // Return the service address
        return(0x00, 0x20)
      }

    // Otherwise, return the computed service address
      mstore(0x00, computed)
      return(0x00, 0x20)
    }
  }

  function serviceAddress(
    address serviceImplementation,
    bytes32 salt,
    uint256 chainId,
    address wallet
  ) external view override returns (address) {
    // solhint-disable-next-line no-inline-assembly
    assembly {
    // Copy bytecode + constant data to memory
      mstore(0x8c, salt) // salt
      mstore(0xac, chainId) // chainId
      mstore(0xcc, wallet) // wallet address (20 bytes)
      mstore(0x6c, 0x5af43d82803e903d91602b57fd5bf3) // ERC-1167 footer
      mstore(0x5d, serviceImplementation) // implementation
      mstore(0x49, 0x3d60ad80600a3d3981f3363d3d373d3d3d363d73) // ERC-1167 constructor + header

    // Copy create2 computation data to memory
      mstore8(0x00, 0xff) // 0xFF
      mstore(0x35, keccak256(0x55, 0x8b)) // keccak256(bytecode) - 0x8b = 139 bytes
      mstore(0x01, shl(96, address())) // registry address
      mstore(0x15, salt) // salt

    // Compute and return the service address
      mstore(0x00, keccak256(0x00, 0x55))
      return(0x00, 0x20)
    }
  }
}

Security Considerations

Ownership and Control

Wallet-linked services MUST be controlled by the ERC-4337 wallet owner to prevent unauthorized access. Implementers SHOULD include safeguards against malicious or unverified implementations.

Upgradeability Risks

If a service is upgradable, ensure secure upgrade mechanisms to prevent unauthorized changes. For example:

  • The owner of the service SHOULD be the wallet itself.

  • Only the wallet SHOULD be able to upgrade the implementation of the service.

  • Implement versioning to ensure backward compatibility between upgrades.

  • Use a timelock or multisig for critical upgrades to reduce the risk of malicious changes.

Reentrancy and Cross-Contract Interactions

Services interacting with external protocols SHOULD follow best practices to prevent reentrancy attacks.

User Education

Clear user interfaces and warnings SHOULD be provided to reduce phishing and social engineering risks.

Testing

Implementers SHOULD thoroughly test the registry and services on testnets to ensure correctness and security before deploying to mainnet.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Francesco Sullo (@sullof), "ERC-7897: Wallet-Linked Services for Smart Accounts [DRAFT]," Ethereum Improvement Proposals, no. 7897, April 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7897.