AlertSourceDiscuss
Skip to content
On this page

ERC-6492: Signature Validation for Predeploy Contracts

A way to verify a signature when the account is a smart contract that has not been deployed yet

⚠️ DraftERC

Draft Notice

This EIP is in the process of being drafted. The content of this EIP is not final and can change at any time; this EIP is not yet suitable for use in production. Thank you!

AuthorsIvo Georgiev (@Ivshti)
Created2023-02-10

Abstract

Contracts can sign verifyable messages via ERC-1271.

However, if the contract is not deployed yet, ERC-1271 verification is impossible, as you can't call the isValidSignature function on said contract.

We propose a standard way for any contract or off-chain actor to verify whether a signature on behalf of a given counterfactual contract (that is not deployed yet) is valid. This standarad way extends ERC-1271.

Motivation

With the popularity of ERC-4337 and other account abstraction initiatives, we often find that the best user experience for contract wallets is to defer contract deployment until the first user transaction, therefore not burdening the user with an additional deploy step before they can use their account. However, at the same time, many dApps expect signatures, not only for interactions, but also just for logging in.

As such, contract wallets have been limited in their ability to sign messages before their de-facto deployment, which is often done on the first transaction.

Furthermore, not being able to sign messages from counterfactual contracts has always been a limitation of ERC-1271.

The initial motivation is discussed in the eth-infinitism/account-abstraction repo.

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.

Quoting ERC-1271,

isValidSignature can call arbitrary methods to validate a given signature, which could be context dependent (e.g. time based or state based), EOA dependent (e.g. signers authorization level within smart wallet), signature scheme Dependent (e.g. ECDSA, multisig, BLS), etc.

This function should be implemented by contracts which desire to sign messages (e.g. smart contract wallets, DAOs, multisignature wallets, etc.) Applications wanting to support contract signatures should call this method if the signer is a contract.

We use the same isValidSignature function, but we add a new wrapper signature format, that signing contracts MAY use before they're deployed, in order to allow support for verification.

The signature verifier MUST perform a contract deployment before attempting to call isValidSignature if the wrapper signature format is detected.

The wrapper format is detected by checking if the signature ends in magicBytes, which MUST be defined as 0x6492649264926492649264926492649264926492649264926492649264926492.

It is RECOMMENDED to use this ERC with CREATE2 contracts, as their deploy address is always predictable.

Signer side

The signer will normally be a contract wallet, but it could be any other counterfactual contract as well.

  • If the contract is deployed, produce a normal ERC-1271 signature
  • If the contract is not deployed yet, wrap the signature as follows: concat(abi.encodePacked((create2Factory, factoryCalldata, originalERC1271Signature), (address, bytes32, bytes, bytes, bytes)), magicBytes)

Note that we're passing factoryCalldata instead of salt and bytecode. We do this in order to make verification compliant with any factory interface. We do not need to calculate the address based on create2Factory/salt/bytecode, because ERC-1271 verification presumes we already know the account address we're verifying the signature for.

Verifier side

Full signature verification MUST be performed in the following order:

  • check if the signature ends with magic bytes, in which case do an eth_call to a multicall contract that will call the factory first with the factoryCalldata and deploy the contract; Then, call contract.isValidSignature as usual
  • check if there's contract code at the address. If so perform ERC-1271 verification as usual by invoking isValidSignature
  • If all this fails, try ecrecover verification

Rationale

We believe that wrapping the signature in a way that allows to pass the deploy data is the only clean way to implement this, as it's completely contract agnostic, but also easy to verify.

The wrapper format ends in magicBytes, which ends with a 0x92, which makes it is impossible for it to collide with a valid ecrecover signature if packed in the r,s,v format, as 0x92 is not a valid value for v. To avoid collisions with normal ERC-1271, magicBytes itself is also quite long (bytes32).

The verification order to ensure correct verification is based on the following rules:

  • checking for magicBytes MUST happen before the usual ERC-1271 check in order to allow counterfactual signatures to be valid even after contract deployment
  • checking for magicBytes MUST happen before ecrecover in order to avoid trying to verify a counterfactual contract signature via ecrecover if such is clearly identifiable
  • checking ecrecover MUST NOT happen before ERC-1271 verification, because a contract may use a signature format that also happens to be a valid ecrecover signature for an EOA with a different address. One such example is a contract that's a wallet controlled by said EOA.

Backwards Compatibility

This ERC is backward compatible with previous work on signature validation, including ERC-1271 and allows for easy verification of all signature types, including EOA signatures and typed data (EIP-712).

Reference Implementation

Example implementation of a verification contract:

solidity
contract ERC6492Verifier {
  /**
   * @notice Verifies that the signature is valid for that signer and hash
   */  
  function isValidUniversalSignature(
    address _signer,
    bytes32 _hash,
    bytes calldata _signature
  ) external override returns (bool) {
    uint32 contractSize;
    assembly {
      size := extcodesize(_signer)
    }

    if (bytes32(_signature[_signature.length-32:signature.length]) == 0x6492649264926492649264926492649264926492649264926492649264926492) {
      _signature.trimToSize(_signature.length - 4);
      address create2Factory;
      bytes memory factoryCalldata;
      (create2Factory, factoryCalldata, sig) = abi.decode(sig, (address, bytes, bytes));
      if (contractSize == 0) {
        (bool success, ) = create2Factory.call(factoryCalldata);
        if (!success) return false;
      }
      return IERC1271Wallet(_signer).isValidSignature(_hash, sig) == 0x1626ba7e;
    }

    if (contractSize > 0) {
      return IERC1271Wallet(_signer).isValidSignature(_hash, _signature) == 0x1626ba7e;
    }
    
    // ecrecover verification
    require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");
    uint8 v = uint8(_signature[64]);
    bytes32 r = _signature.readBytes32(0);
    bytes32 s = _signature.readBytes32(32);
    if (v != 27 && v != 28) {
      revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
    }
    return ecrecover(_hash, v, r, s) == _signer;
  }
}

This verification contract SHOULD be deployed as a singleton on Ethereum mainnet or any other EVM chain and it SHOULD be invoked off-chain via eth_call or equivalent RPC methods by the verifier. Note that even though this function does not have a view modifier, it can still be invoked off-chain via an eth_call.

Alternatively, the verifier may perform the same logic using a singleton contract like MakerDAO's multicall, by performing the aforementioned checks off-chain.

Security Considerations

The same considerations as ERC-1271 apply.

However, deploying a contract requires a CALL rather than a STATICCALL, which introduces reentrancy concerns. As such, we recommend that this ERC is mainly used for off-chain signature validation. It is possible to use it for on-chain signature validation as well, but such a case is by definition redundant, as we are presuming that the contract will be deployed in that case.

Another out-of-scope security consideration worth mentioning is whether the contract is going to be set-up with the correct permissions at deploy time, in order to allow for meaningful signature verification. By design, this is up to the implementation, but it's worth noting that thanks to how CREATE2 works, changing the bytecode or contructor callcode in the signature will not allow you to escalate permissions as it will change the deploy address and therefore make verification fail.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Ivo Georgiev, "ERC-6492: Signature Validation for Predeploy Contracts[DRAFT]," Ethereum Improvement Proposals, no. 6492, 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6492.