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.
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:
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
structHash
)
);
The domain separator is where you kill replays.
Typical domain fields:
nameversionchainIdverifyingContract
That last two are the core security fields.
3) ecrecover is not a library#
Raw ecrecover has sharp edges:
- signature malleability unless you enforce
sin 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:
| Field | Why it exists | Failure mode if missing |
|---|---|---|
nonce | prevents reuse | replay drains or repeats permissions |
deadline | limits theft window | stolen sig works forever |
chainId | binds to chain | cross-chain replay |
verifyingContract | binds to contract | replay on clones |
spender / target | binds to action | signature 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:
- call
permit(...) - 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:
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/amountnoncedeadline
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:
- What is the signed statement in plain English?
- What fields uniquely bind it to that statement?
- What is the replay boundary (per-signer nonce, per-order nonce, per-session salt)?
- What is the expiry model?
- 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#
- Solidity-by-example signature patterns: https://docs.soliditylang.org/en/latest/solidity-by-example.html
- OpenZeppelin ECDSA utilities: https://docs.openzeppelin.com/contracts/5.x/