Skip to main content

signature-vulnerabilities

Signature attack vectors and safe verification patterns for Solidity. Use when implementing permit, meta-transactions, gasless approvals, or any signature-based authentication. Covers replay attacks, EIP-712, nonce management, malleability, and permit front-running.

Zur Installation springen

Quellinformationen

Repository
ccashwell/evm-cortex
Letzte Quellaktivität
10. April 2026 um 16:31
Erkannte Sprache von SKILL.md
Englisch
Sterne
131
Forks
18

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
signature-vulnerabilities
description
Signature attack vectors and safe verification patterns for Solidity. Use when implementing permit, meta-transactions, gasless approvals, or any signature-based authentication. Covers replay attacks, EIP-712, nonce management, malleability, and permit front-running.
# Signature Vulnerabilities ## Replay Attacks A valid signature can be reused unless explicitly prevented. ### Cross-Chain Replay A signature valid on Ethereum mainnet can be replayed on Arbitrum, Optimism, etc. ```solidity // VULNERABLE: no chain ID in signed data bytes32 hash = keccak256(abi.encodePacked(user, amount, nonce)); // FIXED: include chain ID via EIP-712 domain separator bytes32 public immutable DOMAIN_SEPARATOR; constructor() { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes("MyProtocol")), keccak256(bytes("1")), block.chainid, address(this) )); } ``` ### Cross-Contract Replay A signature for contract A can be replayed on contract B if the verifying contract address isn't included. **Defense**: Always include `address(this)` in the domain separator (EIP-712 handles this). ### Same-Contract Replay Reusing a signature multiple times on the same contract. **Defense**: Nonce tracking. ```solidity mapping(address => uint256) public nonces; function executeWithSignature( address user, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { if (block.timestamp > deadline) revert Expired(); bytes32 structHash = keccak256(abi.encode( EXECUTE_TYPEHASH, user, amount, nonces[user]++, deadline )); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); address recovered = ecrecover(digest, v, r, s); if (recovered == address(0) || recovered != user) revert InvalidSignature(); _execute(user, amount); } ``` ## EIP-712 Typed Data Signing The standard for structured data signing. Provides human-readable signing prompts in wallets. ```solidity bytes32 public constant EXECUTE_TYPEHASH = keccak256( "Execute(address user,uint256 amount,uint256 nonce,uint256 deadline)" ); function _hashTypedData(bytes32 structHash) internal view returns (bytes32) { return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); } ``` ### Nested Struct Hashing ```solidity // For complex types, hash inner structs first bytes32 constant ORDER_TYPEHASH = keccak256( "Order(address maker,Asset makerAsset,Asset takerAsset,uint256 nonce,uint256 deadline)" "Asset(address token,uint256 amount)" ); bytes32 constant ASSET_TYPEHASH = keccak256( "Asset(address token,uint256 amount)" ); function hashOrder(Order memory order) internal pure returns (bytes32) { return keccak256(abi.encode( ORDER_TYPEHASH, order.maker, hashAsset(order.makerAsset), hashAsset(order.takerAsset), order.nonce, order.deadline )); } function hashAsset(Asset memory asset) internal pure returns (bytes32) { return keccak256(abi.encode(ASSET_TYPEHASH, asset.token, asset.amount)); } ``` ## Signature Malleability ECDSA signatures have two valid forms for the same message (s and n-s). If both are accepted, a signature can be "replayed" with the alternate form. ```solidity // VULNERABLE: accepts both s values address recovered = ecrecover(hash, v, r, s); // FIXED: enforce low-s value (EIP-2 canonical form) function _verifySignature(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { // s must be in the lower half of the curve order if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { revert MalleableSignature(); } address recovered = ecrecover(hash, v, r, s); if (recovered == address(0)) revert InvalidSignature(); return recovered; } ``` **Best practice**: Use OpenZeppelin's ECDSA library which handles malleability checks. ```solidity import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; function verify(bytes32 hash, bytes memory signature) internal pure returns (address) { return ECDSA.recover(hash, signature); // handles malleability check } ``` ## ecrecover Returns address(0) `ecrecover` returns `address(0)` for invalid signatures instead of reverting. Always check. ```solidity // VULNERABLE: doesn't check for address(0) address signer = ecrecover(hash, v, r, s); require(signer == expectedSigner); // passes if expectedSigner is address(0) // FIXED: explicit zero check address signer = ecrecover(hash, v, r, s); if (signer == address(0)) revert InvalidSignature(); if (signer != expectedSigner) revert Unauthorized(); ``` ## EIP-2612 Permit Front-Running An attacker can front-run a `permit` + action transaction by extracting the permit signature and calling `permit` first. The user's transaction reverts because the nonce is consumed. ```solidity // VULNERABLE: permit reverts if already used function depositWithPermit( uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { token.permit(msg.sender, address(this), amount, deadline, v, r, s); // ← reverts if front-run token.safeTransferFrom(msg.sender, address(this), amount); } // FIXED: try/catch on permit, proceed if allowance already set function depositWithPermit( uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { try token.permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {} token.safeTransferFrom(msg.sender, address(this), amount); } ``` ## Nonce Management Patterns ### Sequential Nonces Simple, prevents replay, but requires sequential use. ```solidity mapping(address => uint256) public nonces; // User must use nonce 0, then 1, then 2, etc. // Cancel a nonce by calling with a no-op payload ``` ### Bitmap Nonces (Permit2 style) Allows out-of-order nonce consumption. ```solidity // Nonces stored as bitmaps: nonce 256*wordPos + bitPos mapping(address => mapping(uint256 => uint256)) public nonceBitmap; function _useNonce(address owner, uint256 nonce) internal { uint256 wordPos = nonce >> 8; uint256 bitPos = nonce & 0xff; uint256 bit = 1 << bitPos; uint256 word = nonceBitmap[owner][wordPos]; if (word & bit != 0) revert NonceAlreadyUsed(); nonceBitmap[owner][wordPos] = word | bit; } ``` ## Signature Verification Checklist - [ ] EIP-712 domain separator includes chainId and verifyingContract - [ ] Domain separator recomputed if chainId can change (forking) - [ ] Nonce included and incremented to prevent replay - [ ] Deadline/expiry included for time-bound signatures - [ ] ecrecover result checked against address(0) - [ ] Signature malleability mitigated (low-s check or OZ ECDSA) - [ ] Permit calls wrapped in try/catch to prevent front-running griefing - [ ] Typed data hashing follows EIP-712 spec exactly - [ ] Offchain signing tested against onchain verification
Auf GitHub ansehen