Crypto Training

Signatures in Solidity: EIP-712, Replay Attacks, and Permit Front-Run DoS

Signatures move authorization off-chain. The bugs are subtle: replay across domains, nonce misuse, and integrations that assume permit always succeeds.

Crypto Training2026-01-204 min read

EIP-712 domain separation illustration

Signatures are a power tool:

  • gasless approvals (permit)
  • meta-transactions
  • off-chain orders and intents
  • delegated execution patterns

They’re also a recurring incident root cause because the failure modes don’t look like “Solidity bugs”. They look like cryptographic ambiguity.

This post is a practical playbook: what to bind, what to store, and what can be front-run.

The one sentence you should remember#

A signature must be valid for exactly one thing.

If it can be replayed for something else, on another chain, or against another contract, you’re shipping a latent exploit.

1) Never accept “the hash” as a parameter#

A common anti-pattern:

  • user provides bytes32 messageHash
  • contract checks signature over messageHash

This punts meaning off-chain.

If you accept an arbitrary hash, a malicious front-end can trick users into signing something they don’t understand, and your contract will happily accept it.

Instead, build the digest on-chain from structured fields.

2) EIP-712: structured data, explicit domain#

EIP-712 isn’t about aesthetics. It’s about binding a signature to a domain.

The digest shape:

SOLIDITY
bytes32 digest = keccak256(
  abi.encodePacked(
    "\x19\x01",
    DOMAIN_SEPARATOR,
    structHash
  )
);

The domain separator is where you kill replays.

Typical domain fields:

  • name
  • version
  • chainId
  • verifyingContract

That last two are the core security fields.

3) ecrecover is not a library#

Raw ecrecover has sharp edges:

  • signature malleability unless you enforce s in the lower half-order
  • invalid signatures returning address(0) instead of reverting

Use a well-reviewed implementation (OpenZeppelin ECDSA).

4) Replay protection is state, not vibes#

If you want “use once”, you need on-chain state.

A good signature scheme usually includes:

  • a per-signer nonce
  • a deadline
  • (optional) a salt / unique id

Here’s the review table I use:

FieldWhy it existsFailure mode if missing
nonceprevents reusereplay drains or repeats permissions
deadlinelimits theft windowstolen sig works forever
chainIdbinds to chaincross-chain replay
verifyingContractbinds to contractreplay on clones
spender / targetbinds to actionsignature used for different action

5) Permit is authorization plus a mempool game#

The subtle part about permit integrations is not signature verification.

It’s how the mempool changes the execution order.

If your contract does:

  1. call permit(...)
  2. then execute a deposit that relies on the allowance

An attacker can copy the permit signature, submit it first, consume the nonce, and then your transaction’s permit call reverts.

No funds are stolen. But users can be griefed. Deposit flows become flaky. Keepers can be disrupted.

This is a front-run DoS.

Mitigation: make permit optional#

Treat permit as an optimization, not a dependency.

Pseudo-flow:

SOLIDITY
try token.permit(...) {
  // ok
} catch {
  // permit might have already been used; continue
}

// now rely on allowance OR revert for insufficient allowance
SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount);

This is one of those patterns that feels weird until you’ve seen the bug in production.

6) A concrete “what to sign” template#

When designing your own signature scheme, include:

  • owner (who authorizes)
  • target (what contract is allowed to act)
  • action (what behavior is permitted)
  • value / amount
  • nonce
  • deadline

If you sign something like “approve unlimited for anyone”, you built a universal skeleton key.

7) Threat modeling signatures like an auditor#

During review, I ask:

  1. What is the signed statement in plain English?
  2. What fields uniquely bind it to that statement?
  3. What is the replay boundary (per-signer nonce, per-order nonce, per-session salt)?
  4. What is the expiry model?
  5. What happens if the signature is used before the user’s transaction (front-run / copying)?

If you can answer those five clearly, you’ll avoid most signature incidents.

Watch: what you are actually signing (EIP-712)#

EIP-712 protects users from “sign this blob of bytes” UX, but it does not automatically protect you from replay, malleability, or ambiguous authorization. The core skill is learning to translate a signature prompt into the on-chain statement it authorizes.

8) Final checklist#

If you want a minimal checklist that still catches real bugs, it’s this:

  • digest is built on-chain from structured data
  • domain includes chain id and verifying contract
  • nonce is consumed exactly once
  • deadline exists
  • library handles malleability and invalid signatures safely
  • permit flows tolerate “permit already used”

Further reading#