example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
Detect ZK proof-verifier contract bugs — missing public-input binding (unconstrained input → forgery), proof malleability, BN254 field-element range checks (input >= field modulus), unchecked pairing/ecAdd/ecMul precompile returns, nullifier reuse / double-spend,
$ npx -y skills add omermaksutii/RugProof --skill zk-verifier-bugs --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/zk-verifier-bugsContext preview
The summary Claude sees to decide when to auto-load this skill.
Detect ZK proof-verifier contract bugs — missing public-input binding (unconstrained input → forgery), proof malleability, BN254 field-element range checks (input >= field modulus), unchecked pairing/ecAdd/ecMul precompile returns, nullifier reuse / double-spend,
name: zk-verifier-bugs description: Detect ZK proof-verifier contract bugs — missing public-input binding (unconstrained input → forgery), proof malleability, BN254 field-element range checks (input >= field modulus), unchecked pairing/ecAdd/ecMul precompile returns, nullifier reuse / double-spend, verification-key upgradeability, and trusted-setup assumptions. Activate whenever a contract verifies a SNARK/STARK proof, calls the bn256/altbn128 precompiles, or consumes nullifiers.
Trigger on any of:
function withdraw(uint256[8] proof, uint256 root, uint256 nullifier, address recipient) external {
require(verifier.verifyProof(proof, [root, nullifier])); // ← recipient NOT in inputs
payable(recipient).transfer(amount);
}**Signal:** a value the contract acts on (`recipient`, `amount`, `chainId`) is not among the proof's public inputs. The proof says nothing about it, so an attacker reuses a valid proof with a different recipient — front-runnable theft. Every value the proof is supposed to authorize must be a bound public input.
// public input passed straight to the verifier without bounds require(input < SNARK_SCALAR_FIELD); // ← if MISSING:
**Signal:** public inputs (and proof point coordinates) must be `< r` (the BN254 scalar/base field modulus, `21888242871839275222246405745257275088548364400416034343698204186575808495617`). An out-of-range input wraps mod r, letting a different field element pass as a valid input — input forgery. Verifiers must `require(x < FIELD_MODULUS)` for every input.
assembly { let ok := staticcall(gas(), 0x08, ...) } // 'ok' / returndata ignored**Signal:** the pairing precompile (`0x08`) returns success flag + a 32-byte result that must equal 1; `ecAdd`/`ecMul` return whether the input points were valid. Ignoring the success bool or the pairing result accepts invalid proofs (precompile reverts on malformed points only sometimes). Always check `success` AND the returned value.
require(verifier.verifyProof(...)); // ← no check that nullifier was unused; no marking after payout(recipient);
**Signal:** nullifier not checked against `spent[nullifier]` before payout, or not marked spent after (or marked after an external call → reentrant double-spend). Same note must not redeem twice.
Groth16 proofs are malleable — a valid `(A,B,C)` can be transformed to another valid proof for the same inputs. If proof bytes are used as a dedup/replay key, the twin bypasses it. Dedup on public inputs / nullifiers, never on proof bytes.
An upgradeable or owner-settable verification key lets an admin swap in a vk that validates forged proofs — a centralization backdoor over the entire system's soundness.
| Pattern | Severity | Notes | |---|---|---| | Action value not a bound public input | **Critical** | Proof reuse → theft | | Nullifier not checked / marked late | **Critical** | Double-spend | | Missing BN254 field range check | **High** | Input forgery via wraparound | | Unchecked pairing/ecMul return | **High** | Invalid proofs accepted | | Mutable vk without timelock/governance | **High** | Soundness backdoor | | Proof bytes used as replay key (malleable) | **Medium** | Twin-proof bypass |
1. **Bind every authorized value** as a public input committed in-circuit (`recipient`, `amount`, `chainId`, `contractAddress`); verify the input vector matches what the contract acts on. 2. **Range-check all public inputs** `< FIELD_MODULUS` (BN254 `r`) before verification — use the snarkjs/circomlib generated verifier's checks; don't strip them. 3. **Check precompile results** — `require(success)` and `require(out == 1)` for pairing; validate `ecAdd`/`ecMul` success. 4. **Nullifier set:** `require(!spent[n]); spent[n] = true;` *before* any external call (CEI). 5. **Dedup on inputs/nullifiers, not proof bytes**, to neutralize Groth16 malleability. 6. **Govern the vk** behind a timelock/multisig; document the trusted-setup ceremony and its toxic-waste assumptions.
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
Repo: omermaksutii/RugProof
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove deliberately skip the EXTCODESIZE…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta,…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the hook's permissions live in the…