# scv-scan

Systematically audit Solidity smart contract codebases for security vulnerabilities using a 4-phase approach - load a vulnerability cheatsheet, sweep code with grep and semantic analysis, deep-validate candidates against reference files, and output a severity-ranked findings

- **Kind:** skill
- **Source:** https://github.com/kadenzipfel/scv-scan
- **Page:** https://forefy.com/skills/92e3c07f-257c-4228-8b1a-96880896295c
- **API (JSON + files):** https://forefy.com/api/asr/92e3c07f-257c-4228-8b1a-96880896295c

---

## README.md

# SCV Scan

A Claude Code skill that scans Solidity codebases for security vulnerabilities by referencing 36 unique vulnerability types sourced from [smart-contract-vulnerabilities](https://github.com/kadenzipfel/smart-contract-vulnerabilities).

## Setup

1. Clone this repo into your Claude skills directory:

```bash
git clone <repo-url> ~/.claude/skills/scv
```

2. Run the skill in your codebase

```bash
cd my_repo
claude
/scv
```

## How It Works

The skill follows a four-phase audit workflow:

1. **Load Cheatsheet** — Claude reads `references/CHEATSHEET.md`, a condensed lookup table of 36 vulnerability classes with grep-able keywords and minimal code snippets.

2. **Codebase Sweep** — Two passes over the target Solidity code:
   - **Syntactic:** grep for trigger keywords from the cheatsheet
   - **Semantic:** read-through for logic bugs with no reliable grep signature (cross-function reentrancy, missing access control, etc.)

3. **Deep Validation** — For each candidate finding, Claude reads the full reference file (e.g., `references/reentrancy.md`) and walks through its detection heuristics and false-positive conditions before confirming or discarding.

4. **Report** — Confirmed findings are output with severity, code snippets, and fix recommendations.

## Project Structure

```
SKILL.md                        # Skill prompt (audit workflow + rules)
references/
  CHEATSHEET.md                 # Condensed quick-reference for all 36 vuln classes
  reentrancy.md                 # Full reference: preconditions, patterns, heuristics,
  overflow-underflow.md         #   false positives, remediation
  delegatecall-untrusted-callee.md
  ...                           # 36 reference files total
```

Each full reference file contains: **Preconditions**, **Vulnerable Pattern** (annotated Solidity), **Detection Heuristics**, **False Positives**, and **Remediation**.

## SKILL.md

---
name: scv-scan
description: Systematically audit Solidity smart contract codebases for security vulnerabilities using a 4-phase approach - load a vulnerability cheatsheet, sweep code with grep and semantic analysis, deep-validate candidates against reference files, and output a severity-ranked findings
---

# Smart Contract Vulnerability Auditor

You are a smart contract security auditor. Your task is to systematically audit a Solidity codebase for vulnerabilities using a three-phase approach that balances thoroughness with efficiency.

## Repository Structure

```
references/
  CHEATSHEET.md          # Condensed pattern reference — always read first
  reentrancy.md          # Full reference files — read selectively in Phase 3
  overflow-underflow.md
  ...
```

## Reference File Format

Each full reference file in `references/` has these sections:

- **Preconditions** — what must be true for the vulnerability to exist
- **Vulnerable Pattern** — annotated Solidity anti-pattern
- **Detection Heuristics** — step-by-step reasoning to confirm the vulnerability
- **False Positives** — when the pattern appears but isn't exploitable
- **Remediation** — how to fix it

## Audit Workflow

### Phase 1: Load the Cheatsheet

**Before touching any Solidity files**, read `references/CHEATSHEET.md` in full.

This file contains a condensed entry for every known vulnerability class: name, what to look for (syntactic and semantic), and default severity. Internalize these patterns — they are your detection surface for the sweep phase. Do NOT read any full reference files yet.

### Phase 2: Codebase Sweep

Perform two complementary passes over the codebase.

#### Pass A: Syntactic Grep Scan

Search for the trigger patterns listed in the cheatsheet under "Grep-able keywords". Use grep, ripgrep, or equivalent to find

For each match, record: file, line number(s), matched pattern, and suspected vulnerability type(s).

#### Pass B: Structural / Semantic Analysis

This pass catches vulnerabilities that have no reliable grep signature. Read through the codebase searching for any relevant logic similar to that explained in the cheatsheet.

For each finding in this pass, record: file, line number(s), description of the concern, and suspected vulnerability type(s).

#### Compile Candidate List

Merge results from Pass A and Pass B into a deduplicated candidate list. Each entry should look like:

```
- File: `path/to/file.sol` L{start}-L{end}
- Suspected: [vulnerability-name] (from CHEATSHEET.md)
- Evidence: [brief description of what was found]
```

### Phase 3: Selective Deep Validation

For each candidate in the list:

1. **Read the full reference file** for the suspected vulnerability type (e.g., `references/reentrancy.md`). Read it now — not before.
2. **Walk through every Detection Heuristic step** against the actual code. Be precise — trace variable values, check modifiers, follow call chains.
3. **Check every False Positive condition**. If any false positive condition matches, discard the finding and note why.
4. **Cross-reference**: one code location can match multiple vulnerability types. If the cheatsheet maps the same pattern to multiple references, read and validate against each.
5. **Confirm or discard.** Only confirmed findings go into the final report.

### Phase 4: Report

For each confirmed finding, output:

```
### [Vulnerability Name]

**File:** `path/to/file.sol` L{start}-L{end}
**Severity:** Critical | High | Medium | Low | Informational

**Description:** What is vulnerable and why, in 1-3 sentences.

**Code:**
\`\`\`solidity
// The vulnerable code snippet
\`\`\`

**Recommendation:** Specific fix, referencing the Remediation section of the reference file.
```

After all findings, include a summary section:

```
## Summary

| Severity | Count |
|----------|-------|
| Critical | N     |
| High     | N     |
| Medium   | N     |
| Low      | N     |
| Info     | N     |
```

Write the final report to `scv-scan.md`

## Severity Guidelines

- **Critical**: Direct loss of funds, unauthorized fund extraction, permanent freezing of funds
- **High**: Conditional fund loss, access control bypass, state corruption exploitable under realistic conditions
- **Medium**: Unlikely fund loss, griefing attacks, DoS on non-critical paths, value leak under edge conditions
- **Low**: Best practice violations, gas inefficiency, code quality issues with no direct exploit path
- **Informational**: Unused variables, style issues, documentation gaps

## Key Principles

- **Cheatsheet first, references on-demand.** Never read all full reference files upfront. The cheatsheet gives you ambient awareness; full references are for validation only.
- **Semantic > syntactic.** The hardest bugs don't grep. Cross-function reentrancy, missing access control, incorrect inheritance — these require reading and reasoning, not pattern matching.
- **Trace across boundaries.** Follow state across function calls, contract calls, and inheritance chains. Hidden external calls (safe mint/transfer hooks, ERC-777 callbacks) are as dangerous as explicit `.call()`.
- **One location, multiple bugs.** A single line can be vulnerable to reentrancy AND unchecked return value. Check all applicable references.
- **Version matters.** Always check `pragma solidity` — many vulnerabilities are version-dependent (e.g., overflow is checked by default in ≥0.8.0).
- **False positives are noise.** Be rigorous about checking false positive conditions. A shorter report with high-confidence findings is more valuable than a long one padded with maybes.

## references

```

```

## references/CHEATSHEET.md

# Vulnerability Cheatsheet

Quick-reference for identifying smart contract vulnerabilities during codebase scanning. Each section points to its full reference file for detailed analysis.

---

## Arbitrary Storage Location

**Reference:** `arbitrary-storage-location.md`

User-controlled index on a dynamic array write (or `sstore` with user-controlled slot) allows overwriting any storage slot, including `owner`. The attacker computes an index that maps through the array's keccak256 layout to target critical slots.

```solidity
data[index] = value; // index from user input, no bounds check
```

### Grep-able keywords
`sstore`, `.length =`, `data[`, `array[`

---

## Asserting Contract from Code Size

**Reference:** `asserting-contract-from-code-size.md`

Using `extcodesize` or `.code.length == 0` to check if the caller is an EOA is bypassable -- contracts calling from their constructor have a code size of 0.

```solidity
require(msg.sender.code.length == 0, "no contracts");
```

### Grep-able keywords
`extcodesize`, `.code.length`, `isContract`

---

## Authorization Through tx.origin

**Reference:** `authorization-txorigin.md`

Using `tx.origin` for authorization allows phishing attacks: if the owner calls a malicious contract, that contract can call back into the victim contract and `tx.origin` will still be the owner. Use `msg.sender` instead.

```solidity
require(tx.origin == owner, "not owner");
```

### Grep-able keywords
`tx.origin`

---

## Delegatecall to Untrusted Callee

**Reference:** `delegatecall-untrusted-callee.md`

If the target of a `delegatecall` is user-controlled or set by an unprotected function, an attacker can execute arbitrary code in the context of the calling contract's storage, overwriting critical state like `owner`.

```solidity
(bool success,) = callee.delegatecall(data); // callee from user input
```

### Grep-able keywords
`delegatecall`, `setImplementation`, `upgradeTo`

---

## DoS with Block Gas Limit

**Reference:** `dos-gas-limit.md`

Iterating over an unbounded dynamic array in a single transaction will eventually exceed the block gas limit as the array grows, permanently bricking the function. Replace push-payment with pull-payment or add batching/pagination.

```solidity
for (uint256 i = 0; i < recipients.length; i++) {
    payable(recipients[i]).transfer(reward);
}
```

### Grep-able keywords
`for (`, `while (`, `.length`, `.push(`

---

## DoS with (Unexpected) Revert

**Reference:** `dos-revert.md`

A single reverting external call inside a loop blocks the entire function. Also: strict balance equality checks (`address(this).balance ==`) can be broken by force-sent ETH via `selfdestruct`, and unvalidated division denominators cause revert.

```solidity
require(payable(recipients[i]).send(amounts[i]), "transfer failed"); // in loop
require(address(this).balance == expectedBalance); // broken by selfdestruct
```

### Grep-able keywords
`selfdestruct`, `.balance ==`, `.send(`, `.transfer(`, `require(success`

---

## Hash Collision with abi.encodePacked

**Reference:** `hash-collision.md`

When `abi.encodePacked` has two or more adjacent variable-length arguments (string, bytes, dynamic arrays), bytes can shift between arguments to produce the same encoding: `encodePacked("a","bc") == encodePacked("ab","c")`. Use `abi.encode` instead.

```solidity
keccak256(abi.encodePacked(stringA, stringB)); // collision possible
```

### Grep-able keywords
`abi.encodePacked`

---

## Inadherence to Standards

**Reference:** `inadherence-to-standards.md`

Token implementations may deviate from ERC20/ERC721 specs (missing return values, missing events). Token integrations that use raw `IERC20.transfer()` instead of `SafeERC20` break on non-compliant tokens (USDT). Hardcoding 18 decimals or ignoring fee-on-transfer is also a risk.

```solidity
require(token.transfer(to, amount)); // reverts on USDT (no return value)
```

### Grep-able keywords
`SafeERC20`, `safeTransfer`, `safeTransferFrom`, `.transfer(`, `.transferFrom(`, `.approve(`, `decimals`

---

## Incorrect Constructor Name

**Reference:** `incorrect-constructor.md`

In Solidity <0.4.22, constructors are named functions matching the contract name. A typo or case mismatch (e.g., `owned()` vs `Owned`) makes the constructor a regular public function anyone can call to seize ownership.

```solidity
contract Owned {
    function owned() public { owner = msg.sender; } // case mismatch!
}
```

### Grep-able keywords
`pragma solidity 0.4`, `function Wallet`, `function owned`

---

## Insufficient Access Control

**Reference:** `insufficient-access-control.md`

State-changing functions (ownership transfer, fee setting, minting, pausing) that lack access control modifiers or `require(msg.sender == ...)` checks are callable by anyone. Also check that `initialize()` in upgradeable contracts has the `initializer` modifier.

```solidity
function setOwner(address newOwner) external { owner = newOwner; } // no auth
```

### Grep-able keywords
`onlyOwner`, `onlyRole`, `msg.sender ==`, `initialize(`, `initializer`

---

## Insufficient Gas Griefing

**Reference:** `insufficient-gas-griefing.md`

In meta-transaction/relayer patterns, if replay protection (nonce marking) occurs before the sub-call and the relayer controls forwarded gas, the relayer can provide insufficient gas to silently fail the inner call while permanently consuming the nonce, censoring the action.

```solidity
executed[nonce] = true; // marked before sub-call
(bool success,) = target.call{gas: gasLimit}(data); // may silently fail
```

### Grep-able keywords
`gasleft()`, `.call{gas:`, `executed[`, `nonce`, `meta-transaction`, `relayer`

---

## Lack of Precision

**Reference:** `lack-of-precision.md`

Division before multiplication truncates intermediate results and compounds rounding error. If the numerator is smaller than the denominator, the result truncates to zero. Always multiply first, then divide.

```solidity
uint256 dailyRate = amount / 365;       // truncates
uint256 fee = dailyRate * daysEarly;     // wrong -- should be amount * daysEarly / 365
```

### Grep-able keywords
`/ `, `* `, `WAD`, `RAY`, `1e18`, `mulDiv`

---

## Missing Protection Against Signature Replay

**Reference:** `missing-protection-signature-replay.md`

If a signed message hash does not include a nonce, `address(this)`, and `block.chainid`, signatures can be replayed on the same contract, across contracts, or across chains. Use EIP-712 with a domain separator.

```solidity
bytes32 hash = keccak256(abi.encodePacked(to, amount)); // no nonce, no address, no chainid
```

### Grep-able keywords
`ecrecover`, `ECDSA.recover`, `nonces`, `block.chainid`, `address(this)`, `EIP712`, `domainSeparator`

---

## msg.value Reuse in Loops

**Reference:** `msgvalue-loop.md`

`msg.value` is constant for the entire transaction. Using it inside a loop allows a single payment to pass a `require(msg.value >= price)` check on every iteration, letting the caller buy N items for the price of one.

```solidity
for (uint256 i = 0; i < ids.length; i++) {
    require(msg.value >= price); // passes every iteration with one payment
    _mint(msg.sender, ids[i]);
}
```

### Grep-able keywords
`msg.value`, `multicall`, `delegatecall`

---

## Off-By-One Errors

**Reference:** `off-by-one.md`

Incorrect loop boundaries (`< length - 1` skips last element, `<= length` goes out of bounds) and wrong comparison operators at thresholds (`<` vs `<=`) cause elements to be skipped, out-of-bounds access, or incorrect boundary enforcement.

```solidity
for (uint256 i = 0; i < users.length - 1; i++) // skips last user
```

### Grep-able keywords
`length - 1`, `<= length`, `< length`

---

## Outdated Compiler Version

**Reference:** `outdated-compiler-version.md`

Using an old Solidity version misses critical security features (e.g., <0.8.0 has no built-in overflow checks) and may contain known compiler bugs. Check `pragma solidity` against the latest stable release and the known bugs list.

### Grep-able keywords
`pragma solidity`

---

## Integer Overflow and Underflow

**Reference:** `overflow-underflow.md`

In Solidity <0.8.0, arithmetic wraps silently. In >=0.8.0, arithmetic inside `unchecked {}` or `assembly {}` blocks still wraps. Type downcasts (e.g., `uint8(bigValue)`) silently truncate in all versions.

```solidity
unchecked { x += 1; }       // wraps to 0 at max
uint8 small = uint8(256);   // truncates to 0
```

### Grep-able keywords
`unchecked`, `SafeMath`, `SafeCast`, `uint8(`, `uint16(`, `int8(`, `assembly`

---

## Reentrancy

**Reference:** `reentrancy.md`

If a contract makes an external call (`.call()`, `.send()`, `.transfer()`, `_safeMint()`, ERC777/ERC1155 hooks) before updating state, the callee can re-enter and exploit stale state. Follow checks-effects-interactions or use `nonReentrant`.

```solidity
(bool success,) = msg.sender.call{value: bal}("");  // external call
balances[msg.sender] = 0;                           // state update AFTER -- vulnerable
```

### Grep-able keywords
`.call{value`, `.send(`, `.transfer(`, `_safeMint`, `_safeTransfer`, `onERC721Received`, `onERC1155Received`, `tokensReceived`, `nonReentrant`, `ReentrancyGuard`

---

## Requirement Violation

**Reference:** `requirement-violation.md`

`require()` conditions that use `>` instead of `>=` (or vice versa) reject valid inputs or accept invalid ones. Also, `require` on external call return values may break on non-compliant tokens (e.g., USDT returns no bool).

```solidity
require(balances[msg.sender] > amount); // should be >= to allow exact balance
```

### Grep-able keywords
`require(`, `assert(`

---

## Shadowing State Variables

**Reference:** `shadowing-state-variables.md`

In Solidity <0.6.0, a child contract can re-declare a state variable with the same name as a parent's, creating two separate storage slots. Parent functions read the parent's variable while child functions read the child's, causing inconsistent behavior.

```solidity
contract Child is Base {
    address public owner; // shadows Base.owner -- two different variables
}
```

### Grep-able keywords
`is `, `override`, `virtual`

---

## Timestamp Dependence

**Reference:** `timestamp-dependence.md`

`block.timestamp` can be manipulated by validators within ~15 seconds. Using it for randomness is always exploitable. Using it in tight conditional windows (<=15s) allows validators to include/exclude transactions. Safe for large time windows (hours/days).

```solidity
uint256 result = uint256(keccak256(abi.encodePacked(block.timestamp))) % 6;
```

### Grep-able keywords
`block.timestamp`, `now`, `block.number`

---

## Transaction-Ordering Dependence (Frontrunning)

**Reference:** `transaction-ordering-dependence.md`

Functions whose outcome depends on transaction ordering (swaps without slippage protection, on-chain secret submissions, ERC20 approve race conditions) are vulnerable to frontrunning/sandwiching from mempool observers.

```solidity
function swap(address tokenIn, address tokenOut, uint256 amountIn) external {
    // no minAmountOut -- sandwich attack possible
}
```

### Grep-able keywords
`minAmountOut`, `deadline`, `slippage`, `approve(`, `increaseAllowance`, `commit`, `reveal`

---

## Unchecked Return Values

**Reference:** `unchecked-return-values.md`

Low-level calls (`.call()`, `.send()`, `.delegatecall()`) return a boolean but do not revert on failure. If the return value is not checked, execution continues with state updates that assume success.

```solidity
msg.sender.send(amount);     // return value ignored -- silent failure
totalPaid += amount;          // updated even if send failed
```

### Grep-able keywords
`.call(`, `.send(`, `.delegatecall(`, `require(success`

---

## Unencrypted Private Data On-Chain

**Reference:** `unencrypted-private-data-on-chain.md`

The `private` visibility modifier only prevents other contracts from reading the variable. Anyone can read any storage slot via `eth_getStorageAt`. Never store plaintext secrets, passwords, or keys on-chain.

```solidity
bytes32 private secretAnswer; // readable via eth_getStorageAt
```

### Grep-able keywords
`private`, `secret`, `password`, `key`, `answer`

---

## Unexpected ecrecover Null Address

**Reference:** `unexpected-ecrecover-null-address.md`

`ecrecover` returns `address(0)` for invalid signatures. If the recovered address is not checked against `address(0)` and the expected signer is uninitialized (defaults to `address(0)`), the auth check passes for anyone. Use OpenZeppelin's `ECDSA.recover`.

```solidity
address recovered = ecrecover(hash, v, r, s);
require(recovered == signer); // if signer is address(0), any invalid sig passes
```

### Grep-able keywords
`ecrecover`, `address(0)`, `ECDSA.recover`

---

## Uninitialized Storage Pointer

**Reference:** `uninitialized-storage-pointer.md`

In Solidity <0.5.0, local struct/array variables without an explicit `memory` or `storage` keyword default to `storage` at slot 0, silently overwriting early state variables (e.g., `owner`) on assignment.

```solidity
User u;           // defaults to storage slot 0 in <0.5.0
u.addr = _addr;   // overwrites slot 0 (e.g., owner)
```

### Grep-able keywords
`pragma solidity 0.4`, `storage`, `memory`

---

## Unsupported Opcodes

**Reference:** `unsupported-opcodes.md`

Contracts compiled with Solidity >=0.8.20 emit the `PUSH0` opcode, which is unsupported on some chains. `.transfer()` and `.send()` use a 2300 gas stipend that is insufficient on chains like zkSync Era. Dynamic `create`/`create2` with runtime bytecode fails on zkSync.

```solidity
payable(msg.sender).transfer(amount); // 2300 gas -- fails on zkSync Era
```

### Grep-able keywords
`pragma solidity 0.8.20`, `.transfer(`, `.send(`, `PUSH0`, `selfdestruct`, `create(`, `create2(`

---

## Use of Deprecated Functions

**Reference:** `use-of-deprecated-functions.md`

Deprecated Solidity keywords (`suicide`, `sha3`, `block.blockhash`, `callcode`, `throw`, `msg.gas`, `constant` as function modifier, `var`) may behave unexpectedly or fail to compile on newer versions. `selfdestruct` is also deprecated post-Dencun.

### Grep-able keywords
`suicide`, `sha3`, `block.blockhash`, `callcode`, `throw`, `msg.gas`, `selfdestruct`, `constant`, `var `

---

## Weak Sources of Randomness

**Reference:** `weak-sources-randomness.md`

Randomness derived from on-chain data (`block.timestamp`, `block.prevrandao`, `blockhash`, `block.number`) is deterministic and publicly visible. Another contract in the same transaction can compute the identical "random" value and only call when the outcome is favorable. Use Chainlink VRF.

```solidity
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao))) % 100;
```

### Grep-able keywords
`block.prevrandao`, `block.difficulty`, `blockhash`, `block.timestamp`, `keccak256`, `% `

---

## Assert Violation

**Reference:** `assert-violation.md`

`assert()` should only be used for invariants that can never fail in a correct contract. Using it for input validation or external call checks wastes all remaining gas on failure (<0.8.0) and provides no custom error message. Use `require()` instead.

```solidity
assert(balances[msg.sender] >= amount); // wrong -- should be require
```

### Grep-able keywords
`assert(`

---

## Incorrect Inheritance Order

**Reference:** `incorrect-inheritance-order.md`

Solidity's C3 linearization gives precedence to the rightmost parent in the inheritance list. If two parents define the same function, the wrong order silently resolves to the unintended parent's implementation. Order from most base (left) to most derived (right).

```solidity
contract Treasury is Governance, Ownable { } // Ownable.owner() wins (rightmost)
```

### Grep-able keywords
`is `, `override(`, `virtual`, `super.`

---

## Unsecure Signatures (Composite)

**Reference:** `unsecure-signatures.md`

A composite vulnerability covering all signature anti-patterns: missing replay protection (no nonce/chainId/address), signature malleability (tracking by raw bytes), unchecked ecrecover null address, hash collisions from `abi.encodePacked` with dynamic types, and absence of EIP-712 structured signing.

```solidity
bytes32 hash = keccak256(abi.encodePacked(to, amount)); // no nonce, no chainid
address recovered = ecrecover(hash, v, r, s);            // no null check
require(!used[sig]); used[sig] = true;                    // malleable bypass
```

### Grep-able keywords
`ecrecover`, `ECDSA.recover`, `abi.encodePacked`, `used[sig]`, `EIP712`, `domainSeparator`

---

## Unbounded Return Data

**Reference:** `unbounded-return-data.md`

When `.call()` targets an untrusted address, Solidity automatically copies all return data into memory. A malicious callee can return megabytes of data, causing quadratic memory expansion costs and an out-of-gas revert. Use assembly to bound `returndatacopy`.

```solidity
(bool success,) = callback.call(data); // attacker returns huge data, OOG
```

### Grep-able keywords
`returndatasize`, `returndatacopy`, `ExcessivelySafeCall`, `.call(`

---

## Unused Variables

**Reference:** `unused-variables.md`

Unused state variables, parameters, or discarded return values may indicate dead code or missing logic (e.g., an unchecked transfer return value). Each unused variable should be evaluated: is it safe to remove, or does it signal a bug?

### Grep-able keywords
Compiler warnings; no single keyword -- review declarations vs. references.

---

## Signature Malleability

**Reference:** `signature-malleability.md`

For every ECDSA signature `(r, s, v)`, a complementary signature `(r, n-s, flipped_v)` also recovers to the same address. If deduplication is done by raw signature bytes (`mapping(bytes => bool)`), an attacker can submit the malleable variant to bypass replay protection. Use OpenZeppelin's ECDSA library or track by nonce/hash.

```solidity
mapping(bytes => bool) public usedSignatures; // malleable bypass
```

### Grep-able keywords
`mapping(bytes =>`, `usedSignatures`, `ecrecover`, `ECDSA.recover`

---

## Unsafe Low-Level Call

**Reference:** `unsafe-low-level-call.md`

Low-level `.call()` to an address with no deployed code silently succeeds (the EVM treats it as a successful no-op). Unchecked return values compound the issue. Verify target has code (`target.code.length > 0`) and always check the return boolean.

```solidity
(bool success,) = target.call(data); // succeeds even if target has no code
require(success);                     // passes -- no actual execution occurred
```

### Grep-able keywords
`.call(`, `.delegatecall(`, `.staticcall(`, `.code.length`, `require(success`

## references/arbitrary-storage-location.md

# Write to Arbitrary Storage Location

## Preconditions
- Contract has a dynamic array in storage
- User input controls the index used for writing to that array
- No bounds checking on the index before the write
- OR: assembly-level `sstore` with a user-controlled slot value

## Vulnerable Pattern
```solidity
uint256[] public data;
address public owner;

function write(uint256 index, uint256 value) external {
    // User controls index — can compute an index that maps
    // to any storage slot via the array's storage layout:
    // array elements start at keccak256(slot_of_length)
    // attacker calculates index to target owner's slot
    data[index] = value;
}

// Assembly variant
function writeSlot(uint256 slot, uint256 value) external {
    assembly {
        sstore(slot, value) // Direct arbitrary storage write
    }
}
```

## Detection Heuristics
1. Search for dynamic array writes where the index comes from user input or function parameters
2. Check if the index is bounds-checked before the write (e.g., `require(index < data.length)`)
3. Search for `sstore` in assembly blocks — check if the slot parameter is user-controlled
4. Search for `.length` assignments on dynamic arrays (Solidity <0.6.0 allowed `array.length = X`, enabling array expansion to reach any slot)
5. If an unbounded array write exists, verify whether the array's keccak256-based slot layout could collide with other state variable slots

## False Positives
- Array index is validated against `array.length` before writing
- The array uses `push()` only (indices are not user-controlled)
- Assembly `sstore` uses a hardcoded or internally computed slot
- Modern Solidity (>=0.6.0) prevents direct `.length` manipulation

## Remediation
- Always bounds-check array indices: `require(index < data.length)`
- Use `push()` and `pop()` instead of direct index assignment for dynamic arrays
- Avoid exposing `sstore` with user-controlled slot values
- Use mappings instead of arrays when random-access writes are needed
```solidity
function safeWrite(uint256 index, uint256 value) external {
    require(index < data.length, "out of bounds");
    data[index] = value;
}
```

## references/assert-violation.md

# Assert Violation

## Preconditions
- Contract uses `assert()` statements
- The `assert` condition can be reached through valid program execution (not a true invariant)
- OR: `assert()` is used to validate user input or external call results instead of `require()`

## Vulnerable Pattern
```solidity
function transfer(address to, uint256 amount) external {
    // WRONG: assert used for input validation — should be require
    // In Solidity <0.8.0, this consumes ALL remaining gas on failure
    assert(balances[msg.sender] >= amount);

    balances[msg.sender] -= amount;
    balances[to] += amount;
}

function withdraw() external {
    uint256 bal = balances[msg.sender];
    balances[msg.sender] = 0;
    (bool success,) = msg.sender.call{value: bal}("");
    // WRONG: assert used to check external call result
    assert(success);
}
```

## Detection Heuristics
1. Search for all `assert(` calls in the codebase
2. For each, determine whether the condition is a true invariant (mathematically impossible to violate in a correct contract) or an input/state validation
3. If the condition depends on user input, external call results, or mutable external state, flag it — it should be `require()` instead
4. Check Solidity version: in <0.8.0, failing `assert` uses the `0xfe` INVALID opcode and consumes ALL remaining gas; in >=0.8.0, it reverts with `Panic(uint256)` error code `0x01` (refunds remaining gas but provides no custom message)
5. If an `assert` can be triggered by an attacker, check if the gas consumption creates a griefing vector

## False Positives
- The `assert` checks a genuine invariant (e.g., `assert(totalSupply == sumOfAllBalances)` after a provably correct operation)
- The condition is mathematically unreachable given the contract's logic
- Used in test code, not production contracts

## Remediation
- Use `require()` for input validation and external call checks — it refunds remaining gas and accepts an error message
- Reserve `assert()` only for invariant checks that should never fail in a correct contract
- In Solidity >=0.8.4, prefer custom errors with `require` for gas-efficient error handling
```solidity
// Correct: require for input validation
function transfer(address to, uint256 amount) external {
    require(balances[msg.sender] >= amount, "insufficient balance");
    balances[msg.sender] -= amount;
    balances[to] += amount;
    // assert is appropriate here: totalSupply should never change during transfer
    assert(balances[msg.sender] + balances[to] == oldSum);
}
```

## references/asserting-contract-from-code-size.md

# Asserting Contract from Code Size

## Preconditions
- Contract uses `extcodesize` or `address.code.length` to check whether an address is an EOA vs. a contract
- This check gates access control, anti-bot logic, or security-sensitive operations
- The check assumes that code size == 0 means EOA

## Vulnerable Pattern
```solidity
modifier onlyEOA() {
    // During constructor execution, extcodesize returns 0
    // An attacker calling from their constructor bypasses this check
    require(msg.sender.code.length == 0, "no contracts");
    _;
}

function mint() external onlyEOA {
    _mint(msg.sender, 1);
}

// Assembly variant
function isContract(address addr) internal view returns (bool) {
    uint256 size;
    assembly { size := extcodesize(addr) }
    return size > 0; // Returns false during constructor
}
```

## Detection Heuristics
1. Search for `extcodesize`, `.code.length`, or helper functions named `isContract`
2. Check if the result is used to gate access (e.g., `require(... == 0)` to allow only EOAs)
3. If used for EOA-only enforcement, flag it — contracts calling from their constructor have code size 0
4. Also check for `tx.origin == msg.sender` as an alternative EOA check — flag it as incompatible with account abstraction (ERC-4337) and smart contract wallets
5. Check if the "no contracts" logic protects anything security-sensitive (minting limits, anti-bot, etc.)

## False Positives
- `extcodesize` is used to check if a contract EXISTS at an address (not to distinguish EOA vs contract)
- The check is combined with other protections that don't rely solely on code size
- The function has no security implications if called by a contract

## Remediation
- There is no fully reliable on-chain method to distinguish EOAs from contracts
- Redesign logic to not depend on this distinction — make the system work correctly regardless of caller type
- If EOA-only behavior is truly needed, consider off-chain verification (e.g., signed messages from known EOAs)
- Remove `isContract` checks from security-critical paths
```solidity
// Instead of gating by caller type, use per-address limits
mapping(address => bool) public hasMinted;

function mint() external {
    require(!hasMinted[msg.sender], "already minted");
    hasMinted[msg.sender] = true;
    _mint(msg.sender, 1);
}
```

## references/authorization-txorigin.md

# Authorization Through tx.origin

## Preconditions
- Contract uses `tx.origin` for authorization or access control checks (e.g., `require(tx.origin == owner)`)
- A legitimate owner/admin may interact with untrusted external contracts

## Vulnerable Pattern
```solidity
contract Wallet {
    address public owner;

    function transferTo(address to, uint256 amount) external {
        // tx.origin is the EOA that initiated the entire tx chain
        // If owner calls MaliciousContract, which calls Wallet.transferTo,
        // tx.origin is still the owner — check passes
        require(tx.origin == owner, "not owner");
        payable(to).transfer(amount);
    }
}

contract Attacker {
    Wallet wallet;
    // Owner calls this (e.g., via a phishing link)
    fallback() external {
        // tx.origin == owner because owner initiated the tx
        wallet.transferTo(address(this), address(wallet).balance);
    }
}
```

## Detection Heuristics
1. Search for all instances of `tx.origin` in the codebase
2. If `tx.origin` is used in a `require`, `if`, or comparison for authorization purposes, flag it
3. Check if `tx.origin` is compared against privileged addresses (owner, admin, etc.)
4. Note: `tx.origin == msg.sender` used to verify EOA status is a different pattern (see asserting-contract-from-code-size) — still flag it but for different reasons (breaks with account abstraction)

## False Positives
- `tx.origin` used only for logging or analytics, not for authorization
- `tx.origin` used in combination with `msg.sender` checks where `msg.sender` is the primary authorization mechanism

## Remediation
- Replace `tx.origin` with `msg.sender` for all authorization checks
- `msg.sender` reflects the immediate caller, not the transaction originator
```solidity
function transferTo(address to, uint256 amount) external {
    require(msg.sender == owner, "not owner"); // Immediate caller check
    payable(to).transfer(amount);
}
```

## references/delegatecall-untrusted-callee.md

# Delegatecall to Untrusted Callee

## Preconditions
- Contract uses `delegatecall`
- The target address of the `delegatecall` is derived from user input, function parameters, or a mutable state variable settable by non-admin users
- OR: a proxy pattern where the implementation address can be changed by unauthorized parties

## Vulnerable Pattern
```solidity
// User-controlled delegatecall target
function forward(address callee, bytes calldata data) external {
    // Attacker supplies callee = malicious contract
    // Malicious contract overwrites storage (e.g., slot 0 = owner)
    (bool success,) = callee.delegatecall(data);
    require(success);
}

// Proxy with unprotected implementation setter
function setImplementation(address _impl) external {
    // Missing: require(msg.sender == admin)
    implementation = _impl;
}
```

## Detection Heuristics
1. Search for all `delegatecall` invocations in the codebase
2. For each, trace the target address: is it hardcoded, immutable, admin-only settable, or user-influenced?
3. If the target comes from a function parameter, calldata, or storage variable — check that only authorized roles can set it
4. For proxy contracts, verify that `upgradeTo` / `setImplementation` functions have proper access control
5. Check storage layout compatibility between the proxy and all possible implementation contracts
6. Flag any generic forwarding function that passes user-supplied addresses to `delegatecall`

## False Positives
- `delegatecall` target is hardcoded or immutable (e.g., `address immutable IMPL`)
- Target is set only in the constructor and cannot be changed
- Target is restricted to a whitelist of audited contracts
- Standard proxy patterns (EIP-1967, UUPS, TransparentProxy) with proper access control on upgrades

## Remediation
- Restrict `delegatecall` targets to trusted, immutable, or admin-only-settable addresses
- Use established proxy patterns (OpenZeppelin TransparentProxy, UUPS) with proper access control
- Never expose `delegatecall` with a user-supplied target address
- Verify storage layout compatibility between proxy and implementation contracts using tools like OpenZeppelin's storage layout checker
```solidity
// Safe: immutable implementation
address immutable implementation;
constructor(address _impl) {
    implementation = _impl;
}
fallback() external payable {
    (bool s,) = implementation.delegatecall(msg.data);
    require(s);
}
```

## references/dos-gas-limit.md

# DoS with Block Gas Limit

## Preconditions
- Contract iterates over a dynamic array or mapping whose size can grow unboundedly
- The iteration must complete in a single transaction (no batching/pagination)
- OR: time-sensitive logic where block stuffing by an attacker can delay transaction inclusion

## Vulnerable Pattern
```solidity
address[] public recipients;

function addRecipient(address r) external {
    recipients.push(r); // Array grows without bound
}

// Push-payment: one tx must process all recipients
function distributeRewards() external {
    for (uint256 i = 0; i < recipients.length; i++) {
        // When recipients.length grows large enough,
        // this loop exceeds block gas limit and ALWAYS reverts
        payable(recipients[i]).transfer(reward);
    }
}
```

## Detection Heuristics
1. Identify all loops (`for`, `while`) in the codebase
2. For each loop, check if the iteration count depends on a dynamic array or storage structure that can grow over time
3. If the loop is unbounded and must complete in a single transaction, flag it — it will eventually exceed the block gas limit
4. Check if the function supports batching or pagination (e.g., `startIndex`, `batchSize` parameters) — if not, flag it
5. For time-sensitive functions (auctions, deadlines, liquidations), check if an attacker could stuff blocks with high-gas transactions to delay inclusion

## False Positives
- Loop iterates over a fixed-size or bounded array (e.g., `uint256[10]`, array with a capped `maxLength`)
- Function supports paginated/batched execution across multiple transactions
- Loop iteration count is controlled by the caller (e.g., batch size parameter with reasonable max)
- The array is admin-only appendable and has a practical maximum

## Remediation
- Replace push-payment (contract sends to all) with pull-payment (recipients withdraw individually)
- If iteration is unavoidable, add batching/pagination with `startIndex` and `batchSize` parameters
- Cap array sizes with a maximum length check on push operations
- For time-sensitive logic, avoid designs where block stuffing can be profitable
```solidity
// Pull-payment pattern
mapping(address => uint256) public pendingWithdrawals;

function claimReward() external {
    uint256 amount = pendingWithdrawals[msg.sender];
    pendingWithdrawals[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
}
```

## references/dos-revert.md

# DoS with (Unexpected) Revert

## Preconditions
- Critical contract logic depends on an external call succeeding
- A single revert in the external call blocks the entire function
- OR: strict equality checks on contract balance can be violated by force-sent ETH
- OR: division by zero is possible due to unvalidated denominators

## Vulnerable Pattern
```solidity
// Push-payment: one reverting recipient blocks all payments
function payAll() external {
    for (uint256 i = 0; i < recipients.length; i++) {
        // If ANY recipient reverts (e.g., contract with no receive()),
        // the entire function reverts — no one gets paid
        require(payable(recipients[i]).send(amounts[i]), "transfer failed");
    }
}

// Strict balance check broken by force-sent ETH
function withdraw() external {
    // Attacker sends ETH via selfdestruct, breaking this check
    require(address(this).balance == expectedBalance, "invariant");
    _processWithdrawal();
}

// Division by zero
function distribute(uint256 totalShares) external {
    // If totalShares == 0, this reverts and blocks the function
    uint256 perShare = totalRewards / totalShares;
}
```

## Detection Heuristics
1. Search for loops containing `require` or `assert` on external call results — one failure blocks all iterations
2. Search for push-payment patterns: contract iterating over recipients and sending ETH/tokens in one transaction
3. Search for strict balance equality checks (`address(this).balance ==`) — these can be broken by `selfdestruct` or coinbase rewards force-sending ETH
4. Search for division operations and check if the denominator can be zero
5. Check for `require(success)` after `.send()` or `.call()` inside loops — this turns a single recipient failure into a full DoS
6. Look for "highest bidder" or "king of the hill" patterns where the current leader's refund must succeed for a new leader to be set

## False Positives
- Pull-payment pattern is used (each recipient withdraws individually)
- The external call target is a trusted, known contract that will not revert
- Division denominator is guaranteed non-zero by prior checks or invariants
- Balance checks use `>=` instead of `==`
- The function handles individual failures gracefully (try/catch, continue on failure)

## Remediation
- Replace push-payment with pull-payment: let recipients withdraw individually
- Use `>=` instead of `==` for balance checks to tolerate force-sent ETH
- Validate all denominators before division: `require(totalShares > 0)`
- In loops, handle individual call failures without reverting the whole transaction
- Use try/catch for external calls where failure should not be fatal
```solidity
// Pull-payment pattern
mapping(address => uint256) public pendingWithdrawals;

function claimPayment() external {
    uint256 amount = pendingWithdrawals[msg.sender];
    require(amount > 0, "nothing to claim");
    pendingWithdrawals[msg.sender] = 0;
    (bool success,) = msg.sender.call{value: amount}("");
    require(success);
}
```

## references/hash-collision.md

# Hash Collision with abi.encodePacked()

## Preconditions
- Contract uses `abi.encodePacked()` to encode data before hashing (typically with `keccak256`)
- Two or more adjacent arguments in the `encodePacked` call are variable-length types (strings, bytes, dynamic arrays)
- The resulting hash is used for authentication, deduplication, or signature verification

## Vulnerable Pattern
```solidity
function verify(string memory a, string memory b, bytes memory sig) external {
    // abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")
    // Attacker shifts bytes between arguments to forge a valid hash
    bytes32 hash = keccak256(abi.encodePacked(a, b));
    require(ECDSA.recover(hash, sig) == trustedSigner);
    _execute(a, b);
}

// Array variant:
// abi.encodePacked([addr1, addr2], [addr3])
//   == abi.encodePacked([addr1], [addr2, addr3])
```

## Detection Heuristics
1. Search for `abi.encodePacked(` calls
2. Check how many arguments are variable-length types (string, bytes, dynamic arrays like `address[]`, `uint256[]`)
3. If two or more adjacent arguments are variable-length, flag it — elements can be shifted between arguments to produce an identical encoding
4. Check if the packed result feeds into `keccak256` for security-sensitive purposes (signature verification, access control, deduplication)
5. If only one argument is variable-length, or all arguments are fixed-length (address, uint256, bool), it is safe

## False Positives
- Only one argument is a variable-length type (no adjacent dynamic types to shift between)
- All arguments are fixed-length types (address, uint256, bytes32, bool, etc.)
- `abi.encode()` is used instead of `abi.encodePacked()` (includes length prefixes, no collision)
- The hash is not used for any security-sensitive purpose

## Remediation
- Replace `abi.encodePacked()` with `abi.encode()` — it includes length prefixes that prevent collisions
- If `encodePacked` must be used for gas efficiency, ensure at most one argument is a variable-length type
- Alternatively, separate variable-length arguments with fixed-length delimiters
```solidity
// Safe: abi.encode includes length prefixes
bytes32 hash = keccak256(abi.encode(a, b));

// Also safe: only one variable-length argument
bytes32 hash = keccak256(abi.encodePacked(fixedAddr, dynamicString));
```

## references/inadherence-to-standards.md

# Inadherence to Standards

## Preconditions
- Contract claims to implement a standard (ERC20, ERC721, ERC1155, etc.) but deviates from the specification
- OR: contract integrates external tokens assuming strict standard compliance without handling common deviations

## Vulnerable Pattern
```solidity
// Non-compliant ERC20: missing return value on transfer
// (matches USDT, BNB behavior — breaks callers that check return)
function transfer(address to, uint256 amount) external {
    balances[msg.sender] -= amount;
    balances[to] += amount;
    // Missing: return true;
    // Missing: emit Transfer(msg.sender, to, amount);
}

// Caller assumes strict compliance — breaks on non-compliant tokens
function depositToken(IERC20 token, uint256 amount) external {
    // Reverts on tokens that don't return bool (USDT)
    require(token.transfer(address(this), amount), "transfer failed");
    deposits[msg.sender] += amount;
    // Bug: doesn't account for fee-on-transfer tokens
    // Actual received amount may be less than `amount`
}
```

## Detection Heuristics
1. For token implementations: check that all required functions, return values, and events match the standard exactly (e.g., ERC20 requires `transfer` returns `bool` and emits `Transfer`)
2. For token integrations: check if `SafeERC20` is used for `transfer`/`transferFrom`/`approve` calls — raw IERC20 calls break on non-compliant tokens
3. Check for hardcoded assumptions: 18 decimals, no fee-on-transfer, no rebasing, no blocklists
4. For fee-on-transfer tokens: check if the contract uses balance-before/balance-after pattern to measure actual received amount
5. Check for missing `safeTransfer`/`safeTransferFrom` wrappers

## False Positives
- The contract explicitly documents that it only supports fully compliant ERC20 tokens and enforces this via a whitelist
- `SafeERC20` from OpenZeppelin is used, which handles missing return values
- The contract checks balance differences to account for fee-on-transfer

## Remediation
- For token implementations: strictly follow the standard — include all return values, events, and function signatures
- For token integrations: use OpenZeppelin's `SafeERC20` for all token interactions
- Use balance-before/balance-after pattern for fee-on-transfer support
- Don't hardcode decimals — read from the token contract
```solidity
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;

function depositToken(IERC20 token, uint256 amount) external {
    uint256 balBefore = token.balanceOf(address(this));
    token.safeTransferFrom(msg.sender, address(this), amount);
    uint256 received = token.balanceOf(address(this)) - balBefore;
    deposits[msg.sender] += received;
}
```

## references/incorrect-constructor.md

# Incorrect Constructor Name

## Preconditions
- Solidity version <0.4.22 where constructors are named functions matching the contract name
- The function name does not exactly match the contract name (typo, case mismatch, or contract renamed without updating the constructor)

## Vulnerable Pattern
```solidity
// Solidity <0.4.22: constructor is a named function
contract Owned {
    address public owner;

    // Typo: "owned" != "Owned" (case mismatch)
    // This becomes a regular public function anyone can call
    function owned() public {
        owner = msg.sender;
    }
}

// Contract renamed but constructor not updated
contract Treasury {
    address public owner;

    // Was "Wallet" before rename — now a regular public function
    function Wallet() public {
        owner = msg.sender;
    }
}
```

## Detection Heuristics
1. Check the Solidity version: if >=0.4.22 and the `constructor` keyword is used, this vulnerability does not apply
2. For <0.4.22 contracts: find the function that sets initial state (owner, parameters) and verify its name exactly matches the contract name (case-sensitive)
3. Search for public/external functions that set `owner` or perform one-time initialization — these may be misnamed constructors
4. Check if the contract was renamed at any point (git history, comments) but the constructor function was not updated
5. Flag any named function that appears to perform initialization logic (sets owner, initializes critical state) but doesn't match the contract name

## False Positives
- Solidity >=0.4.22 using the `constructor` keyword (enforced by compiler)
- The function is intentionally a public initializer (e.g., in proxy patterns) with proper access control

## Remediation
- Upgrade to Solidity >=0.4.22 and use the `constructor` keyword
- For legacy contracts, verify the constructor function name exactly matches the contract name
```solidity
// Modern Solidity: compiler-enforced constructor
contract Owned {
    address public owner;

    constructor() {
        owner = msg.sender;
    }
}
```

## references/incorrect-inheritance-order.md

# Incorrect Inheritance Order

## Preconditions
- Contract uses multiple inheritance (`is ContractA, ContractB, ...`)
- Two or more parent contracts define a function with the same name and signature
- The inheritance order (left-to-right) does not match the developer's intended resolution

## Vulnerable Pattern
```solidity
contract Ownable {
    function owner() public view virtual returns (address) {
        return _owner; // Returns EOA owner
    }
}

contract Governance {
    function owner() public view virtual returns (address) {
        return governance; // Returns governance contract
    }
}

// C3 linearization: rightmost (Ownable) takes precedence
// Developer intended Governance.owner() but gets Ownable.owner()
contract Treasury is Governance, Ownable {
    // owner() resolves to Ownable (rightmost) — may not be intended
    // Should be: is Ownable, Governance (if Governance should win)
}
```

## Detection Heuristics
1. Identify all contracts using multiple inheritance (`is A, B, C`)
2. For each inheritance chain, check if parent contracts define functions with the same name and signature
3. Verify the inheritance order follows general-to-specific (most base first, most derived last) — Solidity's C3 linearization gives precedence to the rightmost parent
4. Check `override` specifiers: `override(A, B)` should explicitly list which parents are being overridden
5. Trace the actual resolution order and compare against documented or intended behavior

## False Positives
- Only one parent defines the function (no ambiguity)
- The contract explicitly overrides the function with `override(A, B)` and provides its own implementation
- The inheritance order is intentionally general-to-specific and produces the correct resolution

## Remediation
- Order inheritance from most base to most derived (general-to-specific, left-to-right)
- Explicitly override conflicting functions and specify which parents: `override(ContractA, ContractB)`
- Test function resolution in complex hierarchies
```solidity
// Correct: general-to-specific order
contract Treasury is Ownable, Governance {
    function owner() public view override(Ownable, Governance) returns (address) {
        return Governance.owner(); // Explicit resolution
    }
}
```

## references/insufficient-access-control.md

# Insufficient Access Control

## Preconditions
- Contract has functions that modify sensitive state (ownership, balances, fees, minting, pausing, protocol parameters)
- Those functions lack access control modifiers (`onlyOwner`, `onlyRole`, etc.) or `require(msg.sender == ...)` checks
- OR: access control exists but is incomplete (e.g., role assignments themselves are unprotected)

## Vulnerable Pattern
```solidity
address public owner;
uint256 public feeRate;

// Missing access control — anyone can call
function setFeeRate(uint256 newRate) external {
    feeRate = newRate;
}

// Missing access control — anyone can take ownership
function setOwner(address newOwner) external {
    owner = newOwner;
}

// Incomplete: role grant is unprotected
function grantRole(address user, bytes32 role) external {
    // Missing: require(hasRole(ADMIN_ROLE, msg.sender))
    _roles[role][user] = true;
}
```

## Detection Heuristics
1. Identify all `external` and `public` functions that modify state variables
2. For each, check if it has an access control modifier (`onlyOwner`, `onlyRole`, `whenNotPaused`, etc.) or an inline `require(msg.sender == ...)` check
3. If a state-changing function has no access control, flag it — especially if it modifies ownership, balances, fees, or addresses
4. Check that role/permission management functions themselves are protected (e.g., `grantRole` requires `ADMIN_ROLE`)
5. Check `initialize()` functions in upgradeable contracts — they must have an `initializer` modifier to prevent re-initialization

## False Positives
- The function is intentionally permissionless by design (e.g., `deposit()`, `claim()` where anyone should be able to call)
- Access control is enforced in an internal function that the external function always calls through
- The contract is an implementation behind a proxy, and access control is enforced at the proxy level

## Remediation
- Add explicit access control to every state-changing function that should be restricted
- Use OpenZeppelin's `Ownable` or `AccessControl` for standardized role management
- Protect `initialize()` with the `initializer` modifier
- Apply the principle of least privilege: each function should require the minimum role necessary
```solidity
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract Secure is Ownable {
    function setFeeRate(uint256 newRate) external onlyOwner {
        feeRate = newRate;
    }
}
```

## references/insufficient-gas-griefing.md

# Insufficient Gas Griefing

## Preconditions
- Contract relays or forwards calls on behalf of users (meta-transactions, multisig execution, relayer patterns)
- The relayer/executor controls how much gas is forwarded to the sub-call
- Replay protection (nonce/hash marking) occurs before or regardless of sub-call success
- No minimum gas requirement is enforced before the sub-call

## Vulnerable Pattern
```solidity
function execute(address target, bytes calldata data, uint256 gasLimit) external {
    // Replay protection BEFORE sub-call — marks as executed regardless
    require(!executed[nonce], "already executed");
    executed[nonce] = true;
    nonce++;

    // Relayer can provide just enough gas for the outer tx to succeed
    // but insufficient gas for the inner call — it silently fails
    (bool success,) = target.call{gas: gasLimit}(data);
    // success is false, but the nonce is already consumed
    // The action is permanently censored
}
```

## Detection Heuristics
1. Identify relayer/meta-transaction patterns: functions that execute calls on behalf of other users
2. Check if the function marks a nonce/hash as used BEFORE the sub-call succeeds — this enables permanent censorship
3. Check if there's a `gasleft()` validation before the sub-call (e.g., `require(gasleft() >= requiredGas + overhead)`)
4. Look for multisig `execute` functions where the executor controls gas forwarding
5. Check if `.call{gas: X}` is used where `X` comes from the caller — the caller can set it too low

## False Positives
- Replay protection only marks the nonce after confirming sub-call success
- The function enforces a minimum gas requirement before the sub-call
- The sub-call failure is propagated (e.g., `require(success)`) so the outer tx also reverts, preserving the nonce
- The gas parameter is fixed or validated against a minimum

## Remediation
- Enforce minimum gas before sub-calls: `require(gasleft() >= gasLimit + OVERHEAD)`
- Only mark nonces/hashes as used AFTER confirming sub-call success
- Propagate sub-call failures to revert the outer transaction when appropriate
- Use EIP-150 rule awareness: the caller retains 1/64 of gas, so forward at least `gasLimit * 64/63`
```solidity
function execute(address target, bytes calldata data, uint256 gasLimit) external {
    require(gasleft() >= gasLimit + 10000, "insufficient gas");

    (bool success, bytes memory result) = target.call{gas: gasLimit}(data);

    // Only mark as executed if sub-call succeeded
    if (success) {
        executed[nonce] = true;
        nonce++;
    }
}
```

## references/lack-of-precision.md

# Lack of Precision

## Preconditions
- Contract performs integer arithmetic (division, fee calculations, reward distributions)
- Division is performed before multiplication, OR
- Numerators can be smaller than denominators (producing zero), OR
- No fixed-point scaling (WAD/RAY) is used for fractional calculations

## Vulnerable Pattern
```solidity
function calculateFee(uint256 amount, uint256 daysEarly) external view returns (uint256) {
    // Division BEFORE multiplication — truncates intermediate result
    uint256 dailyRate = amount / 365; // Loses precision
    uint256 fee = dailyRate * daysEarly; // Error compounds

    // Correct: amount * daysEarly / 365 (multiply first)
    return fee;
}

function distribute(uint256 reward, uint256 totalShares) external {
    for (uint256 i = 0; i < holders.length; i++) {
        // If reward < totalShares, this is always 0
        uint256 share = reward / totalShares * balances[holders[i]];
        _transfer(holders[i], share);
    }
}
```

## Detection Heuristics
1. Search for division operations (`/`) in arithmetic expressions
2. Check if division appears before multiplication in the same expression — this loses precision
3. Check if the numerator can be smaller than the denominator — the result truncates to zero
4. Look for fee, reward, interest, or share calculations without scaling factors (e.g., 1e18)
5. Check rounding direction: does truncation favor the protocol or the user? In fee/debt calculations, rounding should favor the protocol; in reward/credit calculations, it should favor the user

## False Positives
- Multiplication is performed before division in the correct order
- Fixed-point math libraries (WAD = 1e18, RAY = 1e27) are used to maintain precision
- The numerator is guaranteed to be larger than the denominator by prior validation
- The precision loss is intentionally accepted and documented (e.g., dust amounts)

## Remediation
- Always multiply before dividing: `amount * rate / divisor` instead of `amount / divisor * rate`
- Use fixed-point math with scaling factors (1e18 for WAD, 1e27 for RAY)
- Round in favor of the protocol for fees/debts, in favor of users for rewards/credits
- Use `mulDiv` from OpenZeppelin or PRBMath for safe full-precision multiplication then division
```solidity
// Correct: multiply first, then divide
uint256 fee = amount * daysEarly / 365;

// With scaling for precision
uint256 WAD = 1e18;
uint256 scaledRate = (amount * WAD) / totalSupply;
uint256 reward = (scaledRate * userBalance) / WAD;
```

## references/missing-protection-signature-replay.md

# Missing Protection Against Signature Replay

## Preconditions
- Contract verifies ECDSA signatures for authorization
- The signed message does not include a nonce, OR does not include the contract address, OR does not include the chain ID
- No mechanism tracks which signatures have been processed

## Vulnerable Pattern
```solidity
function executeWithSig(address to, uint256 amount, bytes memory sig) external {
    // Missing: nonce, contract address, and chain ID in hash
    // Same signature can be replayed on same contract, other contracts, or other chains
    bytes32 hash = keccak256(abi.encodePacked(to, amount));
    address signer = ECDSA.recover(hash, sig);
    require(signer == authorizer, "invalid sig");

    // No nonce tracking — same signature can be submitted repeatedly
    _transfer(to, amount);
}
```

## Detection Heuristics
1. Search for `ecrecover` or `ECDSA.recover` usage
2. Examine what is included in the signed hash — check for:
   - Nonce: is there a per-signer incrementing nonce? Flag if absent (enables same-contract replay)
   - Contract address (`address(this)`): flag if absent (enables cross-contract replay)
   - Chain ID (`block.chainid`): flag if absent (enables cross-chain replay)
3. Check if processed signatures/hashes are tracked in a mapping to prevent reuse
4. Check if EIP-712 domain separator is used (it includes contract address and chain ID automatically)
5. Verify that the nonce is incremented BEFORE execution, not after (to prevent reentrancy-based replay)

## False Positives
- EIP-712 domain separator is used with proper nonce tracking (covers address + chainId + nonce)
- The signed message includes all three: nonce, contract address, and chain ID
- The signature authorizes a one-time action that is inherently non-replayable (e.g., EIP-2612 permit with deadline and nonce)

## Remediation
- Include nonce, `address(this)`, and `block.chainid` in the signed message hash
- Track processed nonces per signer in a mapping
- Use EIP-712 structured data signing with a domain separator
```solidity
mapping(address => uint256) public nonces;

function executeWithSig(address to, uint256 amount, bytes memory sig) external {
    uint256 nonce = nonces[msg.sender]++;
    bytes32 hash = keccak256(abi.encodePacked(
        to, amount, nonce, address(this), block.chainid
    ));
    bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash);
    address signer = ECDSA.recover(ethHash, sig);
    require(signer == authorizer, "invalid sig");
    _transfer(to, amount);
}
```

## references/msgvalue-loop.md

# msg.value Reuse in Loops

## Preconditions
- `msg.value` is referenced inside a loop (`for`, `while`) or in a function called multiple times within a single external call
- The contract has an existing ETH balance or the logic assumes `msg.value` is "spent" per iteration

## Vulnerable Pattern
```solidity
function batchBuy(uint256[] calldata ids) external payable {
    for (uint256 i = 0; i < ids.length; i++) {
        // msg.value is the SAME on every iteration — it never decreases
        // If price == 1 ETH and user sends 1 ETH, they can buy N items
        require(msg.value >= price, "insufficient payment");
        _mint(msg.sender, ids[i]);
    }
    // User paid 1 ETH but bought N items
}

// Payable multicall — same issue
function multicall(bytes[] calldata calls) external payable {
    for (uint256 i = 0; i < calls.length; i++) {
        // msg.value forwarded to each sub-call — reused each time
        (bool s,) = address(this).delegatecall(calls[i]);
        require(s);
    }
}
```

## Detection Heuristics
1. Search for `msg.value` usage inside `for`, `while`, or `do-while` loops
2. Search for `msg.value` in functions that are called via `delegatecall` in a loop (multicall patterns)
3. Check if `msg.value` is used in a `require` check inside a loop — passes on every iteration after a single payment
4. Search for internal functions that reference `msg.value` and are called multiple times from a payable external function
5. Check if the contract subtracts from a local tracking variable instead of relying on `msg.value` directly

## False Positives
- The function tracks remaining value in a local variable and decrements it per iteration (e.g., `remaining -= price`)
- `msg.value` is only referenced once outside any loop
- The loop is guaranteed to execute exactly once
- The function validates total cost against `msg.value` before the loop (e.g., `require(msg.value == price * ids.length)`)

## Remediation
- Track remaining ETH in a local variable and decrement per operation
- Validate total cost upfront: `require(msg.value == price * count)`
- In multicall patterns, ensure `msg.value` is consumed only once, or use a tracking variable
```solidity
function batchBuy(uint256[] calldata ids) external payable {
    uint256 totalCost = price * ids.length;
    require(msg.value >= totalCost, "insufficient payment");

    for (uint256 i = 0; i < ids.length; i++) {
        _mint(msg.sender, ids[i]);
    }

    // Refund excess
    if (msg.value > totalCost) {
        payable(msg.sender).transfer(msg.value - totalCost);
    }
}
```

## references/off-by-one.md

# Off-By-One Errors

## Preconditions
- Contract uses loops with boundary conditions, comparison operators at thresholds, or array index calculations
- The boundary/comparison is off by exactly one from the intended behavior

## Vulnerable Pattern
```solidity
// Skips the last element
function processAll() external {
    for (uint256 i = 0; i < users.length - 1; i++) {
        // Should be i < users.length
        // Last user is never processed
        _distribute(users[i]);
    }
}

// Off-by-one in threshold check
function liquidate(uint256 ratio) external {
    // Should be ratio < MIN_RATIO (liquidate when below)
    // Using <= means accounts AT the minimum are also liquidated
    require(ratio <= MIN_RATIO, "healthy");
    _liquidate();
}

// Out-of-bounds access
function getLastUser() external view returns (address) {
    return users[users.length]; // Should be users.length - 1
}
```

## Detection Heuristics
1. For every loop, check the boundary condition: `< length` vs `<= length` vs `< length - 1`
2. `< length - 1` skips the last element — flag unless intentional
3. `<= length` goes out of bounds on array access — flag always
4. For comparison operators at thresholds (`>`, `>=`, `<`, `<=`), verify the boundary matches the specification (e.g., "greater than" vs "greater than or equal to")
5. Check pagination logic for fence-post errors: does the first page start at 0 or 1? Does the last batch include the final element?
6. Look for `length - 1` on arrays that could be empty — this underflows to `type(uint256).max` in unchecked contexts or reverts in checked contexts

## False Positives
- The boundary is intentionally exclusive (e.g., `< length - 1` to skip the sentinel/last element by design)
- The comparison operator matches the documented specification exactly
- The code is iterating over pairs (`i < length - 1` to compare `arr[i]` with `arr[i+1]`)

## Remediation
- Verify each boundary condition against the specification or documented intent
- Be explicit about inclusive vs exclusive bounds in comments
- Guard against empty array underflow: check `length > 0` before `length - 1`
```solidity
function processAll() external {
    for (uint256 i = 0; i < users.length; i++) {
        _distribute(users[i]);
    }
}

// Explicit about boundary semantics
function liquidate(uint256 ratio) external {
    require(ratio < MIN_RATIO, "healthy"); // Strictly below = liquidatable
    _liquidate();
}
```

## references/outdated-compiler-version.md

# Outdated Compiler Version

## Preconditions
- Contract is compiled with a Solidity version significantly behind the latest stable release
- The compiler version used has known bugs or is missing important security features

## Vulnerable Pattern
```solidity
// Old version missing built-in overflow checks
pragma solidity 0.7.6;

contract Token {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // No overflow protection — 0.7.x lacks built-in checks
        // Requires manual SafeMath usage
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

// Slightly newer but still has known bugs
pragma solidity 0.8.0;
// 0.8.0 had ABI encoding bugs fixed in later patches
```

## Detection Heuristics
1. Check the `pragma solidity` version in all contract files
2. Compare against the latest stable Solidity release — flag if significantly outdated
3. Cross-reference the exact version against the Solidity known bugs list
4. Check if the contract misses key safety features from newer versions:
   - <0.8.0: no built-in overflow/underflow checks
   - <0.8.4: no custom errors (gas efficiency)
   - <0.8.20: no PUSH0 opcode support
5. Flag if the version has known critical bugs (check https://solidity.readthedocs.io/en/latest/bugs.html)

## False Positives
- The version is intentionally chosen for compatibility with a specific deployment target or toolchain
- The version is recent (within 1-2 minor versions of latest) and has no known critical bugs
- The project has documented reasons for the specific version choice

## Remediation
- Upgrade to the latest stable Solidity version unless there's a specific compatibility constraint
- Cross-reference the current version against the known bugs list before and after upgrading
- When upgrading across major boundaries (e.g., 0.7 to 0.8), review all arithmetic for compatibility with built-in overflow checks
```solidity
// Use latest stable version
pragma solidity 0.8.24;

contract Token {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        balances[msg.sender] -= amount; // Built-in overflow check
        balances[to] += amount;
    }
}
```

## references/overflow-underflow.md

# Integer Overflow and Underflow

## Preconditions
- Solidity <0.8.0 without SafeMath, OR
- Arithmetic inside `unchecked { }` blocks, OR
- Arithmetic inside `assembly { }` / Yul blocks, OR
- Type downcasting (e.g., `uint8(uint256Var)`), OR
- Shift operators (`<<`, `>>`) on values near type boundaries

## Vulnerable Pattern
```solidity
// Pre-0.8.0: wraps silently
uint256 balance = 0;
balance -= 1; // Underflows to 2^256 - 1

// Post-0.8.0 bypass via unchecked block
unchecked {
    uint256 x = type(uint256).max;
    x += 1; // Wraps to 0, no revert
}

// Type downcast truncation (all versions)
uint256 big = 256;
uint8 small = uint8(big); // Silently truncates to 0

// Assembly arithmetic (all versions)
assembly {
    let x := sub(0, 1) // Underflows to max uint256
}
```

## Detection Heuristics
1. Check the Solidity version: if <0.8.0, flag ALL arithmetic operations not wrapped in SafeMath
2. If >=0.8.0, search for `unchecked` blocks and audit every arithmetic operation inside them
3. Search for `assembly` blocks and audit all `add`, `sub`, `mul`, `div`, `shl`, `shr` operations within
4. Search for type downcasts: patterns like `uint8(`, `uint16(`, `int8(`, etc. — verify the source value is bounds-checked before casting
5. Search for shift operations (`<<`, `>>`) and verify the operand cannot overflow the target type
6. Check if overflow-induced reverts on critical paths could cause DoS

## False Positives
- Solidity >=0.8.0 arithmetic outside of `unchecked` and `assembly` blocks (automatically checked)
- `unchecked` blocks used only for loop counter increments (`i++`) where overflow is impossible due to bounded loop
- Downcasts where the value is validated to fit the target type before casting (e.g., `require(x <= type(uint8).max)`)
- Assembly arithmetic on values with proven bounds

## Remediation
- For Solidity <0.8.0: use OpenZeppelin's `SafeMath` for all arithmetic
- For >=0.8.0: minimize `unchecked` blocks to only provably safe operations (e.g., bounded loop counters)
- Use OpenZeppelin's `SafeCast` library for all type downcasts
- Validate bounds before assembly arithmetic
```solidity
// Safe downcast
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
uint8 small = SafeCast.toUint8(big); // Reverts if big > 255
```

## references/reentrancy.md

# Reentrancy

## Preconditions
- Contract makes an external call (ETH transfer, token transfer, `.call()`, `.send()`, `.transfer()`, callback hook)
- State is modified after the external call, not before
- No reentrancy guard (`nonReentrant` modifier) on the function
- For cross-function: two or more functions share state, and at least one makes an external call before updating that shared state
- For cross-contract: Contract B reads Contract A's state, and A makes an external call before updating it
- For read-only: Contract A has a reentrancy guard but updates state after an external call; Contract B reads A's state without sharing the same lock

## Vulnerable Pattern
```solidity
// Single-function reentrancy
function withdraw() external {
    uint256 bal = balances[msg.sender];
    // External call BEFORE state update
    (bool success,) = msg.sender.call{value: bal}("");
    require(success);
    // State update AFTER external call — attacker reenters withdraw()
    // and balances[msg.sender] is still the original value
    balances[msg.sender] = 0;
}

// Hidden external calls that trigger callbacks:
// ERC721._safeMint() -> onERC721Received()
// ERC1155.safeTransferFrom() -> onERC1155Received()
// ERC777 token transfers -> tokensReceived() hook
```

## Detection Heuristics
1. Identify all external calls: `.call()`, `.send()`, `.transfer()`, token transfers, `_safeMint()`, `_safeTransfer()`, ERC777/ERC1155 safe transfers
2. For each external call, check if any state variable is written AFTER the call in the same function
3. If state is written after an external call, check if a `nonReentrant` guard is present on the function — flag if absent
4. Check for cross-function reentrancy: does the function share state with other functions that could be called during the reentrant window?
5. Check for cross-contract reentrancy: does any other contract read this contract's state that is stale during the external call?
6. Check for hidden callbacks: `_safeMint`, `_safeTransfer`, ERC777 hooks, ERC1155 hooks — these are external calls even though they don't look like `.call()`

## False Positives
- State is updated BEFORE the external call (checks-effects-interactions pattern correctly followed)
- `nonReentrant` modifier is applied to the function
- The external call target is a trusted, immutable contract (e.g., WETH) with no callback mechanism
- The function is `view`/`pure` and cannot modify state
- The only state read after reentry is already finalized (e.g., immutable variables)

## Remediation
- Apply the checks-effects-interactions pattern: perform all state changes before any external call
- Add OpenZeppelin's `ReentrancyGuard` with `nonReentrant` modifier to all functions that make external calls
- For cross-contract reentrancy, use a shared reentrancy lock across contracts or ensure state is finalized before external calls
```solidity
function withdraw() external nonReentrant {
    uint256 bal = balances[msg.sender];
    balances[msg.sender] = 0;  // State update BEFORE external call
    (bool success,) = msg.sender.call{value: bal}("");
    require(success);
}
```

## references/requirement-violation.md

# Requirement Violation

## Preconditions
- Contract uses `require()` statements for input or state validation
- The `require` condition is overly restrictive (rejects valid inputs) or too loose (accepts invalid inputs)
- OR: `require()` validates return values from external contracts whose behavior doesn't match assumptions

## Vulnerable Pattern
```solidity
// Overly restrictive: blocks legitimate use case
function withdraw(uint256 amount) external {
    // Fails if user has exactly the required amount (should be >=)
    require(balances[msg.sender] > amount, "insufficient");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}

// External contract assumption mismatch
function processPayment(IERC20 token, uint256 amount) external {
    // Assumes transfer returns true, but some tokens don't return a value
    // require reverts on tokens like USDT
    require(token.transfer(msg.sender, amount), "failed");
}

// Missing error message
function setRate(uint256 rate) external {
    require(rate > 0); // No error message — difficult to debug
}
```

## Detection Heuristics
1. For each `require()`, verify the condition matches the documented business logic (e.g., `>` vs `>=`, `<` vs `<=`)
2. Check if `require()` validates an external call's return value — verify the external contract actually returns what's expected
3. Look for `require()` without error messages — while not a vulnerability, it makes debugging difficult
4. Check if overly strict requirements can DoS critical paths (e.g., withdrawals, liquidations)
5. Check chained contract calls: does an upstream contract provide inputs that could fail downstream `require` checks?

## False Positives
- The `require` condition exactly matches the specification
- The strictness is intentional and documented
- The error message is omitted in a pre-0.8.4 contract for gas optimization (custom errors not yet available)

## Remediation
- Verify each `require` condition against the specification: `>` vs `>=`, `<` vs `<=`
- Add descriptive error messages or use custom errors (Solidity >=0.8.4)
- For external call validations, use SafeERC20 or similar wrappers that handle non-standard return values
```solidity
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "insufficient balance");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}

// Solidity >=0.8.4: custom errors for gas efficiency
error InsufficientBalance(uint256 available, uint256 requested);

function withdrawV2(uint256 amount) external {
    if (balances[msg.sender] < amount)
        revert InsufficientBalance(balances[msg.sender], amount);
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}
```

## references/shadowing-state-variables.md

# Shadowing State Variables

## Preconditions
- Contract inherits from one or more parent contracts
- A state variable in the child contract has the same name as a state variable in a parent contract
- Solidity version <0.6.0 (>=0.6.0 disallows state variable shadowing with a compiler error)
- OR: function parameters or local variables shadow state variables (any Solidity version)

## Vulnerable Pattern
```solidity
contract Base {
    address public owner;

    constructor() {
        owner = msg.sender;
    }
}

// Solidity <0.6.0: this compiles without error
contract Child is Base {
    address public owner; // Shadows Base.owner — creates a NEW variable

    function setOwner(address _owner) external {
        owner = _owner; // Sets Child.owner, NOT Base.owner
    }

    // Base's functions still read Base.owner (the original)
    // Child's functions read Child.owner (the shadow)
    // These are two different storage variables!
}

// Local variable shadowing (any version)
contract Example {
    uint256 public value = 100;

    function getValue() public view returns (uint256) {
        uint256 value = 0; // Shadows state variable
        return value; // Returns 0, not 100
    }
}
```

## Detection Heuristics
1. Check Solidity version: if <0.6.0, search for state variables in child contracts that share names with parent contract variables
2. For any version, search for function parameters and local variables that share names with state variables
3. Check compiler warnings — modern Solidity warns about local shadowing even if it allows it
4. For each shadowed variable, trace which version (parent's or child's) each function reads/writes — inconsistency indicates a bug
5. Pay special attention to `owner`, `admin`, and other access-control variables being shadowed

## False Positives
- Solidity >=0.6.0: state variable shadowing is a compiler error, so it can't happen
- Local variable shadowing where the intent is clear and the state variable is not needed in that scope (still bad practice but not exploitable)
- The shadowing is in a test file or non-production code

## Remediation
- Upgrade to Solidity >=0.6.0 where state variable shadowing is a compiler error
- For local variable shadowing, use distinct names (e.g., prefix with `_` for parameters)
- Rename conflicting variables to be unique across the inheritance chain
```solidity
contract Child is Base {
    // Don't redeclare — use Base.owner directly
    function setOwner(address _newOwner) external {
        owner = _newOwner; // Modifies Base.owner
    }
}
```

## references/signature-malleability.md

# Signature Malleability

## Preconditions
- Contract uses ECDSA signatures for authorization or deduplication
- Signatures are tracked by their raw bytes (e.g., `mapping(bytes => bool)`) to prevent replay
- No enforcement that the `s` value is in the lower half of the curve order

## Vulnerable Pattern
```solidity
mapping(bytes => bool) public usedSignatures;

function claimReward(bytes memory signature, uint256 amount) external {
    // Deduplication by raw signature bytes
    require(!usedSignatures[signature], "already used");

    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
    address signer = ecrecover(hash, v, r, s);
    require(signer == trustedSigner);

    usedSignatures[signature] = true; // Attacker submits (r, n-s, flipped_v)
    // to bypass this check with a valid but different signature
    _payout(msg.sender, amount);
}
```

## Detection Heuristics
1. Search for `mapping(bytes => bool)` or any mapping keyed by raw signature bytes
2. If signatures are used as mapping keys for deduplication, flag it — an attacker can compute the complementary `(r, n-s)` signature
3. Check if `ecrecover` is called directly without an `s`-value range check
4. Check if OpenZeppelin's ECDSA library is used (it enforces lower-s normalization)
5. If neither a library nor a manual `s < secp256k1n/2` check is present, flag it

## False Positives
- Signatures are deduplicated by message hash or nonce, not by raw signature bytes
- OpenZeppelin's `ECDSA.recover` is used, which rejects high-s signatures
- Manual check enforces `s <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0`

## Remediation
- Track used signatures by message hash or nonce, not by raw signature bytes
- Use OpenZeppelin's `ECDSA.recover` which enforces `s` in the lower half of the curve order
- If using raw `ecrecover`, add a manual `s`-value check
```solidity
// Use nonce-based deduplication instead of signature bytes
mapping(address => uint256) public nonces;

function claimReward(uint8 v, bytes32 r, bytes32 s, uint256 amount) external {
    uint256 nonce = nonces[msg.sender]++;
    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount, nonce));
    address signer = ECDSA.recover(hash, v, r, s); // Rejects malleable sigs
    require(signer == trustedSigner);
    _payout(msg.sender, amount);
}
```

## references/timestamp-dependence.md

# Timestamp Dependence

## Preconditions
- Contract uses `block.timestamp` (or the deprecated `now` alias) for security-sensitive logic
- The outcome of that logic can be influenced by a timestamp shift within the manipulation window (~15s on PoW chains, slot-fixed on PoS Ethereum but variable on L2s/sidechains)
- OR: `block.number` is used as a proxy for elapsed time

## Vulnerable Pattern
```solidity
// Timestamp as sole randomness source
function roll() external {
    // Validator can manipulate block.timestamp to bias outcome
    uint256 result = uint256(keccak256(abi.encodePacked(block.timestamp))) % 6;
    if (result == 0) {
        _payWinner(msg.sender);
    }
}

// Tight time window vulnerable to manipulation
function claimBonus() external {
    // 15-second window — validator can push timestamp to include/exclude
    require(block.timestamp >= deadline && block.timestamp <= deadline + 15);
    _sendBonus(msg.sender);
}
```

## Detection Heuristics
1. Search for `block.timestamp` and `now` (deprecated alias) usage
2. If used for randomness (e.g., fed into `keccak256` for a random value), flag immediately — this is always exploitable
3. If used in conditional logic, check the time window: can a ~15-second manipulation affect the outcome?
4. Search for `block.number` used as a time proxy (e.g., `block.number * 12` for seconds) — flag as fragile since block times change
5. For L2/sidechain deployments, check chain-specific timestamp guarantees — some have weaker constraints than mainnet PoS

## False Positives
- `block.timestamp` used only for logging or non-critical display purposes
- Time windows are large enough (hours/days) that a 15-second manipulation is irrelevant
- On PoS Ethereum mainnet, timestamps are fixed per 12-second slots — validator manipulation is constrained to slot boundaries, not arbitrary values
- `block.timestamp` used with a commit-reveal scheme where the timestamp alone doesn't determine the outcome

## Remediation
- Never use `block.timestamp` for randomness — use Chainlink VRF or another verifiable randomness oracle
- For time-dependent logic, ensure the acceptable window is significantly larger than the manipulation range
- Avoid `block.number` as a time proxy; use `block.timestamp` with appropriate tolerance
- On L2s/sidechains, verify chain-specific timestamp constraints before relying on `block.timestamp`
```solidity
// Safe: large time window where 15s manipulation is irrelevant
require(block.timestamp >= vestingEnd, "still vesting");
// vestingEnd is months/years away — manipulation doesn't matter
```

## references/transaction-ordering-dependence.md

# Transaction-Ordering Dependence (Frontrunning)

## Preconditions
- Transaction inputs are visible in the public mempool before block inclusion
- The outcome of the function depends on transaction ordering
- An attacker can profit by observing and front-running (or sandwiching) the victim's transaction

## Vulnerable Pattern
```solidity
// DEX swap without slippage protection
function swap(address tokenIn, address tokenOut, uint256 amountIn) external {
    // No minimum output amount — attacker sandwiches:
    // 1. Front-run: buy tokenOut (price goes up)
    // 2. Victim's swap executes at worse price
    // 3. Back-run: sell tokenOut (profit from price impact)
    uint256 amountOut = getAmountOut(amountIn);
    IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
    IERC20(tokenOut).transfer(msg.sender, amountOut);
}

// On-chain secret submission
function submitAnswer(bytes32 answer) external {
    // Answer visible in mempool — anyone can copy it
    require(keccak256(abi.encodePacked(answer)) == targetHash);
    _reward(msg.sender);
}

// ERC20 approval race condition
// User approves 100, wants to change to 50
// Attacker sees approve(50) in mempool, spends 100, then spends 50 = 150 total
```

## Detection Heuristics
1. Search for DEX/swap functions: check for slippage protection (`minAmountOut` parameter) and deadline parameters — flag if absent
2. Search for on-chain submissions of secrets, answers, or bids — these are observable in the mempool
3. Search for ERC20 `approve` patterns: check if the contract sets allowance to zero before setting a new value
4. Look for auction/bidding logic where observing others' bids provides an advantage
5. Identify any function where the order of execution matters and inputs are publicly visible before inclusion

## False Positives
- Transaction is submitted via a private mempool (Flashbots Protect, MEV Blocker)
- Commit-reveal scheme is used: commitment hash submitted first, reveal in a later block
- Slippage protection with `minAmountOut` and `deadline` parameters are present
- The function's outcome is order-independent (e.g., simple deposit into a vault at a fixed rate)

## Remediation
- For DEX operations: require `minAmountOut` and `deadline` parameters for slippage protection
- For secret submissions: use commit-reveal schemes (hash commitment first, reveal later)
- For ERC20 approvals: use `increaseAllowance`/`decreaseAllowance` or set to zero first
- Consider Flashbots or private transaction relays for MEV-sensitive operations
```solidity
function swap(
    address tokenIn, address tokenOut, uint256 amountIn,
    uint256 minAmountOut, // Slippage protection
    uint256 deadline       // Time protection
) external {
    require(block.timestamp <= deadline, "expired");
    uint256 amountOut = getAmountOut(amountIn);
    require(amountOut >= minAmountOut, "slippage");
    // ... execute swap
}
```

## references/unbounded-return-data.md

# Unbounded Return Data

## Preconditions
- Contract makes a low-level `.call()` to an untrusted or user-specified address
- Solidity's automatic return data copying is used (default behavior up to at least v0.8.26)
- No assembly-level restriction on `returndatacopy` size
- The function is on a critical path where revert would lock funds (withdrawals, undelegation)

## Vulnerable Pattern
```solidity
function unstake(address callback) external {
    uint256 amount = stakes[msg.sender];
    stakes[msg.sender] = 0;

    // Solidity automatically copies ALL return data into memory
    // Attacker's callback contract returns megabytes of data
    // Memory expansion cost grows quadratically — causes out-of-gas
    (bool success,) = callback.call(
        abi.encodeWithSignature("onUnstake(uint256)", amount)
    );
    // Even with limited gas stipend, the return data copy
    // happens in the CALLER's gas context
    require(success, "callback failed");
}
```

## Detection Heuristics
1. Search for `.call(`, `.delegatecall(`, `.staticcall(` to addresses that could be attacker-controlled
2. Check if the return data is handled by Solidity's default (captured as `bytes memory` or discarded but still copied)
3. If the call target is untrusted and no assembly-level return data size limit is used, flag it
4. Check if the function is on a critical path — can a revert here lock user funds?
5. Look for callback patterns (delegation hooks, unstaking callbacks, flash loan callbacks) where the callee is attacker-controlled

## False Positives
- The call target is a trusted, known contract (not user-controlled)
- Assembly is used to limit `returndatacopy` to a bounded size (e.g., max 32 bytes)
- `ExcessivelySafeCall` or similar library is used for bounded return data
- The function doesn't revert on call failure (uses try/catch or ignores success)

## Remediation
- Use assembly to limit return data size instead of Solidity's automatic copy
- Use Nomad's `ExcessivelySafeCall` library for calls to untrusted addresses
- Bound `returndatacopy` to only the bytes you need (typically 0 or 32)
```solidity
// Assembly-bounded return data copy
function safeCall(address target, bytes memory data) internal returns (bool success) {
    assembly {
        success := call(gas(), target, 0, add(data, 0x20), mload(data), 0, 0)
        // Only copy up to 32 bytes of return data
        let rdsize := returndatasize()
        if gt(rdsize, 0) {
            if gt(rdsize, 32) { rdsize := 32 }
            returndatacopy(0, 0, rdsize)
        }
    }
}
```

## references/unchecked-return-values.md

# Unchecked Return Values

## Preconditions
- Contract uses low-level calls: `.call()`, `.send()`, or `.delegatecall()`
- The boolean return value indicating success/failure is not checked
- State changes occur after the unchecked call, assuming it succeeded

## Vulnerable Pattern
```solidity
function withdraw(uint256 amount) external {
    // .send() returns false on failure but does NOT revert
    msg.sender.send(amount); // Return value ignored
    balances[msg.sender] -= amount; // State updated even if send failed
}

function payout(address to, uint256 amount) external {
    // .call() return value captured but never checked
    (bool success,) = to.call{value: amount}("");
    // success could be false, but execution continues
    totalPaid += amount;
}
```

## Detection Heuristics
1. Search for all `.call(`, `.send(`, `.delegatecall(` invocations
2. For each, check whether the returned boolean is captured AND checked (e.g., `require(success)`, `if (!success) revert`)
3. If the return value is captured but never referenced again, flag it
4. If the return value is not captured at all (e.g., bare `addr.send(amount);`), flag it
5. Check for state changes after unchecked calls — these create inconsistent state on silent failure

## False Positives
- Return value is checked with `require(success)` or equivalent
- The call is intentionally fire-and-forget (documented, no state depends on success) — rare but valid
- Using Solidity's high-level function calls (e.g., `IERC20(token).transfer(...)`) which auto-revert on failure

## Remediation
- Always check the return value: `require(success, "call failed")`
- For ETH transfers to untrusted recipients, consider a pull-payment pattern to avoid DoS if the recipient deliberately reverts
- Prefer high-level Solidity calls when interacting with known interfaces
```solidity
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    (bool success,) = msg.sender.call{value: amount}("");
    require(success, "ETH transfer failed");
}
```

## references/unencrypted-private-data-on-chain.md

# Unencrypted Private Data On-Chain

## Preconditions
- Sensitive data (passwords, secrets, private keys, game answers) is stored in contract storage
- The developer relies on the `private` visibility modifier for confidentiality
- OR: sensitive data is passed as transaction calldata (publicly visible)

## Vulnerable Pattern
```solidity
contract SecretGame {
    // `private` only prevents OTHER CONTRACTS from reading
    // Anyone can read this via eth_getStorageAt(address, slot)
    bytes32 private secretAnswer;
    string private password;

    constructor(bytes32 _answer, string memory _pwd) {
        secretAnswer = _answer; // Visible in deployment tx calldata
        password = _pwd;        // Readable from storage slot
    }

    function guess(bytes32 _guess) external {
        // Attacker reads secretAnswer from storage first
        require(_guess == secretAnswer, "wrong");
        _reward(msg.sender);
    }
}
```

## Detection Heuristics
1. Search for state variables storing passwords, secrets, keys, answers, or seeds — regardless of visibility modifier
2. Check if any `private` variable is relied upon for confidentiality (not just access control)
3. Look for game/lottery logic where hidden information is stored on-chain before a reveal phase
4. Check constructor parameters and transaction calldata for sensitive values — these are publicly visible on block explorers
5. Search for comments like "secret", "hidden", "private key", "password" near storage declarations

## False Positives
- Data is encrypted or hashed before storage (e.g., commitment hash in a commit-reveal scheme)
- The `private` modifier is used correctly for access control between contracts, not for data confidentiality
- The "sensitive" data is actually public information (e.g., a contract address)

## Remediation
- Never store plaintext secrets on-chain — all storage is publicly readable
- Use commit-reveal schemes for hidden inputs: store `keccak256(secret || salt)` first, reveal later
- For truly private data, keep it off-chain and only store hashes/commitments on-chain
- Consider zero-knowledge proofs for verifiable computation on private data
```solidity
// Commit-reveal scheme
mapping(address => bytes32) public commitments;

function commit(bytes32 hash) external {
    // User submits keccak256(answer, salt) — answer stays private
    commitments[msg.sender] = hash;
}

function reveal(bytes32 answer, bytes32 salt) external {
    require(commitments[msg.sender] == keccak256(abi.encodePacked(answer, salt)));
    _processAnswer(msg.sender, answer);
}
```

## references/unexpected-ecrecover-null-address.md

# Unexpected ecrecover Null Address

## Preconditions
- Contract uses `ecrecover` directly (not via OpenZeppelin's ECDSA library)
- The recovered address is not checked against `address(0)`
- The expected signer variable could be uninitialized (defaults to `address(0)`)

## Vulnerable Pattern
```solidity
contract Vault {
    address public signer; // Uninitialized — defaults to address(0)

    function withdrawWithSig(uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
        bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));

        // ecrecover returns address(0) for invalid signatures
        // (e.g., v != 27 && v != 28)
        address recovered = ecrecover(hash, v, r, s);

        // If signer is uninitialized (address(0)) and recovered is address(0),
        // this check passes — anyone can withdraw
        require(recovered == signer, "invalid signature");

        _withdraw(msg.sender, amount);
    }
}
```

## Detection Heuristics
1. Search for `ecrecover(` calls in the codebase
2. Check if the returned address is validated against `address(0)` — flag if not
3. Check the variable that the recovered address is compared against — can it ever be `address(0)`? (uninitialized, never set, cleared by admin)
4. Check if OpenZeppelin's `ECDSA.recover` is used instead — it reverts on null recovery automatically
5. In upgradeable contracts, check if the signer is set during `initialize()` — if `initialize` is never called, signer remains `address(0)`

## False Positives
- `require(recovered != address(0))` check is present after `ecrecover`
- OpenZeppelin's `ECDSA.recover` is used (handles null address internally)
- The expected signer is set in the constructor or initializer and can never be `address(0)` (validated on set)

## Remediation
- Always check `require(recovered != address(0), "invalid signature")` after `ecrecover`
- Use OpenZeppelin's `ECDSA.recover` which reverts on invalid signatures and null recovery
- Validate that signer variables cannot be `address(0)` at any point
```solidity
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

function withdrawWithSig(uint256 amount, bytes memory sig) external {
    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
    bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash);
    address recovered = ECDSA.recover(ethHash, sig); // Reverts if address(0)
    require(recovered == signer, "invalid signature");
    _withdraw(msg.sender, amount);
}
```

## references/uninitialized-storage-pointer.md

# Uninitialized Storage Pointer

## Preconditions
- Solidity version <0.5.0
- Local variables of complex types (structs, arrays) are declared without an explicit `storage` or `memory` data location
- The variable defaults to `storage`, pointing to slot 0

## Vulnerable Pattern
```solidity
// Solidity <0.5.0 only
pragma solidity ^0.4.24;

contract Registry {
    address public owner;      // Stored in slot 0
    uint256 public totalUsers; // Stored in slot 1

    struct User {
        address addr;
        uint256 balance;
    }

    User[] public users;

    function addUser(address _addr, uint256 _balance) external {
        // No data location specified — defaults to storage
        // Points to slot 0 (owner) and slot 1 (totalUsers)
        User u;        // u.addr aliases slot 0 (owner)
        u.addr = _addr;    // Overwrites owner!
        u.balance = _balance; // Overwrites totalUsers!
        users.push(u);
    }
}
```

## Detection Heuristics
1. Check the Solidity version: if >=0.5.0, this vulnerability is impossible (compiler requires explicit data location)
2. For <0.5.0: search for local struct or array variable declarations without `storage` or `memory` keyword
3. If a local variable of a complex type has no data location, it defaults to `storage` at slot 0 — writes to it overwrite the first state variables
4. Check which state variables occupy slots 0, 1, 2, etc. — these are the ones at risk of overwrite
5. Look for struct field assignments on local variables that could alias storage

## False Positives
- Solidity >=0.5.0 (compiler enforces explicit data location — this can't happen)
- The variable is explicitly declared as `memory` (e.g., `User memory u`)
- The variable is explicitly declared as `storage` and intentionally points to a known storage location

## Remediation
- Upgrade to Solidity >=0.5.0 where explicit data locations are compiler-enforced
- For legacy contracts, add explicit `memory` or `storage` to all local complex-type declarations
```solidity
function addUser(address _addr, uint256 _balance) external {
    User memory u;         // Explicit memory — no storage aliasing
    u.addr = _addr;
    u.balance = _balance;
    users.push(u);
}
```

## references/unsafe-low-level-call.md

# Unsafe Low-Level Call

## Preconditions
- Contract uses `.call()`, `.delegatecall()`, `.staticcall()`, or `.send()` for external interactions
- The return value is not checked, OR
- The target address may not have deployed code (user-provided, destroyed, or never deployed)

## Vulnerable Pattern
```solidity
function payout(address to, uint256 amount) external {
    // Unchecked return value — silent failure
    to.call{value: amount}("");
    totalPaid += amount; // Updated even if call failed
}

function interact(address target, bytes calldata data) external {
    // Call to non-existent contract "succeeds" silently
    // EVM treats call to codeless address as successful
    (bool success,) = target.call(data);
    require(success); // Passes even if target has no code!
    _markComplete();
}
```

## Detection Heuristics
1. Search for `.call(`, `.send(`, `.delegatecall(`, `.staticcall(` in the codebase
2. For each, check if the returned `bool` is captured AND checked (e.g., `require(success)`)
3. If the return value is not captured or not checked, flag it — execution continues after failure
4. For calls to user-supplied addresses, check if `target.code.length > 0` is verified before the call — the EVM silently succeeds on calls to addresses with no code
5. Note: `address.code.length` check can be bypassed during constructor execution (code size is 0)
6. Check if state changes after the call assume it succeeded

## False Positives
- Return value is properly checked with `require(success)`
- High-level Solidity calls are used (e.g., `IERC20(token).transfer(...)`) which include automatic `extcodesize` checks and revert on failure
- The call is intentionally fire-and-forget with no state depending on success (rare, must be documented)

## Remediation
- Always check return values: `require(success, "call failed")`
- Verify target has code before low-level calls: `require(target.code.length > 0)`
- Prefer high-level Solidity calls for known interfaces — they include automatic code existence checks
- For critical integrations, combine both checks
```solidity
function payout(address to, uint256 amount) external {
    require(to.code.length > 0 || to == tx.origin, "no code at target");
    (bool success,) = to.call{value: amount}("");
    require(success, "transfer failed");
    totalPaid += amount;
}
```

## references/unsecure-signatures.md

# Unsecure Signatures

## Preconditions
- Contract uses ECDSA signatures for authorization, authentication, or message verification
- One or more of the following sub-vulnerabilities are present:
  - Signature malleability (tracking by raw bytes)
  - Missing replay protection (no nonce, chainId, or contract address)
  - Unchecked ecrecover null address return
  - Hash collisions from `abi.encodePacked` with multiple dynamic types
  - No EIP-712 structured data signing

## Vulnerable Pattern
```solidity
// Combines multiple signature anti-patterns
function execute(bytes memory sig, address to, uint256 amount) external {
    // 1. No nonce — replay attack
    // 2. No address(this) — cross-contract replay
    // 3. No block.chainid — cross-chain replay
    bytes32 hash = keccak256(abi.encodePacked(to, amount));

    // 4. Raw ecrecover — no null address check
    address recovered = ecrecover(hash, v, r, s);
    // 5. No s-value malleability check
    require(recovered == signer);

    // 6. Signature tracked by bytes — malleable bypass
    require(!used[sig]);
    used[sig] = true;

    _transfer(to, amount);
}
```

## Detection Heuristics
1. Search for `ecrecover` or `ECDSA.recover` — this indicates signature usage
2. Check each sub-vulnerability in order:
   - **Malleability**: is deduplication done by raw signature bytes? Flag if yes
   - **Replay**: does the signed hash include nonce + `address(this)` + `block.chainid`? Flag any missing
   - **Null address**: is the recovered address checked against `address(0)`? Flag if not
   - **Hash collision**: is `abi.encodePacked` used with multiple dynamic types? Flag if yes
   - **EIP-712**: is structured typed data signing used? Flag if not (lower severity)
3. Check if OpenZeppelin's ECDSA library is used — it handles malleability and null address automatically
4. Check for front-running risk: can a signed message be observed in the mempool and submitted by someone else?

## False Positives
- OpenZeppelin's ECDSA library is used with EIP-712 domain separator, nonce tracking, and proper hash construction — all sub-vulnerabilities are addressed
- EIP-2612 permit pattern is used correctly (includes nonce, deadline, domain separator)
- The contract is a simple forwarder where signature security is handled by a downstream contract

## Remediation
- Use OpenZeppelin's ECDSA library for signature recovery (handles malleability + null address)
- Implement EIP-712 structured data signing with domain separator (covers chainId + contract address)
- Track nonces per signer to prevent replay
- Use `abi.encode` instead of `abi.encodePacked` for hash construction
```solidity
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract SecureSig is EIP712("SecureSig", "1") {
    mapping(address => uint256) public nonces;

    bytes32 constant EXECUTE_TYPEHASH =
        keccak256("Execute(address to,uint256 amount,uint256 nonce)");

    function execute(address to, uint256 amount, bytes memory sig) external {
        uint256 nonce = nonces[msg.sender]++;
        bytes32 structHash = keccak256(abi.encode(EXECUTE_TYPEHASH, to, amount, nonce));
        bytes32 hash = _hashTypedDataV4(structHash);
        address recovered = ECDSA.recover(hash, sig);
        require(recovered == signer, "invalid sig");
        _transfer(to, amount);
    }
}
```

## references/unsupported-opcodes.md

# Unsupported Opcodes on EVM-Compatible Chains

## Preconditions
- Contract is intended for deployment on an EVM-compatible chain other than Ethereum mainnet (zkSync Era, Arbitrum, Optimism, Polygon, BNB Chain, etc.)
- The contract uses opcodes or patterns that are not supported or behave differently on the target chain

## Vulnerable Pattern
```solidity
// PUSH0 opcode: Solidity >=0.8.20 emits PUSH0
// Not supported on all chains — contract fails to deploy
pragma solidity 0.8.20;

contract Token {
    // Compiled bytecode contains PUSH0 — reverts on chains without support
}

// .transfer() on zkSync Era — 2300 gas stipend insufficient
function withdraw() external {
    // transfer() forwards only 2300 gas
    // On zkSync Era, basic operations cost more — this always reverts
    // Gemholic lost 921 ETH to this exact issue
    payable(msg.sender).transfer(amount);
}

// Dynamic create on zkSync Era
function deploy(bytes memory bytecode) external {
    assembly {
        // zkSync requires bytecode known at compile time
        // Runtime create from arbitrary bytecode fails
        let addr := create(0, add(bytecode, 0x20), mload(bytecode))
    }
}
```

## Detection Heuristics
1. Check the Solidity version: if >=0.8.20, verify the target chain supports the PUSH0 opcode
2. Search for `.transfer()` and `.send()` — these use a 2300 gas stipend that may be insufficient on chains with different gas cost structures (especially zkSync Era)
3. Search for assembly `create` / `create2` with runtime-supplied bytecode — this fails on zkSync Era
4. Search for `selfdestruct` — deprecated and non-functional on some chains post-Dencun
5. If the project targets multiple chains, cross-reference all opcodes used against each target chain's compatibility (use evmdiff.com)

## False Positives
- Contract is only deployed on Ethereum mainnet
- Solidity version is <0.8.20 (no PUSH0 emitted)
- `.call{value: amount}("")` is used instead of `.transfer()` (forwards all available gas)
- The project explicitly documents which chains are supported and tests against them

## Remediation
- Use `.call{value: amount}("")` instead of `.transfer()` or `.send()` for ETH transfers
- For multi-chain deployments, compile with Solidity <0.8.20 or use `--evm-version paris` to avoid PUSH0
- On zkSync Era, use compile-time known bytecode for contract creation
- Test deployments on each target chain before mainnet launch
- Use evmdiff.com to verify opcode compatibility per chain
```solidity
// Safe ETH transfer for all EVM chains
function withdraw(uint256 amount) external {
    (bool success,) = msg.sender.call{value: amount}("");
    require(success, "transfer failed");
}

// Avoid PUSH0: compile with Paris EVM target
// solc --evm-version paris
```

## references/unused-variables.md

# Presence of Unused Variables

## Preconditions
- Contract declares state variables, local variables, function parameters, or imports that are never referenced
- OR: return values from function calls are silently discarded

## Vulnerable Pattern
```solidity
contract Vault {
    uint256 public totalDeposits;
    uint256 public unusedCounter; // Declared but never read or written

    function deposit(uint256 amount, bytes memory data) external {
        // `data` parameter never used — possible missing validation
        totalDeposits += amount;
    }

    function process() external {
        // Return value from transfer silently discarded
        // This may indicate missing success check
        IERC20(token).transfer(recipient, amount);

        uint256 result = _calculate();
        // `result` computed but never used — missing logic?
    }
}
```

## Detection Heuristics
1. Search for state variables that are never referenced in any function (only declared)
2. Search for function parameters that are never used in the function body
3. Search for local variables that are assigned but never read
4. Check for return values from external calls that are not captured or checked
5. For each unused variable, determine: is it dead code (safe to remove) or does it indicate missing logic (a bug)?
6. Check compiler warnings for unused variable alerts

## False Positives
- The variable is part of an interface implementation and must be declared for signature compatibility even if unused
- The variable is used in a commented-out or conditional compilation path
- The variable is reserved for future use and documented as such
- Function parameters prefixed with `_` to explicitly mark as unused (e.g., `function hook(uint256 /* _amount */)`)

## Remediation
- For dead code: remove the unused variable entirely
- For missing logic: implement the intended use (e.g., check the return value, use the parameter for validation)
- For interface-required but unused parameters: use the unnamed parameter syntax
```solidity
// Remove dead state variables
// uint256 public unusedCounter; — DELETE

// Unnamed parameters for interface compliance
function onERC721Received(
    address,     // operator — unused
    address,     // from — unused
    uint256,     // tokenId — unused
    bytes memory  // data — unused
) external pure returns (bytes4) {
    return this.onERC721Received.selector;
}

// Check return values — use SafeERC20 for non-compliant tokens
bool success = IERC20(token).transfer(recipient, amount);
require(success);
```

## references/use-of-deprecated-functions.md

# Use of Deprecated Functions

## Preconditions
- Contract uses Solidity functions, keywords, or language features that have been deprecated or removed
- The deprecated feature may behave differently than expected or may not compile on newer Solidity versions

## Vulnerable Pattern
```solidity
pragma solidity ^0.4.24;

contract Legacy {
    function destroy() external {
        // suicide is deprecated — renamed to selfdestruct
        // selfdestruct itself is now deprecated post-Dencun
        suicide(msg.sender);
    }

    function getHash(bytes memory data) external view returns (bytes32) {
        return sha3(data);  // Deprecated — use keccak256
    }

    function getBlockHash(uint256 n) external view returns (bytes32) {
        return block.blockhash(n);  // Deprecated — use blockhash(n)
    }

    function getRemainingGas() external view returns (uint256) {
        return msg.gas;  // Deprecated — use gasleft()
    }
}
```

| Deprecated | Replacement |
|---|---|
| `suicide(address)` | `selfdestruct(address)` (also deprecated) |
| `block.blockhash(uint)` | `blockhash(uint)` |
| `sha3(...)` | `keccak256(...)` |
| `callcode(...)` | `delegatecall(...)` |
| `throw` | `revert()` |
| `msg.gas` | `gasleft()` |
| `constant` (function modifier) | `view` |
| `var` | Explicit type name |

## Detection Heuristics
1. Search for each deprecated keyword: `suicide`, `sha3`, `block.blockhash`, `callcode`, `throw`, `msg.gas`, `constant` (as function modifier), `var`
2. Search for `selfdestruct` — while it's the replacement for `suicide`, it is itself deprecated post-Dencun and non-functional on some chains
3. Check compiler warnings for deprecation notices
4. Flag any usage and recommend the modern replacement

## False Positives
- The deprecated function appears in comments or documentation, not in executable code
- The contract is intentionally targeting an old Solidity version where the deprecated feature is still standard
- Interface definitions that reference deprecated patterns for backward compatibility

## Remediation
- Replace each deprecated function with its modern equivalent (see table above)
- For `selfdestruct`: remove reliance entirely, as it is deprecated and non-functional on some chains post-Dencun
- Upgrade the Solidity version to benefit from compiler enforcement of modern syntax
```solidity
// Modern equivalents
pragma solidity 0.8.24;

contract Modern {
    function getHash(bytes memory data) external pure returns (bytes32) {
        return keccak256(data);
    }

    function getBlockHash(uint256 n) external view returns (bytes32) {
        return blockhash(n);
    }

    function getRemainingGas() external view returns (uint256) {
        return gasleft();
    }
}
```

## references/weak-sources-randomness.md

# Weak Sources of Randomness from Chain Attributes

## Preconditions
- Contract generates "random" values using on-chain data: `block.timestamp`, `blockhash`, `block.difficulty` / `block.prevrandao`, `block.number`, or combinations thereof
- The random value determines outcomes with economic value (lotteries, games, minting, distributions)

## Vulnerable Pattern
```solidity
function drawLottery() external {
    // All inputs are deterministic and publicly visible
    // Another contract can compute the same value in the same tx
    uint256 random = uint256(keccak256(abi.encodePacked(
        block.timestamp,
        block.prevrandao,
        msg.sender
    ))) % 100;

    if (random < 5) {
        _payWinner(msg.sender);
    }
}

// Attacker contract
contract Exploit {
    function attack(Lottery target) external {
        // Compute the same "random" value before calling
        uint256 random = uint256(keccak256(abi.encodePacked(
            block.timestamp,
            block.prevrandao,
            address(this)
        ))) % 100;

        // Only call if we'll win
        if (random < 5) {
            target.drawLottery();
        }
    }
}
```

## Detection Heuristics
1. Search for `block.timestamp`, `block.prevrandao`, `block.difficulty`, `blockhash`, `block.number` used as inputs to `keccak256` or arithmetic operations producing a "random" value
2. Check if the resulting value determines an outcome with economic impact (winner selection, token distribution, NFT rarity, game outcome)
3. If randomness is derived exclusively from on-chain data, flag it — a contract in the same transaction can compute the identical value
4. Check if `blockhash` is used for a future block (returns 0 for blocks not yet mined) or a block older than 256 blocks (also returns 0)
5. Check if validators/miners can influence the inputs to bias the outcome

## False Positives
- Chainlink VRF or another verifiable randomness oracle is used
- On-chain data is combined with an off-chain commit-reveal scheme (the on-chain part alone doesn't determine the outcome)
- The randomness doesn't determine anything with economic value
- `block.prevrandao` on PoS Ethereum provides sufficient entropy for the specific use case (not for high-value outcomes)

## Remediation
- Use Chainlink VRF (Verifiable Random Function) for provably fair randomness
- Implement a commit-reveal scheme: users commit hashed choices, reveal in a later block
- Never use `block.timestamp`, `blockhash`, or `block.prevrandao` alone for randomness in high-value contexts
```solidity
import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

contract FairLottery is VRFConsumerBaseV2 {
    function requestRandom() external {
        requestRandomWords(keyHash, subId, confirmations, gasLimit, 1);
    }

    function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
        uint256 winner = randomWords[0] % participants.length;
        _payWinner(participants[winner]);
    }
}
```

