ERC-1271: Standard Signature Validation Method for Contracts

source
ethereumeipercsignaturessmart-accountssmart-walletoff-chainaccount-abstraction

ERC-1271 is the Ethereum standard for letting smart contracts verify signatures on their own behalf. It defines a single interface method, isValidSignature(bytes32 hash, bytes signature) returns (bytes4 magicValue), that any contract can implement. Authored by Francisco Giordano, Matt Condon, Philippe Castonguay, Amir Bandeali, Jorge Izquierdo, and Bertrand Masius; created 2018-07-25. Standards Track, ERC (application-level) category.

Motivation

Externally Owned Accounts (EOAs) sign messages with their private keys and verifiers call ecrecover to confirm the signer. Smart contracts have no private key, so ecrecover is not an option for them. Without a standard, any dapp that accepted off-chain signatures implicitly required an EOA signer, and every contract-owned identity — smart wallets, DAOs, multisigs, escrows — was locked out of the signature layer.

The canonical motivating example is a decentralized exchange with an off-chain orderbook. Buyers and sellers sign orders off-chain; the exchange contract verifies the signatures when settling. An EOA signs with its private key; a Safe multisig owned by five people cannot. EIP-1271 gives the Safe a way to answer “yes, that order is valid on my behalf” without ever possessing a private key.

Specification

pragma solidity ^0.5.0;

contract ERC1271 {

  // bytes4(keccak256("isValidSignature(bytes32,bytes)")
  bytes4 constant internal MAGICVALUE = 0x1626ba7e;

  /**
   * @dev Should return whether the signature provided is valid for the provided hash
   * @param _hash      Hash of the data to be signed
   * @param _signature Signature byte array associated with _hash
   *
   * MUST return the bytes4 magic value 0x1626ba7e when function passes.
   * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
   * MUST allow external calls
   */
  function isValidSignature(
    bytes32 _hash,
    bytes memory _signature)
    public
    view
    returns (bytes4 magicValue);
}

Three requirements:

  1. Returns the magic value on success: bytes4(0x1626ba7e) = bytes4(keccak256("isValidSignature(bytes32,bytes)")). Anything else — different bytes, a revert, no implementation at all — is a rejection.
  2. Must not modify state: Enforced via view in Solidity 0.5+, STATICCALL in older versions. See isValidSignature for why this matters.
  3. Must allow external calls: The method is public/external because verifiers are always external to the signing contract.

The isValidSignature method can call arbitrary internal logic to determine validity — ECDSA recovery, multisig threshold checking, BLS aggregation, pre-approved hash lookup, role-based permissions, time windows, state-based rules. The standard is agnostic about what “valid” means; it only standardizes the interface.

Who implements it

Any contract that wants to be treated as a signer:

  • Smart contract wallets — Safe (Gnosis), Argent, Ambire, Sequence, Biconomy
  • DAOs — governance contracts that authorize actions via proposal passage
  • Multisignature wallets — owner set plus threshold
  • ERC-4337 smart accounts — account abstraction implementations
  • Escrows and time-locks — contracts that hold assets on behalf of users
  • DEX aggregators — protocols that sign orders on behalf of pooled users

Who calls it

Any verifier that needs to check whether a signature is valid for a contract-owned address. The typical pattern is:

function verify(address signer, bytes32 hash, bytes calldata signature) internal view {
    if (signer.code.length > 0) {
        // Contract signer: use EIP-1271
        bytes4 result = IERC1271(signer).isValidSignature(hash, signature);
        require(result == 0x1626ba7e, "INVALID_SIGNATURE");
    } else {
        // EOA signer: use ecrecover
        require(ECDSA.recover(hash, signature) == signer, "INVALID_SIGNATURE");
    }
}

OpenZeppelin’s SignatureChecker.isValidSignatureNow(signer, hash, signature) implements exactly this fallback: probe ecrecover first, fall back to isValidSignature if the signer has code. See Smart Account Signatures for the full verification pattern and the branching conditions verifiers need.

Rationale

Why bytes4 instead of bool: A contract that does not implement isValidSignature at all — or that implements it incorrectly and returns arbitrary data — must not accidentally satisfy a validity check. A boolean return type would let truthy garbage pass. A specific four-byte magic value cannot be accidentally produced; only an implementation that knew the standard and intended to return success can do so.

Why two arguments: A single “raw message” argument would force every contract to agree on a hashing scheme. EIP-1271 instead takes a pre-hashed bytes32, leaving the hashing scheme out of scope. Callers and contracts must agree on the scheme — typically EIP-712 with a domain separator — but the standard does not mandate it.

Why state cannot be modified: Two reasons. First, it simplifies the implementation surface — there is no “what if the validation has side effects” question. Second, it rules out GasToken-style attacks where an attacker forces a contract to perform gas-refundable work by calling a signature check. Third, it makes off-chain queries cheap: a client can call isValidSignature via eth_call to probe whether a signature is acceptable without paying gas or broadcasting a transaction.

Reference implementation

A minimal example from the EIP — a contract that validates signatures by recovering the signer and checking it matches an owner:

function isValidSignature(
    bytes32 _hash,
    bytes calldata _signature
) external override view returns (bytes4) {
    if (recoverSigner(_hash, _signature) == owner) {
        return 0x1626ba7e;
    } else {
        return 0xffffffff;
    }
}

And a verifier calling it:

function callERC1271isValidSignature(
    address _addr,
    bytes32 _hash,
    bytes calldata _signature
) external view {
    bytes4 result = IERC1271Wallet(_addr).isValidSignature(_hash, _signature);
    require(result == 0x1626ba7e, "INVALID_SIGNATURE");
}

The reference implementation also includes recoverSigner, an ECDSA recovery helper with explicit signature-malleability mitigations (rejecting high-s values per EIP-2, rejecting v values outside {27, 28}, rejecting the zero address recovery). These mitigations are not part of EIP-1271 itself — they are general ECDSA best practice that any isValidSignature implementation should inherit if it uses ecrecover internally.

Security considerations

  • No gas limit: The signing contract may legitimately consume a large amount of gas — multisig checks across 20 owners, BLS aggregation, storage reads, nested calls. Callers MUST NOT hardcode a gas cap when invoking isValidSignature externally, or they will reject valid signatures whose verification happens to be expensive.
  • Contract responsibility: Each signing contract is fully responsible for the correctness of its own validation logic. A buggy implementation that returns the magic value for signatures it should reject is catastrophic — attackers gain arbitrary write access via forged signatures.
  • Verifier responsibility: Verifiers must refuse to accept anything other than the exact magic value. “Any non-zero return” is not a valid check. Neither is “did not revert”.

Applied example: Safe + Farcaster

The Creating a Farcaster Account by Hand walkthrough is a concrete EIP-1271 flow end to end:

  1. A Safe multisig is the intended Farcaster account custody. Farcaster’s SignedKeyRequestValidator expects an EIP-712 signature on the SignedKeyRequest struct from the custody address.
  2. The Safe cannot produce an ECDSA signature. Instead, the Safe pre-approves the Farcaster EIP-712 digest on-chain via a SignMessageLib.signMessage delegatecall, which writes into the Safe’s signedMessages storage mapping.
  3. When Farcaster later calls Safe.isValidSignature(digest, "") (empty signature), the Safe’s CompatibilityFallbackHandler wraps the digest in a SafeMessage(bytes) struct under a Safe-specific EIP-712 domain, looks up the wrapped hash in signedMessages, finds it, and returns 0x1626ba7e.
  4. Farcaster’s validator sees the magic value and accepts. The ed25519 signer goes live.

This is EIP-1271 in its pre-approved-hash form — a pure on-chain attestation standing in for a cryptographic signature. The same contract also supports the runtime-verified form where signature is non-empty and the Safe checks it against its current owner set and threshold.

Connections