Off-Chain Signatures

concept
ethereumsignaturesgas-optimizationmeta-transactionsdapp

Off-chain signatures are the pattern of authorizing on-chain actions by signing a message off-chain and letting a smart contract verify the signature later. The signer pays no gas to produce the signature; whoever submits it to the chain pays gas for the verification and the authorized action. EIP-712 is the standard that makes this pattern safe at scale.

Why sign off-chain

Every on-chain transaction costs gas and occupies block space. Many authorizations do not need to hit the chain immediately — or at all:

  • Deferred execution: An exchange order that sits in a book until matched.
  • Batched execution: Many approvals collected off-chain and submitted together.
  • Conditional execution: An action that should only fire if some other condition is met.
  • Gasless UX: A relayer pays gas on the user’s behalf after verifying the user’s signature.
  • Off-chain consensus: A multisig whose threshold is reached before any transaction hits the chain.

In all of these, the signature itself is the authorization. The user signs once; the signature can be used later by anyone who holds it, subject to the application’s rules.

The shape of an off-chain signature flow

  1. Sign: The user signs a structured message with their wallet via eth_signTypedData. The wallet displays the message contents (field names and values) from the EIP-712 type definition and asks for consent.
  2. Transport: The signature and the original message travel off-chain — through an API, a message relay, a peer-to-peer network, or even a QR code.
  3. Submit: Some party (the user, a relayer, a counterparty, an aggregator) submits the signature and message to a contract in a transaction.
  4. Verify: The contract reconstructs the EIP-712 digest from the message, calls ecrecover (or EIP-1271 for smart accounts), and checks the recovered address matches the expected signer.
  5. Execute: If verification succeeds, the contract performs the authorized action.

Steps 1 and 4 depend entirely on EIP-712 doing its job: the same canonical digest must be produced at the wallet and at the contract from the same message.

What EIP-712 provides

EIP-712 is what makes off-chain signatures safe:

  • Human-readable signing: The wallet shows {from: 0x..., to: 0x..., amount: 100} rather than a 32-byte hex blob. Users can give informed consent.
  • Type separation: typeHash ensures structurally identical messages from different types do not collide.
  • Domain separation: domainSeparator binds signatures to a specific chain, contract, protocol, and version. See EIP-712 Domain Separator.
  • Deterministic encoding: The wallet and the contract compute the same 32-byte digest from the same message.
  • Injective encoding: The \x19\x01 prefix ensures the digest cannot collide with a valid transaction encoding or an \x19Ethereum Signed Message string.

See Ethereum Signed Message Encoding for the full disambiguation scheme.

What EIP-712 does NOT provide

Two critical security properties are explicitly out of scope for the hashing standard:

Replay protection within a domain

An attacker who obtains a valid signature can submit it repeatedly unless the application prevents it. The domain separator prevents cross-domain replay, but within a single contract, the same signature can be submitted twice.

Standard mitigations:

  • Nonces: Each signature includes a monotonically increasing nonce unique to the signer. The contract tracks usedNonces[signer] and rejects reuse. ERC-2612 permit uses this pattern.
  • Idempotency: Design the action so that submitting it twice produces the same final state (e.g., “set balance to X” instead of “add X to balance”).
  • Single-use tokens: Generate a random nonce per signature and mark it as spent on first use.
  • Deadlines: Include a timestamp; the contract rejects signatures past the deadline. Bounds the replay window but does not eliminate it.

Frontrunning

An attacker who sees a user’s signature in the mempool (or in any other broadcast channel) can submit it first with a higher gas price. For applications where the submitter matters, this can steal the intended benefit.

Standard mitigations:

  • Submitter binding: Include the submitter address in the signed message; the contract verifies msg.sender == signedSubmitter.
  • Private mempools: Flashbots and similar services bypass the public mempool.
  • Commit-reveal: Two-phase authorization where the commit is harmless and the reveal is short enough to race.
  • Order-agnostic outcomes: Design actions so the order of submission does not change the result.

Applied examples

  • ERC-2612 permit: Gasless ERC-20 approvals. User signs a Permit struct; anyone submits it to grant allowance. Prevents approve-then-transferFrom two-transaction flows.
  • OpenSea / Seaport orders: User signs an order describing a listing; buyers submit matching orders on-chain to settle.
  • Farcaster SignedKeyRequest: User’s custody address signs authorization for a new ed25519 signing key. See Creating a Farcaster Account by Hand for a walkthrough that does the EIP-712 computation by hand with cast.
  • Safe transactions: Owners sign SafeTx structs off-chain; when the threshold of signatures is reached, anyone submits them in execTransaction.
  • Sign-In with Ethereum (EIP-4361): User signs a structured login message; the web app verifies the signature to establish a session.

Smart account signatures: EIP-1271

EOAs sign with ecrecover. Smart contract accounts cannot — they have no private key. ERC-1271 bridges the gap: a contract implements isValidSignature(bytes32 hash, bytes signature) returns (bytes4 magicValue). Verifiers call this function instead of ecrecover when the signer is a contract. See Smart Account Signatures for the full branching pattern verifiers use.

Safe’s implementation wraps the hash in a SafeMessage(bytes message) struct under a Safe-specific EIP-712 domain, then either checks a stored signature (via signMessage) or validates a threshold of owner signatures at call time. The Farcaster walkthrough above uses the pre-approved variant: the Safe runs one on-chain transaction to record “yes, I signed that digest” before the verifier’s callback arrives.

Connections