Signature Verification Attacks
How the attack works and how to prevent it
Signature Verification Attacks: EIP-712, Permit, Replay, and Domain Separation Bugs
Signature verification attacks happen when a contract accepts a valid signature in the wrong context. The signature may be real. The private key may be uncompromised. The bug is that the contract verifies too little.
Modern DeFi moves authority into signatures: permits, orders, withdrawals, delegations, meta-transactions, bridge messages, and account abstraction. If the signed payload does not bind the exact intent, an attacker can reuse or redirect it.
What Is A Signature Verification Attack?
A signature verification attack is a bug where a contract accepts a signature for the wrong nonce, chain, contract, function, recipient, amount, deadline, or message type.
| Missing Binding | Result |
|---|---|
| Nonce | Same signature can be reused |
| Deadline | Signature may remain valid forever |
| Chain ID | Cross-chain replay risk |
| Verifying contract | Cross-contract replay risk |
| Function intent | Signature for one action authorizes another |
| Recipient or spender | Funds or allowance go to the wrong address |
The dangerous part is that ECDSA.recover() can return the expected signer while the authorization is still broken.
How The Attack Works
-
A user signs a permit, order, withdrawal, delegation, or meta-transaction.
-
The attacker obtains the signature from calldata, an API, a compromised frontend, or phishing.
-
The vulnerable contract recovers the signer but does not verify the full context.
-
The attacker replays or redirects the signature.
-
The contract transfers funds, sets allowance, executes an order, or grants authority.
This is why signature review is intent review as much as cryptography review.
Vulnerable Code
// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
contract BadSignatureVault {
mapping(address => uint256) public balances;
function withdrawWithSig(
address owner,
address to,
uint256 amount,
bytes calldata sig
) external {
bytes32 digest = keccak256(abi.encodePacked(owner, to, amount));
address recovered = ECDSA.recover(digest, sig);
require(recovered == owner, "bad signature");
// Missing nonce, deadline, chain ID, contract address, and typed intent.
balances[owner] -= amount;
payable(to).transfer(amount);
}
}
This contract checks who signed, but not whether the signature is fresh, contract-specific, chain-specific, or single-use.
Safer EIP-712 Pattern
contract SafeSignatureVault is EIP712, ReentrancyGuard {
using ECDSA for bytes32;
bytes32 private constant WITHDRAW_TYPEHASH = keccak256(
"Withdraw(address owner,address to,uint256 amount,uint256 nonce,uint256 deadline)"
);
mapping(address => uint256) public nonces;
mapping(address => uint256) public balances;
constructor() EIP712("SafeSignatureVault", "1") {}
function withdrawWithSig(
address owner,
address payable to,
uint256 amount,
uint256 deadline,
bytes calldata signature
) external nonReentrant {
require(block.timestamp <= deadline, "expired");
require(owner != address(0), "owner zero");
require(to != address(0), "to zero");
uint256 nonce = nonces[owner]++;
bytes32 structHash = keccak256(
abi.encode(WITHDRAW_TYPEHASH, owner, to, amount, nonce, deadline)
);
bytes32 digest = _hashTypedDataV4(structHash);
address recovered = ECDSA.recover(digest, signature);
require(recovered == owner, "bad signature");
balances[owner] -= amount;
to.transfer(amount);
}
}
This version binds the signature to a typed action, contract domain, chain context, nonce, recipient, amount, and deadline.
The example is EOA-only because it uses ECDSA.recover. Production flows that support smart accounts should use EIP-1271-aware verification such as OpenZeppelin SignatureChecker.isValidSignatureNow().
Permit Is High Impact
permit is useful because it lets users approve token allowance with a signature instead of an approval transaction. That also makes it dangerous when the UX or validation is weak.
Review permit flows for:
-
unlimited approvals,
-
missing deadlines,
-
wrong spender,
-
reused nonces,
-
confused token domains,
-
Permit2 or aggregator flows where users do not understand what they signed.
Not every permit drain is a protocol bug. Many are phishing. But protocols still need to design signatures so a leaked or misused signature has the smallest possible blast radius.
Audit Checklist
-
Every signature has a nonce or unique order ID.
-
Nonces are consumed exactly once.
-
Signatures include deadlines.
-
EIP-712 domain includes name, version, chain ID, and verifying contract.
-
Signed struct includes recipient, spender, token, amount, and action intent.
-
Raw
ecrecoveris avoided or fully hardened. -
abi.encodePackedis not used for ambiguous typed messages. -
Contract wallets are handled with EIP-1271 where required.
-
Cancelled orders and invalidated nonces stay invalid after upgrades.
-
Tests cover replay, wrong contract, wrong chain, expired signature, wrong amount, and wrong recipient.
Common Failure Modes
| Failure | Test Case |
|---|---|
| Missing nonce | Use the same signature twice |
| Missing chain ID | Replay on a fork or different chain config |
| Missing contract address | Deploy clone and reuse signature |
| Missing deadline | Execute old signature long after signing |
| Missing recipient | Redirect output to attacker |
If a signature authorizes value movement, test it like an entrypoint.
Related Vulnerabilities
Signature verification bugs frequently become replay attacks. They also combine with phishing attacks, because users can be tricked into signing harmful payloads, and with access control attacks, because signatures often grant privileged authority.
For public transaction ordering risk, review frontrunning attacks.
FAQ
What is a signature verification attack?
It is when a contract accepts a cryptographically valid signature outside the context the signer intended, such as the wrong nonce, chain, contract, function, amount, or deadline.
Does EIP-712 prevent replay attacks automatically?
No. EIP-712 structures the data and domain, but the contract must still include and consume nonces, deadlines, and security-critical fields.
What is domain separation?
Domain separation binds a signature to a specific application context, usually including protocol name, version, chain ID, and verifying contract.
Should contracts use ecrecover directly?
Usually no. Prefer audited libraries such as OpenZeppelin ECDSA, EIP712, and SignatureChecker because raw signature handling is easy to get subtly wrong.
Learn Signature Bugs Properly
Signature bugs are not solved by memorizing EIP names. You need to trace exactly what the user signed and exactly what the contract executes. The Smart Contract Hacking course covers replay protection, phishing risk, access control, and exploit validation in hands-on labs.
Sources and editorial notes
Reviewed by JohnnyTime. Last updated .
Real Signature Verification Attacks hacks to study
A stable selection of high-signal incidents linked to this attack class, ordered by reported loss and recency.
Master Signature Verification Attacks in a safe lab
Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.