example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
Detect ECDSA signature malleability and ecrecover pitfalls — missing low-s (EIP-2) enforcement, unconstrained v, unchecked address(0) from ecrecover, replay across chainId/contract from a missing EIP-712 domain separator, EIP-2098 compact-signature confusion, and signature
Claude Code
$ npx -y skills add omermaksutii/RugProof --skill signature-malleability --agent claude-code
Codex
$ npx skills add omermaksutii/RugProof --skill signature-malleability -a codex -y
Cursor
$ npx skills add omermaksutii/RugProof --skill signature-malleability -a cursor -y
Gemini CLI
$ npx skills add omermaksutii/RugProof --skill signature-malleability -a gemini-cli -y
GitHub Copilot
$ npx skills add omermaksutii/RugProof --skill signature-malleability -a github-copilot -y
OpenCode
$ npx skills add omermaksutii/RugProof --skill signature-malleability -a opencode -y
Windsurf
$ npx skills add omermaksutii/RugProof --skill signature-malleability -a windsurf -y
name: signature-malleability description: Detect ECDSA signature malleability and ecrecover pitfalls — missing low-s (EIP-2) enforcement, unconstrained v, unchecked address(0) from ecrecover, replay across chainId/contract from a missing EIP-712 domain separator, EIP-2098 compact-signature confusion, and signature reuse. Activate whenever code calls ecrecover directly, parses (r,s,v) from bytes, or verifies signed messages without OpenZeppelin ECDSA.
Trigger on any of:
address signer = ecrecover(hash, v, r, s); // no s-range check require(signer == expected); usedSignature[keccak256(abi.encode(r,s,v))] = true; // ← dedup keyed on sig bytes
**Signal:** for any valid `(r,s,v)` the "flipped" signature `(r, n - s, v ^ 1)` recovers the *same* signer (the classic Bitcoin/Ethereum transaction-malleability class). If the signature itself is the replay key, an attacker submits the twin and bypasses dedup. Enforce `s <= secp256k1n/2` (EIP-2).
address signer = ecrecover(hash, v, r, s); require(signer == owner); // if owner could ever be address(0)... or no check at all
**Signal:** malformed inputs make `ecrecover` return `address(0)`. Any code path where the compared-against value can be `address(0)` (uninitialized mapping slot, default) authenticates an attacker with garbage. Always `require(signer != address(0))`.
Accepting arbitrary `v` (not 27/28) or trusting a caller-supplied `v` for EIP-2098 compact sigs, mixing the 64-byte and 65-byte formats. EIP-2098 packs `yParity` into the high bit of `s`; decoding it as a raw 65-byte sig corrupts recovery.
bytes32 digest = keccak256(abi.encode(ORDER_TYPEHASH, maker, amount, nonce));
**Signal:** no EIP-712 domain separator binding `chainId` and `verifyingContract`. A signature is replayable on another chain (post-fork) or a sibling deployment. Build the digest with `_hashTypedDataV4` (`\x19\x01` + domainSeparator + structHash) and recompute the domain separator if `block.chainid` changes.
Same digest accepted twice because there's no per-signer nonce or no marking of consumed digests — meta-tx and claim flows drain repeatedly.
| Pattern | Severity | Notes | |---|---|---| | Malleable sig used as replay/dedup key | **High** | Twin sig bypasses guard | | ecrecover==address(0) authenticates attacker | **High** | Auth bypass | | Missing EIP-712 domain → cross-chain replay | **High** | Signature reuse across deployments | | No nonce → straight replay | **High** | Repeated execution | | Unconstrained v / EIP-2098 confusion | **Medium** | Format-dependent | | Low-s missing but sig never used as a key | **Low** | Defense-in-depth |
1. **Use OpenZeppelin `ECDSA.recover`** — it enforces `s <= secp256k1n/2`, rejects bad `v`, and reverts on `address(0)` (it also handles EIP-2098). 2. **Bind digests with EIP-712** (`EIP712` base + `_hashTypedDataV4`), including `chainId` and `verifyingContract`; recompute the domain separator on chainid change. 3. **Use per-signer nonces** (incrementing) rather than signature-bytes dedup; consume by `(signer, nonce)` not `(r,s,v)`. 4. **Explicit `require(signer != address(0))`** even when using libraries, for any hand-rolled path. 5. Prefer **`SignatureChecker`** when signers may be ERC-1271 contract wallets.
Rugproof your code before someone else does. 🌐 Live site: omermaksutii.github.io/RugProof 📦 Latest: v1.0.0 — 45 commands · 23 agents · 45 skills · 13 MCP servers · tested, offline-first, with rule packs, a benchmark, non-EVM coverage, and post-deploy
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the…
Use for authorized security assessment of REST, GraphQL, WebSocket, or SOAP APIs, including discovery, authentication,…
Extract and decrypt Windows DPAPI-protected secrets (Credential Manager, browser logins/cookies, Wi-Fi credentials, KeePass keys)…
Clean and finalize authorized natural-language text intended for readers by auditing suspicious invisible Unicode and rewriting…