The following specifies a system for services to link their users to a claimable Ethereum address. Services can provide a signed message and unique salt to their users which can be used to deploy a smart contract wallet to the deterministic address through a registry contract using the create2 opcode.
Motivation
It is common for web services to allow their users to hold on-chain assets via custodial wallets. These wallets are typically EOAs, deployed smart contract wallets or omnibus contracts, with private keys or asset ownership information stored on a traditional database. This proposal outlines a solution that avoids the security concerns associated with historical approaches, and rids the need and implications of services controlling user assets
Users on external services that choose to leverage the following specification can be given an Ethereum address to receive assets without the need to do any on-chain transaction. These users can choose to attain control of said addresses at a future point in time. Thus, on-chain assets can be sent to and owned by a user beforehand, therefore enabling the formation of an on-chain identity without requiring the user to interact with the underlying blockchain.
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.
Overview
The system for creating reserved ownership accounts consists of:
An Account Registry which provides deterministic addresses based on the service users’ identifying salts, and implements a signature verified function that enables claiming of Account Instances by the service’s end users.
Account Instances created through the Account Registry by end users which allow access to the assets received at the deterministic address prior to Account Instance deployment.
External services wishing to provide their users with reserved ownership accounts MUST maintain a relationship between a user’s identifying credentials and a salt. The external service SHALL refer to an Account Registry Instance to retrieve the deterministic account address for a given salt. Users of a given service MUST be able to create an Account Instance by validating their identifying credentials via the external service, which SHOULD give the user a signed message for their salt. Signatures SHOULD be generated by the external service using an signing address known to the Account Registry Instance. Users SHALL pass this message and signature to the service’s Account Registry Instance in a call to claimAccount to deploy and claim an Account Instance at the deterministic address.
Account Registry
The Account Registry MUST implement the following interface:
interfaceIAccountRegistry{/**
* @dev Registry instances emit the AccountCreated event upon successful account creation
*/eventAccountCreated(addressaccount,addressaccountImplementation,uint256salt);/**
* @dev Registry instances emit the AccountClaimed event upon successful claim of account by owner
*/eventAccountClaimed(addressaccount,addressowner);/**
* @dev Creates a smart contract account.
*
* If account has already been created, returns the account address without calling create2.
*
* @param salt - The identifying salt for which the user wishes to deploy an Account Instance
*
* Emits AccountCreated event
* @return the address for which the Account Instance was created
*/functioncreateAccount(uint256salt)externalreturns(address);/**
* @dev Allows an owner to claim a smart contract account created by this registry.
*
* If the account has not already been created, the account will be created first using `createAccount`
*
* @param owner - The initial owner of the new Account Instance
* @param salt - The identifying salt for which the user wishes to deploy an Account Instance
* @param expiration - If expiration > 0, represents expiration time for the signature. Otherwise
* signature does not expire.
* @param message - The keccak256 message which validates the owner, salt, expiration
* @param signature - The signature which validates the owner, salt, expiration
*
* Emits AccountClaimed event
* @return the address of the claimed Account Instance
*/functionclaimAccount(addressowner,uint256salt,uint256expiration,bytes32message,bytescalldatasignature)externalreturns(address);/**
* @dev Returns the computed address of a smart contract account for a given identifying salt
*
* @return the computed address of the account
*/functionaccount(uint256salt)externalviewreturns(address);/**
* @dev Fallback signature verification for unclaimed accounts
*/functionisValidSignature(bytes32hash,bytesmemorysignature)externalviewreturns(bytes4);}
createAccount
createAccount is used to deploy the Account Instance for a given salt.
This function MUST deploy a new Account Instance as a ERC-1167 proxy pointing to the account implementation.
This function SHOULD set the initial owner of the Account Instance to the Account Registry Instance.
The account implementation address MUST be immutable, as it is used to compute the deterministic address for the Account Instance.
Upon successful deployment of the Account Instance, the registry SHOULD emit an AccountCreated event.
claimAccount
claimAccount is used to claim ownership of the Account Instance for a given salt.
This function MUST create a new Account Instance if one does not already exist for the given salt.
This function SHOULD verify that the msg.sender has permission to claim ownership over the Account Instance for the identifying salt and initial owner. Verification SHOULD be done by validating the message and signature against the owner, salt and expiration using ECDSA for EOA signers, or ERC-1271 for smart contract signers.
This function SHOULD verify that the block.timestamp < expiration or that expiration == 0.
Upon successful signature verification on calls to claimAccount, the registry MUST completely relinquish control over the Account Instance, and assign ownership to the initial owner by calling setOwner on the Account Instance.
Upon successful claim of the Account Instance, the registry SHOULD emit an AccountClaimed event.
isValidSignature
isValidSignature is a fallback signature verification function used by unclaimed accounts. Valid signatures SHALL be generated by the registry signer by signing a composite hash of the original message hash, and the Account Instance address (e.g. bytes32 compositeHash = keccak256(abi.encodePacked(originalHash, accountAddress))). The function MUST reconstruct the composite hash, where originalHash is the hash passed to the function, and accountAddress is msg.sender (the unclaimed Account Instance). The function MUST verify the signature against the composite hash and registry signer.
Account Instance
The Account Instance MUST implement the following interface:
interfaceIAccountisIERC1271{/**
* @dev Sets the owner of the Account Instance.
*
* Only callable by the current owner of the instance, or by the registry if the Account
* Instance has not yet been claimed.
*
* @param owner - The new owner of the Account Instance
*/functionsetOwner(addressowner)external;}
All Account Instances MUST be created using an Account Registry Instance.
Account Instances SHOULD provide access to assets previously sent to the address at which the Account Instance is deployed to.
setOwner SHOULD update the owner and SHOULD be callable by the current owner of the Account Instance.
If an Account Instance is deployed, but not claimed, the owner of the Account Instance MUST be initialized to the Account Registry Instance.
An Account Instance SHALL determine if it has been claimed by checking if the owner is the Account Registry Instance.
Account Instance Signatures
Account Instances MUST support ERC-1271 by implementing an isValidSignature function. When the owner of an Account Instance wants to sign a message (e.g. to log in to a dApp), the signature MUST be generated in one of the following ways, depending the state of the Account Instance:
If the Account instance is deployed and claimed, the owner should generate the signature, and isValidSignature SHOULD verify that the message hash and signature are valid for the current owner of the Account Instance.
If the Account Instance is deployed, but unclaimed, the registry signer should generate the signature using a composite hash of the original message and address of the Account Instance described above, and isValidSignature SHOULD forward the message hash and signature to the Account Registry Instance’s isValidSignature function.
If the Account Instance is not deployed, the registry signer should generate a signature on the composite hash as done in situation 2, and wrap the signature according to ERC-6492 (e.g. concat(abi.encode((registryAddress, createAccountCalldata, compositeHashSignature), (address, bytes, bytes)), magicBytes)).
Signature validation for Account Instances should be done according to ERC-6492.
Rationale
Service-Owned Registry Instances
While it might seem more user-friendly to implement and deploy a universal registry for reserved ownership accounts, we believe that it is important for external service providers to have the option to own and control their own Account Registry. This provides the flexibility of implementing their own permission controls and account deployment authorization frameworks.
We are providing a reference Registry Factory which can deploy Account Registries for an external service, which comes with:
Immutable Account Instance implementation
Validation for the claimAccount method via ECDSA for EOA signers, or ERC-1271 validation for smart contract signers
Ability for the Account Registry deployer to change the signing addressed used for claimAccount validation
Account Registry and Account Implementation Coupling
Since Account Instances are deployed as ERC-1167 proxies, the account implementation address affects the addresses of accounts deployed from a given Account Registry. Requiring that registry instances be linked to a single, immutable account implementation ensures consistency between a user’s salt and linked address on a given Account Registry Instance.
This also allows services to gain the the trust of users by deploying their registries with a reference to a trusted account implementation address.
Furthermore, account implementations can be designed as upgradeable, so users are not necessarily bound to the implementation specified by the Account Registry Instance used to create their account.
Separate createAccount and claimAccount Operations
Operations to create and claim Account Instances are intentionally separate. This allows services to provide users with valid ERC-6492 signatures before their Account Instance has been deployed.
Reference Implementation
The following is an example of an Account Registry Factory which can be used by external service providers to deploy their own Account Registry Instance.
Deployment of reserved ownership accounts through an Account Registry Instance through calls to createAccount could be front-run by a malicious actor. However, if the malicious actor attempted to alter the owner parameter in the calldata, the Account Registry Instance would find the signature to be invalid, and revert the transaction. Thus, any successful front-running transaction would deploy an identical Account Instance to the original transaction, and the original owner would still gain control over the address.