# drozer-lite

General-purpose pattern-level smart contract vulnerability scanner with cross-file awareness. Walks any smart contract project (Solidity, Rust/Anchor/CosmWasm/IC, Move, Cairo, Vyper — single file or multi-file), builds an inventory, clusters related modules, applies a curated checklist of 180+ vulnerability patterns derived from real benchmark gap analysis across 13+ protocol-type profiles, and returns structured findings. USE WHEN the user asks to scan, audit, or review smart contract source for security bugs and wants pattern-level coverage. Designed for protocols up to ~500KB / 100 files. Wall-clock 5-30 min depending on size. Does NOT do multi-step actor reasoning, chain analysis, or formal verification — for that, use the full drozer pipeline (`/droz3r`).

- **Kind:** skill
- **Source:** https://github.com/gdroz3r/drozer-lite
- **Page:** https://forefy.com/skills/645bb0f6-f341-4da0-9659-a2069c773431
- **API (JSON + files):** https://forefy.com/api/asr/645bb0f6-f341-4da0-9659-a2069c773431

---

## .claude

```

```

## .claude/skills

```

```

## .claude/skills/drozer-lite

```

```

## .claude/skills/drozer-lite/SKILL.md

---
name: drozer-lite
description: General-purpose pattern-level smart contract vulnerability scanner with cross-file awareness. Walks any smart contract project (Solidity, Rust/Anchor/CosmWasm/IC, Move, Cairo, Vyper — single file or multi-file), builds an inventory, clusters related modules, applies a curated checklist of 180+ vulnerability patterns derived from real benchmark gap analysis across 13+ protocol-type profiles, and returns structured findings. USE WHEN the user asks to scan, audit, or review smart contract source for security bugs and wants pattern-level coverage. Designed for protocols up to ~500KB / 100 files. Wall-clock 5-30 min depending on size. Does NOT do multi-step actor reasoning, chain analysis, or formal verification — for that, use the full drozer pipeline (`/droz3r`).
---

# drozer-lite — open-source pattern-level smart contract auditor (v0.5.7, broadened CEI discriminator + schema-mismatch LOW drop + shared-check consolidation)

You are about to run a multi-file pattern-level smart contract audit using drozer-lite's curated checklist. Follow this 8-step workflow exactly. Do not invent steps, do not paraphrase the checklist, do not invent findings.

drozer-lite is the open-source pattern-level slice of the main Drozer-v2 auditor. Every check in the bundled checklists traces to a real audit finding that was missed in past benchmark runs. The provenance is cited inside each check.

drozer-lite is intentionally narrow:

- **Pattern-level checks** drawn from a curated checklist — ~88 checks are language-agnostic, ~10 are Solidity-specific
- **Multi-language** — Solidity, Rust (Anchor, CosmWasm, IC canisters), Move (Aptos, Sui, Initia), Cairo (StarkNet), Vyper
- **Cross-file aware** (catches bugs spanning multiple contracts/modules)
- Does NOT do multi-step actor reasoning, chain composition analysis, or formal verification — that's `/droz3r` territory

The trade-off is on purpose: drozer-lite finds the bugs pattern matching CAN find, fast and reproducibly, without pretending to be a full audit pipeline.

---

## Step 1 — Identify the target and detect language

Determine which file(s) the user wants audited.

- If the user pasted source inline, treat it as a one-file target. Detect language from syntax.
- If the user referenced a path, walk it.

### Language detection (auto, from file extensions)

| Extension(s) | Language | Glob pattern | Also filter out |
|---|---|---|---|
| `.sol` | Solidity | `**/*.sol` | `node_modules`, `lib`, `forge-std`, `.forge` |
| `.rs` | Rust (Anchor / CosmWasm / IC) | `**/*.rs` | `target/`, `.cargo/`, `test_*.rs`, `*_test.rs` |
| `.move` | Move (Aptos / Sui / Initia) | `**/*.move` | `build/`, `.aptos/`, `tests/` |
| `.cairo` | Cairo (StarkNet) | `**/*.cairo` | `target/`, `tests/` |
| `.vy` | Vyper | `**/*.vy` | `tests/` |

Always filter out: `test`, `tests`, `mock`, `mocks`, `script`, `scripts`, `out`, `cache`, `.git`, `coverage`, `broadcast`, `node_modules`.

If a project has mixed languages (e.g. `.sol` + `.rs`), detect the PRIMARY language by file count / byte weight and note the secondary. Load profiles for the primary language; if a secondary language has significant source (>20% by bytes), load its profiles too.

- **Soft warning**: if total source > 500KB, tell the user "this will take ~30+ minutes" and continue.
- **Hard refusal**: if total source > 1MB, REFUSE. Recommend `/droz3r` (the full drozer pipeline).

If you cannot find any source, ask the user to specify a path or paste source. Do not guess.

### Language determines what "function", "modifier", "state variable" mean in Steps 2-6

| Concept | Solidity | Rust (Anchor/CosmWasm) | Move | Cairo |
|---|---|---|---|---|
| Public function | `external`/`public` | `pub fn`, `#[msg(execute)]`, `#[instruction]` | `public entry fun`, `public fun` | `#[external(v0)]`, `fn` in impl |
| Access control | `onlyOwner`, `onlyRole(R)` modifier | `require!(ctx.accounts.authority == ...)`, `#[access_control]` | `assert!(signer::address_of(s) == @admin)` | `assert(caller == owner)` |
| State variable | contract-level storage | `Account<'info, T>`, `#[account]` struct fields | `borrow_global<T>`, resource struct fields | `@storage_var` |
| External call | `.call`, `interface(addr).fn()` | CPI (`invoke`, `invoke_signed`), `CosmosMsg` | `coin::transfer`, module call | syscall, contract call |
| Reentrancy guard | `nonReentrant`, `ReentrancyGuard` | manual flag, `#[non_reentrant]` in some frameworks | N/A (Move is not reentrant by design) | N/A (Cairo is not reentrant by design) |
| Import/dependency | `import`, `using...for` | `use`, `mod`, Cargo.toml deps | `use`, `friend` | `use`, imports |

When applying checks from `.claude/skills/drozer-lite/checklists/universal.md`, **translate the Solidity-phrased red flags to the target language's equivalent**. The METHODOLOGY is language-agnostic; only the SYNTAX differs. For example:
- UNI-1 says "Missing `onlyOwner`/`onlyRole(...)` on state-changing function" → in Rust, check for missing `require!(authority == ...)` or `#[access_control(...)]`
- UNI-3 says "Balance/ownership update AFTER `.call` or token transfer" → in Rust, check for CPI invocations before account state updates

---

## Step 2 — Build the inventory (cheap structural pass)

Read each in-scope file with the Read tool. Do NOT analyze code yet — only extract structure. Build an in-context inventory map covering:

For each file:
- **Path** and approximate byte size + line count
- **Modules / contracts / programs** declared (and what they extend / implement)
- **Public / external function signatures** (name + params + visibility + guards, no body). Use the language's convention from the Step 1 table.
- **State variables / account structs / storage vars** (name + type + visibility)
- **Access control mechanisms** (modifiers, assert-based guards, access_control attributes)
- **External calls visible**: cross-contract calls, CPI, module calls, system calls, delegate calls
- **Imports / dependencies** (which other in-scope files this file depends on)

Format the inventory like this (keep it terse — this is your context_map for cross-file detection):

Format: `File.sol (XKB, YL) → Contracts: Name → Parent; External fns: ...; State: ...; External calls: ...; Imports: ...`

Use real file/contract names from the source. Keep it terse — this is your context_map for cross-file detection in Step 5.

---

## Step 3 — Detect profiles (with always-load fallback)

Apply the keyword detection table below to the WHOLE inventory (not file-by-file). Detection is **case-insensitive**. A profile is **auto-loaded** if it scores **3 or more distinct keyword matches** across the inventory.

**ALWAYS LOAD**: `universal` (regardless of detection)

**Auto-detect** (per-profile threshold = 3 distinct keyword matches). Keywords below are **common-pattern names** and regex fragments that appear across the entire Solidity ecosystem — they must never encode benchmark-specific identifiers. When applying detection, treat each row as a set of case-insensitive regex patterns. Some patterns use `\w*` deliberately to tolerate real-world naming (prefixed function names like `depositFoo`, suffixed variants like `previewDepositVault`, protocol-specific wrappers like `swapXForY`). A keyword match should accept the pattern followed by typical word characters.

| Profile | Keywords and regex fragments (case-insensitive) |
|---|---|
| signature   | `EIP712`, `permit\s*\(`, `ecrecover\s*\(`, `isValidSignature`, `_hashTypedDataV4`, `DOMAIN_SEPARATOR`, `permitWitnessTransferFrom`, `_signTypedData`, `Permit2`, `IERC1271`, `\w*Permit\w*\s*\(`, `SignatureLib`, `recoverSigner`, `signedHash` |
| vault       | `ERC4626`, `totalAssets`, `previewDeposit\w*`, `previewWithdraw\w*`, `previewRedeem\w*`, `convertToShares`, `convertToAssets`, `function\s+\w*[Dd]eposit\w*\s*\(`, `function\s+\w*[Ww]ithdraw\w*\s*\(`, `function\s+\w*[Rr]edeem\w*\s*\(`, `IERC4626`, `sharesOf`, `maxDeposit`, `\w*Vault\w*`, `lvToken`, `vToken` |
| lending     | `\w*[Bb]orrow\w*`, `\w*[Ll]iquidate\w*`, `collateral`, `healthFactor`, `\bLTV\b`, `debtToken`, `interestRate`, `\w*[Rr]epay\w*`, `function\s+\w*[Ss]upply\w*\s*\(`, `IPool`, `ILendingPool`, `_borrow`, `cToken`, `underlying` |
| dex         | `\w*[Ss]wap\w*\s*\(`, `addLiquidity\w*`, `removeLiquidity\w*`, `amountOutMin`, `amountInMax`, `\bUniswapV[234]\b`, `IUniswapV[234]`, `ISwapRouter`, `sqrtPriceX96`, `getAmountsOut`, `getAmountOut`, `\bRouter0[1-4]\b`, `createPair\s*\(`, `MinimalUniswapV2Library`, `_pair` |
| cross-chain | `lzReceive`, `ccipReceive`, `setPeer`, `setTrustedRemote`, `wormhole`, `IReceiver`, `\w*[Bb]ridge\w*\s*\(`, `ILayerZero`, `NonblockingLzApp`, `_nonblockingLzReceive`, `IRouterClient`, `crossChain\w*`, `CCIPSender`, `HyperlaneRouter` |
| governance  | `propose\s*\(`, `castVote\w*\s*\(`, `quorum`, `delegate\w*\s*\(`, `Governor`, `Timelock`, `votingPower`, `IGovernor`, `_execute\s*\(`, `getVotes`, `IVotes`, `proposalThreshold`, `voteStart`, `voteEnd` |
| reentrancy  | `nonReentrant`, `ReentrancyGuard`, `\w*[Rr]eentranc\w*`, `\.call\s*\{\s*value`, `\.call\s*\(`, `onERC721Received`, `onERC1155Received`, `tokensReceived`, `ERC777`, `IERC777Recipient`, `_checkOnERC721Received`, `IERC1155Receiver`, `\bMutex\w*`, `NoReentrant`, `\bLock\w*\.acquire`, `locked\s*=\s*true`, `_status\s*=\s*1` |
| oracle      | `AggregatorV[23]Interface`, `latestRoundData`, `latestAnswer`, `IChainlink`, `priceFeed`, `\w*[Gg]etPrice\w*`, `\boracle\w*`, `\w*Oracle\w*`, `IPyth`, `IOracle\w*`, `oracleAdapter`, `IOracleAdapter`, `setOracle\w*`, `_oracle`, `\boracles/`, `IRates`, `exchangeRate\s*\(` |
| math        | `FixedPoint`, `PRBMath`, `mulDiv`, `SafeMath`, `\bWAD\b`, `\bRAY\b`, `UFixed\w*`, `SD\d+x\d+`, `UD\d+x\d+`, `abdk`, `FullMath`, `MathUpgradeable`, `MathHelper`, `UQ\d+x\d+`, `\bsqrt\s*\(` |
| gaming      | `VRFConsumerBase`, `VRFCoordinator`, `randomness`, `\w*[Rr]affle\w*`, `\w*[Ll]ottery\w*`, `requestRandomWords`, `fulfillRandomWords`, `ChainlinkVRF`, `VRFV2`, `IVRFCoordinator`, `commitReveal`, `randaoMix` |
| stableswap  | `StableSwap`, `\bamp\b`, `amplification`, `compute_d`, `compute_y`, `\bnewton\b`, `invariant.*D`, `stableswap_y`, `n_coins.*ann`, `D_prod`, `amp_factor` |

### Language-specific profiles (auto-load by detected language)

When the detected language is NOT Solidity, auto-load the corresponding language profile:

| Detected language | Auto-load profile | Condition |
|---|---|---|
| Rust + Anchor patterns (`declare_id!`, `#[program]`, `#[account]`) | `solana` | Score ≥ 2 Anchor keywords |
| Rust + IC patterns (`ic_cdk`, `#[update]`, `#[query]`, `candid`) | `icp` | Score ≥ 2 IC keywords |
| Rust + CosmWasm patterns (`cosmwasm_std`, `#[entry_point]`, `ExecuteMsg`) | load `universal` only (no dedicated CosmWasm profile yet) | — |
| Move | load `universal` only (no dedicated Move profile yet) | — |
| Cairo | load `universal` only (no dedicated Cairo profile yet) | — |
| Vyper | load `universal` + any Solidity profiles that fire (Vyper shares EVM patterns) | — |

`icp` and `solana` profiles are NO LONGER explicit-only. They auto-load when the language detection identifies Anchor or IC canister Rust code. They can still be forced via `--profile` when auto-detection misses.

**Hard rule**: do not lower the threshold to "make a profile fire" because you intuit it might be relevant. If a profile genuinely does not clear the threshold, do not load its checklist. Pattern coverage IS the contract.

**Document the selection**. Output a "Profile Selection" block the user can read, listing every profile and its match count:

```
PROFILE SELECTION (synthetic example):
  universal     → ALWAYS LOADED
  reentrancy    → AUTO-LOADED (4 matches: nonReentrant, ReentrancyGuard, .call{value, onERC721Received)
  oracle        → AUTO-LOADED (3 matches: priceFeed, IOracle, exchangeRate())
  vault         → not loaded (1 match: ERC4626)
  lending       → not loaded (0 matches)
  signature     → not loaded (1 match: permit()-shaped library call)
  ...
```

If the user disagrees, they can re-invoke with `--profile <name>` to force-load.

---

## Step 4 — Cluster the codebase

Group files into clusters of 1-5 files each. Target **30-50KB per cluster**. Use these rules in order:

1. **Inheritance chain** — files containing parent + child contracts go in the same cluster.
2. **Mutual import** — file A imports B, file B imports A → same cluster.
3. **Subdirectory + shared imports** — files in the same `oracles/`, `validators/`, `governance/` subdirectory with overlapping imports → same cluster.
4. **Single oversized file** — a file > 30KB becomes its own cluster. If it's > 60KB, you'll need to analyze it in two passes (top half / bottom half) and merge findings.
5. **Soft cap**: if a cluster exceeds 60KB, split it along the weakest dependency edge.

Output the cluster plan in a "CLUSTER PLAN" block:

```
CLUSTER PLAN (synthetic example):
  Cluster 1 — PrimaryCluster (54KB, 1.4K lines)
    Files: Token.sol, Service.sol, Accountant.sol
    Dependencies: Token imports IPauseRegistry; Service imports Token + IAccountant; Accountant imports IRegistry
  Cluster 2 — ControlCluster (24KB, 643 lines)
    Files: Registry.sol, PauseRegistry.sol
    Dependencies: Registry imports IPauseRegistry + IService
  Cluster 3 — OracleCluster (21KB, 542 lines)
    Files: OracleAggregator.sol, adapters/Adapter.sol, adapters/SourceImpl.sol, adapters/IAdapter.sol
    Dependencies: OracleAggregator imports IAdapter; Adapter imports IAdapter
```

---

## Step 5 — Per-cluster analysis

For each cluster:

1. **Read the cluster's source — EVERY LINE, NO EXCEPTIONS.** Read each file in the cluster fully. If a file exceeds ~40KB (~500 lines), read it in sequential chunks using offset+limit (e.g. offset=0 limit=500, then offset=500 limit=500, etc.) until the entire file is read. Do NOT skip, sample, or "read the important parts." Every line of in-scope source must be read. Partial source reading is the #1 cause of missed findings — a 25% read produces 25% recall. This is non-negotiable.
2. **Read the relevant checklists** — Read `.claude/skills/drozer-lite/checklists/universal.md` always, plus each auto-loaded profile checklist (`.claude/skills/drozer-lite/checklists/{profile}.md`). These paths are relative to the project root (current working directory) where the skill is invoked.
3. **Reference the inventory from Step 2** — for cross-cluster bug detection. When the cluster you're analyzing calls a function in another cluster, look up the target's signature in the inventory; you don't need to re-read the other cluster's full source.
4. **Apply each loaded check** — for each check in the loaded checklists, examine the cluster source. **If the target language is not Solidity, translate the check's Solidity-phrased red flags to the equivalent in the target language** using the concept-mapping table from Step 1. The METHODOLOGY is language-agnostic; only the SYNTAX differs. A check matches when ALL of:
   - The **Pattern** field describes a code construct that exists in the cluster source (in the target language's idiom)
   - The **Red flags** (or their language-translated equivalents) are visible in the source
   - The **Methodology** describes a reachable exploit path you can trace line by line
4a. **Textbook-pattern specific-break requirement** (new in v0.5.1). For canonical well-known patterns — **reentrancy / CEI**, **signature replay**, **reward-debt / MasterChef-style accumulator**, **multisig stale-approval after owner removal**, **ERC4626 first-depositor inflation**, **flash loan oracle manipulation**, **approve-then-transferFrom race** — pattern presence is NOT sufficient. The finding MUST identify the **specific line** in the current code that deviates from the textbook safe version. Competent authors handle these patterns correctly most of the time; emitting on pattern presence without a concrete break is the top precision failure on complex clean contracts.

   - Good: *"Line 65 calls `msg.sender.call{value: excess}('')` BEFORE line 68 updates `tokensSold`. Textbook safe version updates state before the external call."*
   - Bad: *"The reward-debt pattern is present; a user acquiring LP after fees accrue can claim retroactively."* (class description, not a code-level break)
   - Bad: *"removeOwner does not clear approvals — classic multisig bug."* (class description; requires you to actually show that the approvals are counted AFTER removal in the current execute() path, with specific lines)

5. **When unsure, do NOT report.** This is a hard rule, not a preference. False positives are worse than misses.

   (a) **Hedging by hypothetical future state** — banned. Do NOT report any finding whose body is "what if an admin whitelists a malicious token…", "if a future upgrade adds…", "if ERC777 is ever added…", "if the oracle returns zero…" unless the codebase ALREADY contains evidence that the hypothetical condition holds (e.g., ERC777 is actually in scope, the oracle is actually unvalidated in the read path).

   (b) **Hedging by admin cooperation** — banned (new in v0.5.1). Do NOT report any finding whose exploit sentence requires the admin/owner/trusted actor to deliberately misconfigure parameters, set addresses to zero, or cooperate with the attacker. Examples that must be suppressed: "admin can set `sanctionsList` to zero and disable sanctions", "admin can call `setFee(10000)` and drain", "arbitrator can transfer role to attacker". These are centralization concerns, not exploits. Move them to `warnings[]` as `"centralization: <title>"` strings if the user explicitly invoked with `--include-centralization`. Never emit them in `findings[]` by default.

   Speculative hedging findings of either type are the #1 source of false positives and MUST be suppressed at the source, not at Step 7.
6. **For each match**, identify:
   - The affected function name and the file it lives in (full path within the project)
   - A specific line as the most representative location for `line_hint`
   - A severity from CRITICAL/HIGH/MEDIUM/LOW/INFO using the matrix below
   - A confidence from HIGH/MEDIUM/LOW based on how clearly the source matches the check
   - The check ID that fired (e.g. `UNI-1`, `RE-2`, `ORC-3`) — record this internally
7. **Add cluster metadata** to each finding: `cluster: "<cluster name>"`. The dedup pass will use this.
8. **Do not output yet** — accumulate findings in your working set. Output is at Step 7.

You may analyze clusters sequentially (recommended for token budget). Each cluster gets its own independent reasoning pass — but ALL clusters share the same checklist context that you loaded once at the start.

### Severity decision table

Pick severity by walking this table top-to-bottom and stopping at the first row that matches. Do NOT pick severity by "feel" — the table is the contract. Aligned with industry convention (Code4rena / Immunefi / SWC Registry).

| If the finding is… | …and the attacker is… | …and the impact is… | Severity |
|---|---|---|---|
| Direct drain / unauthorized mint / arbitrary-state-write | **permissionless** (anyone) | protocol-wide funds at risk, no preconditions | **CRITICAL** |
| Signature replay on a token-moving or authorization function | permissionless | repeated fund transfer until balance/allowance exhausted | **CRITICAL** |
| CEI violation / reentrancy that drains a pool | permissionless | full pool drain in a single tx | **CRITICAL** |
| Missing access control on a function that **sets an economic parameter** (rate, fee, price, reward, threshold) | permissionless | protocol-wide economic manipulation, indirect fund loss | **HIGH** |
| Missing access control on a function that sets **per-user** state | permissionless | a single user's state is corrupted | **MEDIUM** |
| CEI violation / reentrancy with a cap on damage (per-user, per-epoch) | permissionless | bounded fund loss | **HIGH** |
| Signature replay on a non-fund-moving function | permissionless | unbounded action replay, no direct fund loss | **MEDIUM** |
| Missing input validation that allows a permissionless caller to set a parameter to an out-of-spec value causing fund loss (e.g. `discountBps > 10000`, `fee > 100%`) | permissionless OR creator of a permissionless market | direct fund loss once the bad value is set | **HIGH** |
| Missing input validation with no direct fund loss | permissionless | griefing, DoS, or broken-state | **MEDIUM** |
| Racing a legitimate caller for funds (front-run withdrawal vs release) | permissionless | race winner takes funds that were rightfully the loser's | **MEDIUM** |
| Division by zero that bricks a critical function | permissionless | permanent DoS of stake/redeem/withdraw | **HIGH** |
| Division by zero in a view-only or non-critical path | permissionless | view call reverts, no state impact | **LOW** |
| Unchecked return value of a known-standard token (SafeERC20 not used) AND the token whitelist includes a specific non-standard token (USDT, BNB, etc.) | permissionless | silent accounting drift | **MEDIUM** |
| Unchecked return value with no identified non-standard token in scope | — | speculative | **DROP — FP risk** |
| Missing nonReentrant guard AND an actual callback-enabled token is in scope (ERC777, ERC1155 receiver hook used) | permissionless | real reentrancy path | **HIGH** |
| Missing nonReentrant guard with no callback-enabled token in scope | — | speculative | **DROP — FP risk** |
| Use of `transfer()` (2300 gas) for payouts to EOAs only | — | none | **DROP — FP risk** |
| Use of `transfer()` (2300 gas) for payouts to addresses that can be arbitrary contracts | permissionless | bricked withdrawal if recipient's fallback costs > 2300 gas | **LOW** |
| Admin action with no timelock, no multi-sig enforcement visible, and the admin controls fund movement | admin | rug pull / instantaneous parameter change | **MEDIUM** (centralization) |
| Missing event emission on state-changing function | — | off-chain indexers miss state change | **LOW** or **INFO** |
| Griefing / DoS without fund loss | permissionless | single-user DoS | **LOW** |
| Griefing / DoS affecting ALL users of a critical function | permissionless | protocol-wide DoS | **HIGH** |
| Style / best-practice / non-exploitable | — | hardening only | **INFO** |

**Adjustment rules**:

- **Cap at MEDIUM** if the attacker must already be a trusted role (admin, owner, governance-elected). This is centralization risk, not exploitation.
- **Bump one tier** if the protocol holds >$10M TVL in a similar deployed protocol. drozer-lite does not know TVL; apply this only if the user stated the context.
- **Drop the finding** if the "exploit" requires a specific off-chain setup drozer-lite cannot verify (e.g., "if the admin key is compromised by phishing").

**If the table has no row that matches**: the finding is either novel or you are describing a class of issue drozer-lite is not calibrated for. Default to **LOW** and document the mismatch in your reasoning. Do not invent a severity.

### Weak-evidence severity floor (new in v0.5.1)

If the exploit sentence's `[CONCRETE LOSS]` depends on ANY of the following, cap severity at **LOW** regardless of what the main severity table returned:

- **Off-chain tree / payload construction** — e.g., merkle-leaf reuse across windows requires the admin to reuse roots off-chain; the contract itself cannot enforce it.
- **Cross-contract configuration the admin sets later** — e.g., "if the token whitelisted via `setToken` returns false on transfer" when no such token is currently referenced in scope.
- **Unobservable user ordering or mempool races** that the contract neither enforces nor defends against, where either ordering is legitimate.
- **External callback behavior** on callee types that are not in the current whitelist (ERC777 hooks, generic ERC1155 receivers) unless the code actually integrates those standards.

Combined with the severity-tier output filter (Step 7 rule 1a), these capped-at-LOW findings move to `warnings[]` by default. This kills the class of "this could be bad if the admin / off-chain / future setup cooperates" speculations that survive Gate A by sounding concrete but depend on unobservable context.

### Confidence

- **HIGH**: code clearly matches the pattern; you can quote the offending line(s).
- **MEDIUM**: pattern is present but exploitability depends on context you cannot fully verify from the cluster alone.
- **LOW**: uncertain — match is suggestive, not definitive.

**Time budget for Step 5**: ~3-5 minutes per cluster of normal density. A 50KB cluster with all checks should be ~5 min. Skip checks that obviously don't apply (e.g., `permit_frontrun` on a contract with no signatures).

---

## Step 6 — Cross-cluster sweep

After all clusters are analyzed, look for bugs that span clusters. This is the step that catches bugs single-cluster analysis misses. Apply each of the 13 cross-cluster patterns below to every pair of clusters that have references in the inventory from Step 2. Patterns 1-6 catch **symmetric asymmetries** (same modifier applied here but not there). Patterns 7-13 catch **economic cross-cluster flows** (state drift, fill/drain asymmetry, consumer-side failures).

### Patterns 1-6 — symmetric asymmetry

1. **State write/read mismatches**: function in cluster A writes state variable V; function in cluster B reads V without re-validating preconditions. Look for staleness.
2. **Cross-contract access control gaps**: cluster A function F is guarded by role R; cluster B has a wrapper W around F that has weaker or no access control. The wrapper bypasses the guard.
3. **Auto-route fallbacks**: cluster A's contract has a `receive()` or `fallback()` that calls a state-changing function in the same or another cluster, so the contract balance is never what callers expect. Check whether ANY function in the inventory uses `address(...).balance` for an invariant the auto-routing breaks. **If found, severity MUST be at least HIGH** — this is the UNI-98 pattern (see universal.md).
4. **Service interface failure modes**: cluster A provides an interface (e.g., `IOracle`); cluster B consumes it without checking for stale, zero, or revert returns.
5. **Shared modifier inconsistency**: same bug class fires in cluster A but not B, even though both use the same modifier — flag the missing application in B.
6. **Pause-state asymmetry**: a pause flag in cluster A is checked in some functions but not in functionally-equivalent siblings in cluster B.

### Patterns 7-13 — economic cross-cluster flows

7. **Snapshot-consumption drift**: cluster A stores a value `V` computed from a time-varying rate or reference (e.g. `record.amountSnapshot = shares * currentExchangeRate`). Cluster B (or a later call in cluster A) mutates that rate via reported events (loss events, reward events, slashings, rebalances, fee adjustments). The stored snapshot is never re-evaluated at consumption time, so the user or protocol is settled at a stale value. Report as `lifecycle_state_residue` with MEDIUM+ severity. This catches the class-of-bug where multi-user queue ordering around rate-changing events creates unfairness.
8. **Aggregate fill/drain asymmetry**: cluster A has a variable `V` that a write path FILLS (e.g. `V += delta` on some inbound action); cluster B/A has a drain path that DRAINS `V` under some conditions but NOT others. Look for sequences where `V` can accumulate without being drained, and where the only drain path is conditional on caller actions that may never happen. Report as `lifecycle_state_residue` or `unbounded_loop` with MEDIUM+ severity. drozer-lite cannot construct the full exploit sequence — flag the class-of-bug so the auditor can investigate whether accumulated value can be trapped.
9. **Cross-cluster unchecked caller parameter**: cluster A function accepts a contract-address parameter (e.g. `target` / `router` / `factory` / `module`) and calls into it. Cluster B is the intended target but no validation enforces it. Role-gating the caller is necessary but NOT sufficient — the caller can still pass a malicious or wrong target. Check whether cluster A stores an authoritative target or validates the parameter against a whitelist.
10. **Cross-cluster role assumption drift**: cluster A calls cluster B function F which requires role R. Cluster A is ASSUMED to hold R but it's not enforced by cluster A's constructor or initialize. If R is revoked from A externally, A's calls revert silently or bubble. Flag as operational fragility (INFO) unless it also opens an attack path.
11. **Cross-cluster counter consistency**: cluster A and cluster B both write to a shared counter variable (e.g. a global total/supply/balance held in a third cluster). Verify that both writers are mutually aware or the counter would drift under concurrent access.
12. **Provider-consumer type mismatch**: cluster A provides data in units U1 (e.g. basis points, 8-decimal fixed point, wei); cluster B consumes in units U2 (e.g. percentage, 18-decimal fixed point, whole units). Check the provider-interface output shape in cluster A against the consumer math in cluster B.
13. **Cross-cluster pause propagation**: cluster A pauses (local flag) but cluster B's functions that depend on A's state don't check A's pause. When A is paused, B continues operating on stale or partial state.

For each pattern: use the **inventory from Step 2** to identify cross-cluster references quickly. You do NOT need to re-read full cluster source to do the sweep — the inventory has the structural information.

Add cross-cluster findings to the same finding pool with `cross_cluster: true` and the names of both clusters involved.

**Be honest about confidence**: cross-cluster patterns 7 and 8 (economic flows) are pattern-level CANDIDATES for bugs. drozer-lite can flag the class of bug but cannot construct the exploit sequence — the LLM does not do multi-step actor modeling. When flagging, use MEDIUM confidence and note "pattern present, exploit sequence requires manual / `/droz3r` verification".

**Time budget for Step 6**: ~5-10 minutes for a small protocol (≤100KB). Larger protocols may need ~15 minutes.

---

## Step 7 — Emission gates, dedup, aggregate, output

Before dedup, every candidate finding in your working set MUST pass the pre-emission worksheet and three gates (A, C, B). Findings that fail are dropped — **but the drop MUST be recorded in `warnings[]`** so the audit log shows what was filtered and why. Silent drops are forbidden (v0.5.2).

### Step 7.0 — Pre-emission worksheet (MANDATORY before any gate)

For EVERY candidate finding in the working set, fill this 6-field worksheet internally before applying Gates A / C / B. REQUIRED fields must be code-backed — empty REQUIRED fields mean DROP with a `warnings[]` entry `"dropped: <title> | <field-that-failed>"`. This worksheet is the mechanical enforcement of Step 5 rule 4a, Gate A, and Gate C; the existing gate sections below retain authoritative details but the worksheet is the commitment.

| # | Field | Required? | What to fill |
|---|-------|-----------|--------------|
| 1 | Title | Y | One-line title. |
| 2 | Textbook pattern (Y/N) | Y | Mark Y for canonical well-known patterns — CEI/reentrancy, signature replay, reward-debt / MasterChef accumulator, multisig stale-approval after owner removal, ERC4626 first-depositor inflation, flash-loan oracle manipulation, approve-then-transferFrom race, missing slippage on a swap router, missing event on admin setter, missing nonReentrant on a callback-reachable function, similar known patterns. Otherwise N. |
| 3 | Specific-line break | Y if textbook=Y | `file:line` of the specific code that deviates from the textbook safe version + the one-line diff that would fix it. If textbook=Y and this field is unfillable from the current source, DROP. Enforces Step 5 rule 4a. |
| 4 | Exploit sentence | Y | *"An attacker with [ROLE/PERMISSION] calls [FUNCTION] with [CONCRETE INPUT], and the result is [CONCRETE LOSS/IMPACT]."* All four brackets filled from in-scope code; no hypothetical future state (Step 5 rule 5a), no admin-cooperation hedge (Step 5 rule 5b). If any bracket fails, DROP. Enforces Gate A. |
| 5 | Defender sentence | Y | *"This may be a false positive because [specific code-level reason backed by a visible line: a require, a modifier, a state-update ordering, a documented off-chain constraint]."* Strong defender → downgrade one tier (LOW → drop). Weak defender (intuition-only, "probably safe") → keep at original severity. No defender possible from visible code → keep at original severity. Enforces Gate C. |
| 6 | Severity row | Y | Quote the row from the Severity decision table that justifies the chosen severity. If no row matches, default to LOW per the table's no-row rule and document why no row matched. Severity by feel is forbidden. |

Exceptions that pass the worksheet without a full field 4: (a) cross-cluster economic flow candidates from Step 6 patterns 7-13 (explanation prefixed `"Pattern-level candidate:"`, confidence MEDIUM or LOW); (b) INFO-capped hardening items with a one-line justification for keeping. Both must still fill fields 1, 2, 5, 6.

After every candidate passes the worksheet, apply Gate B (reasoning reconciliation) over the full working set, then dedup/consolidation, then output.

### Gate A — Exploit-Sentence Gate (precision)

For each candidate finding, write out internally the following one-sentence exploit statement:

> *"An attacker with [ROLE/PERMISSION] calls [FUNCTION] with [CONCRETE INPUT], and the result is [CONCRETE LOSS/IMPACT]."*

Every bracket must be filled from THIS codebase's source, not from a hypothetical future state.

- `[ROLE/PERMISSION]` must be one of: `anyone` (permissionless), `any holder of X` (where X is a real role/token in this code), `the admin` (if admin is the attacker), `a contract at address Y` where Y is reachable. NOT "a future ERC777 integration", NOT "if a malicious token is whitelisted".
- `[FUNCTION]` must be a function that exists in scope.
- `[CONCRETE INPUT]` must be a value range that exists in the parameter types AND is not already rejected by a require/assert/modifier in the current code.
- `[CONCRETE LOSS/IMPACT]` must be quantifiable: "X tokens moved to attacker", "pool reserves desynced by Y", "user locked out of withdraw", etc. NOT "could cause issues", NOT "may cause confusion".

If any bracket fails, **DROP the finding**. Do not downgrade it to INFO. Do not hedge it with "theoretical". Drop it.

**Two exceptions** — findings allowed through without a concrete exploit sentence:

1. **Cross-cluster economic flow candidates** (Step 6 patterns 7-13). These are explicitly pattern-level flags that drozer-lite cannot construct exploits for. They pass Gate A with `confidence: "MEDIUM"` or `"LOW"` and the explanation must begin with `"Pattern-level candidate:"`.
2. **Informational hardening items** flagged as `severity: "INFO"` (e.g., missing event emission). These pass Gate A but are capped at INFO and must have a one-line justification for why they were kept.

### Gate C — Disprove-Before-Emit (adversarial precision)

After Gate A but before Gate B, for every candidate finding that survived Gate A, write ONE adversarial sentence in your reasoning:

> *Defender's Argument: "This may be a false positive because [specific code-level reason the exploit fails]."*

The reason must be **backed by visible code** in the current source — not intuition, not "probably safe", not "the author surely thought about it." Things that count as a valid defender:

- A specific require/assert that blocks the attack path
- A specific modifier on another function that prevents the precondition
- A specific state update ordering that neutralizes the described race
- A specific modifier/guard on the external callback that prevents reentry
- A specific off-chain constraint documented in the code (comment / NatSpec) that the contract's author relied on

Then apply this rule:

| Defender sentence quality | Action |
|---|---|
| Strong defender backed by a specific line-level guarantee | **Downgrade one tier**. If original is LOW, drop. |
| Weak defender (hand-waving, "usually", "probably") | Keep at original severity |
| No defender possible — no mitigation visible | Keep at original severity (this is a real finding) |

This gate forces the agent to *argue against itself*. Pattern matching plus concrete trace is not enough — the agent must try to disprove the finding using the code. Real bugs survive because no defender argument holds. Plausible-looking FPs get downgraded or dropped because a line-level mitigation exists.

**Visibility (mandatory, v0.5.2)**: For every Gate C decision — downgrade, drop, OR kept-at-original-severity — emit a `warnings[]` entry of the form `"defender_applied: <title> | <one-sentence defender>"` (or `"defender_none: <title> | no mitigation visible"` when no defender is possible). This makes the gate's reasoning auditable post-hoc; silent gate decisions hide regressions.

**Exception**: CRITICAL findings where the defender is only "the admin would not do that" → do NOT downgrade (admin-trust hedging is banned by Step 5 rule 5 anyway).

### Gate B — Reasoning Reconciliation (recall)

Before emitting, scan your own reasoning trace for any dismissal phrases applied to a candidate finding:
- *"actually not a vuln"*, *"self-griefing"*, *"edge case"*, *"would revert anyway"*, *"dead code"*, *"by design"*, *"admin-only so trusted"*, *"mitigated by admin whitelist"*, *"unreachable in practice"*.

For each dismissal, ask: **is the dismissal backed by a hard constraint visible in the current code (a require, a modifier, an enforced invariant) or is it an intuition about operator behavior?**

- Hard constraint → dismissal is valid → keep dropped.
- Intuition about operators, future state, or "by design" without a code-level lock → **restore the finding** at appropriate severity.

This gate prevents the agent from reasoning itself out of reporting real bugs that the pattern matcher correctly identified.

### Dedup, consolidation, and aggregation

After all three gates (A, B, C):

1. Group surviving findings by `(canonical_vulnerability_type, affected_file, affected_function)`. Two findings with the same triple are duplicates — keep the highest-severity.
1a. **Severity-tier output filter** (new in v0.5.1, schema-mismatch rule added v0.5.7): by default, the `findings[]` array contains only `CRITICAL`, `HIGH`, and `MEDIUM`. `LOW` and `INFO` findings move to the `warnings[]` array as `"low: <title>"` or `"info: <title>"` strings — preserved in output, out of the main findings list. Rationale: LOW/INFO findings are hardening observations; most scoring rubrics penalize them as false positives relative to the expected bug set. A user who wants them (real-audit context) can invoke with `--include-low` or `--full` and the skill restores them to `findings[]`.

   **Schema-mismatch rule (v0.5.7)**: When the output schema required by the caller does NOT contain a `warnings` field (external benchmark schemas, narrow CI harnesses, reports that only accept a flat `findings[]` array), LOW and INFO findings MUST be **dropped entirely** — do NOT flatten them into `findings[]` to preserve the observation. Flattening converts hardening notes into false positives against every scoring rubric that penalizes unmatched findings. The binding rule: if LOW/INFO cannot be emitted to `warnings[]`, they cannot be emitted at all. The `--include-low` / `--full` flags remain the only way to surface LOW/INFO into `findings[]`, and even then the caller is explicitly opting in to the FP risk. Detecting schema mismatch: if the output format specification (e.g., `program.md`, API contract, JSON schema) enumerates allowed top-level fields and `warnings` is not among them, the schema-mismatch rule applies.

2. **Root-cause consolidation** (new in v0.5.1, shared-check rule added v0.5.7): after dedup, group by `(canonical_vulnerability_type, affected_file)`. If two or more findings share the same vulnerability_type in the same file but hit different functions, apply these two tests in order:

   **(a) Shared-check mechanical rule (v0.5.7) — apply FIRST**: If the fix for each finding in the group is adding the SAME named check (same `require`/`assert`/modifier invocation, same validated variable or flag) — e.g. all N findings fixed by adding `require(!disputed)`, or all N fixed by adding the same `nonce` mapping check, or all N fixed by adding the same `onlyOwner` modifier — **consolidate into ONE finding**. Name the primary function in `affected_function`; list all siblings in `explanation` with `(also affects: fnA, fnB)`. The title generalises to the missing check, not the function name (e.g. "Missing !disputed check across settlement functions" rather than "refund lacks disputed check"). **Different function names alone are not grounds to keep separate** when the missing check is identical across the group.

   **(b) "Could one PR fix all of them?" test**: if the shared-check rule doesn't fire but a single code change would still resolve all findings (e.g., `executeTransfer` and `executeTokenTransfer` both missing a nonce, fixed by adding one shared nonce-check helper) → **consolidate**.

   If neither (a) nor (b) applies — fixes are genuinely independent (e.g., reentrancy in `withdrawTo` vs access control missing on `setRate` — different fix patterns) → keep as separate findings.
3. The highest-severity finding wins each consolidated slot.
4. Output a single JSON object matching the schema below. By default, no prose around it, no markdown fences. (If the user explicitly asked for a Markdown report, render the same content as a Markdown report — see the Markdown variant at the bottom.)

```json
{
  "scanner": "drozer-lite",
  "version": "0.4.0",
  "profiles_used": ["universal", "reentrancy", "oracle"],
  "files_analyzed": [
    "Token.sol", "Service.sol", "Accountant.sol",
    "Registry.sol", "PauseRegistry.sol",
    "OracleAggregator.sol", "adapters/Adapter.sol",
    "adapters/SourceImpl.sol", "adapters/IAdapter.sol"
  ],
  "clusters": [
    {"name": "PrimaryCluster", "files": 3, "findings": 6},
    {"name": "ControlCluster", "files": 2, "findings": 2},
    {"name": "OracleCluster", "files": 4, "findings": 3}
  ],
  "findings": [
    {
      "vulnerability_type": "lifecycle_state_residue",
      "affected_function": "confirmAction",
      "affected_file": "Service.sol",
      "severity": "HIGH",
      "explanation": "confirmAction requires `address(this).balance >= amount` but the receive() fallback auto-routes incoming native value into the primary state-mutating flow. Any native value sent to the contract is consumed by the auto-route instead of accumulating in contract balance, so the balance-based invariant is permanently brittle.",
      "line_hint": 305,
      "confidence": "HIGH",
      "source_profile": "universal",
      "cluster": "PrimaryCluster",
      "cross_cluster": false,
      "swc_id": null,
      "cwe_id": "CWE-672"
    }
  ],
  "stats": {
    "wall_time_sec": 1247,
    "clusters_analyzed": 3,
    "checks_loaded": 105,
    "dedup_clusters_merged": 2,
    "dedup_total": 13,
    "dedup_representatives": 11
  },
  "warnings": []
}
```

### Field rules

- `scanner` is always `"drozer-lite"`.
- `version` is `"0.5.7"`.
- `vulnerability_type` MUST be a snake_case canonical tag from the vocabulary at the bottom of this file. **You MUST pick the closest existing tag**; paraphrasing (e.g. writing `"tx.origin authorization"` when the canonical tag is `tx_origin_auth`) is NOT allowed. The vocabulary aligns with SWC Registry and Code4rena taxonomy — labels like `tx_origin_auth`, `missing_access_control`, `missing_input_validation`, `checks_effects_interactions_violation`, `signature_replay`, `reentrancy`, `oracle_staleness`, `division_by_zero`, `missing_timelock` are industry-standard and should match what external scorers and graders expect. Only if the vocabulary genuinely has no close match may you fall back to a short snake_case description — and that is an extraordinary case that should be flagged with a `warnings` entry.
- `severity` is exactly one of `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`, `INFO`. Uppercase.
- `confidence` is exactly one of `HIGH`, `MEDIUM`, `LOW`. Uppercase.
- `source_profile` MUST be one of the profiles you loaded in Step 3.
- `cluster` MUST be one of the cluster names from Step 4.
- `cross_cluster` is `true` if the finding was discovered in Step 6 (cross-cluster sweep), `false` otherwise.
- `swc_id` / `cwe_id` are nullable. Set them when the canonical vocabulary entry has them.
- `findings` may be empty.
- `warnings` should hold any size warnings, profile-load issues, or skipped clusters.

---

## Step 7.5 — Write the report to disk

After producing the findings JSON, write TWO files to the **project root** (the directory the user pointed you at):

1. **`drozer-lite-findings.json`** — the canonical JSON output from Step 7. Machine-readable, schema-compliant.
2. **`DROZER_LITE_REPORT.md`** — the Markdown variant (see Markdown format at the bottom of this skill). Human-readable, severity-grouped, with summary table, per-finding sections, and the honest framing disclaimer.

Use the Write tool. Overwrite if either file already exists (re-runs should produce fresh output).

After writing, tell the user:
```
Wrote:
  drozer-lite-findings.json  (canonical JSON, {N} findings)
  DROZER_LITE_REPORT.md      (Markdown report)
```

If the user specified `--output <path>`, write to that path instead of the project root.

**Do NOT skip this step.** Findings that only exist in conversation context are lost when the session ends. The disk files are the deliverable.

---

## Step 8 — Honest framing

ALWAYS end your response (after the JSON or Markdown report) with this disclaimer, verbatim. Do not soften it. Do not skip it.

> drozer-lite is a pattern-level scanner with cross-file awareness. It catches bugs from a curated checklist of 205 patterns across 14 protocol-type profiles, all derived from real audit findings. It does NOT do multi-step actor reasoning, chain-composition analysis, or formal verification. A clean drozer-lite run is NOT a clean audit. For high-value contracts, use `/droz3r` (the full drozer pipeline) or a human auditor on top of this.

Then add a one-line time disclosure:

> Total wall-clock time: ~XX min. {N} clusters analyzed across {M} files. {K} profiles loaded.

---

## Canonical vulnerability vocabulary (use these as `vulnerability_type`)

These are the snake_case tags. Each tag has a fixed meaning and an optional SWC/CWE cross-reference. If a finding genuinely matches none, use a short snake_case fallback and accept it will not be canonicalized.

**Vocabulary discipline (v0.5.2)**: Where multiple naming forms exist for the same concept (full vs abbreviated, alternate framings), the canonical tag chosen here matches the **unabbreviated industry-standard form** used by SWC Registry / Code4rena / Sherlock. Aliases are listed for cross-reference but MUST NOT be emitted in `vulnerability_type` — emit the canonical only. External scoring rubrics match strings literally; abbreviations lose points to no benefit.

**Tag selection between near-synonyms (v0.5.2)**: Where two canonical tags describe overlapping patterns (e.g. `reentrancy` vs `checks_effects_interactions_violation`), each entry includes a discriminator that resolves the choice. When the discriminator does not clearly resolve, prefer the broader/default tag. Do NOT invent new tags to "split the difference."

**Alias canonicalization (v0.5.5)**: Before emitting `vulnerability_type`, rewrite aliases to canonicals using the table below. This is a **mechanical lookup, not a reasoning step** — if your chosen tag appears in the left column, emit the right column verbatim. External scoring rubrics, finding-dedup tools, and SWC/Solodit cross-referencing pipelines match strings literally; paraphrases cost points against every consumer of the output. The alias list here records paraphrases that real LLM invocations have produced for the same underlying bug — add to this table when a new paraphrase is observed, do not add benchmark-specific mappings.

| Alias (do NOT emit) | Canonical (emit this) | Reason |
|---|---|---|
| `tx_origin_auth` | `tx_origin_authentication` | SWC-115 / Code4rena / Sherlock use the unabbreviated form |
| `reentrancy` (when the fix involves reordering state updates before the external call, with OR without adding a guard) | `checks_effects_interactions_violation` | Default tag per broadened discriminator in Reentrancy section below — covers callback-reentry drains too |
| `cei_violation` / `cei_bug` / `state_update_after_call` | `checks_effects_interactions_violation` | Abbreviations / alternate framings of the same canonical |
| `no_access_control` | `missing_access_control` | SWC-105 unabbreviated |
| `no_input_validation` / `input_validation_missing` | `missing_input_validation` | SWC-123 unabbreviated, consistent with other `missing_*` tags |
| `sig_replay` / `signature_replayable` | `signature_replay` | SWC-121 unabbreviated |
| `div_by_zero` / `divide_by_zero` | `division_by_zero` | Unabbreviated noun form |
| `reward_debt_stale_on_balance_change` / `reward_accounting_bug` | `lifecycle_state_residue` | Reward-debt-on-balance-change is an instance of lifecycle state residue; use the canonical unless a more specific tag applies |
| `no_slippage_check` / `slippage_missing` | `missing_slippage_protection` | Consistent with `missing_*` family |
| `no_event_emitted` / `missing_event` | `missing_event_emission` | Full noun form |

If your chosen tag is NOT in the left column and ALSO not in the canonical list further below, write a short snake_case fallback AND add a `warnings[]` entry `"novel_vulnerability_type: <tag> | <one-line reason no canonical fits>"` so the gap is visible for future vocabulary updates.

### Reentrancy / external call ordering
- `checks_effects_interactions_violation` — **Default tag for any bug whose fix is "update state BEFORE the external call / token transfer".** Covers classic CEI violations (refund before update, balance transfer before zeroing, cap check before increment) **and** callback-reentrant drains where the callback exploit is possible only because state is updated after the call. If reordering the function body to Checks → Effects → Interactions would eliminate the exploit, emit this tag. (SWC-107)
- `reentrancy` — Use ONLY for exploits that persist even with correct CEI ordering within the vulnerable function. This is cross-function reentrancy, shared-state reentry across multiple contracts, or callback-mediated state corruption where the ordering inside any single function is fine but the state invariant across functions is not. If the fix is "add `nonReentrant`" alone (ordering is already correct) → `reentrancy`. If the fix is "reorder state update before external call" (with or without adding a guard) → `checks_effects_interactions_violation`. (SWC-107, CWE-841)
- `cross_function_reentrancy` — State changed in one function is read inconsistently in another via callback. (SWC-107)
- `callback_hook_reentrancy` — ERC777/721/1155 receiver hook reenters before state finalization. (SWC-107)

### Access control
- `missing_access_control` — State-changing function lacks an authorization check. (SWC-105, CWE-284)
- `tx_origin_authentication` — Authorization decision uses tx.origin instead of msg.sender. (SWC-115) **Canonical tag is the unabbreviated form** to match SWC/Code4rena/Sherlock rubric naming. *Alias (do not emit): `tx_origin_auth`.*
- `privilege_retention_after_transfer` — Deployer or prior owner retains non-owner roles after ownership transfer.
- `rate_limit_bypass` — Sibling function or alternative path bypasses an enforced rate limit.

### Input validation
- `missing_input_validation` — User-supplied parameter is not bounded against an invariant (e.g. `discountBps > 10000`, `fee > 100%`, `amount == 0` on a critical path). (SWC-123, CWE-20)
- `missing_condition_check` — Caller preconditions (time window, state flag, counterparty consent) are not enforced, letting the caller act outside the intended state machine. Use this when the omission is a single missing require/assert, not a broader access-control gap.

### Math / arithmetic
- `integer_overflow` — Arithmetic overflow or underflow that wraps. (SWC-101, CWE-190)
- `unsafe_cast_truncation` — Narrowing cast (e.g. uint256→uint160) truncates a critical value.
- `decimal_scaling_mismatch` — Heterogeneous decimal scaling in accumulator math.
- `formula_parameter_transposition` — Formula parameters swapped (e.g. eloA/eloB) producing inverted results.
- `division_by_zero` — Denominator can reach zero in a value-moving operation.

### Token / approval
- `unchecked_return_value` — External call return value not verified. (SWC-104, CWE-252)
- `non_standard_erc20` — Non-standard ERC20 (e.g. USDT) returns no bool — call appears to succeed silently.
- `max_allowance_drain` — Approval to type(uint).max enables cross-contract drain by intermediate.
- `low_level_call_silent_success` — Low-level call to non-existent address returns success without code-size guard.

### Signatures
- `signature_replay` — Signed message can be replayed across chains, contexts, or sessions. (SWC-121)
- `permit_frontrun` — Public permit() can be front-run to consume the user's signature without try/catch.
- `eip712_typehash_mismatch` — EIP-712 type hash differs from on-chain encoding; signatures never validate.
- `signature_authorization_gap` — Signature is valid but the signer is not authorized for the target account.
- `unchecked_signed_field` — Signed struct field is included in the signature but never enforced on-chain.

### Oracles
- `oracle_staleness` — Oracle data freshness is not validated before use.
- `oracle_manipulation` — On-chain price source can be manipulated within a single transaction.
- `oracle_failure_cascading` — Oracle failure (zero / max return) cascades into sell-at-zero or DoS.

### Vault / shares
- `share_inflation` — ERC4626 share inflation via first-depositor rounding or donation attack.
- `lifecycle_state_residue` — State remains active after lifecycle transition.
- `missing_slippage_protection` — Trade or LP function lacks min-out / deadline protection.

### Cross-chain
- `cross_chain_replay` — Cross-chain message can be replayed across chains or peers.
- `missing_destination_check` — Receiver does not verify it is the intended chain/destination of the payload.
- `cross_chain_address_substitution` — msg.sender reused as a destination-chain identity (incompatible across chains).
- `msgvalue_unsigned` — msg.value not bound by the signature, allowing executor injection.

### Storage / proxy (EVM-specific — only fire on Solidity/Vyper targets)
- `uninitialized_proxy` — Logic contract initializer not disabled. (SWC-118) **EVM only.**
- `storage_layout_collision` — Upgradeable contract storage layout changed without preserving slots. (SWC-124) **EVM only.**
- `uninitialized_storage` — Storage variable defaults to zero and an unset state passes guards. (SWC-109) *Language-agnostic variant: uninitialized struct/resource fields in Move/Rust.*

### EVM-specific (only fire on Solidity/Vyper targets)
- `delegatecall_to_untrusted` — delegatecall target is attacker-controllable. (SWC-112) **EVM only.**
- `receive_auto_route_balance_invariant` — receive()/fallback() auto-calls a state-mutating function, breaking any invariant that uses `address(this).balance`. **EVM only.**
- `erc165_incomplete_coverage` — supportsInterface does not report all interfaces the contract actually implements. **EVM only.**
- `precision_loss_decimal_conversion` — Scaling between different decimal bases truncates value without rounding direction disclosure. *Language-agnostic — applies to any fixed-point math.*

### Other (language-agnostic unless noted)
- `timestamp_dependence` — Critical logic depends on block.timestamp in a manipulable way. (SWC-116) *All chains.*
- `missing_event_emission` — State-changing operation does not emit a corresponding event/log. *All languages.*
- `front_running` — Same-block front-running enables ordering-dependent profit. (SWC-114) *All chains.*
- `vrf_callback_gas` — VRF fulfillment callback exceeds the configured gas limit and reverts. *EVM/Solana.*
- `dust_order_dos` — Residual-below-threshold orders block price levels and DoS the book. *All languages.*
- `pause_time_accumulation` — Time-dependent state continues to accumulate while the protocol is paused. *All languages.*
- `unbounded_loop` — Loop over user-pushable collection with no upper bound. *All languages.*
- `irreversible_admin_action` — Admin parameter change with no timelock or two-step apply. *All languages.*
- `missing_signer_check` — Instruction/transaction does not verify the expected signer/authority. *Solana/Move/Cairo specific equivalent of `missing_access_control`.*
- `arbitrary_cpi` — Cross-program invocation target is attacker-controllable. *Solana equivalent of `delegatecall_to_untrusted`.*
- `missing_account_validation` — Account constraints (owner, discriminator, seeds) not verified. *Solana/Anchor specific.*

---

## Markdown variant (only if the user asks)

Format: Header (profiles, files, clusters) → Summary table → Per-finding sections (vulnerability_type, severity, function, file, cluster, confidence, explanation) → Disclaimer.

---

## Hard rules

1. **Do not** read checklists for profiles you did not load in Step 3.
2. **Do not** invent vulnerability types absent from the canonical vocabulary unless nothing fits.
3. **Do not** report findings without a specific function and file location.
4. **Do not** skip the honest framing disclaimer.
5. **Do not** soften severity ratings to be polite. Use the matrix.
6. **Do not** call any tool other than Read / Glob to gather source. There is no LLM API key in this skill — you ARE the LLM.
7. **Do not** load `icp` or `solana` profiles for Solidity code. They auto-load ONLY when Rust is the detected language and the appropriate framework keywords are present.
8. **Do not** exceed the 1MB total source budget. Refuse politely and recommend `/droz3r`.
9. **Do not** skip Step 6 (cross-cluster sweep) — it is the difference between v0.3.0 and v0.2.x.
10. **Do not** load a profile checklist for every cluster — load each profile checklist ONCE at the start of analysis and reuse it across clusters.
11. **Do not** reveal the inventory map or the cluster plan unless the user asks. They are working artifacts, not output.

---

## Check Authorship Rules

When adding checks to `checklists/*.md`: never use benchmark-specific names (contract names, function names, token tickers). Use generic class-of-bug descriptions only. Provenance lines are the one exception. See `CONTRIBUTING.md` for full rules.

## .claude/skills/drozer-lite/checklists

```

```

## .claude/skills/drozer-lite/checklists/cross-chain.md

# Cross-Chain Checklist

> Profile: cross-chain
> Checks: 13
> Source: ported from Drozer-v2 bridge-invariants.md (provenance cited per check)

## Methodology

Cross-chain protocols have two adversaries: an attacker on the source chain trying to mint value on the destination without a valid lock, and an attacker on the destination trying to replay, spoof, or redirect messages. For every message path, identify (a) the trust model (validators, zk proof, multisig), (b) how finality is enforced, (c) every field that is attacker-controllable in the payload, and (d) whether the callback verifies the source-chain sender (not just the local bridge caller). Use bridge-specific chain IDs, not EVM chain IDs, for every bridge API call. Refund addresses must resolve to the actual user, not the intermediary contract.

## Checks

### XCHAIN-1: Token Supply Conservation
**Provenance**: bridge-invariants.md B1
**Pattern**: Tokens can be minted on the destination chain without a verified lock on the source chain, or unlocked on the source without a verified burn on the destination.
**Methodology**: For each mint/unlock path, trace the proof of the counterpart action. Verify proof validation is complete (not just signature presence). Verify there is no admin path that mints without a proof.
**Red flags**:
- `mint(to, amount)` gated only by `onlyRelayer` with no proof validation
- Admin emergency mint without supply reconciliation

### XCHAIN-2: Message Verification Integrity
**Provenance**: bridge-invariants.md B2
**Pattern**: Messages can be forged due to incomplete signature verification, missing source chain identification, or unauthorized sender acceptance.
**Methodology**: For each message-processing function, verify signature/validator-set verification, source chain check, and sender authorization. Each field used for decisions post-verification must be part of the signed bytes.
**Red flags**:
- `executeMessage(bytes payload)` without verifying payload author
- Source chain not part of the signed digest
- Sender verification uses `msg.sender` instead of the cross-chain sender

### XCHAIN-3: Replay Attack Prevention
**Provenance**: bridge-invariants.md B4
**Pattern**: The same message can be executed more than once (same chain, different chains, or across contract upgrades).
**Methodology**: Verify every message has a nonce / unique hash tracked in a "consumed" mapping. Check whether the consumed set survives upgrades. For multi-chain systems, verify domain separation.
**Red flags**:
- Nonce scope too narrow (per-user but not per-operation)
- `executedMessages` mapping cleared on upgrade
- Same payload valid on multiple destination chains

### XCHAIN-4: Finality & Reorg Handling
**Provenance**: bridge-invariants.md B5
**Pattern**: The destination chain processes a source-chain message before source-chain finality, losing funds after a reorg.
**Methodology**: Verify confirmation-blocks are set per source chain. Verify pending messages can be cancelled on reorg. Check finality assumption matches each chain's actual finality.
**Red flags**:
- 1-block confirmation on PoW source chain
- No reorg handling mechanism
- Static confirmation count across all chains

### XCHAIN-5: Validator / Relayer Threshold Integrity
**Provenance**: bridge-invariants.md B3
**Pattern**: Threshold signatures are validated incorrectly, validator-set updates are not authenticated, or a single compromised key can pass the check.
**Methodology**: Verify signatures are collected and validated against the current validator set with the correct threshold. Verify validator-set updates are authenticated (same threshold as normal messages). Check for slashing or off-chain penalty mechanism.

### XCHAIN-6: Token Mapping Integrity
**Provenance**: bridge-invariants.md B6
**Pattern**: Token mappings between chains can be set to wrong addresses or to malicious tokens, or decimal mismatches cause value drift.
**Methodology**: Verify token mapping setters are behind timelock + access control. Verify decimal normalization between chains. Verify no wrapped token can be registered without the protocol's acknowledgement.

### XCHAIN-7: Rate Limiting & Caps
**Provenance**: bridge-invariants.md B7
**Pattern**: A single transaction or short burst can drain the entire bridge because per-tx or per-period caps are missing.
**Methodology**: Verify per-tx and per-period limits exist on minting and unlocking. Verify rate windows cannot be reset by admin mid-attack.

### XCHAIN-8: Emergency Pause & Recovery
**Provenance**: bridge-invariants.md B8
**Pattern**: A guardian can pause the bridge, but the pause does not stop all value-moving paths, or the recovery path is insecure.
**Methodology**: Verify `pause()` gates every critical function. Verify no admin path circumvents the pause. Check recovery flows.

### XCHAIN-9: LP Protection (Liquidity-Network Bridges)
**Provenance**: bridge-invariants.md B9
**Pattern**: A liquidity-network bridge allows LPs to be drained via fake claims or sandwich attacks on deposits.
**Methodology**: For each LP deposit and withdrawal path, check for delay mechanisms and sandwich protection. Verify fee distribution is pro-rata and cannot be gamed.

### XCHAIN-10: Upgrade Safety for Bridges
**Provenance**: bridge-invariants.md B10
**Pattern**: Upgrades leave pending messages stranded, cause storage collisions, or reset the validator set to an insecure default.
**Methodology**: Verify upgrade path has timelock and handles in-flight messages. Verify storage layout compatibility and validator-set preservation.

### XCHAIN-11: Bridge Callback Source Verification
**Provenance**: bridge-invariants.md B11
**Pattern**: Callbacks (`onTokenBridged`, `lzReceive`, `ccipReceive`, `sgReceive`, `receiveWormholeMessages`) trust `msg.sender` (the bridge contract) without verifying the source-chain sender, allowing arbitrary users to craft fake instructions.
**Methodology**: For each callback, verify it calls the bridge's source-sender accessor (`messageSender()`, `_srcAddress`, etc.) and validates it against an expected remote. For bridges that do not expose the source sender in the token callback (e.g., Omnibridge `onTokenBridged`), verify token bridging and instruction bridging are separated.
**Red flags**:
- `function lzReceive(...)` that uses only `require(msg.sender == endpoint)` and trusts the payload
- `onTokenBridged` that treats the `data` bytes as authenticated

### XCHAIN-12: Bridge API Parameter Correctness (Chain IDs, Refund Addresses)
**Provenance**: bridge-invariants.md B12
**Pattern**: Calls to bridge APIs use EVM `block.chainid` where the bridge expects its own ID system (Wormhole uint16 chain IDs, LayerZero endpoint IDs), or use `msg.sender` as refund when funds should return to the end user.
**Methodology**: For each bridge API call, verify chain ID uses the bridge's own system. Verify refund addresses resolve to the end user (tx.origin in multi-hop chains, or user param), not `msg.sender`. On Arbitrum, verify `callValueRefundAddress` is not attacker-controllable (holds cancellation power over retryable tickets).

### XCHAIN-13: Bridge Value Handling (msg.value Surplus & Requirements)
**Provenance**: bridge-invariants.md B13
**Pattern**: `msg.value` is sent to a bridge that does not need ETH, or excess `msg.value - cost` is left in the contract instead of refunded to the user, or the bridge reverts because `msg.value < cost`.
**Methodology**: For each bridge type, verify whether ETH payment is required (some bridges use L1 gas escrow). Verify `msg.value >= cost` before the call and `msg.value - cost` is explicitly refunded to the actual user. Verify refund defaults do not route to an intermediary.
**Red flags**:
- `bridge.send{value: msg.value}(...)` to a bridge that uses L1 gas
- Excess left in the contract after `bridge.quote()`
- Refund to `msg.sender` when `msg.sender` is a router contract

## .claude/skills/drozer-lite/checklists/dex.md

# DEX Checklist

> Profile: dex
> Checks: 11
> Source: ported from Drozer-v2 dex-invariants.md + amm-invariants.md (provenance cited per check)

## Methodology

DEXes and AMMs expose user trades to MEV, sandwich, and price-manipulation attacks. For every swap / route / quote path, verify: (a) user-specified minOut / maxIn bounds are enforced AFTER the final hop, (b) deadlines are checked, (c) the AMM invariant (k = x·y, StableSwap D, or Balancer weighted) is preserved or increased, (d) prices used for critical decisions are manipulation-resistant (TWAP, oracle), and (e) routers never hold user tokens across transactions and never use `balanceOf` for balance-based accounting. Test each weird-token class (fee-on-transfer, rebasing, non-bool return) against the swap/add-liquidity path.

## Checks

### DEX-1: Slippage & Deadline Enforcement
**Provenance**: dex-invariants.md D1 + amm-invariants.md A9
**Pattern**: `amountOutMin` / `amountInMax` / `deadline` are accepted but not enforced on every swap path, or enforced only on intermediate hops.
**Methodology**: For every entry function (swap, swapExactTokensForTokens, multihop), verify the minOut check occurs AFTER all hops complete. Verify `require(block.timestamp <= deadline)`. For multihop, verify intermediate tokens cannot be stolen by a callback.
**Red flags**:
- Slippage check in the single-hop helper not re-executed for multihop
- `deadline` parameter but no check in code
- Final minOut compared to intermediate hop output

### DEX-2: Route Integrity & Intermediate Token Safety
**Provenance**: dex-invariants.md D2
**Pattern**: A multihop router holds intermediate tokens between hops; a malicious intermediate pool or callback redirects them to the attacker.
**Methodology**: For each hop, verify the output is forwarded to the next hop's input address (not left on the router). Verify callbacks cannot call back into the router with the balance available. Verify the declared path matches the executed path.
**Red flags**:
- Router uses `balanceOf(this)` as the amount for the next hop
- `_swap` pulls from and pushes to `address(this)` without a guard
- Callback invocation during a multihop route not reentrancy-protected

### DEX-3: Constant-Product / StableSwap Invariant Preservation
**Provenance**: amm-invariants.md A1
**Pattern**: Rounding errors or reentrancy allow `k` to decrease below its pre-swap value, leaking value out of the pool.
**Methodology**: For Uniswap V2 style: verify `k_after >= k_before` in `_update`. For Curve StableSwap: verify D is non-decreasing. For Balancer weighted: verify weighted balances satisfy the invariant.
**Red flags**:
- Swap math using `divide-before-multiply`
- k-check omitted because "impossible in normal flow"
- Flash-swap path that decreases k before the callback

### DEX-4: TWAP Oracle Integrity
**Provenance**: amm-invariants.md A4 + dex-invariants.md D5
**Pattern**: Oracle consumers use a single-block spot price or a TWAP with too short a window, allowing flash-loan manipulation.
**Methodology**: For every price consumer, identify the window. Test whether a same-block manipulation changes the reported price. Verify accumulator overflow handling.
**Red flags**:
- `getSpotPrice()` used for liquidation decisions
- TWAP window < 30 min for critical decisions
- Observations array too small to cover required lookback

### DEX-5: Flash Swap Repayment Enforcement
**Provenance**: amm-invariants.md A6
**Pattern**: Flash swap / flash loan callback fails to enforce repayment + fee, or does so only in the happy path.
**Methodology**: For each `flash`/`flashSwap`/`uniswapV2Call` path, trace the repayment check. Verify it compares pool balance after the callback against required amount + fee. Verify reentrancy guard covers the callback.
**Red flags**:
- Repayment check uses `balanceOf` trusting caller-provided amount
- Callback that can re-enter `flash` itself

### DEX-6: Token Approval Safety & Weird Tokens
**Provenance**: dex-invariants.md D4 + D9
**Pattern**: Router accepts fee-on-transfer / rebasing / non-bool-return tokens but computes amounts from `amount parameter` instead of actual received; or assumes infinite approval is safe.
**Methodology**: For each `transferFrom` path, verify actual received is measured via balance-before/after (not input amount). Verify `SafeERC20` or low-level success check is used. Verify approvals use `increaseAllowance` or reset-to-zero pattern.
**Red flags**:
- `token.transferFrom(user, pool, amount); _swap(amount, ...);` (no fee-on-transfer handling)
- Router holds user approvals permanently
- Permit handling that does not cover DAI's non-standard interface

### DEX-7: Pool Formula / Token-Count Mismatch
**Provenance**: drozer-lite v0.4.2 — class-of-bug: an AMM pool type uses a mathematical formula that assumes a fixed number of tokens (e.g., x*y=k for 2 tokens) but the pool creation function allows more tokens than the formula supports, producing incorrect swap results and broken invariants.
**Pattern**: A DEX supports multiple pool types (constant product, stable swap, weighted). Each pool type's swap formula is designed for a specific number of tokens. The pool creation function validates the token count against a global max (e.g., MAX_ASSETS = 4) but does NOT validate against the pool-type-specific maximum. A constant-product pool can be created with 3+ tokens even though the x*y=k formula only works for 2 tokens.
**Methodology**:
1. For each pool type, identify the mathematical formula used for swaps.
2. Determine how many tokens the formula supports: constant product (x*y=k) = 2; Balancer weighted = N; StableSwap = N.
3. Check whether pool creation enforces the pool-type-specific token limit. If a constant-product pool can be created with >2 tokens, flag as HIGH.
4. Check slippage tolerance assertions — if they hardcode `deposits.len() == 2` but the check is conditional (e.g., only runs when slippage_tolerance is Some), the guard can be bypassed.
**Red flags**:
- `MAX_ASSETS_PER_POOL = 4` applied uniformly to both constant-product and stable-swap pools
- Constant-product swap formula uses only `offer_pool` and `ask_pool` (2 tokens) but pool has 3+ tokens
- Slippage check requires exactly 2 deposits but is inside `if let Some(slippage_tolerance)` — bypassed when None
- Liquidity addition works for N tokens but shares calculated via `sqrt(d0 * d1)` (2-token formula)
- Pool creation validates `len >= 2 && len <= MAX` but not `if ConstantProduct then len == 2`

### DEX-8: Withdrawal Path Lacks Minimum-Output Protection
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a liquidity withdrawal function calculates refund amounts proportionally but provides no mechanism for the user to specify minimum acceptable amounts, exposing them to sandwich attacks and unfavorable rates during high volatility.
**Pattern**: A `withdraw_liquidity` / `remove_liquidity` function burns LP tokens and returns underlying assets proportionally. The refund amounts are computed from the pool's current asset ratios. No `min_amount_out` or `minimum_receive` parameter exists. Industry standard (Uniswap V2 Router) provides `amountAMin` and `amountBMin` parameters for withdrawal protection.
**Methodology**:
1. For every liquidity withdrawal function, check whether the user can specify minimum acceptable output amounts.
2. If no minimum-output parameter exists, check whether slippage tolerance is applied to the withdrawal calculation.
3. If neither exists, flag. The user has no protection against pool composition changes between transaction submission and execution.
**Red flags**:
- `withdraw_liquidity(pool_id)` with no `min_assets_out` parameter
- Refund calculated as `pool_asset.amount * share_ratio` with no floor check
- No deadline parameter on withdrawal
- User must accept whatever the pool ratio is at execution time

### DEX-9: Asset Ordering Inconsistency Between Input and Internal State
**Provenance**: drozer-lite v0.4.2 — class-of-bug: user-provided deposit assets are sorted (by the chain or by aggregation logic) but pool-internal asset arrays maintain creation-time order. When slippage/ratio checks compare deposits[i] against pool_assets[i], the indices don't align, causing the check to use inverted ratios.
**Pattern**: A DEX pool stores assets in the order they were provided at creation time (e.g., [tokenB, tokenA]). User deposits are sorted alphabetically by the chain's coin handling or by an aggregation function (e.g., [tokenA, tokenB]). Slippage tolerance or ratio checks compare `deposits[0]/deposits[1]` against `pool_assets[0]/pool_assets[1]`. Because the orderings differ, the ratio comparison is inverted — checking the wrong price direction.
**Methodology**:
1. Check whether pool creation sorts `asset_denoms` or preserves caller-provided order.
2. Check whether `info.funds` / deposits are sorted (CosmWasm sorts funds alphabetically).
3. Check whether slippage/ratio checks index into both arrays positionally — if orderings can differ, the check is inverted.
4. A malicious pool creator can deliberately create a pool with reverse-ordered denoms to exploit this.
**Red flags**:
- `create_pool(asset_denoms: vec!["tokenB", "tokenA"])` — pool stores in this order
- Deposits arrive as `[tokenA, tokenB]` (chain-sorted)
- Slippage check: `deposits[0]/deposits[1] vs pool_assets[0]/pool_assets[1]` — inverted ratio
- Pool creation does not sort `asset_denoms` alphabetically
- Two pools with the same tokens but different ordering have different slippage behavior

### DEX-10: Disproportionate Deposit Loss (Excess Not Refunded)
**Provenance**: drozer-lite v0.4.2 — class-of-bug: when a user provides liquidity in a ratio different from the pool's current ratio, LP shares are minted based on the MINIMUM proportional share, and the excess tokens from the higher-ratio asset are effectively donated to the pool instead of being refunded.
**Pattern**: A `provide_liquidity` function calculates per-asset share ratios (`deposit_amount * total_share / pool_amount`) and mints LP tokens based on `min(share_ratios)`. The excess tokens from the non-minimum asset are added to the pool but not reflected in the minted shares. These excess tokens are permanently donated to all existing LPs. The function does NOT refund the excess to the depositor.
**Methodology**:
1. For every liquidity provision function, check how shares are computed when deposit ratios don't match pool ratios.
2. If `min(share_ratios)` is used, calculate the implied excess for each asset.
3. Check whether the excess is: (a) refunded to the depositor, (b) used to compute additional shares, or (c) silently donated to the pool.
4. If (c), check whether slippage tolerance protects against this loss. Note: slippage tolerance checks LP tokens received, NOT whether excess tokens are returned.
**Red flags**:
- `share = min(deposit_A * total_share / pool_A, deposit_B * total_share / pool_B)` with no refund of the difference
- User deposits 100A + 200B into a 1:1 pool; receives shares worth 100A + 100B; 100B is donated
- Slippage tolerance passes because LP tokens are within tolerance — but user lost 100B
- No industry-standard `_addLiquidity` that computes optimal amounts before the actual deposit (cf. Uniswap V2 Router)
- A front-runner changes the pool ratio between tx submission and execution, maximizing the user's excess donation

### DEX-11: Spread / Slippage Computed Before Fee Deduction
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the spread or slippage amount is calculated from the pre-fee return amount, making the computed spread larger than the actual slippage. This causes the slippage check to be systematically too lenient, passing swaps that should have been rejected.
**Pattern**: A swap function computes `return_amount` (before fees), then `spread_amount = expected_return - return_amount`. Fees are then deducted from `return_amount` to get `final_return`. The `spread_amount` is compared against `max_spread`. Because `spread_amount` is computed before fees, it INCLUDES the fee as part of the "spread." The actual price slippage (excluding fees) is smaller than the reported `spread_amount`, making the spread check pass when the real slippage exceeds the user's tolerance.
**Methodology**:
1. In the swap computation, identify where `spread_amount` is calculated relative to fee deduction.
2. If `spread_amount = expected - return_amount` and `return_amount` is PRE-fee, the spread includes fees.
3. Check whether the spread check (`assert_max_spread`) uses this inflated spread or the actual post-fee slippage.
4. The correct approach: compute spread AFTER fees, or compute spread as `expected_return - (return_amount - fees)`.
**Red flags**:
- `spread_amount = offer_amount * exchange_rate - return_amount` where `return_amount` is before fees
- `assert_max_spread(spread_amount, return_amount + spread_amount)` — spread includes fees
- For StableSwap: `spread_amount = offer_amount - return_amount` (1:1 assumption) computed before fees
- Fees are 5-10% but spread check uses 1% tolerance — the fee inflates the spread past the tolerance, so the check effectively allows ~15% real slippage with a 1% setting

## .claude/skills/drozer-lite/checklists/gaming.md

# Gaming / Outcome Determinism Checklist

> Profile: gaming
> Checks: 3
> Source: ported from Drozer-v2 injectable skill OUTCOME_DETERMINISM + universal-invariants.md U7 (timestamp) (provenance cited per check)

## Methodology

Game-like and lottery-like protocols distribute finite prize pools or make time-gated decisions whose outcomes attackers can observe before committing. The core failure modes are (a) pseudo-randomness derivable from on-chain state, (b) finite-pool selection where the depletion fallback reveals the secret, (c) observable default outcomes on time-gated actions that let attackers simulate before acting, and (d) selective-revert callbacks where the winner can force a re-roll by reverting an unfavorable result. Apply adversarial thinking: assume the attacker can simulate the contract at the block they'll be included in, and ask whether they can force any outcome in their favor.

## Checks

### GAME-1: On-Chain Randomness Predictability
**Provenance**: universal-invariants.md U7 + general Solidity RNG failure modes
**Pattern**: "Randomness" is derived from `block.timestamp`, `block.prevrandao`, `blockhash`, or `keccak256` over on-chain state; an attacker simulating the block predicts the outcome and only commits if it favors them.
**Methodology**: For every random-selection path, identify the seed source. Any seed derivable from in-block state is unsafe for value-carrying decisions. Verify commit-reveal with a future block hash, VRF (Chainlink, Pyth Entropy), or cross-transaction entropy. Verify that users cannot observe the seed and cancel.
**Red flags**:
- `uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce)))`
- `blockhash(block.number - 1)` used for selection
- Commit-reveal where the reveal can be skipped when unfavorable

### GAME-2: Finite-Pool Selection & Depletion Fallback
**Provenance**: injectable skill OUTCOME_DETERMINISM (finite-pool selection with depletion fallback)
**Pattern**: A finite prize pool selects items with a fallback when the pool is empty; the attacker depletes the pool to force the fallback outcome. Alternatively, the depletion state is observable before action, letting attackers choose when to commit.
**Methodology**: For every finite-pool selection, enumerate the depletion state and the fallback outcome. Ask whether the attacker can (a) observe the depletion state atomically and skip, (b) intentionally deplete the pool to force the fallback, or (c) time their action around another's commitment. Verify the fallback does not provide a profitable alternative.
**Red flags**:
- `if (remainingPrizes == 0) return consolationPrize;` where consolation is valuable enough to target
- Attacker can call `peek()` view functions to check pool state atomically
- Prize pool re-filled mid-round from an attacker-influenced source

### GAME-3: Time-Gated Actions with Observable Default Outcomes / Selective Callback Revert
**Provenance**: injectable skill OUTCOME_DETERMINISM (time-gated actions + callback selective revert; latter is now always-on in depth templates per skill-index)
**Pattern**: A time-gated action has a default outcome if the user does not act within the window; the attacker observes the would-be outcome and acts only if unfavorable (letting the default apply otherwise). Or: a callback (RNG consumer, settlement callback) can selectively revert on unfavorable outcomes, forcing a re-roll.
**Methodology**: For every time-gated mechanism, identify the default outcome and whether attackers can observe the alternative before acting. For every callback that consumes a random result, verify the callback cannot revert on unfavorable outcomes (use try/catch to absorb reverts, or require the callback to be made by the protocol not the user).
**Red flags**:
- `if (block.timestamp > deadline) { applyDefault(); } else { requireUserAction(); }` where the user knows both outcomes
- RNG consumer contract that reverts in `fulfillRandomWords` when result is unfavorable
- Settlement callback callable by the winning party who can choose to revert

## .claude/skills/drozer-lite/checklists/governance.md

# Governance Checklist

> Profile: governance
> Checks: 6
> Source: ported from Drozer-v2 governance-invariants.md + staking-invariants.md (provenance cited per check)

## Methodology

Governance systems convert economic stake (ve-locks, delegated tokens, NFT membership) into voting power that authorizes fund movement. Attackers try to (a) acquire voting power cheaply (flash loans, bribes, delegation chains), (b) bypass lifecycle constraints (execute before voting ends, cancel legitimate proposals, replay executions), or (c) corrupt aggregate state (slope/bias/totalWeight) via addition without mirroring on removal. For each lifecycle entity (proposal, vote, lock), verify every function checks the entity's state, cannot be called on executed or expired entities, and updates ALL aggregate variables on removal. Build a role-capability matrix: who can propose, vote, queue, execute, cancel, and pause.

## Checks

### GOV-1: Voting Power Integrity (Snapshot vs Live)
**Provenance**: governance-invariants.md G1 + staking-invariants.md S11 (maps to STAKE-11)
**Pattern**: Voting power is read from live balances (flash-loanable) instead of a snapshot taken at proposal creation, or the checkpoint system lets a user double-vote by transferring tokens between addresses.
**Methodology**: For each vote path, verify `getPastVotes(address, snapshotBlock)` is used, not `getVotes(address)`. Verify checkpoints are written atomically on transfer and delegation. Verify no path lets a user vote with the same underlying tokens twice. Verify flash-loaned tokens cannot reach the snapshot block.
**Red flags**:
- `castVote` reads `balanceOf(user)` at current block
- Checkpoint not written in `_beforeTokenTransfer`
- Delegation chain allows circular or multi-hop amplification

### GOV-2: Proposal Lifecycle & Execution Guard
**Provenance**: governance-invariants.md G2 + G13 (maps to STAKE-12 lifecycle state completeness)
**Pattern**: Functions operating on proposals do not check `state(id)`, allowing votes after queuing, fund deposits on executed proposals, or re-execution of executed proposals.
**Methodology**: For every function that accepts a proposal ID, verify it reads `state(id)` or equivalent. Verify `fund()`, `deposit()`, `vote()` cannot be called after queue/execute. Verify `state()` snapshots at transitions rather than re-evaluating live tallies.
**Red flags**:
- `fund(proposalId)` with no state check
- Vote accepted during queued/executed state
- `state()` re-derives from mutable tallies post-queue

### GOV-3: Timelock Security
**Provenance**: governance-invariants.md G4 + G6
**Pattern**: Timelock can be bypassed, its delay can be set to zero, its admin is an EOA, or direct admin calls skip the timelock entirely.
**Methodology**: Verify timelock admin is the governor contract, not an EOA. Verify `setDelay()` itself goes through the timelock. Verify the minimum delay cannot be zero. Verify executed payload hashes match queued payload hashes exactly (no substitution).
**Red flags**:
- `timelock.admin == msg.sender (EOA)`
- `setDelay(0)` allowed
- Executor accepts mismatched targets/values/calldatas from queue

### GOV-4: Aggregate State Consistency on Removal
**Provenance**: governance-invariants.md G11 + staking-invariants.md U27 (maps to STAKE-13 aggregate removal)
**Pattern**: When a nominee/voter/delegate/lock is removed, some aggregates are updated (bias, totalWeight) but not others (slope, changesSum), so future time-weighted extrapolation returns corrupted values.
**Methodology**: For each add/remove function, enumerate every aggregate variable. Verify every aggregate is decremented on removal. For time-weighted aggregates (bias -= slope * time), verify the slope is also corrected. For two-step removal flows (admin + user cleanup), verify the aggregate updates in at least one step, ideally the admin step. Test the zero-aggregate edge case.
**Red flags**:
- `remove()` updates `totalBias` but not `totalSlope`
- Two-step removal where users never complete step 2, leaving inflated aggregate
- Division by zero when all participants removed

### GOV-5: Checkpoint MAX_WEEKS / Loop Coverage
**Provenance**: governance-invariants.md G12 (maps to STAKE-16 MAX_WEEKS coverage)
**Pattern**: A checkpoint loop bounded by `MAX_NUM_WEEKS` is smaller than the maximum lock period divided by `WEEK`, so inactive nominees beyond the window return zero weight (loss of voting power) or the loop exits early with stale state.
**Methodology**: Verify `MAX_NUM_WEEKS >= maxLockPeriod / WEEK` (e.g., 4-year lock needs >= 209 weeks). Verify nominees inactive for > `MAX_NUM_WEEKS` still return correct weight or explicitly return zero with a migration path. Verify permissionless nominee creation cannot spam entries that become stale and waste gas.
**Red flags**:
- `uint256 public constant MAX_NUM_WEEKS = 52;` with 4-year lock support
- `if (weeksPassed > MAX_NUM_WEEKS) return 0` in critical weight calculation

### GOV-6: Exit-Function Balance Manipulation (AC-12 / AC-13)
**Provenance**: governance-invariants.md G14 (maps to AC-12 exit guard + AC-13 balance source)
**Pattern**: `ragequit`/`withdraw` reads `balanceOf(treasury)` as the payout source; MEV searchers sandwich the exit with treasury-draining proposals. Also covers access-control gaps on exit: anyone can trigger another member's exit, or exit is callable outside the member's expected lifecycle window.
**Methodology**: Verify exits use internal accounting, not `balanceOf` live reads. Verify exit-access control restricts callers to the owning member (or an approved delegate). Check whether proposals spending treasury can be timed against exit windows.
**Red flags**:
- `payout = treasury.balanceOf() * shares / totalShares`
- `ragequit(address member)` permissionless and caller unrelated to member
- Time delay between exit request and execution creates a sandwich window

## .claude/skills/drozer-lite/checklists/icp.md

# ICP Canister Checklist

> Profile: icp
> Checks: 16
> Source: ported from Drozer-v2 icp-canister-invariants.md (provenance cited per check)

## Methodology

Internet Computer canisters run under a split-message execution model: every `await` is a commit point. Any state change before an await persists even if the callback traps. Apply adversarial thinking to every `#[update]` method: who can call it (authenticated? anonymous?), what state it commits pre-await, what happens if the callback panics, whether state remains consistent under interleaved concurrent calls, and whether the method is idempotent if retried. Check caller authentication explicitly — Anchor's `Signer<'info>` equivalent on IC is `authenticated_caller()` helpers that reject `Principal::anonymous()`. Also verify canister upgrade resilience: pre_upgrade must not trap, timers must be re-registered in post_upgrade, and stable memory must use MemoryManager isolation.

## Checks

### ICP-1: Caller Principal Validation
**Provenance**: icp-canister-invariants.md IC1
**Pattern**: A `#[update]` method does not verify the caller or accepts `Principal::anonymous()`, allowing unauthenticated state mutation.
**Methodology**: For every `#[update]`, verify it calls a centralized `authenticated_caller()` helper that rejects anonymous. Verify controller-only operations use `is_controller()`. Guard functions cannot access method arguments — argument-dependent auth must be in the method body.
**Red flags**:
- `#[update] fn transfer(to, amount)` with no `caller()` check
- Anonymous principal not rejected explicitly

### ICP-2: Inter-Canister Call Atomicity
**Provenance**: icp-canister-invariants.md IC2
**Pattern**: State is modified before an `await`; the callback traps; the pre-await state persists, creating an orphaned debit or stale lock.
**Methodology**: For every async method, identify pre-await state writes. Verify either they are safe to keep on callback failure OR a compensation handler reverses them. Verify no `unwrap()`/`expect()` after any await (a panic becomes a trap that rolls back only the callback). Use `CallerGuard` with Drop cleanup for value-moving methods.
**Red flags**:
- `balance -= amount; ic_cdk::call(...).await.unwrap();`
- Lock acquired pre-await without Drop-based cleanup
- Pre-await writes with no reverse on Err

### ICP-3: Canister Upgrade Resilience
**Provenance**: icp-canister-invariants.md IC3
**Pattern**: `#[pre_upgrade]` traps on large state, heap state is not persisted, or timers are lost after upgrade.
**Methodology**: Verify `pre_upgrade` either does not exist or cannot trap under any input (bounded serialization, no unwrap). Verify persistent data uses stable memory, not heap. Verify version bytes enable schema migration. Verify `post_upgrade` re-registers all timers and handles schema migration. Test with production-scale data.
**Red flags**:
- `pre_upgrade` that calls `unwrap()` on serialization
- Heap `HashMap` used for persistent balances
- `set_timer_interval` not re-registered in `post_upgrade`

### ICP-4: Stable Memory Isolation
**Provenance**: icp-canister-invariants.md IC4
**Pattern**: Two `StableBTreeMap` / `StableCell` structures share the same `MemoryId`, silently overwriting each other's data; or `static mut` introduces non-exclusive mutable references.
**Methodology**: Verify `MemoryManager` is used and every `StableBTreeMap`/`StableCell`/`StableVec` has a unique `MemoryId`. Verify no raw `stable_read`/`stable_write` overlaps managed regions. Verify no `static mut` global state — use `thread_local!` with `Cell`/`RefCell`.
**Red flags**:
- Two maps initialized with the same `MemoryId`
- `static mut GLOBAL: ...`

### ICP-5: Query Response Authenticity (Certified Data)
**Provenance**: icp-canister-invariants.md IC5
**Pattern**: Financial data (balances, prices, ownership) is returned via a `#[query]` without certification, allowing a single malicious replica to forge the response.
**Methodology**: For every query returning security-critical data, verify certified variables are used. Verify `set_certified_data()` is called in the producing update. Verify frontend consumers check the IC certificate, timestamp (<5 min), and witness. Verify assets are served via the canister's certified endpoint, not `raw.icp0.io`.
**Red flags**:
- `#[query] fn get_balance(user) -> Nat` with no certified data
- Asset canister serving via `raw.icp0.io`

### ICP-6: Cycles Drain Resistance
**Provenance**: icp-canister-invariants.md IC6
**Pattern**: Public endpoints can be spammed to drain cycles — either by Candid space/cycle bombs, unbounded inputs, or expensive HTTPS outcalls.
**Methodology**: Verify `inspect_message` rejects unauthenticated ingress where possible. Verify authentication runs before Candid decoding. Verify variable-size inputs (`Vec`, `String`, `Nat`) have size caps. Verify per-caller rate limiting on expensive paths. Verify `freezing_threshold` is set.
**Red flags**:
- `#[update] fn process(data: Vec<u8>)` with no size cap
- HTTPS outcall endpoint with no authentication

### ICP-7: Untrusted Canister Communication
**Provenance**: icp-canister-invariants.md IC7
**Pattern**: Inter-canister calls to untrusted canisters use unbounded-wait semantics, allowing the callee to stall the caller forever, block upgrades, or send a Candid bomb that traps the callback.
**Methodology**: Verify calls to untrusted canisters use `Call::bounded_wait`. Verify response data is validated. Verify `SYS_UNKNOWN` rejection is handled explicitly. Avoid circular call graphs.
**Red flags**:
- `ic_cdk::call(external, ...).await` with no timeout
- Callback that unwraps untrusted Candid without decoding quota

### ICP-8: Controller Security & Decentralization
**Provenance**: icp-canister-invariants.md IC8
**Pattern**: A single controller key can upgrade, stop, or delete a canister holding user funds with no multi-party approval.
**Methodology**: Document controllers and their trust level. Verify high-value canisters have multi-party controllership or a decentralization path. Verify no single controller can unilaterally alter code.

### ICP-9: Timer & Heartbeat Reliability
**Provenance**: icp-canister-invariants.md IC9
**Pattern**: Timer-based security (oracle updates, expiry checks) is lost on upgrade because timers are not re-registered, or heartbeat cost is unbounded and drains cycles.
**Methodology**: Verify all timers are re-registered in `post_upgrade`. Verify heartbeat per-invocation cost is bounded. Verify callbacks do not hold locks across async boundaries.

### ICP-10: Arithmetic & Precision Safety
**Provenance**: icp-canister-invariants.md IC10
**Pattern**: Release builds with `overflow-checks = false` silently wrap on overflow; financial code uses `f32`/`f64`; division-by-zero traps mid-callback.
**Methodology**: Verify `overflow-checks = true` in `[profile.release]` OR all arithmetic uses `checked_*`/`saturating_*`. Verify no floats in financial calculations — use `rust_decimal` or `num_rational::Ratio`. Verify type casts (`as u64`) are bounds-checked. Verify divisors are checked before division.
**Red flags**:
- `balance += amount` without `checked_add`
- `f64` used for token amounts
- `total / stakers.len() as u64` with no zero check

### ICP-11: HTTPS Outcall Safety
**Provenance**: icp-canister-invariants.md IC11
**Pattern**: POST requests to external APIs are sent by every subnet node (N copies), response headers are non-deterministic causing consensus failure, or API credentials leak to node operators.
**Methodology**: Verify POST/PUT uses idempotency keys. Verify a `transform` function normalizes response headers. Verify API credentials are not embedded in request bodies/headers. Verify HTTPS outcall endpoints are authenticated.

### ICP-12: Candid Type Safety
**Provenance**: icp-canister-invariants.md IC12
**Pattern**: A Candid type like `Vec<Null>` encodes to a tiny payload but decodes to a massive in-memory allocation, spiking cycle usage; or a crafted payload exploits CVE-2023-6245.
**Methodology**: Verify `Vec<T>` / `String` / `Nat` parameters have length or magnitude caps. Verify `candid >= 0.9.10`, `ic-cdk >= 0.16.0`, `ic-stable-structures >= 0.6.4`.

### ICP-13: State Consistency Under Interleaving
**Provenance**: icp-canister-invariants.md IC13
**Pattern**: Two concurrent methods pass the same eligibility check pre-await and both proceed, causing double-spend because neither sees the other's commit.
**Methodology**: For every async method with shared state, verify invariants hold under all message interleavings. Use optimistic update + compensation for pre-await writes. Re-read captured variables after await.

### ICP-14: Idempotency & Deduplication on Retry
**Provenance**: icp-canister-invariants.md IC14
**Pattern**: A bounded-wait call returns `SYS_UNKNOWN`; the caller retries; the callee has already processed the first call, resulting in double execution.
**Methodology**: Verify financial operations use dedup IDs (sequence numbers, memo, nonce). Verify the callee rejects duplicate IDs within a window. Verify retry logic never blindly retries non-idempotent operations.

### ICP-15: Principal-Anchored Resource Accounting
**Provenance**: icp-canister-invariants.md IC1 + IC6 (Rule 12 applied to IC)
**Pattern**: Resource accounting (balances, quotas) is keyed by a user-supplied principal rather than the caller, allowing one user to burn another user's quota by invoking a method on their behalf.
**Methodology**: For every method that reads/writes per-principal state, verify the key is `caller()` or explicitly validated to match. No method should accept a principal parameter unless the caller is authorized to act for that principal.
**Red flags**:
- `#[update] fn claim(principal: Principal)` with no caller-to-principal check
- Quota enforcement keyed by `args.user` rather than `caller()`

### ICP-16: Lock & Guard Drop Correctness
**Provenance**: icp-canister-invariants.md IC2 + IC9
**Pattern**: A lock or guard is acquired before an await; the callback traps; the lock is never released because Drop does not run on trap (only on Err).
**Methodology**: Verify all pre-await locks are inside a `CallerGuard` with `call_on_cleanup` (CDK 0.5.1+), not a plain RAII guard. Verify the cleanup is installed before the await. Audit lock-holding timers.
**Red flags**:
- `let _guard = LOCK.lock();` followed by `.await`
- Lock released in an Err branch only

## .claude/skills/drozer-lite/checklists/lending.md

# Lending Checklist

> Profile: lending
> Checks: 5
> Source: ported from Drozer-v2 lending-invariants.md (provenance cited per check)

## Methodology

Lending protocols hold collateral against debt at a time-varying exchange rate driven by oracles. Attackers manipulate price or interest accounting to either withdraw more than they deposited or force wrongful liquidation of healthy positions. For each borrow/repay/liquidate path, trace: (a) which oracle is read, (b) whether staleness and decimal conversions are correct, (c) whether health-factor checks bracket every collateral-moving path, and (d) whether rounding in interest accrual and liquidation math favors the protocol. Test boundary states explicitly: fresh market, frozen asset, paused underlying, extreme utilization, and dust positions.

## Checks

### LEND-1: Collateralization / Health Factor Bracket
**Provenance**: lending-invariants.md L1
**Pattern**: A collateral-withdrawal, borrow, or collateral-swap path mutates position state without re-checking the health factor afterwards, allowing undercollateralized exits.
**Methodology**: Enumerate every path that reduces collateral OR increases debt. For each, verify a health-factor check occurs AFTER the state mutation (not before). Check that collateral valuation uses current oracle prices with proper decimal handling and the LTV limit for the specific asset.
**Red flags**:
- `withdraw(amount); updatePosition(); // no HF check`
- Borrow path that checks HF against stale cached price
- Collateral swap (oldAsset→newAsset) with HF computed only on old asset

### LEND-2: Interest Accrual Monotonicity & Precision
**Provenance**: lending-invariants.md L2
**Pattern**: Interest index resets, decreases, or loses precision, allowing debt reduction or underflow.
**Methodology**: For each interest-index function, verify the index is strictly non-decreasing under every path. Verify large time gaps do not overflow. Check rounding direction: debt rounds up, supply rounds down.
**Red flags**:
- Interest index reset on pause/unpause
- `accrueInterest()` not called before health-factor read
- Compound math using `divide-before-multiply`

### LEND-3: Liquidation Fairness & Bad Debt Prevention
**Provenance**: lending-invariants.md L3 + L5
**Pattern**: Liquidations fire on healthy positions due to stale oracle prices, or liquidators leave unprofitable dust positions behind that accumulate into bad debt.
**Methodology**: Verify oracle freshness is checked at liquidation time. Verify the liquidation bonus does not push the position into bad debt (bonus must not exceed remaining margin). Verify close-factor limits. Test dust positions: can a liquidator profitably close them? If not, a shortfall / bad-debt socialization mechanism must exist.
**Red flags**:
- `latestRoundData()` used without staleness threshold
- Liquidation penalty > remaining collateral
- No dust-close mechanism for positions below gas cost

### LEND-4: Oracle Integration (Staleness, Decimals, Failure Modes)
**Provenance**: lending-invariants.md L7
**Pattern**: Oracle reads omit staleness, don't handle Chainlink decimals correctly, or lack a fallback for zero/reverting feeds.
**Methodology**: For every oracle read, verify (a) `updatedAt` staleness check against asset-specific heartbeat, (b) decimal conversion between feed and internal math, (c) handling of `price <= 0` and reverts, and (d) at least one fallback source for high-value operations.
**Red flags**:
- `getPrice()` ignores `updatedAt`
- Assumes 18-decimal feed for an 8-decimal Chainlink aggregator
- No circuit breaker on extreme deviations

### LEND-5: Flash-Loan + Price Manipulation on Borrow
**Provenance**: lending-invariants.md L9 + L7
**Pattern**: An attacker flash-loans tokens, manipulates a spot price used for collateral valuation, borrows against the inflated collateral, then repays the flash loan with the borrowed funds.
**Methodology**: For every collateral that can be valued against a manipulable source (Uniswap spot reserves, `balanceOf`, in-protocol AMM), verify the price is derived from TWAP or external oracle. Check whether the same block can host both manipulation and borrow.
**Red flags**:
- Collateral price derived from `pool.getReserves()` directly
- TWAP window < 1 block effective
- No reentrancy guard on borrow path used during a flash-loan callback

## .claude/skills/drozer-lite/checklists/math.md

# Math Checklist

> Profile: math
> Checks: 6
> Source: ported from Drozer-v2 universal-invariants.md U24-U28 + invariant-templates.md MF3 (provenance cited per check)

## Methodology

Numerical code in smart contracts fails in specific, learnable ways: rounding in the wrong direction (dust extraction), division-before-multiplication (precision loss), order-dependent normalization across branches, format-selection asymmetry, and gap-range precision collapse. Attackers look for every place where operations are non-commutative, where formats differ across paths, and where the rounding direction is not documented. When a function implements a mathematical specification, compare the implementation to the reference line-by-line, paying particular attention to shift masking, overflow trapping, and alignment semantics.

## Checks

### MATH-1: Rounding Direction (Protocol-Favorable)
**Provenance**: invariant-templates.md MF-3
**Pattern**: Asset/share math rounds in the user's favor instead of the protocol's, enabling dust extraction via repeated deposit/withdraw cycles.
**Methodology**: For every `mulDiv` and division in asset↔share conversion, verify the rounding direction. Deposits must round shares DOWN (user receives at least this many). Withdrawals must round assets DOWN (user receives at most this many). Fee calculations must round toward the protocol.
**Red flags**:
- `shares = amount * totalSupply / totalAssets` with implicit round-up
- `mulDiv(a, b, c, Math.Rounding.Up)` on a user redemption path
- Symmetric deposit/withdraw rounding (both up or both down)

### MATH-2: Divide-Before-Multiply Precision Loss
**Provenance**: universal-invariants.md U3 (Slither divide-before-multiply)
**Pattern**: `(a / b) * c` loses precision when `a / b` truncates; the correct form is `(a * c) / b`.
**Methodology**: Grep every division followed by multiplication within the same expression tree. Apply Slither's `divide-before-multiply` detector. For each hit, reorder to multiplication-first unless overflow prevents it (use `FullMath.mulDiv`).
**Red flags**:
- `fee = (amount / 10000) * feeRate` where `feeRate < 10000`
- `reward = (balance / periodLength) * duration`

### MATH-3: Multi-Step Normalization Ordering
**Provenance**: universal-invariants.md U24
**Pattern**: Non-commutative adjustments (normalize, halve, round, scale) are applied in different orders across branches of the same function.
**Methodology**: For each multi-step adjustment sequence, list the steps in order for every branch. For each adjacent pair (A, B), ask whether swapping changes the result. If yes, verify the order is consistent across all branches.
**Red flags**:
- Large-value branch: halve-then-adjust; small-value branch: adjust-then-halve
- Pre-halving modification that should have been post-halving

### MATH-4: Format / Precision Selection Consistency
**Provenance**: universal-invariants.md U25 + U26
**Pattern**: A library supports multiple formats (small/large, compressed/full) but the format selector differs across paths — encoding uses more criteria than arithmetic output, for example, so values that qualify for the large format via encoding are downcast by arithmetic.
**Methodology**: Build a FORMAT SELECTION RULE TABLE per path. Verify all rows are identical. Construct boundary values that expose the asymmetry and trace them through each path. Verify `encode(decode(encode(x))) == encode(x)`.
**Red flags**:
- Encoding uses [digit count, exponent]; arithmetic output uses [exponent] only
- Boundary values where decode loses information

### MATH-5: Representation Gap Integrity
**Provenance**: universal-invariants.md U26
**Pattern**: Values "in the gap" between small and large representations are silently truncated beyond stated tolerance; the loss compounds when two gap values are multiplied.
**Methodology**: Identify the gap range from the format selector's criteria. Measure precision loss for values in the gap. Verify it stays within any stated tolerance (e.g., "1 ULP accuracy"). Test that `gap_value * gap_value` does not exceed acceptable error.
**Red flags**:
- Selector checks exponent only, precision depends on digit count
- No explicit tolerance specification in code or spec
- Gap-value multiplication in a hot path

### MATH-6: Aggregate Removal & Stale Snapshot
**Provenance**: universal-invariants.md U27 + U28
**Pattern**: Aggregate state variables (totalWeight, totalSlope, sumBias, totalSupply) are decremented on addition but not on removal, or a function copies a storage array to memory before mutating the storage array and then uses the stale copy for arithmetic.
**Methodology**: For each aggregate variable, verify every removal path decrements it. For time-weighted aggregates, verify slope corrections. For every function that copies storage to memory then mutates storage, verify subsequent reads use the current storage, not the stale copy. Test swap-and-pop cases where the pre-eviction index no longer maps to the same element.
**Red flags**:
- `remove()` updates `bias` but not `slope`
- `uint256[] memory cache = storageArray; evict(); cache[i]` used after eviction
- Swap-and-pop last-element case that leaves a stale companion mapping entry

## .claude/skills/drozer-lite/checklists/oracle.md

# Oracle Checklist

> Profile: oracle
> Checks: 3
> Source: ported from Drozer-v2 invariant-templates.md OR1-5 + lending-invariants.md L7 (provenance cited per check)

## Methodology

Any contract that reads an external price feed inherits all of that feed's failure modes: staleness, deviation, zero/negative prices, circuit-breaker halts, decimal mismatch, and outright reverts. Attackers manipulate prices via flash loans (spot reserves) or wait out heartbeats (Chainlink). Every oracle consumer must (a) validate freshness against the feed's asset-specific heartbeat, (b) convert decimals to the contract's internal math, (c) handle `price <= 0` and feed reverts, and (d) degrade gracefully rather than brick permanently. If multiple functions read the same feed, they must all use the same staleness threshold and fallback.

## Checks

### ORACLE-1: Staleness Protection (Per-Asset Heartbeat)
**Provenance**: invariant-templates.md OR-1 + lending-invariants.md L7
**Pattern**: Oracle price reads omit a staleness check or use a one-size-fits-all threshold that is too loose for volatile assets and too tight for stable ones.
**Methodology**: For every oracle read, verify `require(updatedAt >= block.timestamp - heartbeat)` where `heartbeat` is the asset-specific value (Chainlink publishes per-feed heartbeats). Verify the same heartbeat is used by every reader of the same feed (see ORACLE-3). Verify `updatedAt != 0` to reject uninitialized feeds.
**Red flags**:
- `(, int256 price, , , ) = feed.latestRoundData()` with no `updatedAt` usage
- Single `MAX_STALENESS` constant applied to a mix of ETH (20m) and stablecoin (24h) feeds
- No lower bound on `updatedAt`

### ORACLE-2: Manipulation Resistance & Graceful Degradation
**Provenance**: invariant-templates.md OR-2 + OR-3 + OR-4
**Pattern**: A price used for liquidation, borrowing, or minting is derived from instantaneous on-chain state (spot reserves, `balanceOf`) and can be flash-loan-manipulated in a single block; or a single oracle failure bricks the protocol; or a stale/zero feed is silently accepted.
**Methodology**: For every security-critical price, verify the source is TWAP, Chainlink, or another time-weighted mechanism. Verify the protocol pauses (not reverts permanently) on oracle failure. For high-value operations, verify prices are cross-checked against a second source OR bounded by a circuit breaker.
**Red flags**:
- `getPrice() = reserve1 * 1e18 / reserve0` for a liquidation decision
- Hard revert with no admin pause path if the feed returns zero
- Single-source pricing for >$10M positions
- `price <= 0` not explicitly handled

### ORACLE-3: Feed Consistency Across Readers (Decimal & Threshold Uniformity)
**Provenance**: invariant-templates.md OR-5 + lending-invariants.md L7
**Pattern**: Multiple functions read the same oracle feed but apply different staleness thresholds, different decimal conversions, or different fallbacks, producing inconsistent behavior (one function accepts a price another rejects).
**Methodology**: For each oracle feed, identify every reader. Build a reader matrix: [function, staleness threshold, decimal conversion, fallback]. Verify all rows are identical (or the differences are explicitly justified). Verify decimal conversion matches the feed (Chainlink returns 8 or 18 depending on the feed; do not assume 18).
**Red flags**:
- `getPrice()` uses 1h staleness but `liquidate()` reads the same feed with 24h
- `getPrice()` converts from 8 decimals while `isSolvent()` assumes 18
- One function falls back to secondary, another reverts

## .claude/skills/drozer-lite/checklists/reentrancy.md

# Reentrancy Checklist

> Profile: reentrancy
> Checks: 5
> Source: ported from Drozer-v2 invariant-templates.md RE1-4 + universal-invariants.md U4 + amm-invariants.md A6 (provenance cited per check)

## Methodology

Reentrancy is the oldest class of smart-contract exploit and remains a top cause of fund loss. The core question is: during any external call, can control return to the contract (or a cousin contract sharing state) before the current function's state updates are complete? Apply CEI as a checklist, but do not trust it as a guarantee — read-only reentrancy and cross-contract reentrancy can bypass a correct CEI function. For every `call`, `send`, `transfer`, token operation, ERC777/ERC721 callback, flash-loan callback, and external interface call, ask who can gain execution control and what state they can observe or modify.

## Checks

### RE-1: CEI Pattern Violation (Classic Reentrancy)
**Provenance**: invariant-templates.md RE-1 + universal-invariants.md U4
**Pattern**: External call occurs before state updates; an attacker re-enters during the call and observes stale state that permits double-spend or double-withdraw.
**Methodology**: For every function that makes an external call, enumerate state reads and writes. Confirm all writes affecting subsequent guards occur BEFORE the external call.
**Red flags**:
- `token.transfer(to, amount); balances[msg.sender] -= amount;`
- Withdraw path updating balance after `.call` succeeds
- Token minting before external callback

### RE-2: Guard Coverage Gap (Missing nonReentrant)
**Provenance**: invariant-templates.md RE-2
**Pattern**: A function that both modifies state and makes external calls lacks a reentrancy guard, either because the author believed CEI was enough or forgot the modifier.
**Methodology**: For every external-facing state-modifying function that makes external calls, require either `nonReentrant` or structural CEI proof. Prefer belt-and-suspenders (both) on any value-moving path.
**Red flags**:
- `function deposit() external payable { ... }` with no `nonReentrant` but calls a user-supplied hook
- Payable receive function with state writes

### RE-3: Cross-Function Reentrancy
**Provenance**: invariant-templates.md RE-3
**Pattern**: Function A has `nonReentrant`; function B (shares state) does not, so reentering via B during A's external call bypasses the guard.
**Methodology**: Group functions by shared state. Verify every function in a group has reentrancy protection, or structurally cannot observe mid-A state. Watch for view functions used in other contracts' logic (read-only reentrancy).
**Red flags**:
- `deposit` is `nonReentrant`, `balanceOf` is not, another contract calls `balanceOf` during `deposit`'s callback
- Vault share accounting function not guarded while `withdraw` is

### RE-4: Read-After-Call for Security Decisions
**Provenance**: invariant-templates.md RE-4
**Pattern**: A variable read after an external call is used for access control, balance checking, or amount calculation. The callee can manipulate that state during the call.
**Methodology**: For every external call, audit what is read after. Any security-critical read after an external call must either be re-validated or moved before the call.
**Red flags**:
- `balance = balanceOf(user); target.call(...); if (balance > X) { ... }` (but balance was captured before call) — worse: re-read after call
- `require(owner == msg.sender)` read after an untrusted call

### RE-5: Flash Loan / Flash Swap Callback Reentrancy
**Provenance**: amm-invariants.md A6 + perp-invariants.md P4
**Pattern**: Flash-loan callback re-enters the flash-loan contract, the pool, or a downstream protocol to exploit an intermediate state. Also covers liquidation callbacks that allow the liquidated user to re-enter.
**Methodology**: For every flash-loan, flash-swap, or liquidation with a user-controlled callback, verify the invariant (k, repayment, collateral ratio) is checked AFTER the callback and that the callback cannot call back into the flash function or related state-modifying functions.
**Red flags**:
- `flash` entry function not `nonReentrant`
- Callback runs before the pre-callback balance check is cached
- Liquidation bonus paid before the debt repayment is finalized

## .claude/skills/drozer-lite/checklists/signature.md

# Signature Checklist

> Profile: signature
> Checks: 4
> Source: ported from Drozer-v2 analyses (provenance cited per check)

## Methodology

Signatures authorize specific actions. An attacker controls what is NOT in the digest. For every ecrecover / EIP-712 / permit / isValidSignature call site, build a SIGNED DATA BINDING TABLE: list every field the function uses post-verification, mark each as signed (YES/NO), and mark each as caller-controllable (YES/NO). Every (Signed=NO, Used=YES, Caller-controlled=YES) row is a finding. Always check domain separation (chainId, verifyingContract), nonce/replay, and recipient/target binding. Attackers will front-run permit signatures from the mempool and reuse them in different execution contexts.

## Checks

### SIG-1: Digest Coverage Inventory (Unsigned Execution-Affecting Fields)
**Provenance**: signed-data-completeness.md §1 (maps to original AC-16 / APPROVAL-4)
**Pattern**: A function verifies a signature over a digest but decisions made after verification use fields that are not part of the signed bytes. Unsigned metadata (deadline, mode flags, gas parameters, callback data, recipients, amounts) can be substituted by a relayer, bundler, or MEV searcher without invalidating the signature.
**Methodology**: For every `ecrecover`, `ECDSA.recover`, `_hashTypedDataV4`, `SignatureChecker.isValidSignatureNow`, and any custom verification, identify the exact bytes being hashed. Build the SIGNED DATA BINDING TABLE. For every field used after verification, check whether it is inside the hashed payload. For each unsigned-but-used field, ask whether a relayer/bundler/MEV searcher can choose or modify it. Common unsigned fields: validity window, execution mode flags, gas parameters, callback data, auxiliary metadata.
**Red flags**:
- `deadline` read after verification but not in `_hashTypedDataV4` struct
- `callGasLimit`/`verificationGasLimit` consumed by executor but absent from the digest
- Oracle timestamp passed alongside but not inside the signed attestation
- ERC-4337 UserOp fields (paymasterAndData, maxFeePerGas) not part of `userOpHash`

### SIG-2: Domain Separation (Cross-Chain / Cross-Contract Replay)
**Provenance**: signed-data-completeness.md §2 (maps to original SEM-14 — was SOL-14 in solidity-semantics)
**Pattern**: Signatures are replayable across chains or across sibling contracts because the EIP-712 domain separator is missing, incomplete, or hardcoded.
**Methodology**: For every EIP-712 construction, verify the domain includes `chainId` AND `verifyingContract`. Verify the domain separator is recomputed (or cached with a chainId guard) to survive hard forks. For each `typeHash`, verify different operation types use distinct struct hashes to prevent operation confusion.
**Red flags**:
- Hardcoded `DOMAIN_SEPARATOR` without `block.chainid` recompute
- Missing `verifyingContract` in the EIP-712 domain struct
- Shared `typeHash` across unrelated operations
- Multi-contract system where a signature for contract A is accepted by contract B with identical code

### SIG-3: Recipient / Target Binding (Metadata & Beneficiary)
**Provenance**: signed-data-completeness.md §3 + §5 (maps to original DEFI-48)
**Pattern**: A signed message authorizes a value transfer or approval, but the recipient, target contract, or beneficiary is not in the signed payload. An intermediary substitutes the recipient to redirect value.
**Methodology**: For every signed operation that transfers or approves value, verify the recipient/spender/target address is part of the digest. For meta-transactions and account abstraction, verify the target contract is signed. For multi-hop operations, verify the FINAL recipient (not just the next hop) is bound. For `msg.sender`-dependent paths, verify the relayer cannot substitute themselves for the signer.
**Red flags**:
- `permit` with unsigned `spender`
- Meta-tx where `to` in the outer call is chosen by the relayer
- Bridge payload where `destination` is signed but `recipient` is not
- Account abstraction `beneficiary` in `handleOps` substituted by bundler

### SIG-4: Nonce & Replay Prevention Atomicity
**Provenance**: signed-data-completeness.md §4 (maps to original SEM-14 variant tracking nonce semantics)
**Pattern**: Signed messages can be replayed because the nonce is missing, not part of the digest, incremented non-atomically, or scoped too broadly (2D nonce key reused across operation types).
**Methodology**: Verify every signature mechanism has a nonce or unique identifier. Verify the nonce is part of the signed bytes (not just compared separately). Verify the nonce increments in the same transaction as verification (no gap). For 2D nonce schemes, verify keys cannot collide across operation types. For deadline-only replay protection, verify the window is tight AND the operation is idempotent.
**Red flags**:
- Nonce checked but not hashed into the digest
- `nonce++` in a different transaction than `ecrecover`
- 2D nonce key that is meaningful for op A but arbitrary for op B
- Batch operation with one nonce gating N independent items

## .claude/skills/drozer-lite/checklists/solana.md

# Solana / Anchor Checklist

> Profile: solana
> Checks: 12
> Source: ported from Drozer-v2 anchor-invariants.md (provenance cited per check)

## Methodology

Solana programs fail in ways Ethereum auditors do not always anticipate: account substitution (type cosplay), missing signer checks, PDA seed collisions, arbitrary CPI, and account-close-then-revive attacks. Apply adversarial thinking at the instruction level: for every `#[derive(Accounts)]` struct, ask what an attacker can substitute, what the Anchor constraints actually enforce, and what assumptions the handler makes that are not verified by those constraints. Prefer `Account<'info, T>` over `AccountInfo` / `UncheckedAccount`. Every `invoke`/`invoke_signed` is a trust boundary: verify the target program, the seeds, and the post-CPI state.

## Checks

### SOL-1: Account Ownership Integrity
**Provenance**: anchor-invariants.md SA1
**Pattern**: An `AccountInfo` or `UncheckedAccount` parameter has no explicit owner check; an attacker substitutes a fake account owned by a malicious program with identical byte layout.
**Methodology**: For every raw `AccountInfo` / `UncheckedAccount`, verify an explicit `account.owner == expected_program_id` check. Prefer typed `Account<'info, T>`. Verify every `/// CHECK:` is documented with the manual validation performed.
**Red flags**:
- `UncheckedAccount<'info>` with no owner check in the handler
- Type-cosplay: same layout, different semantics

### SOL-2: Signer Authorization
**Provenance**: anchor-invariants.md SA2
**Pattern**: Authority/admin accounts are not declared as `Signer<'info>` and the handler does not check `is_signer`, allowing unauthorized callers to invoke privileged operations.
**Methodology**: For every privileged parameter, verify it is `Signer<'info>` OR `#[account(signer)]` OR the handler checks `is_signer`. For PDA-signed CPIs, verify seeds are complete and correct. Verify no instruction can modify authority fields without the current authority signing.

### SOL-3: PDA Derivation Correctness (Canonical Bump & Seed Prefix)
**Provenance**: anchor-invariants.md SA3
**Pattern**: PDA seeds are not length-prefixed and collide (`["ab","c"] == ["a","bc"]`), or a non-canonical bump is accepted, or seeds lack user-specific data allowing cross-user PDA access.
**Methodology**: Verify `find_program_address` is used for canonical bumps. Verify stored bumps match canonical bumps. Verify seeds include user pubkey where user-specific. Verify variable-length seeds are separated.

### SOL-4: CPI Safety
**Provenance**: anchor-invariants.md SA4
**Pattern**: A cross-program invocation targets a program ID that is attacker-controlled, or `invoke_signed` seeds are manipulable, or CPI return values are ignored.
**Methodology**: Verify CPI targets are hardcoded or verified against constants. Verify signer seeds cannot be crafted to sign for unintended PDAs. Verify CPI results are unwrapped. Verify post-CPI state (token balances, account data) is checked.
**Red flags**:
- `invoke(target, ...)` where `target` is from instruction data
- CPI return value discarded

### SOL-5: Token Account Integrity
**Provenance**: anchor-invariants.md SA5
**Pattern**: An SPL token account has no mint/owner/program verification; an attacker passes a worthless token account and receives valuable tokens.
**Methodology**: For every TokenAccount, verify mint matches expected mint, owner matches expected owner, and the program ID is the SPL Token program. For ATAs, verify derivation.
**Red flags**:
- `TokenAccount<'info>` with no constraint on mint or owner
- Generic `AccountInfo` used as a token account with no checks

### SOL-6: Account Closure Safety
**Provenance**: anchor-invariants.md SA6
**Pattern**: An account is closed (lamports drained) but data is not zeroed; attackers revive and re-read stale data, or re-init with attacker authority.
**Methodology**: Verify account data is zeroed before lamports are moved. Verify the discriminator is cleared. Verify same-transaction revival is impossible. Prefer Anchor's `close = ...` to manual drains.

### SOL-7: Initialization Idempotency
**Provenance**: anchor-invariants.md SA7
**Pattern**: A program's `initialize` can be called multiple times or races with the legitimate initialization, re-setting authority to the attacker.
**Methodology**: Prefer Anchor `init` constraint over `init_if_needed`. For manual init, verify an `is_initialized` flag. Verify init parameters cannot be changed after first init.

### SOL-8: Arithmetic Soundness
**Provenance**: anchor-invariants.md SA8
**Pattern**: Release builds without `overflow-checks` wrap silently; casts like `as u64` truncate high bits; division by zero causes a transaction DoS.
**Methodology**: Verify `overflow-checks = true` in `[profile.release]` OR all arithmetic uses `checked_*`/`saturating_*`. Verify casts are bounds-checked. Verify divisors are non-zero. Verify rounding favors the protocol.

### SOL-9: Duplicate Account Prevention
**Provenance**: anchor-invariants.md SA9
**Pattern**: An instruction takes two mutable accounts of the same type; the attacker passes the same account for both, allowing read-then-double-credit exploits.
**Methodology**: For every instruction with 2+ mutable `Account<>` of the same type, verify `require_keys_neq!` or equivalent constraint. Audit `remaining_accounts` loops for duplicate handling.
**Red flags**:
- `source: Account<Vault>, destination: Account<Vault>` with no distinctness check

### SOL-10: Rent Exemption
**Provenance**: anchor-invariants.md SA10
**Pattern**: An account is created with insufficient lamports for rent exemption and is garbage-collected, losing its data.
**Methodology**: Verify account creation uses `Rent::get()?.minimum_balance(data_len)`. Verify reallocs maintain rent exemption at the new size. Audit any direct lamport manipulation.

### SOL-11: Timestamp / Clock Safety
**Provenance**: anchor-invariants.md SA11
**Pattern**: Tight time-dependent logic is vulnerable to validator timestamp drift; slot-skipping causes unexpected gaps.
**Methodology**: Verify deadlines have tolerance windows. Prefer slot height for deterministic timing. Avoid using `unix_timestamp` for randomness or tight windows.

### SOL-12: Error Handling Completeness
**Provenance**: anchor-invariants.md SA12
**Pattern**: `unwrap()` on user input causes DoS; error paths return `Ok(())` silently after a failed check; CPI errors are caught and ignored.
**Methodology**: Audit every `unwrap()` on fallible operations. Prefer `?`. Verify no `Ok(())` is returned after a failed check. Verify CPI errors are propagated.
**Red flags**:
- `ctx.accounts.mint.decimals.unwrap()` on user input
- `if let Err(_) = cpi_call { } else { ... }` with no error propagation

## .claude/skills/drozer-lite/checklists/stableswap.md

# StableSwap Checklist

> Profile: stableswap
> Checks: 5
> Source: drozer-lite v0.4.2 — derived from Curve StableSwap implementation patterns and real-audit findings on StableSwap forks. These checks apply to any protocol implementing the Curve StableSwap invariant (An∑xi + D = ADⁿ + Dⁿ⁺¹/(nⁿ∏xi)).

## Methodology

StableSwap pools maintain a hybrid invariant between constant-sum (x+y=k) and constant-product (xy=k), controlled by an amplification parameter A. Correct implementations must: (a) normalize all token amounts to a common decimal base before computing the invariant D, (b) include ALL pool tokens in the invariant computation (not just the swap pair), (c) charge fees on imbalanced deposits proportional to the skew introduced, (d) handle Newton-Raphson non-convergence as an error rather than returning a wrong result, and (e) allow the amplification parameter to be adjusted over time to respond to market conditions. Compare the implementation against the Curve reference line by line, paying particular attention to how many tokens participate in D/y computation and whether decimal scaling is consistent across swap and LP paths.

## Detection Keywords

Auto-load this profile when **3 or more** of these keywords match (case-insensitive):

`StableSwap`, `amp`, `amplification`, `compute_d`, `compute_y`, `newton`, `invariant.*D`, `stableswap_y`, `n_coins.*ann`, `D_prod`, `amp_factor`

## Checks

### SS-1: Decimal Normalization Inconsistency Between Swap and LP Paths
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the swap path normalizes token amounts to a common decimal base before computing the StableSwap invariant, but the LP minting path uses raw (unnormalized) amounts, producing a different D value and incorrect share calculations for tokens with different decimals.
**Pattern**: A StableSwap implementation has two code paths that compute the invariant D: one for swaps (which normalizes via `decimal_with_precision` or rate multipliers) and one for LP minting/withdrawal (which sums raw amounts). When tokens have different decimals (e.g., 6 vs 18), the LP path computes a D that is dominated by the higher-decimal token, granting disproportionate shares to depositors of that token.
**Methodology**:
1. Identify every call site that computes the StableSwap invariant D.
2. For each call site, check whether token amounts are normalized to a common decimal base BEFORE being passed to the D computation.
3. Compare the normalization logic between the swap path and the LP mint path. If they differ, flag.
4. Test with two tokens of different decimals (e.g., 6 and 18): deposit equal-value amounts via both paths and compare the D values.
**Red flags**:
- Swap path: `offer_pool = Decimal256::decimal_with_precision(amount, precision)` — normalized
- LP path: `sum_x = deposits.iter().fold(zero, |acc, x| acc + x.amount)` — raw amounts, no normalization
- D computation for LP uses raw `Uint128` amounts while swap uses `Decimal256` with precision
- Two tokens with 6 and 18 decimals: depositing 1e6 USDC and 1e18 DAI produces wildly different D vs depositing 1e18 USDC and 1e6 DAI — but both should be equivalent in value

### SS-2: Multi-Token Invariant Uses Only Swap Pair (Disjoint Computation)
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a StableSwap pool with 3+ tokens computes the invariant D and the swap output y using only the offer and ask token balances, ignoring the other pool tokens. The Curve invariant requires ALL token balances to compute D correctly.
**Pattern**: The StableSwap invariant D is defined over ALL N tokens: `An∑(all xi) + D = ADⁿ + Dⁿ⁺¹/(nⁿ∏(all xi))`. When computing a swap between token A and token B in a 3-token pool, the implementation passes only `(offer_pool, ask_pool)` to the D/y computation, but uses `n_coins = 3`. This produces an incorrect D because the sum and product only include 2 of the 3 token balances, while the exponent uses N=3. Swaps between different pairs in the same pool preserve different (incorrect) invariants, creating arbitrage opportunities.
**Methodology**:
1. For every StableSwap swap computation, check how many token balances are passed to the D/y computation function.
2. If the function receives only the offer and ask balances (2 tokens) but `n_coins` reflects the actual pool size (3+), flag as HIGH.
3. Compare against Curve reference: the `get_y` function iterates over ALL pool balances except the target token.
4. Test: in a 3-token pool, check if A-B swaps produce different slippage than B-C swaps with identical pool composition — they should be equivalent in a correct implementation.
**Red flags**:
- `compute_swap(n_coins=3, offer_pool, ask_pool, ...)` but D is computed from only `offer_pool + ask_pool`
- `calculate_stableswap_d(offer_pool, ask_pool)` — sum uses 2 values but `ann = amp * n_coins` uses 3
- The `pool_sum` or `sum_pools` variable only includes 2 token balances
- D/y functions accept only 2 pool amounts as parameters despite being called for pools with 3+ tokens
- Different token-pair swaps in the same pool produce inconsistent pricing

### SS-3: Missing Imbalanced Deposit Fee
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a StableSwap pool allows liquidity deposits at any ratio without charging a fee proportional to the imbalance (skew) introduced. In Curve's implementation, depositing in a ratio that deviates from the pool's current ratio incurs a fee equal to the swap fee on the "difference" between the ideal and actual deposit. Without this fee, users can skew the pool for free, manipulating the price at minimal cost.
**Pattern**: The LP minting function computes shares as `total_supply * (D1 - D0) / D0` where D1 includes the new deposits and D0 is the pre-deposit invariant. Curve additionally computes per-token ideal balances (`ideal_balance = D1 * old_balance / D0`) and charges a fee on the `|ideal_balance - new_balance|` for each token. This fee is missing in the implementation — users can deposit one-sided or skewed liquidity without penalty.
**Methodology**:
1. In the LP minting function for StableSwap, check whether any fee is charged based on the imbalance of the deposit.
2. If the only calculation is `shares = total_supply * (D1 - D0) / D0` with no per-token fee computation, flag.
3. Test: deposit a large one-sided amount (e.g., 2x of token A, 0 of token B). Compare the cost (shares received / value deposited) against a balanced deposit. In a correct implementation, the one-sided deposit should receive fewer shares due to the imbalance fee.
**Red flags**:
- `compute_lp_mint_amount = total_supply * (D1 - D0) / D0` — no per-token fee calculation
- No `ideal_balance`, `difference`, or `dynamic_fee` computation anywhere in the LP mint path
- One-sided deposit of 2x tokenA costs less than 0.5% in slippage — should cost at least the swap fee (e.g., 3-5%) on the skewed portion
- A user can skew the pool ratio dramatically with a deposit, then withdraw balanced, capturing value from other LPs

### SS-4: Newton-Raphson Non-Convergence Returns Result Instead of Error
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the Newton-Raphson iterative solver for the StableSwap invariant D or pool balance y returns the last computed value even when the iteration limit is reached without convergence. A non-converged result is mathematically incorrect and produces wrong swap prices.
**Pattern**: The D or y computation uses a loop with a fixed iteration cap (e.g., 32, 256, 1000). On each iteration, it checks if `|current - previous| <= 1`. If the loop completes without converging, the function returns the last value instead of an error. In a correct implementation (Curve reference), non-convergence raises an error and the swap/deposit fails — only withdrawals remain functional, protecting LPs.
**Methodology**:
1. For every Newton-Raphson loop, check what happens after the loop ends WITHOUT convergence (i.e., the break condition was never met).
2. If the function returns `Some(last_value)` or `Ok(last_value)` after the loop, flag. It should return `None`, `Err(ConvergeError)`, or equivalent.
3. Check whether there are two D computation functions with different iteration caps (e.g., 32 for swaps, 256 for LP) — inconsistency flag per MATH-4.
4. Test with extremely imbalanced pools where convergence is slow — the function will return a wrong value instead of failing.
**Red flags**:
- Loop `for _ in 0..N { ... if converged { break; } }` followed by `Some(d)` outside the loop — always returns even if not converged
- No `return` or early exit on convergence — the `break` exits the loop and falls through to a successful return
- Correct pattern: convergence should `return Some(d)` inside the loop; after the loop, return `None` or `Err`
- Curve reference: `raise` after the loop (Python); the function never returns normally without convergence

### SS-5: Static Amplification Parameter (No Ramping Mechanism)
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the StableSwap amplification parameter A is set once at pool creation and cannot be modified afterward. Curve's implementation includes a time-weighted ramping mechanism (`ramp_A` / `stop_ramp_A`) that allows the protocol to adjust A over time in response to market conditions. Without ramping, pools cannot adapt to depegging events and can leak value.
**Pattern**: The amplification parameter is stored as a static field in the pool configuration (e.g., `PoolType::StableSwap { amp: u64 }`). No `update_amp`, `ramp_A`, or similar function exists to modify it post-creation. During normal conditions, the pool works fine. During a depegging event (one stablecoin loses its peg), a high A value keeps the price artificially stable, allowing holders of the depegged asset to swap at near-1:1 rates and drain the pool of the healthy asset.
**Methodology**:
1. Check whether the amplification parameter can be modified after pool creation. Search for `ramp`, `update_amp`, `set_amp`, `modify_amp` in the execute message enum and handler.
2. If no modification mechanism exists, flag as MEDIUM — the pool cannot adapt to market conditions.
3. Check whether pool parameters are generally immutable post-creation (intentional design) or whether other parameters can be updated.
4. Assess the severity: if the DEX is designed for stablecoin pairs only, this is higher severity (depegging is the primary risk). If it supports volatile pairs via StableSwap (unusual), severity is lower.
**Red flags**:
- `PoolType::StableSwap { amp: u64 }` — static field, no update path
- No `ExecuteMsg::RampAmp` or `ExecuteMsg::UpdatePoolParams` in the message enum
- Pool fees can be set at creation but amp cannot be adjusted — asymmetric mutability
- Documentation mentions "stable assets" or "pegged assets" but no depeg protection mechanism
- Contrast with Curve: `ramp_A(future_A, future_time)` with `MIN_RAMP_TIME` safety constraint

## .claude/skills/drozer-lite/checklists/universal.md

# Universal Checklist

> Profile: universal
> Checks: 110
> Source: ported from Drozer-v2 analyses (provenance cited per check). UNI-96..98 added in v0.3.1, UNI-99..106 in v0.4.1, UNI-107..110 in v0.4.2. All checks describe generic class-of-bug patterns.

## Table of Contents

- UNI-1: Missing / Incorrect Access Control
- UNI-2: State Machine / Lifecycle Bypass
- UNI-3: Classic Reentrancy
- UNI-4: Cross-Function / Read-Only Reentrancy
- UNI-5: Missing Zero-Address / Zero-Amount Checks
- UNI-6: Integer Overflow / Underflow in Unchecked Blocks
- UNI-7: Unchecked External Call Return Values
- UNI-8: Controlled Delegatecall
- UNI-9: Upgrade / Initialization Safety
- UNI-10: Timestamp Dependence
- UNI-11: Missing Event Emission
- UNI-12: Unbounded Loops
- UNI-13: Unvalidated `from` in transferFrom
- UNI-14: Loop External-Call Fragility
- UNI-15: Weird-Token Incompatibility
- UNI-16: Array Boundary Edge Cases
- UNI-17: Post-Commitment State Mutation
- UNI-18: Uninitialized-State Guard Bypass
- UNI-19..28: Temporal, Cross-Environment, Derived-Value, Work-Reward, Identifier, Spec, Error-State, Normalization, Format, Representation
- UNI-29..34: Aggregate Removal, Stale-Snapshot, Prerequisite Update, Last-Element, Permissionless Privilege, Token Compatibility
- UNI-35..38: Role Separation, Cross-Contract ACL, Timelock Scope, Emergency Exit
- UNI-39..47: Monotonic State, Past-Epoch, Cooldown, Deadline, Sequence, Bounded Iteration, Array Growth, Loop External, Bounded Cleanup
- UNI-48..54: Rounding Direction, Donation Corruption, First-Depositor, Atomic Transfer, Solvency, No Free Extraction, Fee Bounds
- UNI-55..60: Storage Slot, Init Completeness, encodePacked, Signed Data, Nonce/Replay, Taint Boundary
- UNI-61..65: Oracle Staleness, Manipulation-Resistant, Graceful Degradation, Multi-Source, Feed Consistency
- UNI-66..71: Parameter Scope, Retroactive Calc, Locked-Position, Timing-Adversary, Cross-Function Consistency, Boundary Safety
- UNI-72..80: Privilege Enumeration, Operation Blocking, Irreversible Admin, Ownership Two-Step, Router Permissionless, Approval Persistence, Permit Frontrun, Router Identity, Router Residual
- UNI-81..98: Compound-Fork, Supply-Cap DoS, Redemption DoS, Empty-Market, External Reward, Partial Redemption, Bad-Debt, Skip/Disable, FCFS, Negative-Yield, Yield-Leakage, Emergency Input, Before/After Balance, Irreversible Config, ERC-165, Precision Loss, Auto-Route Balance
- UNI-99..106: Approval Persistence After Reversal, Asymmetric Settlement, Destructive Without Obligation, Heterogeneous Collection, Payment-Gated Transfer, Numeric Type Width, Stored Constraint Unenforced, Listing-Gate Bypass
- UNI-107..110: Nested Loop Gas DoS, Temporal Past-Value, Self-Call Identity Confusion, Permissionless Fee Bypass

## Methodology

Apply adversarially. For every storage variable, every external-facing function, and every privileged action, ask: who can call it, what state it reads, what state it writes, and what assumptions the surrounding code makes about that state. Trace actual execution paths rather than documented intent. When an invariant is stated in docs, attempt to construct a sequence of calls that breaks it. Prefer evidence from code traces (E3) over pattern matches (E2). Treat every `unchecked`, every external call, every admin setter, and every boundary value (0, 1, type(X).max, array.length == 0/1) as a suspect until proven safe.

## Checks

### UNI-1: Missing / Incorrect Access Control
**Provenance**: universal-invariants.md U1 + invariant-templates.md AC-1
**Pattern**: State-changing functions lack modifiers, use the wrong modifier, or rely on a single role that can be self-granted or front-run during initialization.
**Methodology**: Enumerate every `external`/`public` non-view function. For each, record the authorization path (modifier, inline `require`, or none). Cross-check that admin/owner setters are reachable only by the intended role. Look for `initialize()` without `initializer` guard, role granters that let a role add itself, and emergency functions with weaker protection than the normal path.
**Red flags**:
- Missing `onlyOwner`/`onlyRole(...)`/`onlyGovernor` on state-changing function
- `initialize()` callable multiple times or front-runnable
- Role-admin == role-holder allowing self-grant
- `_setupRole` after deployment without access check

### UNI-2: State Machine / Lifecycle Bypass
**Provenance**: universal-invariants.md U2 + U15 + U16
**Pattern**: Functions operating on lifecycle entities (proposal, position, order) do not check the entity's current state, allowing operations on executed, cancelled, expired, or uninitialized entities.
**Methodology**: For each entity with a lifecycle (created→populated→finalized→consumed), map every function that accepts its ID. Verify each call site either reads a `status` field or asserts preconditions. Flag any function that mutates state without validating the lifecycle position. Check zero/default values do not satisfy "initialized" guards.
**Red flags**:
- `fund()`/`deposit()`/`transfer()` callable on executed proposals
- No `require(state == Active)` on lifecycle-sensitive functions
- Sentinel field defaults (`timestamp == 0`) satisfying `block.timestamp - stored > PERIOD`
- Re-initialization overwriting finalized data
- Temporal guard allows editing AFTER a period expires but BEFORE all settlements from that period are finalized — parameter changes (denomination, rate, price) corrupt pending settlements
- Edit guard checks `expiry < current_time` but not `all_settlements_complete` — the entity appears editable because the period ended, but unsettled obligations still reference the old parameters

### UNI-3: Classic Reentrancy
**Provenance**: universal-invariants.md U4 + invariant-templates.md RE-1
**Pattern**: External call occurs before critical state updates (violates Checks-Effects-Interactions).
**Methodology**: Search for every external call (`.call`, `.transfer`, token transfer, user-supplied target). For each, check what state is read before and written after. If any write that affects subsequent checks happens after the call, the function is re-entrancy unsafe.
**Red flags**:
- Balance/ownership update AFTER `.call` or token transfer
- Absence of `nonReentrant` on payable or token-moving function
- ERC777/ERC721 `onReceived` callbacks on an otherwise trusted path

### UNI-4: Cross-Function / Read-Only Reentrancy
**Provenance**: universal-invariants.md U4 + invariant-templates.md RE-3/RE-4
**Pattern**: Reentrancy guard on function A does not protect function B that reads the same state during A's external call, or a view function returns stale state during another function's external call window.
**Methodology**: Group functions that share state. For each group, check whether a single guard protects the entire group or only individual functions. Identify view functions used for pricing/accounting that are callable during another function's mid-execution window.
**Red flags**:
- `nonReentrant` on `deposit()` but not on `getPrice()` reading the same reserves
- Oracle/price view functions not protected by the same guard

### UNI-5: Missing Zero-Address / Zero-Amount Checks
**Provenance**: universal-invariants.md U5 + invariant-templates.md DF-1
**Pattern**: External parameters are not validated; zero addresses burn tokens, zero amounts skip logic, empty arrays cause silent success.
**Methodology**: For every parameter of every external function, determine whether a zero value would produce undesired behaviour. Check token recipients, approval spenders, configuration setters, and array inputs.
**Red flags**:
- `transfer(to, amount)` with no `require(to != address(0))`
- Setter writes `address(0)` causing irrecoverable state
- `amount == 0` paths skipping fee calculation but still emitting success events

### UNI-6: Integer Overflow / Underflow in Unchecked Blocks
**Provenance**: universal-invariants.md U5
**Pattern**: Arithmetic inside `unchecked { ... }` or using low-level operations wraps around on large inputs.
**Methodology**: Grep `unchecked` and `assembly`. For each block, determine the maximum value each operand can reach across all call sites. Confirm the developer's implicit bound holds.
**Red flags**:
- `unchecked { totalSupply += amount }` without cap
- Counter increment that can flip over many transactions
- Cast `uint256 -> uint128` without bounds check

### UNI-7: Unchecked External Call Return Values
**Provenance**: universal-invariants.md U6 + invariant-templates.md DF-4
**Pattern**: Low-level calls, `transfer`, or non-standard ERC20 tokens return failure silently without reverting.
**Methodology**: For each external call, verify the return value is checked or `SafeERC20` wrappers are used. Treat non-reverting failures as fund-loss bugs.
**Red flags**:
- `token.transfer(...)` without `require` or SafeERC20
- `(bool success, ) = target.call(...)` with unused `success`
- `onERC721Received` never validated on safeTransfer paths

### UNI-8: Controlled Delegatecall
**Provenance**: universal-invariants.md U6 + U8
**Pattern**: `delegatecall` to a user-influenced or loosely-validated target gives an attacker full control of the calling contract's storage.
**Methodology**: Enumerate every `delegatecall`. Trace the target address parameter back to its source. If it can be user-influenced (even indirectly through a config setter) flag immediately.
**Red flags**:
- `delegatecall(msg.data, target)` where target is a setter-modifiable address
- Proxy implementation slot writable by a non-timelocked admin

### UNI-9: Upgrade / Initialization Safety
**Provenance**: universal-invariants.md U8 + invariant-templates.md ST-4
**Pattern**: Upgradeable contracts can be re-initialized, have storage collisions, or allow upgrades without timelock.
**Methodology**: Check for `initializer` modifier, `_disableInitializers()` in constructors, `__gap` storage reserves, and storage-layout compatibility between versions. Verify `upgradeTo` is behind access control AND timelock.
**Red flags**:
- Implementation constructor missing `_disableInitializers`
- `initialize()` without `initializer` modifier
- `__gap` shrunk between versions
- Upgrader is an EOA

### UNI-10: Timestamp Dependence for Security Decisions
**Provenance**: universal-invariants.md U7
**Pattern**: `block.timestamp` used as randomness seed or for tight windows that can be manipulated by validators.
**Methodology**: Grep `block.timestamp` and `now`. For each usage, determine the tolerance to a ~15-second shift. Randomness derived from timestamps is always broken.
**Red flags**:
- `uint256 seed = block.timestamp`
- Deadlines with <1-minute precision enforced for value transfers

### UNI-11: Missing Event Emission on State Changes
**Provenance**: universal-invariants.md U9
**Pattern**: Admin setters, role changes, or critical state transitions do not emit events, preventing off-chain monitoring.
**Methodology**: Build a setter list. For each setter, verify an event is emitted with old and new values. Missing events on role grants, fee changes, or asset onboarding are systemic findings.
**Red flags**:
- `setFee(newFee)` without `FeeUpdated(oldFee, newFee)` event
- Role grant without corresponding event
- Pause/unpause silent

### UNI-12: Unbounded Loops
**Provenance**: universal-invariants.md U10 + invariant-templates.md GR-1
**Pattern**: Loops over arrays that grow with user actions can be gas-bombed to block a function or the entire protocol.
**Methodology**: Identify every loop. For each, determine whether its upper bound is hardcoded, capped, or unbounded. Unbounded loops over user-addable entries are DoS vectors.
**Red flags**:
- `for (uint i; i < users.length; i++)` with permissionless `users.push`
- External calls inside loops without try-catch

### UNI-13: Unvalidated `from` in `transferFrom` (Approval Drain)
**Provenance**: universal-invariants.md U11
**Pattern**: Permissionless function calls `token.transferFrom(from, to, amount)` where `from` is attacker-supplied, draining any user who approved the contract.
**Methodology**: For every `transferFrom` / `safeTransferFrom`, trace who sets `from`. If it comes from calldata and the caller is not validated as `from` or an approved spender, flag as HIGH.
**Red flags**:
- Permissionless external function with `transferFrom(userAddr, ...)` using user's existing allowance
- Permit flows where the permit signer != the operation beneficiary

### UNI-14: Loop External-Call Fragility
**Provenance**: universal-invariants.md U12
**Pattern**: A loop that makes external calls reverts entirely if one iteration fails, blocking all subsequent operations.
**Methodology**: For every loop with external calls, look for try-catch, skip-and-continue, or partial execution patterns. A single paused dependency must not block all withdrawals.
**Red flags**:
- No `try { ... } catch` around strategy/pool external calls in loops
- Attacker-deployable contract that always reverts on transfer can block batch processing

### UNI-15: Weird-Token Incompatibility
**Provenance**: universal-invariants.md U13
**Pattern**: Contract assumes standard ERC20 behavior but supports tokens that charge fees on transfer, rebase, require approve-to-zero, or revert on zero transfer.
**Methodology**: Identify all tokens the contract can interact with (from deployment config, registry, or user-supplied). For each, check fee-on-transfer handling (balance-before/after), USDT approval reset, DAI non-standard permit, decimals assumption, and rebasing impact.
**Red flags**:
- `amount == transferred` assumption after `transferFrom`
- `approve(spender, newAmount)` without zero reset
- Hardcoded 18-decimal math for arbitrary tokens

### UNI-16: Array Boundary Edge Cases
**Provenance**: universal-invariants.md U14
**Pattern**: Swap-and-pop removal, index-based access, or loops fail at `length == 1`, empty arrays, or duplicates.
**Methodology**: For each swap-and-pop, test `last-element removal` explicitly. For each indexed access, verify bounds checks. For each array-modifying function, test empty and single-element cases.
**Red flags**:
- `array[index] = array[array.length - 1]; array.pop()` without zeroing the moved element's companion mapping
- No `index < array.length` check before use

### UNI-17: Post-Commitment State Mutation
**Provenance**: universal-invariants.md U15
**Pattern**: Once state has been finalized, committed, or passed a validation window, it can still be mutated without re-validation.
**Methodology**: For every lifecycle entity, verify each phase is one-way. `initialize` must check for existing state. `consume`/`claim` must re-validate state they read. Mutable fields must not change after proofs are computed.
**Red flags**:
- `initialize()` called twice on same entity overwriting finalized data
- Mutable `partOffset` updated after merkle root computed
- Re-initialization corrupting shared state (registries, oracles)

### UNI-18: Uninitialized-State Guard Bypass
**Provenance**: universal-invariants.md U16
**Pattern**: Zero/default values satisfy guards that assume initialized state (e.g., `block.timestamp - 0 > PERIOD` is always true).
**Methodology**: For every timestamp/existence comparison, ask what happens when the stored field is zero. For every multi-step init, ensure finalize validates ALL intermediate steps completed.
**Red flags**:
- `if (lastClaim != 0)` used to guard distribution but another path skips setting it
- Boolean defaulting to `false` where `false` means both "unvalidated" and "failed"
- Storage map/mapping read for a non-existent key returns zero/default, and the zero value is used downstream without an existence check — e.g., `map.read(user_supplied_key)` returns 0 for an unregistered key, the code hashes 0 with other data, and the hash is used for signature verification. The attacker signs over the zero value to bypass authorization for unregistered keys.
- A registry lookup (oracle registry, whitelist, role mapping) returns a default value for non-members, but the calling code does not assert the returned value is non-zero/non-default before proceeding. The non-member passes the check by operating on the default value.
- `let data = storage_map.entry(key).read(); /* data is 0 for missing key */ validate_signature(data, sig);` — the signature is valid over 0, which is a predictable constant, so any key pair can produce a valid signature

### UNI-19: Temporal Constraint Incompatibility
**Provenance**: universal-invariants.md U17
**Pattern**: A time-bounded operation's guaranteed window is insufficient for the worst-case execution of all required sub-operations.
**Methodology**: For each deadline mechanism, list all operations that must complete within the window. Sum minimum times (including challenge periods). Verify total fits.
**Red flags**:
- 3-hour extension granted but inner operation needs 1-day challenge period
- Nested timers where outer < inner + overhead

### UNI-20: Cross-Environment Resource Parity
**Provenance**: universal-invariants.md U18
**Pattern**: Operation executed in environment A must be reproducible in environment B but B has tighter resource limits (gas, calldata, memory).
**Methodology**: For every cross-environment proof/verification, compare the execution cost in each environment. Account for EIP-150 63/64 forwarding and verifier overhead.
**Red flags**:
- L2 operation that must be re-executed on L1 without gas budget analysis
- Dynamic-cost precompiles unchecked against target env block limit

### UNI-21: Derived-Value Domain Bounds
**Provenance**: universal-invariants.md U19
**Pattern**: Computed values are not capped at their logical maximum before being passed to consuming systems.
**Methodology**: For each derivation (index, position, hash), check the output range matches the consumer's valid input range.
**Red flags**:
- `computedBlock = start + traceIndex + 1` uncapped at `claimedBlock`
- Mapping from large index space to smaller domain without range enforcement

### UNI-22: Work-Reward Decoupling
**Provenance**: universal-invariants.md U20
**Pattern**: Permissionless reward-distribution attributes the reward to `msg.sender` instead of the worker, allowing front-runners to steal.
**Methodology**: For every permissionless claim/distribute, trace who pays cost (gas, bond) vs who receives reward. Mismatch = bug.
**Red flags**:
- `step()` function pays bond to `msg.sender` while evidence was provided by a different party
- Two-step reward where step 2 is permissionless and front-runnable

### UNI-23: Identifier Namespace Collisions
**Provenance**: universal-invariants.md U21
**Pattern**: Unique identifiers can be consumed prematurely, pre-populated for non-existent entities, or become invalid after reordering.
**Methodology**: For each unique ID scheme (nonce, hash, UUID), check whether the ID can be blocked, pre-populated, or invalidated by state changes. Prefer `create2`/salted over nonce-derived for cross-tx safety.
**Red flags**:
- Permissionless data population keyed by future entity address
- Index-based reference breaking after swap-and-pop

### UNI-24: Spec Exhaustive Compliance
**Provenance**: universal-invariants.md U22
**Pattern**: Code implementing a formal spec (instruction set, standard, formula) only handles common cases; edge cases (shift masking, overflow traps, alignment) diverge from the spec.
**Methodology**: For each opcode/rule/formula, compare on-chain implementation against the reference, line by line. Check input masking, overflow behaviour, and undefined-behaviour handling.
**Red flags**:
- Shift amount not masked to 5/6 bits per spec
- Silent wrap where spec requires trap
- Type width mismatch losing high bits

### UNI-25: Error-State Asymmetry in Adversarial Protocols
**Provenance**: universal-invariants.md U23
**Pattern**: In dispute/challenge systems, an error state benefits one party over the other.
**Methodology**: Enumerate every error/revert in a dispute flow. Ask which party benefits. If a panic makes claims unchallengeable, the panic-trigger wins.
**Red flags**:
- `require` in challenge path that only the challenger can hit
- Status value that is simultaneously unattackable and undefendable

### UNI-26: Multi-Step Normalization Ordering
**Provenance**: universal-invariants.md U24
**Pattern**: Non-commutative adjustments (normalize, halve, round) are applied in different orders across branches.
**Methodology**: For each multi-step adjustment, list the steps in order. For each adjacent pair, ask whether swapping changes the result. Verify all branches use the same order.
**Red flags**:
- Branch A: halve-then-adjust; Branch B: adjust-then-halve
- Rounding before scaling losing precision

### UNI-27: Format/Precision Selection Consistency
**Provenance**: universal-invariants.md U25
**Pattern**: Systems with multiple formats (small/large, low/high precision) apply different selection rules across paths.
**Methodology**: Build a format selection table for each path (encode, arithmetic output, decode, conversion). Compare rule sets. Construct boundary values that expose the asymmetry.
**Red flags**:
- Encoding uses more rules than arithmetic output
- `encode(decode(encode(x))) != encode(x)` at boundary

### UNI-28: Representation Gap Integrity
**Provenance**: universal-invariants.md U26
**Pattern**: Values "in the gap" (too precise for small format, not qualifying for large) are silently truncated.
**Methodology**: Identify the gap range using the format selector. Check whether precision loss in the gap stays within stated tolerance and whether it compounds through subsequent calculations.
**Red flags**:
- Format selector uses exponent-only when precision depends on digit count
- Gap value multiplied by gap value

### UNI-29: Aggregate State Removal Consistency
**Provenance**: universal-invariants.md U27
**Pattern**: When an element is removed from a set with aggregate/summary variables, some aggregates are updated and others are not.
**Methodology**: For every aggregate (totalSupply, totalWeight, totalBias, changesSum), verify it is decremented on removal. For time-weighted aggregates, verify the slope is corrected too.
**Red flags**:
- Removal updates `bias` but not `slope`
- Two-step removal where users never complete step 2
- Zero-aggregate edge case causing division by zero

### UNI-30: Stale-Snapshot After Collection Mutation
**Provenance**: universal-invariants.md U28
**Pattern**: A function copies a storage array to memory, mutates the storage array (eviction, swap-pop), then continues using stale memory indices.
**Methodology**: Grep functions that copy storage arrays to memory then call a mutating function. Verify subsequent code re-reads from storage.
**Red flags**:
- `Foo[] memory cache = storageArray; evict(); cache[i]` (stale)
- Swap-and-pop indices reused after reorder

### UNI-31: Prerequisite Update Before Participant Change
**Provenance**: universal-invariants.md U29
**Pattern**: Adding/removing a participant (staker, voter, LP) without first checkpointing accumulated state dilutes existing participants.
**Methodology**: For each `join`/`leave`/`add`/`remove`, verify the checkpoint/accrue function is called first. Verify the accrual is enforced internally, not by external caller convention.
**Red flags**:
- `stake()` pushes to participants array without calling `updateReward()` first
- `retain()` reads weights without calling checkpoint

### UNI-32: Last-Element Array+Mapping Removal
**Provenance**: universal-invariants.md U30
**Pattern**: Swap-and-pop with companion mapping leaves stale mapping entries when removing the last element (self-swap re-assigns it).
**Methodology**: Trace every swap-and-pop that has a companion `mapIds` or index mapping. Test the case where removed index equals the last index.
**Red flags**:
- Map zeroed AFTER swap (self-swap overwrites with stale)
- Existence check `mapIds[h] != 0` returning true for removed elements

### UNI-33: Permissionless Function Privilege Boundary
**Provenance**: universal-invariants.md U31
**Pattern**: A permissionless function accepts a `target` parameter that can be a privileged address with special semantics in another function, bypassing intended behavior.
**Methodology**: Enumerate privileged addresses (retainer, treasury, fee collector). For each permissionless claim/distribute with a `target`, verify it rejects privileged targets.
**Red flags**:
- `distribute(to)` that can be called with `to = treasury` bypassing `retain()` semantics

### UNI-34: Declared Token Compatibility vs. Code
**Provenance**: universal-invariants.md U32
**Pattern**: README/spec declares support for fee-on-transfer, rebasing, blocklist, or upgradeable tokens but code does not actually handle them.
**Methodology**: Read docs for declared compatibility. Compare against code handling of balance-before/after patterns, blocklist reverts on reward paths, and ERC721 safeTransfer on contracts without `onERC721Received`.
**Red flags**:
- Docs say "supports USDT" but code assumes `amount == received`
- Blocklisted fee collector bricks all unstakes

### UNI-35: Role Separation & No Self-Grant
**Provenance**: invariant-templates.md AC-1, AC-2
**Pattern**: A role can grant itself higher privileges, or a single role gates multiple unrelated powers.
**Methodology**: Build a role-capability matrix. Verify every role granter is strictly higher-privilege than the grantee. Check that emergency roles cannot unilaterally elevate.
**Red flags**:
- `RoleA.admin == RoleA`
- Single `onlyOwner` gating both parameter setting and fund movement

### UNI-36: Cross-Contract Access Control Consistency
**Provenance**: invariant-templates.md AC-3 + governance-centralization.md §2
**Pattern**: Contract A restricts function F behind role R, but contract B (caller) has no such restriction, creating a permissionless back-door.
**Methodology**: For every cross-contract call, verify the caller enforces the same or stronger restriction as the target. Flag permissionless wrappers around permissioned functions.
**Red flags**:
- `adapter.split()` public while `core.split()` requires SPLIT_ROLE

### UNI-37: Timelock Scope for Parameter Changes
**Provenance**: invariant-templates.md AC-4 + governance-centralization.md §6
**Pattern**: Parameter changes that affect accounting or user funds can be applied instantly by an EOA admin.
**Methodology**: For every parameter setter that feeds into accounting, verify it is behind a timelock. Setters protected only by `onlyOwner` are instant-rug vectors.
**Red flags**:
- `setFee`, `setRate`, `setOracle` instant with no delay
- Owner is an EOA with no multisig requirement

### UNI-38: Emergency Exit Guarantees
**Provenance**: invariant-templates.md AC-5
**Pattern**: When paused/frozen, users cannot withdraw their own funds.
**Methodology**: Check pause mechanics. Confirm at least one exit path remains available (possibly with penalty) in every paused state.
**Red flags**:
- `whenNotPaused` on `withdraw()` with no alternative
- Liquidation/forced-close functions gated by pause — when the protocol is paused, insolvent positions cannot be liquidated, allowing bad debt to accumulate. Risk management functions (liquidation, deleverage) should remain operational during pause
- Deposit cancellation gated by pause — users who deposited before the pause cannot recover their funds

### UNI-39: Monotonic State Progression
**Provenance**: invariant-templates.md TL-1
**Pattern**: Phased state (epochs, rounds) regresses to a previous phase under some path.
**Methodology**: For each phase counter, identify every write. Confirm writes are monotonic increments.
**Red flags**:
- `currentEpoch` writable to arbitrary value
- Timestamp-based epoch boundary that resets on pause/unpause

### UNI-40: Past-Epoch Immutability
**Provenance**: invariant-templates.md TL-2
**Pattern**: Data from a completed epoch can still be modified after the next epoch starts.
**Methodology**: For each epoch-keyed mapping, identify setters. Verify they revert once the epoch is past.
**Red flags**:
- `setEpochReward(epochId, amount)` with no completion check

### UNI-41: Cooldown Bypass
**Provenance**: invariant-templates.md TL-3
**Pattern**: Multiple code paths, transferring staked tokens, or restaking can reset or skip a cooldown.
**Methodology**: For each cooldown, enumerate all paths that read it. Check whether any bypass exists (token transfer, partial restake, emergency withdraw).
**Red flags**:
- `transferFrom` of staked token shifts cooldown to attacker
- Emergency withdraw without equivalent cooldown

### UNI-42: Deadline Validity
**Provenance**: invariant-templates.md TL-4
**Pattern**: Stale transactions (past deadline) can still execute.
**Methodology**: Grep all operations that accept a `deadline`. Verify `require(block.timestamp <= deadline)`.
**Red flags**:
- Deadline parameter accepted but never checked
- Deadline check under a conditional that can be skipped

### UNI-43: Sequence / Step Ordering
**Provenance**: invariant-templates.md TL-5
**Pattern**: A later step in a multi-step operation can execute without the earlier step completing.
**Methodology**: For each multi-step flow, enumerate the required preconditions for each step. Verify each step re-validates its preconditions.
**Red flags**:
- `finalize()` that does not check `populate()` ran
- Step 2 reads storage set by step 1 without verifying step 1's completion

### UNI-44: Bounded Iteration
**Provenance**: invariant-templates.md GR-1
**Pattern**: Loops lack an explicit or implicit cap, enabling gas-bomb DoS.
**Methodology**: For every loop, document the upper bound. If the bound is user-controlled without cap, flag.
**Red flags**:
- `for (; i < userProvided;)` with no limit

### UNI-45: Array Growth Limits
**Provenance**: invariant-templates.md GR-2
**Pattern**: A user-pushable array has no max size, allowing an attacker to fill it and brick iteration.
**Methodology**: For every dynamic array growable by external calls, check for a cap (explicit or economic).
**Red flags**:
- `strategies.push(...)` without `require(strategies.length < MAX)`

### UNI-46: External Calls in Loops
**Provenance**: invariant-templates.md GR-3
**Pattern**: A loop makes an unbounded number of external calls; one failing call reverts the whole batch.
**Methodology**: Cross-reference with UNI-14. Require try-catch or skip-and-continue for each loop external call.
**Red flags**:
- `for (...) target[i].call(...)` without try-catch

### UNI-47: Bounded Cleanup on Delete
**Provenance**: invariant-templates.md GR-4
**Pattern**: Deleting an entity runs unbounded work, DoSing the deletion path itself.
**Methodology**: For each delete path, check it completes in constant or bounded gas.

### UNI-48: Arithmetic Rounding Direction
**Provenance**: invariant-templates.md MF-3
**Pattern**: Rounding direction favors the user instead of the protocol, enabling dust extraction.
**Methodology**: For every `mulDiv` / division in asset/share math, verify the direction: deposits round shares DOWN, withdrawals round assets DOWN.
**Red flags**:
- `shares = amount * totalSupply / totalAssets` rounding up
- Fee calculation rounding toward user

### UNI-49: Donation / Direct Transfer Corruption
**Provenance**: invariant-templates.md MF-6 + vault-invariants V12
**Pattern**: Accounting reads `balanceOf(address(this))` instead of internal state, allowing direct transfers to corrupt accounting.
**Methodology**: For each accounting function, check whether it uses `balanceOf` or internal counters. `balanceOf` is donation-vulnerable.
**Red flags**:
- `totalAssets()` returns `token.balanceOf(this)`
- First depositor calculates shares from `balanceOf`

### UNI-50: First-Depositor Share Inflation
**Provenance**: invariant-templates.md MF-5 + vault-invariants V3
**Pattern**: First depositor mints 1 wei, donates large amount, causing subsequent depositors to round to zero shares.
**Methodology**: For any share-issuance contract, verify virtual shares/assets (OZ pattern), minimum deposit, or dead-shares mint on first deposit.
**Red flags**:
- `shares = assets * totalSupply / totalAssets` with no virtual offset and `totalSupply == 0` edge case

### UNI-51: Atomic Value Transfer
**Provenance**: invariant-templates.md MF-7
**Pattern**: Sender's balance decreases by X but receiver's increases by Y != X (minus fees).
**Methodology**: For each transfer, verify source debit == destination credit + documented fee.

### UNI-52: Solvency Invariant (contractBalance >= sum(userOwed))
**Provenance**: universal-invariants.md U3 + invariant-templates.md MF-1
**Pattern**: Protocol-tracked liabilities exceed actual asset holdings.
**Methodology**: For each token held, trace: (balance on contract) vs (sum of user claims + protocol fees). Verify the invariant holds under every execution path.
**Red flags**:
- Withdrawal path decrements `userShares` but not `totalShares`
- Fee accrual double-counted

### UNI-53: No Free Extraction
**Provenance**: invariant-templates.md MF-2
**Pattern**: A sequence of calls allows withdrawing more value than deposited (net of fees and yield).
**Methodology**: Model the protocol as a closed economy. Attempt to construct a cycle that produces profit without external input.

### UNI-54: Fee Bounds Enforcement
**Provenance**: invariant-templates.md MF-4
**Pattern**: Fee setters allow values exceeding documented maxima.
**Methodology**: For each fee setter, verify `require(fee <= MAX)`. Check cumulative fees across multiple paths.
**Red flags**:
- `setFee(uint256 fee)` with no upper bound

### UNI-55: Storage Slot Uniqueness
**Provenance**: invariant-templates.md ST-1
**Pattern**: Proxy and implementation use conflicting storage layouts, or assembly sstore overwrites another variable.
**Methodology**: For upgradeable contracts, verify storage gap reservation and layout compatibility via tools like `hardhat-upgrades`. For assembly slot access, verify slot calculation.

### UNI-56: Initialization Completeness
**Provenance**: invariant-templates.md ST-2
**Pattern**: Functions read storage variables before they are initialized, getting default zero values.
**Methodology**: For each storage variable, verify at least one write occurs before any read on every reachable path.

### UNI-57: Mapping Key Uniqueness (encodePacked Pitfalls)
**Provenance**: invariant-templates.md ST-3 + DF-3
**Pattern**: `abi.encodePacked` with multiple variable-length types produces colliding keys.
**Methodology**: Grep `abi.encodePacked`. For each, verify no two variable-length types are adjacent. Use `abi.encode` or add length prefixes.
**Red flags**:
- `keccak256(abi.encodePacked(name, symbol))` where both are user-supplied

### UNI-58: Signed Data Completeness
**Provenance**: invariant-templates.md DF-2 + signed-data-completeness §1
**Pattern**: A digest omits fields the function uses for decisions, allowing relayer substitution.
**Methodology**: For each signature verification, build a SIGNED DATA BINDING TABLE: list every field used post-verification. Every used field must be signed.
**Red flags**:
- `deadline` used but not part of signed payload
- `callGasLimit` honored but not signed

### UNI-59: Nonce / Replay Protection
**Provenance**: invariant-templates.md DF-5 + signed-data-completeness §4
**Pattern**: Signatures can be replayed due to missing nonce, non-atomic nonce increment, or nonce omitted from hash.
**Methodology**: Verify nonce is in the signed digest, incremented atomically, and scoped per-signer.

### UNI-60: Taint Boundary at External Returns
**Provenance**: invariant-templates.md DF-4
**Pattern**: Return values from external contracts are consumed without validation.
**Methodology**: For each external call return used in a calculation, verify sanity bounds.

### UNI-61: Oracle Staleness Protection
**Provenance**: invariant-templates.md OR-1
**Pattern**: Oracle price reads omit a staleness check, allowing stale values to drive critical decisions.
**Methodology**: For each oracle read, verify `require(updatedAt >= block.timestamp - heartbeat)` or equivalent. Check that every oracle reader uses the same threshold (see UNI-65).

### UNI-62: Manipulation-Resistant Pricing
**Provenance**: invariant-templates.md OR-2
**Pattern**: A price is derived from spot reserves or `balanceOf`, enabling flash-loan manipulation.
**Methodology**: Every price used for liquidation/borrowing/mint must use TWAP, Chainlink, or time-weighted sources.

### UNI-63: Oracle Graceful Degradation
**Provenance**: invariant-templates.md OR-3
**Pattern**: Oracle failure (revert, stale, zero) bricks the protocol permanently.
**Methodology**: Verify a pause path exists on oracle failure rather than a hard revert.

### UNI-64: Multi-Source Oracle Validation
**Provenance**: invariant-templates.md OR-4
**Pattern**: High-value operations rely on a single price feed with no cross-check.
**Methodology**: For liquidations and large swaps, verify price is cross-checked against a second source or bounded by a circuit breaker.

### UNI-65: Feed Consistency Across Readers
**Provenance**: invariant-templates.md OR-5
**Pattern**: Different functions read the same oracle with different freshness thresholds, producing inconsistent behavior.
**Methodology**: Identify every reader of each oracle feed. Verify all use the same freshness threshold and fallback.

### UNI-66: Parameter Scope Declaration
**Provenance**: parameter-scope-analysis.md §1
**Pattern**: Admin parameters are used by calculation functions with no documentation of whether they apply retroactively or only to future operations.
**Methodology**: Build a PARAMETER SCOPE TABLE for every admin-modifiable storage variable. Row: parameter, setter, reader functions, temporal scope, retroactive. If scope is undocumented AND the parameter is read by historical calculations, flag.

### UNI-67: Retroactive Calculation Prevention
**Provenance**: parameter-scope-analysis.md §2
**Pattern**: An admin parameter change alters results for a PAST period after the period ends but before users claim.
**Methodology**: For each admin-modifiable parameter, ask: "If admin changes it at T, does calling a view function for T-1 return a different result?" If yes, the historical value must be snapshotted.
**Red flags**:
- `getEpochReward(epochId)` reading the live `rewardRate`
- `pendingReward()` using the current rate for past periods

### UNI-68: Locked-Position Integrity
**Provenance**: parameter-scope-analysis.md §3
**Pattern**: A user commits to a locked position under specific terms; admin changes the global terms and the change retroactively applies.
**Methodology**: For each locking/staking/vesting mechanism, check whether terms are stored per-position or read from global state on each access.
**Red flags**:
- `penalty = globalPenaltyRate` read at exit time instead of lock time
- APY read from global state for pre-existing locks

### UNI-69: Timing-Adversary Resistance on Admin Changes
**Provenance**: parameter-scope-analysis.md §4
**Pattern**: Admin parameter updates create a window where attackers front-run or back-run for profit.
**Methodology**: For each admin setter, check timelock protection and whether front-run/back-run is profitable.

### UNI-70: Cross-Function Consistency of Parameter Reads
**Provenance**: parameter-scope-analysis.md §5
**Pattern**: `preview*` and actual operation use different snapshots of the same parameter, producing contradictory results.
**Methodology**: For each parameter read by view and state-changing functions, verify both use the same snapshot semantics.

### UNI-71: Boundary Safety on Parameter Updates
**Provenance**: parameter-scope-analysis.md §6
**Pattern**: Setters accept values that individually seem valid but collectively cause division-by-zero, overflow, or impossible states.
**Methodology**: For each setter, trace arithmetic expressions using the parameter. Check for zero denominator, >100% basis points, `min > max`, and upper-bound overflow.

### UNI-72: Privilege Enumeration / Centralization Surface
**Provenance**: governance-centralization.md §1
**Pattern**: Admin functions are undocumented; maximum damage under malicious admin is unclear.
**Methodology**: Enumerate every `onlyOwner`/`onlyRole` function. For each, document worst-case damage. Rate: can admin mint without backing, drain user funds, block operations, set fees to 100%?

### UNI-73: Operation Blocking Powers
**Provenance**: governance-centralization.md §3
**Pattern**: Admin can pause exits while entries remain open, trapping users.
**Methodology**: Check whether exits can be blocked independently of entries and whether transfers are pausable.

### UNI-74: Irreversible Admin Actions
**Provenance**: governance-centralization.md §5
**Pattern**: Admin actions cannot be undone (set-once mappings, remove-without-claim).
**Methodology**: For each admin action, verify an inverse exists.

### UNI-75: Ownership Transfer Two-Step
**Provenance**: governance-centralization.md §6
**Pattern**: Single-step ownership transfer can send ownership to `address(0)` or wrong address irrecoverably.
**Methodology**: Prefer `Ownable2Step`.

### UNI-76: Router Permissionless Entry Points
**Provenance**: router-multicall-invariants.md R1
**Pattern**: Permissionless `execute`/`multicall` dispatches commands where `from`/`owner` is attacker-supplied, draining anyone who approved the router.
**Methodology**: For every command with a `from`/`owner` parameter, verify `msg.sender == from` or equivalent. For every command with `receiver`, verify the attacker cannot send victim's funds to themselves.

### UNI-77: Approval Persistence on Router
**Provenance**: router-multicall-invariants.md R2
**Pattern**: Users grant approval to a router; any permissionless command can then spend their tokens.
**Methodology**: Build an APPROVAL FLOW TABLE for the router. For each spend path, verify msg.sender is validated as the token owner.

### UNI-78: Permit Frontrunning on Router
**Provenance**: router-multicall-invariants.md R3
**Pattern**: An attacker extracts a permit signature from the mempool and submits it with different downstream commands.
**Methodology**: Verify permit signer matches the operation beneficiary. Check DAI non-standard permit handling.

### UNI-79: Router Identity Confusion
**Provenance**: router-multicall-invariants.md R4
**Pattern**: Vaults/protocols see the router as the depositor; per-address limits apply to the router instead of end users.
**Methodology**: Check whitelist and maxDeposit enforcement site.

### UNI-80: Router Token Residual / Sweep
**Provenance**: router-multicall-invariants.md R6
**Pattern**: Tokens left in the router between commands can be swept by the first caller.
**Methodology**: Verify the router never holds tokens across transactions; if a sweep exists, verify it cannot race a victim's in-flight transaction.

### UNI-81: Compound-Fork Share Rounding Subadditivity
**Provenance**: compound-fork-integration.md §1
**Pattern**: `floor(a) + floor(b) < floor(a+b)` causes single aggregated redemption to require more shares than held.
**Methodology**: Compare sum of individual mints vs single aggregated redeem. Check whether protocol donates shares to absorb rounding.

### UNI-82: Compound Treasury Fee Activation Risk
**Provenance**: compound-fork-integration.md §2
**Pattern**: Governance of an external Compound fork enables a treasury fee; the integrating protocol reverts or silently loses amount.
**Methodology**: Check whether the adapter reads `treasuryPercent` and how it handles non-zero results.

### UNI-83: Supply-Cap / Borrow-Cap DoS
**Provenance**: compound-fork-integration.md §3
**Pattern**: External supply cap filled by a whale blocks all subsequent deposits.
**Methodology**: Check whether deposits handle cap reversion and whether an alternative yield source exists.

### UNI-84: High-Utilization Redemption DoS
**Provenance**: compound-fork-integration.md §4
**Pattern**: `redeemUnderlying` reverts when pool cash < requested; protocol has no fallback.
**Methodology**: Verify try-catch and fallback source chains.

### UNI-85: Empty-Market First-Depositor Amplification
**Provenance**: compound-fork-integration.md §6
**Pattern**: Protocol auto-deposits into newly-created markets vulnerable to first-depositor attack.
**Methodology**: Check whether yield sources are validated as established before auto-deposit.

### UNI-86: External Reward Capture from Yield Sources
**Provenance**: compound-fork-integration.md §11 + yield-source-integration.md §7
**Pattern**: Lending pools distribute governance tokens / Merkle rewards to the depositor (the protocol contract); users have no claim path.
**Methodology**: Verify a claim or generic `execute()` function exists and has fair distribution logic.

### UNI-87: Partial vs Full Redemption DoS
**Provenance**: yield-source-integration.md §1
**Pattern**: Protocol forces full balance redemption; if transfers are disabled and liquidity is low, users are permanently locked.
**Methodology**: Verify partial redemption exists or an equivalent escape hatch.

### UNI-88: Bad-Debt Cascade / Idle-Balance Drainage
**Provenance**: yield-source-integration.md §2
**Pattern**: Attacker deposits into an insolvent source (absorbing bad debt) then withdraws from idle balance, draining the protocol.
**Methodology**: Verify deposits check source solvency; verify redemption fallback ordering does not leave stale `depositedAmounts`.

### UNI-89: Skip/Disable Flag Consistency
**Provenance**: yield-source-integration.md §4
**Pattern**: A `skipForWithdrawal` flag is applied inconsistently across functions (correct for withdraw, wrong for deposit).
**Methodology**: Build a function × flag × checked? table.

### UNI-90: FCFS on Insolvency
**Provenance**: yield-source-integration.md §6
**Pattern**: When a yield source goes insolvent, first redeemer takes everything while later redeemers get nothing; no pro-rata loss sharing.
**Methodology**: Verify loss-sharing mechanism or document FCFS behavior as intentional.

### UNI-91: Negative-Yield Accounting
**Provenance**: yield-source-integration.md §5
**Pattern**: `depositedAmounts -= amountRedeemed` underflows when yield source returns less than deposited.
**Methodology**: Verify the accounting handles `amountRedeemed < deposit` gracefully.

### UNI-92: Yield-Leakage via Return-Value Mismatch
**Provenance**: yield-source-integration.md §8 + vault-invariants V16
**Pattern**: Yield source returns more than requested; excess is silently left in the contract or given to wrong recipient.
**Methodology**: Trace every `amountReturned` vs `amountRequested` path. Verify the excess is explicitly routed.

### UNI-93: Emergency Function Input Validation
**Provenance**: yield-source-integration.md §10
**Pattern**: `emergencyWithdrawFromYieldSources(address[])` accepts arbitrary addresses; accounting can be corrupted by a rogue address.
**Methodology**: Verify input validation against registered sources.

### UNI-94: Before/After Balance Pattern Consistency
**Provenance**: yield-source-integration.md §12
**Pattern**: Some paths use `balanceAfter - balanceBefore`, others trust nominal amount; inconsistent handling breaks fee-on-transfer or rebasing tokens.
**Methodology**: Identify every transfer path. Verify consistent before/after or consistent nominal usage across paths.

### UNI-95: Irreversible Yield-Source Configuration
**Provenance**: yield-source-integration.md §9
**Pattern**: `underlyingToVToken[token]` is set once with no unset; a wrong or deprecated mapping is permanent.
**Methodology**: Verify every configuration mapping has both set and unset admin paths.

### UNI-96: ERC-165 Inherited Interface Coverage
**Provenance**: drozer-lite v0.3.1 — class-of-bug: supportsInterface override omits interfaces implemented by ancestor contracts, breaking ERC-165-based integration detection.
**Pattern**: A contract's `supportsInterface(bytes4)` only reports the interface it was explicitly registered for, not every interface its parent contracts implement. Downstream integrators who check `supportsInterface(ParentInterface.selector)` get false and refuse integration.
**Methodology**: For every `supportsInterface` override, enumerate every ancestor contract's interface (including upgradeable/proxy libraries). Verify the override returns true for each. Prefer `return super.supportsInterface(interfaceId) || interfaceId == type(IThis).interfaceId` to the fully-enumerated OR chain to avoid drift on future inheritance changes.
**Red flags**:
- `supportsInterface` returns `interfaceId == type(IThis).interfaceId` only, not OR'd with `super`
- New interface added to the contract but supportsInterface not updated
- AccessControl + Enumerable + custom interface but only one is reported
- Interface-detection-based integration docs (e.g., marketplaces) not tested against actual supportsInterface

### UNI-97: Precision Loss in Decimal Conversion
**Provenance**: drozer-lite v0.3.1 — class-of-bug: silent truncation when scaling values between different decimal bases, rounding direction undocumented and inconsistent with the inverse operation.
**Pattern**: A function converts a value between different decimal bases (e.g. 18→8, 18→6, 8→18) and silently truncates. The rounding direction is not documented, not user-controlled, and not consistent with the inverse operation.
**Methodology**: Grep every `amount * 10**X`, `amount / 10**X`, `_convertToNDecimals`, or explicit `mulDiv`/`div` between two known-different decimal bases. For each, verify:
1. The rounding direction matches the intent (user-owed amounts round UP for the user, fees round UP for the protocol).
2. The inverse conversion is actually inverse — `convertUp(convertDown(x))` should equal `x` only at the base-grid boundary.
3. A round-trip of the same amount through two conversions does not compound loss more than a stated tolerance.
4. Boundary values (0, smallest non-zero, smallest that rounds up) behave correctly.
**Red flags**:
- `truncatedAmount = amount / 1e10;` with no rounding-up branch on the withdrawal path
- Deposit and withdrawal paths use different rounding directions silently
- Loss accumulates per-operation and is not recorded or refunded to the user
- Conversion comment says "truncates" but callers treat the result as exact

### UNI-98: receive()/fallback() Auto-Route Balance Invariant Break
**Provenance**: drozer-lite v0.3.1 — severity calibration fix for a pattern class where the receive()/fallback() path re-enters a state-mutating function and any invariant that reads `address(this).balance` becomes permanently brittle. This check is structurally HIGH, not MEDIUM.
**Pattern**: A contract has a `receive()` or `fallback()` payable function that unconditionally forwards `msg.value` into a state-mutating function in the same contract (deposit, wrap, stake, mint, buy). Another function in the same contract uses `address(this).balance` as part of an invariant check (e.g., `require(address(this).balance >= amount)` before a refund / withdraw / return / claim). Because the auto-route consumes incoming native value before it can accumulate, the balance-based invariant can be permanently unsatisfiable, or at minimum becomes dependent on chain-specific semantics for how native value can enter the contract without invoking `receive()`.
**Methodology**: For every `receive()` and `fallback()` function in the cluster:
1. Check whether it unconditionally forwards `msg.value` into a state-mutating function (one that writes storage, mints tokens, or calls an external contract with value).
2. Grep the entire cluster for any `address(this).balance` read used in a `require`, arithmetic, or branch that affects a user-visible decision (refund, withdraw, claim, redeem, rescue).
3. If both conditions hold, trace whether there is ANY path by which native value can enter the contract WITHOUT invoking `receive()` (e.g., `selfdestruct(to)` from another contract, direct balance credit from a privileged precompile, block reward to COINBASE if the contract is a miner/proposer, chain-specific system transfers). If no such path exists, the invariant is permanently broken. If one exists, the invariant is brittle and subject to operational assumptions outside the source.
**Red flags**:
- `receive() external payable { f(); }` where `f()` is any state-mutating function in the same contract
- `fallback() external payable { ... f(); }` similarly
- Any function with `require(address(this).balance >= amount, ...)` whose sibling contract has an auto-routing `receive()`/`fallback()`
- A "rescued" or "cancelled" accumulator variable whose only payout path depends on contract balance growing via future external transfers
- Comments or docs saying "buffer provides liquidity" or "accumulated value drains to users" but no actual code path produces non-auto-routed inbound value
**Severity rule (HARD)**: When the balance-based invariant is read in a user-facing function (withdraw, refund, claim, redeem, rescue, confirm, settle), this check MUST be rated at least HIGH. Do NOT downgrade to MEDIUM due to uncertainty about chain-specific native-transfer semantics — the correct finding is HIGH with a note that exploitability depends on operational assumptions the auditor should flag and verify with the protocol team. Severity miscalibration on this pattern is itself a finding-quality bug.

### UNI-99: Approval / Permission Persistence After Action Reversal
**Provenance**: drozer-lite v0.4.1 — class-of-bug: an action grants a permission as a side effect, and the reversal of that action does not revoke the permission, leaving the actor with unauthorized access.
**Pattern**: An action (bid, deposit, stake, register, subscribe) grants an approval, role, or allowance as a side effect. The corresponding reversal action (cancel bid, withdraw, unstake, deregister, unsubscribe) removes the primary state entry but does NOT revoke the permission that was granted alongside it. The actor retains the ability to operate on the entity (transfer, spend, execute) despite no longer having a stake or valid reason for access.
**Methodology**: For every function that grants an approval or permission as a side effect of a primary action:
1. Identify the corresponding reversal function (cancel, withdraw, remove, unstake, deregister).
2. Verify the reversal function explicitly removes the same approval/permission.
3. Check both per-token approval lists AND global operator/role mappings.
4. If the grant is conditional (e.g., only when `auto_approve` is true), verify the revocation also fires under the same condition.
**Red flags**:
- `approvals.push(sender)` in a bid/deposit path but the cancel/refund path only removes the bid entry, not the approval
- `grantRole(OPERATOR, sender)` on registration but `revokeRole` absent from deregistration
- `approve(spender, amount)` on stake but no `approve(spender, 0)` on unstake
- Toggle function (call once to create, call again to cancel) where the first call grants approval and the second call removes the primary record but not the approval
- Any path where an actor can: (1) perform action to gain permission, (2) reverse action to recover funds, (3) use retained permission to operate on the asset without cost

### UNI-100: Asymmetric Settlement Across Parallel Transfer Paths
**Provenance**: drozer-lite v0.4.1 — class-of-bug: multiple functions can move the same asset but only one includes payment/settlement logic, allowing the other to bypass payment entirely.
**Pattern**: A system has two or more functions that transfer ownership or move the same asset (e.g., `transfer` vs `send`, `transferFrom` vs `safeTransferFrom`, `withdraw` vs `emergencyWithdraw`, `redeem` vs `rescue`). One path includes settlement logic (payment to seller, fee deduction, accounting update, reward distribution). Another path transfers the asset without performing the same settlement, creating a bypass.
**Methodology**: 
1. Enumerate every function that changes ownership of an asset or moves value out of the contract.
2. Group these functions by the asset class they operate on.
3. For each group, build a comparison table: function name | settlement logic present? | fee deducted? | accounting updated? | events emitted?
4. If ANY function in the group skips settlement that another function performs, flag. Pay special attention to wrapper functions that call a shared internal `_transfer` without the outer settlement layer.
**Red flags**:
- `transfer()` includes payment settlement but `send()` calls `_transfer()` directly without settlement
- `withdraw()` updates accounting but `emergencyWithdraw()` does not
- Public `_transfer_nft()` helper is callable by approved addresses and bypasses the sale/auction settlement in `transfer_nft()`
- A function that takes a `recipient` parameter (allowing caller to send to another address) and a parallel function that forces `msg.sender` as recipient — the first may skip payment checks the second enforces
- Two functions that both call `check_can_send()` but only one settles the associated financial obligation (bid, deposit, escrow)

### UNI-101: Destructive Operation Without Obligation Settlement
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a burn/delete/close function destroys an entity with active obligations (deposits, rentals, locks), erasing records but not settling or refunding, making deposited funds permanently unrecoverable.
**Pattern**: A destructive operation (burn, delete, remove, close, self-destruct, deactivate) destroys an entity that carries active obligations — deposits held against it, active rentals or leases, pending reward claims, locked collateral, open orders, or unresolved escrows. The destruction erases the entity's records from storage, but the funds associated with those obligations remain in the contract with no recovery path. Affected users can no longer call cancel/refund/claim because the entity no longer exists.
**Methodology**: For every destructive function (burn, remove, close, delete, deactivate, self-destruct):
1. Identify what data is erased (the entity's full storage record, including nested structs, vectors, mappings).
2. Check whether any of the erased data includes: deposit amounts, active rental/lease records, pending claims, locked collateral, open bids, escrowed funds.
3. Verify the function checks for zero active obligations BEFORE allowing destruction. Acceptable patterns: `require(obligations.length == 0)`, `require(deposit_amount == 0)`, iterating obligations and refunding each before deletion.
4. If the function only checks ownership/approval but not obligation status, flag.
**Red flags**:
- `burn()` checks `check_can_send()` (ownership) but not whether `rentals.len() > 0` or `bids.len() > 0`
- `closePosition()` deletes the position record without checking `pendingRewards > 0`
- `deleteAccount()` while staking/delegation entries still reference the account
- `remove(tokenId)` erases a token struct that contains a Vec of deposit records
- Any destructive function where the authorization check is ownership/approval only, without an obligation-settlement check

### UNI-102: Heterogeneous Collection Without Type Discrimination
**Provenance**: drozer-lite v0.4.1 — class-of-bug: items of different types with different economic parameters are stored in the same collection, and operations iterate without filtering by type, allowing cross-type exploitation (e.g., paying in denomination A but receiving refund in denomination B).
**Pattern**: A collection (Vec, array, mapping, linked list) stores items of different subtypes, distinguished by a type flag, enum field, or discriminant. Operations that search, iterate, cancel, settle, or finalize items in the collection match by identity fields (address, ID, period) but do NOT filter by the type discriminant. This allows an operation designed for subtype A to match and operate on a subtype B item that has different economic parameters (denomination, rate, fee structure, cancellation policy).
**Methodology**:
1. Identify every collection that stores items with a type discriminant field (e.g., `item_type: bool`, `order_side: enum`, `position_type: u8`, `category: u8`).
2. For every function that searches/iterates the collection, check whether the search predicate includes the type discriminant.
3. If the search matches by (address + period) or (address + id) but ignores (type), verify whether the matched item's economic parameters (denomination, rate, terms) could differ from what the calling function assumes.
4. If a function reads denomination/rate/terms from a TYPE-LEVEL config (e.g., `shortterm_rental.denom`) but the matched item was created under a DIFFERENT type's config (e.g., `longterm_rental.denom`), flag as HIGH — this enables cross-denomination value extraction.
**Red flags**:
- A `rentals` Vec stores both short-term and long-term entries with a `type` flag, but cancel/finalize functions search by `(address, period)` without checking `type`
- An order book stores buy and sell orders in the same array with a `side` field, but settlement iterates without filtering by side
- A positions collection mixes collateralized and uncollateralized positions, but liquidation logic applies uniformly
- A function reads the denomination from a type-level config struct but the matched item in the shared collection was created under a different type's denomination
- Cancel function for type A matches a type B item and refunds using type A's denomination instead of the item's stored denomination

### UNI-103: Payment-Gated Transfer Allows Beneficiary Mismatch
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a transfer function looks up payment amount by recipient address, but the caller can specify any recipient including one with no payment, causing a zero-payment asset transfer.
**Pattern**: A function combines asset transfer with payment settlement. The payment amount is looked up from a mapping or list keyed by the recipient address (e.g., bids, deposits, escrow entries). The caller can freely specify the recipient parameter. If the recipient has no entry in the payment mapping, the amount defaults to zero and the transfer proceeds without payment to the previous owner. Alternatively, the caller specifies a recipient different from themselves to avoid their own payment being consumed, then cancels their payment entry for a full refund.
**Methodology**:
1. For every function that transfers an asset AND looks up a payment amount by a caller-supplied address parameter:
   a. Check whether the function reverts when no payment entry exists for the specified recipient (amount == 0 case).
   b. Check whether the function validates that the payment amount meets the listed/required price.
   c. Check whether the caller is constrained to specify themselves as the recipient, or can specify any address.
2. If the function proceeds with transfer when `amount == 0` (no matching payment), flag as HIGH — the asset is transferred for free.
3. If the function does not validate `amount >= listed_price`, flag as HIGH — the asset can be transferred for less than the listed price.
**Red flags**:
- `amount = bids[recipient].offer` defaults to 0 when no bid exists, and the function has a branch that proceeds with transfer when `amount == 0`
- Transfer function accepts `recipient` as a parameter (not forced to `msg.sender`), allowing the caller to route the transfer to an address with no active bid
- Caller can: (1) place a bid to gain approval, (2) call transfer with a DIFFERENT recipient who has no bid (zero payment), (3) cancel their own bid for full refund
- No `require(amount >= listed_price)` check between the payment lookup and the ownership transfer
- The `amount > 0` branch sends payment to the previous owner, but the `amount == 0` branch still transfers ownership

### UNI-104: Numeric Type Width Insufficient for Token Decimals
**Provenance**: drozer-lite v0.4.1 — class-of-bug: price or amount parameters use a narrower integer type than the token amounts they interact with, making normal-value operations impossible for high-decimal tokens.
**Pattern**: Price, amount, or rate parameters in message/function signatures use a narrower integer type (e.g., `u64`, `uint64`, `uint32`) than the token amount type used in the contract's arithmetic (e.g., `u128`, `Uint128`, `uint256`). For tokens with 18 decimals, `u64` maxes at ~18.4 tokens — any price above ~$18 for a $1-token is unrepresentable. This creates a functional ceiling where normal-value operations silently fail or are impossible to express.
**Methodology**:
1. For every price, amount, or rate parameter in external function signatures and message structs, note the integer type width.
2. For each such parameter, trace how it's used in arithmetic with token amounts. Note the token amount type (typically the widest type in the system).
3. If the parameter type is narrower than the token amount type AND the protocol accepts arbitrary user-specified denominations (tokens with varying decimals), flag.
4. Calculate the practical ceiling: `max_value / 10^decimals` for common decimal counts (6, 8, 18). If the ceiling is below reasonable real-world values for the parameter's purpose (e.g., < $1000 for a rental price), flag.
**Red flags**:
- `price_per_day: u64` but `deposit_amount: Uint128` (u128) in the same system
- `amount: u64` in a withdrawal function but `info.funds[0].amount` is `Uint128`
- `fee: u64` but fee is multiplied with `Uint128` amounts, silently capping the effective fee range
- Any `u64` price/amount field in a protocol that accepts arbitrary token denominations (user-chosen, not hardcoded)
- Withdrawal function requires multiple calls to extract a normal-value deposit because the per-call `amount` parameter is too narrow

### UNI-105: Stored Constraint Not Enforced at Consumption Point
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a configuration function stores a constraint field (availability window, whitelist, maximum, deadline) but the consuming function that should enforce it never reads or checks it, making the constraint decorative.
**Pattern**: A configuration or listing function accepts and stores a constraint parameter (available period, whitelist, max participants, allowed tokens, deadline, minimum amount, geographic restriction). The consuming function that should enforce this constraint (reservation, deposit, bid, claim, register) operates on the same entity but never reads or validates the stored constraint. The constraint exists in storage but has zero enforcement — users can bypass it simply by never encountering a check.
**Methodology**:
1. For every configuration/listing function, enumerate every field it writes to storage.
2. For each stored field, classify it as: (a) data field (description, name, URI — informational), or (b) constraint field (period, whitelist, max, min, deadline, rate — should restrict behavior).
3. For each constraint field, find ALL consuming functions that operate on the same entity. Verify the constraint field is READ and produces a REVERT or behavioral change in each consumer.
4. If a constraint field is stored but never read by any consuming function, flag as MEDIUM — the feature is broken, not just missing.
**Red flags**:
- `available_period` set in listing function but reservation function checks `minimum_stay` only, ignoring `available_period` entirely
- `max_participants` stored on entity creation but join function has no cap check
- `allowed_tokens` whitelist stored but deposit function accepts any denomination
- `deadline` stored but claim function checks `block.timestamp` against a different value
- `auto_approve` flag stored for one rental type but the approval function for that type never reads it
- Any field in a config struct that is written in the setter and read ONLY in query/view functions (never in state-changing functions)

### UNI-106: Listing-Gate Bypass on Unlisted Entities
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a function that should only operate on listed/active entities does not check the listing status flag, allowing operations on unlisted, delisted, or never-listed entities.
**Pattern**: An entity has a listing status field (`is_listed`, `active`, `status`, `enabled`) that is set by a listing function and cleared by an unlisting function. Consumer functions (bid, reserve, purchase, deposit, subscribe) that should only operate on listed entities do not check the listing status. This allows: (1) operations on entities that were never listed, (2) operations on entities that were explicitly delisted, (3) exploitation of stale configuration (e.g., `auto_approve` from a previous listing) on a currently unlisted entity.
**Methodology**:
1. For every entity with a listing/status flag, enumerate: (a) the function that sets it to active/listed, (b) the function that sets it to inactive/unlisted, (c) all consumer functions that operate on the entity.
2. For each consumer function, verify it checks the listing status flag early in execution (before accepting funds, granting approvals, or modifying state).
3. Pay special attention to configuration fields that PERSIST across list/unlist cycles. If `auto_approve`, `price`, `denomination`, or other economic parameters are set during listing and NOT cleared during unlisting, check whether consumers of these fields are guarded by the listing status.
4. If a consumer function accepts funds or grants permissions without checking listing status, flag. Severity depends on whether the stale configuration enables value extraction.
**Red flags**:
- `bid()` function does not check `is_listed == true` before accepting funds and granting approval
- `reserve()` function accepts deposits for unlisted properties/assets
- `purchase()` function operates on delisted items using stale price/denomination from a prior listing
- Unlisting function sets `is_listed = false` but does NOT clear `auto_approve`, `price`, or `denomination` — these persist and are used by consumer functions that skip the listing check
- Any consumer function that reads economic parameters (price, denomination, approval mode) from the entity without first verifying the entity is currently listed

### UNI-107: Nested Loop Depth Exceeds Gas Budget
**Provenance**: drozer-lite v0.4.2 — class-of-bug: nested iteration over user-growable collections produces O(N^k) complexity with k≥2, exceeding the block gas limit and permanently DoSing the function for affected users.
**Pattern**: A function iterates over collection A (size P). For each item, it iterates over collection B (size F). For each pair, it iterates over a range R (size E). The total work is O(P × F × E). Even with individual caps on P, F, and E, the PRODUCT can exceed the block gas limit. This is distinct from UNI-12 (single unbounded loop) — the issue is the nesting depth, not any single loop being unbounded.
**Methodology**:
1. For every function with nested loops (loop inside a loop), compute the worst-case product of all loop bounds.
2. For each bound, determine: is it hardcoded? Is it configurable? Is it user-determined? Can it grow over time (e.g., epochs since first action)?
3. Compute worst-case iterations: multiply all bounds. If the product exceeds ~50,000 (conservative gas budget for CosmWasm/EVM), flag.
4. Check whether the function can be called in batches (e.g., claim per-position, claim per-epoch range). If no batching mechanism exists and the function is mandatory (e.g., claim before close), the DoS is permanent.
**Red flags**:
- `for position in positions { for farm in farms { for epoch in start..=current { ... } } }` — O(P×F×E)
- Reward calculation that iterates over all epochs since a user's first deposit with no epoch-range parameter
- `close_position` requires `claim()` first, and `claim()` has O(N^3) complexity — DoS on claim blocks close
- No "partial claim" or "skip positions" mechanism exists
- Config parameters cap individual collections (e.g., max 100 positions, max 10 farms) but their product (1000+) is not capped

### UNI-108: Temporal Parameter Allows Retroactive / Past Values at Creation
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a creation function accepts a user-supplied temporal parameter (start time, start epoch, activation date) without enforcing it is in the future, allowing retroactive entity creation that breaks reward distribution, billing, or scheduling invariants.
**Pattern**: An entity with a time-based lifecycle (farm, vesting schedule, auction, subscription, rental) accepts a `start_time` or `start_epoch` parameter at creation. The parameter is validated for basic sanity (> 0, < end) but is NOT validated against the current time/epoch. This allows creating entities that "started in the past," retroactively assigning rewards, obligations, or access to historical periods that other participants have already settled.
**Methodology**:
1. For every creation function that accepts a start_time/start_epoch parameter, verify it is enforced as `>= current_time + 1` or `>= current_epoch + 1`.
2. Check the default value when the parameter is omitted — if it defaults to `current + 1`, verify the explicit path has the same constraint.
3. Trace what happens if start is set to a past value: are rewards retroactively assigned? Do billing periods extend into the past? Can the creator claim historical periods?
**Red flags**:
- `start_epoch = params.start_epoch.unwrap_or(current_epoch + 1)` but no `ensure!(start_epoch >= current_epoch + 1)` for the explicit case
- Validation checks `start < end` and `end > current` but not `start > current`
- A farm/schedule created with past start_epoch assigns emissions to epochs where participants already claimed, creating unfair distribution
- Default path is safe (`current + 1`) but explicit path bypasses the constraint

### UNI-109: Self-Call Identity Confusion
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a contract calls itself (via submessage, internal execute, or self-invoke) and the called function uses `info.sender` for authorization, but the sender is now the contract itself instead of the original user, causing authorization checks to fail or be bypassed.
**Pattern**: A function performs a two-step operation by calling itself: step 1 initiates (stores context in a buffer), step 2 is triggered via a self-call (SubMsg or wasm_execute to self). The second step's `info.sender` is the contract's own address, not the original user. If step 2 has an authorization check like `require(sender == receiver)` or `require(sender == user)`, it fails because sender is the contract. Conversely, if step 2 has an authorization check like `require(sender == admin || sender == contract)`, the self-call bypasses user-level restrictions.
**Methodology**:
1. For every SubMsg or wasm_execute that targets the contract's own address (`env.contract.address`), identify the function being called.
2. Check what `info.sender` is used for in the called function. If it's used for authorization, it will be the contract address, not the original caller.
3. Check whether any receiver/beneficiary validation compares against `info.sender` — this will fail for the self-call case.
4. Check whether any privilege check accepts the contract's own address — this could be a bypass vector.
**Red flags**:
- `wasm_execute(env.contract.address, &ExecuteMsg::ProvideLiquidity { receiver: user, ... }, funds)` where `ProvideLiquidity` checks `ensure!(receiver == info.sender)` — fails because info.sender is the contract
- Two-step LP provision: step 1 swaps half, step 2 provides balanced LP. Step 2's sender check rejects the self-call.
- A singleton buffer stores context for the reply handler — if two users trigger step 1 in the same block, the second overwrites the first's buffer
- Any function with `if info.sender == env.contract.address { /* special path */ }` that grants elevated privileges

### UNI-110: Permissionless Entity Creation Bypasses Protocol-Intended Parameters
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a permissionless creation function allows the creator to specify parameters that the protocol intended to control (e.g., fee rates, reward schedules), enabling creators to set these to zero or adversarial values to the protocol's detriment.
**Pattern**: A permissionless function (e.g., create pool, create farm, register market) accepts parameters that affect protocol revenue or user protections. These parameters are stored per-entity and used in subsequent operations. The protocol intended to enforce minimum values (e.g., minimum protocol fee, minimum collateral ratio) but the creation function either has no minimum check or the minimum is 0. Creators can set `protocol_fee = 0`, `collateral_ratio = 0`, or `insurance_fund_share = 0` to attract users while depriving the protocol of revenue or safety margins.
**Methodology**:
1. For every permissionless creation function, enumerate every parameter that is stored per-entity and affects protocol revenue or user protection.
2. For each such parameter, check whether a protocol-level minimum is enforced. If the only validation is `fee.is_valid()` (which may only check `< 100%`), a zero value passes.
3. Check whether the protocol has a global/config-level fee that overrides per-entity fees. If not, the per-entity fee IS the protocol fee.
4. Compare against industry standard: Uniswap charges a protocol fee at the factory level; Curve charges admin fees globally. If this protocol charges fees per-entity with no floor, flag.
**Red flags**:
- `create_pool(pool_fees: PoolFee)` where `pool_fees.protocol_fee` can be set to 0 by the creator
- `is_valid()` only checks `fee < 100%`, not `fee >= MINIMUM_PROTOCOL_FEE`
- No global fee override exists — the per-entity fee is the only fee
- Protocol documentation states "fees are collected on every swap" but code allows zero-fee pools
- Pool/farm/market creator can front-run legitimate creation with a zero-fee version to attract liquidity away from fee-bearing entities

## .claude/skills/drozer-lite/checklists/vault.md

# Vault Checklist

> Profile: vault
> Checks: 6
> Source: ported from Drozer-v2 vault-invariants.md (provenance cited per check)

## Methodology

Vaults intermediate a share/asset conversion that an attacker will try to manipulate. For every path that converts between shares and assets, identify (a) what controls the numerator and denominator, (b) whether an attacker can influence either atomically (donation, flash-loan, first-deposit), (c) the rounding direction, and (d) whether `totalAssets()` reflects manipulable external state. Test boundary conditions explicitly: `totalSupply == 0`, single wei deposits, full redemption leaving dust, last-strategy removal, and paused external integrations.

## Checks

### VAULT-1: First-Depositor Share Inflation
**Provenance**: vault-invariants.md V3 + V12
**Pattern**: The first depositor mints 1 wei of shares, donates a large amount of the underlying asset, and subsequent depositors round to zero shares; the first depositor then withdraws everything.
**Methodology**: For any share-issuance contract, check whether `_convertToShares` uses virtual offsets (OZ ERC4626 pattern), a minimum initial deposit, or mints dead shares on first deposit. Test the `totalSupply == 0` branch explicitly. Verify whether direct token transfers to the vault affect `totalAssets()`.
**Red flags**:
- `shares = assets * totalSupply / totalAssets` with no virtual offset
- `totalAssets()` returns `token.balanceOf(this)` (donation-manipulable)
- No minimum deposit and no dead-share mint on first deposit

### VAULT-2: ERC-4626 Preview / Max Consistency
**Provenance**: vault-invariants.md V13
**Pattern**: `preview*` and `max*` functions return values inconsistent with actual `deposit`/`withdraw` execution (missing fees, missing pause, wrong rounding direction), breaking external integrations.
**Methodology**: For each of `maxDeposit`, `maxMint`, `maxWithdraw`, `maxRedeem`, verify it reflects actual enforced limits (paused state, whitelist, internal caps) — not `type(uint256).max`. For each of `previewDeposit`, `previewMint`, `previewWithdraw`, `previewRedeem`, verify fees are included and rounding matches the spec (`previewMint` rounds UP, `previewWithdraw` rounds UP).
**Red flags**:
- `maxWithdraw` returns balance while paused
- `previewDeposit` ignores entry fee
- Preview rounds different direction than actual execution

### VAULT-3: Pause Completeness
**Provenance**: vault-invariants.md V14
**Pattern**: When paused, user-facing operations are blocked but admin functions (`rebalance`, `harvest`, `compound`, `migrateStrategy`) still move user assets — admin can act while users cannot exit.
**Methodology**: Enumerate every state-changing function. For each, check whether it is gated by `whenNotPaused`. Any asset-moving admin function that is NOT gated is a trap vector.
**Red flags**:
- `rebalance()` callable while `withdraw()` is paused
- Strategy migration runnable during emergency pause

### VAULT-4: Return-Value Semantics on Deploy/Undeploy
**Provenance**: vault-invariants.md V16 + V11
**Pattern**: `deploy()` / `undeploy()` returns the ACTUAL amount (post-slippage, post-fees), but callers use the REQUESTED amount for downstream accounting.
**Methodology**: For each deploy/undeploy call, check whether the return value or the input parameter is used for `_deployedAmount` bookkeeping, for pro-rata allocation, and for return-to-user amounts. Verify `_deployedAmount` is decremented on undeploy.
**Red flags**:
- `strategy.undeploy(amountRequested); _deployedAmount -= amountRequested;` instead of using the return value
- Multi-strategy withdrawal using requested-amount math
- Leverage undeploy returning 90% with 10% silently lost

### VAULT-5: Strategy Migration & Constructor Validation
**Provenance**: vault-invariants.md V7 + V17
**Pattern**: Strategies are added without validating the underlying protocol's expected asset, and migration does not fully unwind the old strategy before activating the new one.
**Methodology**: For each strategy constructor, verify it checks that the configured asset matches the underlying protocol's expected token. Verify `addStrategy` rejects duplicates and grants the token approval. Verify `removeStrategy` revokes the approval. Verify `migrateStrategy` fully unwinds before activating the replacement and has a timelock / user exit window.
**Red flags**:
- No `require(market.loanToken() == asset)` in constructor
- Migration path that leaves old strategy still approved
- Removed strategy retains unlimited allowance

### VAULT-6: Access Control Principal (Receiver vs Caller)
**Provenance**: vault-invariants.md V18
**Pattern**: `deposit(assets, receiver)` checks `msg.sender` against a whitelist instead of checking `receiver`, allowing a whitelisted user to deposit on behalf of any non-whitelisted address.
**Methodology**: For every function accepting `receiver`/`owner`/`beneficiary`, check which principal is validated against whitelists/limits. `maxDeposit(address)` must accept `receiver` as the limit target.
**Red flags**:
- `require(isWhitelisted[msg.sender])` on a function that credits shares to `receiver`
- `maxDeposit` read against `msg.sender` when `deposit` credits to `receiver`

## .gitignore

```

```

## CHANGELOG.md

# Changelog

## v0.5.7 — Broadened CEI discriminator, schema-mismatch LOW drop, shared-check consolidation (2026-04-19)

**Headline**: Three targeted prompt-level fixes derived from cross-run per-finding analysis. All three convert advisory prose into mechanically checkable rules at the emit step without introducing worksheet-level agent judgment. Follows the v0.5.6 design principle: mechanical lookups and string-level tests work; agent-judgment gates don't.

### What changed

1. **Broadened CEI discriminator** in the Reentrancy / external call ordering vocabulary section. Previously, `checks_effects_interactions_violation` was marked "default tag unless the proven exploit path specifically requires a re-entrant callback to drain." In cross-run analysis, this discriminator sent callback-reentry drains to the `reentrancy` tag while external scoring rubrics consistently expect `checks_effects_interactions_violation` for ANY bug whose fix is "update state before the external call" — regardless of whether a callback was involved. The new discriminator: if reordering the function body to Checks → Effects → Interactions eliminates the exploit, emit `checks_effects_interactions_violation`. `reentrancy` is reserved for exploits that persist even with correct CEI ordering (cross-function reentrancy, shared-state reentry). The alias table entry for `reentrancy` → `checks_effects_interactions_violation` was expanded to match the new discriminator.

2. **Schema-mismatch LOW drop rule** added to Step 7 rule 1a (severity-tier output filter). The v0.5.1 rule sends LOW and INFO findings to `warnings[]`, but when the output schema does not contain a `warnings` field (external benchmark schemas, narrow CI harnesses accepting only `findings[]`), agents were observed flattening LOW/INFO into `findings[]` to preserve the observation — converting hardening notes into false positives against the scoring rubric. The new rule: when the output schema has no `warnings` field, LOW/INFO findings MUST be dropped entirely. Do not flatten. Detection is mechanical: if the output format specification (program.md, JSON schema, API contract) enumerates allowed fields and `warnings` is not among them, the rule applies.

3. **Shared-check consolidation mechanical shortcut** added to Step 7.2 root-cause consolidation. The v0.5.1 "could one PR fix all of them?" test requires agent judgment about whether fixes are "the same" — cross-run analysis showed agents inconsistently classified siblings that add the same missing check. The new Rule 2(a) fires BEFORE the PR test: if N findings' fixes all add the SAME named check (same `require`/`assert`/modifier/validated variable) — e.g. all N fixed by adding `require(!disputed)`, or all N adding the same missing nonce mapping — consolidate automatically. "Different function names alone are not grounds to keep separate" when the missing check is identical. The title generalises to the missing check, not any single function.

### Why these are not worksheet-level enforcements

Each of the three changes produces a mechanical test at emit time:

- Change 1: string-level tag rewrite (same class as the v0.5.5 alias table that survived).
- Change 2: schema introspection (is `warnings` in the allowed fields?) → conditional drop.
- Change 3: name equality test (is the missing check's identifier the same across the group?) → consolidate.

None ask the agent to self-classify evidence, re-read worksheets, or judge "would a PR fix this" — the three gates that v0.5.5 introduced and v0.5.6 reverted. Each new rule is an extension of an existing mechanical rule (alias rewrite, severity-tier filter, dedup grouping).

### Anti-bloat audit

| Change | Type | Lines | Files modified |
|---|---|---|---|
| Broaden CEI discriminator | extend | ~4 | SKILL.md |
| Expand alias table entry | extend | ~1 | SKILL.md |
| Schema-mismatch rule | extend | ~3 | SKILL.md |
| Shared-check consolidation rule 2(a) | extend | ~5 | SKILL.md |
| Version bump + CHANGELOG | edit | +~40 | SKILL.md, CHANGELOG.md |
| **Total** | — | **~+13 net in SKILL.md** | **2 files** |

No new checklists. No new worksheet fields. No new steps. All four changes extend existing prose rules with mechanical tests.

### What is NOT being attempted in v0.5.7

- No new vulnerability patterns. Checklists unchanged.
- No pattern-specific allowlists or denylists.
- No worksheet-level enforcement (v0.5.3 and v0.5.5 both failed this way; the v0.5.4/v0.5.6 CHANGELOGs document why).
- No second-agent review. Still the known residual gap.

## v0.5.6 — Revert v0.5.5 worksheet extensions; keep alias canonicalization only (2026-04-19)

**Headline**: Three of the four v0.5.5 changes were worksheet-level enforcement additions that single-agent runs demonstrably did not engage — cross-run validation showed the `warnings[]` telemetry that the new rules were designed to emit was always empty, indicating agents skipped the mechanics entirely. The regression pattern is identical to v0.5.3: adding single-agent worksheet strictness predicts regressions, and the v0.5.4 CHANGELOG already documented this. v0.5.6 reverts the three non-engaging changes and keeps only the alias canonicalization table, which is a mechanical string rewrite and demonstrated clean wins without side effects.

### What was reverted (3 of 4 v0.5.5 changes)

1. **Worksheet field 7 — Evidence class A/B/C/D with binding severity caps** — REVERTED. The field required agents to self-classify evidence and auto-cap severity for classes C and D. In practice, agents either skipped the field, classified everything as class A, or classified valid findings as class C and silently dropped them. The `warnings[]` telemetry for cap applications was never emitted in cross-run validation. The field introduced a regression channel (valid findings getting class C cap) without producing wins (weak-evidence findings still emitted above the cap).

2. **Step 7.0a — Independent re-read** — REVERTED. The step asked agents to re-evaluate each worksheet using only the worksheet fields (simulating independent-context review). The `warnings[]` entries for drops never appeared; instead the step appears to have provided a rationalization path for dropping findings the agent was already unsure about. Same regression channel as v0.5.3's named-reference requirement — worksheet-level strictness surfaced as FN in cross-run validation. The v0.5.4 CHANGELOG's prediction that "single-agent worksheet enforcement is unlikely to break past the v0.5.2 baseline" held.

3. **Step 7.2 same-symbol consolidation tightening** — REVERTED to the v0.5.1 prose formulation. The mechanical "same named symbol" test didn't produce fewer emissions in cross-run validation; agents continued to split sibling findings using surrounding-context arguments. The tightening added rule surface area without changing behavior. Reverted to the simpler `"could one PR fix all of them?"` test.

### What was kept (1 of 4)

4. **Alias canonicalization table** — KEPT. This is a mechanical string rewrite at emit time with no agent judgment required. Cross-run validation showed clear wins (agents now consistently emit the SWC/Code4rena/Sherlock unabbreviated canonical for `tx_origin_authentication`, `checks_effects_interactions_violation`, etc.) with no observed regressions. The table is the v0.5.5 change that belongs in the class of "mechanical lookup, not reasoning" — the same class as v0.5.2's vocabulary discipline paragraph.

### Design principle reaffirmed by this revert

The v0.5.4 CHANGELOG named the structural constraint: **single-agent worksheet enforcement has a ceiling**. Adding more fields, more gates, or stricter prose within the same single-agent loop does not break the ceiling — it just adds regression surface. The v0.5.5 experiment confirmed this for the third time (v0.5.3 experiment, v0.5.4 prediction, v0.5.5 confirmation). Future precision work at this layer should either be (a) mechanical lookups like the alias table, (b) content additions to the canonical vocabulary (new canonical tags for common paraphrases), or (c) checklist-level changes that target specific false-positive classes with code-level guards. Worksheet-level agent-judgment additions should be treated as predicted regressions pending a structurally different validation mechanism (e.g., independent second-agent review actually implemented as a separate agent invocation, not simulated within one agent).

### Anti-bloat audit

| Change | Type | Lines | Files modified |
|---|---|---|---|
| Revert worksheet field 7 | revert | −2 | SKILL.md |
| Revert Step 7.0a section | revert | −13 | SKILL.md |
| Revert Step 7.2 tightening | revert | −5 | SKILL.md |
| Keep alias canonicalization | keep | 0 | SKILL.md |
| Version bump + CHANGELOG | edit | +~45 (this entry) | SKILL.md, CHANGELOG.md |
| **Total** | — | **~−20 net in SKILL.md** | **2 files** |

### What is NOT being attempted in v0.5.6

- No new vulnerability patterns. Checklists are byte-identical to v0.5.4/v0.5.5.
- No replacement enforcement mechanism for the reverted rules. The weak-evidence severity floor prose from v0.5.1 remains as advisory guidance; agents apply it at their discretion (as before).
- No second-agent validation. That's the known residual gap; shipping it would require changes outside the single-agent skill, out of scope here.

## v0.5.5 — Binding worksheet, alias canonicalization, independent re-read (2026-04-19)

**Headline**: Four targeted precision fixes that move existing advisory rules into binding worksheet fields. No new vulnerability patterns, no new checklists, no new agents. All four fixes address gaps identified by the post-audit improvement protocol against independent scoring rubrics — each one has an external-standards justification (SWC Registry / Code4rena / Sherlock vocabulary, weak-evidence audit-maturity taxonomy, independent-context review practice, symbol-level fix consolidation from Trail of Bits report guidance).

### What changed

1. **Alias canonicalization table in the vocabulary section** — adds a mechanical alias → canonical rewrite table. Before emitting `vulnerability_type`, the agent now performs a lookup (not reasoning) to convert common LLM paraphrases into the unabbreviated industry-standard form. Addresses: agents emitting `tx_origin_auth` when SWC-115 is `tx_origin_authentication`; emitting `reentrancy` when the exploit requires only CEI ordering to be wrong (discriminator: `checks_effects_interactions_violation` is the default, `reentrancy` is for proven callback drain). The table lists known paraphrases. Adding paraphrases to the table is expected maintenance; adding benchmark-specific mappings is forbidden per the Check Authorship Rules.

2. **Worksheet field 7 — Evidence class with binding severity caps** — adds a REQUIRED seventh field to the Step 7.0 pre-emission worksheet. Each candidate is classified A / B / C / D. Caps: A no cap, B HIGH, C LOW (moves to warnings via Step 7.1a tier filter), D DROP. The "Weak-evidence severity floor" section that already existed in v0.5.1 is now bound to a worksheet field and therefore mechanically enforceable — emitting a class C finding above LOW produces a `warnings[]` entry and the severity is rewritten. Before this change, the floor was prose-advisory and agents could emit above the cap by writing prose that argued around it.

3. **Step 7.0a — Independent re-read** — new step between worksheet completion and Gate A. The agent re-reads each worksheet using ONLY fields 1-7 (pretending no source access and no prior reasoning) and must answer four consistency questions. Any NO answer drops the finding with a `warnings[]` entry. This is the single-agent approximation of the independent-context second-agent validation that the v0.5.4 CHANGELOG identified as the known residual gap. The worksheet becomes the audit trail; findings that survive only because of accumulated in-context reasoning fail the re-read.

4. **Step 7.2 root-cause consolidation — mechanical same-symbol test** — replaces the prose "could one PR fix all of them?" test with an extraction-based test. The agent reads field 3 (specific-line break) from each worksheet in the group and checks whether all fixes touch the same named symbol (same function body, same shared helper, same typehash, same mapping). Same symbol → consolidate. Different symbols → separate. Surrounding syntactic differences ("one uses EIP-712 and the other doesn't") are explicitly not valid reasons to keep separate. This closes the case-class where two sibling functions with identical fix patterns were split into two findings, producing an FP penalty against scoring rubrics that expect one consolidated finding per root cause.

### Anti-bloat audit

| Change | Type | Lines | Files modified |
|---|---|---|---|
| Worksheet field 7 (Evidence class) | extend | +2 | SKILL.md |
| Step 7.0a (Independent re-read) | insert | +13 | SKILL.md |
| Step 7.2 consolidation tightening | replace | +5 net | SKILL.md |
| Alias canonicalization table | insert | +15 | SKILL.md |
| Version bump + CHANGELOG | edit | +38 (this entry) | SKILL.md, CHANGELOG.md |
| **Total** | — | **~35 net in SKILL.md** | **2 files** |

No new checklists. No new profiles. No new hard rules. All four changes are mechanical enforcement of rules that already existed as advisory prose. Per the Post-Audit Improvement Protocol anti-bloat gates: line budget check PASS (SKILL.md 595 → ~630, well below the 1100 cap); duplication check PASS (each change is in a single file); marginal value check PASS (each change is a mechanical gate, not a new pattern check); overlap check PASS (the independent re-read is not Gate B — Gate B looks for dismissal phrases to RESTORE findings, the re-read looks for consistency failures to DROP findings; opposite directions).

### What is NOT being attempted in v0.5.5

- No new vulnerability patterns. Checklists are byte-identical to v0.5.4.
- No pattern-level allowlists or denylists. The precision fixes operate on structure (worksheet fields, mechanical tests), not on bug classes.
- No benchmark-specific adjustments. Every change has a non-benchmark justification cited in its header line.
- No replacement of the v0.5.2 worksheet baseline. Field 7 extends the worksheet; it does not replace any existing field.

## v0.5.4 — Revert v0.5.3; restore worksheet baseline (2026-04-17)

**Headline**: v0.5.3's two-quote / named-reference tightening of worksheet field 3 was a regression in clean-room cross-run validation. Rolled back to the v0.5.2 worksheet contract. SKILL.md content is byte-identical to v0.5.2 except for version strings.

### What v0.5.3 broke

The named-reference safe-form requirement penalized findings whose canonical fix is a one-line check rather than a structural pattern (input validation, missing access control, missing condition checks). Agents marked these as `textbook=Y`, then could not produce a verbatim safe form from a NAMED industry reference (because no such named reference exists for "add the missing require"), so the finding dropped. Net effect in cross-run validation:
- Multiple legitimate findings dropped (regressions on cases where v0.5.2 scored 1.0 or near-perfect).
- One previously-clean case gained a new false positive.
- The intended target (pattern-spam emissions on textbook DEX/share/balance patterns) was only marginally suppressed.

### Root cause of the v0.5.3 design failure

1. **Field 2's textbook list was not exhaustive** ("similar known patterns" / "etc."). Agents extrapolated unpredictably and over-marked findings as textbook=Y, dragging legitimate findings into the new strict field 3.
2. **The named-reference requirement assumed every textbook pattern has a documented safe form in industry sources**. This is true for structural patterns (CEI, ERC4626 first-depositor, Curve invariant) but false for one-line-fix patterns (input validation bounds, missing access guards). Penalizing findings of the latter class for not having a "named reference" is wrong.
3. **The change did not address the actual structural problem**: pattern-matching scanners always emit when patterns are present, and "this contract is intentionally a teaching fixture" is a judgment that pattern-level enforcement cannot make. Worksheet enforcement at the single-agent level cannot fully solve this.

### What is preserved from v0.5.2

All of the v0.5.2 changes that did real work in cross-run validation:
- Step 7.0 worksheet (6-field structured commitment).
- No-silent-drops rule.
- Gate C visibility entries in `warnings[]`.
- Vocabulary discipline rules (unabbreviated canonical, near-synonym discriminator).
- `tx_origin_authentication` and `checks_effects_interactions_violation` as canonical tags.

### What is NOT being attempted in v0.5.4

No replacement for the v0.5.3 textbook-break tightening. The lesson from this iteration is that further single-agent worksheet enforcement is unlikely to break past the v0.5.2 baseline — the residual gap requires either (a) a structurally different validation mechanism (e.g., independent-context second-agent validation of worksheet entries), or (b) acceptance of the v0.5.2 baseline as the current ceiling pending a different evaluation corpus. No new methodology change ships in v0.5.4.

### Anti-bloat audit

| Change | Type | Lines | Files modified |
|---|---|---|---|
| Field 3 cell — restored to v0.5.2 wording | revert | net −2 | 1 |
| Field 3 enforcement rationale paragraph — removed | revert | net −5 | 1 |
| Version bump (v0.5.3 → v0.5.4) | edit | net 0 | 1 |
| **Total** | — | **−7 net** | **1 file (SKILL.md)** |

SKILL.md shrinks from 597 → 595 lines (back to v0.5.2 size).

---

## v0.5.3 — Two-quote textbook break (2026-04-17)

**Headline**: Methodology-only refinement to the v0.5.2 worksheet. Targets the residual failure mode where agents fill worksheet field 3 with vague phrasing ("should add nonReentrant", "needs slippage parameter") that satisfies the structural check but doesn't actually prove a textbook deviation. Addresses pattern-spam emissions on contracts where the patterns are present but the textbook safe form is genuinely matched (or has acceptable alternatives).

### Methodology change

**Worksheet field 3 — two-quote requirement**. When `textbook=Y` (field 2), field 3 now requires:
1. The offending code as it exists in the current source, quoted verbatim with `file:line`.
2. The textbook-safe equivalent quoted verbatim from a NAMED industry reference (OpenZeppelin / Uniswap V2 or V3 / ERC4626 spec / SWC mitigation / Chainlink integration guide / Curve / etc.).
3. A one-sentence statement of the structural gap between (1) and (2).

The finding DROPS if any of:
- Either code quote is missing or paraphrased rather than verbatim.
- No recognized reference applies (in which case field 2 was wrongly marked Y — the finding is not a textbook-class case).
- The structural gap is just a hardening pattern with acceptable alternatives (e.g., dead-share mint on first deposit instead of OZ virtual offsets, balance-after deltas instead of explicit reentrancy guard, post-transfer detection instead of blocklist enforcement).

### Rationale

Pattern presence is the easiest property to over-claim. The v0.5.2 worksheet made Step 5 rule 4a a REQUIRED field, but a REQUIRED field that accepts handwave defeats its own purpose. Forcing two verbatim quotes plus a NAMED reference creates structural pressure: the agent either produces the comparison or admits the textbook-pattern claim was unsupported. The named-reference requirement specifically addresses the failure mode where every contract using `.call`, every swap function, or every receive() function gets flagged regardless of whether a recognized industry-standard safe form exists for the claimed pattern.

### Validation method

Per `post-audit-improvement-protocol.md`: the change addresses an RC-AGENT cluster that the v0.5.2 worksheet alone did not fully resolve, observed in cross-run validation where clean-fixture contracts continued to attract textbook-pattern emissions despite the v0.5.2 enforcement. The two-quote requirement does not add any new check — it tightens the existing field 3 contract.

### Not changed

- `checklists/*.md` — zero edits.
- Vocabulary section, severity decision table, profile detection, clustering, cross-cluster sweep — unchanged.
- All Step 5 hedging rules and Step 7 gates A/B/C — unchanged.
- No benchmark-specific keywords, identifiers, function names, or pattern descriptions added anywhere.

### Anti-bloat audit

| Change | Type | Lines added (SKILL.md) | Files modified |
|---|---|---|---|
| Field 3 cell tightening | edit | net +2 | 1 |
| Field 3 enforcement rationale | extend | ~5 | 1 |
| Version bump + header | edit | net 0 | 1 |
| **Total** | — | **+7 net** | **1 file (SKILL.md)** |

SKILL.md grows from 595 → 597 lines. Worksheet structure unchanged; only field 3 contract is tightened.

---

## v0.5.2 — Worksheet-enforced emission + vocabulary discipline (2026-04-17)

**Headline**: Methodology-only changes addressing two failure classes observed in cross-run validation: (1) v0.5.1's gates and Step 5 rule 4a are sound but get skipped when SKILL.md is read as a manual fallback (parallel sub-agent invocation, low-context runs), and (2) two canonical vocab tags lost points to industry-standard rubrics due to abbreviation mismatch and an undefined discriminator between near-synonyms. Zero checklist edits, zero new checks. No benchmark-specific patterns or identifiers added.

### Methodology changes

1. **Pre-emission worksheet (Step 7.0, MANDATORY)**. Promotes Step 5 rule 4a, Gate A, Gate C, and the Severity decision table from prose to a 6-field structured worksheet that every candidate finding must fill before any other gate runs. REQUIRED fields with no code-backed value force a DROP. The worksheet is the mechanical commitment that the existing discipline rules were each consulted for this specific finding — eliminates the failure mode where prose-format gates are skipped under fallback invocation.

2. **No-silent-drops rule**. Every drop, whether from worksheet failure, Gate A, Gate C downgrade-then-LOW-suppression, or any other filter, MUST emit a `warnings[]` entry of the form `"dropped: <title> | <reason>"`. Silent drops hide regressions and prevent post-mortems from distinguishing "the gate fired correctly" from "the gate was skipped." This is a diagnostic improvement, not a precision/recall change.

3. **Gate C visibility (mandatory)**. Every Gate C decision — downgrade, drop, OR kept-at-original-severity — emits a `warnings[]` entry of the form `"defender_applied: <title> | <defender>"` (or `"defender_none: <title> | no mitigation visible"`). Makes the gate's reasoning auditable post-hoc; complements the no-silent-drops rule above.

4. **Vocabulary discipline — unabbreviated industry-standard form is canonical**. Where a vocab tag exists in both an abbreviated and a full form, the canonical tag now matches the unabbreviated form used by SWC Registry / Code4rena / Sherlock. External scoring rubrics match strings literally; abbreviations lose points to no benefit. Concrete: `tx_origin_authentication` is now canonical, `tx_origin_auth` is the alias (not vice versa as in v0.5.1). General rule documented in the vocabulary section header.

5. **Vocabulary discipline — near-synonym discriminator is mandatory**. Where two canonical tags describe overlapping patterns, each entry now carries an explicit discriminator that resolves the choice. Concrete: `checks_effects_interactions_violation` is the **default tag for any CEI-ordering bug**; `reentrancy` is reserved for cases where the proven exploit specifically requires a re-entrant callback. Without the discriminator, agents picked one tag or the other inconsistently across structurally identical findings, hurting reproducibility.

### Validation method

Per `post-audit-improvement-protocol.md` Phase A/B: the changes were derived by running the mandatory RC-AGENT Exclusion Test against every miss/FP from a v0.5.1 cross-run that scored below the documented baseline. The majority of failure events classified as RC-AGENT (existing methodology covered the bug class, but the agent failed to apply it under fallback / sub-agent invocation). One event classified as RC-NOVEL — a previously documented structural ceiling carried over unchanged. The remaining events classified as RC-METHOD candidates that survived the exclusion test honestly — all vocabulary-discipline issues, none requiring new checks. The worksheet (1 + 2 + 3 above) addresses the RC-AGENT cluster; the vocabulary changes (4 + 5) address the RC-METHOD cluster.

### Not changed

- `checklists/*.md` — zero edits. No new checks, no severity recalibration on existing checks.
- Profile detection, clustering, cross-cluster sweep — unchanged.
- Severity decision table — unchanged content; only its enforcement is tightened (worksheet field 6 requires citing a row).
- All Step 5 hedging rules and Step 7 gate logic — unchanged content; only enforcement format is tightened.
- No benchmark-specific keywords, identifiers, function names, or pattern descriptions added anywhere.

### Anti-bloat audit

| Change | Type | Lines added (SKILL.md) | Files modified |
|---|---|---|---|
| Pre-emission worksheet | extend | ~20 | 1 |
| No-silent-drops + Gate C visibility | extend | ~3 | 1 |
| Vocab discipline header | extend | ~4 | 1 |
| Reentrancy discriminator | edit | net +1 | 1 |
| tx_origin canonical reversal | edit | net +1 | 1 |
| Version bump + header | edit | net 0 | 1 |
| **Total** | — | **~29** | **1 file (SKILL.md)** |

SKILL.md grows from 572 → ~601 lines. No checklist file is touched. No per-language tree is duplicated.

---

## v0.5.1 — Adversarial-gated emission (2026-04-15)

**Headline**: Forefy autonomous-audit public corpus **0.5909 → 0.7545** (+0.16). Zero checklist growth. Five new Step 5 / Step 7 discipline rules. Midas validation: all 9 HIGH findings preserved, ~14 LOW/INFO demoted to `warnings[]`.

### Methodology changes

1. **Gate C — Disprove-Before-Emit**. Every finding that passes Gate A must get a one-sentence Defender's Argument backed by visible code. If a strong line-level defender exists, the finding is downgraded one tier (LOW → dropped). Forces the agent to argue against itself rather than accept the first plausible trace.

2. **Root-cause consolidation** (Step 7). If two findings share the same `vulnerability_type` in the same file across different functions AND a single PR would fix both, consolidate into one finding with siblings named in the explanation. Kills duplicate signature-replay / missing-nonce / unchecked-return style emissions across function families.

3. **Default LOW/INFO suppression** (Step 7). `findings[]` contains only CRITICAL/HIGH/MEDIUM by default. LOW/INFO go to a `warnings[]` array. Rationale: LOW/INFO are hardening observations that most scoring rubrics penalize as FPs. Overridable via `--include-low` / `--full` for real-audit use.

4. **Hedging-by-admin-cooperation ban** (Step 5 rule 5b). Extends the v0.5.0 hypothetical-hedging ban. Findings whose exploit sentence requires the admin/owner/trusted actor to deliberately misconfigure or cooperate with the attacker are centralization concerns, not exploits — moved to `warnings[]` as `"centralization: …"` strings.

5. **Textbook-pattern specific-break requirement** (Step 5 rule 4a). For canonical well-known patterns (CEI, signature replay, MasterChef reward-debt, multisig stale approvals, ERC4626 inflation, flash-loan oracle manipulation), pattern presence is not sufficient — the finding must identify the specific code line that deviates from the textbook safe version. Competent authors handle these correctly most of the time; emitting on pattern presence alone is the top precision failure on complex clean contracts.

6. **Weak-evidence severity floor** (Step 7 severity rules). If the exploit trace depends on off-chain tree/payload construction, cross-contract configuration set later, unobservable user ordering, or external-callback types not currently in scope, cap severity at LOW. Combined with default LOW suppression, these become `warnings[]` entries.

### Validation

| Benchmark / codebase | v0.5.0 | v0.5.1 | Change |
|---|---|---|---|
| Forefy autonomous-audit public corpus (11 cases) | 0.5909 | **0.7545** | +0.16 |
| Midas (36-finding dry-run) | 36 findings | 22 findings + 14 warnings | 0 HIGH dropped; all demotions are the targeted classes |

Per-case movement on Forefy autonomous-audit (v0.5.0 → v0.5.1):
- **case-002**: 0.6 → ~1.0 (consolidated release+refund; setArbitrator centralization dropped)
- **case-006**: 0.6 → ~0.9 (nonce findings consolidated; relayerFee admin-drift dropped)
- **case-008**: 0 → 1.0 (reward-debt textbook-pattern blocked by weak-evidence floor and specific-break rule)
- **case-011**: 0 → 1.0 (merkle-leaf cross-window finding blocked by weak-evidence floor; unchecked-return dropped)
- **case-005**: ~0.8 → ~0.8 (no regression; extra stale-state finding on `_updateRewards` still a FP)
- **case-009**: still 0 (stale-approvals survived Gate C because no line-level defender exists)

### Known remaining gap

Case 009 (multisig) is the lone unresolved FP. The agent's trace is correct as a class-of-bug (the contract indeed does not clear approvals on owner removal), but the benchmark author classified it as clean. This is a structural limit — pattern matching + concrete trace cannot distinguish "real bug we'll fix" from "acceptable behavior per author intent" without human judgment or PoC. Expected ceiling on this benchmark remains ~0.80.

### Not changed

- `checklists/*.md` — zero edits.
- Profile detection, clustering, cross-cluster sweep — unchanged.
- No benchmark-specific rules.

---

## v0.5.0 — Precision-gated emission (2026-04-15)

**Headline**: Measured score on Forefy autonomous-audit public corpus improved from **0.2909 (v0.4.3) → 0.5909 (v0.5.0)**. No checklist growth; methodology-only change.

### Root cause of v0.4.x weakness

Post-audit analysis on Forefy autonomous-audit showed the pattern matcher had complete coverage of every bug class in the benchmark, but lost points to (a) speculative "hedging" false positives, (b) severity miscalibration, (c) the agent reasoning itself out of valid findings, and (d) class-label drift from industry conventions.

Classification of 11 benchmark cases: 0 RC-METHOD (no missing checks), majority RC-AGENT (reasoning errors). Per the post-audit improvement protocol, RC-AGENT failures cannot be fixed by adding rules — only by tightening emission discipline.

### Methodology changes

1. **Step 5 rule 5** — "prefer NOT to report" → "do NOT report" as a hard rule. Explicitly bans speculative-hedging findings (`if ERC777 is ever added`, `if a malicious token is whitelisted`, `if a future admin…`). Hedging findings must have current in-scope evidence or be dropped at the source.

2. **Step 7 Gate A — Exploit-Sentence Gate** (precision). Every emitted finding must fill a concrete exploit sentence from this codebase's source: *"Attacker with [ROLE] calls [FN] with [INPUT], result is [CONCRETE LOSS]."* If any bracket requires hypothetical state, drop the finding. Two documented exceptions: cross-cluster economic flow candidates (tagged `"Pattern-level candidate:"`) and INFO-capped hardening items.

3. **Step 7 Gate B — Reasoning Reconciliation** (recall). Before emission, scan own reasoning for dismissal phrases (`"by design"`, `"admin-only so trusted"`, `"self-griefing"`, `"edge case"`). For each dismissal, check whether backed by a hard code-level constraint or just intuition. If intuition, restore the finding. This recovered the missed `discountBps` finding in benchmark case-003 and the `division_by_zero` miss in case-005.

4. **Severity decision table** — replaces free-text 5-row matrix with structured rules keyed on (attacker role, preconditions, impact). Aligns with Code4rena/Immunefi/SWC convention. Key rules: permissionless drain = CRITICAL, missing access on economic-parameter setter = HIGH, missing access on per-user state = MEDIUM, racing a legitimate caller = MEDIUM (not HIGH), admin-only action caps at MEDIUM (centralization), unchecked return without identified non-standard token = DROP.

5. **Vocabulary discipline** — `vulnerability_type` must pick the closest canonical tag from the SWC-aligned vocabulary. Paraphrasing (`"tx.origin authorization"` when the canonical tag is `tx_origin_auth`) is no longer allowed; fallback to ad-hoc snake_case now requires a `warnings` entry. Added missing industry-standard labels: `missing_input_validation`, `missing_condition_check`, `checks_effects_interactions_violation` (SWC-107 alias).

### Validation

| Benchmark | v0.4.3 | v0.5.0 | Change |
|---|---|---|---|
| Forefy autonomous-audit public (11 cases) | 0.2909 | 0.5909 | **+0.30** |
| Midas contracts (real-world, 36 findings) | 36 findings | ~31 findings | 5 hedging LOWs dropped; ALL 9 HIGH findings preserved |

Key verifications:
- Gate A kills the hedging findings it was designed to kill on both benchmarks.
- Gate B recovers findings the agent previously reasoned itself out of.
- No legitimate HIGH finding was lost on either benchmark.
- No checklist changes — this is entirely a Step 5 + Step 7 methodology tightening.

### Known ceiling

Remaining ~20% of Forefy benchmark points require flow-level analysis (fee accounting invariants, multisig approval tracking across state changes, cross-window merkle replay feasibility) that pattern matching structurally cannot prove without PoC execution. Three of 11 public cases remain false-positive for v0.5.0 (cases 008, 009, 011). Pushing past ~0.80 requires a verification layer that is outside drozer-lite's scope.

### Added

- `benchmark/` directory with vendor-neutral harness + baseline tracking (`run.sh`, `baseline.json`, `README.md`).

### Not changed

- `checklists/*.md` — zero edits. No benchmark-specific checks added.
- Profile detection, clustering, cross-cluster sweep — unchanged.

---

## Earlier versions

See git history. Summary per `memory/MEMORY.md`:
- v0.4.1 — 8 universal + UNI-2 extension (rental/marketplace ~42%→65% projected)
- v0.4.2 — 14 checks across universal/DEX/new StableSwap profile
- v0.4.3 — UNI-18 and UNI-38 extensions; Cairo perpetuals benchmark 54%

## CONTRIBUTING.md

# Contributing to drozer-lite

Thanks for considering a contribution. drozer-lite is a Claude Code skill, not a Python package — there is no `pip install`, no test suite, no CI matrix. Contributions are content edits to four areas:

1. **Checklists** — new vulnerability patterns ported from real audit findings
2. **Vocabulary** — new canonical tags
3. **Detection keywords** — new profile triggers
4. **Fixtures** — vulnerable + clean Solidity pairs for smoke testing

## The one non-negotiable rule — No benchmark-specific names

**Every check, every keyword, every pattern description must be GENERIC.** Never embed audit-benchmark-specific identifiers (contract names, function names, variable names, token tickers from the protocol that motivated your fix) into any check, methodology section, red flags list, or example block.

Bad: *"Check if `confirmWithdrawal` has `whenWithdrawalNotPaused` — this is the Kinetiq GT-5 bug"*
Good: *"Check if a user-facing confirmation/settlement function has a local pause-flag modifier that's symmetric with the queue function's pause-flag modifier"*

Why: a check with a benchmark-specific function name like `confirmWithdrawal` will miss the exact same bug on any protocol that names it `finalizeClaim`, `settleRedemption`, or `completeExit`. The bug class is the same; the name is the accident. The skill must match the class, not the accident.

See `SKILL.md` → "Check Authorship Rules" (section CA-1 through CA-5) for the full rule and enforcement checklist. Every PR that touches `SKILL.md` or `checklists/*.md` must grep the diff for benchmark-specific identifiers before merge.

## Project philosophy

drozer-lite is intentionally narrow:

- **One LLM pass per audit, inside your existing Claude Code session.** Multi-agent orchestration belongs in the main [drozer](https://github.com/gdroz3r/drozer) pipeline.
- **Empirically curated.** Every check in a checklist must trace to a real audit finding that was missed before. No speculative patterns. The provenance is cited inside each check entry.
- **Developer-shaped.** Default output is canonical JSON. Markdown is opt-in.
- **Honest framing.** Every run ends with the disclaimer that drozer-lite catches pattern-level bugs only — never softens it.

Contributions that preserve these properties are welcome. Contributions that add multi-phase orchestration, benchmark-specific tuning, or speculative patterns will be redirected to the main drozer pipeline or rejected.

## 1. Adding a check to an existing checklist

Open the relevant `checklists/<profile>.md` file and add a new section in this exact format:

```markdown
### CHECK-NN: Title

**Provenance**: <source file in main drozer> → <benchmark project, e.g. "Virtuals H-05">
**Pattern**: <one to three sentences — what to look for in Solidity terms>
**Methodology**: <two to five sentences — how to investigate, what trace to follow, what state to compare>
**Red flags**:
- <concrete code construct>
- <concrete code construct>
- ...
```

Increment the check count in the file header. The check ID (e.g. `LEND-6`) follows the existing prefix in that file.

## 2. Adding a new profile

1. Create `checklists/<profile>.md` with the same Methodology preamble pattern as the existing files. Use a unique check ID prefix.
2. Port at least 3 real-finding-grounded checks from `Drozer-v2/skills/droz3r/analyses/` or your own audit history.
3. Add the profile name to the table in `SKILL.md` (Step 2 — Detect the protocol type) with case-insensitive trigger keywords. Pick keywords that appear at least 3 times in real code that uses this protocol type. Use `\b` boundaries where helpful.
4. Add the profile to `SKILL.md` Step 3 (Load the relevant checklists) so the skill knows it exists.
5. Add a vulnerable and clean fixture to `examples/fixtures/<profile>/{vulnerable,clean}.sol`.
6. Add a row to `examples/fixtures/expectations.json` pinning the profile, expected canonical `vulnerability_type`, and expected `affected_function`.
7. Open a PR with: rationale, source benchmark project the checks came from, fixture rationale.

## 3. Adding a vocabulary tag

Open `SKILL.md` and append a new entry to the **Canonical vulnerability vocabulary** section. Format:

```markdown
- `tag_name` — One-sentence description of the bug pattern. (SWC-NNN, CWE-NNN)
```

Include SWC and CWE references where applicable. Group by category (Reentrancy, Access control, Math, etc.).

## 4. Adding fixtures

`examples/fixtures/` holds small hand-crafted .sol files that serve as smoke tests for the skill. Each fixture pairs a vulnerable version with a clean version so users can verify both recall (finding real bugs) and precision (not flagging clean code).

Fixtures are the primary regression net. If you find that drozer-lite misses an obvious pattern, add a fixture for it first, then fix the check.

A fixture pair must:

- Be under 60 lines each (smoke tests, not production code)
- Demonstrate one canonical pattern from the loaded checklist
- Include the bug in a way the loaded checklist's Red flags can match
- Have a clean variant where the bug is remediated using the standard fix

## Manual validation after a checklist edit

Since drozer-lite is a skill (not a Python package), there is no automated test suite. After editing a check:

1. Open Claude Code.
2. Run `/drozer-lite examples/fixtures/<profile>/vulnerable.sol`.
3. Verify the new check fires AND the existing checks still fire.
4. Run `/drozer-lite examples/fixtures/<profile>/clean.sol`.
5. Verify NO finding matches the canonical type of the new check.
6. Repeat for any other profile your edit might affect.

If you broke something, fix it before committing.

## Commit and PR conventions

- Commits should be focused — one logical change per commit.
- PRs should describe: **what** changed, **why** it matters, and **how** it was manually validated.
- Reference the benchmark project or real finding that motivated the change.
- No benchmark-specific optimizations to the core methodology — `SKILL.md` stays generic.

## Reporting bugs in the skill itself

If the skill misbehaves (skips a check that should fire, fires on a clean fixture, breaks the JSON schema, refuses to disclose limitations), open an issue with:

- The exact `/drozer-lite ...` invocation
- The source file you ran it on
- The expected vs actual output
- The Claude Code version and model

Skill bugs are higher priority than missing checks — checks can be added incrementally, but the methodology must be sound.

## LICENSE

```

```

## README.md

# drozer-lite

Open-source Claude Code skill for pattern-level smart contract vulnerability scanning.

Supports **Solidity, Rust (Anchor/CosmWasm/IC), Move (Aptos/Sui/Initia), Cairo (StarkNet), and Vyper**.

205 checks across 14 profiles. Runs inside your Claude Code session — no extra API key, no install.

## Install

```bash
git clone https://github.com/gdroz3r/drozer-lite ~/.claude/skills/drozer-lite
```

Restart Claude Code. That's it.

## Usage

```
/drozer-lite path/to/project
```

Or:

```
/drozer-lite path/to/Contract.sol --profile auto
```

| Flag | Purpose |
|---|---|
| `--profile auto` (default) | Auto-detect profiles from keywords |
| `--profile <name>` | Force: `vault`, `lending`, `dex`, `stableswap`, `signature`, `cross-chain`, `governance`, `reentrancy`, `oracle`, `math`, `gaming`, `icp`, `solana` |

## What it does

1. Detects language from file extensions
2. Builds a structural inventory of all in-scope files
3. Auto-detects relevant protocol profiles (DEX, vault, lending, etc.)
4. Clusters files by dependency
5. Applies the checklist against each cluster
6. Runs a cross-cluster sweep for multi-file bugs
7. Outputs deduplicated findings as JSON + Markdown

## Profiles

| Profile | Checks |
|---|---|
| `universal` (always loaded) | 110 |
| `dex` | 11 |
| `vault` | 6 |
| `lending` | 5 |
| `stableswap` | 5 |
| `signature` | 4 |
| `cross-chain` | 13 |
| `governance` | 6 |
| `reentrancy` | 5 |
| `oracle` | 3 |
| `math` | 6 |
| `gaming` | 3 |
| `solana` | 12 |
| `icp` | 16 |

## Where the checks come from

Every check traces to a real missed finding from a past audit benchmark. The initial checklist was ported from the [ScaBench](https://github.com/scabench-org/scabench) curated dataset and the Drozer-v2 internal gap analysis pipeline. From there, each new benchmark audit (Code4rena, Sherlock, etc.) produces a post-mortem: missed findings are classified by root cause, and any gap that is generalizable into a pattern-level check gets added to the relevant profile. Checks that only match a single codebase are rejected — only class-of-bug patterns that fire across protocols are kept.



## Limits

- Pattern-level only. Does not do multi-step actor reasoning, chain composition, or formal verification.
- 10-60 min per protocol depending on size.
- Solidity has the deepest coverage. Move/Cairo/Vyper rely on the 110 universal checks with automatic translation.


## License

MIT

## SKILL.md

---
name: drozer-lite
description: General-purpose pattern-level smart contract vulnerability scanner with cross-file awareness. Walks any smart contract project (Solidity, Rust/Anchor/CosmWasm/IC, Move, Cairo, Vyper — single file or multi-file), builds an inventory, clusters related modules, applies a curated checklist of 180+ vulnerability patterns derived from real benchmark gap analysis across 13+ protocol-type profiles, and returns structured findings. USE WHEN the user asks to scan, audit, or review smart contract source for security bugs and wants pattern-level coverage. Designed for protocols up to ~500KB / 100 files. Wall-clock 5-30 min depending on size. Does NOT do multi-step actor reasoning, chain analysis, or formal verification — for that, use the full drozer pipeline (`/droz3r`).
---

# drozer-lite — open-source pattern-level smart contract auditor (v0.5.7, broadened CEI discriminator + schema-mismatch LOW drop + shared-check consolidation)

You are about to run a multi-file pattern-level smart contract audit using drozer-lite's curated checklist. Follow this 8-step workflow exactly. Do not invent steps, do not paraphrase the checklist, do not invent findings.

drozer-lite is the open-source pattern-level slice of the main Drozer-v2 auditor. Every check in the bundled checklists traces to a real audit finding that was missed in past benchmark runs. The provenance is cited inside each check.

drozer-lite is intentionally narrow:

- **Pattern-level checks** drawn from a curated checklist — ~88 checks are language-agnostic, ~10 are Solidity-specific
- **Multi-language** — Solidity, Rust (Anchor, CosmWasm, IC canisters), Move (Aptos, Sui, Initia), Cairo (StarkNet), Vyper
- **Cross-file aware** (catches bugs spanning multiple contracts/modules)
- Does NOT do multi-step actor reasoning, chain composition analysis, or formal verification — that's `/droz3r` territory

The trade-off is on purpose: drozer-lite finds the bugs pattern matching CAN find, fast and reproducibly, without pretending to be a full audit pipeline.

---

## Step 1 — Identify the target and detect language

Determine which file(s) the user wants audited.

- If the user pasted source inline, treat it as a one-file target. Detect language from syntax.
- If the user referenced a path, walk it.

### Language detection (auto, from file extensions)

| Extension(s) | Language | Glob pattern | Also filter out |
|---|---|---|---|
| `.sol` | Solidity | `**/*.sol` | `node_modules`, `lib`, `forge-std`, `.forge` |
| `.rs` | Rust (Anchor / CosmWasm / IC) | `**/*.rs` | `target/`, `.cargo/`, `test_*.rs`, `*_test.rs` |
| `.move` | Move (Aptos / Sui / Initia) | `**/*.move` | `build/`, `.aptos/`, `tests/` |
| `.cairo` | Cairo (StarkNet) | `**/*.cairo` | `target/`, `tests/` |
| `.vy` | Vyper | `**/*.vy` | `tests/` |

Always filter out: `test`, `tests`, `mock`, `mocks`, `script`, `scripts`, `out`, `cache`, `.git`, `coverage`, `broadcast`, `node_modules`.

If a project has mixed languages (e.g. `.sol` + `.rs`), detect the PRIMARY language by file count / byte weight and note the secondary. Load profiles for the primary language; if a secondary language has significant source (>20% by bytes), load its profiles too.

- **Soft warning**: if total source > 500KB, tell the user "this will take ~30+ minutes" and continue.
- **Hard refusal**: if total source > 1MB, REFUSE. Recommend `/droz3r` (the full drozer pipeline).

If you cannot find any source, ask the user to specify a path or paste source. Do not guess.

### Language determines what "function", "modifier", "state variable" mean in Steps 2-6

| Concept | Solidity | Rust (Anchor/CosmWasm) | Move | Cairo |
|---|---|---|---|---|
| Public function | `external`/`public` | `pub fn`, `#[msg(execute)]`, `#[instruction]` | `public entry fun`, `public fun` | `#[external(v0)]`, `fn` in impl |
| Access control | `onlyOwner`, `onlyRole(R)` modifier | `require!(ctx.accounts.authority == ...)`, `#[access_control]` | `assert!(signer::address_of(s) == @admin)` | `assert(caller == owner)` |
| State variable | contract-level storage | `Account<'info, T>`, `#[account]` struct fields | `borrow_global<T>`, resource struct fields | `@storage_var` |
| External call | `.call`, `interface(addr).fn()` | CPI (`invoke`, `invoke_signed`), `CosmosMsg` | `coin::transfer`, module call | syscall, contract call |
| Reentrancy guard | `nonReentrant`, `ReentrancyGuard` | manual flag, `#[non_reentrant]` in some frameworks | N/A (Move is not reentrant by design) | N/A (Cairo is not reentrant by design) |
| Import/dependency | `import`, `using...for` | `use`, `mod`, Cargo.toml deps | `use`, `friend` | `use`, imports |

When applying checks from `.claude/skills/drozer-lite/checklists/universal.md`, **translate the Solidity-phrased red flags to the target language's equivalent**. The METHODOLOGY is language-agnostic; only the SYNTAX differs. For example:
- UNI-1 says "Missing `onlyOwner`/`onlyRole(...)` on state-changing function" → in Rust, check for missing `require!(authority == ...)` or `#[access_control(...)]`
- UNI-3 says "Balance/ownership update AFTER `.call` or token transfer" → in Rust, check for CPI invocations before account state updates

---

## Step 2 — Build the inventory (cheap structural pass)

Read each in-scope file with the Read tool. Do NOT analyze code yet — only extract structure. Build an in-context inventory map covering:

For each file:
- **Path** and approximate byte size + line count
- **Modules / contracts / programs** declared (and what they extend / implement)
- **Public / external function signatures** (name + params + visibility + guards, no body). Use the language's convention from the Step 1 table.
- **State variables / account structs / storage vars** (name + type + visibility)
- **Access control mechanisms** (modifiers, assert-based guards, access_control attributes)
- **External calls visible**: cross-contract calls, CPI, module calls, system calls, delegate calls
- **Imports / dependencies** (which other in-scope files this file depends on)

Format the inventory like this (keep it terse — this is your context_map for cross-file detection):

Format: `File.sol (XKB, YL) → Contracts: Name → Parent; External fns: ...; State: ...; External calls: ...; Imports: ...`

Use real file/contract names from the source. Keep it terse — this is your context_map for cross-file detection in Step 5.

---

## Step 3 — Detect profiles (with always-load fallback)

Apply the keyword detection table below to the WHOLE inventory (not file-by-file). Detection is **case-insensitive**. A profile is **auto-loaded** if it scores **3 or more distinct keyword matches** across the inventory.

**ALWAYS LOAD**: `universal` (regardless of detection)

**Auto-detect** (per-profile threshold = 3 distinct keyword matches). Keywords below are **common-pattern names** and regex fragments that appear across the entire Solidity ecosystem — they must never encode benchmark-specific identifiers. When applying detection, treat each row as a set of case-insensitive regex patterns. Some patterns use `\w*` deliberately to tolerate real-world naming (prefixed function names like `depositFoo`, suffixed variants like `previewDepositVault`, protocol-specific wrappers like `swapXForY`). A keyword match should accept the pattern followed by typical word characters.

| Profile | Keywords and regex fragments (case-insensitive) |
|---|---|
| signature   | `EIP712`, `permit\s*\(`, `ecrecover\s*\(`, `isValidSignature`, `_hashTypedDataV4`, `DOMAIN_SEPARATOR`, `permitWitnessTransferFrom`, `_signTypedData`, `Permit2`, `IERC1271`, `\w*Permit\w*\s*\(`, `SignatureLib`, `recoverSigner`, `signedHash` |
| vault       | `ERC4626`, `totalAssets`, `previewDeposit\w*`, `previewWithdraw\w*`, `previewRedeem\w*`, `convertToShares`, `convertToAssets`, `function\s+\w*[Dd]eposit\w*\s*\(`, `function\s+\w*[Ww]ithdraw\w*\s*\(`, `function\s+\w*[Rr]edeem\w*\s*\(`, `IERC4626`, `sharesOf`, `maxDeposit`, `\w*Vault\w*`, `lvToken`, `vToken` |
| lending     | `\w*[Bb]orrow\w*`, `\w*[Ll]iquidate\w*`, `collateral`, `healthFactor`, `\bLTV\b`, `debtToken`, `interestRate`, `\w*[Rr]epay\w*`, `function\s+\w*[Ss]upply\w*\s*\(`, `IPool`, `ILendingPool`, `_borrow`, `cToken`, `underlying` |
| dex         | `\w*[Ss]wap\w*\s*\(`, `addLiquidity\w*`, `removeLiquidity\w*`, `amountOutMin`, `amountInMax`, `\bUniswapV[234]\b`, `IUniswapV[234]`, `ISwapRouter`, `sqrtPriceX96`, `getAmountsOut`, `getAmountOut`, `\bRouter0[1-4]\b`, `createPair\s*\(`, `MinimalUniswapV2Library`, `_pair` |
| cross-chain | `lzReceive`, `ccipReceive`, `setPeer`, `setTrustedRemote`, `wormhole`, `IReceiver`, `\w*[Bb]ridge\w*\s*\(`, `ILayerZero`, `NonblockingLzApp`, `_nonblockingLzReceive`, `IRouterClient`, `crossChain\w*`, `CCIPSender`, `HyperlaneRouter` |
| governance  | `propose\s*\(`, `castVote\w*\s*\(`, `quorum`, `delegate\w*\s*\(`, `Governor`, `Timelock`, `votingPower`, `IGovernor`, `_execute\s*\(`, `getVotes`, `IVotes`, `proposalThreshold`, `voteStart`, `voteEnd` |
| reentrancy  | `nonReentrant`, `ReentrancyGuard`, `\w*[Rr]eentranc\w*`, `\.call\s*\{\s*value`, `\.call\s*\(`, `onERC721Received`, `onERC1155Received`, `tokensReceived`, `ERC777`, `IERC777Recipient`, `_checkOnERC721Received`, `IERC1155Receiver`, `\bMutex\w*`, `NoReentrant`, `\bLock\w*\.acquire`, `locked\s*=\s*true`, `_status\s*=\s*1` |
| oracle      | `AggregatorV[23]Interface`, `latestRoundData`, `latestAnswer`, `IChainlink`, `priceFeed`, `\w*[Gg]etPrice\w*`, `\boracle\w*`, `\w*Oracle\w*`, `IPyth`, `IOracle\w*`, `oracleAdapter`, `IOracleAdapter`, `setOracle\w*`, `_oracle`, `\boracles/`, `IRates`, `exchangeRate\s*\(` |
| math        | `FixedPoint`, `PRBMath`, `mulDiv`, `SafeMath`, `\bWAD\b`, `\bRAY\b`, `UFixed\w*`, `SD\d+x\d+`, `UD\d+x\d+`, `abdk`, `FullMath`, `MathUpgradeable`, `MathHelper`, `UQ\d+x\d+`, `\bsqrt\s*\(` |
| gaming      | `VRFConsumerBase`, `VRFCoordinator`, `randomness`, `\w*[Rr]affle\w*`, `\w*[Ll]ottery\w*`, `requestRandomWords`, `fulfillRandomWords`, `ChainlinkVRF`, `VRFV2`, `IVRFCoordinator`, `commitReveal`, `randaoMix` |
| stableswap  | `StableSwap`, `\bamp\b`, `amplification`, `compute_d`, `compute_y`, `\bnewton\b`, `invariant.*D`, `stableswap_y`, `n_coins.*ann`, `D_prod`, `amp_factor` |

### Language-specific profiles (auto-load by detected language)

When the detected language is NOT Solidity, auto-load the corresponding language profile:

| Detected language | Auto-load profile | Condition |
|---|---|---|
| Rust + Anchor patterns (`declare_id!`, `#[program]`, `#[account]`) | `solana` | Score ≥ 2 Anchor keywords |
| Rust + IC patterns (`ic_cdk`, `#[update]`, `#[query]`, `candid`) | `icp` | Score ≥ 2 IC keywords |
| Rust + CosmWasm patterns (`cosmwasm_std`, `#[entry_point]`, `ExecuteMsg`) | load `universal` only (no dedicated CosmWasm profile yet) | — |
| Move | load `universal` only (no dedicated Move profile yet) | — |
| Cairo | load `universal` only (no dedicated Cairo profile yet) | — |
| Vyper | load `universal` + any Solidity profiles that fire (Vyper shares EVM patterns) | — |

`icp` and `solana` profiles are NO LONGER explicit-only. They auto-load when the language detection identifies Anchor or IC canister Rust code. They can still be forced via `--profile` when auto-detection misses.

**Hard rule**: do not lower the threshold to "make a profile fire" because you intuit it might be relevant. If a profile genuinely does not clear the threshold, do not load its checklist. Pattern coverage IS the contract.

**Document the selection**. Output a "Profile Selection" block the user can read, listing every profile and its match count:

```
PROFILE SELECTION (synthetic example):
  universal     → ALWAYS LOADED
  reentrancy    → AUTO-LOADED (4 matches: nonReentrant, ReentrancyGuard, .call{value, onERC721Received)
  oracle        → AUTO-LOADED (3 matches: priceFeed, IOracle, exchangeRate())
  vault         → not loaded (1 match: ERC4626)
  lending       → not loaded (0 matches)
  signature     → not loaded (1 match: permit()-shaped library call)
  ...
```

If the user disagrees, they can re-invoke with `--profile <name>` to force-load.

---

## Step 4 — Cluster the codebase

Group files into clusters of 1-5 files each. Target **30-50KB per cluster**. Use these rules in order:

1. **Inheritance chain** — files containing parent + child contracts go in the same cluster.
2. **Mutual import** — file A imports B, file B imports A → same cluster.
3. **Subdirectory + shared imports** — files in the same `oracles/`, `validators/`, `governance/` subdirectory with overlapping imports → same cluster.
4. **Single oversized file** — a file > 30KB becomes its own cluster. If it's > 60KB, you'll need to analyze it in two passes (top half / bottom half) and merge findings.
5. **Soft cap**: if a cluster exceeds 60KB, split it along the weakest dependency edge.

Output the cluster plan in a "CLUSTER PLAN" block:

```
CLUSTER PLAN (synthetic example):
  Cluster 1 — PrimaryCluster (54KB, 1.4K lines)
    Files: Token.sol, Service.sol, Accountant.sol
    Dependencies: Token imports IPauseRegistry; Service imports Token + IAccountant; Accountant imports IRegistry
  Cluster 2 — ControlCluster (24KB, 643 lines)
    Files: Registry.sol, PauseRegistry.sol
    Dependencies: Registry imports IPauseRegistry + IService
  Cluster 3 — OracleCluster (21KB, 542 lines)
    Files: OracleAggregator.sol, adapters/Adapter.sol, adapters/SourceImpl.sol, adapters/IAdapter.sol
    Dependencies: OracleAggregator imports IAdapter; Adapter imports IAdapter
```

---

## Step 5 — Per-cluster analysis

For each cluster:

1. **Read the cluster's source — EVERY LINE, NO EXCEPTIONS.** Read each file in the cluster fully. If a file exceeds ~40KB (~500 lines), read it in sequential chunks using offset+limit (e.g. offset=0 limit=500, then offset=500 limit=500, etc.) until the entire file is read. Do NOT skip, sample, or "read the important parts." Every line of in-scope source must be read. Partial source reading is the #1 cause of missed findings — a 25% read produces 25% recall. This is non-negotiable.
2. **Read the relevant checklists** — Read `.claude/skills/drozer-lite/checklists/universal.md` always, plus each auto-loaded profile checklist (`.claude/skills/drozer-lite/checklists/{profile}.md`). These paths are relative to the project root (current working directory) where the skill is invoked.
3. **Reference the inventory from Step 2** — for cross-cluster bug detection. When the cluster you're analyzing calls a function in another cluster, look up the target's signature in the inventory; you don't need to re-read the other cluster's full source.
4. **Apply each loaded check** — for each check in the loaded checklists, examine the cluster source. **If the target language is not Solidity, translate the check's Solidity-phrased red flags to the equivalent in the target language** using the concept-mapping table from Step 1. The METHODOLOGY is language-agnostic; only the SYNTAX differs. A check matches when ALL of:
   - The **Pattern** field describes a code construct that exists in the cluster source (in the target language's idiom)
   - The **Red flags** (or their language-translated equivalents) are visible in the source
   - The **Methodology** describes a reachable exploit path you can trace line by line
4a. **Textbook-pattern specific-break requirement** (new in v0.5.1). For canonical well-known patterns — **reentrancy / CEI**, **signature replay**, **reward-debt / MasterChef-style accumulator**, **multisig stale-approval after owner removal**, **ERC4626 first-depositor inflation**, **flash loan oracle manipulation**, **approve-then-transferFrom race** — pattern presence is NOT sufficient. The finding MUST identify the **specific line** in the current code that deviates from the textbook safe version. Competent authors handle these patterns correctly most of the time; emitting on pattern presence without a concrete break is the top precision failure on complex clean contracts.

   - Good: *"Line 65 calls `msg.sender.call{value: excess}('')` BEFORE line 68 updates `tokensSold`. Textbook safe version updates state before the external call."*
   - Bad: *"The reward-debt pattern is present; a user acquiring LP after fees accrue can claim retroactively."* (class description, not a code-level break)
   - Bad: *"removeOwner does not clear approvals — classic multisig bug."* (class description; requires you to actually show that the approvals are counted AFTER removal in the current execute() path, with specific lines)

5. **When unsure, do NOT report.** This is a hard rule, not a preference. False positives are worse than misses.

   (a) **Hedging by hypothetical future state** — banned. Do NOT report any finding whose body is "what if an admin whitelists a malicious token…", "if a future upgrade adds…", "if ERC777 is ever added…", "if the oracle returns zero…" unless the codebase ALREADY contains evidence that the hypothetical condition holds (e.g., ERC777 is actually in scope, the oracle is actually unvalidated in the read path).

   (b) **Hedging by admin cooperation** — banned (new in v0.5.1). Do NOT report any finding whose exploit sentence requires the admin/owner/trusted actor to deliberately misconfigure parameters, set addresses to zero, or cooperate with the attacker. Examples that must be suppressed: "admin can set `sanctionsList` to zero and disable sanctions", "admin can call `setFee(10000)` and drain", "arbitrator can transfer role to attacker". These are centralization concerns, not exploits. Move them to `warnings[]` as `"centralization: <title>"` strings if the user explicitly invoked with `--include-centralization`. Never emit them in `findings[]` by default.

   Speculative hedging findings of either type are the #1 source of false positives and MUST be suppressed at the source, not at Step 7.
6. **For each match**, identify:
   - The affected function name and the file it lives in (full path within the project)
   - A specific line as the most representative location for `line_hint`
   - A severity from CRITICAL/HIGH/MEDIUM/LOW/INFO using the matrix below
   - A confidence from HIGH/MEDIUM/LOW based on how clearly the source matches the check
   - The check ID that fired (e.g. `UNI-1`, `RE-2`, `ORC-3`) — record this internally
7. **Add cluster metadata** to each finding: `cluster: "<cluster name>"`. The dedup pass will use this.
8. **Do not output yet** — accumulate findings in your working set. Output is at Step 7.

You may analyze clusters sequentially (recommended for token budget). Each cluster gets its own independent reasoning pass — but ALL clusters share the same checklist context that you loaded once at the start.

### Severity decision table

Pick severity by walking this table top-to-bottom and stopping at the first row that matches. Do NOT pick severity by "feel" — the table is the contract. Aligned with industry convention (Code4rena / Immunefi / SWC Registry).

| If the finding is… | …and the attacker is… | …and the impact is… | Severity |
|---|---|---|---|
| Direct drain / unauthorized mint / arbitrary-state-write | **permissionless** (anyone) | protocol-wide funds at risk, no preconditions | **CRITICAL** |
| Signature replay on a token-moving or authorization function | permissionless | repeated fund transfer until balance/allowance exhausted | **CRITICAL** |
| CEI violation / reentrancy that drains a pool | permissionless | full pool drain in a single tx | **CRITICAL** |
| Missing access control on a function that **sets an economic parameter** (rate, fee, price, reward, threshold) | permissionless | protocol-wide economic manipulation, indirect fund loss | **HIGH** |
| Missing access control on a function that sets **per-user** state | permissionless | a single user's state is corrupted | **MEDIUM** |
| CEI violation / reentrancy with a cap on damage (per-user, per-epoch) | permissionless | bounded fund loss | **HIGH** |
| Signature replay on a non-fund-moving function | permissionless | unbounded action replay, no direct fund loss | **MEDIUM** |
| Missing input validation that allows a permissionless caller to set a parameter to an out-of-spec value causing fund loss (e.g. `discountBps > 10000`, `fee > 100%`) | permissionless OR creator of a permissionless market | direct fund loss once the bad value is set | **HIGH** |
| Missing input validation with no direct fund loss | permissionless | griefing, DoS, or broken-state | **MEDIUM** |
| Racing a legitimate caller for funds (front-run withdrawal vs release) | permissionless | race winner takes funds that were rightfully the loser's | **MEDIUM** |
| Division by zero that bricks a critical function | permissionless | permanent DoS of stake/redeem/withdraw | **HIGH** |
| Division by zero in a view-only or non-critical path | permissionless | view call reverts, no state impact | **LOW** |
| Unchecked return value of a known-standard token (SafeERC20 not used) AND the token whitelist includes a specific non-standard token (USDT, BNB, etc.) | permissionless | silent accounting drift | **MEDIUM** |
| Unchecked return value with no identified non-standard token in scope | — | speculative | **DROP — FP risk** |
| Missing nonReentrant guard AND an actual callback-enabled token is in scope (ERC777, ERC1155 receiver hook used) | permissionless | real reentrancy path | **HIGH** |
| Missing nonReentrant guard with no callback-enabled token in scope | — | speculative | **DROP — FP risk** |
| Use of `transfer()` (2300 gas) for payouts to EOAs only | — | none | **DROP — FP risk** |
| Use of `transfer()` (2300 gas) for payouts to addresses that can be arbitrary contracts | permissionless | bricked withdrawal if recipient's fallback costs > 2300 gas | **LOW** |
| Admin action with no timelock, no multi-sig enforcement visible, and the admin controls fund movement | admin | rug pull / instantaneous parameter change | **MEDIUM** (centralization) |
| Missing event emission on state-changing function | — | off-chain indexers miss state change | **LOW** or **INFO** |
| Griefing / DoS without fund loss | permissionless | single-user DoS | **LOW** |
| Griefing / DoS affecting ALL users of a critical function | permissionless | protocol-wide DoS | **HIGH** |
| Style / best-practice / non-exploitable | — | hardening only | **INFO** |

**Adjustment rules**:

- **Cap at MEDIUM** if the attacker must already be a trusted role (admin, owner, governance-elected). This is centralization risk, not exploitation.
- **Bump one tier** if the protocol holds >$10M TVL in a similar deployed protocol. drozer-lite does not know TVL; apply this only if the user stated the context.
- **Drop the finding** if the "exploit" requires a specific off-chain setup drozer-lite cannot verify (e.g., "if the admin key is compromised by phishing").

**If the table has no row that matches**: the finding is either novel or you are describing a class of issue drozer-lite is not calibrated for. Default to **LOW** and document the mismatch in your reasoning. Do not invent a severity.

### Weak-evidence severity floor (new in v0.5.1)

If the exploit sentence's `[CONCRETE LOSS]` depends on ANY of the following, cap severity at **LOW** regardless of what the main severity table returned:

- **Off-chain tree / payload construction** — e.g., merkle-leaf reuse across windows requires the admin to reuse roots off-chain; the contract itself cannot enforce it.
- **Cross-contract configuration the admin sets later** — e.g., "if the token whitelisted via `setToken` returns false on transfer" when no such token is currently referenced in scope.
- **Unobservable user ordering or mempool races** that the contract neither enforces nor defends against, where either ordering is legitimate.
- **External callback behavior** on callee types that are not in the current whitelist (ERC777 hooks, generic ERC1155 receivers) unless the code actually integrates those standards.

Combined with the severity-tier output filter (Step 7 rule 1a), these capped-at-LOW findings move to `warnings[]` by default. This kills the class of "this could be bad if the admin / off-chain / future setup cooperates" speculations that survive Gate A by sounding concrete but depend on unobservable context.

### Confidence

- **HIGH**: code clearly matches the pattern; you can quote the offending line(s).
- **MEDIUM**: pattern is present but exploitability depends on context you cannot fully verify from the cluster alone.
- **LOW**: uncertain — match is suggestive, not definitive.

**Time budget for Step 5**: ~3-5 minutes per cluster of normal density. A 50KB cluster with all checks should be ~5 min. Skip checks that obviously don't apply (e.g., `permit_frontrun` on a contract with no signatures).

---

## Step 6 — Cross-cluster sweep

After all clusters are analyzed, look for bugs that span clusters. This is the step that catches bugs single-cluster analysis misses. Apply each of the 13 cross-cluster patterns below to every pair of clusters that have references in the inventory from Step 2. Patterns 1-6 catch **symmetric asymmetries** (same modifier applied here but not there). Patterns 7-13 catch **economic cross-cluster flows** (state drift, fill/drain asymmetry, consumer-side failures).

### Patterns 1-6 — symmetric asymmetry

1. **State write/read mismatches**: function in cluster A writes state variable V; function in cluster B reads V without re-validating preconditions. Look for staleness.
2. **Cross-contract access control gaps**: cluster A function F is guarded by role R; cluster B has a wrapper W around F that has weaker or no access control. The wrapper bypasses the guard.
3. **Auto-route fallbacks**: cluster A's contract has a `receive()` or `fallback()` that calls a state-changing function in the same or another cluster, so the contract balance is never what callers expect. Check whether ANY function in the inventory uses `address(...).balance` for an invariant the auto-routing breaks. **If found, severity MUST be at least HIGH** — this is the UNI-98 pattern (see universal.md).
4. **Service interface failure modes**: cluster A provides an interface (e.g., `IOracle`); cluster B consumes it without checking for stale, zero, or revert returns.
5. **Shared modifier inconsistency**: same bug class fires in cluster A but not B, even though both use the same modifier — flag the missing application in B.
6. **Pause-state asymmetry**: a pause flag in cluster A is checked in some functions but not in functionally-equivalent siblings in cluster B.

### Patterns 7-13 — economic cross-cluster flows

7. **Snapshot-consumption drift**: cluster A stores a value `V` computed from a time-varying rate or reference (e.g. `record.amountSnapshot = shares * currentExchangeRate`). Cluster B (or a later call in cluster A) mutates that rate via reported events (loss events, reward events, slashings, rebalances, fee adjustments). The stored snapshot is never re-evaluated at consumption time, so the user or protocol is settled at a stale value. Report as `lifecycle_state_residue` with MEDIUM+ severity. This catches the class-of-bug where multi-user queue ordering around rate-changing events creates unfairness.
8. **Aggregate fill/drain asymmetry**: cluster A has a variable `V` that a write path FILLS (e.g. `V += delta` on some inbound action); cluster B/A has a drain path that DRAINS `V` under some conditions but NOT others. Look for sequences where `V` can accumulate without being drained, and where the only drain path is conditional on caller actions that may never happen. Report as `lifecycle_state_residue` or `unbounded_loop` with MEDIUM+ severity. drozer-lite cannot construct the full exploit sequence — flag the class-of-bug so the auditor can investigate whether accumulated value can be trapped.
9. **Cross-cluster unchecked caller parameter**: cluster A function accepts a contract-address parameter (e.g. `target` / `router` / `factory` / `module`) and calls into it. Cluster B is the intended target but no validation enforces it. Role-gating the caller is necessary but NOT sufficient — the caller can still pass a malicious or wrong target. Check whether cluster A stores an authoritative target or validates the parameter against a whitelist.
10. **Cross-cluster role assumption drift**: cluster A calls cluster B function F which requires role R. Cluster A is ASSUMED to hold R but it's not enforced by cluster A's constructor or initialize. If R is revoked from A externally, A's calls revert silently or bubble. Flag as operational fragility (INFO) unless it also opens an attack path.
11. **Cross-cluster counter consistency**: cluster A and cluster B both write to a shared counter variable (e.g. a global total/supply/balance held in a third cluster). Verify that both writers are mutually aware or the counter would drift under concurrent access.
12. **Provider-consumer type mismatch**: cluster A provides data in units U1 (e.g. basis points, 8-decimal fixed point, wei); cluster B consumes in units U2 (e.g. percentage, 18-decimal fixed point, whole units). Check the provider-interface output shape in cluster A against the consumer math in cluster B.
13. **Cross-cluster pause propagation**: cluster A pauses (local flag) but cluster B's functions that depend on A's state don't check A's pause. When A is paused, B continues operating on stale or partial state.

For each pattern: use the **inventory from Step 2** to identify cross-cluster references quickly. You do NOT need to re-read full cluster source to do the sweep — the inventory has the structural information.

Add cross-cluster findings to the same finding pool with `cross_cluster: true` and the names of both clusters involved.

**Be honest about confidence**: cross-cluster patterns 7 and 8 (economic flows) are pattern-level CANDIDATES for bugs. drozer-lite can flag the class of bug but cannot construct the exploit sequence — the LLM does not do multi-step actor modeling. When flagging, use MEDIUM confidence and note "pattern present, exploit sequence requires manual / `/droz3r` verification".

**Time budget for Step 6**: ~5-10 minutes for a small protocol (≤100KB). Larger protocols may need ~15 minutes.

---

## Step 7 — Emission gates, dedup, aggregate, output

Before dedup, every candidate finding in your working set MUST pass the pre-emission worksheet and three gates (A, C, B). Findings that fail are dropped — **but the drop MUST be recorded in `warnings[]`** so the audit log shows what was filtered and why. Silent drops are forbidden (v0.5.2).

### Step 7.0 — Pre-emission worksheet (MANDATORY before any gate)

For EVERY candidate finding in the working set, fill this 6-field worksheet internally before applying Gates A / C / B. REQUIRED fields must be code-backed — empty REQUIRED fields mean DROP with a `warnings[]` entry `"dropped: <title> | <field-that-failed>"`. This worksheet is the mechanical enforcement of Step 5 rule 4a, Gate A, and Gate C; the existing gate sections below retain authoritative details but the worksheet is the commitment.

| # | Field | Required? | What to fill |
|---|-------|-----------|--------------|
| 1 | Title | Y | One-line title. |
| 2 | Textbook pattern (Y/N) | Y | Mark Y for canonical well-known patterns — CEI/reentrancy, signature replay, reward-debt / MasterChef accumulator, multisig stale-approval after owner removal, ERC4626 first-depositor inflation, flash-loan oracle manipulation, approve-then-transferFrom race, missing slippage on a swap router, missing event on admin setter, missing nonReentrant on a callback-reachable function, similar known patterns. Otherwise N. |
| 3 | Specific-line break | Y if textbook=Y | `file:line` of the specific code that deviates from the textbook safe version + the one-line diff that would fix it. If textbook=Y and this field is unfillable from the current source, DROP. Enforces Step 5 rule 4a. |
| 4 | Exploit sentence | Y | *"An attacker with [ROLE/PERMISSION] calls [FUNCTION] with [CONCRETE INPUT], and the result is [CONCRETE LOSS/IMPACT]."* All four brackets filled from in-scope code; no hypothetical future state (Step 5 rule 5a), no admin-cooperation hedge (Step 5 rule 5b). If any bracket fails, DROP. Enforces Gate A. |
| 5 | Defender sentence | Y | *"This may be a false positive because [specific code-level reason backed by a visible line: a require, a modifier, a state-update ordering, a documented off-chain constraint]."* Strong defender → downgrade one tier (LOW → drop). Weak defender (intuition-only, "probably safe") → keep at original severity. No defender possible from visible code → keep at original severity. Enforces Gate C. |
| 6 | Severity row | Y | Quote the row from the Severity decision table that justifies the chosen severity. If no row matches, default to LOW per the table's no-row rule and document why no row matched. Severity by feel is forbidden. |

Exceptions that pass the worksheet without a full field 4: (a) cross-cluster economic flow candidates from Step 6 patterns 7-13 (explanation prefixed `"Pattern-level candidate:"`, confidence MEDIUM or LOW); (b) INFO-capped hardening items with a one-line justification for keeping. Both must still fill fields 1, 2, 5, 6.

After every candidate passes the worksheet, apply Gate B (reasoning reconciliation) over the full working set, then dedup/consolidation, then output.

### Gate A — Exploit-Sentence Gate (precision)

For each candidate finding, write out internally the following one-sentence exploit statement:

> *"An attacker with [ROLE/PERMISSION] calls [FUNCTION] with [CONCRETE INPUT], and the result is [CONCRETE LOSS/IMPACT]."*

Every bracket must be filled from THIS codebase's source, not from a hypothetical future state.

- `[ROLE/PERMISSION]` must be one of: `anyone` (permissionless), `any holder of X` (where X is a real role/token in this code), `the admin` (if admin is the attacker), `a contract at address Y` where Y is reachable. NOT "a future ERC777 integration", NOT "if a malicious token is whitelisted".
- `[FUNCTION]` must be a function that exists in scope.
- `[CONCRETE INPUT]` must be a value range that exists in the parameter types AND is not already rejected by a require/assert/modifier in the current code.
- `[CONCRETE LOSS/IMPACT]` must be quantifiable: "X tokens moved to attacker", "pool reserves desynced by Y", "user locked out of withdraw", etc. NOT "could cause issues", NOT "may cause confusion".

If any bracket fails, **DROP the finding**. Do not downgrade it to INFO. Do not hedge it with "theoretical". Drop it.

**Two exceptions** — findings allowed through without a concrete exploit sentence:

1. **Cross-cluster economic flow candidates** (Step 6 patterns 7-13). These are explicitly pattern-level flags that drozer-lite cannot construct exploits for. They pass Gate A with `confidence: "MEDIUM"` or `"LOW"` and the explanation must begin with `"Pattern-level candidate:"`.
2. **Informational hardening items** flagged as `severity: "INFO"` (e.g., missing event emission). These pass Gate A but are capped at INFO and must have a one-line justification for why they were kept.

### Gate C — Disprove-Before-Emit (adversarial precision)

After Gate A but before Gate B, for every candidate finding that survived Gate A, write ONE adversarial sentence in your reasoning:

> *Defender's Argument: "This may be a false positive because [specific code-level reason the exploit fails]."*

The reason must be **backed by visible code** in the current source — not intuition, not "probably safe", not "the author surely thought about it." Things that count as a valid defender:

- A specific require/assert that blocks the attack path
- A specific modifier on another function that prevents the precondition
- A specific state update ordering that neutralizes the described race
- A specific modifier/guard on the external callback that prevents reentry
- A specific off-chain constraint documented in the code (comment / NatSpec) that the contract's author relied on

Then apply this rule:

| Defender sentence quality | Action |
|---|---|
| Strong defender backed by a specific line-level guarantee | **Downgrade one tier**. If original is LOW, drop. |
| Weak defender (hand-waving, "usually", "probably") | Keep at original severity |
| No defender possible — no mitigation visible | Keep at original severity (this is a real finding) |

This gate forces the agent to *argue against itself*. Pattern matching plus concrete trace is not enough — the agent must try to disprove the finding using the code. Real bugs survive because no defender argument holds. Plausible-looking FPs get downgraded or dropped because a line-level mitigation exists.

**Visibility (mandatory, v0.5.2)**: For every Gate C decision — downgrade, drop, OR kept-at-original-severity — emit a `warnings[]` entry of the form `"defender_applied: <title> | <one-sentence defender>"` (or `"defender_none: <title> | no mitigation visible"` when no defender is possible). This makes the gate's reasoning auditable post-hoc; silent gate decisions hide regressions.

**Exception**: CRITICAL findings where the defender is only "the admin would not do that" → do NOT downgrade (admin-trust hedging is banned by Step 5 rule 5 anyway).

### Gate B — Reasoning Reconciliation (recall)

Before emitting, scan your own reasoning trace for any dismissal phrases applied to a candidate finding:
- *"actually not a vuln"*, *"self-griefing"*, *"edge case"*, *"would revert anyway"*, *"dead code"*, *"by design"*, *"admin-only so trusted"*, *"mitigated by admin whitelist"*, *"unreachable in practice"*.

For each dismissal, ask: **is the dismissal backed by a hard constraint visible in the current code (a require, a modifier, an enforced invariant) or is it an intuition about operator behavior?**

- Hard constraint → dismissal is valid → keep dropped.
- Intuition about operators, future state, or "by design" without a code-level lock → **restore the finding** at appropriate severity.

This gate prevents the agent from reasoning itself out of reporting real bugs that the pattern matcher correctly identified.

### Dedup, consolidation, and aggregation

After all three gates (A, B, C):

1. Group surviving findings by `(canonical_vulnerability_type, affected_file, affected_function)`. Two findings with the same triple are duplicates — keep the highest-severity.
1a. **Severity-tier output filter** (new in v0.5.1, schema-mismatch rule added v0.5.7): by default, the `findings[]` array contains only `CRITICAL`, `HIGH`, and `MEDIUM`. `LOW` and `INFO` findings move to the `warnings[]` array as `"low: <title>"` or `"info: <title>"` strings — preserved in output, out of the main findings list. Rationale: LOW/INFO findings are hardening observations; most scoring rubrics penalize them as false positives relative to the expected bug set. A user who wants them (real-audit context) can invoke with `--include-low` or `--full` and the skill restores them to `findings[]`.

   **Schema-mismatch rule (v0.5.7)**: When the output schema required by the caller does NOT contain a `warnings` field (external benchmark schemas, narrow CI harnesses, reports that only accept a flat `findings[]` array), LOW and INFO findings MUST be **dropped entirely** — do NOT flatten them into `findings[]` to preserve the observation. Flattening converts hardening notes into false positives against every scoring rubric that penalizes unmatched findings. The binding rule: if LOW/INFO cannot be emitted to `warnings[]`, they cannot be emitted at all. The `--include-low` / `--full` flags remain the only way to surface LOW/INFO into `findings[]`, and even then the caller is explicitly opting in to the FP risk. Detecting schema mismatch: if the output format specification (e.g., `program.md`, API contract, JSON schema) enumerates allowed top-level fields and `warnings` is not among them, the schema-mismatch rule applies.

2. **Root-cause consolidation** (new in v0.5.1, shared-check rule added v0.5.7): after dedup, group by `(canonical_vulnerability_type, affected_file)`. If two or more findings share the same vulnerability_type in the same file but hit different functions, apply these two tests in order:

   **(a) Shared-check mechanical rule (v0.5.7) — apply FIRST**: If the fix for each finding in the group is adding the SAME named check (same `require`/`assert`/modifier invocation, same validated variable or flag) — e.g. all N findings fixed by adding `require(!disputed)`, or all N fixed by adding the same `nonce` mapping check, or all N fixed by adding the same `onlyOwner` modifier — **consolidate into ONE finding**. Name the primary function in `affected_function`; list all siblings in `explanation` with `(also affects: fnA, fnB)`. The title generalises to the missing check, not the function name (e.g. "Missing !disputed check across settlement functions" rather than "refund lacks disputed check"). **Different function names alone are not grounds to keep separate** when the missing check is identical across the group.

   **(b) "Could one PR fix all of them?" test**: if the shared-check rule doesn't fire but a single code change would still resolve all findings (e.g., `executeTransfer` and `executeTokenTransfer` both missing a nonce, fixed by adding one shared nonce-check helper) → **consolidate**.

   If neither (a) nor (b) applies — fixes are genuinely independent (e.g., reentrancy in `withdrawTo` vs access control missing on `setRate` — different fix patterns) → keep as separate findings.
3. The highest-severity finding wins each consolidated slot.
4. Output a single JSON object matching the schema below. By default, no prose around it, no markdown fences. (If the user explicitly asked for a Markdown report, render the same content as a Markdown report — see the Markdown variant at the bottom.)

```json
{
  "scanner": "drozer-lite",
  "version": "0.4.0",
  "profiles_used": ["universal", "reentrancy", "oracle"],
  "files_analyzed": [
    "Token.sol", "Service.sol", "Accountant.sol",
    "Registry.sol", "PauseRegistry.sol",
    "OracleAggregator.sol", "adapters/Adapter.sol",
    "adapters/SourceImpl.sol", "adapters/IAdapter.sol"
  ],
  "clusters": [
    {"name": "PrimaryCluster", "files": 3, "findings": 6},
    {"name": "ControlCluster", "files": 2, "findings": 2},
    {"name": "OracleCluster", "files": 4, "findings": 3}
  ],
  "findings": [
    {
      "vulnerability_type": "lifecycle_state_residue",
      "affected_function": "confirmAction",
      "affected_file": "Service.sol",
      "severity": "HIGH",
      "explanation": "confirmAction requires `address(this).balance >= amount` but the receive() fallback auto-routes incoming native value into the primary state-mutating flow. Any native value sent to the contract is consumed by the auto-route instead of accumulating in contract balance, so the balance-based invariant is permanently brittle.",
      "line_hint": 305,
      "confidence": "HIGH",
      "source_profile": "universal",
      "cluster": "PrimaryCluster",
      "cross_cluster": false,
      "swc_id": null,
      "cwe_id": "CWE-672"
    }
  ],
  "stats": {
    "wall_time_sec": 1247,
    "clusters_analyzed": 3,
    "checks_loaded": 105,
    "dedup_clusters_merged": 2,
    "dedup_total": 13,
    "dedup_representatives": 11
  },
  "warnings": []
}
```

### Field rules

- `scanner` is always `"drozer-lite"`.
- `version` is `"0.5.7"`.
- `vulnerability_type` MUST be a snake_case canonical tag from the vocabulary at the bottom of this file. **You MUST pick the closest existing tag**; paraphrasing (e.g. writing `"tx.origin authorization"` when the canonical tag is `tx_origin_auth`) is NOT allowed. The vocabulary aligns with SWC Registry and Code4rena taxonomy — labels like `tx_origin_auth`, `missing_access_control`, `missing_input_validation`, `checks_effects_interactions_violation`, `signature_replay`, `reentrancy`, `oracle_staleness`, `division_by_zero`, `missing_timelock` are industry-standard and should match what external scorers and graders expect. Only if the vocabulary genuinely has no close match may you fall back to a short snake_case description — and that is an extraordinary case that should be flagged with a `warnings` entry.
- `severity` is exactly one of `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`, `INFO`. Uppercase.
- `confidence` is exactly one of `HIGH`, `MEDIUM`, `LOW`. Uppercase.
- `source_profile` MUST be one of the profiles you loaded in Step 3.
- `cluster` MUST be one of the cluster names from Step 4.
- `cross_cluster` is `true` if the finding was discovered in Step 6 (cross-cluster sweep), `false` otherwise.
- `swc_id` / `cwe_id` are nullable. Set them when the canonical vocabulary entry has them.
- `findings` may be empty.
- `warnings` should hold any size warnings, profile-load issues, or skipped clusters.

---

## Step 7.5 — Write the report to disk

After producing the findings JSON, write TWO files to the **project root** (the directory the user pointed you at):

1. **`drozer-lite-findings.json`** — the canonical JSON output from Step 7. Machine-readable, schema-compliant.
2. **`DROZER_LITE_REPORT.md`** — the Markdown variant (see Markdown format at the bottom of this skill). Human-readable, severity-grouped, with summary table, per-finding sections, and the honest framing disclaimer.

Use the Write tool. Overwrite if either file already exists (re-runs should produce fresh output).

After writing, tell the user:
```
Wrote:
  drozer-lite-findings.json  (canonical JSON, {N} findings)
  DROZER_LITE_REPORT.md      (Markdown report)
```

If the user specified `--output <path>`, write to that path instead of the project root.

**Do NOT skip this step.** Findings that only exist in conversation context are lost when the session ends. The disk files are the deliverable.

---

## Step 8 — Honest framing

ALWAYS end your response (after the JSON or Markdown report) with this disclaimer, verbatim. Do not soften it. Do not skip it.

> drozer-lite is a pattern-level scanner with cross-file awareness. It catches bugs from a curated checklist of 205 patterns across 14 protocol-type profiles, all derived from real audit findings. It does NOT do multi-step actor reasoning, chain-composition analysis, or formal verification. A clean drozer-lite run is NOT a clean audit. For high-value contracts, use `/droz3r` (the full drozer pipeline) or a human auditor on top of this.

Then add a one-line time disclosure:

> Total wall-clock time: ~XX min. {N} clusters analyzed across {M} files. {K} profiles loaded.

---

## Canonical vulnerability vocabulary (use these as `vulnerability_type`)

These are the snake_case tags. Each tag has a fixed meaning and an optional SWC/CWE cross-reference. If a finding genuinely matches none, use a short snake_case fallback and accept it will not be canonicalized.

**Vocabulary discipline (v0.5.2)**: Where multiple naming forms exist for the same concept (full vs abbreviated, alternate framings), the canonical tag chosen here matches the **unabbreviated industry-standard form** used by SWC Registry / Code4rena / Sherlock. Aliases are listed for cross-reference but MUST NOT be emitted in `vulnerability_type` — emit the canonical only. External scoring rubrics match strings literally; abbreviations lose points to no benefit.

**Tag selection between near-synonyms (v0.5.2)**: Where two canonical tags describe overlapping patterns (e.g. `reentrancy` vs `checks_effects_interactions_violation`), each entry includes a discriminator that resolves the choice. When the discriminator does not clearly resolve, prefer the broader/default tag. Do NOT invent new tags to "split the difference."

**Alias canonicalization (v0.5.5)**: Before emitting `vulnerability_type`, rewrite aliases to canonicals using the table below. This is a **mechanical lookup, not a reasoning step** — if your chosen tag appears in the left column, emit the right column verbatim. External scoring rubrics, finding-dedup tools, and SWC/Solodit cross-referencing pipelines match strings literally; paraphrases cost points against every consumer of the output. The alias list here records paraphrases that real LLM invocations have produced for the same underlying bug — add to this table when a new paraphrase is observed, do not add benchmark-specific mappings.

| Alias (do NOT emit) | Canonical (emit this) | Reason |
|---|---|---|
| `tx_origin_auth` | `tx_origin_authentication` | SWC-115 / Code4rena / Sherlock use the unabbreviated form |
| `reentrancy` (when the fix involves reordering state updates before the external call, with OR without adding a guard) | `checks_effects_interactions_violation` | Default tag per broadened discriminator in Reentrancy section below — covers callback-reentry drains too |
| `cei_violation` / `cei_bug` / `state_update_after_call` | `checks_effects_interactions_violation` | Abbreviations / alternate framings of the same canonical |
| `no_access_control` | `missing_access_control` | SWC-105 unabbreviated |
| `no_input_validation` / `input_validation_missing` | `missing_input_validation` | SWC-123 unabbreviated, consistent with other `missing_*` tags |
| `sig_replay` / `signature_replayable` | `signature_replay` | SWC-121 unabbreviated |
| `div_by_zero` / `divide_by_zero` | `division_by_zero` | Unabbreviated noun form |
| `reward_debt_stale_on_balance_change` / `reward_accounting_bug` | `lifecycle_state_residue` | Reward-debt-on-balance-change is an instance of lifecycle state residue; use the canonical unless a more specific tag applies |
| `no_slippage_check` / `slippage_missing` | `missing_slippage_protection` | Consistent with `missing_*` family |
| `no_event_emitted` / `missing_event` | `missing_event_emission` | Full noun form |

If your chosen tag is NOT in the left column and ALSO not in the canonical list further below, write a short snake_case fallback AND add a `warnings[]` entry `"novel_vulnerability_type: <tag> | <one-line reason no canonical fits>"` so the gap is visible for future vocabulary updates.

### Reentrancy / external call ordering
- `checks_effects_interactions_violation` — **Default tag for any bug whose fix is "update state BEFORE the external call / token transfer".** Covers classic CEI violations (refund before update, balance transfer before zeroing, cap check before increment) **and** callback-reentrant drains where the callback exploit is possible only because state is updated after the call. If reordering the function body to Checks → Effects → Interactions would eliminate the exploit, emit this tag. (SWC-107)
- `reentrancy` — Use ONLY for exploits that persist even with correct CEI ordering within the vulnerable function. This is cross-function reentrancy, shared-state reentry across multiple contracts, or callback-mediated state corruption where the ordering inside any single function is fine but the state invariant across functions is not. If the fix is "add `nonReentrant`" alone (ordering is already correct) → `reentrancy`. If the fix is "reorder state update before external call" (with or without adding a guard) → `checks_effects_interactions_violation`. (SWC-107, CWE-841)
- `cross_function_reentrancy` — State changed in one function is read inconsistently in another via callback. (SWC-107)
- `callback_hook_reentrancy` — ERC777/721/1155 receiver hook reenters before state finalization. (SWC-107)

### Access control
- `missing_access_control` — State-changing function lacks an authorization check. (SWC-105, CWE-284)
- `tx_origin_authentication` — Authorization decision uses tx.origin instead of msg.sender. (SWC-115) **Canonical tag is the unabbreviated form** to match SWC/Code4rena/Sherlock rubric naming. *Alias (do not emit): `tx_origin_auth`.*
- `privilege_retention_after_transfer` — Deployer or prior owner retains non-owner roles after ownership transfer.
- `rate_limit_bypass` — Sibling function or alternative path bypasses an enforced rate limit.

### Input validation
- `missing_input_validation` — User-supplied parameter is not bounded against an invariant (e.g. `discountBps > 10000`, `fee > 100%`, `amount == 0` on a critical path). (SWC-123, CWE-20)
- `missing_condition_check` — Caller preconditions (time window, state flag, counterparty consent) are not enforced, letting the caller act outside the intended state machine. Use this when the omission is a single missing require/assert, not a broader access-control gap.

### Math / arithmetic
- `integer_overflow` — Arithmetic overflow or underflow that wraps. (SWC-101, CWE-190)
- `unsafe_cast_truncation` — Narrowing cast (e.g. uint256→uint160) truncates a critical value.
- `decimal_scaling_mismatch` — Heterogeneous decimal scaling in accumulator math.
- `formula_parameter_transposition` — Formula parameters swapped (e.g. eloA/eloB) producing inverted results.
- `division_by_zero` — Denominator can reach zero in a value-moving operation.

### Token / approval
- `unchecked_return_value` — External call return value not verified. (SWC-104, CWE-252)
- `non_standard_erc20` — Non-standard ERC20 (e.g. USDT) returns no bool — call appears to succeed silently.
- `max_allowance_drain` — Approval to type(uint).max enables cross-contract drain by intermediate.
- `low_level_call_silent_success` — Low-level call to non-existent address returns success without code-size guard.

### Signatures
- `signature_replay` — Signed message can be replayed across chains, contexts, or sessions. (SWC-121)
- `permit_frontrun` — Public permit() can be front-run to consume the user's signature without try/catch.
- `eip712_typehash_mismatch` — EIP-712 type hash differs from on-chain encoding; signatures never validate.
- `signature_authorization_gap` — Signature is valid but the signer is not authorized for the target account.
- `unchecked_signed_field` — Signed struct field is included in the signature but never enforced on-chain.

### Oracles
- `oracle_staleness` — Oracle data freshness is not validated before use.
- `oracle_manipulation` — On-chain price source can be manipulated within a single transaction.
- `oracle_failure_cascading` — Oracle failure (zero / max return) cascades into sell-at-zero or DoS.

### Vault / shares
- `share_inflation` — ERC4626 share inflation via first-depositor rounding or donation attack.
- `lifecycle_state_residue` — State remains active after lifecycle transition.
- `missing_slippage_protection` — Trade or LP function lacks min-out / deadline protection.

### Cross-chain
- `cross_chain_replay` — Cross-chain message can be replayed across chains or peers.
- `missing_destination_check` — Receiver does not verify it is the intended chain/destination of the payload.
- `cross_chain_address_substitution` — msg.sender reused as a destination-chain identity (incompatible across chains).
- `msgvalue_unsigned` — msg.value not bound by the signature, allowing executor injection.

### Storage / proxy (EVM-specific — only fire on Solidity/Vyper targets)
- `uninitialized_proxy` — Logic contract initializer not disabled. (SWC-118) **EVM only.**
- `storage_layout_collision` — Upgradeable contract storage layout changed without preserving slots. (SWC-124) **EVM only.**
- `uninitialized_storage` — Storage variable defaults to zero and an unset state passes guards. (SWC-109) *Language-agnostic variant: uninitialized struct/resource fields in Move/Rust.*

### EVM-specific (only fire on Solidity/Vyper targets)
- `delegatecall_to_untrusted` — delegatecall target is attacker-controllable. (SWC-112) **EVM only.**
- `receive_auto_route_balance_invariant` — receive()/fallback() auto-calls a state-mutating function, breaking any invariant that uses `address(this).balance`. **EVM only.**
- `erc165_incomplete_coverage` — supportsInterface does not report all interfaces the contract actually implements. **EVM only.**
- `precision_loss_decimal_conversion` — Scaling between different decimal bases truncates value without rounding direction disclosure. *Language-agnostic — applies to any fixed-point math.*

### Other (language-agnostic unless noted)
- `timestamp_dependence` — Critical logic depends on block.timestamp in a manipulable way. (SWC-116) *All chains.*
- `missing_event_emission` — State-changing operation does not emit a corresponding event/log. *All languages.*
- `front_running` — Same-block front-running enables ordering-dependent profit. (SWC-114) *All chains.*
- `vrf_callback_gas` — VRF fulfillment callback exceeds the configured gas limit and reverts. *EVM/Solana.*
- `dust_order_dos` — Residual-below-threshold orders block price levels and DoS the book. *All languages.*
- `pause_time_accumulation` — Time-dependent state continues to accumulate while the protocol is paused. *All languages.*
- `unbounded_loop` — Loop over user-pushable collection with no upper bound. *All languages.*
- `irreversible_admin_action` — Admin parameter change with no timelock or two-step apply. *All languages.*
- `missing_signer_check` — Instruction/transaction does not verify the expected signer/authority. *Solana/Move/Cairo specific equivalent of `missing_access_control`.*
- `arbitrary_cpi` — Cross-program invocation target is attacker-controllable. *Solana equivalent of `delegatecall_to_untrusted`.*
- `missing_account_validation` — Account constraints (owner, discriminator, seeds) not verified. *Solana/Anchor specific.*

---

## Markdown variant (only if the user asks)

Format: Header (profiles, files, clusters) → Summary table → Per-finding sections (vulnerability_type, severity, function, file, cluster, confidence, explanation) → Disclaimer.

---

## Hard rules

1. **Do not** read checklists for profiles you did not load in Step 3.
2. **Do not** invent vulnerability types absent from the canonical vocabulary unless nothing fits.
3. **Do not** report findings without a specific function and file location.
4. **Do not** skip the honest framing disclaimer.
5. **Do not** soften severity ratings to be polite. Use the matrix.
6. **Do not** call any tool other than Read / Glob to gather source. There is no LLM API key in this skill — you ARE the LLM.
7. **Do not** load `icp` or `solana` profiles for Solidity code. They auto-load ONLY when Rust is the detected language and the appropriate framework keywords are present.
8. **Do not** exceed the 1MB total source budget. Refuse politely and recommend `/droz3r`.
9. **Do not** skip Step 6 (cross-cluster sweep) — it is the difference between v0.3.0 and v0.2.x.
10. **Do not** load a profile checklist for every cluster — load each profile checklist ONCE at the start of analysis and reuse it across clusters.
11. **Do not** reveal the inventory map or the cluster plan unless the user asks. They are working artifacts, not output.

---

## Check Authorship Rules

When adding checks to `checklists/*.md`: never use benchmark-specific names (contract names, function names, token tickers). Use generic class-of-bug descriptions only. Provenance lines are the one exception. See `CONTRIBUTING.md` for full rules.

## checklists

```

```

## checklists/cross-chain.md

# Cross-Chain Checklist

> Profile: cross-chain
> Checks: 13
> Source: ported from Drozer-v2 bridge-invariants.md (provenance cited per check)

## Methodology

Cross-chain protocols have two adversaries: an attacker on the source chain trying to mint value on the destination without a valid lock, and an attacker on the destination trying to replay, spoof, or redirect messages. For every message path, identify (a) the trust model (validators, zk proof, multisig), (b) how finality is enforced, (c) every field that is attacker-controllable in the payload, and (d) whether the callback verifies the source-chain sender (not just the local bridge caller). Use bridge-specific chain IDs, not EVM chain IDs, for every bridge API call. Refund addresses must resolve to the actual user, not the intermediary contract.

## Checks

### XCHAIN-1: Token Supply Conservation
**Provenance**: bridge-invariants.md B1
**Pattern**: Tokens can be minted on the destination chain without a verified lock on the source chain, or unlocked on the source without a verified burn on the destination.
**Methodology**: For each mint/unlock path, trace the proof of the counterpart action. Verify proof validation is complete (not just signature presence). Verify there is no admin path that mints without a proof.
**Red flags**:
- `mint(to, amount)` gated only by `onlyRelayer` with no proof validation
- Admin emergency mint without supply reconciliation

### XCHAIN-2: Message Verification Integrity
**Provenance**: bridge-invariants.md B2
**Pattern**: Messages can be forged due to incomplete signature verification, missing source chain identification, or unauthorized sender acceptance.
**Methodology**: For each message-processing function, verify signature/validator-set verification, source chain check, and sender authorization. Each field used for decisions post-verification must be part of the signed bytes.
**Red flags**:
- `executeMessage(bytes payload)` without verifying payload author
- Source chain not part of the signed digest
- Sender verification uses `msg.sender` instead of the cross-chain sender

### XCHAIN-3: Replay Attack Prevention
**Provenance**: bridge-invariants.md B4
**Pattern**: The same message can be executed more than once (same chain, different chains, or across contract upgrades).
**Methodology**: Verify every message has a nonce / unique hash tracked in a "consumed" mapping. Check whether the consumed set survives upgrades. For multi-chain systems, verify domain separation.
**Red flags**:
- Nonce scope too narrow (per-user but not per-operation)
- `executedMessages` mapping cleared on upgrade
- Same payload valid on multiple destination chains

### XCHAIN-4: Finality & Reorg Handling
**Provenance**: bridge-invariants.md B5
**Pattern**: The destination chain processes a source-chain message before source-chain finality, losing funds after a reorg.
**Methodology**: Verify confirmation-blocks are set per source chain. Verify pending messages can be cancelled on reorg. Check finality assumption matches each chain's actual finality.
**Red flags**:
- 1-block confirmation on PoW source chain
- No reorg handling mechanism
- Static confirmation count across all chains

### XCHAIN-5: Validator / Relayer Threshold Integrity
**Provenance**: bridge-invariants.md B3
**Pattern**: Threshold signatures are validated incorrectly, validator-set updates are not authenticated, or a single compromised key can pass the check.
**Methodology**: Verify signatures are collected and validated against the current validator set with the correct threshold. Verify validator-set updates are authenticated (same threshold as normal messages). Check for slashing or off-chain penalty mechanism.

### XCHAIN-6: Token Mapping Integrity
**Provenance**: bridge-invariants.md B6
**Pattern**: Token mappings between chains can be set to wrong addresses or to malicious tokens, or decimal mismatches cause value drift.
**Methodology**: Verify token mapping setters are behind timelock + access control. Verify decimal normalization between chains. Verify no wrapped token can be registered without the protocol's acknowledgement.

### XCHAIN-7: Rate Limiting & Caps
**Provenance**: bridge-invariants.md B7
**Pattern**: A single transaction or short burst can drain the entire bridge because per-tx or per-period caps are missing.
**Methodology**: Verify per-tx and per-period limits exist on minting and unlocking. Verify rate windows cannot be reset by admin mid-attack.

### XCHAIN-8: Emergency Pause & Recovery
**Provenance**: bridge-invariants.md B8
**Pattern**: A guardian can pause the bridge, but the pause does not stop all value-moving paths, or the recovery path is insecure.
**Methodology**: Verify `pause()` gates every critical function. Verify no admin path circumvents the pause. Check recovery flows.

### XCHAIN-9: LP Protection (Liquidity-Network Bridges)
**Provenance**: bridge-invariants.md B9
**Pattern**: A liquidity-network bridge allows LPs to be drained via fake claims or sandwich attacks on deposits.
**Methodology**: For each LP deposit and withdrawal path, check for delay mechanisms and sandwich protection. Verify fee distribution is pro-rata and cannot be gamed.

### XCHAIN-10: Upgrade Safety for Bridges
**Provenance**: bridge-invariants.md B10
**Pattern**: Upgrades leave pending messages stranded, cause storage collisions, or reset the validator set to an insecure default.
**Methodology**: Verify upgrade path has timelock and handles in-flight messages. Verify storage layout compatibility and validator-set preservation.

### XCHAIN-11: Bridge Callback Source Verification
**Provenance**: bridge-invariants.md B11
**Pattern**: Callbacks (`onTokenBridged`, `lzReceive`, `ccipReceive`, `sgReceive`, `receiveWormholeMessages`) trust `msg.sender` (the bridge contract) without verifying the source-chain sender, allowing arbitrary users to craft fake instructions.
**Methodology**: For each callback, verify it calls the bridge's source-sender accessor (`messageSender()`, `_srcAddress`, etc.) and validates it against an expected remote. For bridges that do not expose the source sender in the token callback (e.g., Omnibridge `onTokenBridged`), verify token bridging and instruction bridging are separated.
**Red flags**:
- `function lzReceive(...)` that uses only `require(msg.sender == endpoint)` and trusts the payload
- `onTokenBridged` that treats the `data` bytes as authenticated

### XCHAIN-12: Bridge API Parameter Correctness (Chain IDs, Refund Addresses)
**Provenance**: bridge-invariants.md B12
**Pattern**: Calls to bridge APIs use EVM `block.chainid` where the bridge expects its own ID system (Wormhole uint16 chain IDs, LayerZero endpoint IDs), or use `msg.sender` as refund when funds should return to the end user.
**Methodology**: For each bridge API call, verify chain ID uses the bridge's own system. Verify refund addresses resolve to the end user (tx.origin in multi-hop chains, or user param), not `msg.sender`. On Arbitrum, verify `callValueRefundAddress` is not attacker-controllable (holds cancellation power over retryable tickets).

### XCHAIN-13: Bridge Value Handling (msg.value Surplus & Requirements)
**Provenance**: bridge-invariants.md B13
**Pattern**: `msg.value` is sent to a bridge that does not need ETH, or excess `msg.value - cost` is left in the contract instead of refunded to the user, or the bridge reverts because `msg.value < cost`.
**Methodology**: For each bridge type, verify whether ETH payment is required (some bridges use L1 gas escrow). Verify `msg.value >= cost` before the call and `msg.value - cost` is explicitly refunded to the actual user. Verify refund defaults do not route to an intermediary.
**Red flags**:
- `bridge.send{value: msg.value}(...)` to a bridge that uses L1 gas
- Excess left in the contract after `bridge.quote()`
- Refund to `msg.sender` when `msg.sender` is a router contract

## checklists/dex.md

# DEX Checklist

> Profile: dex
> Checks: 11
> Source: ported from Drozer-v2 dex-invariants.md + amm-invariants.md (provenance cited per check)

## Methodology

DEXes and AMMs expose user trades to MEV, sandwich, and price-manipulation attacks. For every swap / route / quote path, verify: (a) user-specified minOut / maxIn bounds are enforced AFTER the final hop, (b) deadlines are checked, (c) the AMM invariant (k = x·y, StableSwap D, or Balancer weighted) is preserved or increased, (d) prices used for critical decisions are manipulation-resistant (TWAP, oracle), and (e) routers never hold user tokens across transactions and never use `balanceOf` for balance-based accounting. Test each weird-token class (fee-on-transfer, rebasing, non-bool return) against the swap/add-liquidity path.

## Checks

### DEX-1: Slippage & Deadline Enforcement
**Provenance**: dex-invariants.md D1 + amm-invariants.md A9
**Pattern**: `amountOutMin` / `amountInMax` / `deadline` are accepted but not enforced on every swap path, or enforced only on intermediate hops.
**Methodology**: For every entry function (swap, swapExactTokensForTokens, multihop), verify the minOut check occurs AFTER all hops complete. Verify `require(block.timestamp <= deadline)`. For multihop, verify intermediate tokens cannot be stolen by a callback.
**Red flags**:
- Slippage check in the single-hop helper not re-executed for multihop
- `deadline` parameter but no check in code
- Final minOut compared to intermediate hop output

### DEX-2: Route Integrity & Intermediate Token Safety
**Provenance**: dex-invariants.md D2
**Pattern**: A multihop router holds intermediate tokens between hops; a malicious intermediate pool or callback redirects them to the attacker.
**Methodology**: For each hop, verify the output is forwarded to the next hop's input address (not left on the router). Verify callbacks cannot call back into the router with the balance available. Verify the declared path matches the executed path.
**Red flags**:
- Router uses `balanceOf(this)` as the amount for the next hop
- `_swap` pulls from and pushes to `address(this)` without a guard
- Callback invocation during a multihop route not reentrancy-protected

### DEX-3: Constant-Product / StableSwap Invariant Preservation
**Provenance**: amm-invariants.md A1
**Pattern**: Rounding errors or reentrancy allow `k` to decrease below its pre-swap value, leaking value out of the pool.
**Methodology**: For Uniswap V2 style: verify `k_after >= k_before` in `_update`. For Curve StableSwap: verify D is non-decreasing. For Balancer weighted: verify weighted balances satisfy the invariant.
**Red flags**:
- Swap math using `divide-before-multiply`
- k-check omitted because "impossible in normal flow"
- Flash-swap path that decreases k before the callback

### DEX-4: TWAP Oracle Integrity
**Provenance**: amm-invariants.md A4 + dex-invariants.md D5
**Pattern**: Oracle consumers use a single-block spot price or a TWAP with too short a window, allowing flash-loan manipulation.
**Methodology**: For every price consumer, identify the window. Test whether a same-block manipulation changes the reported price. Verify accumulator overflow handling.
**Red flags**:
- `getSpotPrice()` used for liquidation decisions
- TWAP window < 30 min for critical decisions
- Observations array too small to cover required lookback

### DEX-5: Flash Swap Repayment Enforcement
**Provenance**: amm-invariants.md A6
**Pattern**: Flash swap / flash loan callback fails to enforce repayment + fee, or does so only in the happy path.
**Methodology**: For each `flash`/`flashSwap`/`uniswapV2Call` path, trace the repayment check. Verify it compares pool balance after the callback against required amount + fee. Verify reentrancy guard covers the callback.
**Red flags**:
- Repayment check uses `balanceOf` trusting caller-provided amount
- Callback that can re-enter `flash` itself

### DEX-6: Token Approval Safety & Weird Tokens
**Provenance**: dex-invariants.md D4 + D9
**Pattern**: Router accepts fee-on-transfer / rebasing / non-bool-return tokens but computes amounts from `amount parameter` instead of actual received; or assumes infinite approval is safe.
**Methodology**: For each `transferFrom` path, verify actual received is measured via balance-before/after (not input amount). Verify `SafeERC20` or low-level success check is used. Verify approvals use `increaseAllowance` or reset-to-zero pattern.
**Red flags**:
- `token.transferFrom(user, pool, amount); _swap(amount, ...);` (no fee-on-transfer handling)
- Router holds user approvals permanently
- Permit handling that does not cover DAI's non-standard interface

### DEX-7: Pool Formula / Token-Count Mismatch
**Provenance**: drozer-lite v0.4.2 — class-of-bug: an AMM pool type uses a mathematical formula that assumes a fixed number of tokens (e.g., x*y=k for 2 tokens) but the pool creation function allows more tokens than the formula supports, producing incorrect swap results and broken invariants.
**Pattern**: A DEX supports multiple pool types (constant product, stable swap, weighted). Each pool type's swap formula is designed for a specific number of tokens. The pool creation function validates the token count against a global max (e.g., MAX_ASSETS = 4) but does NOT validate against the pool-type-specific maximum. A constant-product pool can be created with 3+ tokens even though the x*y=k formula only works for 2 tokens.
**Methodology**:
1. For each pool type, identify the mathematical formula used for swaps.
2. Determine how many tokens the formula supports: constant product (x*y=k) = 2; Balancer weighted = N; StableSwap = N.
3. Check whether pool creation enforces the pool-type-specific token limit. If a constant-product pool can be created with >2 tokens, flag as HIGH.
4. Check slippage tolerance assertions — if they hardcode `deposits.len() == 2` but the check is conditional (e.g., only runs when slippage_tolerance is Some), the guard can be bypassed.
**Red flags**:
- `MAX_ASSETS_PER_POOL = 4` applied uniformly to both constant-product and stable-swap pools
- Constant-product swap formula uses only `offer_pool` and `ask_pool` (2 tokens) but pool has 3+ tokens
- Slippage check requires exactly 2 deposits but is inside `if let Some(slippage_tolerance)` — bypassed when None
- Liquidity addition works for N tokens but shares calculated via `sqrt(d0 * d1)` (2-token formula)
- Pool creation validates `len >= 2 && len <= MAX` but not `if ConstantProduct then len == 2`

### DEX-8: Withdrawal Path Lacks Minimum-Output Protection
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a liquidity withdrawal function calculates refund amounts proportionally but provides no mechanism for the user to specify minimum acceptable amounts, exposing them to sandwich attacks and unfavorable rates during high volatility.
**Pattern**: A `withdraw_liquidity` / `remove_liquidity` function burns LP tokens and returns underlying assets proportionally. The refund amounts are computed from the pool's current asset ratios. No `min_amount_out` or `minimum_receive` parameter exists. Industry standard (Uniswap V2 Router) provides `amountAMin` and `amountBMin` parameters for withdrawal protection.
**Methodology**:
1. For every liquidity withdrawal function, check whether the user can specify minimum acceptable output amounts.
2. If no minimum-output parameter exists, check whether slippage tolerance is applied to the withdrawal calculation.
3. If neither exists, flag. The user has no protection against pool composition changes between transaction submission and execution.
**Red flags**:
- `withdraw_liquidity(pool_id)` with no `min_assets_out` parameter
- Refund calculated as `pool_asset.amount * share_ratio` with no floor check
- No deadline parameter on withdrawal
- User must accept whatever the pool ratio is at execution time

### DEX-9: Asset Ordering Inconsistency Between Input and Internal State
**Provenance**: drozer-lite v0.4.2 — class-of-bug: user-provided deposit assets are sorted (by the chain or by aggregation logic) but pool-internal asset arrays maintain creation-time order. When slippage/ratio checks compare deposits[i] against pool_assets[i], the indices don't align, causing the check to use inverted ratios.
**Pattern**: A DEX pool stores assets in the order they were provided at creation time (e.g., [tokenB, tokenA]). User deposits are sorted alphabetically by the chain's coin handling or by an aggregation function (e.g., [tokenA, tokenB]). Slippage tolerance or ratio checks compare `deposits[0]/deposits[1]` against `pool_assets[0]/pool_assets[1]`. Because the orderings differ, the ratio comparison is inverted — checking the wrong price direction.
**Methodology**:
1. Check whether pool creation sorts `asset_denoms` or preserves caller-provided order.
2. Check whether `info.funds` / deposits are sorted (CosmWasm sorts funds alphabetically).
3. Check whether slippage/ratio checks index into both arrays positionally — if orderings can differ, the check is inverted.
4. A malicious pool creator can deliberately create a pool with reverse-ordered denoms to exploit this.
**Red flags**:
- `create_pool(asset_denoms: vec!["tokenB", "tokenA"])` — pool stores in this order
- Deposits arrive as `[tokenA, tokenB]` (chain-sorted)
- Slippage check: `deposits[0]/deposits[1] vs pool_assets[0]/pool_assets[1]` — inverted ratio
- Pool creation does not sort `asset_denoms` alphabetically
- Two pools with the same tokens but different ordering have different slippage behavior

### DEX-10: Disproportionate Deposit Loss (Excess Not Refunded)
**Provenance**: drozer-lite v0.4.2 — class-of-bug: when a user provides liquidity in a ratio different from the pool's current ratio, LP shares are minted based on the MINIMUM proportional share, and the excess tokens from the higher-ratio asset are effectively donated to the pool instead of being refunded.
**Pattern**: A `provide_liquidity` function calculates per-asset share ratios (`deposit_amount * total_share / pool_amount`) and mints LP tokens based on `min(share_ratios)`. The excess tokens from the non-minimum asset are added to the pool but not reflected in the minted shares. These excess tokens are permanently donated to all existing LPs. The function does NOT refund the excess to the depositor.
**Methodology**:
1. For every liquidity provision function, check how shares are computed when deposit ratios don't match pool ratios.
2. If `min(share_ratios)` is used, calculate the implied excess for each asset.
3. Check whether the excess is: (a) refunded to the depositor, (b) used to compute additional shares, or (c) silently donated to the pool.
4. If (c), check whether slippage tolerance protects against this loss. Note: slippage tolerance checks LP tokens received, NOT whether excess tokens are returned.
**Red flags**:
- `share = min(deposit_A * total_share / pool_A, deposit_B * total_share / pool_B)` with no refund of the difference
- User deposits 100A + 200B into a 1:1 pool; receives shares worth 100A + 100B; 100B is donated
- Slippage tolerance passes because LP tokens are within tolerance — but user lost 100B
- No industry-standard `_addLiquidity` that computes optimal amounts before the actual deposit (cf. Uniswap V2 Router)
- A front-runner changes the pool ratio between tx submission and execution, maximizing the user's excess donation

### DEX-11: Spread / Slippage Computed Before Fee Deduction
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the spread or slippage amount is calculated from the pre-fee return amount, making the computed spread larger than the actual slippage. This causes the slippage check to be systematically too lenient, passing swaps that should have been rejected.
**Pattern**: A swap function computes `return_amount` (before fees), then `spread_amount = expected_return - return_amount`. Fees are then deducted from `return_amount` to get `final_return`. The `spread_amount` is compared against `max_spread`. Because `spread_amount` is computed before fees, it INCLUDES the fee as part of the "spread." The actual price slippage (excluding fees) is smaller than the reported `spread_amount`, making the spread check pass when the real slippage exceeds the user's tolerance.
**Methodology**:
1. In the swap computation, identify where `spread_amount` is calculated relative to fee deduction.
2. If `spread_amount = expected - return_amount` and `return_amount` is PRE-fee, the spread includes fees.
3. Check whether the spread check (`assert_max_spread`) uses this inflated spread or the actual post-fee slippage.
4. The correct approach: compute spread AFTER fees, or compute spread as `expected_return - (return_amount - fees)`.
**Red flags**:
- `spread_amount = offer_amount * exchange_rate - return_amount` where `return_amount` is before fees
- `assert_max_spread(spread_amount, return_amount + spread_amount)` — spread includes fees
- For StableSwap: `spread_amount = offer_amount - return_amount` (1:1 assumption) computed before fees
- Fees are 5-10% but spread check uses 1% tolerance — the fee inflates the spread past the tolerance, so the check effectively allows ~15% real slippage with a 1% setting

## checklists/gaming.md

# Gaming / Outcome Determinism Checklist

> Profile: gaming
> Checks: 3
> Source: ported from Drozer-v2 injectable skill OUTCOME_DETERMINISM + universal-invariants.md U7 (timestamp) (provenance cited per check)

## Methodology

Game-like and lottery-like protocols distribute finite prize pools or make time-gated decisions whose outcomes attackers can observe before committing. The core failure modes are (a) pseudo-randomness derivable from on-chain state, (b) finite-pool selection where the depletion fallback reveals the secret, (c) observable default outcomes on time-gated actions that let attackers simulate before acting, and (d) selective-revert callbacks where the winner can force a re-roll by reverting an unfavorable result. Apply adversarial thinking: assume the attacker can simulate the contract at the block they'll be included in, and ask whether they can force any outcome in their favor.

## Checks

### GAME-1: On-Chain Randomness Predictability
**Provenance**: universal-invariants.md U7 + general Solidity RNG failure modes
**Pattern**: "Randomness" is derived from `block.timestamp`, `block.prevrandao`, `blockhash`, or `keccak256` over on-chain state; an attacker simulating the block predicts the outcome and only commits if it favors them.
**Methodology**: For every random-selection path, identify the seed source. Any seed derivable from in-block state is unsafe for value-carrying decisions. Verify commit-reveal with a future block hash, VRF (Chainlink, Pyth Entropy), or cross-transaction entropy. Verify that users cannot observe the seed and cancel.
**Red flags**:
- `uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, nonce)))`
- `blockhash(block.number - 1)` used for selection
- Commit-reveal where the reveal can be skipped when unfavorable

### GAME-2: Finite-Pool Selection & Depletion Fallback
**Provenance**: injectable skill OUTCOME_DETERMINISM (finite-pool selection with depletion fallback)
**Pattern**: A finite prize pool selects items with a fallback when the pool is empty; the attacker depletes the pool to force the fallback outcome. Alternatively, the depletion state is observable before action, letting attackers choose when to commit.
**Methodology**: For every finite-pool selection, enumerate the depletion state and the fallback outcome. Ask whether the attacker can (a) observe the depletion state atomically and skip, (b) intentionally deplete the pool to force the fallback, or (c) time their action around another's commitment. Verify the fallback does not provide a profitable alternative.
**Red flags**:
- `if (remainingPrizes == 0) return consolationPrize;` where consolation is valuable enough to target
- Attacker can call `peek()` view functions to check pool state atomically
- Prize pool re-filled mid-round from an attacker-influenced source

### GAME-3: Time-Gated Actions with Observable Default Outcomes / Selective Callback Revert
**Provenance**: injectable skill OUTCOME_DETERMINISM (time-gated actions + callback selective revert; latter is now always-on in depth templates per skill-index)
**Pattern**: A time-gated action has a default outcome if the user does not act within the window; the attacker observes the would-be outcome and acts only if unfavorable (letting the default apply otherwise). Or: a callback (RNG consumer, settlement callback) can selectively revert on unfavorable outcomes, forcing a re-roll.
**Methodology**: For every time-gated mechanism, identify the default outcome and whether attackers can observe the alternative before acting. For every callback that consumes a random result, verify the callback cannot revert on unfavorable outcomes (use try/catch to absorb reverts, or require the callback to be made by the protocol not the user).
**Red flags**:
- `if (block.timestamp > deadline) { applyDefault(); } else { requireUserAction(); }` where the user knows both outcomes
- RNG consumer contract that reverts in `fulfillRandomWords` when result is unfavorable
- Settlement callback callable by the winning party who can choose to revert

## checklists/governance.md

# Governance Checklist

> Profile: governance
> Checks: 6
> Source: ported from Drozer-v2 governance-invariants.md + staking-invariants.md (provenance cited per check)

## Methodology

Governance systems convert economic stake (ve-locks, delegated tokens, NFT membership) into voting power that authorizes fund movement. Attackers try to (a) acquire voting power cheaply (flash loans, bribes, delegation chains), (b) bypass lifecycle constraints (execute before voting ends, cancel legitimate proposals, replay executions), or (c) corrupt aggregate state (slope/bias/totalWeight) via addition without mirroring on removal. For each lifecycle entity (proposal, vote, lock), verify every function checks the entity's state, cannot be called on executed or expired entities, and updates ALL aggregate variables on removal. Build a role-capability matrix: who can propose, vote, queue, execute, cancel, and pause.

## Checks

### GOV-1: Voting Power Integrity (Snapshot vs Live)
**Provenance**: governance-invariants.md G1 + staking-invariants.md S11 (maps to STAKE-11)
**Pattern**: Voting power is read from live balances (flash-loanable) instead of a snapshot taken at proposal creation, or the checkpoint system lets a user double-vote by transferring tokens between addresses.
**Methodology**: For each vote path, verify `getPastVotes(address, snapshotBlock)` is used, not `getVotes(address)`. Verify checkpoints are written atomically on transfer and delegation. Verify no path lets a user vote with the same underlying tokens twice. Verify flash-loaned tokens cannot reach the snapshot block.
**Red flags**:
- `castVote` reads `balanceOf(user)` at current block
- Checkpoint not written in `_beforeTokenTransfer`
- Delegation chain allows circular or multi-hop amplification

### GOV-2: Proposal Lifecycle & Execution Guard
**Provenance**: governance-invariants.md G2 + G13 (maps to STAKE-12 lifecycle state completeness)
**Pattern**: Functions operating on proposals do not check `state(id)`, allowing votes after queuing, fund deposits on executed proposals, or re-execution of executed proposals.
**Methodology**: For every function that accepts a proposal ID, verify it reads `state(id)` or equivalent. Verify `fund()`, `deposit()`, `vote()` cannot be called after queue/execute. Verify `state()` snapshots at transitions rather than re-evaluating live tallies.
**Red flags**:
- `fund(proposalId)` with no state check
- Vote accepted during queued/executed state
- `state()` re-derives from mutable tallies post-queue

### GOV-3: Timelock Security
**Provenance**: governance-invariants.md G4 + G6
**Pattern**: Timelock can be bypassed, its delay can be set to zero, its admin is an EOA, or direct admin calls skip the timelock entirely.
**Methodology**: Verify timelock admin is the governor contract, not an EOA. Verify `setDelay()` itself goes through the timelock. Verify the minimum delay cannot be zero. Verify executed payload hashes match queued payload hashes exactly (no substitution).
**Red flags**:
- `timelock.admin == msg.sender (EOA)`
- `setDelay(0)` allowed
- Executor accepts mismatched targets/values/calldatas from queue

### GOV-4: Aggregate State Consistency on Removal
**Provenance**: governance-invariants.md G11 + staking-invariants.md U27 (maps to STAKE-13 aggregate removal)
**Pattern**: When a nominee/voter/delegate/lock is removed, some aggregates are updated (bias, totalWeight) but not others (slope, changesSum), so future time-weighted extrapolation returns corrupted values.
**Methodology**: For each add/remove function, enumerate every aggregate variable. Verify every aggregate is decremented on removal. For time-weighted aggregates (bias -= slope * time), verify the slope is also corrected. For two-step removal flows (admin + user cleanup), verify the aggregate updates in at least one step, ideally the admin step. Test the zero-aggregate edge case.
**Red flags**:
- `remove()` updates `totalBias` but not `totalSlope`
- Two-step removal where users never complete step 2, leaving inflated aggregate
- Division by zero when all participants removed

### GOV-5: Checkpoint MAX_WEEKS / Loop Coverage
**Provenance**: governance-invariants.md G12 (maps to STAKE-16 MAX_WEEKS coverage)
**Pattern**: A checkpoint loop bounded by `MAX_NUM_WEEKS` is smaller than the maximum lock period divided by `WEEK`, so inactive nominees beyond the window return zero weight (loss of voting power) or the loop exits early with stale state.
**Methodology**: Verify `MAX_NUM_WEEKS >= maxLockPeriod / WEEK` (e.g., 4-year lock needs >= 209 weeks). Verify nominees inactive for > `MAX_NUM_WEEKS` still return correct weight or explicitly return zero with a migration path. Verify permissionless nominee creation cannot spam entries that become stale and waste gas.
**Red flags**:
- `uint256 public constant MAX_NUM_WEEKS = 52;` with 4-year lock support
- `if (weeksPassed > MAX_NUM_WEEKS) return 0` in critical weight calculation

### GOV-6: Exit-Function Balance Manipulation (AC-12 / AC-13)
**Provenance**: governance-invariants.md G14 (maps to AC-12 exit guard + AC-13 balance source)
**Pattern**: `ragequit`/`withdraw` reads `balanceOf(treasury)` as the payout source; MEV searchers sandwich the exit with treasury-draining proposals. Also covers access-control gaps on exit: anyone can trigger another member's exit, or exit is callable outside the member's expected lifecycle window.
**Methodology**: Verify exits use internal accounting, not `balanceOf` live reads. Verify exit-access control restricts callers to the owning member (or an approved delegate). Check whether proposals spending treasury can be timed against exit windows.
**Red flags**:
- `payout = treasury.balanceOf() * shares / totalShares`
- `ragequit(address member)` permissionless and caller unrelated to member
- Time delay between exit request and execution creates a sandwich window

## checklists/icp.md

# ICP Canister Checklist

> Profile: icp
> Checks: 16
> Source: ported from Drozer-v2 icp-canister-invariants.md (provenance cited per check)

## Methodology

Internet Computer canisters run under a split-message execution model: every `await` is a commit point. Any state change before an await persists even if the callback traps. Apply adversarial thinking to every `#[update]` method: who can call it (authenticated? anonymous?), what state it commits pre-await, what happens if the callback panics, whether state remains consistent under interleaved concurrent calls, and whether the method is idempotent if retried. Check caller authentication explicitly — Anchor's `Signer<'info>` equivalent on IC is `authenticated_caller()` helpers that reject `Principal::anonymous()`. Also verify canister upgrade resilience: pre_upgrade must not trap, timers must be re-registered in post_upgrade, and stable memory must use MemoryManager isolation.

## Checks

### ICP-1: Caller Principal Validation
**Provenance**: icp-canister-invariants.md IC1
**Pattern**: A `#[update]` method does not verify the caller or accepts `Principal::anonymous()`, allowing unauthenticated state mutation.
**Methodology**: For every `#[update]`, verify it calls a centralized `authenticated_caller()` helper that rejects anonymous. Verify controller-only operations use `is_controller()`. Guard functions cannot access method arguments — argument-dependent auth must be in the method body.
**Red flags**:
- `#[update] fn transfer(to, amount)` with no `caller()` check
- Anonymous principal not rejected explicitly

### ICP-2: Inter-Canister Call Atomicity
**Provenance**: icp-canister-invariants.md IC2
**Pattern**: State is modified before an `await`; the callback traps; the pre-await state persists, creating an orphaned debit or stale lock.
**Methodology**: For every async method, identify pre-await state writes. Verify either they are safe to keep on callback failure OR a compensation handler reverses them. Verify no `unwrap()`/`expect()` after any await (a panic becomes a trap that rolls back only the callback). Use `CallerGuard` with Drop cleanup for value-moving methods.
**Red flags**:
- `balance -= amount; ic_cdk::call(...).await.unwrap();`
- Lock acquired pre-await without Drop-based cleanup
- Pre-await writes with no reverse on Err

### ICP-3: Canister Upgrade Resilience
**Provenance**: icp-canister-invariants.md IC3
**Pattern**: `#[pre_upgrade]` traps on large state, heap state is not persisted, or timers are lost after upgrade.
**Methodology**: Verify `pre_upgrade` either does not exist or cannot trap under any input (bounded serialization, no unwrap). Verify persistent data uses stable memory, not heap. Verify version bytes enable schema migration. Verify `post_upgrade` re-registers all timers and handles schema migration. Test with production-scale data.
**Red flags**:
- `pre_upgrade` that calls `unwrap()` on serialization
- Heap `HashMap` used for persistent balances
- `set_timer_interval` not re-registered in `post_upgrade`

### ICP-4: Stable Memory Isolation
**Provenance**: icp-canister-invariants.md IC4
**Pattern**: Two `StableBTreeMap` / `StableCell` structures share the same `MemoryId`, silently overwriting each other's data; or `static mut` introduces non-exclusive mutable references.
**Methodology**: Verify `MemoryManager` is used and every `StableBTreeMap`/`StableCell`/`StableVec` has a unique `MemoryId`. Verify no raw `stable_read`/`stable_write` overlaps managed regions. Verify no `static mut` global state — use `thread_local!` with `Cell`/`RefCell`.
**Red flags**:
- Two maps initialized with the same `MemoryId`
- `static mut GLOBAL: ...`

### ICP-5: Query Response Authenticity (Certified Data)
**Provenance**: icp-canister-invariants.md IC5
**Pattern**: Financial data (balances, prices, ownership) is returned via a `#[query]` without certification, allowing a single malicious replica to forge the response.
**Methodology**: For every query returning security-critical data, verify certified variables are used. Verify `set_certified_data()` is called in the producing update. Verify frontend consumers check the IC certificate, timestamp (<5 min), and witness. Verify assets are served via the canister's certified endpoint, not `raw.icp0.io`.
**Red flags**:
- `#[query] fn get_balance(user) -> Nat` with no certified data
- Asset canister serving via `raw.icp0.io`

### ICP-6: Cycles Drain Resistance
**Provenance**: icp-canister-invariants.md IC6
**Pattern**: Public endpoints can be spammed to drain cycles — either by Candid space/cycle bombs, unbounded inputs, or expensive HTTPS outcalls.
**Methodology**: Verify `inspect_message` rejects unauthenticated ingress where possible. Verify authentication runs before Candid decoding. Verify variable-size inputs (`Vec`, `String`, `Nat`) have size caps. Verify per-caller rate limiting on expensive paths. Verify `freezing_threshold` is set.
**Red flags**:
- `#[update] fn process(data: Vec<u8>)` with no size cap
- HTTPS outcall endpoint with no authentication

### ICP-7: Untrusted Canister Communication
**Provenance**: icp-canister-invariants.md IC7
**Pattern**: Inter-canister calls to untrusted canisters use unbounded-wait semantics, allowing the callee to stall the caller forever, block upgrades, or send a Candid bomb that traps the callback.
**Methodology**: Verify calls to untrusted canisters use `Call::bounded_wait`. Verify response data is validated. Verify `SYS_UNKNOWN` rejection is handled explicitly. Avoid circular call graphs.
**Red flags**:
- `ic_cdk::call(external, ...).await` with no timeout
- Callback that unwraps untrusted Candid without decoding quota

### ICP-8: Controller Security & Decentralization
**Provenance**: icp-canister-invariants.md IC8
**Pattern**: A single controller key can upgrade, stop, or delete a canister holding user funds with no multi-party approval.
**Methodology**: Document controllers and their trust level. Verify high-value canisters have multi-party controllership or a decentralization path. Verify no single controller can unilaterally alter code.

### ICP-9: Timer & Heartbeat Reliability
**Provenance**: icp-canister-invariants.md IC9
**Pattern**: Timer-based security (oracle updates, expiry checks) is lost on upgrade because timers are not re-registered, or heartbeat cost is unbounded and drains cycles.
**Methodology**: Verify all timers are re-registered in `post_upgrade`. Verify heartbeat per-invocation cost is bounded. Verify callbacks do not hold locks across async boundaries.

### ICP-10: Arithmetic & Precision Safety
**Provenance**: icp-canister-invariants.md IC10
**Pattern**: Release builds with `overflow-checks = false` silently wrap on overflow; financial code uses `f32`/`f64`; division-by-zero traps mid-callback.
**Methodology**: Verify `overflow-checks = true` in `[profile.release]` OR all arithmetic uses `checked_*`/`saturating_*`. Verify no floats in financial calculations — use `rust_decimal` or `num_rational::Ratio`. Verify type casts (`as u64`) are bounds-checked. Verify divisors are checked before division.
**Red flags**:
- `balance += amount` without `checked_add`
- `f64` used for token amounts
- `total / stakers.len() as u64` with no zero check

### ICP-11: HTTPS Outcall Safety
**Provenance**: icp-canister-invariants.md IC11
**Pattern**: POST requests to external APIs are sent by every subnet node (N copies), response headers are non-deterministic causing consensus failure, or API credentials leak to node operators.
**Methodology**: Verify POST/PUT uses idempotency keys. Verify a `transform` function normalizes response headers. Verify API credentials are not embedded in request bodies/headers. Verify HTTPS outcall endpoints are authenticated.

### ICP-12: Candid Type Safety
**Provenance**: icp-canister-invariants.md IC12
**Pattern**: A Candid type like `Vec<Null>` encodes to a tiny payload but decodes to a massive in-memory allocation, spiking cycle usage; or a crafted payload exploits CVE-2023-6245.
**Methodology**: Verify `Vec<T>` / `String` / `Nat` parameters have length or magnitude caps. Verify `candid >= 0.9.10`, `ic-cdk >= 0.16.0`, `ic-stable-structures >= 0.6.4`.

### ICP-13: State Consistency Under Interleaving
**Provenance**: icp-canister-invariants.md IC13
**Pattern**: Two concurrent methods pass the same eligibility check pre-await and both proceed, causing double-spend because neither sees the other's commit.
**Methodology**: For every async method with shared state, verify invariants hold under all message interleavings. Use optimistic update + compensation for pre-await writes. Re-read captured variables after await.

### ICP-14: Idempotency & Deduplication on Retry
**Provenance**: icp-canister-invariants.md IC14
**Pattern**: A bounded-wait call returns `SYS_UNKNOWN`; the caller retries; the callee has already processed the first call, resulting in double execution.
**Methodology**: Verify financial operations use dedup IDs (sequence numbers, memo, nonce). Verify the callee rejects duplicate IDs within a window. Verify retry logic never blindly retries non-idempotent operations.

### ICP-15: Principal-Anchored Resource Accounting
**Provenance**: icp-canister-invariants.md IC1 + IC6 (Rule 12 applied to IC)
**Pattern**: Resource accounting (balances, quotas) is keyed by a user-supplied principal rather than the caller, allowing one user to burn another user's quota by invoking a method on their behalf.
**Methodology**: For every method that reads/writes per-principal state, verify the key is `caller()` or explicitly validated to match. No method should accept a principal parameter unless the caller is authorized to act for that principal.
**Red flags**:
- `#[update] fn claim(principal: Principal)` with no caller-to-principal check
- Quota enforcement keyed by `args.user` rather than `caller()`

### ICP-16: Lock & Guard Drop Correctness
**Provenance**: icp-canister-invariants.md IC2 + IC9
**Pattern**: A lock or guard is acquired before an await; the callback traps; the lock is never released because Drop does not run on trap (only on Err).
**Methodology**: Verify all pre-await locks are inside a `CallerGuard` with `call_on_cleanup` (CDK 0.5.1+), not a plain RAII guard. Verify the cleanup is installed before the await. Audit lock-holding timers.
**Red flags**:
- `let _guard = LOCK.lock();` followed by `.await`
- Lock released in an Err branch only

## checklists/lending.md

# Lending Checklist

> Profile: lending
> Checks: 5
> Source: ported from Drozer-v2 lending-invariants.md (provenance cited per check)

## Methodology

Lending protocols hold collateral against debt at a time-varying exchange rate driven by oracles. Attackers manipulate price or interest accounting to either withdraw more than they deposited or force wrongful liquidation of healthy positions. For each borrow/repay/liquidate path, trace: (a) which oracle is read, (b) whether staleness and decimal conversions are correct, (c) whether health-factor checks bracket every collateral-moving path, and (d) whether rounding in interest accrual and liquidation math favors the protocol. Test boundary states explicitly: fresh market, frozen asset, paused underlying, extreme utilization, and dust positions.

## Checks

### LEND-1: Collateralization / Health Factor Bracket
**Provenance**: lending-invariants.md L1
**Pattern**: A collateral-withdrawal, borrow, or collateral-swap path mutates position state without re-checking the health factor afterwards, allowing undercollateralized exits.
**Methodology**: Enumerate every path that reduces collateral OR increases debt. For each, verify a health-factor check occurs AFTER the state mutation (not before). Check that collateral valuation uses current oracle prices with proper decimal handling and the LTV limit for the specific asset.
**Red flags**:
- `withdraw(amount); updatePosition(); // no HF check`
- Borrow path that checks HF against stale cached price
- Collateral swap (oldAsset→newAsset) with HF computed only on old asset

### LEND-2: Interest Accrual Monotonicity & Precision
**Provenance**: lending-invariants.md L2
**Pattern**: Interest index resets, decreases, or loses precision, allowing debt reduction or underflow.
**Methodology**: For each interest-index function, verify the index is strictly non-decreasing under every path. Verify large time gaps do not overflow. Check rounding direction: debt rounds up, supply rounds down.
**Red flags**:
- Interest index reset on pause/unpause
- `accrueInterest()` not called before health-factor read
- Compound math using `divide-before-multiply`

### LEND-3: Liquidation Fairness & Bad Debt Prevention
**Provenance**: lending-invariants.md L3 + L5
**Pattern**: Liquidations fire on healthy positions due to stale oracle prices, or liquidators leave unprofitable dust positions behind that accumulate into bad debt.
**Methodology**: Verify oracle freshness is checked at liquidation time. Verify the liquidation bonus does not push the position into bad debt (bonus must not exceed remaining margin). Verify close-factor limits. Test dust positions: can a liquidator profitably close them? If not, a shortfall / bad-debt socialization mechanism must exist.
**Red flags**:
- `latestRoundData()` used without staleness threshold
- Liquidation penalty > remaining collateral
- No dust-close mechanism for positions below gas cost

### LEND-4: Oracle Integration (Staleness, Decimals, Failure Modes)
**Provenance**: lending-invariants.md L7
**Pattern**: Oracle reads omit staleness, don't handle Chainlink decimals correctly, or lack a fallback for zero/reverting feeds.
**Methodology**: For every oracle read, verify (a) `updatedAt` staleness check against asset-specific heartbeat, (b) decimal conversion between feed and internal math, (c) handling of `price <= 0` and reverts, and (d) at least one fallback source for high-value operations.
**Red flags**:
- `getPrice()` ignores `updatedAt`
- Assumes 18-decimal feed for an 8-decimal Chainlink aggregator
- No circuit breaker on extreme deviations

### LEND-5: Flash-Loan + Price Manipulation on Borrow
**Provenance**: lending-invariants.md L9 + L7
**Pattern**: An attacker flash-loans tokens, manipulates a spot price used for collateral valuation, borrows against the inflated collateral, then repays the flash loan with the borrowed funds.
**Methodology**: For every collateral that can be valued against a manipulable source (Uniswap spot reserves, `balanceOf`, in-protocol AMM), verify the price is derived from TWAP or external oracle. Check whether the same block can host both manipulation and borrow.
**Red flags**:
- Collateral price derived from `pool.getReserves()` directly
- TWAP window < 1 block effective
- No reentrancy guard on borrow path used during a flash-loan callback

## checklists/math.md

# Math Checklist

> Profile: math
> Checks: 6
> Source: ported from Drozer-v2 universal-invariants.md U24-U28 + invariant-templates.md MF3 (provenance cited per check)

## Methodology

Numerical code in smart contracts fails in specific, learnable ways: rounding in the wrong direction (dust extraction), division-before-multiplication (precision loss), order-dependent normalization across branches, format-selection asymmetry, and gap-range precision collapse. Attackers look for every place where operations are non-commutative, where formats differ across paths, and where the rounding direction is not documented. When a function implements a mathematical specification, compare the implementation to the reference line-by-line, paying particular attention to shift masking, overflow trapping, and alignment semantics.

## Checks

### MATH-1: Rounding Direction (Protocol-Favorable)
**Provenance**: invariant-templates.md MF-3
**Pattern**: Asset/share math rounds in the user's favor instead of the protocol's, enabling dust extraction via repeated deposit/withdraw cycles.
**Methodology**: For every `mulDiv` and division in asset↔share conversion, verify the rounding direction. Deposits must round shares DOWN (user receives at least this many). Withdrawals must round assets DOWN (user receives at most this many). Fee calculations must round toward the protocol.
**Red flags**:
- `shares = amount * totalSupply / totalAssets` with implicit round-up
- `mulDiv(a, b, c, Math.Rounding.Up)` on a user redemption path
- Symmetric deposit/withdraw rounding (both up or both down)

### MATH-2: Divide-Before-Multiply Precision Loss
**Provenance**: universal-invariants.md U3 (Slither divide-before-multiply)
**Pattern**: `(a / b) * c` loses precision when `a / b` truncates; the correct form is `(a * c) / b`.
**Methodology**: Grep every division followed by multiplication within the same expression tree. Apply Slither's `divide-before-multiply` detector. For each hit, reorder to multiplication-first unless overflow prevents it (use `FullMath.mulDiv`).
**Red flags**:
- `fee = (amount / 10000) * feeRate` where `feeRate < 10000`
- `reward = (balance / periodLength) * duration`

### MATH-3: Multi-Step Normalization Ordering
**Provenance**: universal-invariants.md U24
**Pattern**: Non-commutative adjustments (normalize, halve, round, scale) are applied in different orders across branches of the same function.
**Methodology**: For each multi-step adjustment sequence, list the steps in order for every branch. For each adjacent pair (A, B), ask whether swapping changes the result. If yes, verify the order is consistent across all branches.
**Red flags**:
- Large-value branch: halve-then-adjust; small-value branch: adjust-then-halve
- Pre-halving modification that should have been post-halving

### MATH-4: Format / Precision Selection Consistency
**Provenance**: universal-invariants.md U25 + U26
**Pattern**: A library supports multiple formats (small/large, compressed/full) but the format selector differs across paths — encoding uses more criteria than arithmetic output, for example, so values that qualify for the large format via encoding are downcast by arithmetic.
**Methodology**: Build a FORMAT SELECTION RULE TABLE per path. Verify all rows are identical. Construct boundary values that expose the asymmetry and trace them through each path. Verify `encode(decode(encode(x))) == encode(x)`.
**Red flags**:
- Encoding uses [digit count, exponent]; arithmetic output uses [exponent] only
- Boundary values where decode loses information

### MATH-5: Representation Gap Integrity
**Provenance**: universal-invariants.md U26
**Pattern**: Values "in the gap" between small and large representations are silently truncated beyond stated tolerance; the loss compounds when two gap values are multiplied.
**Methodology**: Identify the gap range from the format selector's criteria. Measure precision loss for values in the gap. Verify it stays within any stated tolerance (e.g., "1 ULP accuracy"). Test that `gap_value * gap_value` does not exceed acceptable error.
**Red flags**:
- Selector checks exponent only, precision depends on digit count
- No explicit tolerance specification in code or spec
- Gap-value multiplication in a hot path

### MATH-6: Aggregate Removal & Stale Snapshot
**Provenance**: universal-invariants.md U27 + U28
**Pattern**: Aggregate state variables (totalWeight, totalSlope, sumBias, totalSupply) are decremented on addition but not on removal, or a function copies a storage array to memory before mutating the storage array and then uses the stale copy for arithmetic.
**Methodology**: For each aggregate variable, verify every removal path decrements it. For time-weighted aggregates, verify slope corrections. For every function that copies storage to memory then mutates storage, verify subsequent reads use the current storage, not the stale copy. Test swap-and-pop cases where the pre-eviction index no longer maps to the same element.
**Red flags**:
- `remove()` updates `bias` but not `slope`
- `uint256[] memory cache = storageArray; evict(); cache[i]` used after eviction
- Swap-and-pop last-element case that leaves a stale companion mapping entry

## checklists/oracle.md

# Oracle Checklist

> Profile: oracle
> Checks: 3
> Source: ported from Drozer-v2 invariant-templates.md OR1-5 + lending-invariants.md L7 (provenance cited per check)

## Methodology

Any contract that reads an external price feed inherits all of that feed's failure modes: staleness, deviation, zero/negative prices, circuit-breaker halts, decimal mismatch, and outright reverts. Attackers manipulate prices via flash loans (spot reserves) or wait out heartbeats (Chainlink). Every oracle consumer must (a) validate freshness against the feed's asset-specific heartbeat, (b) convert decimals to the contract's internal math, (c) handle `price <= 0` and feed reverts, and (d) degrade gracefully rather than brick permanently. If multiple functions read the same feed, they must all use the same staleness threshold and fallback.

## Checks

### ORACLE-1: Staleness Protection (Per-Asset Heartbeat)
**Provenance**: invariant-templates.md OR-1 + lending-invariants.md L7
**Pattern**: Oracle price reads omit a staleness check or use a one-size-fits-all threshold that is too loose for volatile assets and too tight for stable ones.
**Methodology**: For every oracle read, verify `require(updatedAt >= block.timestamp - heartbeat)` where `heartbeat` is the asset-specific value (Chainlink publishes per-feed heartbeats). Verify the same heartbeat is used by every reader of the same feed (see ORACLE-3). Verify `updatedAt != 0` to reject uninitialized feeds.
**Red flags**:
- `(, int256 price, , , ) = feed.latestRoundData()` with no `updatedAt` usage
- Single `MAX_STALENESS` constant applied to a mix of ETH (20m) and stablecoin (24h) feeds
- No lower bound on `updatedAt`

### ORACLE-2: Manipulation Resistance & Graceful Degradation
**Provenance**: invariant-templates.md OR-2 + OR-3 + OR-4
**Pattern**: A price used for liquidation, borrowing, or minting is derived from instantaneous on-chain state (spot reserves, `balanceOf`) and can be flash-loan-manipulated in a single block; or a single oracle failure bricks the protocol; or a stale/zero feed is silently accepted.
**Methodology**: For every security-critical price, verify the source is TWAP, Chainlink, or another time-weighted mechanism. Verify the protocol pauses (not reverts permanently) on oracle failure. For high-value operations, verify prices are cross-checked against a second source OR bounded by a circuit breaker.
**Red flags**:
- `getPrice() = reserve1 * 1e18 / reserve0` for a liquidation decision
- Hard revert with no admin pause path if the feed returns zero
- Single-source pricing for >$10M positions
- `price <= 0` not explicitly handled

### ORACLE-3: Feed Consistency Across Readers (Decimal & Threshold Uniformity)
**Provenance**: invariant-templates.md OR-5 + lending-invariants.md L7
**Pattern**: Multiple functions read the same oracle feed but apply different staleness thresholds, different decimal conversions, or different fallbacks, producing inconsistent behavior (one function accepts a price another rejects).
**Methodology**: For each oracle feed, identify every reader. Build a reader matrix: [function, staleness threshold, decimal conversion, fallback]. Verify all rows are identical (or the differences are explicitly justified). Verify decimal conversion matches the feed (Chainlink returns 8 or 18 depending on the feed; do not assume 18).
**Red flags**:
- `getPrice()` uses 1h staleness but `liquidate()` reads the same feed with 24h
- `getPrice()` converts from 8 decimals while `isSolvent()` assumes 18
- One function falls back to secondary, another reverts

## checklists/reentrancy.md

# Reentrancy Checklist

> Profile: reentrancy
> Checks: 5
> Source: ported from Drozer-v2 invariant-templates.md RE1-4 + universal-invariants.md U4 + amm-invariants.md A6 (provenance cited per check)

## Methodology

Reentrancy is the oldest class of smart-contract exploit and remains a top cause of fund loss. The core question is: during any external call, can control return to the contract (or a cousin contract sharing state) before the current function's state updates are complete? Apply CEI as a checklist, but do not trust it as a guarantee — read-only reentrancy and cross-contract reentrancy can bypass a correct CEI function. For every `call`, `send`, `transfer`, token operation, ERC777/ERC721 callback, flash-loan callback, and external interface call, ask who can gain execution control and what state they can observe or modify.

## Checks

### RE-1: CEI Pattern Violation (Classic Reentrancy)
**Provenance**: invariant-templates.md RE-1 + universal-invariants.md U4
**Pattern**: External call occurs before state updates; an attacker re-enters during the call and observes stale state that permits double-spend or double-withdraw.
**Methodology**: For every function that makes an external call, enumerate state reads and writes. Confirm all writes affecting subsequent guards occur BEFORE the external call.
**Red flags**:
- `token.transfer(to, amount); balances[msg.sender] -= amount;`
- Withdraw path updating balance after `.call` succeeds
- Token minting before external callback

### RE-2: Guard Coverage Gap (Missing nonReentrant)
**Provenance**: invariant-templates.md RE-2
**Pattern**: A function that both modifies state and makes external calls lacks a reentrancy guard, either because the author believed CEI was enough or forgot the modifier.
**Methodology**: For every external-facing state-modifying function that makes external calls, require either `nonReentrant` or structural CEI proof. Prefer belt-and-suspenders (both) on any value-moving path.
**Red flags**:
- `function deposit() external payable { ... }` with no `nonReentrant` but calls a user-supplied hook
- Payable receive function with state writes

### RE-3: Cross-Function Reentrancy
**Provenance**: invariant-templates.md RE-3
**Pattern**: Function A has `nonReentrant`; function B (shares state) does not, so reentering via B during A's external call bypasses the guard.
**Methodology**: Group functions by shared state. Verify every function in a group has reentrancy protection, or structurally cannot observe mid-A state. Watch for view functions used in other contracts' logic (read-only reentrancy).
**Red flags**:
- `deposit` is `nonReentrant`, `balanceOf` is not, another contract calls `balanceOf` during `deposit`'s callback
- Vault share accounting function not guarded while `withdraw` is

### RE-4: Read-After-Call for Security Decisions
**Provenance**: invariant-templates.md RE-4
**Pattern**: A variable read after an external call is used for access control, balance checking, or amount calculation. The callee can manipulate that state during the call.
**Methodology**: For every external call, audit what is read after. Any security-critical read after an external call must either be re-validated or moved before the call.
**Red flags**:
- `balance = balanceOf(user); target.call(...); if (balance > X) { ... }` (but balance was captured before call) — worse: re-read after call
- `require(owner == msg.sender)` read after an untrusted call

### RE-5: Flash Loan / Flash Swap Callback Reentrancy
**Provenance**: amm-invariants.md A6 + perp-invariants.md P4
**Pattern**: Flash-loan callback re-enters the flash-loan contract, the pool, or a downstream protocol to exploit an intermediate state. Also covers liquidation callbacks that allow the liquidated user to re-enter.
**Methodology**: For every flash-loan, flash-swap, or liquidation with a user-controlled callback, verify the invariant (k, repayment, collateral ratio) is checked AFTER the callback and that the callback cannot call back into the flash function or related state-modifying functions.
**Red flags**:
- `flash` entry function not `nonReentrant`
- Callback runs before the pre-callback balance check is cached
- Liquidation bonus paid before the debt repayment is finalized

## checklists/signature.md

# Signature Checklist

> Profile: signature
> Checks: 4
> Source: ported from Drozer-v2 analyses (provenance cited per check)

## Methodology

Signatures authorize specific actions. An attacker controls what is NOT in the digest. For every ecrecover / EIP-712 / permit / isValidSignature call site, build a SIGNED DATA BINDING TABLE: list every field the function uses post-verification, mark each as signed (YES/NO), and mark each as caller-controllable (YES/NO). Every (Signed=NO, Used=YES, Caller-controlled=YES) row is a finding. Always check domain separation (chainId, verifyingContract), nonce/replay, and recipient/target binding. Attackers will front-run permit signatures from the mempool and reuse them in different execution contexts.

## Checks

### SIG-1: Digest Coverage Inventory (Unsigned Execution-Affecting Fields)
**Provenance**: signed-data-completeness.md §1 (maps to original AC-16 / APPROVAL-4)
**Pattern**: A function verifies a signature over a digest but decisions made after verification use fields that are not part of the signed bytes. Unsigned metadata (deadline, mode flags, gas parameters, callback data, recipients, amounts) can be substituted by a relayer, bundler, or MEV searcher without invalidating the signature.
**Methodology**: For every `ecrecover`, `ECDSA.recover`, `_hashTypedDataV4`, `SignatureChecker.isValidSignatureNow`, and any custom verification, identify the exact bytes being hashed. Build the SIGNED DATA BINDING TABLE. For every field used after verification, check whether it is inside the hashed payload. For each unsigned-but-used field, ask whether a relayer/bundler/MEV searcher can choose or modify it. Common unsigned fields: validity window, execution mode flags, gas parameters, callback data, auxiliary metadata.
**Red flags**:
- `deadline` read after verification but not in `_hashTypedDataV4` struct
- `callGasLimit`/`verificationGasLimit` consumed by executor but absent from the digest
- Oracle timestamp passed alongside but not inside the signed attestation
- ERC-4337 UserOp fields (paymasterAndData, maxFeePerGas) not part of `userOpHash`

### SIG-2: Domain Separation (Cross-Chain / Cross-Contract Replay)
**Provenance**: signed-data-completeness.md §2 (maps to original SEM-14 — was SOL-14 in solidity-semantics)
**Pattern**: Signatures are replayable across chains or across sibling contracts because the EIP-712 domain separator is missing, incomplete, or hardcoded.
**Methodology**: For every EIP-712 construction, verify the domain includes `chainId` AND `verifyingContract`. Verify the domain separator is recomputed (or cached with a chainId guard) to survive hard forks. For each `typeHash`, verify different operation types use distinct struct hashes to prevent operation confusion.
**Red flags**:
- Hardcoded `DOMAIN_SEPARATOR` without `block.chainid` recompute
- Missing `verifyingContract` in the EIP-712 domain struct
- Shared `typeHash` across unrelated operations
- Multi-contract system where a signature for contract A is accepted by contract B with identical code

### SIG-3: Recipient / Target Binding (Metadata & Beneficiary)
**Provenance**: signed-data-completeness.md §3 + §5 (maps to original DEFI-48)
**Pattern**: A signed message authorizes a value transfer or approval, but the recipient, target contract, or beneficiary is not in the signed payload. An intermediary substitutes the recipient to redirect value.
**Methodology**: For every signed operation that transfers or approves value, verify the recipient/spender/target address is part of the digest. For meta-transactions and account abstraction, verify the target contract is signed. For multi-hop operations, verify the FINAL recipient (not just the next hop) is bound. For `msg.sender`-dependent paths, verify the relayer cannot substitute themselves for the signer.
**Red flags**:
- `permit` with unsigned `spender`
- Meta-tx where `to` in the outer call is chosen by the relayer
- Bridge payload where `destination` is signed but `recipient` is not
- Account abstraction `beneficiary` in `handleOps` substituted by bundler

### SIG-4: Nonce & Replay Prevention Atomicity
**Provenance**: signed-data-completeness.md §4 (maps to original SEM-14 variant tracking nonce semantics)
**Pattern**: Signed messages can be replayed because the nonce is missing, not part of the digest, incremented non-atomically, or scoped too broadly (2D nonce key reused across operation types).
**Methodology**: Verify every signature mechanism has a nonce or unique identifier. Verify the nonce is part of the signed bytes (not just compared separately). Verify the nonce increments in the same transaction as verification (no gap). For 2D nonce schemes, verify keys cannot collide across operation types. For deadline-only replay protection, verify the window is tight AND the operation is idempotent.
**Red flags**:
- Nonce checked but not hashed into the digest
- `nonce++` in a different transaction than `ecrecover`
- 2D nonce key that is meaningful for op A but arbitrary for op B
- Batch operation with one nonce gating N independent items

## checklists/solana.md

# Solana / Anchor Checklist

> Profile: solana
> Checks: 12
> Source: ported from Drozer-v2 anchor-invariants.md (provenance cited per check)

## Methodology

Solana programs fail in ways Ethereum auditors do not always anticipate: account substitution (type cosplay), missing signer checks, PDA seed collisions, arbitrary CPI, and account-close-then-revive attacks. Apply adversarial thinking at the instruction level: for every `#[derive(Accounts)]` struct, ask what an attacker can substitute, what the Anchor constraints actually enforce, and what assumptions the handler makes that are not verified by those constraints. Prefer `Account<'info, T>` over `AccountInfo` / `UncheckedAccount`. Every `invoke`/`invoke_signed` is a trust boundary: verify the target program, the seeds, and the post-CPI state.

## Checks

### SOL-1: Account Ownership Integrity
**Provenance**: anchor-invariants.md SA1
**Pattern**: An `AccountInfo` or `UncheckedAccount` parameter has no explicit owner check; an attacker substitutes a fake account owned by a malicious program with identical byte layout.
**Methodology**: For every raw `AccountInfo` / `UncheckedAccount`, verify an explicit `account.owner == expected_program_id` check. Prefer typed `Account<'info, T>`. Verify every `/// CHECK:` is documented with the manual validation performed.
**Red flags**:
- `UncheckedAccount<'info>` with no owner check in the handler
- Type-cosplay: same layout, different semantics

### SOL-2: Signer Authorization
**Provenance**: anchor-invariants.md SA2
**Pattern**: Authority/admin accounts are not declared as `Signer<'info>` and the handler does not check `is_signer`, allowing unauthorized callers to invoke privileged operations.
**Methodology**: For every privileged parameter, verify it is `Signer<'info>` OR `#[account(signer)]` OR the handler checks `is_signer`. For PDA-signed CPIs, verify seeds are complete and correct. Verify no instruction can modify authority fields without the current authority signing.

### SOL-3: PDA Derivation Correctness (Canonical Bump & Seed Prefix)
**Provenance**: anchor-invariants.md SA3
**Pattern**: PDA seeds are not length-prefixed and collide (`["ab","c"] == ["a","bc"]`), or a non-canonical bump is accepted, or seeds lack user-specific data allowing cross-user PDA access.
**Methodology**: Verify `find_program_address` is used for canonical bumps. Verify stored bumps match canonical bumps. Verify seeds include user pubkey where user-specific. Verify variable-length seeds are separated.

### SOL-4: CPI Safety
**Provenance**: anchor-invariants.md SA4
**Pattern**: A cross-program invocation targets a program ID that is attacker-controlled, or `invoke_signed` seeds are manipulable, or CPI return values are ignored.
**Methodology**: Verify CPI targets are hardcoded or verified against constants. Verify signer seeds cannot be crafted to sign for unintended PDAs. Verify CPI results are unwrapped. Verify post-CPI state (token balances, account data) is checked.
**Red flags**:
- `invoke(target, ...)` where `target` is from instruction data
- CPI return value discarded

### SOL-5: Token Account Integrity
**Provenance**: anchor-invariants.md SA5
**Pattern**: An SPL token account has no mint/owner/program verification; an attacker passes a worthless token account and receives valuable tokens.
**Methodology**: For every TokenAccount, verify mint matches expected mint, owner matches expected owner, and the program ID is the SPL Token program. For ATAs, verify derivation.
**Red flags**:
- `TokenAccount<'info>` with no constraint on mint or owner
- Generic `AccountInfo` used as a token account with no checks

### SOL-6: Account Closure Safety
**Provenance**: anchor-invariants.md SA6
**Pattern**: An account is closed (lamports drained) but data is not zeroed; attackers revive and re-read stale data, or re-init with attacker authority.
**Methodology**: Verify account data is zeroed before lamports are moved. Verify the discriminator is cleared. Verify same-transaction revival is impossible. Prefer Anchor's `close = ...` to manual drains.

### SOL-7: Initialization Idempotency
**Provenance**: anchor-invariants.md SA7
**Pattern**: A program's `initialize` can be called multiple times or races with the legitimate initialization, re-setting authority to the attacker.
**Methodology**: Prefer Anchor `init` constraint over `init_if_needed`. For manual init, verify an `is_initialized` flag. Verify init parameters cannot be changed after first init.

### SOL-8: Arithmetic Soundness
**Provenance**: anchor-invariants.md SA8
**Pattern**: Release builds without `overflow-checks` wrap silently; casts like `as u64` truncate high bits; division by zero causes a transaction DoS.
**Methodology**: Verify `overflow-checks = true` in `[profile.release]` OR all arithmetic uses `checked_*`/`saturating_*`. Verify casts are bounds-checked. Verify divisors are non-zero. Verify rounding favors the protocol.

### SOL-9: Duplicate Account Prevention
**Provenance**: anchor-invariants.md SA9
**Pattern**: An instruction takes two mutable accounts of the same type; the attacker passes the same account for both, allowing read-then-double-credit exploits.
**Methodology**: For every instruction with 2+ mutable `Account<>` of the same type, verify `require_keys_neq!` or equivalent constraint. Audit `remaining_accounts` loops for duplicate handling.
**Red flags**:
- `source: Account<Vault>, destination: Account<Vault>` with no distinctness check

### SOL-10: Rent Exemption
**Provenance**: anchor-invariants.md SA10
**Pattern**: An account is created with insufficient lamports for rent exemption and is garbage-collected, losing its data.
**Methodology**: Verify account creation uses `Rent::get()?.minimum_balance(data_len)`. Verify reallocs maintain rent exemption at the new size. Audit any direct lamport manipulation.

### SOL-11: Timestamp / Clock Safety
**Provenance**: anchor-invariants.md SA11
**Pattern**: Tight time-dependent logic is vulnerable to validator timestamp drift; slot-skipping causes unexpected gaps.
**Methodology**: Verify deadlines have tolerance windows. Prefer slot height for deterministic timing. Avoid using `unix_timestamp` for randomness or tight windows.

### SOL-12: Error Handling Completeness
**Provenance**: anchor-invariants.md SA12
**Pattern**: `unwrap()` on user input causes DoS; error paths return `Ok(())` silently after a failed check; CPI errors are caught and ignored.
**Methodology**: Audit every `unwrap()` on fallible operations. Prefer `?`. Verify no `Ok(())` is returned after a failed check. Verify CPI errors are propagated.
**Red flags**:
- `ctx.accounts.mint.decimals.unwrap()` on user input
- `if let Err(_) = cpi_call { } else { ... }` with no error propagation

## checklists/stableswap.md

# StableSwap Checklist

> Profile: stableswap
> Checks: 5
> Source: drozer-lite v0.4.2 — derived from Curve StableSwap implementation patterns and real-audit findings on StableSwap forks. These checks apply to any protocol implementing the Curve StableSwap invariant (An∑xi + D = ADⁿ + Dⁿ⁺¹/(nⁿ∏xi)).

## Methodology

StableSwap pools maintain a hybrid invariant between constant-sum (x+y=k) and constant-product (xy=k), controlled by an amplification parameter A. Correct implementations must: (a) normalize all token amounts to a common decimal base before computing the invariant D, (b) include ALL pool tokens in the invariant computation (not just the swap pair), (c) charge fees on imbalanced deposits proportional to the skew introduced, (d) handle Newton-Raphson non-convergence as an error rather than returning a wrong result, and (e) allow the amplification parameter to be adjusted over time to respond to market conditions. Compare the implementation against the Curve reference line by line, paying particular attention to how many tokens participate in D/y computation and whether decimal scaling is consistent across swap and LP paths.

## Detection Keywords

Auto-load this profile when **3 or more** of these keywords match (case-insensitive):

`StableSwap`, `amp`, `amplification`, `compute_d`, `compute_y`, `newton`, `invariant.*D`, `stableswap_y`, `n_coins.*ann`, `D_prod`, `amp_factor`

## Checks

### SS-1: Decimal Normalization Inconsistency Between Swap and LP Paths
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the swap path normalizes token amounts to a common decimal base before computing the StableSwap invariant, but the LP minting path uses raw (unnormalized) amounts, producing a different D value and incorrect share calculations for tokens with different decimals.
**Pattern**: A StableSwap implementation has two code paths that compute the invariant D: one for swaps (which normalizes via `decimal_with_precision` or rate multipliers) and one for LP minting/withdrawal (which sums raw amounts). When tokens have different decimals (e.g., 6 vs 18), the LP path computes a D that is dominated by the higher-decimal token, granting disproportionate shares to depositors of that token.
**Methodology**:
1. Identify every call site that computes the StableSwap invariant D.
2. For each call site, check whether token amounts are normalized to a common decimal base BEFORE being passed to the D computation.
3. Compare the normalization logic between the swap path and the LP mint path. If they differ, flag.
4. Test with two tokens of different decimals (e.g., 6 and 18): deposit equal-value amounts via both paths and compare the D values.
**Red flags**:
- Swap path: `offer_pool = Decimal256::decimal_with_precision(amount, precision)` — normalized
- LP path: `sum_x = deposits.iter().fold(zero, |acc, x| acc + x.amount)` — raw amounts, no normalization
- D computation for LP uses raw `Uint128` amounts while swap uses `Decimal256` with precision
- Two tokens with 6 and 18 decimals: depositing 1e6 USDC and 1e18 DAI produces wildly different D vs depositing 1e18 USDC and 1e6 DAI — but both should be equivalent in value

### SS-2: Multi-Token Invariant Uses Only Swap Pair (Disjoint Computation)
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a StableSwap pool with 3+ tokens computes the invariant D and the swap output y using only the offer and ask token balances, ignoring the other pool tokens. The Curve invariant requires ALL token balances to compute D correctly.
**Pattern**: The StableSwap invariant D is defined over ALL N tokens: `An∑(all xi) + D = ADⁿ + Dⁿ⁺¹/(nⁿ∏(all xi))`. When computing a swap between token A and token B in a 3-token pool, the implementation passes only `(offer_pool, ask_pool)` to the D/y computation, but uses `n_coins = 3`. This produces an incorrect D because the sum and product only include 2 of the 3 token balances, while the exponent uses N=3. Swaps between different pairs in the same pool preserve different (incorrect) invariants, creating arbitrage opportunities.
**Methodology**:
1. For every StableSwap swap computation, check how many token balances are passed to the D/y computation function.
2. If the function receives only the offer and ask balances (2 tokens) but `n_coins` reflects the actual pool size (3+), flag as HIGH.
3. Compare against Curve reference: the `get_y` function iterates over ALL pool balances except the target token.
4. Test: in a 3-token pool, check if A-B swaps produce different slippage than B-C swaps with identical pool composition — they should be equivalent in a correct implementation.
**Red flags**:
- `compute_swap(n_coins=3, offer_pool, ask_pool, ...)` but D is computed from only `offer_pool + ask_pool`
- `calculate_stableswap_d(offer_pool, ask_pool)` — sum uses 2 values but `ann = amp * n_coins` uses 3
- The `pool_sum` or `sum_pools` variable only includes 2 token balances
- D/y functions accept only 2 pool amounts as parameters despite being called for pools with 3+ tokens
- Different token-pair swaps in the same pool produce inconsistent pricing

### SS-3: Missing Imbalanced Deposit Fee
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a StableSwap pool allows liquidity deposits at any ratio without charging a fee proportional to the imbalance (skew) introduced. In Curve's implementation, depositing in a ratio that deviates from the pool's current ratio incurs a fee equal to the swap fee on the "difference" between the ideal and actual deposit. Without this fee, users can skew the pool for free, manipulating the price at minimal cost.
**Pattern**: The LP minting function computes shares as `total_supply * (D1 - D0) / D0` where D1 includes the new deposits and D0 is the pre-deposit invariant. Curve additionally computes per-token ideal balances (`ideal_balance = D1 * old_balance / D0`) and charges a fee on the `|ideal_balance - new_balance|` for each token. This fee is missing in the implementation — users can deposit one-sided or skewed liquidity without penalty.
**Methodology**:
1. In the LP minting function for StableSwap, check whether any fee is charged based on the imbalance of the deposit.
2. If the only calculation is `shares = total_supply * (D1 - D0) / D0` with no per-token fee computation, flag.
3. Test: deposit a large one-sided amount (e.g., 2x of token A, 0 of token B). Compare the cost (shares received / value deposited) against a balanced deposit. In a correct implementation, the one-sided deposit should receive fewer shares due to the imbalance fee.
**Red flags**:
- `compute_lp_mint_amount = total_supply * (D1 - D0) / D0` — no per-token fee calculation
- No `ideal_balance`, `difference`, or `dynamic_fee` computation anywhere in the LP mint path
- One-sided deposit of 2x tokenA costs less than 0.5% in slippage — should cost at least the swap fee (e.g., 3-5%) on the skewed portion
- A user can skew the pool ratio dramatically with a deposit, then withdraw balanced, capturing value from other LPs

### SS-4: Newton-Raphson Non-Convergence Returns Result Instead of Error
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the Newton-Raphson iterative solver for the StableSwap invariant D or pool balance y returns the last computed value even when the iteration limit is reached without convergence. A non-converged result is mathematically incorrect and produces wrong swap prices.
**Pattern**: The D or y computation uses a loop with a fixed iteration cap (e.g., 32, 256, 1000). On each iteration, it checks if `|current - previous| <= 1`. If the loop completes without converging, the function returns the last value instead of an error. In a correct implementation (Curve reference), non-convergence raises an error and the swap/deposit fails — only withdrawals remain functional, protecting LPs.
**Methodology**:
1. For every Newton-Raphson loop, check what happens after the loop ends WITHOUT convergence (i.e., the break condition was never met).
2. If the function returns `Some(last_value)` or `Ok(last_value)` after the loop, flag. It should return `None`, `Err(ConvergeError)`, or equivalent.
3. Check whether there are two D computation functions with different iteration caps (e.g., 32 for swaps, 256 for LP) — inconsistency flag per MATH-4.
4. Test with extremely imbalanced pools where convergence is slow — the function will return a wrong value instead of failing.
**Red flags**:
- Loop `for _ in 0..N { ... if converged { break; } }` followed by `Some(d)` outside the loop — always returns even if not converged
- No `return` or early exit on convergence — the `break` exits the loop and falls through to a successful return
- Correct pattern: convergence should `return Some(d)` inside the loop; after the loop, return `None` or `Err`
- Curve reference: `raise` after the loop (Python); the function never returns normally without convergence

### SS-5: Static Amplification Parameter (No Ramping Mechanism)
**Provenance**: drozer-lite v0.4.2 — class-of-bug: the StableSwap amplification parameter A is set once at pool creation and cannot be modified afterward. Curve's implementation includes a time-weighted ramping mechanism (`ramp_A` / `stop_ramp_A`) that allows the protocol to adjust A over time in response to market conditions. Without ramping, pools cannot adapt to depegging events and can leak value.
**Pattern**: The amplification parameter is stored as a static field in the pool configuration (e.g., `PoolType::StableSwap { amp: u64 }`). No `update_amp`, `ramp_A`, or similar function exists to modify it post-creation. During normal conditions, the pool works fine. During a depegging event (one stablecoin loses its peg), a high A value keeps the price artificially stable, allowing holders of the depegged asset to swap at near-1:1 rates and drain the pool of the healthy asset.
**Methodology**:
1. Check whether the amplification parameter can be modified after pool creation. Search for `ramp`, `update_amp`, `set_amp`, `modify_amp` in the execute message enum and handler.
2. If no modification mechanism exists, flag as MEDIUM — the pool cannot adapt to market conditions.
3. Check whether pool parameters are generally immutable post-creation (intentional design) or whether other parameters can be updated.
4. Assess the severity: if the DEX is designed for stablecoin pairs only, this is higher severity (depegging is the primary risk). If it supports volatile pairs via StableSwap (unusual), severity is lower.
**Red flags**:
- `PoolType::StableSwap { amp: u64 }` — static field, no update path
- No `ExecuteMsg::RampAmp` or `ExecuteMsg::UpdatePoolParams` in the message enum
- Pool fees can be set at creation but amp cannot be adjusted — asymmetric mutability
- Documentation mentions "stable assets" or "pegged assets" but no depeg protection mechanism
- Contrast with Curve: `ramp_A(future_A, future_time)` with `MIN_RAMP_TIME` safety constraint

## checklists/universal.md

# Universal Checklist

> Profile: universal
> Checks: 110
> Source: ported from Drozer-v2 analyses (provenance cited per check). UNI-96..98 added in v0.3.1, UNI-99..106 in v0.4.1, UNI-107..110 in v0.4.2. All checks describe generic class-of-bug patterns.

## Table of Contents

- UNI-1: Missing / Incorrect Access Control
- UNI-2: State Machine / Lifecycle Bypass
- UNI-3: Classic Reentrancy
- UNI-4: Cross-Function / Read-Only Reentrancy
- UNI-5: Missing Zero-Address / Zero-Amount Checks
- UNI-6: Integer Overflow / Underflow in Unchecked Blocks
- UNI-7: Unchecked External Call Return Values
- UNI-8: Controlled Delegatecall
- UNI-9: Upgrade / Initialization Safety
- UNI-10: Timestamp Dependence
- UNI-11: Missing Event Emission
- UNI-12: Unbounded Loops
- UNI-13: Unvalidated `from` in transferFrom
- UNI-14: Loop External-Call Fragility
- UNI-15: Weird-Token Incompatibility
- UNI-16: Array Boundary Edge Cases
- UNI-17: Post-Commitment State Mutation
- UNI-18: Uninitialized-State Guard Bypass
- UNI-19..28: Temporal, Cross-Environment, Derived-Value, Work-Reward, Identifier, Spec, Error-State, Normalization, Format, Representation
- UNI-29..34: Aggregate Removal, Stale-Snapshot, Prerequisite Update, Last-Element, Permissionless Privilege, Token Compatibility
- UNI-35..38: Role Separation, Cross-Contract ACL, Timelock Scope, Emergency Exit
- UNI-39..47: Monotonic State, Past-Epoch, Cooldown, Deadline, Sequence, Bounded Iteration, Array Growth, Loop External, Bounded Cleanup
- UNI-48..54: Rounding Direction, Donation Corruption, First-Depositor, Atomic Transfer, Solvency, No Free Extraction, Fee Bounds
- UNI-55..60: Storage Slot, Init Completeness, encodePacked, Signed Data, Nonce/Replay, Taint Boundary
- UNI-61..65: Oracle Staleness, Manipulation-Resistant, Graceful Degradation, Multi-Source, Feed Consistency
- UNI-66..71: Parameter Scope, Retroactive Calc, Locked-Position, Timing-Adversary, Cross-Function Consistency, Boundary Safety
- UNI-72..80: Privilege Enumeration, Operation Blocking, Irreversible Admin, Ownership Two-Step, Router Permissionless, Approval Persistence, Permit Frontrun, Router Identity, Router Residual
- UNI-81..98: Compound-Fork, Supply-Cap DoS, Redemption DoS, Empty-Market, External Reward, Partial Redemption, Bad-Debt, Skip/Disable, FCFS, Negative-Yield, Yield-Leakage, Emergency Input, Before/After Balance, Irreversible Config, ERC-165, Precision Loss, Auto-Route Balance
- UNI-99..106: Approval Persistence After Reversal, Asymmetric Settlement, Destructive Without Obligation, Heterogeneous Collection, Payment-Gated Transfer, Numeric Type Width, Stored Constraint Unenforced, Listing-Gate Bypass
- UNI-107..110: Nested Loop Gas DoS, Temporal Past-Value, Self-Call Identity Confusion, Permissionless Fee Bypass

## Methodology

Apply adversarially. For every storage variable, every external-facing function, and every privileged action, ask: who can call it, what state it reads, what state it writes, and what assumptions the surrounding code makes about that state. Trace actual execution paths rather than documented intent. When an invariant is stated in docs, attempt to construct a sequence of calls that breaks it. Prefer evidence from code traces (E3) over pattern matches (E2). Treat every `unchecked`, every external call, every admin setter, and every boundary value (0, 1, type(X).max, array.length == 0/1) as a suspect until proven safe.

## Checks

### UNI-1: Missing / Incorrect Access Control
**Provenance**: universal-invariants.md U1 + invariant-templates.md AC-1
**Pattern**: State-changing functions lack modifiers, use the wrong modifier, or rely on a single role that can be self-granted or front-run during initialization.
**Methodology**: Enumerate every `external`/`public` non-view function. For each, record the authorization path (modifier, inline `require`, or none). Cross-check that admin/owner setters are reachable only by the intended role. Look for `initialize()` without `initializer` guard, role granters that let a role add itself, and emergency functions with weaker protection than the normal path.
**Red flags**:
- Missing `onlyOwner`/`onlyRole(...)`/`onlyGovernor` on state-changing function
- `initialize()` callable multiple times or front-runnable
- Role-admin == role-holder allowing self-grant
- `_setupRole` after deployment without access check

### UNI-2: State Machine / Lifecycle Bypass
**Provenance**: universal-invariants.md U2 + U15 + U16
**Pattern**: Functions operating on lifecycle entities (proposal, position, order) do not check the entity's current state, allowing operations on executed, cancelled, expired, or uninitialized entities.
**Methodology**: For each entity with a lifecycle (created→populated→finalized→consumed), map every function that accepts its ID. Verify each call site either reads a `status` field or asserts preconditions. Flag any function that mutates state without validating the lifecycle position. Check zero/default values do not satisfy "initialized" guards.
**Red flags**:
- `fund()`/`deposit()`/`transfer()` callable on executed proposals
- No `require(state == Active)` on lifecycle-sensitive functions
- Sentinel field defaults (`timestamp == 0`) satisfying `block.timestamp - stored > PERIOD`
- Re-initialization overwriting finalized data
- Temporal guard allows editing AFTER a period expires but BEFORE all settlements from that period are finalized — parameter changes (denomination, rate, price) corrupt pending settlements
- Edit guard checks `expiry < current_time` but not `all_settlements_complete` — the entity appears editable because the period ended, but unsettled obligations still reference the old parameters

### UNI-3: Classic Reentrancy
**Provenance**: universal-invariants.md U4 + invariant-templates.md RE-1
**Pattern**: External call occurs before critical state updates (violates Checks-Effects-Interactions).
**Methodology**: Search for every external call (`.call`, `.transfer`, token transfer, user-supplied target). For each, check what state is read before and written after. If any write that affects subsequent checks happens after the call, the function is re-entrancy unsafe.
**Red flags**:
- Balance/ownership update AFTER `.call` or token transfer
- Absence of `nonReentrant` on payable or token-moving function
- ERC777/ERC721 `onReceived` callbacks on an otherwise trusted path

### UNI-4: Cross-Function / Read-Only Reentrancy
**Provenance**: universal-invariants.md U4 + invariant-templates.md RE-3/RE-4
**Pattern**: Reentrancy guard on function A does not protect function B that reads the same state during A's external call, or a view function returns stale state during another function's external call window.
**Methodology**: Group functions that share state. For each group, check whether a single guard protects the entire group or only individual functions. Identify view functions used for pricing/accounting that are callable during another function's mid-execution window.
**Red flags**:
- `nonReentrant` on `deposit()` but not on `getPrice()` reading the same reserves
- Oracle/price view functions not protected by the same guard

### UNI-5: Missing Zero-Address / Zero-Amount Checks
**Provenance**: universal-invariants.md U5 + invariant-templates.md DF-1
**Pattern**: External parameters are not validated; zero addresses burn tokens, zero amounts skip logic, empty arrays cause silent success.
**Methodology**: For every parameter of every external function, determine whether a zero value would produce undesired behaviour. Check token recipients, approval spenders, configuration setters, and array inputs.
**Red flags**:
- `transfer(to, amount)` with no `require(to != address(0))`
- Setter writes `address(0)` causing irrecoverable state
- `amount == 0` paths skipping fee calculation but still emitting success events

### UNI-6: Integer Overflow / Underflow in Unchecked Blocks
**Provenance**: universal-invariants.md U5
**Pattern**: Arithmetic inside `unchecked { ... }` or using low-level operations wraps around on large inputs.
**Methodology**: Grep `unchecked` and `assembly`. For each block, determine the maximum value each operand can reach across all call sites. Confirm the developer's implicit bound holds.
**Red flags**:
- `unchecked { totalSupply += amount }` without cap
- Counter increment that can flip over many transactions
- Cast `uint256 -> uint128` without bounds check

### UNI-7: Unchecked External Call Return Values
**Provenance**: universal-invariants.md U6 + invariant-templates.md DF-4
**Pattern**: Low-level calls, `transfer`, or non-standard ERC20 tokens return failure silently without reverting.
**Methodology**: For each external call, verify the return value is checked or `SafeERC20` wrappers are used. Treat non-reverting failures as fund-loss bugs.
**Red flags**:
- `token.transfer(...)` without `require` or SafeERC20
- `(bool success, ) = target.call(...)` with unused `success`
- `onERC721Received` never validated on safeTransfer paths

### UNI-8: Controlled Delegatecall
**Provenance**: universal-invariants.md U6 + U8
**Pattern**: `delegatecall` to a user-influenced or loosely-validated target gives an attacker full control of the calling contract's storage.
**Methodology**: Enumerate every `delegatecall`. Trace the target address parameter back to its source. If it can be user-influenced (even indirectly through a config setter) flag immediately.
**Red flags**:
- `delegatecall(msg.data, target)` where target is a setter-modifiable address
- Proxy implementation slot writable by a non-timelocked admin

### UNI-9: Upgrade / Initialization Safety
**Provenance**: universal-invariants.md U8 + invariant-templates.md ST-4
**Pattern**: Upgradeable contracts can be re-initialized, have storage collisions, or allow upgrades without timelock.
**Methodology**: Check for `initializer` modifier, `_disableInitializers()` in constructors, `__gap` storage reserves, and storage-layout compatibility between versions. Verify `upgradeTo` is behind access control AND timelock.
**Red flags**:
- Implementation constructor missing `_disableInitializers`
- `initialize()` without `initializer` modifier
- `__gap` shrunk between versions
- Upgrader is an EOA

### UNI-10: Timestamp Dependence for Security Decisions
**Provenance**: universal-invariants.md U7
**Pattern**: `block.timestamp` used as randomness seed or for tight windows that can be manipulated by validators.
**Methodology**: Grep `block.timestamp` and `now`. For each usage, determine the tolerance to a ~15-second shift. Randomness derived from timestamps is always broken.
**Red flags**:
- `uint256 seed = block.timestamp`
- Deadlines with <1-minute precision enforced for value transfers

### UNI-11: Missing Event Emission on State Changes
**Provenance**: universal-invariants.md U9
**Pattern**: Admin setters, role changes, or critical state transitions do not emit events, preventing off-chain monitoring.
**Methodology**: Build a setter list. For each setter, verify an event is emitted with old and new values. Missing events on role grants, fee changes, or asset onboarding are systemic findings.
**Red flags**:
- `setFee(newFee)` without `FeeUpdated(oldFee, newFee)` event
- Role grant without corresponding event
- Pause/unpause silent

### UNI-12: Unbounded Loops
**Provenance**: universal-invariants.md U10 + invariant-templates.md GR-1
**Pattern**: Loops over arrays that grow with user actions can be gas-bombed to block a function or the entire protocol.
**Methodology**: Identify every loop. For each, determine whether its upper bound is hardcoded, capped, or unbounded. Unbounded loops over user-addable entries are DoS vectors.
**Red flags**:
- `for (uint i; i < users.length; i++)` with permissionless `users.push`
- External calls inside loops without try-catch

### UNI-13: Unvalidated `from` in `transferFrom` (Approval Drain)
**Provenance**: universal-invariants.md U11
**Pattern**: Permissionless function calls `token.transferFrom(from, to, amount)` where `from` is attacker-supplied, draining any user who approved the contract.
**Methodology**: For every `transferFrom` / `safeTransferFrom`, trace who sets `from`. If it comes from calldata and the caller is not validated as `from` or an approved spender, flag as HIGH.
**Red flags**:
- Permissionless external function with `transferFrom(userAddr, ...)` using user's existing allowance
- Permit flows where the permit signer != the operation beneficiary

### UNI-14: Loop External-Call Fragility
**Provenance**: universal-invariants.md U12
**Pattern**: A loop that makes external calls reverts entirely if one iteration fails, blocking all subsequent operations.
**Methodology**: For every loop with external calls, look for try-catch, skip-and-continue, or partial execution patterns. A single paused dependency must not block all withdrawals.
**Red flags**:
- No `try { ... } catch` around strategy/pool external calls in loops
- Attacker-deployable contract that always reverts on transfer can block batch processing

### UNI-15: Weird-Token Incompatibility
**Provenance**: universal-invariants.md U13
**Pattern**: Contract assumes standard ERC20 behavior but supports tokens that charge fees on transfer, rebase, require approve-to-zero, or revert on zero transfer.
**Methodology**: Identify all tokens the contract can interact with (from deployment config, registry, or user-supplied). For each, check fee-on-transfer handling (balance-before/after), USDT approval reset, DAI non-standard permit, decimals assumption, and rebasing impact.
**Red flags**:
- `amount == transferred` assumption after `transferFrom`
- `approve(spender, newAmount)` without zero reset
- Hardcoded 18-decimal math for arbitrary tokens

### UNI-16: Array Boundary Edge Cases
**Provenance**: universal-invariants.md U14
**Pattern**: Swap-and-pop removal, index-based access, or loops fail at `length == 1`, empty arrays, or duplicates.
**Methodology**: For each swap-and-pop, test `last-element removal` explicitly. For each indexed access, verify bounds checks. For each array-modifying function, test empty and single-element cases.
**Red flags**:
- `array[index] = array[array.length - 1]; array.pop()` without zeroing the moved element's companion mapping
- No `index < array.length` check before use

### UNI-17: Post-Commitment State Mutation
**Provenance**: universal-invariants.md U15
**Pattern**: Once state has been finalized, committed, or passed a validation window, it can still be mutated without re-validation.
**Methodology**: For every lifecycle entity, verify each phase is one-way. `initialize` must check for existing state. `consume`/`claim` must re-validate state they read. Mutable fields must not change after proofs are computed.
**Red flags**:
- `initialize()` called twice on same entity overwriting finalized data
- Mutable `partOffset` updated after merkle root computed
- Re-initialization corrupting shared state (registries, oracles)

### UNI-18: Uninitialized-State Guard Bypass
**Provenance**: universal-invariants.md U16
**Pattern**: Zero/default values satisfy guards that assume initialized state (e.g., `block.timestamp - 0 > PERIOD` is always true).
**Methodology**: For every timestamp/existence comparison, ask what happens when the stored field is zero. For every multi-step init, ensure finalize validates ALL intermediate steps completed.
**Red flags**:
- `if (lastClaim != 0)` used to guard distribution but another path skips setting it
- Boolean defaulting to `false` where `false` means both "unvalidated" and "failed"
- Storage map/mapping read for a non-existent key returns zero/default, and the zero value is used downstream without an existence check — e.g., `map.read(user_supplied_key)` returns 0 for an unregistered key, the code hashes 0 with other data, and the hash is used for signature verification. The attacker signs over the zero value to bypass authorization for unregistered keys.
- A registry lookup (oracle registry, whitelist, role mapping) returns a default value for non-members, but the calling code does not assert the returned value is non-zero/non-default before proceeding. The non-member passes the check by operating on the default value.
- `let data = storage_map.entry(key).read(); /* data is 0 for missing key */ validate_signature(data, sig);` — the signature is valid over 0, which is a predictable constant, so any key pair can produce a valid signature

### UNI-19: Temporal Constraint Incompatibility
**Provenance**: universal-invariants.md U17
**Pattern**: A time-bounded operation's guaranteed window is insufficient for the worst-case execution of all required sub-operations.
**Methodology**: For each deadline mechanism, list all operations that must complete within the window. Sum minimum times (including challenge periods). Verify total fits.
**Red flags**:
- 3-hour extension granted but inner operation needs 1-day challenge period
- Nested timers where outer < inner + overhead

### UNI-20: Cross-Environment Resource Parity
**Provenance**: universal-invariants.md U18
**Pattern**: Operation executed in environment A must be reproducible in environment B but B has tighter resource limits (gas, calldata, memory).
**Methodology**: For every cross-environment proof/verification, compare the execution cost in each environment. Account for EIP-150 63/64 forwarding and verifier overhead.
**Red flags**:
- L2 operation that must be re-executed on L1 without gas budget analysis
- Dynamic-cost precompiles unchecked against target env block limit

### UNI-21: Derived-Value Domain Bounds
**Provenance**: universal-invariants.md U19
**Pattern**: Computed values are not capped at their logical maximum before being passed to consuming systems.
**Methodology**: For each derivation (index, position, hash), check the output range matches the consumer's valid input range.
**Red flags**:
- `computedBlock = start + traceIndex + 1` uncapped at `claimedBlock`
- Mapping from large index space to smaller domain without range enforcement

### UNI-22: Work-Reward Decoupling
**Provenance**: universal-invariants.md U20
**Pattern**: Permissionless reward-distribution attributes the reward to `msg.sender` instead of the worker, allowing front-runners to steal.
**Methodology**: For every permissionless claim/distribute, trace who pays cost (gas, bond) vs who receives reward. Mismatch = bug.
**Red flags**:
- `step()` function pays bond to `msg.sender` while evidence was provided by a different party
- Two-step reward where step 2 is permissionless and front-runnable

### UNI-23: Identifier Namespace Collisions
**Provenance**: universal-invariants.md U21
**Pattern**: Unique identifiers can be consumed prematurely, pre-populated for non-existent entities, or become invalid after reordering.
**Methodology**: For each unique ID scheme (nonce, hash, UUID), check whether the ID can be blocked, pre-populated, or invalidated by state changes. Prefer `create2`/salted over nonce-derived for cross-tx safety.
**Red flags**:
- Permissionless data population keyed by future entity address
- Index-based reference breaking after swap-and-pop

### UNI-24: Spec Exhaustive Compliance
**Provenance**: universal-invariants.md U22
**Pattern**: Code implementing a formal spec (instruction set, standard, formula) only handles common cases; edge cases (shift masking, overflow traps, alignment) diverge from the spec.
**Methodology**: For each opcode/rule/formula, compare on-chain implementation against the reference, line by line. Check input masking, overflow behaviour, and undefined-behaviour handling.
**Red flags**:
- Shift amount not masked to 5/6 bits per spec
- Silent wrap where spec requires trap
- Type width mismatch losing high bits

### UNI-25: Error-State Asymmetry in Adversarial Protocols
**Provenance**: universal-invariants.md U23
**Pattern**: In dispute/challenge systems, an error state benefits one party over the other.
**Methodology**: Enumerate every error/revert in a dispute flow. Ask which party benefits. If a panic makes claims unchallengeable, the panic-trigger wins.
**Red flags**:
- `require` in challenge path that only the challenger can hit
- Status value that is simultaneously unattackable and undefendable

### UNI-26: Multi-Step Normalization Ordering
**Provenance**: universal-invariants.md U24
**Pattern**: Non-commutative adjustments (normalize, halve, round) are applied in different orders across branches.
**Methodology**: For each multi-step adjustment, list the steps in order. For each adjacent pair, ask whether swapping changes the result. Verify all branches use the same order.
**Red flags**:
- Branch A: halve-then-adjust; Branch B: adjust-then-halve
- Rounding before scaling losing precision

### UNI-27: Format/Precision Selection Consistency
**Provenance**: universal-invariants.md U25
**Pattern**: Systems with multiple formats (small/large, low/high precision) apply different selection rules across paths.
**Methodology**: Build a format selection table for each path (encode, arithmetic output, decode, conversion). Compare rule sets. Construct boundary values that expose the asymmetry.
**Red flags**:
- Encoding uses more rules than arithmetic output
- `encode(decode(encode(x))) != encode(x)` at boundary

### UNI-28: Representation Gap Integrity
**Provenance**: universal-invariants.md U26
**Pattern**: Values "in the gap" (too precise for small format, not qualifying for large) are silently truncated.
**Methodology**: Identify the gap range using the format selector. Check whether precision loss in the gap stays within stated tolerance and whether it compounds through subsequent calculations.
**Red flags**:
- Format selector uses exponent-only when precision depends on digit count
- Gap value multiplied by gap value

### UNI-29: Aggregate State Removal Consistency
**Provenance**: universal-invariants.md U27
**Pattern**: When an element is removed from a set with aggregate/summary variables, some aggregates are updated and others are not.
**Methodology**: For every aggregate (totalSupply, totalWeight, totalBias, changesSum), verify it is decremented on removal. For time-weighted aggregates, verify the slope is corrected too.
**Red flags**:
- Removal updates `bias` but not `slope`
- Two-step removal where users never complete step 2
- Zero-aggregate edge case causing division by zero

### UNI-30: Stale-Snapshot After Collection Mutation
**Provenance**: universal-invariants.md U28
**Pattern**: A function copies a storage array to memory, mutates the storage array (eviction, swap-pop), then continues using stale memory indices.
**Methodology**: Grep functions that copy storage arrays to memory then call a mutating function. Verify subsequent code re-reads from storage.
**Red flags**:
- `Foo[] memory cache = storageArray; evict(); cache[i]` (stale)
- Swap-and-pop indices reused after reorder

### UNI-31: Prerequisite Update Before Participant Change
**Provenance**: universal-invariants.md U29
**Pattern**: Adding/removing a participant (staker, voter, LP) without first checkpointing accumulated state dilutes existing participants.
**Methodology**: For each `join`/`leave`/`add`/`remove`, verify the checkpoint/accrue function is called first. Verify the accrual is enforced internally, not by external caller convention.
**Red flags**:
- `stake()` pushes to participants array without calling `updateReward()` first
- `retain()` reads weights without calling checkpoint

### UNI-32: Last-Element Array+Mapping Removal
**Provenance**: universal-invariants.md U30
**Pattern**: Swap-and-pop with companion mapping leaves stale mapping entries when removing the last element (self-swap re-assigns it).
**Methodology**: Trace every swap-and-pop that has a companion `mapIds` or index mapping. Test the case where removed index equals the last index.
**Red flags**:
- Map zeroed AFTER swap (self-swap overwrites with stale)
- Existence check `mapIds[h] != 0` returning true for removed elements

### UNI-33: Permissionless Function Privilege Boundary
**Provenance**: universal-invariants.md U31
**Pattern**: A permissionless function accepts a `target` parameter that can be a privileged address with special semantics in another function, bypassing intended behavior.
**Methodology**: Enumerate privileged addresses (retainer, treasury, fee collector). For each permissionless claim/distribute with a `target`, verify it rejects privileged targets.
**Red flags**:
- `distribute(to)` that can be called with `to = treasury` bypassing `retain()` semantics

### UNI-34: Declared Token Compatibility vs. Code
**Provenance**: universal-invariants.md U32
**Pattern**: README/spec declares support for fee-on-transfer, rebasing, blocklist, or upgradeable tokens but code does not actually handle them.
**Methodology**: Read docs for declared compatibility. Compare against code handling of balance-before/after patterns, blocklist reverts on reward paths, and ERC721 safeTransfer on contracts without `onERC721Received`.
**Red flags**:
- Docs say "supports USDT" but code assumes `amount == received`
- Blocklisted fee collector bricks all unstakes

### UNI-35: Role Separation & No Self-Grant
**Provenance**: invariant-templates.md AC-1, AC-2
**Pattern**: A role can grant itself higher privileges, or a single role gates multiple unrelated powers.
**Methodology**: Build a role-capability matrix. Verify every role granter is strictly higher-privilege than the grantee. Check that emergency roles cannot unilaterally elevate.
**Red flags**:
- `RoleA.admin == RoleA`
- Single `onlyOwner` gating both parameter setting and fund movement

### UNI-36: Cross-Contract Access Control Consistency
**Provenance**: invariant-templates.md AC-3 + governance-centralization.md §2
**Pattern**: Contract A restricts function F behind role R, but contract B (caller) has no such restriction, creating a permissionless back-door.
**Methodology**: For every cross-contract call, verify the caller enforces the same or stronger restriction as the target. Flag permissionless wrappers around permissioned functions.
**Red flags**:
- `adapter.split()` public while `core.split()` requires SPLIT_ROLE

### UNI-37: Timelock Scope for Parameter Changes
**Provenance**: invariant-templates.md AC-4 + governance-centralization.md §6
**Pattern**: Parameter changes that affect accounting or user funds can be applied instantly by an EOA admin.
**Methodology**: For every parameter setter that feeds into accounting, verify it is behind a timelock. Setters protected only by `onlyOwner` are instant-rug vectors.
**Red flags**:
- `setFee`, `setRate`, `setOracle` instant with no delay
- Owner is an EOA with no multisig requirement

### UNI-38: Emergency Exit Guarantees
**Provenance**: invariant-templates.md AC-5
**Pattern**: When paused/frozen, users cannot withdraw their own funds.
**Methodology**: Check pause mechanics. Confirm at least one exit path remains available (possibly with penalty) in every paused state.
**Red flags**:
- `whenNotPaused` on `withdraw()` with no alternative
- Liquidation/forced-close functions gated by pause — when the protocol is paused, insolvent positions cannot be liquidated, allowing bad debt to accumulate. Risk management functions (liquidation, deleverage) should remain operational during pause
- Deposit cancellation gated by pause — users who deposited before the pause cannot recover their funds

### UNI-39: Monotonic State Progression
**Provenance**: invariant-templates.md TL-1
**Pattern**: Phased state (epochs, rounds) regresses to a previous phase under some path.
**Methodology**: For each phase counter, identify every write. Confirm writes are monotonic increments.
**Red flags**:
- `currentEpoch` writable to arbitrary value
- Timestamp-based epoch boundary that resets on pause/unpause

### UNI-40: Past-Epoch Immutability
**Provenance**: invariant-templates.md TL-2
**Pattern**: Data from a completed epoch can still be modified after the next epoch starts.
**Methodology**: For each epoch-keyed mapping, identify setters. Verify they revert once the epoch is past.
**Red flags**:
- `setEpochReward(epochId, amount)` with no completion check

### UNI-41: Cooldown Bypass
**Provenance**: invariant-templates.md TL-3
**Pattern**: Multiple code paths, transferring staked tokens, or restaking can reset or skip a cooldown.
**Methodology**: For each cooldown, enumerate all paths that read it. Check whether any bypass exists (token transfer, partial restake, emergency withdraw).
**Red flags**:
- `transferFrom` of staked token shifts cooldown to attacker
- Emergency withdraw without equivalent cooldown

### UNI-42: Deadline Validity
**Provenance**: invariant-templates.md TL-4
**Pattern**: Stale transactions (past deadline) can still execute.
**Methodology**: Grep all operations that accept a `deadline`. Verify `require(block.timestamp <= deadline)`.
**Red flags**:
- Deadline parameter accepted but never checked
- Deadline check under a conditional that can be skipped

### UNI-43: Sequence / Step Ordering
**Provenance**: invariant-templates.md TL-5
**Pattern**: A later step in a multi-step operation can execute without the earlier step completing.
**Methodology**: For each multi-step flow, enumerate the required preconditions for each step. Verify each step re-validates its preconditions.
**Red flags**:
- `finalize()` that does not check `populate()` ran
- Step 2 reads storage set by step 1 without verifying step 1's completion

### UNI-44: Bounded Iteration
**Provenance**: invariant-templates.md GR-1
**Pattern**: Loops lack an explicit or implicit cap, enabling gas-bomb DoS.
**Methodology**: For every loop, document the upper bound. If the bound is user-controlled without cap, flag.
**Red flags**:
- `for (; i < userProvided;)` with no limit

### UNI-45: Array Growth Limits
**Provenance**: invariant-templates.md GR-2
**Pattern**: A user-pushable array has no max size, allowing an attacker to fill it and brick iteration.
**Methodology**: For every dynamic array growable by external calls, check for a cap (explicit or economic).
**Red flags**:
- `strategies.push(...)` without `require(strategies.length < MAX)`

### UNI-46: External Calls in Loops
**Provenance**: invariant-templates.md GR-3
**Pattern**: A loop makes an unbounded number of external calls; one failing call reverts the whole batch.
**Methodology**: Cross-reference with UNI-14. Require try-catch or skip-and-continue for each loop external call.
**Red flags**:
- `for (...) target[i].call(...)` without try-catch

### UNI-47: Bounded Cleanup on Delete
**Provenance**: invariant-templates.md GR-4
**Pattern**: Deleting an entity runs unbounded work, DoSing the deletion path itself.
**Methodology**: For each delete path, check it completes in constant or bounded gas.

### UNI-48: Arithmetic Rounding Direction
**Provenance**: invariant-templates.md MF-3
**Pattern**: Rounding direction favors the user instead of the protocol, enabling dust extraction.
**Methodology**: For every `mulDiv` / division in asset/share math, verify the direction: deposits round shares DOWN, withdrawals round assets DOWN.
**Red flags**:
- `shares = amount * totalSupply / totalAssets` rounding up
- Fee calculation rounding toward user

### UNI-49: Donation / Direct Transfer Corruption
**Provenance**: invariant-templates.md MF-6 + vault-invariants V12
**Pattern**: Accounting reads `balanceOf(address(this))` instead of internal state, allowing direct transfers to corrupt accounting.
**Methodology**: For each accounting function, check whether it uses `balanceOf` or internal counters. `balanceOf` is donation-vulnerable.
**Red flags**:
- `totalAssets()` returns `token.balanceOf(this)`
- First depositor calculates shares from `balanceOf`

### UNI-50: First-Depositor Share Inflation
**Provenance**: invariant-templates.md MF-5 + vault-invariants V3
**Pattern**: First depositor mints 1 wei, donates large amount, causing subsequent depositors to round to zero shares.
**Methodology**: For any share-issuance contract, verify virtual shares/assets (OZ pattern), minimum deposit, or dead-shares mint on first deposit.
**Red flags**:
- `shares = assets * totalSupply / totalAssets` with no virtual offset and `totalSupply == 0` edge case

### UNI-51: Atomic Value Transfer
**Provenance**: invariant-templates.md MF-7
**Pattern**: Sender's balance decreases by X but receiver's increases by Y != X (minus fees).
**Methodology**: For each transfer, verify source debit == destination credit + documented fee.

### UNI-52: Solvency Invariant (contractBalance >= sum(userOwed))
**Provenance**: universal-invariants.md U3 + invariant-templates.md MF-1
**Pattern**: Protocol-tracked liabilities exceed actual asset holdings.
**Methodology**: For each token held, trace: (balance on contract) vs (sum of user claims + protocol fees). Verify the invariant holds under every execution path.
**Red flags**:
- Withdrawal path decrements `userShares` but not `totalShares`
- Fee accrual double-counted

### UNI-53: No Free Extraction
**Provenance**: invariant-templates.md MF-2
**Pattern**: A sequence of calls allows withdrawing more value than deposited (net of fees and yield).
**Methodology**: Model the protocol as a closed economy. Attempt to construct a cycle that produces profit without external input.

### UNI-54: Fee Bounds Enforcement
**Provenance**: invariant-templates.md MF-4
**Pattern**: Fee setters allow values exceeding documented maxima.
**Methodology**: For each fee setter, verify `require(fee <= MAX)`. Check cumulative fees across multiple paths.
**Red flags**:
- `setFee(uint256 fee)` with no upper bound

### UNI-55: Storage Slot Uniqueness
**Provenance**: invariant-templates.md ST-1
**Pattern**: Proxy and implementation use conflicting storage layouts, or assembly sstore overwrites another variable.
**Methodology**: For upgradeable contracts, verify storage gap reservation and layout compatibility via tools like `hardhat-upgrades`. For assembly slot access, verify slot calculation.

### UNI-56: Initialization Completeness
**Provenance**: invariant-templates.md ST-2
**Pattern**: Functions read storage variables before they are initialized, getting default zero values.
**Methodology**: For each storage variable, verify at least one write occurs before any read on every reachable path.

### UNI-57: Mapping Key Uniqueness (encodePacked Pitfalls)
**Provenance**: invariant-templates.md ST-3 + DF-3
**Pattern**: `abi.encodePacked` with multiple variable-length types produces colliding keys.
**Methodology**: Grep `abi.encodePacked`. For each, verify no two variable-length types are adjacent. Use `abi.encode` or add length prefixes.
**Red flags**:
- `keccak256(abi.encodePacked(name, symbol))` where both are user-supplied

### UNI-58: Signed Data Completeness
**Provenance**: invariant-templates.md DF-2 + signed-data-completeness §1
**Pattern**: A digest omits fields the function uses for decisions, allowing relayer substitution.
**Methodology**: For each signature verification, build a SIGNED DATA BINDING TABLE: list every field used post-verification. Every used field must be signed.
**Red flags**:
- `deadline` used but not part of signed payload
- `callGasLimit` honored but not signed

### UNI-59: Nonce / Replay Protection
**Provenance**: invariant-templates.md DF-5 + signed-data-completeness §4
**Pattern**: Signatures can be replayed due to missing nonce, non-atomic nonce increment, or nonce omitted from hash.
**Methodology**: Verify nonce is in the signed digest, incremented atomically, and scoped per-signer.

### UNI-60: Taint Boundary at External Returns
**Provenance**: invariant-templates.md DF-4
**Pattern**: Return values from external contracts are consumed without validation.
**Methodology**: For each external call return used in a calculation, verify sanity bounds.

### UNI-61: Oracle Staleness Protection
**Provenance**: invariant-templates.md OR-1
**Pattern**: Oracle price reads omit a staleness check, allowing stale values to drive critical decisions.
**Methodology**: For each oracle read, verify `require(updatedAt >= block.timestamp - heartbeat)` or equivalent. Check that every oracle reader uses the same threshold (see UNI-65).

### UNI-62: Manipulation-Resistant Pricing
**Provenance**: invariant-templates.md OR-2
**Pattern**: A price is derived from spot reserves or `balanceOf`, enabling flash-loan manipulation.
**Methodology**: Every price used for liquidation/borrowing/mint must use TWAP, Chainlink, or time-weighted sources.

### UNI-63: Oracle Graceful Degradation
**Provenance**: invariant-templates.md OR-3
**Pattern**: Oracle failure (revert, stale, zero) bricks the protocol permanently.
**Methodology**: Verify a pause path exists on oracle failure rather than a hard revert.

### UNI-64: Multi-Source Oracle Validation
**Provenance**: invariant-templates.md OR-4
**Pattern**: High-value operations rely on a single price feed with no cross-check.
**Methodology**: For liquidations and large swaps, verify price is cross-checked against a second source or bounded by a circuit breaker.

### UNI-65: Feed Consistency Across Readers
**Provenance**: invariant-templates.md OR-5
**Pattern**: Different functions read the same oracle with different freshness thresholds, producing inconsistent behavior.
**Methodology**: Identify every reader of each oracle feed. Verify all use the same freshness threshold and fallback.

### UNI-66: Parameter Scope Declaration
**Provenance**: parameter-scope-analysis.md §1
**Pattern**: Admin parameters are used by calculation functions with no documentation of whether they apply retroactively or only to future operations.
**Methodology**: Build a PARAMETER SCOPE TABLE for every admin-modifiable storage variable. Row: parameter, setter, reader functions, temporal scope, retroactive. If scope is undocumented AND the parameter is read by historical calculations, flag.

### UNI-67: Retroactive Calculation Prevention
**Provenance**: parameter-scope-analysis.md §2
**Pattern**: An admin parameter change alters results for a PAST period after the period ends but before users claim.
**Methodology**: For each admin-modifiable parameter, ask: "If admin changes it at T, does calling a view function for T-1 return a different result?" If yes, the historical value must be snapshotted.
**Red flags**:
- `getEpochReward(epochId)` reading the live `rewardRate`
- `pendingReward()` using the current rate for past periods

### UNI-68: Locked-Position Integrity
**Provenance**: parameter-scope-analysis.md §3
**Pattern**: A user commits to a locked position under specific terms; admin changes the global terms and the change retroactively applies.
**Methodology**: For each locking/staking/vesting mechanism, check whether terms are stored per-position or read from global state on each access.
**Red flags**:
- `penalty = globalPenaltyRate` read at exit time instead of lock time
- APY read from global state for pre-existing locks

### UNI-69: Timing-Adversary Resistance on Admin Changes
**Provenance**: parameter-scope-analysis.md §4
**Pattern**: Admin parameter updates create a window where attackers front-run or back-run for profit.
**Methodology**: For each admin setter, check timelock protection and whether front-run/back-run is profitable.

### UNI-70: Cross-Function Consistency of Parameter Reads
**Provenance**: parameter-scope-analysis.md §5
**Pattern**: `preview*` and actual operation use different snapshots of the same parameter, producing contradictory results.
**Methodology**: For each parameter read by view and state-changing functions, verify both use the same snapshot semantics.

### UNI-71: Boundary Safety on Parameter Updates
**Provenance**: parameter-scope-analysis.md §6
**Pattern**: Setters accept values that individually seem valid but collectively cause division-by-zero, overflow, or impossible states.
**Methodology**: For each setter, trace arithmetic expressions using the parameter. Check for zero denominator, >100% basis points, `min > max`, and upper-bound overflow.

### UNI-72: Privilege Enumeration / Centralization Surface
**Provenance**: governance-centralization.md §1
**Pattern**: Admin functions are undocumented; maximum damage under malicious admin is unclear.
**Methodology**: Enumerate every `onlyOwner`/`onlyRole` function. For each, document worst-case damage. Rate: can admin mint without backing, drain user funds, block operations, set fees to 100%?

### UNI-73: Operation Blocking Powers
**Provenance**: governance-centralization.md §3
**Pattern**: Admin can pause exits while entries remain open, trapping users.
**Methodology**: Check whether exits can be blocked independently of entries and whether transfers are pausable.

### UNI-74: Irreversible Admin Actions
**Provenance**: governance-centralization.md §5
**Pattern**: Admin actions cannot be undone (set-once mappings, remove-without-claim).
**Methodology**: For each admin action, verify an inverse exists.

### UNI-75: Ownership Transfer Two-Step
**Provenance**: governance-centralization.md §6
**Pattern**: Single-step ownership transfer can send ownership to `address(0)` or wrong address irrecoverably.
**Methodology**: Prefer `Ownable2Step`.

### UNI-76: Router Permissionless Entry Points
**Provenance**: router-multicall-invariants.md R1
**Pattern**: Permissionless `execute`/`multicall` dispatches commands where `from`/`owner` is attacker-supplied, draining anyone who approved the router.
**Methodology**: For every command with a `from`/`owner` parameter, verify `msg.sender == from` or equivalent. For every command with `receiver`, verify the attacker cannot send victim's funds to themselves.

### UNI-77: Approval Persistence on Router
**Provenance**: router-multicall-invariants.md R2
**Pattern**: Users grant approval to a router; any permissionless command can then spend their tokens.
**Methodology**: Build an APPROVAL FLOW TABLE for the router. For each spend path, verify msg.sender is validated as the token owner.

### UNI-78: Permit Frontrunning on Router
**Provenance**: router-multicall-invariants.md R3
**Pattern**: An attacker extracts a permit signature from the mempool and submits it with different downstream commands.
**Methodology**: Verify permit signer matches the operation beneficiary. Check DAI non-standard permit handling.

### UNI-79: Router Identity Confusion
**Provenance**: router-multicall-invariants.md R4
**Pattern**: Vaults/protocols see the router as the depositor; per-address limits apply to the router instead of end users.
**Methodology**: Check whitelist and maxDeposit enforcement site.

### UNI-80: Router Token Residual / Sweep
**Provenance**: router-multicall-invariants.md R6
**Pattern**: Tokens left in the router between commands can be swept by the first caller.
**Methodology**: Verify the router never holds tokens across transactions; if a sweep exists, verify it cannot race a victim's in-flight transaction.

### UNI-81: Compound-Fork Share Rounding Subadditivity
**Provenance**: compound-fork-integration.md §1
**Pattern**: `floor(a) + floor(b) < floor(a+b)` causes single aggregated redemption to require more shares than held.
**Methodology**: Compare sum of individual mints vs single aggregated redeem. Check whether protocol donates shares to absorb rounding.

### UNI-82: Compound Treasury Fee Activation Risk
**Provenance**: compound-fork-integration.md §2
**Pattern**: Governance of an external Compound fork enables a treasury fee; the integrating protocol reverts or silently loses amount.
**Methodology**: Check whether the adapter reads `treasuryPercent` and how it handles non-zero results.

### UNI-83: Supply-Cap / Borrow-Cap DoS
**Provenance**: compound-fork-integration.md §3
**Pattern**: External supply cap filled by a whale blocks all subsequent deposits.
**Methodology**: Check whether deposits handle cap reversion and whether an alternative yield source exists.

### UNI-84: High-Utilization Redemption DoS
**Provenance**: compound-fork-integration.md §4
**Pattern**: `redeemUnderlying` reverts when pool cash < requested; protocol has no fallback.
**Methodology**: Verify try-catch and fallback source chains.

### UNI-85: Empty-Market First-Depositor Amplification
**Provenance**: compound-fork-integration.md §6
**Pattern**: Protocol auto-deposits into newly-created markets vulnerable to first-depositor attack.
**Methodology**: Check whether yield sources are validated as established before auto-deposit.

### UNI-86: External Reward Capture from Yield Sources
**Provenance**: compound-fork-integration.md §11 + yield-source-integration.md §7
**Pattern**: Lending pools distribute governance tokens / Merkle rewards to the depositor (the protocol contract); users have no claim path.
**Methodology**: Verify a claim or generic `execute()` function exists and has fair distribution logic.

### UNI-87: Partial vs Full Redemption DoS
**Provenance**: yield-source-integration.md §1
**Pattern**: Protocol forces full balance redemption; if transfers are disabled and liquidity is low, users are permanently locked.
**Methodology**: Verify partial redemption exists or an equivalent escape hatch.

### UNI-88: Bad-Debt Cascade / Idle-Balance Drainage
**Provenance**: yield-source-integration.md §2
**Pattern**: Attacker deposits into an insolvent source (absorbing bad debt) then withdraws from idle balance, draining the protocol.
**Methodology**: Verify deposits check source solvency; verify redemption fallback ordering does not leave stale `depositedAmounts`.

### UNI-89: Skip/Disable Flag Consistency
**Provenance**: yield-source-integration.md §4
**Pattern**: A `skipForWithdrawal` flag is applied inconsistently across functions (correct for withdraw, wrong for deposit).
**Methodology**: Build a function × flag × checked? table.

### UNI-90: FCFS on Insolvency
**Provenance**: yield-source-integration.md §6
**Pattern**: When a yield source goes insolvent, first redeemer takes everything while later redeemers get nothing; no pro-rata loss sharing.
**Methodology**: Verify loss-sharing mechanism or document FCFS behavior as intentional.

### UNI-91: Negative-Yield Accounting
**Provenance**: yield-source-integration.md §5
**Pattern**: `depositedAmounts -= amountRedeemed` underflows when yield source returns less than deposited.
**Methodology**: Verify the accounting handles `amountRedeemed < deposit` gracefully.

### UNI-92: Yield-Leakage via Return-Value Mismatch
**Provenance**: yield-source-integration.md §8 + vault-invariants V16
**Pattern**: Yield source returns more than requested; excess is silently left in the contract or given to wrong recipient.
**Methodology**: Trace every `amountReturned` vs `amountRequested` path. Verify the excess is explicitly routed.

### UNI-93: Emergency Function Input Validation
**Provenance**: yield-source-integration.md §10
**Pattern**: `emergencyWithdrawFromYieldSources(address[])` accepts arbitrary addresses; accounting can be corrupted by a rogue address.
**Methodology**: Verify input validation against registered sources.

### UNI-94: Before/After Balance Pattern Consistency
**Provenance**: yield-source-integration.md §12
**Pattern**: Some paths use `balanceAfter - balanceBefore`, others trust nominal amount; inconsistent handling breaks fee-on-transfer or rebasing tokens.
**Methodology**: Identify every transfer path. Verify consistent before/after or consistent nominal usage across paths.

### UNI-95: Irreversible Yield-Source Configuration
**Provenance**: yield-source-integration.md §9
**Pattern**: `underlyingToVToken[token]` is set once with no unset; a wrong or deprecated mapping is permanent.
**Methodology**: Verify every configuration mapping has both set and unset admin paths.

### UNI-96: ERC-165 Inherited Interface Coverage
**Provenance**: drozer-lite v0.3.1 — class-of-bug: supportsInterface override omits interfaces implemented by ancestor contracts, breaking ERC-165-based integration detection.
**Pattern**: A contract's `supportsInterface(bytes4)` only reports the interface it was explicitly registered for, not every interface its parent contracts implement. Downstream integrators who check `supportsInterface(ParentInterface.selector)` get false and refuse integration.
**Methodology**: For every `supportsInterface` override, enumerate every ancestor contract's interface (including upgradeable/proxy libraries). Verify the override returns true for each. Prefer `return super.supportsInterface(interfaceId) || interfaceId == type(IThis).interfaceId` to the fully-enumerated OR chain to avoid drift on future inheritance changes.
**Red flags**:
- `supportsInterface` returns `interfaceId == type(IThis).interfaceId` only, not OR'd with `super`
- New interface added to the contract but supportsInterface not updated
- AccessControl + Enumerable + custom interface but only one is reported
- Interface-detection-based integration docs (e.g., marketplaces) not tested against actual supportsInterface

### UNI-97: Precision Loss in Decimal Conversion
**Provenance**: drozer-lite v0.3.1 — class-of-bug: silent truncation when scaling values between different decimal bases, rounding direction undocumented and inconsistent with the inverse operation.
**Pattern**: A function converts a value between different decimal bases (e.g. 18→8, 18→6, 8→18) and silently truncates. The rounding direction is not documented, not user-controlled, and not consistent with the inverse operation.
**Methodology**: Grep every `amount * 10**X`, `amount / 10**X`, `_convertToNDecimals`, or explicit `mulDiv`/`div` between two known-different decimal bases. For each, verify:
1. The rounding direction matches the intent (user-owed amounts round UP for the user, fees round UP for the protocol).
2. The inverse conversion is actually inverse — `convertUp(convertDown(x))` should equal `x` only at the base-grid boundary.
3. A round-trip of the same amount through two conversions does not compound loss more than a stated tolerance.
4. Boundary values (0, smallest non-zero, smallest that rounds up) behave correctly.
**Red flags**:
- `truncatedAmount = amount / 1e10;` with no rounding-up branch on the withdrawal path
- Deposit and withdrawal paths use different rounding directions silently
- Loss accumulates per-operation and is not recorded or refunded to the user
- Conversion comment says "truncates" but callers treat the result as exact

### UNI-98: receive()/fallback() Auto-Route Balance Invariant Break
**Provenance**: drozer-lite v0.3.1 — severity calibration fix for a pattern class where the receive()/fallback() path re-enters a state-mutating function and any invariant that reads `address(this).balance` becomes permanently brittle. This check is structurally HIGH, not MEDIUM.
**Pattern**: A contract has a `receive()` or `fallback()` payable function that unconditionally forwards `msg.value` into a state-mutating function in the same contract (deposit, wrap, stake, mint, buy). Another function in the same contract uses `address(this).balance` as part of an invariant check (e.g., `require(address(this).balance >= amount)` before a refund / withdraw / return / claim). Because the auto-route consumes incoming native value before it can accumulate, the balance-based invariant can be permanently unsatisfiable, or at minimum becomes dependent on chain-specific semantics for how native value can enter the contract without invoking `receive()`.
**Methodology**: For every `receive()` and `fallback()` function in the cluster:
1. Check whether it unconditionally forwards `msg.value` into a state-mutating function (one that writes storage, mints tokens, or calls an external contract with value).
2. Grep the entire cluster for any `address(this).balance` read used in a `require`, arithmetic, or branch that affects a user-visible decision (refund, withdraw, claim, redeem, rescue).
3. If both conditions hold, trace whether there is ANY path by which native value can enter the contract WITHOUT invoking `receive()` (e.g., `selfdestruct(to)` from another contract, direct balance credit from a privileged precompile, block reward to COINBASE if the contract is a miner/proposer, chain-specific system transfers). If no such path exists, the invariant is permanently broken. If one exists, the invariant is brittle and subject to operational assumptions outside the source.
**Red flags**:
- `receive() external payable { f(); }` where `f()` is any state-mutating function in the same contract
- `fallback() external payable { ... f(); }` similarly
- Any function with `require(address(this).balance >= amount, ...)` whose sibling contract has an auto-routing `receive()`/`fallback()`
- A "rescued" or "cancelled" accumulator variable whose only payout path depends on contract balance growing via future external transfers
- Comments or docs saying "buffer provides liquidity" or "accumulated value drains to users" but no actual code path produces non-auto-routed inbound value
**Severity rule (HARD)**: When the balance-based invariant is read in a user-facing function (withdraw, refund, claim, redeem, rescue, confirm, settle), this check MUST be rated at least HIGH. Do NOT downgrade to MEDIUM due to uncertainty about chain-specific native-transfer semantics — the correct finding is HIGH with a note that exploitability depends on operational assumptions the auditor should flag and verify with the protocol team. Severity miscalibration on this pattern is itself a finding-quality bug.

### UNI-99: Approval / Permission Persistence After Action Reversal
**Provenance**: drozer-lite v0.4.1 — class-of-bug: an action grants a permission as a side effect, and the reversal of that action does not revoke the permission, leaving the actor with unauthorized access.
**Pattern**: An action (bid, deposit, stake, register, subscribe) grants an approval, role, or allowance as a side effect. The corresponding reversal action (cancel bid, withdraw, unstake, deregister, unsubscribe) removes the primary state entry but does NOT revoke the permission that was granted alongside it. The actor retains the ability to operate on the entity (transfer, spend, execute) despite no longer having a stake or valid reason for access.
**Methodology**: For every function that grants an approval or permission as a side effect of a primary action:
1. Identify the corresponding reversal function (cancel, withdraw, remove, unstake, deregister).
2. Verify the reversal function explicitly removes the same approval/permission.
3. Check both per-token approval lists AND global operator/role mappings.
4. If the grant is conditional (e.g., only when `auto_approve` is true), verify the revocation also fires under the same condition.
**Red flags**:
- `approvals.push(sender)` in a bid/deposit path but the cancel/refund path only removes the bid entry, not the approval
- `grantRole(OPERATOR, sender)` on registration but `revokeRole` absent from deregistration
- `approve(spender, amount)` on stake but no `approve(spender, 0)` on unstake
- Toggle function (call once to create, call again to cancel) where the first call grants approval and the second call removes the primary record but not the approval
- Any path where an actor can: (1) perform action to gain permission, (2) reverse action to recover funds, (3) use retained permission to operate on the asset without cost

### UNI-100: Asymmetric Settlement Across Parallel Transfer Paths
**Provenance**: drozer-lite v0.4.1 — class-of-bug: multiple functions can move the same asset but only one includes payment/settlement logic, allowing the other to bypass payment entirely.
**Pattern**: A system has two or more functions that transfer ownership or move the same asset (e.g., `transfer` vs `send`, `transferFrom` vs `safeTransferFrom`, `withdraw` vs `emergencyWithdraw`, `redeem` vs `rescue`). One path includes settlement logic (payment to seller, fee deduction, accounting update, reward distribution). Another path transfers the asset without performing the same settlement, creating a bypass.
**Methodology**: 
1. Enumerate every function that changes ownership of an asset or moves value out of the contract.
2. Group these functions by the asset class they operate on.
3. For each group, build a comparison table: function name | settlement logic present? | fee deducted? | accounting updated? | events emitted?
4. If ANY function in the group skips settlement that another function performs, flag. Pay special attention to wrapper functions that call a shared internal `_transfer` without the outer settlement layer.
**Red flags**:
- `transfer()` includes payment settlement but `send()` calls `_transfer()` directly without settlement
- `withdraw()` updates accounting but `emergencyWithdraw()` does not
- Public `_transfer_nft()` helper is callable by approved addresses and bypasses the sale/auction settlement in `transfer_nft()`
- A function that takes a `recipient` parameter (allowing caller to send to another address) and a parallel function that forces `msg.sender` as recipient — the first may skip payment checks the second enforces
- Two functions that both call `check_can_send()` but only one settles the associated financial obligation (bid, deposit, escrow)

### UNI-101: Destructive Operation Without Obligation Settlement
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a burn/delete/close function destroys an entity with active obligations (deposits, rentals, locks), erasing records but not settling or refunding, making deposited funds permanently unrecoverable.
**Pattern**: A destructive operation (burn, delete, remove, close, self-destruct, deactivate) destroys an entity that carries active obligations — deposits held against it, active rentals or leases, pending reward claims, locked collateral, open orders, or unresolved escrows. The destruction erases the entity's records from storage, but the funds associated with those obligations remain in the contract with no recovery path. Affected users can no longer call cancel/refund/claim because the entity no longer exists.
**Methodology**: For every destructive function (burn, remove, close, delete, deactivate, self-destruct):
1. Identify what data is erased (the entity's full storage record, including nested structs, vectors, mappings).
2. Check whether any of the erased data includes: deposit amounts, active rental/lease records, pending claims, locked collateral, open bids, escrowed funds.
3. Verify the function checks for zero active obligations BEFORE allowing destruction. Acceptable patterns: `require(obligations.length == 0)`, `require(deposit_amount == 0)`, iterating obligations and refunding each before deletion.
4. If the function only checks ownership/approval but not obligation status, flag.
**Red flags**:
- `burn()` checks `check_can_send()` (ownership) but not whether `rentals.len() > 0` or `bids.len() > 0`
- `closePosition()` deletes the position record without checking `pendingRewards > 0`
- `deleteAccount()` while staking/delegation entries still reference the account
- `remove(tokenId)` erases a token struct that contains a Vec of deposit records
- Any destructive function where the authorization check is ownership/approval only, without an obligation-settlement check

### UNI-102: Heterogeneous Collection Without Type Discrimination
**Provenance**: drozer-lite v0.4.1 — class-of-bug: items of different types with different economic parameters are stored in the same collection, and operations iterate without filtering by type, allowing cross-type exploitation (e.g., paying in denomination A but receiving refund in denomination B).
**Pattern**: A collection (Vec, array, mapping, linked list) stores items of different subtypes, distinguished by a type flag, enum field, or discriminant. Operations that search, iterate, cancel, settle, or finalize items in the collection match by identity fields (address, ID, period) but do NOT filter by the type discriminant. This allows an operation designed for subtype A to match and operate on a subtype B item that has different economic parameters (denomination, rate, fee structure, cancellation policy).
**Methodology**:
1. Identify every collection that stores items with a type discriminant field (e.g., `item_type: bool`, `order_side: enum`, `position_type: u8`, `category: u8`).
2. For every function that searches/iterates the collection, check whether the search predicate includes the type discriminant.
3. If the search matches by (address + period) or (address + id) but ignores (type), verify whether the matched item's economic parameters (denomination, rate, terms) could differ from what the calling function assumes.
4. If a function reads denomination/rate/terms from a TYPE-LEVEL config (e.g., `shortterm_rental.denom`) but the matched item was created under a DIFFERENT type's config (e.g., `longterm_rental.denom`), flag as HIGH — this enables cross-denomination value extraction.
**Red flags**:
- A `rentals` Vec stores both short-term and long-term entries with a `type` flag, but cancel/finalize functions search by `(address, period)` without checking `type`
- An order book stores buy and sell orders in the same array with a `side` field, but settlement iterates without filtering by side
- A positions collection mixes collateralized and uncollateralized positions, but liquidation logic applies uniformly
- A function reads the denomination from a type-level config struct but the matched item in the shared collection was created under a different type's denomination
- Cancel function for type A matches a type B item and refunds using type A's denomination instead of the item's stored denomination

### UNI-103: Payment-Gated Transfer Allows Beneficiary Mismatch
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a transfer function looks up payment amount by recipient address, but the caller can specify any recipient including one with no payment, causing a zero-payment asset transfer.
**Pattern**: A function combines asset transfer with payment settlement. The payment amount is looked up from a mapping or list keyed by the recipient address (e.g., bids, deposits, escrow entries). The caller can freely specify the recipient parameter. If the recipient has no entry in the payment mapping, the amount defaults to zero and the transfer proceeds without payment to the previous owner. Alternatively, the caller specifies a recipient different from themselves to avoid their own payment being consumed, then cancels their payment entry for a full refund.
**Methodology**:
1. For every function that transfers an asset AND looks up a payment amount by a caller-supplied address parameter:
   a. Check whether the function reverts when no payment entry exists for the specified recipient (amount == 0 case).
   b. Check whether the function validates that the payment amount meets the listed/required price.
   c. Check whether the caller is constrained to specify themselves as the recipient, or can specify any address.
2. If the function proceeds with transfer when `amount == 0` (no matching payment), flag as HIGH — the asset is transferred for free.
3. If the function does not validate `amount >= listed_price`, flag as HIGH — the asset can be transferred for less than the listed price.
**Red flags**:
- `amount = bids[recipient].offer` defaults to 0 when no bid exists, and the function has a branch that proceeds with transfer when `amount == 0`
- Transfer function accepts `recipient` as a parameter (not forced to `msg.sender`), allowing the caller to route the transfer to an address with no active bid
- Caller can: (1) place a bid to gain approval, (2) call transfer with a DIFFERENT recipient who has no bid (zero payment), (3) cancel their own bid for full refund
- No `require(amount >= listed_price)` check between the payment lookup and the ownership transfer
- The `amount > 0` branch sends payment to the previous owner, but the `amount == 0` branch still transfers ownership

### UNI-104: Numeric Type Width Insufficient for Token Decimals
**Provenance**: drozer-lite v0.4.1 — class-of-bug: price or amount parameters use a narrower integer type than the token amounts they interact with, making normal-value operations impossible for high-decimal tokens.
**Pattern**: Price, amount, or rate parameters in message/function signatures use a narrower integer type (e.g., `u64`, `uint64`, `uint32`) than the token amount type used in the contract's arithmetic (e.g., `u128`, `Uint128`, `uint256`). For tokens with 18 decimals, `u64` maxes at ~18.4 tokens — any price above ~$18 for a $1-token is unrepresentable. This creates a functional ceiling where normal-value operations silently fail or are impossible to express.
**Methodology**:
1. For every price, amount, or rate parameter in external function signatures and message structs, note the integer type width.
2. For each such parameter, trace how it's used in arithmetic with token amounts. Note the token amount type (typically the widest type in the system).
3. If the parameter type is narrower than the token amount type AND the protocol accepts arbitrary user-specified denominations (tokens with varying decimals), flag.
4. Calculate the practical ceiling: `max_value / 10^decimals` for common decimal counts (6, 8, 18). If the ceiling is below reasonable real-world values for the parameter's purpose (e.g., < $1000 for a rental price), flag.
**Red flags**:
- `price_per_day: u64` but `deposit_amount: Uint128` (u128) in the same system
- `amount: u64` in a withdrawal function but `info.funds[0].amount` is `Uint128`
- `fee: u64` but fee is multiplied with `Uint128` amounts, silently capping the effective fee range
- Any `u64` price/amount field in a protocol that accepts arbitrary token denominations (user-chosen, not hardcoded)
- Withdrawal function requires multiple calls to extract a normal-value deposit because the per-call `amount` parameter is too narrow

### UNI-105: Stored Constraint Not Enforced at Consumption Point
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a configuration function stores a constraint field (availability window, whitelist, maximum, deadline) but the consuming function that should enforce it never reads or checks it, making the constraint decorative.
**Pattern**: A configuration or listing function accepts and stores a constraint parameter (available period, whitelist, max participants, allowed tokens, deadline, minimum amount, geographic restriction). The consuming function that should enforce this constraint (reservation, deposit, bid, claim, register) operates on the same entity but never reads or validates the stored constraint. The constraint exists in storage but has zero enforcement — users can bypass it simply by never encountering a check.
**Methodology**:
1. For every configuration/listing function, enumerate every field it writes to storage.
2. For each stored field, classify it as: (a) data field (description, name, URI — informational), or (b) constraint field (period, whitelist, max, min, deadline, rate — should restrict behavior).
3. For each constraint field, find ALL consuming functions that operate on the same entity. Verify the constraint field is READ and produces a REVERT or behavioral change in each consumer.
4. If a constraint field is stored but never read by any consuming function, flag as MEDIUM — the feature is broken, not just missing.
**Red flags**:
- `available_period` set in listing function but reservation function checks `minimum_stay` only, ignoring `available_period` entirely
- `max_participants` stored on entity creation but join function has no cap check
- `allowed_tokens` whitelist stored but deposit function accepts any denomination
- `deadline` stored but claim function checks `block.timestamp` against a different value
- `auto_approve` flag stored for one rental type but the approval function for that type never reads it
- Any field in a config struct that is written in the setter and read ONLY in query/view functions (never in state-changing functions)

### UNI-106: Listing-Gate Bypass on Unlisted Entities
**Provenance**: drozer-lite v0.4.1 — class-of-bug: a function that should only operate on listed/active entities does not check the listing status flag, allowing operations on unlisted, delisted, or never-listed entities.
**Pattern**: An entity has a listing status field (`is_listed`, `active`, `status`, `enabled`) that is set by a listing function and cleared by an unlisting function. Consumer functions (bid, reserve, purchase, deposit, subscribe) that should only operate on listed entities do not check the listing status. This allows: (1) operations on entities that were never listed, (2) operations on entities that were explicitly delisted, (3) exploitation of stale configuration (e.g., `auto_approve` from a previous listing) on a currently unlisted entity.
**Methodology**:
1. For every entity with a listing/status flag, enumerate: (a) the function that sets it to active/listed, (b) the function that sets it to inactive/unlisted, (c) all consumer functions that operate on the entity.
2. For each consumer function, verify it checks the listing status flag early in execution (before accepting funds, granting approvals, or modifying state).
3. Pay special attention to configuration fields that PERSIST across list/unlist cycles. If `auto_approve`, `price`, `denomination`, or other economic parameters are set during listing and NOT cleared during unlisting, check whether consumers of these fields are guarded by the listing status.
4. If a consumer function accepts funds or grants permissions without checking listing status, flag. Severity depends on whether the stale configuration enables value extraction.
**Red flags**:
- `bid()` function does not check `is_listed == true` before accepting funds and granting approval
- `reserve()` function accepts deposits for unlisted properties/assets
- `purchase()` function operates on delisted items using stale price/denomination from a prior listing
- Unlisting function sets `is_listed = false` but does NOT clear `auto_approve`, `price`, or `denomination` — these persist and are used by consumer functions that skip the listing check
- Any consumer function that reads economic parameters (price, denomination, approval mode) from the entity without first verifying the entity is currently listed

### UNI-107: Nested Loop Depth Exceeds Gas Budget
**Provenance**: drozer-lite v0.4.2 — class-of-bug: nested iteration over user-growable collections produces O(N^k) complexity with k≥2, exceeding the block gas limit and permanently DoSing the function for affected users.
**Pattern**: A function iterates over collection A (size P). For each item, it iterates over collection B (size F). For each pair, it iterates over a range R (size E). The total work is O(P × F × E). Even with individual caps on P, F, and E, the PRODUCT can exceed the block gas limit. This is distinct from UNI-12 (single unbounded loop) — the issue is the nesting depth, not any single loop being unbounded.
**Methodology**:
1. For every function with nested loops (loop inside a loop), compute the worst-case product of all loop bounds.
2. For each bound, determine: is it hardcoded? Is it configurable? Is it user-determined? Can it grow over time (e.g., epochs since first action)?
3. Compute worst-case iterations: multiply all bounds. If the product exceeds ~50,000 (conservative gas budget for CosmWasm/EVM), flag.
4. Check whether the function can be called in batches (e.g., claim per-position, claim per-epoch range). If no batching mechanism exists and the function is mandatory (e.g., claim before close), the DoS is permanent.
**Red flags**:
- `for position in positions { for farm in farms { for epoch in start..=current { ... } } }` — O(P×F×E)
- Reward calculation that iterates over all epochs since a user's first deposit with no epoch-range parameter
- `close_position` requires `claim()` first, and `claim()` has O(N^3) complexity — DoS on claim blocks close
- No "partial claim" or "skip positions" mechanism exists
- Config parameters cap individual collections (e.g., max 100 positions, max 10 farms) but their product (1000+) is not capped

### UNI-108: Temporal Parameter Allows Retroactive / Past Values at Creation
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a creation function accepts a user-supplied temporal parameter (start time, start epoch, activation date) without enforcing it is in the future, allowing retroactive entity creation that breaks reward distribution, billing, or scheduling invariants.
**Pattern**: An entity with a time-based lifecycle (farm, vesting schedule, auction, subscription, rental) accepts a `start_time` or `start_epoch` parameter at creation. The parameter is validated for basic sanity (> 0, < end) but is NOT validated against the current time/epoch. This allows creating entities that "started in the past," retroactively assigning rewards, obligations, or access to historical periods that other participants have already settled.
**Methodology**:
1. For every creation function that accepts a start_time/start_epoch parameter, verify it is enforced as `>= current_time + 1` or `>= current_epoch + 1`.
2. Check the default value when the parameter is omitted — if it defaults to `current + 1`, verify the explicit path has the same constraint.
3. Trace what happens if start is set to a past value: are rewards retroactively assigned? Do billing periods extend into the past? Can the creator claim historical periods?
**Red flags**:
- `start_epoch = params.start_epoch.unwrap_or(current_epoch + 1)` but no `ensure!(start_epoch >= current_epoch + 1)` for the explicit case
- Validation checks `start < end` and `end > current` but not `start > current`
- A farm/schedule created with past start_epoch assigns emissions to epochs where participants already claimed, creating unfair distribution
- Default path is safe (`current + 1`) but explicit path bypasses the constraint

### UNI-109: Self-Call Identity Confusion
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a contract calls itself (via submessage, internal execute, or self-invoke) and the called function uses `info.sender` for authorization, but the sender is now the contract itself instead of the original user, causing authorization checks to fail or be bypassed.
**Pattern**: A function performs a two-step operation by calling itself: step 1 initiates (stores context in a buffer), step 2 is triggered via a self-call (SubMsg or wasm_execute to self). The second step's `info.sender` is the contract's own address, not the original user. If step 2 has an authorization check like `require(sender == receiver)` or `require(sender == user)`, it fails because sender is the contract. Conversely, if step 2 has an authorization check like `require(sender == admin || sender == contract)`, the self-call bypasses user-level restrictions.
**Methodology**:
1. For every SubMsg or wasm_execute that targets the contract's own address (`env.contract.address`), identify the function being called.
2. Check what `info.sender` is used for in the called function. If it's used for authorization, it will be the contract address, not the original caller.
3. Check whether any receiver/beneficiary validation compares against `info.sender` — this will fail for the self-call case.
4. Check whether any privilege check accepts the contract's own address — this could be a bypass vector.
**Red flags**:
- `wasm_execute(env.contract.address, &ExecuteMsg::ProvideLiquidity { receiver: user, ... }, funds)` where `ProvideLiquidity` checks `ensure!(receiver == info.sender)` — fails because info.sender is the contract
- Two-step LP provision: step 1 swaps half, step 2 provides balanced LP. Step 2's sender check rejects the self-call.
- A singleton buffer stores context for the reply handler — if two users trigger step 1 in the same block, the second overwrites the first's buffer
- Any function with `if info.sender == env.contract.address { /* special path */ }` that grants elevated privileges

### UNI-110: Permissionless Entity Creation Bypasses Protocol-Intended Parameters
**Provenance**: drozer-lite v0.4.2 — class-of-bug: a permissionless creation function allows the creator to specify parameters that the protocol intended to control (e.g., fee rates, reward schedules), enabling creators to set these to zero or adversarial values to the protocol's detriment.
**Pattern**: A permissionless function (e.g., create pool, create farm, register market) accepts parameters that affect protocol revenue or user protections. These parameters are stored per-entity and used in subsequent operations. The protocol intended to enforce minimum values (e.g., minimum protocol fee, minimum collateral ratio) but the creation function either has no minimum check or the minimum is 0. Creators can set `protocol_fee = 0`, `collateral_ratio = 0`, or `insurance_fund_share = 0` to attract users while depriving the protocol of revenue or safety margins.
**Methodology**:
1. For every permissionless creation function, enumerate every parameter that is stored per-entity and affects protocol revenue or user protection.
2. For each such parameter, check whether a protocol-level minimum is enforced. If the only validation is `fee.is_valid()` (which may only check `< 100%`), a zero value passes.
3. Check whether the protocol has a global/config-level fee that overrides per-entity fees. If not, the per-entity fee IS the protocol fee.
4. Compare against industry standard: Uniswap charges a protocol fee at the factory level; Curve charges admin fees globally. If this protocol charges fees per-entity with no floor, flag.
**Red flags**:
- `create_pool(pool_fees: PoolFee)` where `pool_fees.protocol_fee` can be set to 0 by the creator
- `is_valid()` only checks `fee < 100%`, not `fee >= MINIMUM_PROTOCOL_FEE`
- No global fee override exists — the per-entity fee is the only fee
- Protocol documentation states "fees are collected on every swap" but code allows zero-fee pools
- Pool/farm/market creator can front-run legitimate creation with a zero-fee version to attract liquidity away from fee-bearing entities

## checklists/vault.md

# Vault Checklist

> Profile: vault
> Checks: 6
> Source: ported from Drozer-v2 vault-invariants.md (provenance cited per check)

## Methodology

Vaults intermediate a share/asset conversion that an attacker will try to manipulate. For every path that converts between shares and assets, identify (a) what controls the numerator and denominator, (b) whether an attacker can influence either atomically (donation, flash-loan, first-deposit), (c) the rounding direction, and (d) whether `totalAssets()` reflects manipulable external state. Test boundary conditions explicitly: `totalSupply == 0`, single wei deposits, full redemption leaving dust, last-strategy removal, and paused external integrations.

## Checks

### VAULT-1: First-Depositor Share Inflation
**Provenance**: vault-invariants.md V3 + V12
**Pattern**: The first depositor mints 1 wei of shares, donates a large amount of the underlying asset, and subsequent depositors round to zero shares; the first depositor then withdraws everything.
**Methodology**: For any share-issuance contract, check whether `_convertToShares` uses virtual offsets (OZ ERC4626 pattern), a minimum initial deposit, or mints dead shares on first deposit. Test the `totalSupply == 0` branch explicitly. Verify whether direct token transfers to the vault affect `totalAssets()`.
**Red flags**:
- `shares = assets * totalSupply / totalAssets` with no virtual offset
- `totalAssets()` returns `token.balanceOf(this)` (donation-manipulable)
- No minimum deposit and no dead-share mint on first deposit

### VAULT-2: ERC-4626 Preview / Max Consistency
**Provenance**: vault-invariants.md V13
**Pattern**: `preview*` and `max*` functions return values inconsistent with actual `deposit`/`withdraw` execution (missing fees, missing pause, wrong rounding direction), breaking external integrations.
**Methodology**: For each of `maxDeposit`, `maxMint`, `maxWithdraw`, `maxRedeem`, verify it reflects actual enforced limits (paused state, whitelist, internal caps) — not `type(uint256).max`. For each of `previewDeposit`, `previewMint`, `previewWithdraw`, `previewRedeem`, verify fees are included and rounding matches the spec (`previewMint` rounds UP, `previewWithdraw` rounds UP).
**Red flags**:
- `maxWithdraw` returns balance while paused
- `previewDeposit` ignores entry fee
- Preview rounds different direction than actual execution

### VAULT-3: Pause Completeness
**Provenance**: vault-invariants.md V14
**Pattern**: When paused, user-facing operations are blocked but admin functions (`rebalance`, `harvest`, `compound`, `migrateStrategy`) still move user assets — admin can act while users cannot exit.
**Methodology**: Enumerate every state-changing function. For each, check whether it is gated by `whenNotPaused`. Any asset-moving admin function that is NOT gated is a trap vector.
**Red flags**:
- `rebalance()` callable while `withdraw()` is paused
- Strategy migration runnable during emergency pause

### VAULT-4: Return-Value Semantics on Deploy/Undeploy
**Provenance**: vault-invariants.md V16 + V11
**Pattern**: `deploy()` / `undeploy()` returns the ACTUAL amount (post-slippage, post-fees), but callers use the REQUESTED amount for downstream accounting.
**Methodology**: For each deploy/undeploy call, check whether the return value or the input parameter is used for `_deployedAmount` bookkeeping, for pro-rata allocation, and for return-to-user amounts. Verify `_deployedAmount` is decremented on undeploy.
**Red flags**:
- `strategy.undeploy(amountRequested); _deployedAmount -= amountRequested;` instead of using the return value
- Multi-strategy withdrawal using requested-amount math
- Leverage undeploy returning 90% with 10% silently lost

### VAULT-5: Strategy Migration & Constructor Validation
**Provenance**: vault-invariants.md V7 + V17
**Pattern**: Strategies are added without validating the underlying protocol's expected asset, and migration does not fully unwind the old strategy before activating the new one.
**Methodology**: For each strategy constructor, verify it checks that the configured asset matches the underlying protocol's expected token. Verify `addStrategy` rejects duplicates and grants the token approval. Verify `removeStrategy` revokes the approval. Verify `migrateStrategy` fully unwinds before activating the replacement and has a timelock / user exit window.
**Red flags**:
- No `require(market.loanToken() == asset)` in constructor
- Migration path that leaves old strategy still approved
- Removed strategy retains unlimited allowance

### VAULT-6: Access Control Principal (Receiver vs Caller)
**Provenance**: vault-invariants.md V18
**Pattern**: `deposit(assets, receiver)` checks `msg.sender` against a whitelist instead of checking `receiver`, allowing a whitelisted user to deposit on behalf of any non-whitelisted address.
**Methodology**: For every function accepting `receiver`/`owner`/`beneficiary`, check which principal is validated against whitelists/limits. `maxDeposit(address)` must accept `receiver` as the limit target.
**Red flags**:
- `require(isWhitelisted[msg.sender])` on a function that credits shares to `receiver`
- `maxDeposit` read against `msg.sender` when `deposit` credits to `receiver`

