# move-auditor

Audits Move contracts (Sui & Aptos) for security bugs.

- **Kind:** skill
- **Source:** https://github.com/pantheraudits/move-auditor
- **Page:** https://forefy.com/skills/d3dd667c-1db8-459f-b70d-bcae46c3ab02
- **API (JSON + files):** https://forefy.com/api/asr/d3dd667c-1db8-459f-b70d-bcae46c3ab02

---

## .DS_Store

```

```

## CHANGELOG.md

# Changelog

All notable changes to `move-auditor` are documented here.

Versioning follows [Semantic Versioning](https://semver.org/):
- **MAJOR** — breaking changes to skill interface or report format
- **MINOR** — new checks, new reference files, expanded coverage
- **PATCH** — fixes to existing checks, wording improvements, bug corrections

Each release is tagged as `move-auditor@X.Y.Z`.

---

## [3.6.1] — 2026-04-03

### Audit methodology improvements — admin analysis, parallel subsystem checks, bit-shift safety

- **SKILL.md Phase 2**: Added Perspective 5 — Bidirectional Admin Checker. Every admin
  function now mandates analysis in both directions (admin→user harm AND user→admin grief)
- **SKILL.md Phase 7**: Added Step 6 — Post-Confirmation Parallel Subsystem Check. After
  confirming any Medium+ finding, grep all call sites and verify the same bug doesn't exist
  in parallel subsystems (deposit/borrow, token0/token1, pool A/pool B)
- **common-move.md 2.5**: Added Bit-Shift Wrapping (Silent Overflow). Move bit-shifts
  (`<<`/`>>`) silently wrap instead of aborting — unlike standard arithmetic. Off-by-one
  in custom overflow checks produces corrupted results, not aborts

---

## [3.6.0] — 2026-04-01

### Aptos Patterns Enhancement — input validation, object safety, testing

Expanded Aptos-specific coverage with new input validation checks, stronger object
safety guidance, and build/test tooling for Phase 1 detection.

**aptos-patterns.md — 1 new pattern + 2 enhanced checks:**
- APT-25: Input Validation Gaps — structured 6-category checklist for Aptos entry function
  parameter validation (zero amount, max limit, vector length, string length, zero address,
  enum-like range). Cross-references APT-13, APT-10, common-move 2.1
- APT-17 enhanced: Added ungated transfer control (`object::set_untransferable()`) and
  DeleteRef discipline checks — objects that shouldn't be freely transferable or deletable
  now have explicit verification items
- Aptos Verification Checklist: 3 new items for APT-25, ungated transfers, DeleteRef safety
- Aptos Build & Test Commands: Added `aptos move compile`, `aptos move test --coverage`,
  `aptos move coverage summary`, `aptos move coverage source` commands for Phase 1 build
  detection, with coverage threshold guidance

**SKILL.md:**
- Updated aptos-patterns.md reference range from APT-24 to APT-25

---

## [3.5.0] — 2026-03-27

### Expanded Sui Patterns & Logic Checks — 16 new vulnerability patterns

Broadens Sui-specific coverage with 13 new checks (SUI-30 to SUI-42) targeting
object model design flaws, shared object contention, composability anti-patterns,
and Sui framework misuse. Also adds 3 new chain-agnostic logic checks to
`common-move.md` for subtle security bugs that static analysis misses.

**common-move.md — 3 new chain-agnostic patterns:**
- 1.6: Authorization returns bool without assertion — callers can silently discard
  the result, bypassing access control entirely
- 4.5: Inverted security logic — checks that block the wrong party, use the wrong
  comparison direction, or assert the opposite of the intended condition
- 4.6: Wrong field update — functions that modify a different same-typed field than
  intended (compiler cannot catch field swaps between `u64` fields)
- 3 new verification checklist items for the above

**sui-patterns.md — 13 new Sui-specific patterns (SUI-30 to SUI-42):**
- SUI-30: VecMap/VecSet for unbounded collections — O(n) DoS when user-driven growth exceeds ~1K entries
- SUI-31: Shared object contention — excessive `&mut` on read-only paths forces consensus ordering
- SUI-32: Blind transfer without receive logic — objects transferred to object addresses with no extraction path
- SUI-33: `address` type where `ID` should be used — loses type safety for object references
- SUI-34: Internal transfer instead of return — breaks PTB composability
- SUI-35: Batch function instead of PTB loop — unnecessary complexity, vector mismatch risk
- SUI-36: Solidity-style auth (address→role maps) instead of Move capability objects
- SUI-37: Framework type name shadowing — user types named CoinMetadata, TreasuryCap, Publisher, etc.
- SUI-38: Metadata/Display frozen before required fields set — irreversible
- SUI-39: Multiple Publisher objects per package — split authority, governance complexity
- SUI-40: Unnecessary `public(package)` visibility — attack surface expansion
- SUI-41: NFT stores constant fields — per-collection data belongs in Display templates, not per-instance structs
- SUI-42: Migration function in non-upgraded v1 package — dead code
- 14 new verification checklist items for the above

**SKILL.md:**
- Updated sui-patterns.md reference range: SUI-01 to SUI-42 (was SUI-01 to SUI-28)
- Version bumped to 3.5.0

---

## [3.4.0] — 2026-03-21

### Anti-False-Positive Overhaul — TOB-inspired confidence gating and evidence chains

Benchmarking against CurrenSui revealed a ~25% false positive rate, mostly from
LLM rationalizations, pattern-matching without data flow analysis, and inflated
severity on non-exploitable findings. This release integrates anti-FP techniques
adapted from Trail of Bits' skills (rationalizations tables, confidence gating,
evidence templates, FP catalogs, devil's advocate reviews) into the Move auditor.

**New files:**

- **`move-fp-catalog.md`** (Always loaded) — 10-row rationalizations table of
  Move-specific LLM shortcuts to reject, 29+ false positive patterns across 5
  categories (Sui Object Model, Move Type System, Abort Semantics, PTB Composition,
  DeFi Design Patterns), and a 5-point self-hallucination check protocol
- **`evidence-chains.md`** (Phase 7) — Structured evidence templates: data flow
  with Move trust levels, mathematical bounds proofs, attacker control analysis,
  PoC pseudocode (Sui PTB / Aptos tx format), negative PoC for dismissals, and
  13-question devil's advocate protocol
- **`confidence-gates.md`** (Phase 7) — Multi-signal confidence gating: 3 levels
  (`confirmed`/`likely`/`needs_review`), 8 ranked signal types, hard evidence
  requirements per finding type, completeness thresholds, 6-gate checklist

**SKILL.md changes (net reduction: 538 → 497 lines):**
- Removed Known FP Patterns section (migrated to `move-fp-catalog.md` Section 2E)
- Removed Quick Maturity Assessment (low value, covered by reference files)
- Added "When NOT to Use" section
- Added 3 new files to Reference Files table
- Phase 7 now loads `evidence-chains.md` and `confidence-gates.md`
- Kill Question 6: mandatory self-hallucination check
- Step 3 labels now include confidence levels (`confirmed`/`likely`/`needs_review`)
- Condensed verbose prose throughout

**Modified files:**
- `verification-policy.md` — added Hard Evidence Requirements cross-reference and
  Confidence Gating section with severity caps
- `checklist-router.md` — added `move-fp-catalog.md` to Always Load, added
  Verification Phase Loading section for evidence-chains and confidence-gates
- `sample-finding.md` — added second example finding demonstrating evidence chain
  table, signal strengths, confidence level, recoverability assessment, and gate verification

---

## [3.3.0] — 2026-03-19

### Workflow and verification improvements

Adds three new reference files that improve coverage selection, state-consistency
review, and verification rigor for subtle High/Critical Move bugs:

- **`checklist-router.md`** — improves coverage planning from detected protocol signals
- **`semantic-gap-checks.md`** — adds a dedicated stale-state and state-desync review pass
- **`verification-policy.md`** — strengthens exploitability validation and severity discipline

**SKILL.md:**
- Added the 3 new files to Reference Files and made `verification-policy.md` +
  `checklist-router.md` mandatory on every audit
- **Phase 1:** now requires a router-driven coverage plan instead of purely
  heuristic file loading
- **Phase 5:** new Semantic Gap & Stale-State Scan before cross-module interaction review
- **Verify & Triage:** now applies stronger evidence requirements plus
  reachability/math feasibility gates
- Report triage now uses the existing finding labels with stronger evidence requirements

**README.md:**
- Documented the new router-driven coverage step, semantic-gap pass, and
  stronger verification workflow
- Added the 3 new files to the published skill structure

This release is focused on reducing false refutations and improving detection of
state-desync bugs such as stale checkpoints, skipped accumulator writes,
cross-module cleanup gaps, and dual-source metric inconsistencies.

## [3.2.0] — 2026-03-17

### Build & Test Log Analysis — Runtime-informed vulnerability detection

Adds a conditional build-and-test phase to the audit workflow. When the target project
compiles, the auditor runs the test suite, captures output, and systematically analyzes
logs for arithmetic aborts, assertion failures, expected-failure annotations, and edge-case
panics that may indicate latent High/Critical bugs invisible to static-only pattern matching.

**SKILL.md:**
- **Phase 1:** Added Build Detection gate — checks `Move.toml` + runs `sui move build` or
  `aptos move compile`. Sets `BUILD_AVAILABLE` flag. If build fails, logs errors and skips
  test analysis. If build succeeds, runs Test Log Analysis (common-move.md Section 13)

**common-move.md:**
- **Section 13 — Build & Test Log Analysis** (full procedure):
  - 13.1: Build Verification — compile check with error categorization
  - 13.2: Test Execution & Log Capture — run test suite with output capture
  - 13.3: Log Analysis — 5 signal categories: arithmetic aborts, assertion failures,
    expected-failure annotations, test failures/skipped tests, gas/execution limits
  - 13.4: Triage & Escalation — priority table with escalation rules; arithmetic aborts
    in financial modules auto-escalate to Recoverability Matrix analysis
  - 13.5: Reporting — structured TEST-NNN format with cross-referencing to main findings
- **1 new verification checklist item** for Section 13

**README.md:**
- Added "Best results" section explaining the skill works best on buildable projects
- Documented static-only fallback mode for non-buildable code

---

## [3.1.0] — 2026-03-17

### Arithmetic/Accounting DoS — Catch hidden fixed-point overflow and accumulator deadlock

Based on a missed High-severity finding in a Sui lending protocol where a multiply-before-divide
overflow inside a fixed-point helper permanently froze all lending operations. The overflow
occurred in `float::mul` before the normalizing division could execute, and the abort happened
before `last_update_time_ms` was checkpointed — creating an irrecoverable deadlock.

**common-move.md:**
- **2.6 Fixed-Point Helper Library Overflow:** Mandatory check to open fixed-point helper modules
  (`float`, `decimal`, `wad_ray`) and derive internal overflow bounds for `mul`/`div`/`from`.
  Targets hidden overflow where calling code looks safe (`A.mul(B).div(C)`) but the helper
  aborts before `div(C)` executes
- **12.1 Abort-Before-Checkpoint Deadlock:** Checks that state checkpoints (`last_update_time`,
  `cumulative_index`) are written before or atomically with potentially-aborting arithmetic.
  Includes concrete example with reward manager pattern
- **12.2 Admin-Origin Latent User DoS:** Explicit guidance that admin-configured parameters
  are reportable as High/Critical when users/liquidators are later bricked
- **Recoverability Matrix:** Mandatory 7-question matrix for every DoS candidate — traces
  cancel/claim/close/emergency paths to determine if deadlock is temporary, conditional,
  or permanent
- **4 new verification checklist items** for sections 2.6, 12.1, 12.2

**defi/defi-math-precision.md:**
- **DEFI-85:** Multiply-Before-Divide Overflow in Fixed-Point Helpers — full analysis
  methodology with 3-step process (derive helper bounds → compute overflow threshold →
  build threshold table with production token decimals). Includes worked example with
  USDC/SUI reward programs showing overflow at 10.25 hours / 5.12 hours of inactivity
- **DEFI-86:** Accumulator Checkpoint Liveness — detects abort-before-state-advance
  patterns in reward/interest accumulators. Includes entry point tracing checklist
- **3 new verification checklist items** for DEFI-85, DEFI-86

**SKILL.md:**
- **Phase 3:** Added mandatory fixed-point helper inspection step — auditor must open
  helper source and derive overflow bounds, not trust calling code at face value
- **Phase 5 pair 9:** `reward_manager_update ↔ all lending operations` — checks whether
  accumulator abort-before-checkpoint traps all user and admin paths
- **Severity Reference:** Added admin-origin latent user DoS guidance — severity is based
  on who is blocked (users/liquidators), not who created the configuration

### Impact
This release ensures the auditor will:
1. Always open and inspect fixed-point math helper internals (not just calling code)
2. Derive concrete overflow bounds using production token decimals and time units
3. Check checkpoint ordering in every accumulator update function
4. Trace all entry points through stuck accumulators (including admin cancel/close)
5. Never dismiss a finding as "admin-only" when users are the actual victims
6. Complete a Recoverability Matrix before assigning DoS severity

---

## [3.0.0] — 2026-03-14

### Added
- **SUI-28:** PTB Repeated Call Limit Bypass — close factor, rate limits, cooldowns bypassed via multi-call PTBs
- **DEFI-83:** Close Factor Cumulative Enforcement — per-transaction vs per-call limit tracking
- **DEFI-84:** Admin Config Update Resets Embedded Runtime State — limiters, accumulators destroyed by config writes
- **DESIGN-L1 caveat:** Missing EMA-spot divergence tolerance in liquidation path
- **DEFI-54 enhancement:** Sui PTB amplification note for partial liquidation bypass
- **Phase 2 Perspective 4:** Symmetry Checker (deposit/withdraw, borrow/repay, mint/burn, trigger/seize)
- **Phase 5 pairs 5-8:** New mandatory cross-module interaction checks
- **Known False Positive Patterns:** 5 patterns that appear vulnerable but are commonly intentional
- **Quick Maturity Assessment:** Adapted from Trail of Bits Code Maturity Framework
- **APT-24:** Unchecked Signer Parameter — `&signer` accepted without `signer::address_of` authorization check
- **Phase 1 Entry Point Classification:** Sui vs Aptos visibility table showing that ALL `public fun` are PTB-callable on Sui
- **Phase 1 Access Control Classification:** Heuristic for classifying entry points by access tier (Public/Owner/Role/Review Required)
- Trail of Bits methodology integration: asymmetry detection, secure-by-default checks, entry-point-analyzer heuristics

### Changed
- Phase 1 now includes Entry Point Classification table, Access Control Classification heuristic, and Quick Maturity Assessment
- Phase 2 Perspective 1 (Attacker) now includes unchecked `&signer` scan (Aptos) and PTB-composability check (Sui)
- Phase 2 now has 4 perspectives (added Symmetry Checker)
- DEFI-54 now includes Sui PTB amplification guidance
- DESIGN-L1 now includes caveat about missing tolerance checks

### Benchmark
- v2.3.0 found 2/6 known CurrentSUI bugs
- v3.0.0 target: 4/6 known bugs + 2 novel bugs (close factor bypass, limiter reset) that v2.3.0 missed

---

## [2.3.0] — 2026-03-10

### False Positive Reduction — Benchmarking-driven verification improvements

Based on live benchmarking results that identified systematic false positive patterns,
this release strengthens the verification and triage phase with concrete kill mechanisms.

**SKILL.md — Phase 5 verification overhaul:**
- Enhanced Dimension 5 (Precondition Feasibility) with **Invariant Reachability Check**:
  trace every precondition back to constructors/setters to verify the required state is
  actually achievable on-chain
- Added **Dimension 8 — Counterfactual Fix Test**: apply the recommended fix mentally and
  verify it actually changes observable behavior. "Same outcome, different error code" is
  not a vulnerability
- Added **Mandatory Kill Questions** (Step 4): 5 concrete questions every VALID finding must
  answer — precondition construction, fix impact, established pattern check, victim/dollar
  quantification
- Added **Root-Cause Deduplication** (Step 5): group findings by the single line of code that
  would need to change, not by downstream effect
- Added **Dead Code / Unreachable Branch Detection** to Phase 3: verify code branches are
  reachable before recording findings; TODO comments are aspirational, not current bugs

**New file — `defi/defi-lending-design-patterns.md`:**
- DESIGN-L1: Spot prices for liquidation seize, EMA for eligibility (Compound/Aave standard)
- DESIGN-L2: Flash loan not updating accounting fields (hot potato guarantees correctness)
- DESIGN-L3: Blocking borrows when cash < reserve (protective, not DoS)
- DESIGN-L4: Asymmetric EMA/spot divergence formulas (intentional risk asymmetry)

**`defi/defi-liquidation.md` — Liquidation Economics Validation:**
- New section requiring economic viability analysis before reporting liquidation findings
- If the recommended fix makes liquidation unprofitable → the fix causes bad debt → worse
  than the "bug"

---

## [2.2.0] — 2026-03-09

### Expanded Pattern Coverage — 11 new patterns from community research

Integrated high-value patterns from [forefy/MOVE-CHECKS.md](https://github.com/forefy/.context/blob/main/skills/smart-contract-audit/MOVE-CHECKS.md),
deduplicated against existing checks, and placed in the correct chain-specific files.

**common-move.md** — 4 new chain-agnostic patterns:
- 7.4 Incomplete Pause Coverage — pause flag not checked on all public functions
- 7.5 Unpinned Dependencies in Move.toml — supply chain risk from unversioned git deps
- 9.4 Self-Transfer Snapshot Manipulation — self-transfer games fee/reward snapshots
- 9.5 Round-Trip Profitability — `withdraw(deposit(X)) <= X` invariant test

**sui-patterns.md** — 5 new Sui-specific patterns (SUI-23 to SUI-27):
- SUI-23: Shared Object Version Check (upgrade safety)
- SUI-24: Publisher Object Not Secured (Display/royalty spoofing)
- SUI-25: Dynamic Field Cleanup Before Object Deletion (orphaned fund loss)
- SUI-26: Kiosk Transfer Policy Bypass (royalty evasion)
- SUI-27: UpgradeCap Lifecycle Mismanagement (premature immutability / overly permissive policy)

**aptos-patterns.md** — 2 new Aptos-specific patterns (APT-22 to APT-23):
- APT-22: Struct Layout Change on Upgrade (binary deserialization failure)
- APT-23: Resource Account Signer Scope Creep (cross-module resource manipulation)

All verification checklists updated with corresponding new items.

---

## [2.1.0] — 2026-03-09

### Move-Expert Verify & Triage Phase

Added **Phase 5 — Verify & Triage** between vulnerability scanning and report output.
Every candidate finding must now survive a Move-expert validation pass before inclusion.

- **Dual Narrative Test:** Each finding requires a concrete Legitimate User Story vs
  Attacker Story with specific Move function calls, object/resource interactions, and
  quantified outcomes — vague findings are rejected
- **Move-Expert Disproof (7 Dimensions):** Challenges each finding against Move's type
  system & linearity, call path completeness, object/resource model (Sui ownership vs
  Aptos acquires), execution model reality (no delegatecall/callbacks), precondition
  feasibility (consensus ordering, gas costs), economic rationality, and existing protections
- **Finding Labels:** VALID, QUESTIONABLE, DISMISSED, OVERCLASSIFIED — only VALID and
  QUESTIONABLE findings reach the final report
- **Report format updated:** Added Triage Summary, Confidence field, Verification
  reasoning per finding, DISMISSED findings documented in Verified Clean Checks
- Previous Phase 5 (Report) renumbered to Phase 6
- Version bumped to 2.1.0

---

## [2.0.0] — 2026-03-09

### DeFi Deep-Dive — 69 new vulnerability patterns (DEFI-11 to DEFI-79)

Added 8 DeFi subcategory reference files under `defi/`, adapted from best-in-class
Solidity audit patterns and fully rewritten for Move (Sui & Aptos). Each pattern
includes vulnerable code, safe code, and auditor check instructions.

**defi/defi-staking.md** — 6 patterns (DEFI-11 to DEFI-16):
- First depositor share theft, reward dilution via direct transfer, precision loss
  in reward accumulators, flash deposit/withdraw griefing, stale reward index,
  balance caching mismatch

**defi/defi-oracle.md** — 8 patterns (DEFI-17 to DEFI-24):
- Stale price data (Pyth/Switchboard), same staleness threshold, decimal/exponent
  mismatch, wrong feed ID, depeg events, min/max price bounds, price direction
  confusion, missing circuit breakers

**defi/defi-lending.md** — 10 patterns (DEFI-25 to DEFI-34):
- Premature liquidation, collateral manipulation, loan closure without repayment,
  asymmetric pause, token denylist blocking repayment, no grace period, incorrect
  liquidation share, dust positions, forced debt, refinancing manipulation

**defi/defi-math-precision.md** — 8 patterns (DEFI-35 to DEFI-42):
- Division before multiplication, rounding to zero, decimal mismatch between tokens,
  unsafe u128→u64 downcasting, wrong rounding direction, inverted oracle pairs,
  time unit confusion (Sui ms vs Aptos seconds), exponentiation precision loss

**defi/defi-slippage.md** — 7 patterns (DEFI-43 to DEFI-49):
- Zero/missing min_amount_out, no deadline, hardcoded slippage, on-chain
  self-referential slippage, LP operation slippage, token vs USD confusion,
  PTB composability sandwich (Sui-specific)

**defi/defi-liquidation.md** — 17 patterns (DEFI-50 to DEFI-66):
- Incentive & mechanism (no incentive, small positions, collateral withdrawal,
  bad debt, partial bypass), calculation errors (decimals, fees, yield, swap fees,
  oracle sandwich), DoS vectors (unbounded loops, front-running, pending withdrawal,
  token freeze), fairness (grace period, post-liquidation health, no slippage)

**defi/defi-auction-clm.md** — 7 patterns (DEFI-67 to DEFI-73):
- Self-bidding timer reset, insufficient auction length, off-by-one seizure,
  missing TWAP on rebalance, TWAP parameter manipulation, stuck tokens from
  tick math rounding, retrospective fee application

**defi/defi-signatures.md** — 6 patterns (DEFI-74 to DEFI-79):
- Nonce replay, cross-chain replay (Sui↔Aptos), missing parameters in signed
  message, no expiration, unchecked verification return value, secp256k1
  signature malleability

### Skill infrastructure updates

- **SKILL.md**: Expanded reference table with 8 conditional-load DeFi files, updated
  Phase 1 (subcategory detection) and Phase 4 (subcategory loading), bumped to v2.0.0
- **defi-vectors.md**: Added DeFi Subcategory Detection Table, expanded verification
  checklist from 10 to 20 items with cross-references to new patterns
- **CLAUDE.md**: Updated contribution rules for `defi/` subdirectory
- **CONTRIBUTING.md**: Added DeFi subcategory file guide and next-ID tracking

---

## [1.0.0] — 2025-03-05

### Initial release

**SKILL.md**
- Auto-activation on `.move` files (Sui and Aptos detection)
- 5-phase audit workflow: Assessment → Multi-Perspective → Scan → DeFi → Report
- Structured report format with severity table, PoC scenarios, and fix recommendations
- Severity framework: Critical / High / Medium / Low / Info with Likelihood × Impact criteria

**common-move.md**
- Access control checks (SIG-01 to SIG-04): missing capability gates, copy-ability on caps, hardcoded addresses, two-step transfer
- Arithmetic checks: overflow DoS, division-before-multiplication, div-by-zero, cast truncation
- Resource safety: leaks, unauthorized extraction, double-spend via phantom resources
- Logic invariants: missing assertions, comparison operator bugs, state machine violations, timestamp manipulation
- Input validation: zero-value, address, vector bounds
- Cross-module safety: reentrancy, unvalidated returns, upgradeable dependencies
- Upgradeability: single-key authority, reinitialization, missing pause

**sui-patterns.md** — 10 checks (SUI-01 to SUI-10):
- Object ownership confusion
- Shared object reentrancy / PTB state inconsistency
- Witness pattern abuse (OTW copy-ability)
- Transfer to wrong owner
- Wrapping/unwrapping attacks
- Dynamic field injection
- Clock/epoch oracle manipulation
- Capability object theft/forgery
- Hot potato misuse
- Event spoofing

**aptos-patterns.md** — 11 checks (APT-01 to APT-11):
- Missing/incorrect `acquires` annotations
- Resource account privilege escalation
- Coin type confusion / generic type whitelist bypass
- Signer capability abuse
- Table/iterable table safety
- Timestamp oracle manipulation
- Event handle exhaustion / missing events
- Module upgrade safety
- FungibleAsset vs Coin framework mixing
- Unbounded vector / smart_vector growth
- `#[view]` function side effects

**defi-vectors.md** — 10 DeFi checks (DEFI-01 to DEFI-10):
- Oracle manipulation (spot price, TWAP, staleness)
- Flash loan attack surface
- Liquidity pool manipulation (first depositor, rounding, precision)
- Loan/borrow invariants
- Reward/yield calculation errors
- Liquidation mechanism abuse
- Slippage and front-running
- Interest rate model safety
- Governance/timelock bypass
- Bridge/cross-chain patterns

**sample-finding.md**
- Full example audit output with Critical and High findings, PoC scenarios, and fixes

## CLAUDE.md

# CLAUDE.md

Instructions for Claude when working inside the `move-auditor` repository.

---

## What this repo is

This is a Claude Code skill for auditing Move smart contracts on Sui and Aptos.
The skill lives in `move-auditor/` and is installed by copying that directory to
`~/.claude/commands/move-auditor`.

---

## When contributing checks or patterns

1. **New common checks** go in `move-auditor/common-move.md`
2. **Sui-specific checks** go in `move-auditor/sui-patterns.md` — numbered `SUI-XX`
3. **Aptos-specific checks** go in `move-auditor/aptos-patterns.md` — numbered `APT-XX`
4. **DeFi cross-cutting checks** go in `move-auditor/defi-vectors.md` — DEFI-01 to DEFI-10
5. **DeFi subcategory checks** go in `move-auditor/defi/defi-<category>.md` — DEFI-11+
   - `defi-staking.md` (DEFI-11–16), `defi-oracle.md` (DEFI-17–24)
   - `defi-lending.md` (DEFI-25–34), `defi-math-precision.md` (DEFI-35–42)
   - `defi-slippage.md` (DEFI-43–49), `defi-liquidation.md` (DEFI-50–66)
   - `defi-auction-clm.md` (DEFI-67–73), `defi-signatures.md` (DEFI-74–79)
   - `defi-lending-design-patterns.md` (DESIGN-L1–L4, known-good patterns)
   - Next available ID: **DEFI-88**
6. **Anti-FP / verification files** are at top-level:
   - `move-fp-catalog.md` — Always loaded; FP patterns and rationalizations to reject
   - `evidence-chains.md` — Phase 7; structured evidence templates
   - `confidence-gates.md` — Phase 7; confidence gating and hard evidence requirements
7. **New reference files** (e.g., a vulnerability database) go in `move-auditor/`
   and must be referenced from `SKILL.md` with a load instruction

---

## Version tagging

Releases are tagged as `move-auditor@X.Y.Z` matching `metadata.version` in `SKILL.md`.

Before tagging a release:
1. Update `metadata.version` in `move-auditor/SKILL.md`
2. Add a changelog entry in `CHANGELOG.md`
3. Tag: `git tag move-auditor@X.Y.Z && git push --tags`

---

## File size limits

- `SKILL.md` must stay under 500 lines (this is the always-loaded context)
- Reference files can be longer — they are loaded on demand
- If a reference file exceeds ~400 lines, split into sub-files

---

## Do not

- Add Solidity-specific, EVM-specific, or Rust/Anchor-specific content
- Add placeholders that require manual editing before use
- Add checks without a code example showing vulnerable and safe patterns

## CONTRIBUTING.md

# Contributing

Contributions are welcome — new vulnerability checks, real-world findings, improved patterns,
and chain-specific updates all help make this skill more useful for the Move security community.

---

## Adding a new check

1. Fork the repo and create a branch: `git checkout -b add/SUI-11-xyz`
2. Determine the correct file for your check:
   - **Common Move checks** → `common-move.md`
   - **Sui-specific** → `sui-patterns.md` (SUI-XX)
   - **Aptos-specific** → `aptos-patterns.md` (APT-XX)
   - **DeFi cross-cutting** → `defi-vectors.md` (DEFI-01 to DEFI-10)
   - **DeFi subcategory** → `defi/defi-<category>.md` (DEFI-11+, see table below)
3. Add your check with:
   - A numbered ID (next available: SUI-43, APT-25, DEFI-88)
   - Vulnerable code pattern (with comment `// VULNERABLE`)
   - Safe code pattern (with comment `// SAFE`)
   - Risk description and attack scenario
   - Check instructions for the auditor
4. Add the check to the verification checklist at the bottom of the file
5. If based on a real finding: link to the report or contest submission
6. Open a PR with a clear description of what the check catches and why it matters

### DeFi subcategory file guide

| Category | File | Current IDs |
|----------|------|-------------|
| Staking / Yield | `defi/defi-staking.md` | DEFI-11 to DEFI-16 |
| Oracle | `defi/defi-oracle.md` | DEFI-17 to DEFI-24 |
| Lending / Borrowing | `defi/defi-lending.md` | DEFI-25 to DEFI-34, DEFI-80, DEFI-82, DEFI-84 |
| Math / Precision | `defi/defi-math-precision.md` | DEFI-35 to DEFI-42, DEFI-85 to DEFI-87 |
| Slippage / MEV | `defi/defi-slippage.md` | DEFI-43 to DEFI-49 |
| Liquidation | `defi/defi-liquidation.md` | DEFI-50 to DEFI-66, DEFI-81, DEFI-83 |
| Auction / CLM | `defi/defi-auction-clm.md` | DEFI-67 to DEFI-73 |
| Signatures | `defi/defi-signatures.md` | DEFI-74 to DEFI-79 |
| Lending Design | `defi/defi-lending-design-patterns.md` | DESIGN-L1 to DESIGN-L4 |

New DeFi checks should use the next sequential ID (DEFI-88+) and go in the matching subcategory file. If no subcategory fits, create a new `defi/defi-<category>.md` file and register it in `SKILL.md` and `defi-vectors.md`.

---

## Adding a real-world finding

Found a Move vulnerability in a public audit report or contest? Add it to the vulnerability database
(coming soon: `references/vuln-db.md`). Format:

```markdown
### [VULN-NNN] Finding Title
**Source:** Contest/protocol name, date, link
**Chain:** Sui / Aptos
**Category:** Access Control / Arithmetic / etc.
**Summary:** One-paragraph description of the bug
**Pattern:** Code snippet showing the vulnerable pattern
**Fix:** What was done to fix it
```

---

## PR checklist

- [ ] Check is in the correct reference file
- [ ] Check has a numbered ID
- [ ] Vulnerable and safe code examples included
- [ ] Added to verification checklist
- [ ] `SKILL.md` line count still under 500
- [ ] If adding a new reference file: referenced from `SKILL.md`

---

## License

By contributing, you agree your contributions are licensed under MIT.

## README.md

<p align="center">
  <strong>move-auditor</strong><br>
  <em>Claude Code skill for Move smart contract security auditing</em>
</p>

<p align="center">
  <a href="https://opensource.org/license/mit/"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
  <a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg" alt="Contributions Welcome"></a>
  <img src="https://img.shields.io/badge/version-3.6.1-blue.svg" alt="Version 3.6.1">
  <img src="https://img.shields.io/badge/patterns-180%2B-red.svg" alt="180+ Patterns">
  <img src="https://img.shields.io/badge/chains-Sui%20%7C%20Aptos-purple.svg" alt="Sui | Aptos">
</p>

<p align="center">
  Built by <a href="https://x.com/thepantherplus">Panther</a>
</p>

---

A skill you plug into [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that turns it into a Move (Sui & Aptos) smart contract security auditor — battle-tested vulnerability patterns drawn from real-world exploits, ready to hunt bugs the moment you open a `.move` file.

**Read the full write-up:** [The Move Auditor — Blog Post](https://pantheraudits.com/blog/the-move-auditor.html)

---

## Features

- **180+ vulnerability patterns** across chain-agnostic, Sui-specific, Aptos-specific, and DeFi checks
- **Auto-activates** on `.move` files — no setup, no slash commands needed
- **8-phase audit workflow** — from codebase mapping to verified, triaged report
- **Anti-false-positive engine** — confidence gating, evidence chains, FP catalog, and self-hallucination checks
- **Build & test log analysis** — catches arithmetic aborts, assertion failures, and `#[expected_failure]` anomalies
- **Signal-based coverage routing** — detects protocol type and loads only relevant patterns
- **DeFi deep-dive** — 87 patterns covering staking, oracles, lending, liquidation, slippage, auctions, and signatures
- **Semantic gap detection** — stale state, accumulator drift, cross-module accounting desync
- **Real-world validated** — findings accepted into production codebases (see below)

---

## Install

```bash
git clone https://github.com/pantheraudits/move-auditor.git
mkdir -p ~/.claude/commands
cp -r move-auditor ~/.claude/commands/move-auditor
```

**Update to latest:**
```bash
cd move-auditor && git pull
cp -r . ~/.claude/commands/move-auditor
```

---

## Usage

> `/move-auditor` is a slash command inside Claude Code — not a terminal command.
> Run it from within a Claude Code session.

### Quick start

```bash
# 1. Navigate to your Move project
cd /path/to/your-move-project

# 2. Start Claude Code
claude

# 3. Inside the session, run:
/move-auditor              # Full audit of all .move files in scope
/move-auditor [file]       # Audit a specific file
```

### Best results

For the deepest analysis, run the skill against a **buildable project** — one where
`sui move build` (Sui) or `aptos move compile` (Aptos) succeeds. The auditor will run
the test suite, capture logs, and analyze them for arithmetic aborts, assertion failures,
and suspicious `#[expected_failure]` annotations that may indicate latent High/Critical
bugs invisible to static-only review.

> **Static-only mode:** If the project doesn't build (missing deps, partial code, review-only
> context), the skill still runs the full pattern-based audit — it just skips test log analysis.

---

## How It Works

The skill runs an **8-phase pipeline** on every audit:

```
Phase 1  Detect chain, map codebase, classify entry points, build coverage plan
     |
Phase 2  Multi-perspective review (Attacker, Designer, Integrator, Symmetry,
         Bidirectional Admin, Consistency)
     |
Phase 3  Structured vulnerability scan — every check in every loaded reference file
     |
Phase 4  DeFi & protocol-specific deep-dive (87 subcategory patterns)
     |
Phase 5  Semantic gap & stale-state scan (accumulators, checkpoints, cross-module drift)
     |
Phase 6  Cross-module interaction scan (9 mandatory interaction pairs)
     |
Phase 7  Verify & triage — Move-expert validation, dual narrative test, 8-dimension
         disproof, kill questions, evidence chains, confidence gating
     |
Phase 8  Structured audit report with severity, confidence, PoC, and fix
```

Reference files are loaded **on demand** — the agent reads only what's relevant to the
detected chain and protocol type, keeping the context window lean.

---

## Pattern Coverage

| Category | File | Patterns |
|----------|------|----------|
| Chain-agnostic | `common-move.md` | Access control, arithmetic, resource safety, logic, input validation, cross-module, upgradeability, build/test analysis |
| Sui-specific | `sui-patterns.md` | SUI-01 to SUI-44 |
| Aptos-specific | `aptos-patterns.md` | APT-01 to APT-25 |
| DeFi cross-cutting | `defi-vectors.md` | DEFI-01 to DEFI-10 |
| Staking & yield | `defi/defi-staking.md` | DEFI-11 to DEFI-16 |
| Oracles | `defi/defi-oracle.md` | DEFI-17 to DEFI-24 |
| Lending & borrowing | `defi/defi-lending.md` | DEFI-25 to DEFI-34, DEFI-80, DEFI-82, DEFI-84 |
| Math & precision | `defi/defi-math-precision.md` | DEFI-35 to DEFI-42, DEFI-85 to DEFI-87 |
| Slippage & MEV | `defi/defi-slippage.md` | DEFI-43 to DEFI-49 |
| Liquidation | `defi/defi-liquidation.md` | DEFI-50 to DEFI-66, DEFI-81, DEFI-83 |
| Auctions & CLM | `defi/defi-auction-clm.md` | DEFI-67 to DEFI-73 |
| Signatures | `defi/defi-signatures.md` | DEFI-74 to DEFI-79 |

---

## Skill Structure

```
move-auditor/
├── SKILL.md                          # Orchestrator — 8-phase workflow, coverage routing
│
├── common-move.md                    # Chain-agnostic checks + verification checklist
├── sui-patterns.md                   # Sui-specific patterns (SUI-01 to SUI-44)
├── aptos-patterns.md                 # Aptos-specific patterns (APT-01 to APT-25)
│
├── checklist-router.md               # Signal-based coverage planner & file router
├── verification-policy.md            # Evidence hierarchy, feasibility gates, severity discipline
├── semantic-gap-checks.md            # Stale-state, accumulator, cross-module desync checks
│
├── move-fp-catalog.md                # Anti-FP: rationalizations to reject, FP catalog
├── evidence-chains.md                # Structured evidence templates (Phase 7)
├── confidence-gates.md               # Confidence gating, hard evidence requirements (Phase 7)
│
├── defi-vectors.md                   # DeFi attack vectors (DEFI-01 to DEFI-10) + router
├── defi/
│   ├── defi-staking.md               # Staking/yield (DEFI-11 to DEFI-16)
│   ├── defi-oracle.md                # Oracles (DEFI-17 to DEFI-24)
│   ├── defi-lending.md               # Lending/borrowing (DEFI-25 to DEFI-34, 80, 82, 84)
│   ├── defi-math-precision.md        # Math & precision (DEFI-35 to DEFI-42, 85-87)
│   ├── defi-slippage.md              # Slippage & DEX (DEFI-43 to DEFI-49)
│   ├── defi-liquidation.md           # Liquidation (DEFI-50 to DEFI-66, 81, 83)
│   ├── defi-auction-clm.md           # Auctions & CLM (DEFI-67 to DEFI-73)
│   ├── defi-signatures.md            # Signatures (DEFI-74 to DEFI-79)
│   └── defi-lending-design-patterns.md  # Known-good patterns (DESIGN-L1 to L4)
│
├── audit-prompts.md                  # Deep-dive prompts & vulnerability pattern pack
├── sample-finding.md                 # Example audit output format
│
└── benchmarks/
    ├── BENCHMARK.md                  # Benchmarking methodology
    ├── BENCHMARK-openzeppelin.md     # OpenZeppelin contracts-sui benchmark
    └── BENCHMARK-currensui.md        # CurrenSui lending protocol benchmark
```

---

## Real-World Impact

Bugs found by `move-auditor` have been accepted into production codebases, contest leaderboards, and paid bug bounties. In every case the skill surfaced the *candidate* finding — a human auditor reproduced, narrowed, and wrote up the bug before submission.

| Context | Finding | Outcome |
|---------|---------|---------|
| [Current Finance](https://audits.sherlock.xyz/contests/current-finance) — Sherlock contest, Sui Move lending protocol | 1 High + 2 Medium confirmed findings: opposite-direction EMA/spot deviations creating unliquidatable positions, ADL using reserve-level instead of emode-group-level debt, deposit cap double-subtraction bypass. Identified with `move-auditor`, manually verified by [Panther](https://x.com/thepantherplus). | **#27 out of 170+ participants** |
| [OpenZeppelin Contracts for Sui](https://github.com/OpenZeppelin/contracts-sui) | Missing `EDivideByZero` guard in fixed-point `div`/`mod` — relied on opaque VM abort instead of descriptive error | [PR #263](https://github.com/OpenZeppelin/contracts-sui/pull/263) **Merged** |
| Aptos perps protocol (private bug bounty, name withheld) | Candidate High-severity finding (originally triaged as Critical, downgraded to High by the program) plus 1 confirmed Medium already paid. Additional High and Medium findings accepted as valid and in triage. Surfaced with `move-auditor`, reproduced and written up manually by [Panther](https://x.com/thepantherplus). | **20,000 USDC (1 High) + 1 Medium paid** — further awards pending triage |
| Sui DeFi margin protocol (bug bounty, name withheld) | Missing post-trade health check in margin trading proxy — leveraged accounts can keep trading after becoming liquidatable, enabling value extraction to a second account and leaving bad debt for lenders | **Confirmed** (duplicate of prior report) |

> The OpenZeppelin find was a unique result from [benchmarking](benchmarks/BENCHMARK-openzeppelin.md) — no other AI audit tool (MAIA, Raw Claude CLI) caught it.
>
> **How to read this table**: `move-auditor` is a *candidate generator*, not a proof system. Each row represents a bug a human auditor reproduced, triaged, and submitted. The skill narrows where to look; the auditor still does the reading, the PoC, and the write-up.

---

## Benchmarks

The skill is [benchmarked](benchmarks/BENCHMARK.md) against baseline prompts (raw Claude, MAIA) and manual review to measure where it actually makes a difference. Benchmark results drove multiple improvements:

- **v2.3.0 → v3.0.0**: CurrenSui detection improved from 2/6 to 4/6 known bugs + 2 novel findings
- **v3.4.0**: Anti-FP overhaul reduced false positive rate after [CurrenSui benchmark](benchmarks/BENCHMARK-currensui.md) revealed ~25% FP rate
- **v3.5.0**: 16 new Sui patterns from design-level anti-pattern analysis
- **v3.6.x**: Patterns validated against Current Finance contest — 1 High + 2 Medium confirmed, #27 placement

---

## Roadmap

- [ ] Vulnerability database (real-world Move CVEs and contest findings)
- [ ] Sui DeFi protocol-specific patterns (Cetus, Aftermath, Turbos)
- [ ] Aptos DeFi protocol-specific patterns (Thala, Aries, Echelon)
- [ ] Automated grep patterns for common Move anti-patterns
- [ ] Machine-readable audit artifacts (`coverage-plan`, validated findings, structured clean checks)
- [ ] Report templates for private audits vs. contest submissions
- [x] Benchmarking against baseline prompts and manual review

---

## Disclaimer

AI-assisted audit output **must be manually verified**. This skill accelerates your workflow — it does not replace deep manual review and PoC testing. All findings require human confirmation before being included in any report.

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

---

## Contact

Panther Audits — [GitHub](https://github.com/pantheraudits) · [Telegram](https://t.me/theblackpantherhere) · [X](https://x.com/thepantherplus)

## SKILL.md

---
name: move-auditor
description: Audits Move contracts (Sui & Aptos) for security bugs.
metadata:
  version: "3.6.1"
  author: pantheraudits
  category: security
  tags:
    - move
    - sui
    - aptos
    - smart-contract-audit
    - web3-security
---

# Move Auditor

> Fast, systematic security feedback on Move smart contracts (Sui & Aptos).
> Activates automatically on `.move` files — no setup, no copy-pasting.

---

## Activation

This skill activates whenever:
- `.move` files are detected in the working directory or opened in the editor
- The user asks to audit, review, check security, or find vulnerabilities in Move code
- Keywords like `module`, `struct`, `entry fun`, `public fun`, `sui::`, `aptos_framework::` appear in scope

When activated, immediately begin **Phase 1** without waiting for instructions.

## When NOT to Use

- Non-Move contracts (Solidity, Rust/Anchor, EVM, TEAL, FunC)
- General code review for style, performance, or refactoring
- Writing Move code, generating patches, or fixing bugs
- When user explicitly requests a quick scan without full verification

---

## Reference Files

All reference files are in the **same directory as this SKILL.md**.
When the instructions below say "read `filename.md`", use the Read tool on the
file in this skill's directory (e.g., if SKILL.md is at
`~/.claude/commands/move-auditor/SKILL.md`, read
`~/.claude/commands/move-auditor/common-move.md`).

| File | When to load |
|------|-------------|
| `common-move.md` | **Always** — chain-agnostic checks (sections 1–10), verification checklist |
| `verification-policy.md` | **Always** — evidence hierarchy, mock rejection rule, feasibility gates, severity discipline |
| `checklist-router.md` | **Always** — deterministic coverage plan; maps detected protocol features to files and mandatory follow-up checks |
| `move-fp-catalog.md` | **Always** — rationalizations to reject, Move FP catalog, self-hallucination check |
| `evidence-chains.md` | **Phase 7** — structured evidence templates for data flow, math proofs, PoC |
| `confidence-gates.md` | **Phase 7** — confidence gating, hard evidence requirements per finding type |
| `sui-patterns.md` | When chain is **Sui** (imports `sui::object`, `sui::transfer`, etc.) — SUI-01 to SUI-44 |
| `aptos-patterns.md` | When chain is **Aptos** (imports `aptos_framework`, `aptos_std`, etc.) — APT-01 to APT-25 |
| `defi-vectors.md` | When protocol involves tokens, swaps, lending, staking, or oracles — DEFI-01 to DEFI-10 + subcategory router |
| `semantic-gap-checks.md` | When the protocol has accumulators, checkpoints, rewards, lending state, cross-module accounting, or multi-step state transitions |
| `defi/defi-staking.md` | When staking/yield detected (`stake`, `unstake`, `reward_per_share`, `accumulator`) — DEFI-11 to DEFI-16 |
| `defi/defi-oracle.md` | When oracle usage detected (`get_price`, `oracle`, `pyth`, `switchboard`, `price_feed`) — DEFI-17 to DEFI-24 |
| `defi/defi-lending.md` | When lending/borrowing detected (`borrow`, `repay`, `collateral`, `health_factor`) — DEFI-25 to DEFI-34, DEFI-80, DEFI-82, DEFI-84 |
| `defi/defi-math-precision.md` | When complex financial math detected (`PRECISION`, `DECIMAL`, `float`, `Decimal`, `WAD`, fee/share math) OR when reward/accumulator/liquidity_mining patterns detected — DEFI-35 to DEFI-42, DEFI-85 to DEFI-87 |
| `defi/defi-slippage.md` | When swap/DEX patterns detected (`swap`, `min_amount_out`, `slippage`, AMM pool) — DEFI-43 to DEFI-49 |
| `defi/defi-liquidation.md` | When liquidation mechanisms detected (`liquidat`, `seize`, `bad_debt`, `insurance`) — DEFI-50 to DEFI-66, DEFI-81, DEFI-83 |
| `defi/defi-auction-clm.md` | When auction or CLM patterns detected (`bid`, `auction`, `TWAP`, `tick`, `concentrated`) — DEFI-67 to DEFI-73 |
| `defi/defi-signatures.md` | When signature verification detected (`ed25519`, `secp256k1`, `verify_signature`, `nonce`) — DEFI-74 to DEFI-79 |
| `defi/defi-lending-design-patterns.md` | When lending/borrowing detected — known-good patterns (DESIGN-L1 to L4) that should NOT be reported as bugs |
| `audit-prompts.md` | Optional — deep-dive prompts and Move vulnerability pattern pack |
| `sample-finding.md` | Reference for output format — do not load during audits |

---

## Auditor Mindset

You are a senior Move security researcher. Find real, exploitable vulnerabilities — not theoretical ones. Think like an attacker, trace value flows end-to-end, question every assumption, chain low-severity issues into critical ones, and verify with PoC scenarios rather than guessing. Consult `move-fp-catalog.md` to avoid common false positives.

---

## Workflow

### Phase 1 — Initial Assessment (auto-run on activation)

**Detect chain and framework:**
- Presence of `sui::object`, `sui::transfer`, `sui::tx_context` → **Sui Move**
- Presence of `aptos_framework`, `aptos_std`, `#[test_only]` → **Aptos Move**
- Load reference files from the skill directory (see Reference Files table above):
  - **Always** → read `common-move.md`
  - **Always** → read `verification-policy.md`
  - **Always** → read `checklist-router.md`
  - **Always** → read `move-fp-catalog.md`
  - **Sui** → also read `sui-patterns.md`
  - **Aptos** → also read `aptos-patterns.md`
  - **DeFi protocols** → also read `defi-vectors.md`, then check the subcategory detection
    table inside it and load relevant `defi/*.md` files (multiple may apply — e.g., a lending
    protocol should load `defi-lending.md`, `defi-liquidation.md`, and `defi-oracle.md`)
  - **Accumulator / checkpoint / rewards / cross-module accounting signals** (grep: `last_update`, `checkpoint`, `cumulative`, `reward_manager`, `pool_reward`, `liquidity_mining`, `accumulated`) → also read `semantic-gap-checks.md` AND `defi/defi-math-precision.md` (for DEFI-85/86/87)

**Map the codebase:**
```
- List all modules
- List all public/entry functions (these are the attack surface — see table below)
- List all structs with key/store abilities (persistent state)
- List all capability types (objects that grant permissions)
- Identify any admin/owner patterns
- Identify any cross-module calls
- Estimate complexity: LoC, number of entry points, external dependencies
```

**Coverage Plan (mandatory):**
Use `checklist-router.md` to derive a coverage plan listing: detected chain, protocol families, feature flags, reference files loaded, and required follow-up passes. If a route fires, load the file.

**Entry Point Classification:**
Attack surface differs by chain:

| Visibility | Sui (PTB-callable?) | Aptos (tx entry?) |
|------------|---------------------|-------------------|
| `public entry fun` | Yes — PTB + direct tx | Yes — transaction entry |
| `public fun` | **Yes — PTB-callable!** | **No** — module-callable only |
| `entry fun` | Yes — direct tx only | Yes — transaction only |
| `public(package) fun` | No — package-internal | No — package-internal |
| `fun` (private) | No | No |

**Critical Sui distinction:** ALL `public fun` on Sui are PTB-callable, making Sui's attack surface larger than Aptos.

**Access Control Classification — for each entry point:**
- **Sui:** Owned object with "Cap" in name → specific role; Owned object without "Cap" → Owner-gated; Shared object parameter with no cap → **Public/Unrestricted**
- **Aptos:** `signer::address_of` compared to stored address → Role-based; `exists<*Cap>(addr)` → Capability-based; `&signer` with NO address check → **Review Required** (see APT-24)
- Any function classified as Public/Unrestricted that mutates state → highest audit priority

**Build Detection & Test Log Analysis (conditional):**
Check if the project builds (`Move.toml` + `sui move build` or `aptos move compile`). If build succeeds (`BUILD_AVAILABLE = true`), run Test Log Analysis (common-move.md Section 13) to detect arithmetic aborts, assertion failures, and runtime anomalies. If build fails, note errors and skip.

**Output a one-paragraph codebase summary** (include build status) before proceeding.

---

### Phase 2 — Multi-Perspective Review

**Perspective 1 — The Attacker**
For each entry function:
- What inputs does it accept without validation?
- Can I pass an object I don't own?
- Can I bypass any `assert!` by constructing a specific state?
- Can I call this in a sequence that wasn't intended?
- (Aptos) Does any `public entry fun` accept `&signer` without ever calling `signer::address_of` for authorization? → APT-24
- (Sui) Is this a `public fun` (not just `entry`)? If so, it's PTB-composable — can it be chained with other calls to bypass per-call limits or create unexpected state?

**Perspective 2 — The Protocol Designer**
- What invariants does this protocol rely on?
- Which of those invariants are enforced on-chain vs. assumed off-chain?
- What happens if those assumptions break?

**Perspective 3 — The Integrator**
- If another protocol calls into this one, what can go wrong?
- Are there flash loan vectors?
- Can object references be reused or replayed across transactions?

**Perspective 4 — The Symmetry Checker**
For every pair of inverse operations, verify symmetry:
- deposit/withdraw: `withdraw(deposit(X)) <= X` always (rounding favors protocol)
- borrow/repay: `repay(borrow(X)) >= X` always (rounding favors protocol)
- mint/burn: `burn(mint(X)) <= X` always
- liquidation trigger/seize: same price oracle type, or bounded divergence
- rate limit add/reduce: reduce applied to same time segment as add
- admin update: only config changes, runtime state preserved
For each pair, check: (a) rounding direction, (b) state consistency, (c) oracle consistency, (d) access control symmetry

**Perspective 5 — The Bidirectional Admin Checker**
For every admin/privileged function that affects user funds or state, analyze BOTH directions:
- **Direction 1 — Admin harms users:** Can admin confiscate funds, lock positions, retroactively change terms, or brick user operations?
- **Direction 2 — Users grief admin:** Can users block admin cleanup, prevent pool closure, or make admin operations revert?
Both directions must be checked. Finding one does NOT mean the other doesn't exist. A close/reclaim function that checks `counter == 0` may be griefable by users (Direction 2) AND may confiscate from passive users whose entitlement isn't tracked by the counter (Direction 1).

**Perspective 6 — The Consistency Checker**
When a module uses an explicit safety pattern (e.g., `EDivideByZero` guard before division, bounds check on construction params, `assert!(amount > 0)` on inputs), check if sibling modules in the same package follow the same pattern. Inconsistencies are Low.
- Grep for the pattern across all modules in the package
- If Module A guards division with an explicit zero-check but Module B performing the same operation does not → flag the inconsistency
- Also applies to: error code usage, input validation, capability checks, event emission

---

### Phase 3 — Structured Vulnerability Scan

Before starting the per-check scan, confirm the coverage plan from `checklist-router.md`
is complete. If the codebase contains a signal with no corresponding deep check loaded,
fix the plan first.

Work through every check in `common-move.md`, then the chain-specific reference. For each check:

1. Search the codebase for the pattern
2. If found: record location, describe impact, assign severity
3. If clean: note it as verified

**Do not skip checks.** A clean check is still a check — mark it ✅.

**Fixed-Point Library Inspection Gate (MANDATORY — #1 missed critical bug class):**

Before completing Phase 3, you MUST complete ALL steps and output confirmation:

1. **Identify** all math helpers: grep for `float`, `decimal`, `wad`, `ray`, `fixed_point`, `Decimal`, `WAD`, `Float`
2. **Read internals** of each helper's `mul`, `div`, `from` — do NOT assume from name
3. **Derive overflow bound** for `mul(a,b)`: write the intermediate expression, simplify to raw input constraint (e.g., `A * B <= U64_MAX`)
4. **Find all call sites** of `mul()`. For each `A.mul(B)` chain: can `A * B` exceed the bound with realistic values? (token decimals: USDC=6, SUI=9, APT=8). Compute threshold table per DEFI-85
5. **Check checkpoint ordering** for each overflow-reachable site: abort BEFORE or AFTER checkpoint? If BEFORE → Recoverability Matrix (12.1)
6. **Output:** "FIXED-POINT GATE: [N] helpers, [M] call sites, [K] overflow-reachable" — if K > 0, include threshold table + recoverability

**Skipping this gate = missing permanent-deadlock bugs.** See 2.6, 12.1, DEFI-85–87.

**Dead Code / Unreachable Branch Detection:**
Before recording any finding that depends on a specific code branch:
1. **Is the branch reachable?** Trace all callers and all paths that set the condition variable.
   If a guard like `if (!X) { continue }` exists but X is invariantly true due to
   constructor/setter validation, the entire path after the guard is dead code.
2. **TODO comments describe aspirational features, not current bugs.** A TODO saying
   "skip check for non-collateral" doesn't mean non-collateral assets exist — it means
   the developer considered adding support but didn't.
3. **Do not report findings that require executing dead code.**

---

### Phase 4 — DeFi & Protocol-Specific Checks

If the protocol involves tokens, swaps, lending, staking, or oracles:
1. Read `defi-vectors.md` and run cross-cutting DeFi checks (DEFI-01 to DEFI-10)
2. Based on the subcategory detection table in `defi-vectors.md`, read relevant `defi/*.md` files
3. Run all checks from loaded subcategory files
4. Cross-reference DeFi findings with chain-specific patterns (e.g., SUI-02 + DEFI-14 for
   Sui staking flash attacks, APT-21 + DEFI-50 for Aptos liquidation reentrancy, SUI-21 + DEFI-29
   for denylist blocking repayment)

---

### Phase 5 — Semantic Gap & Stale-State Scan

If the protocol has multiple accounting variables, reward indices/accumulators, checkpoints, or lending state across modules — read `semantic-gap-checks.md` and run this phase. Mandatory for lending, staking, vault, reward, liquidation, and oracle-heavy protocols.

Required outputs: writer path, stale/mismatched consumer path, persistence window, numeric trace for any High/Critical candidate.

---

### Phase 6 — Cross-Module Interaction Scan

After completing per-file analysis, explicitly trace these interaction pairs.
For each pair, ask: does function A in module X leave module Y in an
inconsistent or permanently broken state?

Required pairs to check in every lending protocol audit.

**CHECK #1 IS HIGHEST PRIORITY — do it first, do it thoroughly:**

1. **[CRITICAL PRIORITY] reward_manager_update ↔ all lending operations** —
   Does the reward/accumulator update perform arithmetic that can abort BEFORE writing
   `last_update_time`? If ALL user operations (deposit/withdraw/borrow/repay/liquidate/claim)
   AND admin recovery (cancel/close) call this update → permanent deadlock.
   (→ 12.1, DEFI-85–87). Trace: overflow bounds, checkpoint ordering, admin recovery paths, threshold table.

2. **repay ↔ rewards/liquidity_mining** —
   When repay fully clears the last debt on an obligation (permissionless path),
   is the reward tracker for that obligation cleaned up?
   If not: orphaned tracker may block pool closure. (→ common-move.md 11.1)

3. **liquidate ↔ reserve (collateral reserve)** —
   Does the liquidation path check that the collateral reserve has
   idle cash >= seize_amount before calling `balance::split()` or equivalent?
   If not: liquidation reverts at high utilization → bad debt accumulates. (→ DEFI-81)

4. **adl ↔ emode** —
   Does the ADL entry condition and the ADL stop condition both read total borrows
   from the same source (both reserve-level OR both emode-group-level)?
   If different sources → wrongful liquidation or stuck ADL state. (→ DEFI-82)

5. **admin_config ↔ interest/reserve** —
   Does every admin function that updates a rate model or fee rate call
   `accrue_interest()` before applying the new value?
   If not → retroactive rate application, mispriced interest for all users. (→ DEFI-80)

6. **liquidate ↔ close_factor** —
   Is the close factor enforced per-TRANSACTION, not per-call?
   On Sui, PTBs allow calling liquidate() N times atomically. If close factor is
   checked against current (shrinking) debt, total liquidation = 1-(1-CF)^N. (→ SUI-28, DEFI-83)

7. **admin_config ↔ rate_limiters** —
   Does the config update function preserve accumulated runtime state (limiter segments,
   accumulators, counters)?
   If config update resets limiters → sandwich attack: borrow to limit → admin resets → borrow again. (→ DEFI-84)

8. **oracle_eligibility ↔ oracle_seize** —
   Does liquidation use the same price type for both trigger and seize, OR enforce a
   bounded divergence between them?
   If borrow/withdraw enforce EMA-spot tolerance but liquidation does NOT → unbounded
   price divergence in the only operational code path during volatility. (→ DESIGN-L1 caveat)

9. **flash_loan ↔ deposit/borrow/withdraw** —
   Do operations during an active flash loan see stale accounting fields (cash, total_borrows)?
   If hot potato guarantees repayment, not updating cash is intentional (DESIGN-L2). But if
   other operations READ the stale value mid-PTB, they may misprice shares or health. (→ DESIGN-L2 caveat)

For any interaction pair where the answer is NO → report as HIGH.
This phase is mandatory. Do not skip it even if all per-file scans were clean.

---

### Phase 7 — Verify & Triage (Move-Expert Validation)

Before reporting, every candidate finding from Phases 3-6 must survive a Move-expert
verification pass. This phase eliminates false positives, corrects inflated severities,
and ensures only real, exploitable findings reach the report.

Before verifying any finding, read `verification-policy.md`, `evidence-chains.md`,
and `confidence-gates.md`. Apply:

- evidence source tagging
- the mock rejection rule
- reachability gate
- math-bounds gate
- severity discipline for High/Critical

**Step 1 — Dual Narrative Test**

For each candidate finding, write two concrete stories:

- **Legitimate User Story:** How this code path behaves under normal Move usage —
  correct object ownership, valid signer, expected type parameters, intended call sequence.
- **Attacker Story:** Step-by-step exploitation using Move-specific primitives — exact
  function calls with type parameters, PTB composition steps (Sui) or transaction
  sequence (Aptos), object IDs/resource addresses involved, and the final extractable value.

**Rule:** If you cannot write a concrete attacker story with specific Move function calls,
object/resource interactions, and a quantified outcome — the finding is invalid. Move's
strict type system means vague "an attacker could..." stories are insufficient.

**Step 2 — Move-Expert Disproof (8 Dimensions)**

Systematically challenge each finding against Move's unique properties:

1. **Move Type System & Linearity** — Does Move's linear type system, borrow checker,
   or ability constraints already prevent this? Key Move eliminators:
   - No reentrancy via callbacks (no dynamic dispatch, no fallback functions)
   - No double-spend of resources (linearity enforces single ownership)
   - No capability forgery if abilities are correct (`key` only, no `copy`)
   - No type confusion if generic parameters are properly constrained
   - No storage collision (typed global storage / Sui UID-based objects)

2. **Call Path Completeness** — Trace the full call path including
   `public(package)`/`public(friend)` visibility. Does an upstream function already
   validate the input? Does a downstream `assert!` or abort prevent the exploit?
   Does the return type force the caller to handle it (hot-potato pattern)?

3. **Object/Resource Model** — Sui: is the target owned (only owner can access),
   shared (consensus-ordered), wrapped (inaccessible), or frozen (immutable)?
   Aptos: does `acquires` enforce exclusive access? Does `exists<T>(addr)` check
   prevent the setup? Ownership often makes EVM-style attacks infeasible.

4. **Execution Model Reality** — Move has no `delegatecall`, no callbacks, no dynamic
   dispatch, no inline assembly. Sui PTBs compose only through `public` interfaces —
   they cannot call `public(package)` functions. Does the finding assume EVM capabilities
   that Move doesn't have?

5. **Precondition Feasibility & Invariant Reachability** — Can the attacker reach the
   vulnerable state on mainnet?
   - Sui: shared object consensus ordering — can attacker reliably front-run?
   - Aptos: Block-STM parallel execution — does execution order matter?
   - Gas costs, object creation constraints, minimum amounts, time locks
   - Does attacker need a capability/object they cannot obtain?
   - **Invariant Reachability:** If the finding requires a field to have value X, find
     EVERY code path that sets that field and verify X is achievable. Check all
     constructors, setters, and validation guards. Pay special attention to parameter
     validation in admin/init functions — they often create invariants that make edge
     cases unreachable (e.g., `assert!(a < b)` on `u64` makes `b = 0` impossible).

6. **Economic Rationality** — Attack profit vs total cost (gas, flash loan fees, capital
   lockup, slippage, MEV competition). If `cost >= profit`, downgrade to Info. For Sui
   sandwich attacks: is the attacker a validator?

7. **Existing Protections Missed** — Did the scanner overlook:
   - `assert!` conditions in the function or its callees
   - Capability/signer gates on upstream entry points
   - Abort-on-overflow as implicit protection (prevents silent corruption, enables DoS)
   - Time/epoch locks, rate limits, cooldowns, minimum amounts
   - Admin pause mechanisms blocking the attack path
   - Move Prover `spec` blocks enforcing invariants

8. **Counterfactual Fix Test** — Apply your recommended fix mentally:
   - Does the fix change the **observable behavior**? If the transaction still aborts,
     the same funds are still locked, the same DoS occurs — the finding is cosmetic.
   - If downstream code would ALSO block the scenario independently of the bug,
     the bug has no incremental impact. Trace the FULL execution path PAST the
     buggy line — if the function fails at line N+5 anyway, the bug at line N
     is informational at best.
   - "Same value, different error code" is not a vulnerability — both produce
     transaction abort with identical user-facing outcome.

**Step 3 — Label Each Finding**

- **VALID** — Survives all checks. Exploitable on mainnet. Include at stated severity. Assign confidence: `confirmed` or `likely`.
- **QUESTIONABLE** — Plausible, but decisive proof is missing. Confidence: `needs_review`. Max severity: Medium.
- **DISMISSED** — Disproven by trusted local evidence (`[CODE]`, `[TEST]`, `[PROD-SOURCE]`, `[PROD-STATE]`).
- **OVERCLASSIFIED** — Real issue, severity inflated. Downgrade with reasoning. Re-assign confidence level.

**Step 4 — Mandatory Kill Questions**

Every finding labeled VALID or QUESTIONABLE must answer ALL of these. If any answer
is "no" or uncertain, downgrade or dismiss:

1. **Can I construct the precondition state through valid protocol operations?**
   Write the EXACT sequence of transactions. If you can't → INVALID.
2. **Does my recommended fix change observable behavior?**
   Apply the fix. Does the tx succeed now? Does the user get different output?
   If behavior is identical → INFORMATIONAL at best.
3. **For any function I claim "reverts when it shouldn't" — what would it DO if it
   didn't revert?** Would the result be meaningful? (e.g., ADL on zero-collateral:
   even without revert, seized=0, repaid=0 → no-op.)
4. **Is this a pattern used by established protocols (Compound, Aave, MakerDAO)?**
   If yes, load `defi/defi-lending-design-patterns.md` and check whether this is a
   known-good design. Explain why THIS protocol's context differs if reporting.
5. **Who loses money, how much, and under what conditions?**
   If you can't name a specific dollar impact and a specific victim → downgrade severity.
6. **Am I hallucinating this vulnerability?** Re-read the ACTUAL source code now.
   Does the code I'm referencing exist? Can I name exact file:line? Run the
   Self-Hallucination Check in `move-fp-catalog.md` Section 3. If any check fails → INVALID.

**Step 5 — Root-Cause Deduplication**

Before finalizing the finding list, group by the single LINE OF CODE that would
need to change, not by downstream effect:
- "Division by zero when X is zero" and "Function reverts when X is zero" → SAME finding
- "EMA/spot asymmetry" reported as tolerance bypass vs withdrawal blocking → SAME finding
- "Cash not updated" reported as exchange rate issue vs liquidity check issue → SAME finding

Keep only the highest-impact framing of each root cause.

**Step 6 — Post-Confirmation Parallel Subsystem Check**

After confirming any finding at Medium+ severity, check for parallel instances:
1. Identify the root-cause function containing the bug
2. Grep for ALL call sites of that function across the entire codebase
3. For each call site that enters through a DIFFERENT subsystem (deposit vs borrow, token0 vs token1, pool A vs pool B, group 0 vs group 1): does the same bug manifest through this path?
4. If yes → expand the finding to cover all affected subsystems or note the parallel instances explicitly

This is mandatory because bugs in shared logic affect ALL consumers, not just the first path discovered.

**Evidence Audit (mandatory):** For every non-trivial finding, include a short evidence
table from `verification-policy.md` showing each decisive claim and its source tag.

**Output rules:** Only VALID and QUESTIONABLE findings proceed to Phase 8.
DISMISSED findings go to "Verified Clean Checks" with dismissal reason.
OVERCLASSIFIED findings proceed at adjusted severity.

---

### Phase 8 — Report

Produce a structured audit report in this exact format:

```
## Audit Report — [Module/Protocol Name]
**Chain:** Sui | Aptos
**Date:** [today]
**Severity Summary:** X Critical, X High, X Medium, X Low, X Info
**Triage Summary:** N candidates → X VALID, Y QUESTIONABLE, Z DISMISSED, W reclassified

---

### [SEVERITY-NNN] Finding Title

| Field      | Value |
|------------|-------|
| Severity   | Critical / High / Medium / Low / Info |
| Confidence | VALID (`confirmed`/`likely`) / QUESTIONABLE (`needs_review`) |
| Location   | module_name.move, line N, function name |
| Category   | [Access Control / Arithmetic / Resource Safety / etc.] |

**Description:**
Clear explanation of what the vulnerability is and why it exists.

**Attack Scenario (PoC):**
Step-by-step exploitation using Move-specific primitives with concrete values.

**Verification:** Disproof dimensions challenged and passed.

**Recommended Fix:**
Concrete code-level recommendation. Show the fix, not just the concept.

---
```

After all findings, add `## Verified Clean Checks` (with DISMISSED findings and reasoning) and `## Auditor Notes` (code quality, centralization, upgrade risks).

---

## Severity Reference

| Level    | Criteria |
|----------|----------|
| Critical | Direct loss of funds, unauthorized minting, permanent protocol takeover |
| High     | Significant fund loss under realistic conditions, major access control bypass |
| Medium   | Partial fund loss, requires specific conditions, breaks core invariants |
| Low      | Minor issues, best-practice violations, low-probability edge cases |
| Info     | Code quality, gas inefficiency, documentation gaps, non-exploitable patterns |

**Likelihood × Impact = Severity.** A theoretically catastrophic bug that requires a nation-state adversary is not Critical. A low-impact bug that's trivially exploitable is Medium, not Low.

**Admin-origin latent user DoS:** Never dismiss a bug as "admin-only" or "trusted setup" if the admin action is routine (e.g., adding a reward program, setting a fee rate) and unprivileged users or liquidators are later bricked. Severity is based on who is blocked and what is blocked (fund lock, liquidation failure), not on who created the initial configuration. See common-move.md 12.2.

---

## Important Rules

- **Never hallucinate findings.** If you cannot point to exact code that is vulnerable, do not file a finding.
- **Always cite exact file + line + function.** No vague references.
- **Provide a PoC scenario for every High and Critical.** If you can't construct one, downgrade severity.
- **AI output is not final.** Always flag that findings must be manually verified and tested before reporting.
- **One contract at a time.** If given a multi-module codebase, audit module by module and flag cross-module interactions separately.

## aptos-patterns.md

# Aptos Move — Security Patterns

Aptos-specific vulnerability patterns. Load this when auditing any codebase that imports
`aptos_framework`, `aptos_std`, or uses `#[test_only]` Aptos test annotations.

---

## Aptos Mental Model

Aptos uses a global storage model where resources live at account addresses.
The key concepts creating unique attack surfaces:

- **Global storage** is the primary storage: `move_to`, `move_from`, `borrow_global`, `borrow_global_mut`
- **Signer** represents the transaction sender and is the primary access control primitive
- **Resource accounts** are special accounts controlled by on-chain logic, not private keys
- **Coin & FungibleAsset** frameworks have specific patterns for token handling
- **`acquires` annotations** must exactly match resources accessed
- **Events** are emitted via `event::emit_event` and are critical for off-chain systems

---

## APT-01 — Missing `acquires` Annotation

**Description:** A function that calls `borrow_global` or `borrow_global_mut` on a resource must declare `acquires T`. Missing or incorrect `acquires` annotations cause compile-time errors — but the check: are the `acquires` annotations accurate?

**Pattern:**
```move
// Potentially confusing — acquires annotation on public function
// means any caller indirectly acquires these resources
public fun do_thing(): u64 acquires Config, State {
    let config = borrow_global<Config>(@admin);
    let state = borrow_global<State>(@admin);
    config.value + state.count
}
```

**Check:**
1. Verify that `acquires` annotations match the actual resources accessed (including transitively through helper functions)
2. Functions with large `acquires` lists may have unexpected reentrancy-like behavior if called mid-state-update
3. Public functions with `acquires` expose the resource to the entire call chain

---

## APT-02 — Resource Account Privilege Escalation

**Description:** Resource accounts are controlled by a `SignerCapability`. If this capability is stored insecurely or accessible to unauthorized parties, full control of the resource account is compromised.

**Pattern:**
```move
// VULNERABLE — SignerCapability stored in a globally readable resource
struct ProtocolConfig has key {
    signer_cap: account::SignerCapability,  // anyone can read this!
}

public fun do_admin_thing(caller: &signer) acquires ProtocolConfig {
    let config = borrow_global<ProtocolConfig>(@protocol);
    let resource_signer = account::create_signer_with_capability(&config.signer_cap);
    // resource_signer has full power — but config is readable by anyone
}
```

**Risk:** If `SignerCapability` can be extracted or the resource holding it accessed without proper guards, an attacker gains full control of the resource account.

**Check:**
1. `SignerCapability` should be stored in a resource with access control
2. Functions that use `SignerCapability` to create signers must be admin-gated
3. Verify `SignerCapability` is not accidentally exposed in public structs
4. Check initialization: who receives the `SignerCapability` at creation time?

---

## APT-03 — Coin Type Confusion

**Description:** Generic functions that accept `CoinType` parameters without enforcing which coin types are valid.

**Pattern:**
```move
// VULNERABLE — accepts any coin type as collateral
public entry fun deposit_collateral<CoinType>(
    user: &signer,
    amount: u64
) {
    let coins = coin::withdraw<CoinType>(user, amount);
    // No validation that CoinType is an approved collateral asset!
    add_to_vault<CoinType>(coins);
}
```

**Risk:** Attacker deposits a worthless self-created token as collateral, then borrows
valuable assets against it. Classic DeFi attack.

**Check:**
1. All functions accepting generic `CoinType` must whitelist valid coin types
2. Whitelisting should be enforced on-chain, not just off-chain
3. Price oracles must reject unrecognized coin types
4. `coin::value()` on an unregistered type aborts — but whitelisting should happen before that

*See also: `common-move.md` 8.1 for the general generic type validation pattern*

---

## APT-04 — Signer Capability Abuse (via `create_signer_with_capability`)

**Description:** `account::create_signer_with_capability` creates a real signer that can do anything the resource account can do. Any code path that reaches this function without proper authorization is critical.

**Check:**
1. How many code paths can reach `create_signer_with_capability`?
2. Is each path gated by admin authorization?
3. Can an attacker craft a sequence of calls that reaches this function?
4. Is the resulting signer used only for intended operations?

---

## APT-05 — Table / Iterable Table Safety

**Description:** Aptos `table::Table` and `table_with_length::TableWithLength` have specific safety requirements.

**Patterns:**
```move
// DANGEROUS — table access without existence check
let value = table::borrow(&protocol.balances, user_addr);
// Aborts if key doesn't exist — attacker can DoS by providing non-existent key

// SAFE
assert!(table::contains(&protocol.balances, user_addr), E_NOT_REGISTERED);
let value = table::borrow(&protocol.balances, user_addr);
```

**Check:**
1. All `table::borrow` calls must be preceded by `table::contains` check
2. All `table::remove` calls must be preceded by `table::contains` check
3. Iterating over tables: `TableWithLength` provides length — `Table` does not; verify no unbounded iteration
4. Tables that grow unboundedly (e.g., per-user tables) can cause DoS via storage cost
5. `smart_table` vs `table`: verify the right one is used for the expected access pattern

---

## APT-06 — Timestamp Oracle

**Description:** Aptos provides `timestamp::now_seconds()` and `timestamp::now_microseconds()`.

**Risk:** Block times in Aptos are typically ~1s. Validators have limited ability to adjust timestamps. However:
- Exact timestamp equality checks are fragile
- Time-windows shorter than a few seconds are gameable
- Epoch transitions create predictable timing events

**Pattern:**
```move
// FRAGILE — exact timestamp match never occurs in practice
assert!(timestamp::now_seconds() == deadline, E_NOT_YET);

// BETTER — range check
assert!(timestamp::now_seconds() >= start && timestamp::now_seconds() <= end, E_OUT_OF_WINDOW);
```

**Check:**
1. No exact timestamp equality checks
2. Interest/reward accrual at exact timestamps — check for boundary rounding
3. Lock periods: verify off-by-one on `>` vs `>=` at unlock time
4. Flash loan windows: ensure timestamp-gated operations can't be bypassed by manipulating block timing

---

## APT-07 — Event Handle Exhaustion / Missing Events

**Description:** Aptos uses `EventHandle` for emitting events. Issues arise from:
1. Event handles shared across multiple emitters (counter collisions)
2. Missing events on critical state changes (breaks off-chain monitoring)
3. Events emitted with stale/incorrect data

**Check:**
1. Each logical event source should have its own `EventHandle`
2. Critical state changes (deposits, withdrawals, admin changes) must emit events
3. Event data should reflect post-state (after the change), not pre-state
4. Verify that event emission cannot be skipped via an early return or error path

---

## APT-08 — Module Upgrade Safety

**Description:** Aptos supports module upgrades. Upgrade policies range from `arbitrary` (any upgrade allowed) to `immutable` (no upgrades). Upgrade bugs:

**Check:**
1. What is the upgrade policy? `arbitrary` upgrades are a centralization risk
2. Can storage layout change break existing resources?
3. Does the upgrade add/remove fields in structs that are stored on-chain?
4. Is there a timelock on upgrades? Flag single-key upgrade authority
5. Check for `#[test_only]` functions that were accidentally left accessible in production builds

---

## APT-09 — FungibleAsset Framework vs Legacy Coin

**Description:** Aptos is migrating from `aptos_framework::coin` to `aptos_framework::fungible_asset`. Mixed usage creates compatibility issues.

**Pattern:**
```move
// Protocol mixes frameworks
public entry fun deposit_coin<T>(user: &signer, amount: u64) {
    let coin = coin::withdraw<T>(user, amount);
    // internally converts to FungibleAsset — conversion path must be verified
}
```

**Check:**
1. Identify whether the protocol uses `coin`, `fungible_asset`, or both
2. Conversion between `Coin<T>` and `FungibleAsset` must use official framework functions
3. Balance accounting must be consistent across both frameworks
4. `primary_fungible_store` vs manual store management — verify correct usage

---

## APT-10 — vector / smart_vector Unbounded Growth

**Description:** Vectors that grow unboundedly create DoS vectors through gas exhaustion.

**Pattern:**
```move
struct UserList has key {
    users: vector<address>,  // grows with every new user
}

// Iterating over this in a transaction costs O(n) gas
public entry fun process_all(admin: &signer) acquires UserList {
    let list = borrow_global<UserList>(@protocol);
    let i = 0;
    while (i < vector::length(&list.users)) {
        // O(n) — becomes untransactable as n grows
        process_user(*vector::borrow(&list.users, i));
        i = i + 1;
    }
}
```

**Check:**
1. Any vector that grows with user count is a long-term DoS vector
2. Functions iterating over user-input-sized vectors must have length limits
3. Prefer `smart_table` over `vector<(K, V)>` for key-value lookups
4. Unbounded iteration is a Critical finding if it blocks core protocol functions

---

## APT-11 — `#[view]` Function Side Effect Risks

**Description:** `#[view]` functions should be read-only but if they interact with mutable state patterns, they can cause unexpected behavior.

**Check:**
1. `#[view]` functions must not mutate state
2. Verify `#[view]` functions don't call non-view functions that mutate state
3. View functions used by front-ends for price/balance quotes — ensure they can't be sandwiched

---

## APT-12 — Test / Debug Functions as Privilege Escalation

**Description:** Functions intended for testing that are left accessible in production. Unlike `#[test_only]` functions (which the compiler strips), these are regular `public` functions with names like `test_mint`, `debug_set_admin`, or helper functions that bypass normal access control.

**Pattern:**
```move
// VULNERABLE — test helper left in production, anyone gets admin
public fun test_create_admin(account: &signer): AdminCap {
    // No #[test_only] attribute! Callable in production
    AdminCap { signer_cap: account::create_test_signer_cap(signer::address_of(account)) }
}

// VULNERABLE — init-like function without one-time guard
public entry fun setup_for_testing(admin: &signer) {
    // Meant for tests but callable by anyone — reinitializes protocol
    move_to(admin, Config { fee: 0, admin: signer::address_of(admin) });
}
```

**Check:**
1. Search for functions with `test`, `debug`, `mock`, `setup` in names — are they `#[test_only]`?
2. Any function that creates admin capabilities or signers outside of `init` — is it restricted?
3. Check for `public` functions that set storage directly without access control
4. Verify `#[test_only]` attribute is present on ALL test helper functions and modules

*Real audit ref: Multiple protocols (test code not restricted with #[test_only],
anyone gains admin privileges — Critical)*

---

## APT-13 — FungibleAsset Zero-Value Manipulation

**Description:** Zero-value operations on `FungibleAsset` that corrupt counters, bypass limits, or manipulate investor tracking.

**Pattern:**
```move
// VULNERABLE — zero-value withdrawal increments counter, blocking real withdrawals
public fun withdraw_fa(
    store: &mut FungibleStore,
    amount: u64,
    account: &signer
) acquires WithdrawTracker {
    let tracker = borrow_global_mut<WithdrawTracker>(signer::address_of(account));
    tracker.withdraw_count = tracker.withdraw_count + 1;  // increments even for amount=0
    // If max_withdrawals is 3, attacker sends 3 zero-value txs to block real withdrawals
    assert!(tracker.withdraw_count <= MAX_WITHDRAWALS, E_LIMIT_REACHED);
    fungible_asset::withdraw(account, store, amount);
}

// VULNERABLE — zero-value burn decrements investor count
public fun burn_fa(store: &mut FungibleStore, amount: u64) acquires InvestorTracker {
    let tracker = borrow_global_mut<InvestorTracker>(@protocol);
    tracker.investor_count = tracker.investor_count - 1;  // decrements even for amount=0!
    fungible_asset::burn(store, amount);
}

// SAFE — reject zero-value operations
public fun withdraw_fa(store: &mut FungibleStore, amount: u64, account: &signer) {
    assert!(amount > 0, E_ZERO_AMOUNT);
    // ...
}
```

**Check:**
1. All `fungible_asset::withdraw` / `burn` / `transfer` — what happens with `amount = 0`?
2. Do zero-value operations increment/decrement counters, limits, or tracking variables?
3. Can zero-value deposits create entries that affect reward distribution or voting power?
4. Check `primary_fungible_store` operations for the same zero-value patterns

*Real audit refs: Securitize (zero-value withdrawals block legitimate withdrawals — High,
zero-value burns corrupt investor counts — High)*

---

## APT-14 — Concurrent Privilege Escalation

**Description:** Multiple pending privilege requests (admin, treasury, operator) that can be claimed simultaneously, creating role conflicts or privilege duplication.

**Pattern:**
```move
// VULNERABLE — multiple admins can have pending claims simultaneously
public entry fun claim_admin_privileges(account: &signer) acquires PendingAdmin {
    let pending = borrow_global<PendingAdmin>(@protocol);
    assert!(signer::address_of(account) == pending.new_admin, E_NOT_PENDING);
    // Grants admin — but what if there are two pending requests?
    // Both could claim, creating two admins
}

// VULNERABLE — treasury can also claim admin role
public entry fun claim_admin_privileges(account: &signer) acquires AdminStore {
    let store = borrow_global_mut<AdminStore>(@protocol);
    // No check that caller isn't already treasury — role confusion
    store.admin = signer::address_of(account);
}

// SAFE — cancel previous pending before creating new
public entry fun set_pending_admin(
    admin: &signer,
    new_admin: address
) acquires AdminStore {
    let store = borrow_global_mut<AdminStore>(@protocol);
    assert!(signer::address_of(admin) == store.admin, E_NOT_ADMIN);
    store.pending_admin = option::some(new_admin);
    // Only one pending admin at a time — previous is overwritten
}
```

**Check:**
1. Can multiple privilege transfers be pending simultaneously?
2. Are admin and treasury roles distinct? Can one claim the other's privileges?
3. Does `cancel_admin_privileges` / `cancel_treasury_privileges` have proper access control?
4. Single-step ownership transfer: is it validated? Wrong address = permanent lockout

*Real audit refs: Baptswap (multiple simultaneous pending privileges — High,
cancel_admin callable by anyone — High,
treasury can claim admin — High,
single-step transfer danger — High)*

---

## APT-15 — Ordered Map Key Field Ordering (Lexicographic Sort Trap)

**Description:** When a struct is used as a key in `OrderedMap` or `BigOrderedMap`, fields are compared **lexicographically starting from the first declared field** in the struct definition. If the first field isn't your intended primary sort key, every range scan, `borrow_front`, `borrow_back`, and early termination is silently wrong.

**Pattern:**
```move
// VULNERABLE — struct sorts by account first, but code assumes sorting by price
struct OrderKey has copy, drop, store {
    account: address,   // <-- sorts by this first!
    order_id: u64,      // then this
    price: u64,         // this barely matters for ordering
}

// Developer assumes orders are sorted by price — WRONG
// borrow_front returns lowest account address, not lowest price
let cheapest = ordered_map::borrow_front(&orderbook);

// SAFE — put primary sort field first
struct OrderKey has copy, drop, store {
    price: u64,         // primary sort key — first field
    order_id: u64,      // tiebreaker
    account: address,   // least significant
}
```

**Check:**
1. Find every struct used as a key in `OrderedMap` or `BigOrderedMap`
2. Verify the first declared field is the intended primary sort key
3. Check all `borrow_front`, `borrow_back`, and range iteration — do they return what the code expects?
4. No compiler warning, no runtime error — the map works, just not in the order you think

---

## APT-16 — Map Type Selection DoS

**Description:** Aptos has multiple map types with very different performance characteristics. Using the wrong one for permissionless data is a DoS vulnerability.

**Map types and when to use them:**

| Type | Backing | Lookup | Growth | Use for |
|------|---------|--------|--------|---------|
| `SimpleMap` (deprecated) | vector | O(n) linear scan | bounded | Never for permissionless data |
| `OrderedMap` | single slot | O(log n) | bounded | Small bounded sets only |
| `Table` | one slot per key | O(1) | unbounded | Unbounded data, no iteration needed |
| `BigOrderedMap` | B+ tree | O(log n) | unbounded, concurrent | Unbounded data with iteration |

**Pattern:**
```move
// VULNERABLE — SimpleMap with permissionless additions
struct Registry has key {
    users: SimpleMap<address, UserInfo>,  // O(n) lookup, anyone can add
}

public entry fun register(account: &signer) acquires Registry {
    let registry = borrow_global_mut<Registry>(@protocol);
    // Attacker registers thousands of entries
    // Every subsequent lookup/insert costs O(n) gas
    // Eventually: mint, burn, liquidate all bricked
    simple_map::add(&mut registry.users, signer::address_of(account), UserInfo {});
}

// SAFE — use Table or BigOrderedMap for permissionless data
struct Registry has key {
    users: Table<address, UserInfo>,  // O(1) lookup, scales to any size
}
```

**Check:**
1. Flag any `SimpleMap` or `SmartTable` usage — both are deprecated but still in production codebases
2. If the data structure allows permissionless additions (any user can add entries), it MUST use `Table` or `BigOrderedMap`
3. Check if the protocol iterates over the map — `Table` doesn't support iteration; use `BigOrderedMap` if iteration is needed
4. The data structure layer is where some of the highest-impact DoS bugs hide

---

## APT-17 — ConstructorRef Leak

**Description:** When creating Aptos Objects, exposing the `ConstructorRef` allows anyone to generate `TransferRef`, `DeleteRef`, `ExtendRef`, etc. — giving full control over the object. An NFT mint function that returns `ConstructorRef` lets the original creator reclaim the NFT after it's sold.

**Pattern:**
```move
// VULNERABLE — returning ConstructorRef lets caller generate TransferRef
public fun mint(creator: &signer): ConstructorRef {
    let constructor_ref = token::create_named_token(creator, ...);
    constructor_ref  // attacker stores this, generates TransferRef, reclaims NFT after sale
}

// SAFE — never expose ConstructorRef
public fun mint(creator: &signer) {
    let constructor_ref = token::create_named_token(creator, ...);
    // Use constructor_ref internally, then let it go out of scope
}
```

**Check:**
1. No function should return `ConstructorRef` — it's the master key for an object
2. `TransferRef`, `DeleteRef`, `ExtendRef` derived from `ConstructorRef` must be stored securely or not at all
3. If `TransferRef` is stored, verify it's access-gated — otherwise original creator can transfer the object back at will
4. Check NFT minting flows especially — returned refs enable post-sale theft
5. **Ungated transfer control:** If ungated transfers are NOT needed, verify `object::set_untransferable()` is called during construction. Without this, anyone holding a `TransferRef` can move the object freely
6. **DeleteRef discipline:** `DeleteRef` should only be generated for objects that are genuinely intended to be burnable/deletable. Unnecessary `DeleteRef` generation creates object destruction risk — if it leaks or is stored without access control, anyone can permanently destroy the object

*Source: [Aptos Move Security Guidelines](https://aptos.dev/build/smart-contracts/move-security-guidelines)*

---

## APT-18 — Object Account Resource Grouping

**Description:** Multiple `key`-able resources stored at the **same object account** are all transferred together when any one of them is transferred. `object::transfer` operates on `ObjectCore`, which applies to all resources at that address.

**Pattern:**
```move
// VULNERABLE — Monkey and Toad at same object account
fun mint_two(sender: &signer, recipient: address) {
    let constructor_ref = &object::create_object_from_account(sender);
    let obj_signer = object::generate_signer(constructor_ref);
    move_to(&obj_signer, Monkey {});
    move_to(&obj_signer, Toad {});  // same address as Monkey!

    let monkey_obj = object::address_to_object<Monkey>(obj_addr);
    object::transfer(sender, monkey_obj, recipient);
    // BUG: Toad is also transferred — both resources share the object account
}

// SAFE — separate object accounts per resource
fun mint_two(sender: &signer, recipient: address) {
    let ref_monkey = &object::create_object(signer::address_of(sender));
    let ref_toad = &object::create_object(signer::address_of(sender));
    move_to(&object::generate_signer(ref_monkey), Monkey {});
    move_to(&object::generate_signer(ref_toad), Toad {});
    // Now each resource has its own object account — independent transfers
}
```

**Check:**
1. For every `object::create_object` call: how many resources are stored at that object address?
2. If multiple resources share an object account, transferring one transfers ALL — is this intended?
3. Especially dangerous in NFT collections, multi-asset vaults, and gaming items
4. Each independently-transferable resource should have its own object account

*Source: [Aptos Move Security Guidelines](https://aptos.dev/build/smart-contracts/move-security-guidelines)*

---

## APT-19 — Mutable Reference Swap Attack (mem::swap)

**Description:** Passing `&mut T` to untrusted code (callbacks, function values) allows the callee to use `mem::swap` to **replace the entire value** behind the reference. This bypasses private field protections without ever reading or writing them directly.

**Pattern:**
```move
// VULNERABLE — validates asset, passes &mut to untrusted callback, uses asset after
public fun do_with_fa(
    user: address, asset: FungibleAsset, hook: |&mut FungibleAsset|
) {
    check_metadata(&asset);      // verify it's the expected asset
    hook(&mut asset);            // untrusted code: can mem::swap a worthless asset in
    // asset may now be a completely different token!
    primary_fungible_store::deposit(@treasury, asset);  // deposits worthless asset
    mint_to(user, fungible_asset::amount(&asset));      // mints real tokens
}

// SAFE — re-validate after untrusted mutation
public fun do_with_fa(
    user: address, asset: FungibleAsset, hook: |&mut FungibleAsset|
) {
    check_metadata(&asset);
    hook(&mut asset);
    check_metadata(&asset);      // re-check after untrusted code touched it
    // ...
}
```

**Check:**
1. Any `&mut T` passed to a callback, function value, or cross-trust-boundary call — can the callee swap the whole value?
2. Invariants validated before passing `&mut` must be **re-validated after** the call returns
3. Prefer `public(friend)` over `public` for mutation-heavy APIs
4. Don't pass `&mut` to untrusted code at all if possible
5. Especially dangerous for `FungibleAsset`, `Coin`, and any value type used in financial logic

*Source: [Aptos Move Security Guidelines — mem::swap / AIP-105](https://aptos.dev/build/smart-contracts/move-security-guidelines)*

---

## APT-20 — Randomness Bias (Test-and-Abort + Undergasing)

**Description:** Aptos provides on-chain randomness via `aptos_framework::randomness`. Two attack vectors allow biasing outcomes:

1. **Test-and-abort:** If a randomness-using function is `public` (not just `entry`), an attacker composes it with an `assert!` that aborts on unfavorable outcomes. Retry until desired result.
2. **Undergasing:** If favorable and unfavorable code paths consume different gas, attacker sets gas limit that only allows the favorable path to complete. Unfavorable path runs out of gas and aborts.

**Pattern:**
```move
// VULNERABLE — public allows composition with abort-on-bad-outcome
#[lint::allow_unsafe_randomness]
public entry fun play(user: &signer) {
    let random = randomness::u64_range(0, 100);
    if (random == 42) { mint_reward(user); }
}
// Attacker: play(attacker); assert!(exists<Reward>(attacker_addr)); // aborts if lost

// VULNERABLE — win() uses less gas than lose(), attacker limits gas to exclude lose path
#[randomness]
entry fun play(user: &signer) {
    let r = randomness::u64_range(0, 100);
    if (r == 42) { win(user); }    // cheap path
    else { lose(user); }            // expensive path — runs out of gas
}

// SAFE — entry only (not public), equal gas paths
#[randomness]
entry fun play(user: &signer) {
    let r = randomness::u64_range(0, 100);
    // commit random result, resolve in separate tx
    save_result(user, r);
}
```

**Check:**
1. Functions using `randomness::*` must be `entry` only — NOT `public` or `public entry`
2. Favorable and unfavorable code paths must consume similar gas
3. Prefer commit-reveal: save random result in one tx, act on it in a separate tx
4. Only admin-controlled functions should use `#[lint::allow_unsafe_randomness]`

*Source: [Aptos Move Security Guidelines — Randomness](https://aptos.dev/build/smart-contracts/move-security-guidelines)*

---

## APT-21 — Function Value Reentrancy (Move 2.2+)

**Description:** Since Move language version 2.2, function values (closures) enable reentrancy patterns that were previously impossible. While dispatchable fungible assets are protected by reentrancy locks, **function values passed as callbacks are NOT locked**. A callback can re-enter the calling module via dynamic dispatch.

**Mitigations built into Move:**
- Re-entered modules **cannot access their own resources** during dynamic dispatch (attempts to `borrow_global` or `move_from` will abort)
- But attackers can still exploit by altering parameters (e.g., inflating amounts) or swapping values via captured references

**Pattern:**
```move
// VULNERABLE — untrusted function value can re-enter and alter amount
public fun withdraw_operations(
    user: &signer, amount: u64,
    f: |address, &Grant, u64|      // attacker-supplied function value
) {
    let addr = address_of(user);
    assert!(balance(addr) >= amount, E_INSUFFICIENT);
    let g = grant();
    f(addr, &g, amount);           // attacker ignores amount, passes 100_000_000
}

// SAFE — bind amount into a non-droppable Grant at creation time
public fun withdraw_operations(user: &signer, amount: u64, f: |address, Grant|) {
    let addr = address_of(user);
    assert!(balance(addr) >= amount, E_INSUFFICIENT);
    let g = grant(addr, amount);   // state updated + amount fixed inside Grant
    f(addr, g);                    // Grant controls the amount, not the callback
}
```

**Check:**
1. Any function accepting a function value (`|...|` parameter) — can it re-enter the module?
2. Validate that state updates happen BEFORE the callback is invoked (checks-effects-interactions)
3. Don't trust parameters passed to callbacks — bind critical values into non-droppable structs
4. Check for `mem::swap` attacks on `&mut` references captured by closures
5. Dispatchable fungible assets are safe (locked against reentrancy) — other function values are NOT

*Source: [Aptos Move Security Guidelines — Function Values](https://aptos.dev/build/smart-contracts/move-security-guidelines)*

---

## APT-22 — Struct Layout Change on Upgrade

**Description:** When a module is upgraded on Aptos, existing on-chain resources retain
their original binary layout. If struct fields are reordered, removed, or types changed,
deserialization of existing resources fails — all existing user positions become
permanently inaccessible.

**Pattern:**
```move
// v1 — original struct (stored on-chain for all users)
struct Position has key, store {
    owner: address,
    amount: u64,
    debt: u64,
}

// v2 VULNERABLE — field reordered + type changed, existing resources break
struct Position has key, store {
    debt: u128,       // was u64, now u128 — binary layout mismatch
    amount: u64,
    owner: address,   // reordered — deserialization reads wrong bytes
}

// v2 SAFE — append-only changes, existing layout preserved
struct Position has key, store {
    owner: address,   // same order
    amount: u64,      // same type
    debt: u64,        // same type
}

// If migration is needed, use a new struct + migration function
struct PositionV2 has key, store {
    owner: address,
    amount: u64,
    debt: u128,       // upgraded field
}

public entry fun migrate_position(user: &signer) acquires Position {
    let old = move_from<Position>(signer::address_of(user));
    let Position { owner, amount, debt } = old;
    move_to(user, PositionV2 { owner, amount, debt: (debt as u128) });
}
```

**Check:**
1. Compare pre- and post-upgrade struct definitions — field order and types must be preserved
2. If layout changes are needed, a separate V2 struct + migration function must exist
3. Verify migration function handles all existing users (or is callable per-user)
4. New fields can only be appended at the end (append-only compatibility)

---

## APT-23 — Resource Account Signer Scope Creep

**Description:** A `SignerCapability` for a resource account grants unrestricted signer
access to that account. If multiple modules store resources at the same resource account
address, a `SignerCapability` holder can manipulate ALL resources there — not just the
ones their module created.

**Pattern:**
```move
// VULNERABLE — two modules share one resource account
// Module A creates the resource account and stores its signer cap
public fun init_module_a(deployer: &signer) {
    let (resource_signer, cap) = account::create_resource_account(deployer, b"shared");
    move_to(&resource_signer, ModuleAState { value: 0 });
    move_to(deployer, SignerStore { cap }); // Module A holds signer cap
}

// Module B stores resources at the SAME resource account address
public fun init_module_b(admin: &signer) acquires SignerStore {
    let cap = &borrow_global<SignerStore>(@module_a).cap;
    let resource_signer = account::create_signer_with_capability(cap);
    move_to(&resource_signer, ModuleBState { balance: 1000 }); // co-located
}

// Module A can now manipulate Module B's resources!
public fun steal(admin: &signer) acquires SignerStore, ModuleBState {
    let cap = &borrow_global<SignerStore>(@module_a).cap;
    let signer = account::create_signer_with_capability(cap);
    let state = move_from<ModuleBState>(signer::address_of(&signer));
    // Module A just stole Module B's state
}

// SAFE — each module uses its own resource account
public fun init_module_a(deployer: &signer) {
    let (resource_signer, cap) = account::create_resource_account(deployer, b"module_a");
    move_to(&resource_signer, ModuleAState { value: 0 });
    move_to(deployer, SignerStoreA { cap });
}

public fun init_module_b(deployer: &signer) {
    let (resource_signer, cap) = account::create_resource_account(deployer, b"module_b");
    move_to(&resource_signer, ModuleBState { balance: 1000 });
    move_to(deployer, SignerStoreB { cap });
}
```

**Check:**
1. Verify each resource account is used by exactly one module
2. If shared, verify that all modules with `SignerCapability` access are trusted
3. Check that `SignerCapability` is stored privately — not accessible by other modules
4. Cross-ref: APT-04 (signer capability abuse)

---

## APT-24 — Unchecked Signer Parameter (No Address Validation)

**Description:** A `public entry fun` that accepts `&signer` but never validates the signer's address against any stored admin/owner/role address. The `&signer` type only proves someone signed the transaction — it does NOT prove they are authorized. Without a `signer::address_of` comparison, ANY account that signs a transaction can execute the function.

**Pattern:**
```move
// VULNERABLE — &signer accepted but never validated against stored authority
public entry fun set_config(admin: &signer, new_fee: u64) acquires Config {
    let config = borrow_global_mut<Config>(@protocol);
    config.fee = new_fee;
    config.admin = signer::address_of(admin); // sets caller as admin — no check!
}

// VULNERABLE — &signer used only for move_to, anyone can create admin state
public entry fun initialize(account: &signer) {
    move_to(account, AdminConfig {
        admin: signer::address_of(account),
        treasury: signer::address_of(account),
    });
    // No guard: exists<AdminConfig>(@protocol) or one-time init check
}

// SAFE — validates signer address against stored admin
public entry fun set_config(admin: &signer, new_fee: u64) acquires Config {
    let config = borrow_global_mut<Config>(@protocol);
    assert!(signer::address_of(admin) == config.admin, E_NOT_ADMIN);
    config.fee = new_fee;
}
```

**Check:**
1. For every `public entry fun` and `entry fun` that takes `&signer`: search for `signer::address_of` in the function body and all callees
2. If `signer::address_of` is NEVER called, or is called but never compared to a stored/hardcoded authority address → flag as Critical
3. Common false patterns: `signer::address_of` used only as a destination (e.g., `move_to(account, ...)`) but never as an authorization check
4. `init_module(account: &signer)` is a special case — runs once at publish time. But verify it IS `init_module` and not a re-callable setup function

**Risk:** Complete access control bypass. Any wallet can call admin functions, drain funds, change protocol parameters, or take over governance.

*Cross-ref: common-move.md 1.1 (missing capability validation), APT-12 (test functions without restrictions)*

---

## APT-25 — Input Validation Gaps

**Description:** Entry functions that accept user-supplied parameters without validating them against safe ranges. Unlike arithmetic overflow (which Move aborts on), missing input validation allows logically invalid operations to succeed silently — zero-value deposits that corrupt accounting, oversized strings that bloat storage, zero addresses that brick ownership, or out-of-range enum values that bypass intended logic.

**Pattern:**
```move
// VULNERABLE — no input validation, multiple issues
public entry fun create_pool(
    admin: &signer,
    name: String,
    fee_bps: u64,
    recipient: address,
    pool_type: u8,
    initial_tokens: vector<address>
) {
    // name could be empty or 10KB — storage bloat / display issues
    // fee_bps could be 0 (no fees collected) or 100_000 (1000% fee)
    // recipient could be @0x0 — funds sent to unrecoverable address
    // pool_type could be 255 — no enum range check, undefined behavior
    // initial_tokens could be empty — pool created with no assets
}

// SAFE — comprehensive input validation
public entry fun create_pool(
    admin: &signer,
    name: String,
    fee_bps: u64,
    recipient: address,
    pool_type: u8,
    initial_tokens: vector<address>
) {
    // String length
    assert!(string::length(&name) > 0, E_EMPTY_NAME);
    assert!(string::length(&name) <= MAX_NAME_LENGTH, E_NAME_TOO_LONG);

    // Numeric bounds
    assert!(fee_bps > 0, E_ZERO_FEE);
    assert!(fee_bps <= MAX_FEE_BPS, E_FEE_TOO_HIGH);

    // Address validation
    assert!(recipient != @0x0, E_ZERO_ADDRESS);

    // Enum-like range
    assert!(pool_type < NUM_POOL_TYPES, E_INVALID_POOL_TYPE);

    // Vector length
    assert!(vector::length(&initial_tokens) > 0, E_EMPTY_VECTOR);
    assert!(vector::length(&initial_tokens) <= MAX_TOKENS, E_TOO_MANY_TOKENS);
}
```

**Check — 6 validation categories:**
1. **Zero amount:** All `amount: u64` parameters → `assert!(amount > 0, E_ZERO_AMOUNT)`. Zero-value operations can corrupt counters (see APT-13), create empty positions, or bypass minimum thresholds
2. **Max limit:** Numeric inputs bounded by protocol constants → `assert!(amount <= MAX, E_TOO_HIGH)`. Prevents overflow in downstream arithmetic and enforces protocol invariants (e.g., max fee, max leverage)
3. **Vector length:** `assert!(vector::length(&v) > 0, E_EMPTY)` and `assert!(vector::length(&v) <= MAX, E_TOO_MANY)`. Empty vectors cause silent no-ops; unbounded vectors cause gas DoS (see APT-10)
4. **String length:** `assert!(string::length(&s) <= MAX_LENGTH, E_TOO_LONG)`. Unbounded strings bloat on-chain storage and can cause display issues in frontends
5. **Zero address:** `assert!(addr != @0x0, E_ZERO_ADDRESS)`. Setting admin/treasury/recipient to `@0x0` permanently bricks the associated functionality — no private key can sign for `@0x0`
6. **Enum-like range:** `assert!(type_id < NUM_TYPES, E_INVALID_TYPE)`. Out-of-range values on `u8`/`u64` used as type discriminators bypass intended match arms or hit default cases

*Cross-ref: APT-13 (zero-value FA manipulation), APT-10 (vector unbounded growth), common-move.md 2.1 (arithmetic)*

---

## Aptos Verification Checklist

- [ ] All `table::borrow` / `table::remove` preceded by `table::contains`
- [ ] No generic `CoinType` functions without whitelist enforcement
- [ ] `SignerCapability` stored securely and access-gated
- [ ] No exact timestamp equality checks
- [ ] All critical operations emit events
- [ ] Upgrade policy noted and flagged if `arbitrary`
- [ ] No unbounded vector iteration in public functions
- [ ] Mixed `coin` / `fungible_asset` usage cross-checked
- [ ] `#[test_only]` functions not accessible in production
- [ ] `acquires` annotations verified for accuracy
- [ ] No test/debug/mock functions without `#[test_only]` attribute (APT-12)
- [ ] Zero-value FungibleAsset operations don't corrupt counters or limits (APT-13)
- [ ] No concurrent pending privilege requests that can both be claimed (APT-14)
- [ ] Ordered map key structs have primary sort field as first declared field (APT-15)
- [ ] No `SimpleMap` / `SmartTable` for permissionless unbounded data — use `Table` or `BigOrderedMap` (APT-16)
- [ ] No function returns or exposes `ConstructorRef` — check NFT mints especially (APT-17)
- [ ] Multiple resources at same object account are intentionally co-transferred (APT-18)
- [ ] `&mut` references re-validated after passing to untrusted code / callbacks (APT-19)
- [ ] Randomness functions are `entry` only (not `public`), equal gas on all paths (APT-20)
- [ ] Function value callbacks cannot re-enter with altered parameters — bind values into structs (APT-21)
- [ ] Struct field order and types preserved across upgrades — append-only or migration function exists (APT-22)
- [ ] Each resource account used by exactly one module — no cross-module signer scope creep (APT-23)
- [ ] Every `public entry fun` / `entry fun` with `&signer` validates address against stored authority — not just used as destination (APT-24)
- [ ] All entry function parameters validated: zero amounts, max limits, vector lengths, string lengths, zero addresses, enum-like ranges (APT-25)
- [ ] Objects that should NOT be freely transferable call `object::set_untransferable()` during construction (APT-17.5/6)
- [ ] `DeleteRef` only generated for objects intended to be burnable — not generated "just in case" (APT-17.6)

### Aptos Build & Test Commands

Run these during Phase 1 build detection when `BUILD_AVAILABLE = true`:

```bash
# Compile — catches type errors, missing acquires, ability violations
aptos move compile

# Run tests — catches logic bugs, assertion failures
aptos move test

# Coverage — target 100% on security-critical modules
aptos move test --coverage
aptos move coverage summary

# Per-module coverage detail
aptos move coverage source --module <module_name>
```

Flag if coverage is below 80% on any module containing `entry fun` or `borrow_global_mut`.

## audit-prompts.md

# Move Audit Prompts & Vulnerability Pattern Pack

Supplementary prompts for deep-dive manual review. Load this file when you want
targeted prompts for specific modules, functions, or resources.

---

## Generic Move Audit Prompts (Adapted Checklist)

Use these prompts directly during manual review or with an AI assistant. Replace
`<module>`, `<function>`, and `<resource>` with concrete code targets.

### A. Attack Surface & Privileges

- "List every `public` and `entry` function in `<module>`. For each, state who is allowed to call it and how that is enforced on-chain."
- "Find every code path that can mutate protocol-critical state in `<module>`. Highlight any path that lacks signer or capability authorization."
- "Identify all capability-like structs in `<module>`. Explain whether any ability set (`copy`, `store`, `drop`) makes privilege escalation possible."
- "Trace admin authority from initialization to current state. Can admin rights be lost, duplicated, or unintentionally transferred?"

### B. Asset & Value Flow

- "Trace all asset inflows/outflows in `<module>` and confirm accounting invariants hold after each state transition."
- "For each user credit/mint operation, show the exact on-chain asset movement that backs it. Flag any synthetic or unbacked credit path."
- "Check whether any withdraw/redeem path allows receiving more value than deposited due to rounding, ordering, or stale state."
- "Identify whether the protocol can become insolvent if edge-case aborts happen mid-flow."

### C. Arithmetic & Precision

- "Review all arithmetic in `<function>`. Show where caller-controlled inputs can force aborts (underflow, overflow, divide-by-zero)."
- "Find all formulas using division. Verify multiplication-before-division where precision matters."
- "Identify all casts to smaller integer types and verify explicit bounds checks before narrowing."
- "Evaluate whether rounding direction (up/down) consistently favors protocol safety."

### D. Resource & Storage Safety

- "Review every `move_from`, `borrow_global`, and `borrow_global_mut` use in `<module>`. Verify ownership or capability checks are enforced first."
- "Identify any storage read/write that can abort via missing resource/key and assess whether this can be used for DoS."
- "Map lifecycle of each `<resource>`: creation, storage, mutation, and destruction. Flag orphaned or unreachable states."
- "Check whether resource extraction, table removal, or object transfer can occur for addresses not controlled by caller."

### E. State Machines & Invariants

- "Document the intended state machine for `<module>`, then list all valid transitions and where each is enforced."
- "Find transitions that can be skipped, repeated, or executed out of order."
- "List core invariants (supply, collateralization, ownership, one-time init) and show where each invariant is asserted."
- "Review boundary checks (`>`, `>=`, `<`, `<=`) and identify off-by-one conditions that unlock restricted actions."

### F. External Dependencies & Integrations

- "List all cross-module calls from `<module>` and explain assumptions made about return values and side effects."
- "Check whether protocol state is left inconsistent before external calls and whether failures can strand partial updates."
- "Identify dependencies on upgradeable external modules and describe how an upgrade could violate local assumptions. On Sui: does the dependency use object version checks? If so, an upgrade changes all object versions — will the audited protocol's calls to the old package fail permanently?"
- "For every external call that reads a value then calls into another module: does the called module internally mutate the read value (e.g., interest accrual, reward distribution)? If so, the pre-read value is stale. Check yield vaults, lending wrappers, and aggregators built on top of other protocols."
- "For oracle/pricing dependencies, verify stale, missing, or manipulated data cannot create profitable attack paths."

### G. Initialization, Upgrades, Emergency Controls

- "Audit `init`/`initialize` logic: prove it is one-time-only and cannot be replayed through alternate entry points."
- "Identify all upgrade authorities and classify operational risk (single key, multisig, timelock, immutable)."
- "Check if emergency pause/kill-switch exists, who controls it, and whether it can be abused for censorship or fund lock."
- "Review migration/upgrade flows for storage compatibility and privilege continuity."

### H. Adversarial Scenario Prompts

- "Assume attacker has zero privileges and arbitrary call sequencing. What is the shortest path to unauthorized fund movement?"
- "Assume attacker can create many accounts and send dust inputs. Can they trigger systemic aborts or gas-based DoS?"
- "Assume attacker can exploit timing/epoch boundaries. Which functions become exploitable at boundary conditions?"
- "Assume a privileged key is compromised. What is maximum blast radius and time-to-mitigation?"

---

## Move Vulnerability Patterns Prompt Pack (from web3-sec-ai-prompts)

Source: `common/move-patterns.md` in Panther Audits `web3-sec-ai-prompts`.

### Purpose

Use this prompt to check a Move contract against the most common vulnerability
patterns found across 200+ public Move audit reports (1141 findings). Covers
Sui, Aptos, Supra, and other Move-based chains.

Reference database: [Move Vulnerability Database](https://movemaverick.github.io/move-vulnerability-database/)

### Master Prompt

```text
You are a Move smart contract security expert. Review the following contract and check for these vulnerability patterns, derived from 1141 real findings across 200+ audited Move protocols.

[Paste contract code or reference file path]

The top 5 vulnerability classes account for 70%+ of all Critical/High findings in Move. Check them first.

1. Business Logic (296 findings, 21 Critical, 58 High)
- Reward/staking timing exploits
- Flash loan reward manipulation
- Liquidation logic flaws
- Partial close/withdrawal bypasses
- Pool creation validation
- Constant product invariant breaks
- State reset on update
- Queue/tree data structure bugs

2. Input Validation (170 findings, 16 Critical, 29 High)
- Missing generic type checks
- Missing UID/object validation
- Flash loan receipt manipulation
- Zero-value inputs
- Arbitrary asset repayment
- Signature validation
- Uncallable functions

3. Calculation Errors (148 findings, 13 Critical, 28 High)
- Precision/decimal mismatches
- Scaled vs unscaled mixing
- Time constant errors
- Double scaling
- Share price manipulation
- Arithmetic overflow
- Formula errors
- Missing rewarder updates
- Refund precision

4. Access Control (73 findings, 13 Critical, 20 High)
- Public function visibility (`public` vs `public(package)`/`public(friend)`)
- Missing capability checks
- Resource signer exposure
- Liquidation access control inconsistencies
- Test code in production
- Pool creation permissions
- Front-running via public minting

5. State Management (64 findings, 7 Critical, 14 High)
- Stale state dependencies
- Incorrect index tracking
- Tail pointer corruption
- Accumulator ordering
- Timestamp manipulation
- Recording zero values

6. Oracle Issues (27 findings, 3 Critical, 5 High)
- Stale price acceptance
- Price manipulation via low-liquidity sources
- Incorrect decimal scaling
- Missing circuit breaker / deviation bounds

7. Denial of Service (40 findings, 2 Critical, 4 High)
- Unbounded loops over dynamic collections
- Single bad entry blocking batch operations
- Arithmetic overflow causing function-level DoS

8. Data Inconsistency (31 findings, 2 Critical, 10 High)
- Non-atomic state updates across related variables
- Incorrect/stale event emission
- Cross-module state assumption drift

9. Constant Definition (21 findings, 3 Critical, 2 High)
- Wrong constant values
- Constants not matching specs/docs
- Hardcoded values that should be configurable

10. Front-Running (7 findings, 0 Critical, 3 High)
- Ordering manipulation
- Payload front-running
- Missing commit-reveal for sensitive flows

For each pattern found:
1. State the specific vulnerability class from the list above
2. Indicate severity (Critical/High/Medium/Low) with justification
3. Point to the exact code location
4. Describe the exploit scenario
5. Reference similar historical findings from Move audits if applicable
```

### High-Signal Usage Tips

- Prioritize top 3 classes first: business logic, input validation, and calculation errors.
- Always audit generic type parameter validation in every function that accepts generic types.
- Treat Move function visibility as a first-class access-control surface: review every `public` function.
- Use this with the verification checklist in `common-move.md` for broad + Move-specific coverage.

---

## MVD-Derived Targeted Prompts

These prompts target the most frequently exploited patterns from 200+ real Move audit reports.
Use them for focused deep-dives after the initial scan.

### I. Generic Type & Receipt Validation

- "Find every function with a generic type parameter `<T>`, `<CoinType>`, `<X, Y>`, etc. For each, trace how the type is validated. Flag any function where the type parameter is not bound to the pool, vault, or receipt it operates on — an attacker can pass any coin type."
- "Find all flash loan / flash swap functions. Trace the receipt from creation to repayment. Is the receipt's pool ID / coin type validated during repayment? Can a receipt from Pool A be repaid to Pool B?"
- "Search for functions that accept an object reference (`&T`, `&mut T`) used for pricing, share calculation, or permission checks. Is the object's ID validated against a registry or known constant? Could an attacker create their own instance with manipulated values?"

### J. Constant & Scaling Verification

- "Grep all `const` definitions in the codebase. For each constant: (a) verify the value matches its name (e.g., `DAY_SECONDS` should be 86400), (b) check MAX_U64/MAX_U128 have the correct number of digits, (c) verify time constants (seconds vs milliseconds) are consistent with how they're used. Flag any mismatch — these are Critical/High bugs."
- "Find all variables named `scaled_*`, `index_*`, or `*_per_share`. Trace every arithmetic operation that uses them. Are they ever mixed with raw token amounts without conversion? Are there places where a scaled value is compared to an unscaled value or vice versa?"

### K. State Update & Repeated Action Prevention

- "For every function that transfers tokens, mints shares, or distributes rewards: does it update state to prevent being called again for the same entitlement? Search for claim/refund/withdraw functions that don't set a `claimed` flag, don't burn the receipt, or don't decrement the claimable balance."
- "Find every fee collection point (`balance::join`, `coin::put`, `coin::merge_all`). For each, trace if there's a corresponding admin withdrawal function. If fees accumulate with no extraction path, they're permanently locked."

### L. Accumulator & Reward Manipulation

- "Identify all reward accumulator / `reward_per_token` / `reward_per_share` update logic. Can an attacker stake a large amount, trigger an accumulator update, then immediately unstake and claim inflated rewards — all in one transaction? Is there a minimum staking duration enforced?"
- "For every stake/unstake function pair: simulate a flash loan attack where an attacker borrows → stakes → claims → unstakes → repays in one transaction. What is the maximum extractable value? Is this prevented by time-based locks or snapshot-based calculations?"

### M. Liquidation & Solvency

- "Trace the complete liquidation flow from health check to collateral seizure. At each step, verify: (a) the correct variable is passed (debt amount vs collateral amount), (b) solvency is checked AFTER withdrawal/repayment, (c) liquidation cannot be blocked by cooldowns or paused states, (d) remaining collateral is returned to the user, not destroyed."
- "For every `withdraw` function in a lending protocol: is there a solvency check AFTER the withdrawal amount is deducted? Can a user withdraw collateral while their position is underwater?"

---

## Audit Methodology — Rules & Heuristics

Core audit rules and thinking heuristics. Apply these throughout every review
alongside the pattern-specific checks above.

### 1. Trust Model

- **Admin is trusted** — check the specs/docs for which roles are trusted.
  Don't report findings based on trusted actors acting maliciously. However,
  **logic bugs in admin actions are valid findings** — if an admin function
  has a code bug that causes unintended behavior, that's reportable.
- **Defense in Depth:** Assume any privileged account can be compromised.
  Evaluate blast radius — can a compromised admin rug the entire protocol?
  Can they drain all funds in one transaction, or are there timelocks/limits?
- **Principle of Least Privilege:** No actor should have more power than needed.
  If a value can be read on-chain, a function shouldn't accept it as an
  arbitrary parameter. If a function only needs read access, it shouldn't
  take `&mut`.

### 2. Asymmetry Detection

This is one of the highest-signal audit techniques. Open similar functions
side by side and compare line by line.

**Compare these pairs:**
- `deposit` vs `withdraw`
- `buy` vs `sell`
- `mint` vs `burn`
- `borrow` vs `repay`
- `stake` vs `unstake`
- User version vs admin version of the same action (e.g., user redemption vs force redemption)

**What to look for:**
- A check present in one function but missing in its counterpart
- One function uses an oracle/on-chain value; its counterpart accepts an arbitrary parameter
- Different rounding directions that should be symmetric (or asymmetric for protocol safety)
- Admin functions are underrepresented in testing and frequently contain critical bugs —
  devs focus on user flows and neglect admin flows

**Bad symmetry (defensive code as a vulnerability):**
A safety check duplicated from a "prepare" function into a "redeem/claim" function
can cause permanent DoS if the prepare step already decremented the counter to zero.
Too-restrictive checks can brick functionality — defensive code can itself be a
critical vulnerability.

### 3. State Variable Deep Dive

- **Enumerate all state variables** (struct fields in `key` resources, shared objects,
  table entries). For each one:
  - Where is it written? Is it updated correctly at every mutation point?
  - Can an attacker manipulate it to reach an exploitable state?
  - Is it ever read after a mutation in the same function without refresh?
- **Setter/update functions:** When a setter changes a state variable, does it
  retroactively impact any live instance? E.g., changing a fee rate mid-epoch
  that affects already-accrued rewards, or updating an address without
  reclaiming tokens/allowances from the old one.
- **Coding pattern present everywhere except one place:** If a state update
  pattern (e.g., `update_rewards()` before balance change) appears in 9 out
  of 10 functions, the missing 10th instance is likely a bug.

### 4. Constants & Formulas

- For every `const` defined in the contract: check if it's defined correctly,
  check its usage in the code. Is it logically correct? Is it technically
  implemented correctly? The formulas and math where it's used — are they
  correct or do they have bugs?
- **Copy-paste errors:** When you see similar constants, hashes, or IDs,
  verify they're actually different. Search for duplicate values across the
  codebase with grep.
- See `common-move.md` section 8.4 for specific constant bugs from real audits.

### 5. Input Validation & Edge Cases

- **Check for these edge-case inputs on every public/entry function:**
  - Zero values (`amount = 0`) — does it corrupt state, skip logic, or divide-by-zero downstream?
  - Very small values (1 unit) — dust attacks, rounding exploits
  - Very large values (near MAX_U64) — overflow in multiplication
  - Empty vectors/lists — loop bypass: if a `while` loop iterates over a user-supplied
    vector and returns a result, what happens with an empty vector? The loop is skipped
    and may return a default value (e.g., `true`) that bypasses validation.
  - Identical inputs (same address for sender and recipient, same coin for both sides of a swap)
  - Unvalidated object/address references — can a user pass a malicious object that
    implements the expected struct layout?
- **Do functions validate that token/coin types actually belong to the protocol?**
  Can a user pass in a worthless self-created coin type?
- **Many small ops vs one large op:** Do many small deposits/withdrawals produce
  the same end state as one large one? If not, there's a bug (rounding, fees,
  state corruption). This is a powerful black-box testing technique.

### 6. Arithmetic & Precision

- **Casting bugs:** Casting a `u128` to `u64` truncates silently. Multiplying
  two `u64` values and storing in `u64` overflows even if the result is assigned
  to a `u128` later — must cast *before* multiplying.
- **Precision annotation technique:** For each variable in a formula, annotate
  its decimal precision (e.g., `// 6 decimals`, `// 18 decimals`, `// RAY = 27`).
  Look for addition/subtraction between variables of different precision.
  Common pattern: protocol uses internal precision (e.g., 18 decimals) but
  interacts with tokens of different precision (e.g., USDC 6 decimals) —
  look for missing or incorrect conversion.
- **Off-by-one errors:** `<` vs `<=`, especially with slightly different checks
  in different functions. Compare boundary conditions across all functions that
  reference the same threshold.

### 7. Look for What's Missing

**Missing checks are harder to spot than incorrect ones.** Train yourself to
notice absence:

- Missing access control on a privileged function
- Missing solvency check after withdrawal
- Missing existence check before table access
- Missing state update after claim/refund
- Missing fee withdrawal function (locked funds)
- Missing pause mechanism (no emergency brake)
- Missing minimum amount check (enables dust/1-wei attacks — recommend protocols
  implement minimums to cut off these vectors)
- Missing duplicate check when adding to a list (duplicates can break downstream logic)
- Missing slippage protection on any function that interacts with AMM pools
  (especially admin functions like unpause, setPositionWidth, rebalance)
- Missing reclamation when updating an address (if a function updates an external
  contract address, does it first reclaim tokens from the old address?)
- Missing decimal adjustment when changing a token address (if a function allows
  changing a token, does the new token have different decimals? Will that corrupt
  internal accounting?)

### 8. Unchecked Return Values

- Functions that return `bool` instead of aborting on failure — do callers check
  the return value? In Move, most operations abort on failure, but custom functions
  may return `bool` or `Option` to indicate success/failure.
- Check all cross-module calls: is the return value validated before being used
  in critical logic?
- Especially dangerous: functions that return `(bool, u64)` where the caller
  uses the `u64` without checking the `bool`.

### 9. Black-Box Testing Mindset

- Code up scenarios with known expected inputs, outputs, and state changes.
  Run them mentally (or write test cases) and see if actual results match.
  Reverse-engineer the bug location from unexpected outputs.
- **Gaps in test suite:** Look for untested interactions between modules,
  edge cases not covered, and paths that are only tested with happy-case inputs.
- **Use the protocol as an attacker would:** Try to construct a sequence of
  transactions that reaches an invalid state. Think in PTBs (Sui) or
  multi-step transactions (Aptos).

### 10. Beyond the Checklist

The rules above cover known patterns. But the highest-value findings often come
from areas NOT on any checklist. Use your experience and imagination:

- **Explore all possible paths** — check every single line and all flows
- **Question every assumption** the developer made
- **Think about what the code does, not what it's supposed to do**
- **Read the code as if you've never seen it before** — fresh eyes catch
  what familiarity blinds you to

## benchmarks

```

```

## benchmarks/BENCHMARK-currensui.md

# Move Auditor Skill Benchmark — CurrenSui Lending Protocol

> **Protocol**: CurrenSui — Sui Move lending protocol  
> **Contest**: Sherlock, 6,470 nSLOC, March 2026  
> **Date**: 2026-03-11  
> **Commit audited**: `dc2975d`  
> **Tools compared**: `pantheraudits/move-auditor` · `forefy/.context` · Raw Claude CLI (no skill)

---

## Ground Truth — 6 Confirmed Known Issues

| ID | Description | File | Severity |
|---|---|---|:---:|
| KI-1 | eMode stale aggregate → borrow cap silently exceeded | `emode.move:183` | High |
| KI-2 | `deposit_limit_breached` double-subtracts `cash_reserve` | `reserve.move:89` | High |
| KI-3 | `update_market_asset_interest_model` missing pre-accrual → retroactive rate | `market.move` | High |
| KI-4 | ADL uses `reserve.debt()` instead of `emode_group.borrow_amount()` → wrongful liquidation | `adl.move` | High |
| KI-5 | `liquidate_ctokens` reverts at high utilization → bad debt accumulates | `reserve.move:171` | High |
| KI-6 | `repay_on_behalf` orphans reward tracker → admin reward pool permanently locked | `repay.move` + `liquidity_mining.move` | High |

---

## Known Issue Coverage

| ID | Known Issue | `move-auditor` | `forefy/.context` | Raw Claude CLI |
|---|---|:---:|:---:|:---:|
| KI-1 | eMode stale aggregate borrow cap bypass | ✅ | ❌ | ✅ |
| KI-2 | `deposit_limit_breached` double-subtraction | ✅ | ❌ | ❌ |
| KI-3 | Retroactive interest rate — missing pre-accrual | ❌ | ❌ | ❌ |
| KI-4 | ADL wrong debt source → wrongful liquidation | ❌ | ❌ | ⚠️ Partial |
| KI-5 | Liquidation reverts at high utilization | ❌ | ❌ | ❌ |
| KI-6 | `repay_on_behalf` orphans reward tracker | ❌ | ⚠️ Partial* | ❌ |
| **Score** | | **2 / 6** | **0 / 6** | **1 / 6** |

> ⚠️ Partial = found the function but diagnosed the wrong root cause

---

## False Positive Rate

| Tool | Reported | Valid | False Positives | FP Rate |
|---|:---:|:---:|:---:|:---:|
| `move-auditor` | 4 | 2 | 1 Low/Info* | ~25% |
| `forefy/.context` | 5 | 0 | 5 | 100% |
| Raw Claude CLI | 5 | 1–2 | 3 | ~60% |

> *N-1 (partial liquidation skips `min_borrow_amount`) is a real code defect, Low/Info severity — not a hallucination, just over-classified one level

---

## Summary Scorecard

| Metric | `move-auditor` | `forefy/.context` | Raw Claude CLI |
|---|:---:|:---:|:---:|
| Known issues found | **2 / 6** | 0 / 6 | 1 / 6 |
| False positive rate | **~25%** | 100% | ~60% |
| Severity accuracy | Conservative | N/A | Correct |
| First-run reliability | Needed re-prompt | ✅ | ✅ |
| Run time | ~45 min | 25 min | 49 min |

---

## Verdict

| Rank | Tool | Reason |
|:---:|---|---|
| 🥇 | **`move-auditor`** | Best coverage (2/6), lowest FP rate, zero hallucinated bugs |
| 🥈 | Raw Claude CLI | Correct on what it found, but 5/6 missed and high FP rate |
| 🥉 | `forefy/.context` | 100% FP rate — triage correctly rejected all 5, but underlying audit found nothing valid |

---

## Patterns Every Tool Missed

| Gap | Affects | Description |
|---|---|---|
| Admin setter without pre-state-sync | KI-3 | Rate model update without prior `accrue_interest()` applies new rate retroactively |
| Cross-module lifecycle cleanup | KI-6 | Permissionless repay creates orphaned tracker in a separate rewards module |
| Cash availability before liquidation | KI-5 | `liquidate_ctokens` calls `balance.split()` without checking idle cash ≥ seize amount |
| Dual-source metric consistency | KI-4 | ADL entry check and stop check read total debt from different sources |

---

*All findings manually verified against source at commit `dc2975d`.*

> **Note:** This benchmark may not be 100% accurate. If you spot any mistakes, have questions, or believe something is incorrect, please reach out on X ([@thepantherplus](https://x.com/thepantherplus)) so it can be corrected.

## benchmarks/BENCHMARK-openzeppelin.md

# Move Auditor Skill Benchmark — OpenZeppelin Contracts for Sui

> **Protocol**: OpenZeppelin Contracts for Sui — Move library suite (access control + math)
> **Code**: https://github.com/OpenZeppelin/contracts-sui
> **Model**: Claude Opus 4.6 max
> **Date**: 2026-03-24
> **Tools compared**: `pantheraudits/move-auditor` · `Monethic/monethic-maia` (MAIA) · Raw Claude CLI (no skill)

---

## Codebase Summary

| Metric | Value |
|---|---|
| Chain | Sui Move |
| Source files | 19 |
| Source LoC | ~5,500 |
| Test files | 42 |
| Packages | `openzeppelin_access`, `openzeppelin_math`, `openzeppelin_fp_math` |
| Entry points | 0 `entry fun` — all `public fun` (library-only) |
| Nature | Composable library — no protocol logic, no DeFi state |

---

## Ground Truth — 12 Verified Findings (Manual Verification)

All findings were manually verified against source code at commit `7cfc07c`. Since this is a library codebase (not a contest), ground truth was established through independent code review rather than from known issues.

**Verdict: 0 Critical, 0 High, 0 Medium, 3 Low, 9 Informational — zero exploitable bugs.**

| ID | Description | File | Verified Severity |
|---|---|---|:---:|
| VF-1 | Missing descriptive error on div-by-zero in fixed-point `div`/`mod` — relies on opaque VM abort instead of `EDivideByZero` like core math module | `ud30x9_base.move:287`, `sd29x9_base.move:312` | Low |
| VF-2 | No minimum bound on `min_delay_ms` — allows zero-delay wrap defeating time-lock purpose | `delayed.move:114` | Info |
| VF-3 | No upper bound on `min_delay_ms` — near-`u64::MAX` value causes overflow abort in `schedule_transfer`, permanently locking wrapped object | `delayed.move:214,246` | Low |
| VF-4 | Shared-object executor can bind cancel authority via `ctx.sender()` — documented design constraint with 3 security warnings | `two_step.move:254` | Info |
| VF-5 | `PendingOwnershipTransfer` shared object permanently orphaned if both `from` and `to` lose access — no timeout mechanism | `two_step.move:267` | Info |
| VF-6 | `casting_u128::into_UD30x9` name misleadingly suggests scaling but is just `wrap()` — `into_UD30x9(42)` gives `0.000000042` not `42.0` | `casting/u128.move:13` | Low |
| VF-7 | Quicksort `O(n^2)` worst-case gas consumption — documented, mitigated by median-of-three | `vector.move:64` | Info |
| VF-8 | `pow()` uses `O(n)` repeated multiplication with compounding truncation error — documented, intentional for truncation semantics | `ud30x9_base.move:313`, `sd29x9_base.move:343` | Info |
| VF-9 | `borrow_mut`/`borrow_val` allows unrestricted mutation of wrapped object during pending delayed transfer — by design, owner has custody | `delayed.move:153` | Info |
| VF-10 | Fixed-point `mul`/`div` truncate toward zero without rounding option — industry standard, documented | `sd29x9_base.move:291`, `ud30x9_base.move:266` | Info |
| VF-11 | Two-step transfer accept/cancel race on shared object — intended security property (owner retains cancel rights) | `two_step.move:267` | Info |
| VF-12 | `cancel_schedule` works after delay elapses — delay provides observation time, not binding commitment | `delayed.move:342` | Info |

---

## Finding Coverage

| ID | Verified Finding | `move-auditor` | MAIA | Raw Claude CLI |
|---|---|:---:|:---:|:---:|
| VF-1 | Missing descriptive div-by-zero in FP ops | ✅ Low | ❌ | ❌ |
| VF-2 | No minimum bound on `min_delay_ms` | ✅ Low | ✅ Low | ✅ Medium |
| VF-3 | No upper bound on `min_delay_ms` / overflow lock | ❌ | ✅ Low | ⚠️ Medium* |
| VF-4 | Shared-object executor binds cancel authority | ✅ Info | ✅ Medium | ✅ Medium |
| VF-5 | Orphaned `PendingOwnershipTransfer` | ✅ Info | ✅ Low | ✅ Info |
| VF-6 | `into_UD30x9` misleading name | ❌ | ❌ | ✅ Low |
| VF-7 | Quicksort `O(n^2)` worst-case | ✅ Info | ❌ | ✅ Low |
| VF-8 | `pow()` compounding truncation | ✅ Info | ❌ | ✅ Low |
| VF-9 | `borrow_mut` during pending transfer | ❌ | ✅ Info | ❌ |
| VF-10 | `mul`/`div` truncation (no rounding option) | ✅ Info | ✅ Info | ❌ |
| VF-11 | Accept/cancel race | ✅ Info | ✅ Info | ❌ |
| VF-12 | `cancel_schedule` after delay elapses | ❌ | ❌ | ✅ Info |
| **Score** | | **8 / 12** | **7 / 12** | **7 / 12** |

> ⚠️ Raw CLI M-1 described wrong overflow mechanism — claimed Move "wraps" on overflow when it actually **aborts** (checked arithmetic). The permanent-lock effect is real but the technical explanation is incorrect.

---

## False Positive Analysis

| Tool | Total Reported | Valid | False Positives | FP Rate |
|---|:---:|:---:|:---:|:---:|
| `move-auditor` | 7 | 7 | 0 | **0%** |
| MAIA | 8 | 7 | 1 | **12.5%** |
| Raw Claude CLI | 9 (non-info) | 7 | 2 | **22.2%** |

### False Positive Details

| Tool | Finding | Why It's a False Positive |
|---|---|---|
| MAIA | CF-008: SD29x9 `pow()` uses `<=` instead of `<` | The `<=` is correct — must allow `res_mag == 2^127` for the legitimate negative minimum case (`-2^127`). Changing to `<` would break valid computations. `wrap_components` correctly handles the final range check. |
| Raw CLI | M-4: Transfer to `@0x0` = permanent loss | `@0x0` is a valid Sui address (framework address). Checking it specifically wouldn't prevent transfers to any other uncontrolled address. Self-inflicted by owner across two deliberate calls with delay. MAIA correctly rejected this as FP-16. |
| Raw CLI | M-1: Overflow mechanism error | Claimed Move "wraps" on overflow — Move uses **checked arithmetic** and aborts. The permanent-lock effect is real but the technical analysis is factually wrong. Counted as partial (found the effect, wrong mechanism). |

### MAIA Internal Triage Quality

MAIA's pipeline deserves special note: it generated 73 raw findings, deduplicated to 26, and rejected 18 as false positives — **all 18 rejections were correct**. Notable correct rejections:

| FP ID | What Raw CLI reported as | MAIA's correct rejection rationale |
|---|---|---|
| FP-16 | M-4 (Medium) | `@0x0` valid on Sui; `cancel_schedule` provides safety net |
| FP-17 | — | Push-based transfer is the design; two-step exists for pull-based |
| FP-10 | L-3 (Low) | Library macro, documented worst-case, gas metering bounds execution |
| FP-12 | — | Hot potato + ID checks + Move type system = sufficient safety |

---

## Severity Accuracy

How accurately each tool classified the findings it found (compared to verified severity):

| Tool | Exact Match | Over by 1 | Over by 2+ | Accuracy |
|---|:---:|:---:|:---:|:---:|
| `move-auditor` | 7 / 8 | 1 / 8 | 0 / 8 | **87.5%** |
| MAIA | 4 / 7 | 2 / 7 | 1 / 7 | **57.1%** |
| Raw Claude CLI | 2 / 8 | 3 / 8 | 3 / 8 | **25.0%** |

### Severity Misclassifications

| Tool | Finding | Reported | Verified | Delta |
|---|---|:---:|:---:|:---:|
| `move-auditor` | VF-2 (zero delay) | Low | Info | +1 |
| MAIA | VF-2 (zero delay) | Low | Info | +1 |
| MAIA | VF-4 (shared executor) | Medium | Info | +2 |
| MAIA | VF-5 (orphaned transfer) | Low | Info | +1 |
| Raw CLI | VF-2 (zero delay) | Medium | Info | +2 |
| Raw CLI | VF-3 (overflow lock) | Medium | Low | +1 |
| Raw CLI | VF-4 (shared executor) | Medium | Info | +2 |
| Raw CLI | VF-7 (quicksort) | Low | Info | +1 |
| Raw CLI | VF-8 (pow truncation) | Low | Info | +1 |
| Raw CLI | VF-12 (cancel after delay) | Low | Info | +1 |

> Raw Claude CLI reported **4 Medium findings** when the verified ground truth contains **zero Mediums**. This indicates a systematic tendency to over-classify severity.

---

## Summary Scorecard

| Metric | `move-auditor` | MAIA | Raw Claude CLI |
|---|:---:|:---:|:---:|
| Findings found | **8 / 12** | 7 / 12 | 7 / 12 |
| False positive rate | **0%** | 12.5% | 22.2% |
| Severity accuracy | **87.5%** | 57.1% | 25.0% |
| Factual errors | **0** | 0 | 1 (Move overflow) |
| Unique valid finds | 1 (VF-1) | 1 (VF-9) | 2 (VF-6, VF-12) |
| Highest false severity | Low (1x) | Medium (1x) | Medium (4x) |
| Internal FP filtering | N/A | 18/18 correct | N/A |
| Report quality | Structured, clean | Most thorough | Over-stated conclusions |

---

## Verdict

| Rank | Tool | Reason |
|:---:|---|---|
| 🥇 | **`move-auditor`** | Best coverage (8/12), **zero false positives**, best severity accuracy (87.5%), found the only unique Low nobody else caught (div-by-zero inconsistency), no factual errors |
| 🥈 | **MAIA** | Strong coverage (7/12), excellent internal triage pipeline (18/18 correct FP rejections), most thorough report with detailed reasoning. Loses points for 1 FP (CF-008) and over-classifying CF-001 as Medium despite 3 security warnings in code |
| 🥉 | **Raw Claude CLI** | Same coverage (7/12) but worst severity accuracy (25%), 1 factual error about Move semantics, 2 false positives, and 4 findings at Medium when ground truth has zero Mediums. Found 2 unique issues (VF-6, VF-12) but offset by unreliable classifications |

---

## Key Observations

### What Every Tool Agreed On
All three tools correctly identified that this is a well-engineered library with no exploitable vulnerabilities. The zero-delay and shared-object executor findings were found by all three, confirming these are the most surface-level observations.

### What Only One Tool Found

| Finding | Tool | Why Others Missed It |
|---|---|---|
| VF-1: div-by-zero inconsistency | `move-auditor` | Requires comparing fixed-point module against core math module's `EDivideByZero` pattern — cross-module consistency check. **Submitted upstream and merged: [OpenZeppelin/contracts-sui#263](https://github.com/OpenZeppelin/contracts-sui/pull/263)** |
| VF-6: `into_UD30x9` naming | Raw CLI | API usability concern requiring understanding of the `wrap()` vs scaling distinction |
| VF-9: `borrow_mut` during transfer | MAIA | Requires reasoning about state visibility guarantees during delay windows |

### Patterns in Tool Behavior

| Pattern | Observation |
|---|---|
| **Severity inflation** | Raw CLI inflated every finding by 1-2 severity levels. All 4 "Mediums" should be Info or Low. |
| **Mechanism accuracy** | Raw CLI claimed Move wraps on overflow — a fundamental misunderstanding of Move's checked arithmetic. move-auditor and MAIA correctly described Move semantics. |
| **FP filtering** | MAIA's pipeline (73 → 26 → 8) demonstrates structured triage. Raw CLI had no filtering — everything it thought of was reported. |
| **Conservative vs. aggressive** | move-auditor was most conservative (no over-classification beyond +1). Raw CLI was most aggressive (systematic +2 inflation). |
| **Documentation awareness** | move-auditor and MAIA correctly recognized documented design choices. Raw CLI repeatedly flagged documented behavior as bugs. |

### Codebase Assessment (All Tools Agree)

The OpenZeppelin Contracts for Sui library is production-quality code:
- Excellent documentation with explicit security warnings
- Strong use of Sui patterns (hot potato, no `store` on wrappers)
- Correct math across overflow/underflow/rounding edge cases
- Healthy test-to-source ratio (~2.2:1 for test files to source files)
- Zero exploitable vulnerabilities found by any tool

---

### Upstream Contribution

The unique `move-auditor` find (VF-1: missing `EDivideByZero` guard) was submitted as a fix to OpenZeppelin and **merged** — [OpenZeppelin/contracts-sui#263](https://github.com/OpenZeppelin/contracts-sui/pull/263). This makes `move-auditor` a contributor to OpenZeppelin's Sui contracts.

---

*All findings manually verified against source at commit `7cfc07c`. Ground truth established through independent code review.*

> **Note:** This benchmark may not be 100% accurate. If you spot any mistakes, have questions, or believe something is incorrect, please reach out on X ([@thepantherplus](https://x.com/thepantherplus)) so it can be corrected.

## benchmarks/BENCHMARK.md

# Move Auditor Benchmarks

Comparative benchmarks measuring `move-auditor` against other AI audit tools on real Sui Move codebases.

---

## Benchmarks

| # | Target | Type | LoC | Tools Compared | Result |
|---|---|---|:---:|---|:---:|
| 1 | [CurrenSui Lending](BENCHMARK-currensui.md) | DeFi protocol (Sherlock contest) | 6,470 | `move-auditor` vs `forefy/.context` vs Raw CLI | 🥇 move-auditor |
| 2 | [OpenZeppelin Contracts](BENCHMARK-openzeppelin.md) | Library (hardened, no exploitable bugs) | 5,500 | `move-auditor` vs `MAIA` vs Raw CLI | 🥇 move-auditor |

---

## Aggregate Results

| Metric | `move-auditor` | Competitors (best) |
|---|:---:|:---:|
| Bug coverage | **8/12, 2/6** | 7/12, 1/6 |
| False positive rate | **0% – 25%** | 12.5% – 100% |
| Severity accuracy | **87.5%** | 57.1% |
| Factual errors | **0** | 1 (Move overflow semantics) |

---

### What each benchmark tests

- **CurrenSui** — Can the tool find real High-severity bugs in a complex DeFi lending protocol? Ground truth from Sherlock contest known issues.
- **OpenZeppelin** — Can the tool accurately assess a hardened library without hallucinating bugs? Tests false positive discipline and severity calibration on production-quality code.

Together they measure both **recall** (finding real bugs) and **precision** (not inventing fake ones).

> **Note:** These benchmarks may not be 100% accurate. If you spot any mistakes, have questions, or believe something is incorrect, please reach out on X ([@thepantherplus](https://x.com/thepantherplus)) so it can be corrected.

## checklist-router.md

# Checklist Router

Use this file at audit start to decide which deep-check files must be loaded.

The goal is simple: if the code exposes a signal, load the right reference file
and run the right follow-up check.

## Coverage Plan

Produce a short plan:

```md
### Coverage Plan
- Chain: Sui
- Signals: lending, oracle, accumulator, fixed-point
- Files loaded: `common-move.md`, `sui-patterns.md`, `defi/defi-lending.md`, ...
- Follow-ups: semantic-gap scan, fixed-point helper inspection, cross-module interaction scan
```

## Always Load

- `common-move.md`
- `verification-policy.md`
- `checklist-router.md`
- `move-fp-catalog.md`

## Verification Phase Loading

Load these files when entering Phase 7 — Verify & Triage:

- `evidence-chains.md` — structured evidence templates for data flow, math proofs, PoC
- `confidence-gates.md` — confidence gating, hard evidence requirements per finding type

## Chain Routing

| Signal | Load |
|--------|------|
| `sui::object`, `sui::transfer`, `sui::tx_context`, `UID` | `sui-patterns.md` |
| `aptos_framework`, `aptos_std`, `#[test_only]`, `SignerCapability`, `fungible_asset` | `aptos-patterns.md` |

## Protocol Routing

| Signal | Load | Follow-up |
|--------|------|-----------|
| `borrow`, `repay`, `collateral`, `health_factor`, `margin`, `risk_ratio`, `leverage` | `defi-vectors.md`, `defi/defi-lending.md` | cross-module interaction scan |
| `liquidat`, `seize`, `bad_debt`, `insurance`, `self_match` | `defi/defi-liquidation.md` | idle-cash and price-source checks |
| `oracle`, `pyth`, `switchboard`, `price_feed`, `twap` | `defi/defi-oracle.md` | stale/deviation audit |
| `reward_per_share`, `accumulator`, `claim`, `stake`, `unstake`, `reward_manager`, `pool_reward`, `liquidity_mining`, `total_rewards` | `defi/defi-staking.md`, `defi/defi-math-precision.md`, `semantic-gap-checks.md` | checkpoint/accumulator review + **mandatory DEFI-85/86 fixed-point overflow check** |
| `swap`, `pool`, `lp`, `min_amount_out`, `slippage` | `defi/defi-slippage.md` | PTB / multi-hop review |
| `ed25519`, `secp256k1`, `verify_signature`, `nonce` | `defi/defi-signatures.md` | replay / domain separation review |

## Feature Flags

| Signal | Action |
|--------|--------|
| `dynamic_field`, `dynamic_object_field`, `object::new`, `object::delete` | force object lifecycle cleanup review |
| Sui `public fun` mutates state | force PTB composability review |
| `fixed_point`, `decimal`, `wad`, `ray`, `float`, custom `Decimal` / `WAD` / `Float` wrapper, any `from().mul()` or `.mul().div()` chain | force fixed-point helper inspection + load `defi/defi-math-precision.md` |
| `last_update`, `checkpoint`, `index`, `cumulative` | load `semantic-gap-checks.md` |
| `rate_model`, `interest_model`, `reward_rate`, `fee_rate` admin setters | force pre-accrual review |
| `clock::timestamp_ms` combined with oracle timestamps | force unit-conversion review |

## Escalation Rules

- If lending is detected, run both semantic-gap and cross-module interaction review.
- If oracle is detected, always check stale price, deviation reference, and liquidation price-source consistency.
- If Sui stateful `public fun` is detected, think in PTB sequences, not single-call flows.
- **If any reward/accumulator/checkpoint pattern is detected**, force DEFI-85/86 + 12.1 checkpoint deadlock analysis. This is the **#1 missed bug class** in Move audits. Open every fixed-point helper, derive overflow bounds, compute threshold table, and apply the Recoverability Matrix.
- If any fixed-point/decimal/float helper library exists in the codebase, you MUST open and read its `mul`, `div`, `from` functions before completing Phase 3. Do not rely on calling code — inspect the helper internals.

## common-move.md

# Common Move Security Patterns

Chain-agnostic security checks that apply to all Move code, regardless of whether
it targets Sui or Aptos. Run these on every audit before loading chain-specific patterns.

---

## 1. Access Control

### 1.1 Missing Signer/Capability Validation
**Pattern:** `public entry fun` with no `&signer` parameter and no capability check.
**Risk:** Anyone can call the function.
**Check:** Every state-mutating entry function must either:
- Accept a `&signer` and validate it against a stored admin/owner address, OR
- Accept a capability object that is unforgeable (no `copy` ability)

```move
// VULNERABLE — no access control
public entry fun set_fee(new_fee: u64) {
    borrow_global_mut<Config>(@admin).fee = new_fee;
}

// SAFE — capability gating
public entry fun set_fee(cap: &AdminCap, new_fee: u64) {
    borrow_global_mut<Config>(@admin).fee = new_fee;
}
```

### 1.2 Overly Broad Capability Abilities
**Pattern:** Capability struct has `copy` or `store` ability.
**Risk:** Capabilities can be duplicated or stored arbitrarily, defeating the access model.
**Check:** Administrative capability structs should have zero or only `drop` ability.

```move
// VULNERABLE
struct AdminCap has copy, store, drop {}

// SAFE
struct AdminCap has drop {}
```

### 1.3 Hardcoded Address Checks
**Pattern:** `assert!(signer::address_of(account) == @0x1234, E_NOT_ADMIN)`
**Risk:** Admin address is immutable; if key is lost, the protocol is bricked. No upgrade path.
**Check:** Flag hardcoded address checks. Prefer capability-based patterns.

### 1.4 Two-Step Ownership Transfer Missing
**Pattern:** Ownership transferred in a single step without confirmation.
**Risk:** Typo in address permanently locks the protocol.
**Check:** Critical ownership transfers should use a pending → accept pattern.

### 1.5 API Naming Consistency (Misleading Conversion Functions)
**Pattern:** `into_X` / `from_X` conversion functions that don't actually perform conversion — they're raw wrappers that just reinterpret the value without scaling, type conversion, or validation.
**Risk:** Integrators assume the function performs real conversion (e.g., `into_UD30x9` implies scaling to 30-digit-9-decimal fixed-point), but it's just a struct wrap. Downstream math is silently wrong.
**Check:** For every `into_*` / `from_*` / `to_*` function, verify the function body matches what the name implies. Misleading names are Low/Informational.

### 1.6 Authorization Returns Bool Without Assertion
**Pattern:** Authorization function returns `bool` instead of aborting on failure. Callers can silently discard the return value, bypassing the check entirely.
**Risk:** If a caller writes `is_authorized(registry, addr);` instead of `assert!(is_authorized(registry, addr), E_NOT_AUTHORIZED)`, the authorization check runs but the result is ignored — access is granted unconditionally.

```move
// VULNERABLE — returns bool, caller can ignore the result
public fun is_authorized(registry: &Registry, addr: address): bool {
    registry.admins.contains(&addr)
}

// Caller forgets to check return value — authorization silently bypassed
public entry fun admin_action(registry: &Registry, ctx: &TxContext) {
    is_authorized(registry, tx_context::sender(ctx)); // return value discarded!
    do_critical_operation();
}

// SAFE — aborts on failure, cannot be silently ignored
public fun assert_authorized(registry: &Registry, addr: address) {
    assert!(registry.admins.contains(&addr), ENotAuthorized);
}
```

**Check:**
1. Grep for authorization/permission functions that return `bool` (names like `is_admin`, `is_authorized`, `has_role`, `check_permission`)
2. Trace every call site — is the return value used in an `assert!` or `if` check?
3. If ANY call site discards the return value → flag as High
4. Recommend converting to `assert_*` pattern that aborts on failure

---

## 2. Arithmetic & Overflow

### 2.1 Unchecked Integer Arithmetic
**Pattern:** Addition, subtraction, multiplication without overflow/underflow checks.
**Risk:** In Move, integer overflow **aborts** by default — but subtraction underflow on `u64` is a runtime abort that can be used as a DoS.
**Check:** Verify that arithmetic paths can't be forced into abort by a malicious caller.

```move
// POTENTIAL DOS — attacker supplies balance = 0
let result = balance - fee; // aborts if fee > balance
```

### 2.2 Division Before Multiplication (Precision Loss)
**Pattern:** `(a / b) * c` instead of `(a * c) / b`
**Risk:** Integer division truncation causes systematic precision loss, exploitable in DeFi.
**Check:** In any fee/interest/share calculation, verify multiplication happens before division.

### 2.3 Division by Zero
**Pattern:** Division where denominator can be zero.
**Risk:** Runtime abort (DoS).
**Check:** All divisions must assert denominator != 0.

### 2.4 Cast Truncation
**Pattern:** Casting from larger to smaller integer type (e.g., `u128` → `u64`).
**Risk:** Silent truncation of high bits.
**Check:** All narrowing casts should have bounds assertions.

### 2.5 Bit-Shift Wrapping (Silent Overflow)
**Pattern:** Bit-shift operations (`<<`, `>>`) in custom math or fixed-point libraries.
**Risk:** Unlike standard arithmetic (`+`, `-`, `*`), **bit-shifts in Move do NOT abort on overflow — they silently wrap**. `(1u64 << 64)` produces `0`, not an abort. `(x as u256) << 64` wraps around if the result exceeds `MAX_U256`. This makes custom overflow checks for bit-shifts critical — an off-by-one in the boundary condition (`<` vs `>=`) silently produces a corrupted result instead of aborting.
**Check:**
1. Grep for `<<`, `>>`, `shl`, `shr`, `checked_shl`, `checked_shr` in math/utility modules
2. For each shift: is the operand validated against bit-width overflow BEFORE the shift?
3. What is the exact boundary condition? Verify the comparison operator (`<` vs `<=` vs `>=`)
4. If the shift result feeds into balance, liquidity, supply, or share calculations → Critical

```move
// VULNERABLE — boundary check uses < instead of >=, allows value at exact boundary
fun checked_shl(n: u256, shift: u8): u256 {
    assert!(n < (1u256 << (256 - (shift as u16))), E_OVERFLOW); // off-by-one: n == boundary passes
    n << shift  // silently wraps to small number
}

// SAFE — correct boundary
fun checked_shl(n: u256, shift: u8): u256 {
    assert!(n <= (MAX_U256 >> (shift as u16)), E_OVERFLOW);  // tight bound
    n << shift
}
```

### 2.6 Fixed-Point Helper Library Overflow (Multiply-Before-Divide)

**Pattern:** Protocol uses a fixed-point math library (e.g., `float.move`, `decimal.move`, `wad_ray.move`) whose `mul` function computes `(a.value * b.value) / WAD` internally. The intermediate product `a.value * b.value` can overflow and abort **before** the normalizing division executes. When the caller does `A.mul(B).div(C)`, the overflow in `mul` fires before `div(C)` can reduce the result to a safe range.

**Risk:** If the abort occurs inside a periodic accounting update (reward accumulator, interest accrual, index refresh) and the state checkpoint (`last_update_time`, `cumulative_index`) is written **after** the overflowing line, the state never advances. Every future call hits the same overflow with an ever-growing time delta — **permanent, irrecoverable protocol deadlock**.

**Why this is different from 2.2:** Section 2.2 checks expression-level multiply-before-divide for precision loss. This check targets **hidden overflow inside helper library internals** that the calling module cannot see. The calling code looks safe (`from(x).mul(from(y)).div(from(z))`) but the helper's internal representation and bounds enforcement create an overflow that fires before the division.

```move
// VULNERABLE — overflow hidden inside float::mul
// float::mul does: (a.value * b.value) / WAD, then asserts result <= VALUE_MAX
// If total_rewards * time_passed_ms > U64_MAX, the mul aborts before div executes
let unlocked_rewards =
    float::from(pool_reward.total_rewards)
        .mul(float::from(time_passed_ms))       // <-- OVERFLOW HERE
        .div(float::from(duration));             // never reached

// SAFE — divide first, then multiply (intermediate stays small)
let unlocked_rewards =
    float::from(pool_reward.total_rewards)
        .div(float::from(duration))              // total_rewards / duration <= total_rewards
        .mul(float::from(time_passed_ms));       // result * time_passed <= total_rewards
```

**Check:**
1. Identify ALL fixed-point/decimal helper modules used by the target protocol (`float`, `decimal`, `wad_ray`, `fixed_point32`, `fixed_point64`, `math`)
2. **Open each helper module.** Read `mul`, `div`, `from`, `floor`, `ceil`. Derive:
   - Internal representation (e.g., `value * WAD` where WAD = 1e18)
   - Intermediate expression in `mul` (e.g., `a.value * b.value / WAD`)
   - Maximum allowed value (e.g., `VALUE_MAX = U64_MAX * WAD`)
   - Whether overflow check fires before or after the normalizing division
3. For every call site of the form `A.mul(B).div(C)` or `A * B / C` using helpers:
   - Derive: can `A * B` (in raw scaled representation) exceed the helper's max before `/ C` executes?
   - Derive concrete bounds: what values of A and B trigger overflow?
   - Use production-realistic values (token decimals, time in ms, reward amounts)
4. If overflow is reachable, check whether it occurs **before a state checkpoint** (see 12.1)
5. Cross-ref: 12.1, DEFI-85, DEFI-86

### 2.5 Bitwise Operations — No Overflow Protection

**Pattern:** Move auto-aborts on arithmetic overflow (addition, subtraction, multiplication), but bitwise operations (`<<`, `>>`, `&`, `|`, `^`) have **no such safeguards**. Bit shifts can silently overflow or produce unexpected results.

```move
// VULNERABLE — left shift can silently lose high bits
let shifted = value << amount;  // no overflow abort like arithmetic ops

// SAFE — guard shift amount and check for overflow
assert!(amount < 64, E_SHIFT_OVERFLOW);
assert!(value <= (MAX_U64 >> amount), E_WOULD_OVERFLOW);
let shifted = value << amount;
```

**Check:**
1. Grep all bitwise operations (`<<`, `>>`, `&`, `|`, `^`) in the codebase
2. For each left shift: can the shift amount exceed the bit width? Can high bits be lost?
3. For each right shift: is precision loss acceptable?
4. Especially dangerous in fee calculations, fixed-point math, and bitmap/flag manipulation

### 2.7 Delayed Overflow via Immutable Construction Parameters

**Pattern:** A struct field is set at construction time (e.g., `wrap`, `new`, `init`) and never validated for upper bounds. Later, the field is combined with a runtime value (e.g., `clock::timestamp_ms() + self.delay`) in arithmetic that can overflow `u64`.

**Risk:** Since Move uses checked arithmetic (abort, not wrap), the overflow permanently bricks the function. If the field is immutable (no setter, no admin rescue), the object is permanently locked with no recovery path. This is distinct from 2.6 (library-internal overflow) — here the overflow is in application-level arithmetic between a stored param and a runtime value.

```move
// VULNERABLE — no upper bound on delay, permanently locks object if near u64::MAX
public fun wrap<T: key + store>(obj: T, min_delay_ms: u64, ctx: &mut TxContext): DelayedWrapper<T> {
    // min_delay_ms stored as-is, no validation
    DelayedWrapper { id: object::new(ctx), obj, min_delay_ms }
}

public fun schedule<T: key + store>(self: &mut DelayedWrapper<T>, clock: &Clock) {
    let deadline = clock::timestamp_ms(clock) + self.min_delay_ms; // overflows → abort forever
    self.deadline = deadline;
}

// SAFE — bounded at construction
const MAX_DELAY_MS: u64 = 365 * 24 * 60 * 60 * 1000; // 1 year

public fun wrap<T: key + store>(obj: T, min_delay_ms: u64, ctx: &mut TxContext): DelayedWrapper<T> {
    assert!(min_delay_ms <= MAX_DELAY_MS, ETooLong);
    DelayedWrapper { id: object::new(ctx), obj, min_delay_ms }
}
```

**Check:**
1. Find all struct fields set at construction/`wrap`/`new` time with no upper-bound validation
2. Trace each field to where it's used in arithmetic with runtime values (`clock`, `epoch`, counters)
3. If `immutable_field + clock::timestamp_ms()` can overflow `u64`, flag it
4. Verify a recovery path exists (unwrap, admin rescue, timeout fallback)
5. Also check: missing public accessors for construction parameters — downstream protocols cannot programmatically validate the configured value

---

## 3. Resource Safety

### 3.1 Resource Leak
**Pattern:** A resource is created but never moved to storage or dropped.
**Risk:** Move's type system prevents this at compile time — but check for structs without `drop` that might be accidentally destructured.
**Check:** All `key`-ability structs must end up in global storage. If a function creates a resource, trace where it goes.

### 3.2 Unauthorized Resource Extraction
**Pattern:** `move_from<T>(addr)` without verifying the caller owns that address.
**Risk:** Theft of stored resources.
**Check:** Every `move_from` must be preceded by an ownership/capability check.

```move
// VULNERABLE
public entry fun withdraw(account: &signer, target: address) {
    let coin = move_from<CoinStore>(target); // no ownership check!
    // ...
}
```

### 3.3 Borrow After Move
**Pattern:** Using a reference to a value after it has been moved.
**Risk:** Caught by the type system, but watch for patterns that try to work around it.

### 3.4 Double Spend via Phantom Resources
**Pattern:** Protocol tracks balances off-chain or in a separate table while actual assets flow differently.
**Risk:** Inconsistency between accounting and actual assets.
**Check:** For every credit to internal accounting, verify there is a corresponding on-chain asset transfer.

---

## 4. Logic & Invariant Violations

### 4.1 Missing Invariant Assertions
**Pattern:** Protocol has documented invariants (e.g., "total supply == sum of all balances") with no on-chain enforcement.
**Risk:** Invariants can drift due to edge cases, creating exploitable inconsistencies.
**Check:** Critical invariants should be checked with `assert!` at the end of state-mutating functions, especially during development/testing.

### 4.2 Incorrect Comparison Operators
**Pattern:** `>` vs `>=`, `<` vs `<=` in boundary checks.
**Risk:** Off-by-one exploits in withdrawal limits, stake amounts, etc.
**Check:** Every boundary condition — pay extra attention to fee calculations, minimum deposits, maximum withdrawals.

### 4.3 State Machine Violations
**Pattern:** State enum transitions without exhaustive checks.
**Risk:** Skipping states or transitioning to invalid states.
**Check:** Map all state machine transitions. Verify each is gated and exhaustive.

### 4.4 Timestamp/Epoch Manipulation
**Pattern:** Logic that depends on `Clock` (Sui) or `timestamp::now_seconds` (Aptos).
**Risk:** Validators have limited but real ability to influence block timestamps. Flash-loan window exploits.
**Check:** Avoid hardcoded time windows shorter than ~30 seconds. Flag any logic where timestamp manipulation gives economic benefit.

### 4.5 Inverted Security Logic
**Pattern:** A security check that blocks the wrong party, compares the wrong direction, or asserts the opposite of the intended condition. The check exists but protects the attacker instead of the protocol.
**Risk:** The presence of the check creates a false sense of security — code reviewers see an `assert!` and move on, but the logic is backwards.

```move
// VULNERABLE — checks recipient (to) instead of sender (from) for liquidate-only restriction
fun assert_not_liquidate_only<T>(registry: &InvestorInfo<T>, to: &PartyInfo) {
    assert!(!lock_manager::is_liquidate_only(registry, *to.id()), ELiquidateOnly);
    // BUG: should check the sender, not the recipient
}

// VULNERABLE — inverted time comparison, lock expires immediately
fun assert_lock_active(lock: &Lock, clock: &Clock) {
    assert!(clock::timestamp_ms(clock) > lock.expires_at, ELockActive);
    // BUG: should be < (lock is active while time is BEFORE expiry)
}

// SAFE — correct party and correct direction
fun assert_not_liquidate_only<T>(registry: &InvestorInfo<T>, from: &PartyInfo) {
    assert!(!lock_manager::is_liquidate_only(registry, *from.id()), ELiquidateOnly);
}
fun assert_lock_active(lock: &Lock, clock: &Clock) {
    assert!(clock::timestamp_ms(clock) < lock.expires_at, ELockActive);
}
```

**Check:**
1. For every `assert!` in authorization/security context: does the variable being checked match the intended party (sender vs recipient, from vs to)?
2. For every comparison operator in time/deadline checks: does `<` vs `>` match the intended semantics?
3. For every boolean negation (`!`): trace the logic — is the condition checking what the error message claims?
4. Cross-ref with error constant names — does the error name match what the condition actually prevents?

### 4.6 Wrong Field Update
**Pattern:** A function intended to update field X accidentally reads or writes to field Y. Both fields have the same type (`u64`, `u128`, `address`), so the compiler doesn't catch it.
**Risk:** Silent data corruption — the intended field is unchanged, a different field is overwritten. Can lead to authorization bypass if an admin field is overwritten, or fund loss if a balance field is corrupted.

```move
// VULNERABLE — function is called set_fee but updates balance
public fun set_fee(config: &mut Config, new_fee: u64) {
    config.balance = new_fee;  // BUG: should be config.fee = new_fee
}

// VULNERABLE — reads wrong field for comparison
public fun check_limit(pool: &Pool, amount: u64) {
    assert!(amount <= pool.min_deposit, E_EXCEEDS_LIMIT);
    // BUG: should compare against pool.max_withdrawal
}

// SAFE — correct fields
public fun set_fee(config: &mut Config, new_fee: u64) {
    config.fee = new_fee;
}
```

**Check:**
1. For every `set_*` / `update_*` function, verify the field being written matches the function name and parameter name
2. For every comparison in validation logic, verify the field being compared is the one relevant to the check (e.g., `max_withdrawal` for withdrawal limits, not `min_deposit`)
3. Pay special attention to structs with multiple same-typed fields (`u64`, `address`) — compiler cannot catch field swaps
4. Cross-ref: field names in events should match the fields that were actually modified

---

## 5. Input Validation

### 5.1 Missing Zero-Value Checks
**Pattern:** Functions that accept `amount: u64` without asserting `amount > 0`.
**Risk:** Zero-value operations that corrupt state, skip logic, or trigger division-by-zero downstream.

### 5.2 Missing Address Validation
**Pattern:** Functions that accept `address` parameters without validating they are non-zero or known.
**Risk:** Sending to zero address, interacting with uninitialized modules.

### 5.3 Length/Bounds Checks on Vectors
**Pattern:** Accessing `vector<T>` by index without bounds checking.
**Risk:** Runtime abort (DoS) if index is out of bounds.
**Check:** All vector index operations should be bounds-checked or use safe access patterns.

---

## 6. Cross-Module & External Call Safety

### 6.1 Reentrancy via Cross-Module Calls
**Pattern:** Calling an external module function while holding mutable borrows or mid-state-update.
**Risk:** Move doesn't have EVM-style reentrancy, but cross-module calls while in inconsistent state can still be exploited.
**Check:** Ensure state is in a consistent, valid state before any external call. Update state after, not before external calls (checks-effects-interactions pattern).

### 6.2 Unvalidated Return Values
**Pattern:** Return values from external module calls used without validation.
**Risk:** External module could return unexpected values.
**Check:** Validate all values returned from external calls before using them in critical logic.

### 6.3 Dependency on Upgradeable Modules
**Pattern:** Protocol depends on an external module that can be upgraded.
**Risk:** Upgrade changes behavior, breaking assumptions.
**Check:** Flag all external module dependencies. Note which are upgradeable.

### 6.4 Stale State from Hidden External Mutations

**Pattern:** Protocol reads a value (exchange rate, price, index), then calls an external module that internally mutates that same value (e.g., interest accrual), making the previously-read value stale.

```move
// VULNERABLE — reads exchange rate, then calls withdraw() which accrues interest internally
public fun user_withdraw(vault: &mut Vault, pool: &mut ExternalPool, shares: u64, ctx: &mut TxContext) {
    let rate = get_exchange_rate(pool);         // reads rate BEFORE accrual
    let amount = shares * rate / PRECISION;     // calculates with stale rate
    external_pool::withdraw(pool, amount);      // this internally calls accrue_interest()!
    // User underpaid — rate was stale, vault accounting silently drifts
}

// SAFE — accrue first, then read, or re-read after external call
public fun user_withdraw(vault: &mut Vault, pool: &mut ExternalPool, shares: u64, ctx: &mut TxContext) {
    external_pool::accrue_interest(pool);       // force accrual first
    let rate = get_exchange_rate(pool);         // now rate is fresh
    let amount = shares * rate / PRECISION;
    external_pool::withdraw(pool, amount);
}
```

**Check:**
1. For every external call: does the called function internally mutate state that you already read?
2. Common in yield vaults, lending wrappers, and aggregators built on top of other protocols
3. Look for `get_*` / `calculate_*` calls followed by an external `deposit` / `withdraw` / `swap`
4. Re-read or re-derive values after any external call that may have side effects

---

## 7. Upgradeability & Admin Risks

### 7.1 Unconstrained Upgrade Authority
**Pattern:** Single key controls upgrades with no timelock or multisig.
**Risk:** Compromised key = full protocol takeover.
**Check:** Upgrade authority should be governed. Flag single-key upgrade authority as Medium/High depending on TVL.

### 7.2 Initialization Functions Callable Multiple Times
**Pattern:** `init` or `initialize` function that can be called by anyone after deployment.
**Risk:** Reinitialization overwrites config, disables the protocol, or escalates privileges.
**Check:** Initialization must be one-time-only, enforced on-chain.

```move
// VULNERABLE — anyone can reinitialize
public entry fun initialize(admin: &signer, config: Config) {
    move_to(admin, config);
}

// SAFE — aborts if already initialized
public entry fun initialize(admin: &signer, config: Config) {
    assert!(!exists<Config>(signer::address_of(admin)), E_ALREADY_INITIALIZED);
    move_to(admin, config);
}
```

### 7.3 Emergency Pause Missing
**Pattern:** No circuit breaker / pause mechanism.
**Risk:** In an active exploit, there's no way to halt the protocol.
**Check:** Note absence of pause mechanism. Not a vulnerability itself, but an operational risk worth flagging as Info.

### 7.4 Incomplete Pause Coverage
**Pattern:** Pause flag exists but is not checked on ALL public/entry functions.
**Risk:** Attacker routes through an unpaused code path while the protocol believes it's halted.

```move
// VULNERABLE — pause checked on deposit but not on withdraw
public entry fun deposit(state: &State, amount: u64) {
    assert!(!state.paused, E_PAUSED); // checked here
}
public entry fun withdraw(state: &State, amount: u64) {
    // Missing pause check — attacker withdraws during "pause"
}

// SAFE — every state-mutating function checks pause
public entry fun withdraw(state: &State, amount: u64) {
    assert!(!state.paused, E_PAUSED);
    // ... withdrawal logic
}
```

**Check:** Grep `paused` or `is_paused`. List every public/entry function. Verify EACH one checks the pause flag. Admin emergency functions may intentionally bypass pause.

### 7.5 Unpinned Dependencies in Move.toml
**Pattern:** Git dependencies in `Move.toml` without pinned `rev` or `tag`.
**Risk:** Dependency can change silently — supply chain attack imports malicious code or breaking changes.

```toml
# VULNERABLE — unpinned, tracks latest commit on main
[dependencies]
SomeProtocol = { git = "https://github.com/example/protocol.git", subdir = "contracts" }

# SAFE — pinned to specific commit
[dependencies]
SomeProtocol = { git = "https://github.com/example/protocol.git", subdir = "contracts", rev = "abc123def" }
```

**Check:** Open `Move.toml`. Every git dependency must have `rev = "..."` or `tag = "..."`. Flag unpinned deps as Medium (supply chain risk).

---

## 8. Type Safety & Value Validation

### 8.1 Generic Type Parameter Not Validated

**Pattern:** Functions accepting generic `<T>` without verifying `T` matches the stored/expected type.

**Risk:** Attackers deposit worthless tokens, repay with wrong assets, or drain pools by type confusion.
This is the **#1 Critical pattern** across real Move audits.

```move
// VULNERABLE — accepts any CoinType for repayment
public fun repay_flash_loan<T>(
    pool: &mut Pool,
    coin: Coin<T>,
    receipt: FlashReceipt,
) {
    // No check that T matches the originally borrowed coin type!
    balance::join(&mut pool.balance, coin::into_balance(coin));
    let FlashReceipt { amount: _ } = receipt;
}

// SAFE — type parameter bound to pool and receipt
public fun repay_flash_loan<T>(
    pool: &mut Pool<T>,
    coin: Coin<T>,
    receipt: FlashReceipt<T>,
) {
    balance::join(&mut pool.balance, coin::into_balance(coin));
    let FlashReceipt { amount: _ } = receipt;
}
```

**Check:**
1. Every function with a generic type parameter — how is the type validated?
2. Flash loan repayment: does the receipt bind the type to the original loan?
3. Lending functions: is `CoinType` verified against the reserve/pool it belongs to?
4. Cross-check: does the protocol store the expected type and compare at runtime?

*Real audit refs: Navi (all lending functions lack CoinType validation — Critical),
Econia (place_market_order no type check — Critical)*

*See also: APT-03 (Aptos coin type whitelisting), SUI-20 (Sui flash loan receipt pool validation)*

### 8.2 Return Values in Wrong Order

**Pattern:** Functions returning multiple values in incorrect order, silently corrupting all callers.

```move
// VULNERABLE — returns (reserve_y, reserve_x) instead of (reserve_x, reserve_y)
public fun get_reserves<X, Y>(pool: &Pool<X, Y>): (u64, u64) {
    (pool.reserve_y, pool.reserve_x)  // swapped!
}
```

**Check:** Verify all multi-return functions return values in the documented/expected order.
Cross-reference every call site — a swap here corrupts all swap calculations downstream.

*Real audit ref: KriyaDEX (get_reserves wrong order — High)*

### 8.3 Self-Referential Validation (Always-True Checks)

**Pattern:** Security checks that compare a value against itself or use tautological conditions.

```move
// VULNERABLE — compares version against itself, always passes
public fun check_version(config: &Config) {
    assert!(config.version == config.version, E_WRONG_VERSION);
}

// VULNERABLE — inverted existence check
public fun remove_authorized_user(list: &mut vector<address>, user: address) {
    assert!(!vector::contains(list, &user), E_NOT_FOUND);  // should be WITHOUT the !
}

// SAFE
public fun check_version(config: &Config) {
    assert!(config.version == CURRENT_VERSION, E_WRONG_VERSION);
}
```

**Check:**
1. Search for `assert!` conditions where both sides reference the same variable
2. Check for inverted boolean logic (`!exists` vs `exists`, `!contains` vs `contains`)
3. Verify all security-critical comparisons use an independent reference value

*Real audit refs: Hop Aggregator (version self-comparison — High),
Typus Finance (inverted existence check — High)*

### 8.4 Constant Definition Errors

**Pattern:** Hardcoded constants with wrong values — silently breaks security assumptions.

```move
// REAL BUGS FROM AUDITS
const MAX_U64: u64 = 0xFFFFFFFFFFFFFFF;              // 15 hex digits, should be 16
const DAY_SECONDS: u64 = 600;                          // 10 minutes, not 24 hours!
const ONE_DAY: u64 = 0;                                // should be 86_400_000
const SECONDS_PER_YEAR: u64 = 365 * 24 * 60 * 60 * 1000; // 1000x too large (ms not s)

// CORRECT
const MAX_U64: u64 = 0xFFFFFFFFFFFFFFFF;               // 16 hex digits
const DAY_SECONDS: u64 = 86_400;                        // 24 * 60 * 60
const ONE_DAY_MS: u64 = 86_400_000;                     // 24 * 60 * 60 * 1000
const SECONDS_PER_YEAR: u64 = 31_536_000;               // 365 * 24 * 60 * 60
```

**Check:**
1. Grep all `const` definitions — verify values match names and documentation
2. Check MAX_U64/MAX_U128 have correct number of hex digits
3. Verify time constants: `86400` (day), `31536000` (year), `3600` (hour)
4. Check precision/scaling constants match token decimals

*Real audit refs: Bluefin (MAX_u64 missing digit — Critical),
Dexlyn (DAY_SECONDS=600 — High), SuiPad (one_day=0 — High),
Navi (SECONDS_PER_YEAR 1000x — Critical)*

---

## 9. State Consistency

### 9.1 Missing State Update After Claim/Refund/Withdraw

**Pattern:** Function transfers assets but doesn't flip a "claimed" flag or decrement balance.
Users call repeatedly to drain the protocol.

```move
// VULNERABLE — no state update after refund
public entry fun claim_refund(
    vault: &mut Vault,
    cert: &Certificate,
    ctx: &mut TxContext
) {
    let refund = coin::take(&mut vault.balance, cert.invested_amount, ctx);
    transfer::public_transfer(refund, tx_context::sender(ctx));
    // BUG: cert.claimed never set to true — user calls again to drain
}

// SAFE — mark as claimed
public entry fun claim_refund(
    vault: &mut Vault,
    cert: &mut Certificate,
    ctx: &mut TxContext
) {
    assert!(!cert.claimed, E_ALREADY_CLAIMED);
    cert.claimed = true;
    let refund = coin::take(&mut vault.balance, cert.invested_amount, ctx);
    transfer::public_transfer(refund, tx_context::sender(ctx));
}
```

**Check:**
1. Every claim/refund/withdraw function — is there a flag or balance update preventing re-invocation?
2. Search for transfer/send calls — does the function modify state to reflect the transfer?
3. Check if the receipt/certificate/ticket is consumed (destroyed) or just read

*Real audit refs: SuiPad (claim_refund no state update — Critical),
MoveGPT (refund_entry callable multiple times — High),
Mysten Republic (repeated invocation for excessive claims — High)*

### 9.2 Double Scaling / Unit Mixing

**Pattern:** Scaled balances (with interest index) mixed with raw amounts in the same calculation.

```move
// VULNERABLE — comparing scaled debt with unscaled repayment
let scaled_debt = user.scaled_variable_debt;    // in RAY units (1e27)
let repay_amount = coin::value(&payment);       // in token decimals (1e6 for USDC)
assert!(repay_amount >= scaled_debt, E_UNDERPAY); // apples vs oranges!

// SAFE — normalize to same scale
let actual_debt = scaled_debt * borrow_index / RAY;
assert!(repay_amount >= actual_debt, E_UNDERPAY);
```

**Check:**
1. Identify all "scaled" or "indexed" values in the codebase
2. Every arithmetic operation must use values in the same scale
3. Watch for variables named `scaled_*` used directly with raw amounts
4. Check interest index: multiply or divide? Verify direction matches the math

*Real audit refs: Navi (scaled supply used with unscaled amounts — Critical),
AAVE v3 (borrow index set to token decimals not RAY — High),
ThalaSwapV2 (double-upscaling in pay_flashloan — Critical)*

### 9.3 Missing Recovery / Withdrawal Functions

**Pattern:** Tokens or fees accumulate in a contract with no function to extract them.

**Check:**
1. For every fee collection (`balance::join`, `coin::put`): does a corresponding withdrawal function exist?
2. For every vault/pool: can residual tokens be recovered by admin?
3. Check refund flows: can unused tokens in failed campaigns/auctions be recovered?
4. If missing: severity is High (permanent fund lock)

*Real audit refs: Kofi Finance (deposit fees, no withdraw — Critical),
Scallop (flash loan fees trapped — High),
SuiPad (unused tokens stuck in vault — High)*

### 9.4 Self-Transfer Snapshot Manipulation
**Pattern:** User transfers tokens to themselves, triggering fee/reward snapshot updates without real economic activity.
**Risk:** If fee collection or reward distribution logic fires on every transfer (including self-transfers), an attacker can manipulate accumulators, claim unearned rewards, or force fee distributions.

```move
// VULNERABLE — transfer triggers reward snapshot, no self-transfer check
public fun transfer(pool: &mut Pool, from: address, to: address, amount: u64) {
    update_reward_snapshot(pool, from);  // triggers on self-transfer too
    update_reward_snapshot(pool, to);
    move_tokens(pool, from, to, amount);
}

// SAFE — block self-transfers or skip snapshot on self-transfer
public fun transfer(pool: &mut Pool, from: address, to: address, amount: u64) {
    assert!(from != to, E_SELF_TRANSFER);
    update_reward_snapshot(pool, from);
    update_reward_snapshot(pool, to);
    move_tokens(pool, from, to, amount);
}
```

**Check:** Search for transfer/send functions. Does `from == to` trigger any side effects (rewards, fees, snapshots)?

### 9.5 Round-Trip Profitability
**Pattern:** `deposit(X)` followed by immediate `withdraw(all)` returns more than X.
**Risk:** Rounding asymmetry, fee accounting gaps, or share calculation bugs allow value extraction through repeated deposit/withdraw cycles.

```move
// VULNERABLE — deposit rounds UP shares, withdraw rounds UP tokens
public fun deposit(pool: &mut Pool, amount: u64): u64 {
    let shares = (amount * pool.total_shares + pool.total_assets - 1) / pool.total_assets; // rounds UP
    pool.total_shares = pool.total_shares + shares;
    shares
}
public fun withdraw(pool: &mut Pool, shares: u64): u64 {
    let amount = (shares * pool.total_assets + pool.total_shares - 1) / pool.total_shares; // rounds UP
    pool.total_shares = pool.total_shares - shares;
    amount // user gets MORE than deposited
}

// SAFE — deposit rounds DOWN (fewer shares), withdraw rounds DOWN (fewer tokens)
```

**Check:** Invariant: `withdraw(deposit(X)) <= X` must always hold. Deposit should round DOWN (protocol keeps dust), withdraw should round DOWN (protocol keeps dust). Cross-ref: DEFI-39

---

## 10. Control Flow & Protocol Logic

### 10.1 Recursive / Circular Function Calls

**Pattern:** Function A calls function B which calls A again — infinite recursion, permanent DoS.

```move
// VULNERABLE — circular call chain
public fun distribute_fees<X, Y>(pool: &mut Pool<X, Y>) {
    let fee_coins = collect_fees(pool);
    swap_exact_x_to_y_direct(pool, fee_coins); // this calls distribute_fees!
}

public fun swap_exact_x_to_y_direct<X, Y>(
    pool: &mut Pool<X, Y>, coins: Coin<X>
): Coin<Y> {
    // ... swap logic ...
    distribute_fees(pool); // infinite recursion!
}
```

**Check:**
1. Trace call chains for cycles — especially fee distribution that calls swap internally
2. Any function that both triggers and is triggered by the same action
3. Look for functions called in hooks/callbacks that can re-enter the calling function

*Real audit ref: Baptswap (distribute_dex_fees → swap → distribute_dex_fees — High)*

### 10.2 Flash Loan Accumulator Manipulation

**Pattern:** Stake/unstake in the same transaction to manipulate reward accumulators.

```move
// VULNERABLE — accumulator updates on every stake/unstake
public fun stake(pool: &mut Pool, amount: u64) {
    update_reward_accumulator(pool);  // updates based on current total_staked
    pool.total_staked = pool.total_staked + amount;
}

public fun unstake(pool: &mut Pool, amount: u64): u64 {
    update_reward_accumulator(pool);  // updates again
    pool.total_staked = pool.total_staked - amount;
    calculate_and_return_rewards(pool) // inflated rewards!
}

// Attack: flash_loan → stake(huge) → unstake + claim rewards → repay
```

**Check:**
1. Can stake + claim + unstake happen in the same transaction/PTB?
2. Does the accumulator use time-weighted values or instant values?
3. Is there a minimum staking duration before rewards are claimable?
4. Does `total_staked` changing mid-tx affect other users' reward share?

*Real audit refs: Thala Labs (improper accumulator updates — Critical, 2x),
Kofi Finance (kAPT double minting — High)*

### 10.3 Cooldown / Timelock Bypass via Inverted Logic

**Pattern:** Wrong comparison operator or inverted boolean makes time-based protection useless.

```move
// VULNERABLE — wrong operator, allows action BEFORE cooldown expires
public fun withdraw(state: &State, clock: &Clock) {
    let elapsed = clock::timestamp_ms(clock) - state.last_action;
    assert!(elapsed < COOLDOWN_PERIOD, E_COOLDOWN); // BUG: should be >=
}

// VULNERABLE — zero value bypasses the entire check
public fun check_time(end_time: u64, now: u64) {
    if (end_time != 0 && end_time < now) { abort E_EXPIRED };
    // BUG: end_time == 0 skips check entirely
}

// SAFE
public fun withdraw(state: &State, clock: &Clock) {
    let elapsed = clock::timestamp_ms(clock) - state.last_action;
    assert!(elapsed >= COOLDOWN_PERIOD, E_COOLDOWN_NOT_MET);
}
```

**Check:**
1. Every time comparison: verify operator direction matches intent (`>=` for "after", `<` for "before")
2. Check for zero-value bypass in time fields (if `time == 0`, is the check skipped?)
3. Verify boolean conditions aren't inverted

*Real audit refs: Elixir (wrong comparison, cooldown bypass — High),
Securitize (inverted logic, zero never aborts — Critical)*

### 10.4 Incorrect Liquidation Logic

**Pattern:** Liquidation functions that pass the wrong variable, skip solvency checks, or miscalculate amounts.

```move
// VULNERABLE — burns collateral amount instead of debt amount
public fun liquidate(position: &mut Position, collateral_to_seize: u64, debt_to_repay: u64) {
    burn_debt_tokens(position, collateral_to_seize); // BUG: should be debt_to_repay!
    transfer_collateral(position, collateral_to_seize);
}

// VULNERABLE — no solvency check after withdrawal
public fun withdraw(account: &mut Account, amount: u64) {
    account.balance = account.balance - amount;
    // Missing: assert!(is_solvent(account), E_WOULD_BE_INSOLVENT);
}
```

**Check:**
1. Verify the correct variable (debt vs collateral) is passed at each step in the liquidation flow
2. `withdraw` must check solvency AFTER the withdrawal, not before
3. Liquidation must not be blockable by cooldowns, paused states, or other guards
4. Verify liquidation incentive math doesn't let liquidators extract more than intended

*Real audit refs: AAVE v3 (collateral burned instead of debt — High),
Echelon (missing solvency check — High),
Aries Markets (settle_share_amount wrong conversion — High)*

---

## 11. Cross-Module Lifecycle

### 11.1 Cross-Module Terminal State Cleanup

**Pattern:** When any function — especially a permissionless one — can transition an
obligation or position to a terminal state (zero debt, zero collateral,
fully liquidated, fully repaid), ALL associated sub-objects across ALL
modules must have a cleanup path.

In Move, sub-objects are typically stored in separate modules:
- Reward / liquidity mining trackers
- Referral fee entries
- Rate limiter records
- eMode group membership entries
- Insurance fund records

If a sub-object has no cleanup path when its parent reaches terminal state,
AND a permissionless function can trigger that terminal state, the result is a
permanently orphaned object that can block admin operations forever.

The highest-risk combination is:
`permissionless_fn → terminal_state → orphaned_tracker → blocks_admin_fn`

**Risk:** Admin-funded resources (reward pools, insurance reserves) can be
permanently locked with no upgrade path in an immutable contract.

```move
// VULNERABLE — repay_on_behalf can clear last debt but reward tracker is orphaned
// In repay.move:
public fun repay_on_behalf(
    obligation: &mut Obligation,
    payment: Coin<USDC>,
    _ctx: &mut TxContext,
) {
    let amount = coin::value(&payment);
    obligation.debt = obligation.debt - amount;
    // BUG: no cleanup of reward tracker in liquidity_mining module
    // If debt == 0, obligation is terminal but tracker persists
}

// In liquidity_mining.move:
public fun close_pool_reward(
    _cap: &AdminCap,
    pool: &mut RewardPool,
) {
    // Checks that no active trackers remain
    assert!(pool.active_trackers == 0, E_TRACKERS_EXIST);
    // Orphaned tracker blocks this forever → reward tokens locked
    let rewards = balance::withdraw_all(&mut pool.rewards);
    // ...
}

// SAFE — repayment cleans up all cross-module state on terminal transition
public fun repay_on_behalf(
    obligation: &mut Obligation,
    mining_pool: &mut RewardPool,
    payment: Coin<USDC>,
    _ctx: &mut TxContext,
) {
    let amount = coin::value(&payment);
    obligation.debt = obligation.debt - amount;
    if (obligation.debt == 0) {
        // Clean up reward tracker on terminal state
        cleanup_reward_tracker(mining_pool, object::id(obligation));
    };
}
```

**Check:**
1. For every permissionless function that can fully repay, fully redeem, or
   fully liquidate a position — list all modules that hold per-obligation state
2. For each: does the permissionless function (or a function it calls) clean
   up that module's record when the position reaches terminal state?
3. Find admin/maintenance functions (`close_pool`, `collect_fees`, `end_epoch`)
   that check for "zero active trackers" or "empty registry" before executing
4. If any such admin function can be permanently blocked by an orphaned
   tracker that a permissionless function can create → HIGH
5. Search for permissionless entry functions (no capability arg) that call
   repay, liquidate, or withdraw; grep all other module files for
   structs keyed by `ObligationID` or `PositionID`; verify cleanup calls exist

---

## 12. Arithmetic / Accounting DoS

### 12.1 Abort-Before-Checkpoint Deadlock

**Pattern:** A periodic accounting function (reward accumulator update, interest accrual, index refresh) performs arithmetic that can abort, and the state checkpoint (`last_update_time`, `cumulative_index`, `reward_per_share`) is written **after** the potentially-aborting line. If the arithmetic aborts, the checkpoint never advances. On the next call, the time delta is even larger, making the overflow worse — the function is permanently uncallable.

**Risk:** Every operation that calls the stuck accounting function also reverts. In lending protocols, this typically freezes deposits, withdrawals, borrows, repayments, liquidations, and reward claims for the affected pool/CoinType. Undercollateralized positions cannot be liquidated, causing unbounded bad debt.

```move
// VULNERABLE — checkpoint written AFTER the overflowing computation
public fun update_pool_reward(pool_reward: &mut PoolReward, clock: &Clock) {
    let now = clock::timestamp_ms(clock);
    let time_passed = now - pool_reward.last_update_time_ms;     // grows every second

    // This line aborts when total_rewards * time_passed > U64_MAX
    let unlocked = float::from(pool_reward.total_rewards)
        .mul(float::from(time_passed))                            // <-- ABORT
        .div(float::from(pool_reward.duration));

    pool_reward.accumulated = pool_reward.accumulated + unlocked;
    pool_reward.last_update_time_ms = now;                        // <-- NEVER REACHED
}

// SAFE — reorder arithmetic OR checkpoint before risky computation
// Option A: divide first (prevents overflow)
let unlocked = float::from(pool_reward.total_rewards)
    .div(float::from(pool_reward.duration))
    .mul(float::from(time_passed));

// Option B: cap time_passed to remaining duration
let time_passed = math::min(time_passed, pool_reward.end_time_ms - pool_reward.last_update_time_ms);
```

**Check:**
1. For every periodic update function (grep: `last_update`, `last_accrual`, `last_checkpoint`, `cumulative_index`, `reward_per_share`):
   - Is the checkpoint variable written AFTER potentially-aborting arithmetic?
   - If the function aborts, does the time delta grow on every retry?
2. If yes → apply the **Recoverability Matrix** below

### 12.2 Admin-Origin Latent User DoS

**Pattern:** An admin performs a normal, expected configuration action (adding rewards, setting parameters, enabling a feature). The configuration is valid and reasonable at creation time. Later, under production conditions (pool inactivity, time passage, token accumulation), the configuration causes a user-facing function to abort. The admin action is the **origin**, but the **victims** are unprivileged users and liquidators.

**Risk:** This is commonly dismissed as "admin-only" or "trusted admin." That is incorrect when:
- The admin action is routine and expected (adding a reward program, setting a fee)
- The failure occurs later in a permissionless code path
- Users/liquidators are the ones blocked, not the admin
- The admin cannot fix it because recovery paths traverse the same failing code

**Severity Rule:** Severity is based on **who is blocked and what is blocked**, not who created the initial configuration.
- Users cannot withdraw → fund lock → **High/Critical**
- Liquidations blocked → bad debt accumulation → **High**
- Only admin convenience impacted → **Low/Medium**

**Check:**
1. For every admin-configurable parameter that enters a mathematical expression in a user-facing path:
   - Can the configured value, combined with elapsed time or accumulated state, overflow?
   - What is the maximum safe value? Express in atomic units with token decimals.
   - What is the realistic operational range? (e.g., 500K USDC reward over 30 days)
2. Never dismiss a finding as "admin-only" if users or liquidators are bricked
3. Cross-ref: 2.6, 12.1, DEFI-85

### Recoverability Matrix (mandatory for every DoS candidate)

For every suspected DoS/deadlock, answer ALL of these before assigning severity:

| Question | Answer |
|----------|--------|
| **What call first aborts?** | Name the exact function and line |
| **Does abort occur before checkpoint/state advance?** | If yes → state is stuck |
| **Does the failing condition worsen over time?** | e.g., time_delta grows → overflow gets worse |
| **Can admin cancel/close/modify to fix?** | Trace cancel/close paths — do they call the same update? |
| **Can users claim/withdraw to work around it?** | Trace claim/withdraw — do they also trigger the update? |
| **Is there an emergency/bypass path?** | Admin pause, emergency withdraw, governance override? |
| **Is the deadlock temporary, conditional, or permanent?** | Temporary: resolves on its own; Conditional: requires specific action; Permanent: only fixable by protocol upgrade |

**Severity from matrix:**
- Permanent deadlock + fund lock + no bypass → **Critical**
- Permanent deadlock + blocked liquidations → **High**
- Conditional deadlock with admin recovery path → **Medium**
- Temporary DoS that self-resolves → **Low**

**Example — Reward Manager Overflow:**

| Question | Answer |
|----------|--------|
| What call first aborts? | `update_pool_reward_manager` at `float::from(total_rewards).mul(float::from(time_passed))` |
| Before checkpoint? | Yes — `last_update_time_ms` is written after the abort point |
| Worsens over time? | Yes — `time_passed` grows every millisecond |
| Admin cancel? | `cancel_pool_reward` calls `update_pool_reward_manager` → also aborts |
| User claim? | `claim_rewards` calls `update_pool_reward_manager` → also aborts |
| Emergency bypass? | None — no function modifies `last_update_time_ms` without calling update |
| Duration? | **Permanent** — only fixable by protocol version upgrade |

**Result:** Permanent fund freeze + blocked liquidations → **High**

---

## 13. Build & Test Log Analysis

> **Prerequisite:** This section runs ONLY when `BUILD_AVAILABLE = true` (the project
> compiles successfully). If the project does not build, skip this entire section.
> The auditor sets this flag during Phase 1 of the SKILL.md workflow.

### Why test logs matter for security audits

Test suites exercise code paths that static analysis reads but never runs. Test logs can
reveal arithmetic aborts, assertion failures, unexpected error codes, and edge-case panics
that the developer may have papered over with `#[expected_failure]` annotations — or that
indicate latent bugs the developer hasn't noticed. A test that passes with
`#[expected_failure(abort_code = ...)]` is the developer **acknowledging** an abort exists.
The auditor's job is to determine whether that abort can happen in production under
realistic conditions.

### 13.1 Build Verification

**Procedure:**
1. Check for `Move.toml` in the project root (or `sources/` structure)
2. Run the appropriate build command:
   - **Sui:** `sui move build 2>&1` — capture stdout + stderr
   - **Aptos:** `aptos move compile 2>&1` — capture stdout + stderr
3. If build succeeds (exit code 0) → proceed to 13.2
4. If build fails → record build errors in the audit summary under "Auditor Notes".
   Build failures themselves can be informative:
   - Missing dependencies → potential supply chain risk (cross-ref 7.5)
   - Type errors → possible upgrade incompatibility (cross-ref APT-22)
   - Unused imports/variables → may indicate incomplete refactoring
   Do NOT run test log analysis on a project that does not compile.

### 13.2 Test Execution & Log Capture

**Procedure:**
1. Run the full test suite and capture all output:
   - **Sui:** `sui move test 2>&1` — capture full output
   - **Aptos:** `aptos move test 2>&1` — capture full output
2. If the project has custom test commands (check `Makefile`, `justfile`, `package.json`
   scripts, or README), run those as well
3. Save the raw test output for analysis

**Important:** If the test suite is very large (>5 minutes), run with `--filter` on
security-critical modules first (modules containing financial math, access control,
or state-mutating entry points).

### 13.3 Log Analysis — What to Look For

Analyze the test output systematically. For each category below, search the logs
and flag anything suspicious:

**Category 1 — Arithmetic Aborts (High-signal for overflow/underflow bugs)**
- Search for: `arithmetic error`, `ARITHMETIC_ERROR`, `overflow`, `underflow`,
  `abort code 4001` (Move stdlib arithmetic), `MoveAbort`, `execution failed`
- For each abort found:
  - Is it inside an `#[expected_failure]` test? If yes → the developer knows about it.
    Ask: **can this abort happen in production, not just tests?**
  - Is it in a test that exercises a user-facing code path (deposit, withdraw, borrow,
    repay, liquidate, claim)? If yes → potential DoS vector
  - What function and module does it trace back to?
  - Cross-ref: Section 2 (Arithmetic & Overflow), Section 12 (Arithmetic/Accounting DoS)

**Category 2 — Assertion Failures (Medium-signal for invariant violations)**
- Search for: `assertion failure`, `ABORTED`, `abort code`, `E_` error constant names
- For each assertion failure:
  - What invariant is being checked?
  - Is the test intentionally triggering the assertion (negative test) or is it unexpected?
  - If a positive test (should-succeed test) hits an assertion → likely a real bug
  - Cross-ref: Section 4 (Logic & Invariant Violations)

**Category 3 — Expected Failure Annotations (Medium-signal for papered-over bugs)**
- Search for: `#[expected_failure]`, `expected_failure(abort_code`
- For each expected-failure test:
  - What abort code is expected? Map it back to the error constant and the assert that fires
  - Is the abort a legitimate input validation (e.g., "zero amount rejected") or does it
    indicate a code path that aborts when it shouldn't (e.g., "overflow in reward calc")?
  - **Key question:** If this abort fires in production, does it block user operations?
  - Tests that expect arithmetic overflow aborts in financial functions are HIGH PRIORITY —
    the developer is acknowledging the overflow exists

**Category 4 — Test Failures / Skipped Tests (Low-signal but informative)**
- Search for: `FAILED`, `test result: FAILED`, `ignored`, `filtered out`
- Failing tests may indicate:
  - Incomplete implementation (code under development)
  - Regression from recent changes
  - Edge cases the developer hasn't fixed yet
- Skipped/ignored tests deserve review — they may have been disabled because they
  exposed problematic behavior

**Category 5 — Gas / Execution Limits (Low-signal for DoS)**
- Search for: `OUT_OF_GAS`, `EXECUTION_LIMIT_REACHED`, `timeout`
- Functions hitting gas limits in tests may indicate unbounded loops or excessive
  computation — potential DoS in production

### 13.4 Triage & Escalation

For each flagged log entry, apply this triage:

| Log Signal | Initial Priority | Escalation Criteria |
|------------|-----------------|---------------------|
| Arithmetic abort in user-facing path | High | If abort occurs before state checkpoint (→ 12.1), escalate to Critical investigation |
| Expected-failure test for overflow in financial math | High | Cross-check with DEFI-85/DEFI-86 — if overflow is reachable with production values, report |
| Assertion failure in positive test | Medium | Investigate the invariant — is it reachable from external inputs? |
| Expected-failure for input validation | Low | Usually legitimate — verify the validation is correct |
| Failing/skipped test | Info | Note in Auditor Notes unless it reveals a security pattern |
| Gas limit hit | Medium | Check if the function is user-callable with attacker-controlled iteration count |

**Escalation rule:** Any arithmetic abort or overflow-related `#[expected_failure]` in
a module that handles financial state (balances, rewards, interest, shares) MUST be
cross-referenced against the vulnerability patterns in this file. The auditor must:
1. Trace the abort back to the exact function and line
2. Determine if the abort is reachable from a user-facing entry point
3. If reachable → apply the Recoverability Matrix (Section 12.1)
4. Report findings to human for manual verification with:
   - The exact test name and abort code
   - The production code path that can trigger it
   - Whether the abort occurs before a state checkpoint
   - A severity assessment based on the Recoverability Matrix

### 13.5 Reporting Test Log Findings

Test log findings are reported in a separate subsection of the audit report:

```
## Test Log Analysis

**Build Status:** ✅ Compiled successfully | ❌ Build failed (see Auditor Notes)
**Test Results:** X passed, Y failed, Z skipped
**Flags Raised:** N items requiring investigation

### [TEST-NNN] Flag Title

| Field | Value |
|-------|-------|
| Priority | High / Medium / Low / Info |
| Test Name | `test_module::test_function_name` |
| Log Signal | Arithmetic abort / Expected failure / Assertion / etc. |
| Production Path | function_name in module_name.move:line |
| Checkpoint Safe? | Yes (abort after checkpoint) / No (abort before checkpoint) |

**Analysis:** What the test log revealed and why it may indicate a security issue.

**Recommendation:** Further manual investigation needed / Confirmed as vulnerability (cross-ref FINDING-NNN) / Benign
```

**Integration with main findings:** If a test log flag confirms or strengthens a finding
from Phases 3–5, cross-reference it in the main finding's Verification section rather than
duplicating it. Test log evidence increases finding confidence from QUESTIONABLE to VALID.

---

## Verification Checklist

Run through each item and mark ✅ (clean) or ❌ (finding):

- [ ] All entry functions have access control
- [ ] No capability structs with `copy` ability
- [ ] No authorization functions returning bool with unchecked call sites — use assert pattern (1.6)
- [ ] All arithmetic checked for overflow/underflow DoS
- [ ] No division before multiplication in financial math
- [ ] All divisions guarded against zero denominator
- [ ] No narrowing casts without bounds assertions
- [ ] All bitwise operations checked for overflow/precision loss — Move does NOT auto-check these (2.5)
- [ ] All `move_from` calls preceded by ownership check
- [ ] No timestamp dependencies exploitable in <30s window
- [ ] All user inputs validated (zero checks, bounds checks)
- [ ] State consistent before all external calls
- [ ] No stale reads before external calls that internally mutate the read value (6.4)
- [ ] Initialization is one-time-only
- [ ] Upgrade authority is governed or noted
- [ ] Pause flag checked on ALL public/entry functions, not just some (7.4)
- [ ] All git dependencies in Move.toml pinned with `rev` or `tag` (7.5)
- [ ] All generic type parameters validated against stored/expected types (8.1)
- [ ] Multi-return functions return values in documented order (8.2)
- [ ] No self-referential or always-true validation checks (8.3)
- [ ] All constants verified: time (86400/day), precision, MAX values correct digit count (8.4)
- [ ] Every claim/refund/withdraw updates state to prevent re-invocation (9.1)
- [ ] No mixed scaled/unscaled values in arithmetic (9.2)
- [ ] Every fee collection has a corresponding withdrawal function (9.3)
- [ ] Self-transfers cannot manipulate reward/fee snapshots (9.4)
- [ ] Round-trip `deposit→withdraw` never returns more than input (9.5)
- [ ] No circular/recursive function call chains (10.1)
- [ ] Stake/unstake cannot manipulate reward accumulators in same transaction (10.2)
- [ ] All time comparisons use correct operator direction (10.3)
- [ ] No inverted security logic — assert conditions check the right party and correct comparison direction (4.5)
- [ ] No wrong-field updates — set/update functions modify the intended field, not a same-typed sibling (4.6)
- [ ] Liquidation functions pass correct variables — debt vs collateral (10.4)
- [ ] Permissionless terminal-state transitions clean up all cross-module sub-objects (11.1)
- [ ] All fixed-point helper libraries opened and value bounds derived — `mul` intermediate cannot overflow before normalizing division (2.6)
- [ ] Every periodic accounting update writes checkpoint BEFORE or ATOMICALLY WITH potentially-aborting arithmetic (12.1)
- [ ] Admin-configured parameters validated against overflow bounds using production-realistic values and token decimals (12.2)
- [ ] Recoverability Matrix completed for every DoS candidate — cancel/claim/close paths checked for shared failure (12.1, 12.2)
- [ ] Build & test log analysis completed (if project is buildable) — arithmetic aborts, assertion failures, and error patterns reviewed (13)

> **Deep-dive prompts and the Move Vulnerability Patterns prompt pack have been moved to
> `audit-prompts.md` in this directory.** Load that file for targeted per-module,
> per-function, and adversarial-scenario prompts derived from 1141 real findings
> across 200+ Move audit reports.

## confidence-gates.md

# Confidence Gates

Load this file during **Phase 7 — Verify & Triage**. It defines a multi-signal
confidence model that prevents low-evidence findings from receiving high severity.

---

## Section 1: Confidence Levels

Every finding must be assigned one of these confidence levels:

| Level | Definition | Required signals | Max severity allowed |
|-------|-----------|-----------------|---------------------|
| `confirmed` | Two or more independent signals corroborate the finding | 2+ from Section 2 | Critical / High / Medium / Low |
| `likely` | One strong signal supports the finding | 1 strong signal (strength ≥ 3) | High (if signal is strong) / Medium / Low |
| `needs_review` | Pattern match only — no concrete corroboration | 0-1 weak signals | **Medium max** — cannot be High or Critical |

### Rules

- A finding at `needs_review` confidence can NEVER be rated High or Critical.
- `confirmed` requires signals from at least two different categories (e.g., code
  pattern + math proof, not two code patterns).
- If you cannot achieve `likely` or better, the finding should be `QUESTIONABLE`
  in the triage label AND capped at Medium severity.

---

## Section 2: Signal Types for Move Audits

Signals are ordered from weakest to strongest. Each has a numeric strength for gating.

| # | Signal Type | Strength | Description | Example |
|---|------------|----------|-------------|---------|
| 1 | Code pattern match | 1 (weakest) | "This looks like vulnerable pattern X" | Function takes shared object without auth check |
| 2 | Missing check confirmed in source | 2 | Verified that a specific validation is absent by reading the code | No `assert!(sender == admin)` in `set_fee()` — confirmed by reading lines 1-50 |
| 3 | Exploitable call path traced | 3 | Full path from entry point to vulnerable code, all intermediate checks verified | `entry fun liquidate()` → `internal_seize()` → no cash check at line 42 |
| 4 | Mathematical proof | 4 | Algebraic demonstration with concrete values showing overflow/precision loss | `reward_per_share * user_shares` overflows u64 when shares > 10^12 and rate > 10^7 |
| 5 | Concrete PoC pseudocode | 4 | Step-by-step PTB/tx sequence with specific function calls and arguments | PTB: `1. borrow_flash() 2. manipulate_price() 3. borrow() 4. repay_flash()` |
| 6 | Known-vulnerable pattern from real audit | 3 | Pattern matches a documented vulnerability from a published audit report | Matches CertiK finding in Protocol X: same accumulator-before-checkpoint pattern |
| 7 | Test log abort matching claimed bug | 4 | Project's own tests show abort at the exact location/condition claimed | `sui move test` shows `arithmetic_error` at `rewards.move:87` — matches overflow claim |
| 8 | On-chain state confirming precondition | 5 (strongest) | Production data shows the prerequisite state exists or is reachable | Mainnet pool has $50M TVL with reward_rate that triggers overflow after 10 hours |

### Signal Combination Examples

| Signals present | Confidence | Rationale |
|----------------|-----------|-----------|
| #1 only (pattern match) | `needs_review` | No corroboration |
| #2 + #4 (missing check + math proof) | `confirmed` | Two independent signals from different categories |
| #3 only (call path traced) | `likely` | One strong signal (strength 3) |
| #5 + #7 (PoC + test abort) | `confirmed` | Two strong signals |
| #1 + #2 (pattern + missing check) | `likely` | One strong signal (#2), one weak (#1) |

---

## Section 3: Hard Evidence Requirements

For each finding type, the following evidence is NEVER optional. A finding without
its required evidence is automatically `needs_review` regardless of other signals.

### Access Control Bypass
- [ ] Exact missing check identified (file:line where check should be)
- [ ] Who can call the function (entry point classification)
- [ ] What object, signer, or capability is needed vs what is checked
- [ ] Proof that attacker can obtain/access the required inputs

### Arithmetic Overflow / Precision Loss
- [ ] Concrete input values that trigger the overflow
- [ ] Proof those values are reachable in production (token decimals, supply ranges)
- [ ] Impact calculation: what happens after the overflow (abort = DoS or corruption?)
- [ ] For fixed-point: overflow bound derived from helper source (not assumed)

### Oracle Manipulation
- [ ] Price impact calculation: how much capital needed to move price by X%
- [ ] Profit vs cost analysis: flash loan fee + gas + slippage vs extracted value
- [ ] Time window: how long does the manipulated state persist?
- [ ] Alternative oracle check: does the protocol use TWAP, Pyth, or other resistant oracle?

### Flash Loan Attack
- [ ] Full PTB/tx sequence: loan → manipulate → profit → repay
- [ ] Flash loan source identified (which protocol/pool)
- [ ] Intermediate state during flash loan that enables the exploit
- [ ] Hot potato correctly handled (or not) in the sequence

### Reentrancy (Cross-Module State Mutation)
- [ ] Cross-module state mutation path: module A writes, module B reads stale
- [ ] Move has no callbacks — explain the specific mechanism (PTB composition, friend calls)
- [ ] State that becomes inconsistent between the two operations
- [ ] Why the inconsistency is exploitable (not just theoretical)

### Front-Running / MEV
- [ ] Proof attacker can observe the target transaction
  - Sui: **no public mempool** — requires validator collusion or specific conditions
  - Aptos: mempool exists — front-running is feasible
- [ ] Time window for front-running
- [ ] Economic incentive exceeds cost of attack

### Stale State / State Desync
- [ ] Writer path: which function updates the state (file:line)
- [ ] Consumer path: which function reads stale state (file:line)
- [ ] Persistence window: how long can stale state persist?
- [ ] Numeric trace: concrete values showing stale vs fresh state difference

### DoS via Abort
- [ ] Reachable caller: who triggers the abort (entry point)
- [ ] Input values that cause the abort
- [ ] Economic impact: what is blocked? (withdrawals, liquidations, all operations)
- [ ] Recovery path: can the protocol recover, or is it permanent?

---

## Section 4: Completeness Thresholds

Minimum analysis requirements to ensure thorough coverage.

### Per Entry Point

Before marking an entry point as "reviewed," ensure:
- [ ] At least 2 invariants identified and verified (or "none applicable" with reason)
- [ ] At least 3 assumptions documented (e.g., "caller is owner," "amount > 0," "oracle is fresh")
- [ ] All state-mutating paths traced to completion
- [ ] All abort conditions cataloged with reachability assessment

### Per Finding

Before including a finding in the report:
- [ ] Evidence audit table completed (from `verification-policy.md`)
- [ ] Confidence level assigned (`confirmed` / `likely` / `needs_review`)
- [ ] At least 1 concrete value trace (not just "could overflow")
- [ ] Self-hallucination check passed (5-point protocol — re-read source after concluding)
- [ ] Devil's advocate questions 1-11 answered (structured challenge protocol)

---

## Section 5: Confidence Gate Checklist

Run this 6-gate checklist on every finding before finalizing. ALL gates must pass
for the finding to proceed at its claimed severity.

### Gate 1: Process Gate
- [ ] Finding survived Phase 7 Step 1 (Dual Narrative Test)
- [ ] Finding survived Phase 7 Step 2 (8-Dimension Disproof)
- [ ] Finding survived Phase 7 Step 4 (Kill Questions 1-6)
- If any step was skipped → finding cannot be VALID

### Gate 2: Reachability Gate
- [ ] Attacker-accessible entry point identified
- [ ] Full call path traced from entry to vulnerable code
- [ ] All intermediate checks verified (none block the path)
- [ ] Precondition state achievable through valid protocol operations
- If reachability is uncertain → confidence capped at `needs_review`

### Gate 3: Real Impact Gate
- [ ] Specific victim identified (users, LPs, protocol treasury)
- [ ] Dollar-denominated impact estimated (or "non-financial: [description]")
- [ ] Impact survives economic rationality check (profit > cost for attacker)
- If impact is speculative → severity capped at Medium

### Gate 4: PoC Gate
- [ ] Concrete PoC pseudocode written (PTB or transaction script)
- [ ] PoC uses only functions available to the attacker (correct visibility)
- [ ] PoC accounts for gas costs and transaction fees
- If no PoC → confidence capped at `likely`, severity capped at Medium

### Gate 5: Math Bounds Gate
- [ ] All arithmetic claims backed by concrete values
- [ ] Overflow/precision bounds derived from type maximums AND realistic ranges
- [ ] Token decimals and supply ranges sourced from production data or reasonable estimates
- If math is hand-waved → finding cannot be `confirmed`

### Gate 6: Move Safety Gate
- [ ] Finding does not assume EVM capabilities (reentrancy, delegatecall, storage collision)
- [ ] Finding accounts for Move's type system protections (linearity, abilities, no dynamic dispatch)
- [ ] Finding accounts for chain-specific properties (Sui: owned objects, PTB model; Aptos: signer, acquires)
- If finding relies on non-Move assumptions → DISMISSED

## defi

```

```

## defi-vectors.md

# DeFi Attack Vectors — Move

Load this reference when auditing protocols involving tokens, swaps, lending, staking,
oracles, or any financial logic. These patterns apply to both Sui and Aptos Move.

---

## DeFi Subcategory Detection

After loading this file, detect which DeFi subcategories the protocol uses and load
the corresponding deep-dive reference files from the `defi/` subdirectory. **Multiple
files may apply** — a lending protocol typically needs lending + liquidation + oracle.

| Subcategory | Detect when code contains... | Load file |
|-------------|------------------------------|-----------|
| Staking/Yield | `stake`, `unstake`, `reward_per_share`, `reward_per_token`, `accumulator`, `farming` | `defi/defi-staking.md` |
| Oracle | `get_price`, `oracle`, `pyth`, `switchboard`, `price_feed`, `price_info` | `defi/defi-oracle.md` |
| Lending/Borrowing | `borrow`, `repay`, `collateral`, `health_factor`, `loan`, `debt` | `defi/defi-lending.md` |
| Math/Precision | Complex fee/share/interest math, `PRECISION`, `DECIMAL`, `RAY`, `WAD` | `defi/defi-math-precision.md` |
| Slippage/DEX | `swap`, `min_amount_out`, `slippage`, `deadline`, `amm`, `pool` | `defi/defi-slippage.md` |
| Liquidation | `liquidat`, `seize`, `health_factor`, `bad_debt`, `insurance_fund` | `defi/defi-liquidation.md` |
| Auction/CLM | `bid`, `auction`, `TWAP`, `tick`, `concentrated_liquidity`, `rebalance` | `defi/defi-auction-clm.md` |
| Signatures | `ed25519`, `secp256k1`, `verify_signature`, `ecrecover`, `nonce` | `defi/defi-signatures.md` |

---

## Cross-Cutting DeFi Checks (DEFI-01 to DEFI-10)

The checks below apply across all DeFi subcategories. For detailed, category-specific
patterns with full code examples, see the `defi/*.md` files listed above.

---

## DEFI-01 — Oracle Manipulation

**Description:** Price oracles that can be manipulated by a single large transaction.

**Types to check:**
1. **Spot price oracles** — Using current pool ratio as price. Flashloan-manipulable.
2. **Single-source oracles** — Trusting one external module's price feed.
3. **Stale price oracles** — No staleness check on price data.
4. **Circular oracles** — Protocol A uses Protocol B's price, which uses Protocol A's price.

**Pattern to flag:**
```move
// VULNERABLE — spot price from pool ratio
let price = pool.reserve_b / pool.reserve_a;  // flash-loan manipulable
```

**Check:**
- All price reads must include a staleness assertion (e.g., `last_updated > now - MAX_STALE`)
- TWAP should be used for anything that can be economically attacked in a single transaction
- Flash loan protection: verify price is read before and after any large operation; compare

---

## DEFI-02 — Flash Loan Attack Surface

**Description:** Any protocol that can be economically attacked with a flash loan.

**Scenarios to evaluate:**
1. **Collateral inflation:** Borrow → manipulate oracle → borrow more against inflated value → repay
2. **Governance attacks:** Borrow tokens → vote → repay (if voting snapshot is same block)
3. **Sandwich attacks:** Is the protocol vulnerable to MEV sandwiching?
4. **Reentrancy-via-flash-loan:** Flash loan triggers a callback that re-enters the protocol

**Check on every lending/borrowing function:**
- Price read uses TWAP, not spot
- Any callback mechanism (flash loan repay hook) cannot re-enter the main protocol
- Governance snapshots are not in the same transaction as the vote

---

## DEFI-03 — Liquidity Pool Manipulation

**Description:** AMM/pool vulnerabilities specific to Move protocols.

**Patterns:**
1. **First depositor attack:** If pool starts empty, first depositor sets the price.
   Check: Minimum initial liquidity burned to dead address.

2. **Rounding in LP share calculation:**
   ```
   shares = (deposit * total_shares) / total_reserves
   ```
   If `total_reserves` rounds against the protocol, attackers can extract value.
   Check: Rounding direction always favors the protocol.

3. **Imbalanced pool draining:** Providing one-sided liquidity to skew pool, then swapping.
   Check: Slippage limits on all swaps.

4. **Infinite mint via precision:**
   ```move
   // 1 wei deposit → mints 1e18 shares if total_supply = 0
   ```
   Check: Dead shares minted at initialization prevent this.

---

## DEFI-04 — Loan / Borrow Invariants

**Description:** Lending protocol invariants that must hold at all times.

**Invariants to verify:**
1. `total_borrowed ≤ total_deposited` (solvency)
2. `user_borrow_value ≤ user_collateral_value * LTV` (individual solvency)
3. Liquidation threshold > borrow threshold (liquidation is possible before insolvency)
4. Interest accrual doesn't make healthy positions undercollateralized in one block

**Check:**
- Is solvency checked at the end of every borrow and withdrawal?
- Can interest accrual cause a position to become instantly liquidatable without warning?
- Are bad debt scenarios handled? What happens if liquidation profit < gas cost?

---

## DEFI-05 — Reward / Yield Calculation Errors

**Description:** Errors in reward distribution math.

**Common bugs:**

1. **Rewards before staking:** Rewards accrued from block 0 instead of from user deposit time.
2. **Reward dilution:** New stakers retroactively receive past rewards.
3. **Precision loss in accumulator:** `reward_per_token` accumulator loses precision for small stakes.
4. **Integer division in per-user share:**
   ```
   user_reward = (user_stake * reward_per_token_stored) / PRECISION
   ```
   If PRECISION is too small, large stakers lose dust rewards to rounding.

**Check:**
- Reward calculation uses the "per-token accumulator" pattern correctly
- New stakers don't receive historical rewards
- PRECISION constant is large enough (1e12 or greater recommended)
- Reward accrual handles the zero-stakers case (no division by zero)

---

## DEFI-06 — Liquidation Mechanism

**Description:** Liquidation functions that can be abused or blocked.

**Check:**

1. **Liquidation griefing:** Can a position be made impossible to liquidate?
   (e.g., by making the collateral transfer fail)

2. **Dust liquidation:** Can tiny positions never be liquidated profitably?
   → Bad debt accumulates.

3. **Liquidation bonus manipulation:** Is the liquidation bonus (incentive for liquidators)
   fixed or calculable? Can it be gamed?

4. **Partial vs full liquidation:** If partial liquidation is allowed, can an attacker
   leave a position just above the threshold to prevent full liquidation?

5. **Collateral type manipulation during liquidation:** On Sui, if collateral is a mutable
   shared object, can its value change between the liquidation check and the liquidation execution?

6. **Off-by-threshold (sequential check trap):** Multiple sequential health factor checks
   where the first is stricter than needed, blocking valid liquidations.
   ```move
   // VULNERABLE — two sequential checks, first one blocks valid liquidations
   public fun liquidate(position: &Position) {
       let hf = calculate_health_factor(position);
       assert!(hf < 9500, E_HEALTHY);  // 0.95 — too strict!
       assert!(hf < 10000, E_HEALTHY); // 1.0 — correct threshold
       // Users with HF between 0.95–1.0 are unhealthy but unliquidatable
       // Bad debt silently accumulates
   }

   // SAFE — single threshold check
   public fun liquidate(position: &Position) {
       let hf = calculate_health_factor(position);
       assert!(hf < 10000, E_HEALTHY); // only check: HF < 1.0
       // Separate close factor logic can use 0.95 threshold if needed
   }
   ```
   Check: Verify liquidation entry has only one health factor gate matching the actual
   liquidation threshold. Stricter checks (like close factor) should control *how much*
   is liquidated, not *whether* liquidation is allowed.
   *Real audit ref: Aptos AAVE fork (hf < 0.95 blocks liquidation for 0.95–1.0 accounts,
   originally caught by Certora — High)*

---

## DEFI-07 — Slippage and Front-Running

**Description:** Transactions without slippage protection are vulnerable to MEV.

**Pattern:**
```move
// VULNERABLE — no minimum output specified
public entry fun swap(
    pool: &mut Pool,
    coin_in: Coin<A>,
    ctx: &mut TxContext
): Coin<B> {
    // No min_amount_out check!
    execute_swap(pool, coin_in)
}
```

**Check:**
1. All swap functions must have a `min_amount_out` parameter
2. `min_amount_out = 0` should be disallowed or flagged as dangerous
3. Deadline parameters should be enforced for time-sensitive operations
4. On Aptos: verify that `min_amount_out` is checked against actual output, not input

---

## DEFI-08 — Interest Rate Model Safety

**Description:** Interest rate models that can be driven to extreme values.

**Check:**
1. Interest rate has a defined maximum cap (e.g., 1000% APR)
2. Interest rate calculation doesn't overflow when utilization approaches 100%
3. Compound interest calculation doesn't overflow for large time deltas
4. Division in interest calculation: verify denominator can never be zero

---

## DEFI-09 — Governance / Timelock Bypass

**Description:** Governance mechanisms that can be bypassed or short-circuited.

**Check:**
1. Proposal execution has a timelock — flag absence as High
2. Flash loan governance attacks: voting power snapshot taken before proposal, not at vote time
3. Can a whale manipulate a token price to acquire governance power in one transaction?
4. Emergency powers: who has them, and under what conditions?

---

## DEFI-10 — Bridge / Cross-Chain Patterns (Move)

**Description:** Move protocols that bridge assets between Sui ↔ Aptos or to/from EVM chains.

**Check:**
1. Message replay protection: each bridge message has a unique nonce/hash
2. Signature threshold: minimum N-of-M validators required
3. Token supply consistency: minting on destination must match burning on source
4. Validator set changes: how is the validator set updated? Can a compromised validator set update itself?
5. Finality assumptions: how many confirmations before a bridge event is considered final?

---

## DeFi Verification Checklist

**Cross-cutting (always check):**
- [ ] All price reads use TWAP or include staleness check (→ `defi/defi-oracle.md`)
- [ ] First depositor attack mitigated (dead shares at init) (→ `defi/defi-staking.md`)
- [ ] Rounding always favors protocol, not user (→ `defi/defi-math-precision.md`)
- [ ] Solvency check on every borrow and withdrawal (→ `defi/defi-lending.md`)
- [ ] Reward calculation uses per-token accumulator correctly (→ `defi/defi-staking.md`)
- [ ] Liquidation profitable for all realistic collateral values (→ `defi/defi-liquidation.md`)
- [ ] All swap functions have `min_amount_out` parameter (→ `defi/defi-slippage.md`)
- [ ] Interest rate model has maximum cap and no overflow
- [ ] Governance has timelock
- [ ] Bridge messages have replay protection

**Subcategory-specific (check when loaded):**
- [ ] Staking: Flash deposit/withdraw griefing mitigated (DEFI-14)
- [ ] Oracle: Different staleness thresholds per feed (DEFI-18)
- [ ] Oracle: Depeg scenarios handled for wrapped assets (DEFI-21)
- [ ] Lending: Pause mechanism is symmetric (repay ↔ liquidate) (DEFI-28)
- [ ] Lending: Token denylist/freeze cannot permanently block operations (DEFI-29)
- [ ] Math: Division always after multiplication (DEFI-35)
- [ ] Math: Time units consistent — Sui ms, Aptos seconds (DEFI-41)
- [ ] Slippage: No hardcoded slippage tolerance (DEFI-45)
- [ ] Liquidation: Grace period before liquidation after unpause (DEFI-64)
- [ ] Signatures: Chain ID included in signed messages (DEFI-75)

## defi/defi-auction-clm.md

# DeFi Auctions & Concentrated Liquidity — Move

Vulnerability patterns for auction mechanisms and concentrated liquidity managers (CLMs)
in Move DeFi protocols. Covers liquidation auctions, Dutch auctions, NFT auctions,
and concentrated liquidity management (Cetus CLMM, Turbos, etc.).

---

## Auction Patterns

---

## DEFI-67 — Self-Bidding Timer Reset

**Description:** An auction participant (often the position owner in liquidation auctions)
bids on their own auction to reset the timer, extending it indefinitely. The borrower
avoids liquidation by perpetually refreshing the auction without actually losing collateral.

**Pattern:**
```move
// VULNERABLE — any bid resets timer, including from the position owner
public fun bid(auction: &mut Auction, bid_amount: Coin<USDC>, ctx: &mut TxContext) {
    assert!(coin::value(&bid_amount) > auction.highest_bid, E_BID_TOO_LOW);
    // Return previous highest bid
    refund_previous_bidder(auction);
    auction.highest_bid = coin::value(&bid_amount);
    auction.highest_bidder = tx_context::sender(ctx);
    auction.end_time = auction.end_time + TIME_EXTENSION; // timer reset!
}

// SAFE — prevent self-bidding and cap total extensions
public fun bid(auction: &mut Auction, bid_amount: Coin<USDC>, ctx: &mut TxContext) {
    let sender = tx_context::sender(ctx);
    assert!(sender != auction.position_owner, E_SELF_BID_PROHIBITED);
    assert!(coin::value(&bid_amount) > auction.highest_bid, E_BID_TOO_LOW);
    refund_previous_bidder(auction);
    auction.highest_bid = coin::value(&bid_amount);
    auction.highest_bidder = sender;
    // Cap total extensions
    let new_end = auction.end_time + TIME_EXTENSION;
    let max_end = auction.start_time + MAX_AUCTION_DURATION;
    auction.end_time = if (new_end < max_end) { new_end } else { max_end };
}
```

**Check:**
1. Can the position owner / auction creator bid on their own auction?
2. Is there a cap on total auction duration (max extensions)?
3. Does each bid require a meaningful increase (e.g., 5% minimum increment)?

---

## DEFI-68 — Insufficient Auction Length Validation

**Description:** Auction duration can be set to zero or very short values, allowing
the creator or an attacker to seize assets immediately or with minimal competition.

**Pattern:**
```move
// VULNERABLE — no minimum auction duration
public fun create_auction(
    admin_cap: &AdminCap,
    item: Object,
    duration: u64,
    clock: &Clock,
    ctx: &mut TxContext
) {
    // duration = 1 (1 millisecond) → auction ends instantly
    let auction = Auction {
        item,
        end_time: clock::timestamp_ms(clock) + duration,
        // ...
    };
}

// SAFE — enforce minimum duration
const MIN_AUCTION_DURATION_MS: u64 = 3_600_000; // 1 hour minimum

public fun create_auction(
    admin_cap: &AdminCap,
    item: Object,
    duration: u64,
    clock: &Clock,
    ctx: &mut TxContext
) {
    assert!(duration >= MIN_AUCTION_DURATION_MS, E_DURATION_TOO_SHORT);
    let auction = Auction {
        item,
        end_time: clock::timestamp_ms(clock) + duration,
    };
}
```

**Check:**
1. Is there a minimum auction duration? Flag absence as High
2. Can admin set duration to 0 or 1?
3. On Aptos: check seconds vs milliseconds in duration

---

## DEFI-69 — Off-by-One Auction Seizure

**Description:** Using `>=` instead of `>` (or vice versa) in the auction end time
comparison allows seizure one timestamp unit before the auction truly ends, or
prevents seizure at exactly the end time.

**Pattern:**
```move
// VULNERABLE — off-by-one allows seizure during active auction
public fun seize(auction: &Auction, clock: &Clock): Object {
    // >= means seizure possible at exactly end_time, while auction
    // should still be active until end_time passes
    assert!(clock::timestamp_ms(clock) >= auction.end_time, E_AUCTION_ACTIVE);
    // A bidder could bid at end_time, then seizure happens in same ms
    remove_item(auction)
}

// SAFE — strictly after end_time
public fun seize(auction: &Auction, clock: &Clock): Object {
    assert!(clock::timestamp_ms(clock) > auction.end_time, E_AUCTION_ACTIVE);
    remove_item(auction)
}
```

**Check:**
1. Verify `>` vs `>=` in all timestamp comparisons for auction boundaries
2. Check both auction start and end conditions for off-by-one
3. Ensure bid acceptance and seizure windows don't overlap

---

## Concentrated Liquidity Manager Patterns

---

## DEFI-70 — Missing TWAP Checks on Rebalance

**Description:** CLM rebalance operations redeploy liquidity to new tick ranges. If
rebalance doesn't check TWAP, an attacker can sandwich the rebalance: manipulate spot
price → trigger rebalance at wrong tick range → reverse manipulation → profit.

**Pattern:**
```move
// VULNERABLE — rebalance uses spot price, no TWAP protection
public fun rebalance(
    clm: &mut CLMVault,
    pool: &mut Pool,
    new_lower_tick: u32,
    new_upper_tick: u32,
) {
    // Removes liquidity from old range, adds to new range
    // If pool price is manipulated, new range is wrong
    let current_tick = pool::current_tick(pool);
    remove_liquidity(clm, pool);
    add_liquidity_at_range(clm, pool, new_lower_tick, new_upper_tick);
}

// SAFE — verify spot price is close to TWAP before rebalancing
public fun rebalance(
    clm: &mut CLMVault,
    pool: &mut Pool,
    new_lower_tick: u32,
    new_upper_tick: u32,
    clock: &Clock,
) {
    let spot_price = pool::current_sqrt_price(pool);
    let twap_price = pool::get_twap(pool, TWAP_WINDOW);
    let deviation = abs_diff(spot_price, twap_price) * 10000 / twap_price;
    assert!(deviation <= MAX_DEVIATION_BPS, E_PRICE_MANIPULATION);
    remove_liquidity(clm, pool);
    add_liquidity_at_range(clm, pool, new_lower_tick, new_upper_tick);
}
```

**Check:**
1. Every function that deploys/redeploys liquidity must check TWAP
2. `MAX_DEVIATION_BPS` should be reasonable (e.g., 100-500 BPS)
3. TWAP window should be long enough to resist manipulation (e.g., 30 minutes)

---

## DEFI-71 — TWAP Parameter Manipulation

**Description:** Admin can set TWAP parameters (deviation threshold, observation window)
to ineffective values, disabling protection. Setting `MAX_DEVIATION = 10000` (100%)
or `TWAP_WINDOW = 1` (1 second) effectively removes TWAP protection.

**Pattern:**
```move
// VULNERABLE — no bounds on TWAP parameters
public fun set_twap_params(
    admin_cap: &AdminCap,
    config: &mut Config,
    max_deviation: u64,
    twap_window: u64,
) {
    // Admin can set max_deviation = 10000 (100%) — no protection
    // Or twap_window = 0 — reads current price as TWAP
    config.max_deviation = max_deviation;
    config.twap_window = twap_window;
}

// SAFE — enforce parameter bounds
public fun set_twap_params(
    admin_cap: &AdminCap,
    config: &mut Config,
    max_deviation: u64,
    twap_window: u64,
) {
    assert!(max_deviation >= MIN_DEVIATION && max_deviation <= MAX_DEVIATION, E_INVALID);
    assert!(twap_window >= MIN_TWAP_WINDOW, E_INVALID); // e.g., >= 300 seconds
    config.max_deviation = max_deviation;
    config.twap_window = twap_window;
}
```

**Check:**
1. Can admin set deviation to 100% or TWAP window to 0?
2. Are there hardcoded minimum bounds for both parameters?
3. Flag missing validation as Medium (admin trust assumption)

---

## DEFI-72 — Stuck Tokens from Tick Math Rounding

**Description:** Concentrated liquidity calculations involving tick math and `u64`
precision cause rounding dust. Over many rebalances, tiny token amounts become
permanently stuck in the contract — never withdrawable.

**Pattern:**
```move
// VULNERABLE — rounding dust lost on each rebalance
public fun rebalance(clm: &mut CLMVault, pool: &mut Pool) {
    let (amount_a, amount_b) = remove_all_liquidity(clm, pool);
    // After remove: amount_a=999999, amount_b=500001
    let (used_a, used_b) = add_liquidity_at_new_range(pool, amount_a, amount_b);
    // After add: used_a=999998, used_b=500000 — 1 unit of each stuck
    // Over 1000 rebalances: 1000 units of each token permanently stuck
}

// SAFE — sweep dust back to vault or fee collector
public fun rebalance(clm: &mut CLMVault, pool: &mut Pool) {
    let (amount_a, amount_b) = remove_all_liquidity(clm, pool);
    let (used_a, used_b) = add_liquidity_at_new_range(pool, amount_a, amount_b);
    let dust_a = amount_a - used_a;
    let dust_b = amount_b - used_b;
    // Return dust to vault balance, not lost
    if (dust_a > 0) { balance::join(&mut clm.idle_a, dust_a); };
    if (dust_b > 0) { balance::join(&mut clm.idle_b, dust_b); };
}
```

**Check:**
1. After liquidity operations, is the difference between input and used amounts tracked?
2. Can accumulated dust be withdrawn by an admin or fee mechanism?
3. Over N rebalances, what's the total token loss?

---

## DEFI-73 — Retrospective Fee Application on New Liquidity

**Description:** When protocol fees are updated, the new fee rate retroactively applies
to previously earned but unclaimed fees. Users who earned fees at 5% are suddenly
charged 10% on their existing earnings.

**Pattern:**
```move
// VULNERABLE — fee change applies retroactively to unclaimed rewards
public fun set_protocol_fee(admin: &AdminCap, vault: &mut Vault, new_fee: u64) {
    // Changes fee immediately — unclaimed rewards now charged at new rate
    vault.protocol_fee_bps = new_fee;
}

// SAFE — harvest existing rewards before changing fee
public fun set_protocol_fee(admin: &AdminCap, vault: &mut Vault, new_fee: u64) {
    // Collect all pending fees at current rate first
    harvest_all_pending_fees(vault);
    // Then update fee rate for future earnings only
    vault.protocol_fee_bps = new_fee;
}
```

**Check:**
1. When fees are updated, are existing unclaimed rewards settled first?
2. Can a fee increase be applied retroactively to disadvantage users?
3. Is there a timelock on fee changes to allow users to claim before change?

---

## Auction / CLM Verification Checklist

- [ ] Self-bidding prevented in auction mechanisms (DEFI-67)
- [ ] Minimum auction duration enforced (DEFI-68)
- [ ] Auction timestamp comparisons use correct operator (`>` vs `>=`) (DEFI-69)
- [ ] All liquidity deployment functions check TWAP before execution (DEFI-70)
- [ ] TWAP deviation and window parameters have enforced bounds (DEFI-71)
- [ ] Rounding dust from tick math is tracked and recoverable (DEFI-72)
- [ ] Fee changes do not apply retroactively to unclaimed rewards (DEFI-73)

## defi/defi-lending-design-patterns.md

# Known-Good Lending Design Patterns (NOT Bugs)

These are established, intentional design patterns used by battle-tested lending protocols
(Compound, Aave, MakerDAO, and their Move forks). **Do NOT report these as vulnerabilities**
unless you can demonstrate why the specific protocol's context makes the pattern unsafe.

Load this file alongside `defi-lending.md` when auditing lending protocols. Cross-reference
every lending-related candidate finding against these patterns before labeling it VALID.

---

## DESIGN-L1 — Spot Prices for Liquidation Seize, EMA/TWAP for Eligibility

**Why it's correct:**
- **Eligibility check (is this position liquidatable?)** uses EMA/TWAP to resist flash
  crashes and oracle manipulation — a momentary price spike shouldn't trigger mass liquidations.
- **Seize calculation (how much collateral does the liquidator get?)** uses spot price because
  the liquidator must sell the seized collateral at current market price. Using EMA for seize
  would underpay liquidators, making liquidation unprofitable, leading to bad debt accumulation.

**This is how Compound, Aave, and most lending protocols work.**

**False positive pattern:** "Seize uses spot while eligibility uses EMA — inconsistent oracle usage!"

**When it IS a bug:** If the protocol documentation explicitly states both should use the same
oracle mode, or if the spot/EMA divergence creates an arbitrage where attackers can self-liquidate
at a profit during high volatility (cross-ref: DEFI-59).

**Caveat — Missing EMA-Spot Divergence Guard in Liquidation Path:**
While using spot for seize and EMA for eligibility is a valid design choice, the liquidation path should still enforce a MAXIMUM EMA-spot divergence tolerance. If borrow/withdraw operations enforce this tolerance (reverting when EMA and spot diverge by >X%), but liquidation does NOT, then during extreme volatility the liquidation path becomes the only functioning code path — and it operates with potentially arbitrarily stale or divergent prices. Consider: add a wider (but not unlimited) tolerance check for liquidation, e.g., 2x the borrow/withdraw tolerance.

---

## DESIGN-L2 — Flash Loan Not Updating Accounting Fields (cash/debt)

**Why it's correct:**
- Hot potato / receipt pattern guarantees repayment within the same transaction.
- The accounting field (e.g., `cash`, `total_borrows`) correctly reflects the post-repayment
  state because repayment is guaranteed by Move's type system — the receipt struct has no
  `drop` ability, so the transaction aborts if repayment doesn't happen.
- **Decrementing cash during flash loan would UNDERSTATE true reserves** and create an
  exploitable exchange rate depression during the flash loan window. Other users' share
  calculations would be temporarily wrong.

**False positive pattern:** "cash not decremented during flash loan — inflated exchange rate!"

**When it IS a bug:**
- If the receipt CAN be destroyed without full repayment (check receipt struct abilities)
- If the accounting field is read by OTHER functions in the same PTB/transaction between
  borrow and repay, and those functions make decisions based on the stale value
- If there is no receipt pattern and repayment is merely checked by balance comparison

Cross-ref: SUI-09 (hot potato), SUI-17 (hot potato state reset), DEFI-27 (loan closure)

---

## DESIGN-L3 — Blocking Borrows When Cash < Cash Reserve

**Why it's correct:**
- Protocol reserves (cash reserve ratio) must be maintained for withdrawal liquidity.
- When accumulated fees/interest exceed available cash, the protocol correctly blocks new
  borrows until deposits or repayments restore liquidity.
- This is protective behavior, not a DoS vulnerability. It resolves naturally via normal
  protocol operations (deposits, repayments, fee harvesting).

**False positive pattern:** "Underflow in borrow check causes DoS in high-utilization markets!"

**When it IS a bug:**
- If the underflow aborts with an unhelpful error instead of a clean "insufficient liquidity"
  message (informational, not a vulnerability)
- If the blocking condition can be triggered by an attacker at low cost to grief legitimate
  borrowers permanently (not just during natural high utilization)
- If repayment or deposit paths are ALSO blocked, creating a deadlock

---

## DESIGN-L4 — Asymmetric EMA/Spot Divergence Formulas

**Why it's correct:**
- Many protocols intentionally use formulas that are MORE restrictive during risky price
  movements (spot crashing below EMA, indicating potential manipulation or flash crash)
  and LESS restrictive during safe movements (spot rising above EMA, indicating organic
  price recovery).
- The asymmetry protects the protocol during the exact conditions when manipulation is
  most likely, while avoiding unnecessary restrictions during normal market movements.

**False positive pattern:** "Formula divides by wrong denominator — asymmetric tolerance
calculation!"

**When it IS a bug:**
- If the asymmetry direction is INVERTED (more permissive during crashes, more restrictive
  during recovery — backwards from the intended protection)
- If the formula produces values outside [0, 1] range for a tolerance check
- If there is no documentation or comment explaining the intentional asymmetry

---

## How to Use This File

When reviewing a lending protocol finding in Phase 5 (Verify & Triage):

1. Check if the finding matches any DESIGN-L pattern above
2. If it matches: verify the protocol's implementation actually follows the established
   pattern (not a broken variant)
3. If the implementation matches the pattern correctly → DISMISS the finding, citing
   the specific DESIGN-L reference
4. If the implementation deviates from the pattern in a meaningful way → proceed with
   the finding but note the deviation explicitly

**Key principle:** The burden of proof shifts when a pattern matches established protocol
design. Instead of proving the code is safe, you must prove why the established pattern
is unsafe in THIS specific context.

## defi/defi-lending.md

# DeFi Lending Vulnerability Patterns (DEFI-25 to DEFI-34)

Deep-dive reference for auditing Move lending protocols on Sui and Aptos.
Load when code contains `borrow`, `repay`, `collateral`, `health_factor`,
`loan`, or `debt`.

---

## DEFI-25 — Premature Liquidation (Threshold Off-by-One)

**Description:** Wrong comparison operator causes positions at exactly the
liquidation threshold to be liquidatable when the spec considers them healthy.

**Pattern:**
```move
// VULNERABLE — `<` liquidates at exactly LIQUIDATION_THRESHOLD
public fun liquidate<T>(pool: &mut LendingPool, borrower: address,
    repay: Coin<T>, ctx: &mut TxContext) {
    let hf = calculate_health_factor(pool, borrower);
    assert!(hf < LIQUIDATION_THRESHOLD, E_HEALTHY); // BUG: == triggers liquidation
    execute_liquidation(pool, borrower, repay, ctx);
}

// SAFE — `<=` means only strictly-below threshold is liquidatable
public fun liquidate<T>(pool: &mut LendingPool, borrower: address,
    repay: Coin<T>, ctx: &mut TxContext) {
    let hf = calculate_health_factor(pool, borrower);
    assert!(hf <= LIQUIDATION_THRESHOLD, E_HEALTHY);
    execute_liquidation(pool, borrower, repay, ctx);
}
```

**Check:**
1. Verify `<` vs `<=` matches the protocol spec for the liquidation boundary
2. Cross-ref: DEFI-06

---

## DEFI-26 — Collateral Manipulation Between Check and Execution

**Description:** On Sui, shared-object collateral can change between PTB steps.
On Aptos, an external call between assert and seizure can mutate collateral,
causing stale data to drive the seizure amount.

**Pattern:**
```move
// VULNERABLE — read collateral, external call, then use stale value
public fun liquidate_position(pool: &mut LendingPool, vault: &mut CollateralVault,
    oracle: &PriceOracle, borrower: address, ctx: &mut TxContext) {
    let coll_val = get_collateral_value(vault, oracle, borrower);
    let debt_val = get_debt_value(pool, borrower);
    assert!((coll_val * PRECISION) / debt_val < LIQUIDATION_THRESHOLD, E_HEALTHY);
    accrue_interest(pool); // external call — vault could change
    transfer_collateral(vault, borrower, tx_context::sender(ctx),
        compute_seize(coll_val, debt_val)); // stale coll_val
}

// SAFE — settle state first, then atomically check-and-execute
public fun liquidate_position(pool: &mut LendingPool, vault: &mut CollateralVault,
    oracle: &PriceOracle, borrower: address, ctx: &mut TxContext) {
    accrue_interest(pool);
    let coll_val = get_collateral_value(vault, oracle, borrower);
    let debt_val = get_debt_value(pool, borrower);
    let health = (coll_val * PRECISION) / debt_val;
    assert!(health < LIQUIDATION_THRESHOLD, E_HEALTHY);
    transfer_collateral(vault, borrower, tx_context::sender(ctx),
        compute_seize(coll_val, debt_val));
    assert!(calculate_health_factor(pool, vault, oracle, borrower) > health, E_NOT_IMPROVED);
}
```

**Check:**
1. Look for external calls between reading collateral and seizing it
2. Cross-ref: common-move.md 6.4 (TOCTOU), SUI-02 (shared-object races)

---

## DEFI-27 — Loan Closure Without Full Repayment

**Description:** The hot-potato/receipt pattern fails to enforce full repayment
plus fees before the receipt is destroyed, allowing pool drainage.

**Pattern:**
```move
// VULNERABLE — receipt destroyed without checking repayment amount
public fun flash_repay(pool: &mut LendingPool, repayment: Coin<SUI>, receipt: FlashReceipt) {
    let FlashReceipt { borrow_amount: _, fee: _ } = receipt; // no value check
    coin::put(&mut pool.reserves, repayment);
}

// SAFE — assert full repayment before destroying receipt
public fun flash_repay(pool: &mut LendingPool, repayment: Coin<SUI>, receipt: FlashReceipt) {
    let FlashReceipt { borrow_amount, fee } = receipt;
    assert!(coin::value(&repayment) >= borrow_amount + fee, E_INSUFFICIENT_REPAYMENT);
    coin::put(&mut pool.reserves, repayment);
}
```

**Check:**
1. Verify receipt enforces `repaid >= borrow_amount + fee` before destruction
2. Check fee bypass via zero-value coin or splitting
3. Cross-ref: SUI-09 (hot-potato integrity), SUI-20 (flash loan patterns)

---

## DEFI-28 — Asymmetric Pause (Repayment Blocked, Liquidation Active)

**Description:** Protocol pauses repayments but leaves liquidation active,
creating an unfair forced-liquidation window where borrowers are helpless.

**Pattern:**
```move
// VULNERABLE — repay checks pause, liquidate does not
public fun repay<T>(pool: &mut LendingPool, payment: Coin<T>, ctx: &mut TxContext) {
    assert!(!pool.paused, E_PAUSED);
    reduce_debt(pool, tx_context::sender(ctx), coin::value(&payment));
    coin::put(&mut pool.reserves, payment);
}
public fun liquidate<T>(pool: &mut LendingPool, borrower: address,
    repay_coin: Coin<T>, ctx: &mut TxContext) {
    // BUG: no pause check — liquidation proceeds while repay is blocked
    assert!(calculate_health_factor(pool, borrower) < LIQUIDATION_THRESHOLD, E_HEALTHY);
    execute_liquidation(pool, borrower, repay_coin, ctx);
}

// SAFE — symmetric pause on both repay and liquidate
public fun liquidate<T>(pool: &mut LendingPool, borrower: address,
    repay_coin: Coin<T>, ctx: &mut TxContext) {
    assert!(!pool.paused, E_PAUSED); // symmetric with repay
    assert!(calculate_health_factor(pool, borrower) < LIQUIDATION_THRESHOLD, E_HEALTHY);
    execute_liquidation(pool, borrower, repay_coin, ctx);
}
```

**Check:**
1. Trace every function gated by `paused`; ensure repay, deposit-collateral, and liquidate are consistent
2. If liquidation is intentionally active during pause, verify a grace period exists (DEFI-30)

---

## DEFI-29 — Token Denylist/Freeze Blocking Repayment

**Description:** Sui `DenyCapV2` or Aptos `FungibleAsset` freeze blocks
transfers from denylisted addresses, preventing repayment and forcing
liquidation through no fault of the borrower.

**Pattern:**
```move
// VULNERABLE — only direct repayment; denylisted borrower tx reverts
public fun repay<T>(pool: &mut LendingPool, payment: Coin<T>, ctx: &mut TxContext) {
    reduce_debt(pool, tx_context::sender(ctx), coin::value(&payment));
    coin::put(&mut pool.reserves, payment);
}

// SAFE — third-party repayment path for denylisted borrowers
public fun repay<T>(pool: &mut LendingPool, payment: Coin<T>, ctx: &mut TxContext) {
    process_repayment(pool, tx_context::sender(ctx), payment);
}
public fun repay_on_behalf<T>(pool: &mut LendingPool, borrower: address,
    payment: Coin<T>, _ctx: &mut TxContext) {
    process_repayment(pool, borrower, payment); // payer != borrower
}
fun process_repayment<T>(pool: &mut LendingPool, borrower: address, payment: Coin<T>) {
    reduce_debt(pool, borrower, coin::value(&payment));
    coin::put(&mut pool.reserves, payment);
}
```

**Check:**
1. Identify whether any supported token is regulated/freezable
2. Verify `repay_on_behalf` or proxy-repayment exists
3. Cross-ref: SUI-21 (DenyList / regulated coin risks)

---

## DEFI-30 — No Grace Period Before Liquidation

**Description:** Positions become liquidatable and are immediately seized with
no time buffer for users to add collateral or repay.

**Pattern:**
```move
// VULNERABLE — instant liquidation the moment health drops
public fun liquidate<T>(pool: &mut LendingPool, clock: &Clock,
    borrower: address, repay_coin: Coin<T>, ctx: &mut TxContext) {
    accrue_interest(pool, clock);
    assert!(calculate_health_factor(pool, borrower) < LIQUIDATION_THRESHOLD, E_HEALTHY);
    execute_liquidation(pool, borrower, repay_coin, ctx);
}

// SAFE — require grace period after position first becomes unhealthy
public fun liquidate<T>(pool: &mut LendingPool, clock: &Clock,
    borrower: address, repay_coin: Coin<T>, ctx: &mut TxContext) {
    accrue_interest(pool, clock);
    assert!(calculate_health_factor(pool, borrower) < LIQUIDATION_THRESHOLD, E_HEALTHY);
    let now = clock::timestamp_ms(clock);
    let pos = borrow_position_mut(pool, borrower);
    if (pos.unhealthy_since == 0) {
        pos.unhealthy_since = now;
        abort E_GRACE_PERIOD_ACTIVE
    };
    assert!(now - pos.unhealthy_since >= GRACE_PERIOD_MS, E_GRACE_PERIOD_ACTIVE);
    execute_liquidation(pool, borrower, repay_coin, ctx);
}
public fun reset_grace(pool: &mut LendingPool, borrower: address) {
    if (calculate_health_factor(pool, borrower) >= LIQUIDATION_THRESHOLD) {
        borrow_position_mut(pool, borrower).unhealthy_since = 0;
    };
}
```

**Check:**
1. Look for `grace_period` or `unhealthy_since` in position state
2. Verify grace resets when health restored; evaluate interaction with DEFI-28

---

## DEFI-31 — Incorrect Liquidation Share Calculation

**Description:** Liquidation omits close factor and liquidation bonus, or
calculates seizure from the wrong base, letting the liquidator take more
collateral than warranted or liquidate the entire debt in one call.

**Pattern:**
```move
// VULNERABLE — no close_factor, no bonus, no post-check
public fun liquidate(pool: &mut LendingPool, borrower: address,
    repay_amount: u64, coll_price: u64, debt_price: u64) {
    seize_collateral(pool, borrower, (repay_amount * debt_price) / coll_price);
}

// SAFE — close_factor cap, bonus, post-health validation
public fun liquidate(pool: &mut LendingPool, borrower: address,
    repay_amount: u64, coll_price: u64, debt_price: u64) {
    let total_debt = get_total_debt(pool, borrower);
    assert!(repay_amount <= (total_debt * CLOSE_FACTOR_BPS) / 10000, E_EXCEEDS_CLOSE_FACTOR);
    let num = (repay_amount as u128) * (debt_price as u128) * (10000u128 + (BONUS_BPS as u128));
    let seize = ((num / ((coll_price as u128) * 10000u128)) as u64);
    assert!(seize <= get_collateral_balance(pool, borrower), E_INSUFFICIENT_COLLATERAL);
    seize_collateral(pool, borrower, seize);
    reduce_debt(pool, borrower, repay_amount);
    assert!(calculate_health_factor(pool, borrower) > LIQUIDATION_THRESHOLD
        || total_debt == repay_amount, E_NOT_IMPROVED);
}
```

**Check:**
1. Verify close_factor caps maximum repayable debt per call
2. Confirm bonus is bounded; check post-liquidation health validated
3. Cross-ref: DEFI-06, common-move.md 10.4

---

## DEFI-32 — Dust Position Accumulation

**Description:** `u64` arithmetic means tiny positions round repay amounts to
zero. These dust positions can never be closed, accumulating as bad debt.

**Pattern:**
```move
// VULNERABLE — small debt rounds to 0; dust remains forever
public fun calc_repay(user_debt: u64, ratio: u64): u64 {
    (user_debt * ratio) / PRECISION_BPS // (5 * 1000) / 10000 = 0
}

// SAFE — minimum position size + force-close for dust
const MIN_BORROW: u64 = 1000;
public fun borrow(pool: &mut LendingPool, amount: u64, ctx: &mut TxContext) {
    assert!(amount >= MIN_BORROW, E_BELOW_MINIMUM);
    create_loan(pool, tx_context::sender(ctx), amount);
}
public fun repay(pool: &mut LendingPool, borrower: address, amount: u64) {
    let debt = get_debt(pool, borrower);
    if (debt <= MIN_BORROW || amount >= debt) {
        reduce_debt(pool, borrower, debt); // force full closure
    } else {
        assert!(debt - amount >= MIN_BORROW, E_DUST_POSITION);
        reduce_debt(pool, borrower, amount);
    };
}
```

**Check:**
1. Verify minimum borrow size enforced at creation
2. Check partial repay cannot leave dust below minimum

---

## DEFI-33 — Forced Debt / Unauthorized Loan Creation

**Description:** Attacker forces debt onto unwilling users. On Sui, `store`
lets debt objects be transferred to victims. On Aptos, a forwarded signer
allows `move_to` at an arbitrary address.

**Pattern:**
```move
// VULNERABLE (Sui) — `store` lets anyone transfer debt to victim
public struct DebtObligation has key, store { id: UID, amount: u64 }
public fun force_debt(victim: address, ctx: &mut TxContext) {
    transfer::public_transfer(
        DebtObligation { id: object::new(ctx), amount: 1_000_000 }, victim);
}

// SAFE (Sui) — no `store`; debt bound to sender only
public struct DebtObligation has key { id: UID, borrower: address, amount: u64 }
public fun borrow(pool: &mut LendingPool, amount: u64, ctx: &mut TxContext): Coin<SUI> {
    let borrower = tx_context::sender(ctx);
    transfer::transfer(DebtObligation { id: object::new(ctx), borrower, amount }, borrower);
    withdraw_from_pool(pool, amount, ctx)
}

// VULNERABLE (Aptos) — public fun takes arbitrary signer
public fun create_debt(account: &signer, amount: u64) {
    move_to(account, DebtObligation { amount }); // forwarded signer
}

// SAFE (Aptos) — entry fun; signer is tx sender
public entry fun borrow(borrower: &signer, pool: &mut LendingPool, amount: u64) {
    let addr = signer::address_of(borrower);
    assert!(!exists<DebtObligation>(addr), E_ALREADY_HAS_DEBT);
    move_to(borrower, DebtObligation { amount });
    transfer_coins(pool, addr, amount);
}
```

**Check:**
1. On Sui: verify debt structs lack `store`
2. On Aptos: verify debt functions require borrower's own `&signer`
3. No entry point creates debt for other than sender
4. Cross-ref: SUI-04, APT-04

---

## DEFI-34 — State Manipulation via Refinancing

**Description:** Borrow + refinance in the same transaction lets the attacker
reset their interest index, skipping accumulated interest owed.

**Pattern:**
```move
// VULNERABLE — no cooldown; index reset skips owed interest
public fun borrow(pool: &mut LendingPool, clock: &Clock, amount: u64, ctx: &mut TxContext) {
    accrue_interest(pool, clock);
    let pos = get_or_create_position(pool, tx_context::sender(ctx));
    pos.borrowed = pos.borrowed + amount;
    pos.interest_index = pool.global_interest_index;
    withdraw_from_reserves(pool, amount, ctx);
}
public fun refinance(pool: &mut LendingPool, clock: &Clock, ctx: &mut TxContext) {
    accrue_interest(pool, clock);
    let pos = get_or_create_position(pool, tx_context::sender(ctx));
    pos.interest_index = pool.global_interest_index; // skips owed interest
}

// SAFE — cooldown + settle accrued interest before index reset
public fun borrow(pool: &mut LendingPool, clock: &Clock, amount: u64, ctx: &mut TxContext) {
    accrue_interest(pool, clock);
    let pos = get_or_create_position(pool, tx_context::sender(ctx));
    pos.borrowed = pos.borrowed + amount;
    pos.interest_index = pool.global_interest_index;
    pos.last_action_ts = clock::timestamp_ms(clock);
    withdraw_from_reserves(pool, amount, ctx);
}
public fun refinance(pool: &mut LendingPool, clock: &Clock, ctx: &mut TxContext) {
    accrue_interest(pool, clock);
    let pos = get_or_create_position(pool, tx_context::sender(ctx));
    let now = clock::timestamp_ms(clock);
    assert!(now - pos.last_action_ts >= MIN_REFINANCE_COOLDOWN, E_COOLDOWN);
    pos.borrowed = pos.borrowed +
        calc_accrued(pos.borrowed, pos.interest_index, pool.global_interest_index);
    pos.interest_index = pool.global_interest_index;
    pos.last_action_ts = now;
}
```

**Check:**
1. Verify minimum cooldown between borrow/refinance on the same position
2. Confirm accrued interest settled before index reset
3. Check `accrue_interest` is idempotent within a single timestamp

---

## DEFI-80 — Admin Parameter Update Without Pre-Sync

**Description:** Any function that updates an interest rate model, fee rate, reward rate, or
exchange rate parameter must call the protocol's state-flush function
(`accrue_interest`, `update_index`, `sync_rewards`, etc.) BEFORE applying the new
value. If the flush is missing, the new parameter is applied retroactively to
the entire period since the last flush, incorrectly charging or crediting users.

Severity is HIGH because interest/rewards for all users are mispriced for the entire
elapsed period. The error scales with time-since-last-flush × rate-delta × total-borrowed/deposited.
Example: admin updates a rate from 5% to 10% APR after 30 days of no interaction — all 30 days
are retroactively charged at 10% instead of 5%.

**Pattern:**
```move
// VULNERABLE — new rate model applied retroactively to entire elapsed period
public fun update_interest_model(
    _cap: &AdminCap,
    reserve: &mut Reserve,
    new_model: InterestRateModel,
) {
    // BUG: no accrue_interest() call — new rate applies retroactively
    reserve.interest_rate_model = new_model;
}

// SAFE — flush accrued interest at old rate before applying new model
public fun update_interest_model(
    _cap: &AdminCap,
    reserve: &mut Reserve,
    clock: &Clock,
    new_model: InterestRateModel,
) {
    accrue_interest(reserve, clock); // settle at OLD rate first
    reserve.interest_rate_model = new_model;
}
```

**Check:**
1. For every admin/privileged setter that modifies interest rate model, borrow/supply rate
   curves, liquidation bonus/fee percentages, or reward emission rates — verify the function
   body contains a call to `accrue_interest()` or equivalent state-flush BEFORE the assignment
2. Search for: `fun update_.*model`, `fun set_.*rate`, `fun update_.*interest`
3. If absent → HIGH

---

## DEFI-82 — Emergency Mechanism Entry/Stop Source Mismatch

**Description:** Emergency mechanisms (ADL, circuit breakers, pause triggers, liquidation
incentive escalators) have two conditions: an ENTRY condition that activates
the mechanism and a STOP condition that deactivates it. Both conditions must
measure the same metric from the same source.

If the entry condition reads metric A and the stop condition reads metric B,
and A ≠ B (even when both are "total borrows"):
- The mechanism can activate when it should not (A inflated by other groups)
- The mechanism can fail to deactivate when it should (B understates reality)
- Healthy positions can be force-liquidated (wrongful ADL)
- Emergency state persists indefinitely (stop condition never true)

Severity is HIGH if different scopes (reserve-level vs group-level),
MEDIUM if same scope but different rounding direction.

**Pattern:**
```move
// VULNERABLE — ADL entry reads reserve-level debt, stop reads emode-group debt
public fun trigger_adl(reserve: &Reserve, emode_group: &EModeGroup) {
    // Entry: aggregate debt across ALL emode groups
    let total_debt = reserve.total_debt();  // reserve-level
    assert!(total_debt > reserve.adl_threshold, E_NOT_TRIGGERED);
    // ADL executes against positions in this emode_group...
}
public fun stop_adl(emode_group: &EModeGroup) {
    // Stop: only this group's debt
    let group_debt = emode_group.borrow_amount();  // group-level
    assert!(group_debt <= emode_group.target, E_STILL_ABOVE);
    // BUG: different scope — reserve inflated by other groups,
    // but stop checks only this group. ADL fires on healthy groups.
}

// SAFE — both entry and stop read from the same source
public fun trigger_adl(emode_group: &EModeGroup) {
    let group_debt = emode_group.borrow_amount();
    assert!(group_debt > emode_group.adl_threshold, E_NOT_TRIGGERED);
}
public fun stop_adl(emode_group: &EModeGroup) {
    let group_debt = emode_group.borrow_amount();
    assert!(group_debt <= emode_group.target, E_STILL_ABOVE);
}
```

**Check:**
1. For every emergency/escalation mechanism, identify the ENTRY check (what variable, from
   which module/function) and the STOP check (what variable, from which module/function)
2. If entry reads `reserve.total_debt()` and stop reads `emode_group.borrow_amount()`
   → different scopes → HIGH
3. If entry reads a `ceil()` value and stop reads `floor()` of the same value
   → asymmetric threshold → MEDIUM
4. Search for ADL / deleverage / circuit_breaker activation functions; trace the variable
   passed to the entry assert and to the stop/exit check back to their source struct

---

## DEFI-84 — Admin Config Update Overwrites Embedded Runtime State

**Description:** Admin parameter update functions that replace an entire config struct also destroy embedded runtime state (rate limiters, accumulators, counters, timestamps) if the runtime state lives inside the same struct or is unconditionally rebuilt during the update.

**Pattern to flag:**
```move
// VULNERABLE: update() replaces BOTH config AND runtime state
public fun update(emode: &mut EMode, new_params: NewEMode) {
    emode.collateral_config = new_params.collateral;  // config — OK to replace
    emode.limiter = new_limiter(new_params.limiter);   // runtime state — DESTROYED
}
```

**Check:**
1. For every admin config update function, identify ALL fields that get written
2. Classify each field as "config" (intended to change) vs "runtime state" (accumulated values, counters, limiters, timestamps)
3. If any runtime state is overwritten/reset as a side effect of a config update, flag it
4. Check if frontrunning the admin tx allows bypassing the limit: borrow up to limit → admin resets limiter → borrow again

**Impact:**
- Rate limiters reset to zero: borrowers can immediately borrow 2x the limit by sandwiching the admin tx
- Accumulators reset: interest/rewards for the current period are lost
- Counters reset: tracking of active positions becomes inaccurate

**Fix:** Separate config parameters from runtime state. Only update the config fields, preserve runtime state:
```move
public fun update(emode: &mut EMode, new_params: NewEMode) {
    emode.collateral_config = new_params.collateral;
    // DO NOT touch emode.limiter — it contains runtime state
    // Only update limiter CONFIG (window size, max amount) without resetting segments:
    emode.limiter.update_config(new_params.limiter_config);
}
```

**References:** DEFI-80 (missing pre-sync), SUI-28 (PTB repeated call bypass after reset).

---

## DEFI-88 — Missing Post-Trade Health Check in Margin Trading Proxy

**Description:** Margin protocols often wrap an underlying orderbook's trading functions
behind a proxy module that enforces margin-specific invariants. If the proxy checks
price bounds and pool identity but does NOT revalidate the borrower's health ratio
after the trade executes, a leveraged account can keep placing loss-making trades
even when already liquidatable.

**Pattern:**
```move
// VULNERABLE — proxy validates price but not post-trade health
public fun place_order<B, Q>(
    registry: &Registry,
    margin_account: &mut MarginAccount<B, Q>,
    pool: &mut Pool<B, Q>,
    price: u64,
    quantity: u64,
    is_bid: bool,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo {
    assert!(margin_account.pool() == pool.id(), E_WRONG_POOL);
    assert!(registry.pool_enabled(pool), E_DISABLED);
    registry.assert_price_bounds(pool.id(), price, is_bid, clock);
    // NO health check after trade
    let proof = margin_account.trade_proof(ctx);
    pool.place_order(margin_account.balance_manager_mut(ctx), &proof, price, quantity, is_bid, clock, ctx)
}

// SAFE — revalidate health after every trade when debt exists
public fun place_order<B, Q>(
    registry: &Registry,
    margin_account: &mut MarginAccount<B, Q>,
    pool: &mut Pool<B, Q>,
    price: u64,
    quantity: u64,
    is_bid: bool,
    clock: &Clock,
    ctx: &TxContext,
): OrderInfo {
    assert!(margin_account.pool() == pool.id(), E_WRONG_POOL);
    assert!(registry.pool_enabled(pool), E_DISABLED);
    registry.assert_price_bounds(pool.id(), price, is_bid, clock);
    let proof = margin_account.trade_proof(ctx);
    let info = pool.place_order(margin_account.balance_manager_mut(ctx), &proof, price, quantity, is_bid, clock, ctx);
    if (margin_account.has_debt()) {
        let rr = margin_account.risk_ratio(registry, pool, clock);
        assert!(registry.can_trade(pool.id(), rr), E_HEALTH_TOO_LOW);
    };
    info
}
```

**Check:**
1. Find every proxy function that places orders on behalf of a margin/leveraged account
2. Verify each one recomputes health ratio AFTER the trade settles
3. Compare with borrow/withdraw paths — if those check health but trade paths don't, flag it
4. Cross-ref: DEFI-52 (withdrawal threshold), DEFI-89 (self-trade value extraction)

---

## Lending Verification Checklist

- [ ] Liquidation threshold boundary: `<` vs `<=` matches the spec (DEFI-25)
- [ ] No check-then-act gap between reading collateral and seizing it (DEFI-26)
- [ ] Flash loan receipts enforce `repaid >= borrowed + fee` before destruction (DEFI-27)
- [ ] Pause mechanism is symmetric: repay paused implies liquidation paused (DEFI-28)
- [ ] Repayment possible for denylisted/frozen addresses via proxy path (DEFI-29)
- [ ] Grace period between position becoming unhealthy and liquidation (DEFI-30)
- [ ] Liquidation uses close_factor cap, bonus, and post-health validation (DEFI-31)
- [ ] Minimum position size prevents dust; partial repay cannot leave sub-minimum remainder (DEFI-32)
- [ ] Debt objects cannot be transferred to unwilling recipients (DEFI-33)
- [ ] Cooldown between borrow and refinance; accrued interest settled before index reset (DEFI-34)
- [ ] Admin parameter setters call `accrue_interest()` before applying new values (DEFI-80)
- [ ] Emergency mechanism entry and stop conditions read from the same source (DEFI-82)
- [ ] Admin config updates do NOT overwrite embedded runtime state (limiters, accumulators, counters) (DEFI-84)
- [ ] Margin trade proxy revalidates health ratio after every trade when debt exists (DEFI-88)

## defi/defi-liquidation.md

# DeFi Liquidation — Move

Liquidation vulnerability patterns for Move lending/borrowing protocols.

---

## DEFI-50 — No Liquidation Incentive

**Description:** Liquidation provides no bonus to the liquidator. Without economic
incentive, trustless liquidators won't spend gas, leading to bad debt accumulation.

**Pattern:**
```move
// VULNERABLE — liquidator receives exactly the debt value, no bonus
public fun liquidate(position: &mut Position, repayment: Coin<Debt>): Coin<Collateral> {
    let repay_value = coin::value(&repayment);
    withdraw_collateral(position, repay_value) // no profit
}

// SAFE — liquidation bonus incentivizes liquidators
public fun liquidate(position: &mut Position, repayment: Coin<Debt>): Coin<Collateral> {
    let repay_value = coin::value(&repayment);
    let bonus = repay_value * LIQUIDATION_BONUS_BPS / 10000; // 5-10%
    withdraw_collateral(position, repay_value + bonus)
}
```

**Check:** Bonus must exist (5-15%) and exceed gas costs. Cross-ref: DEFI-06

---

## DEFI-51 — No Incentive for Small Positions

**Description:** Dust positions cost more gas to liquidate than the bonus provides.

**Pattern:**
```move
// VULNERABLE — no minimum position size
public fun borrow<T>(account: &mut Account, amount: u64) {
    add_debt(account, amount); // amount = 1 is allowed
}

// SAFE — enforce minimum borrow size
public fun borrow<T>(account: &mut Account, amount: u64) {
    assert!(amount >= MIN_BORROW_AMOUNT, E_BELOW_MINIMUM);
    add_debt(account, amount);
}
```

**Check:** Enforce minimum position sizes. `bonus * min_position > gas_cost`?

---

## DEFI-52 — Collateral Withdrawal Eliminates Liquidation Incentive

**Description:** User withdraws to just above liquidation threshold. Any price drop
causes bad debt with minimal collateral.

**Pattern:**
```move
// VULNERABLE — allows withdrawal to exact liquidation threshold
public fun withdraw_collateral(account: &mut Account, amount: u64) {
    remove_collateral(account, amount);
    assert!(health_factor(account) >= THRESHOLD, E_UNHEALTHY);
}

// SAFE — enforce borrow threshold (higher buffer)
public fun withdraw_collateral(account: &mut Account, amount: u64) {
    remove_collateral(account, amount);
    assert!(health_factor(account) >= BORROW_THRESHOLD, E_INSUFFICIENT);
}
```

**Check:** Withdrawal must enforce BORROW threshold, not LIQUIDATION threshold.

---

## DEFI-53 — No Bad Debt Handling Mechanism

**Description:** When debt exceeds collateral, no mechanism absorbs the loss.

**Pattern:**
```move
// VULNERABLE — bad debt ignored, protocol becomes insolvent
public fun liquidate(position: &mut Position, repayment: Coin<USDC>) {
    let remaining = position.debt - coin::value(&repayment);
    if (remaining > 0 && position.collateral == 0) { /* nothing happens */ };
}

// SAFE — insurance fund absorbs bad debt
public fun liquidate(position: &mut Position, insurance: &mut InsuranceFund, repayment: Coin<USDC>) {
    let remaining = position.debt - coin::value(&repayment);
    if (remaining > 0 && position.collateral == 0) {
        balance::split(&mut insurance.balance, remaining);
        position.debt = 0;
    };
}
```

**Check:** Protocol needs insurance fund or socialized loss mechanism.

---

## DEFI-54 — Partial Liquidation Bypass

**Description:** (a) Partial liquidation lets liquidators cherry-pick profitable portions,
or (b) no partial liquidation means whale positions can't be liquidated.

**Pattern:**
```move
// VULNERABLE — full liquidation only
public fun liquidate(position: &mut Position, repayment: Coin<USDC>) {
    assert!(coin::value(&repayment) == position.debt, E_MUST_REPAY_ALL);
}

// SAFE — close_factor with health improvement check
public fun liquidate(position: &mut Position, repayment: Coin<USDC>) {
    let max_repay = position.debt * CLOSE_FACTOR / 10000;
    assert!(coin::value(&repayment) <= max_repay, E_EXCEEDS_CLOSE_FACTOR);
    execute_liquidation(position, repayment);
    assert!(health_factor(position) > health_factor_before, E_HEALTH_NOT_IMPROVED);
}
```

**Check:** Partial liquidation must improve health. Close_factor prevents cherry-picking.

**Sui PTB Amplification (see SUI-28):**
On Sui, partial liquidation bypass is amplified because PTBs allow calling `liquidate()` N times atomically. Unlike EVM where each block typically processes liquidations independently, a Sui PTB can chain: liquidate_50% → liquidate_25% → liquidate_12.5% → ... in one atomic tx. The close factor becomes exponentially decaying rather than a hard cap. Verify: does the protocol track cumulative liquidation per-transaction or per-call?

---

## DEFI-55 — Incorrect Liquidation Reward Decimals

**Description:** Bonus calculated with wrong decimal scaling.

**Pattern:**
```move
// VULNERABLE — missing / 10000
public fun calculate_bonus(amount: u64, bonus_bps: u64): u64 {
    amount * bonus_bps  // overflow
}
// SAFE
public fun calculate_bonus(amount: u64, bonus_bps: u64): u64 {
    ((amount as u128) * (bonus_bps as u128) / 10000 as u64)
}
```

**Check:** Verify bonus arithmetic uses consistent BPS scaling. Cross-ref: DEFI-37

---

## DEFI-56 — Excessive Protocol Fees Reduce Liquidator Incentive

**Description:** Protocol fee exceeds liquidation bonus, making liquidation unprofitable.

**Check:** Verify `liquidation_bonus - protocol_fee - gas > 0`. Fee must be strictly less than bonus.

---

## DEFI-57 — Unaccounted Yield/PnL in Health Factor

**Description:** Accrued yield not included in health factor causes premature liquidation.

**Pattern:**
```move
// VULNERABLE — ignores accrued yield
public fun health_factor(pos: &Position): u64 {
    pos.deposited_collateral * get_price() * PRECISION / pos.debt
}
// SAFE — include all value components
public fun health_factor(pos: &Position): u64 {
    let value = pos.deposited_collateral * get_price() + calculate_pending_yield(pos);
    value * PRECISION / pos.debt
}
```

**Check:** Health factor must include accrued interest, pending rewards, unrealized PnL.

---

## DEFI-58 — Missing Swap Fees in Liquidation Cost Model

**Description:** Liquidator must swap seized collateral. If swap fees and price impact
aren't accounted for in the bonus, liquidation may be unprofitable.

**Check:** `liquidation_bonus > swap_fee + price_impact + gas`? Higher bonus for illiquid collateral?

---

## DEFI-59 — Oracle Sandwich Self-Liquidation

**Description:** Attacker manipulates oracle to make position appear liquidatable,
self-liquidates to extract bonus, then price normalizes.

**Pattern:**
```move
// SAFE — TWAP oracle + minimum position age
public fun liquidate(position: &mut Position, clock: &Clock) {
    assert!(clock::timestamp_ms(clock) - position.created_at > MIN_POSITION_AGE_MS,
        E_POSITION_TOO_YOUNG);
    let price = get_twap_price(position.collateral_type);
}
```

**Check:** Can user create + liquidate in same tx/epoch? TWAP or spot oracle? Cross-ref: DEFI-01

---

## DEFI-60 — Unbounded Loops in Liquidation Path

**Description:** Liquidation iterates over unbounded collateral list, exceeding gas limit.

**Pattern:**
```move
// VULNERABLE — iterates all collateral types
public fun liquidate(account: &mut Account) {
    let i = 0;
    while (i < vector::length(&account.collaterals)) {
        seize_collateral(vector::borrow_mut(&mut account.collaterals, i));
        i = i + 1;
    };
}
// SAFE — liquidate specific collateral
public fun liquidate(account: &mut Account, idx: u64) {
    assert!(idx < vector::length(&account.collaterals), E_INVALID);
    seize_collateral(vector::borrow_mut(&mut account.collaterals, idx));
}
```

**Check:** No unbounded loops. Limit max collateral types. Cross-ref: APT-10

---

## DEFI-61 — Front-Running Liquidation

**Description:** Position owner front-runs by repaying minimal amount to raise health
factor above threshold. Liquidation tx fails.

**Check:** Anti-front-running mechanism needed (e.g., Dutch auction). On Sui: shared
object ordering may help. On Aptos: mempool ordering matters.

---

## DEFI-62 — Pending Withdrawal Blocking Liquidation

**Description:** Pending withdrawal locks collateral from seizure.

**Pattern:**
```move
// VULNERABLE — pending withdrawal reduces seizable collateral to ~0
public fun liquidate(position: &mut Position) {
    let available = position.collateral - position.pending_withdrawal;
    calculate_seize(available); // nothing to seize
}
// SAFE — cancel pending withdrawals on liquidation
public fun liquidate(position: &mut Position) {
    position.pending_withdrawal = 0;
    calculate_seize(position.collateral);
}
```

**Check:** Liquidation must override all pending operations.

---

## DEFI-63 — Token Denylist/Freeze Blocking Liquidation

**Description:** Regulated coins with denylist (Sui `DenyCapV2`) or freeze (Aptos
`FungibleAsset`) block collateral transfer, preventing liquidation.

**Pattern:**
```move
// VULNERABLE — direct transfer fails if denylisted
transfer::public_transfer(collateral, liquidator);
// SAFE — escrow mechanism
escrow::deposit(escrow, collateral); // liquidator claims from escrow
```

**Check:** Fallback path for blocked transfers needed. Cross-ref: DEFI-29

---

## DEFI-64 — Interest Accumulation During Pause

**Description:** Protocol paused but interest keeps accruing. Users can't repay.
On unpause, healthy positions are instantly liquidated.

**Pattern:**
```move
// VULNERABLE — repayment blocked during pause, interest keeps accruing
public fun repay(pos: &mut Position, payment: Coin<USDC>, state: &State) {
    assert!(!state.paused, E_PAUSED);
}
// SAFE — freeze interest during pause
public fun calculate_debt(pos: &Position, state: &State, clock: &Clock): u64 {
    let elapsed = if (state.paused) {
        state.pause_timestamp - pos.last_update
    } else { clock::timestamp_ms(clock) - pos.last_update };
    pos.principal + calculate_interest(pos.principal, elapsed)
}
```

**Check:** Interest must freeze during pause or grace period after unpause. Cross-ref: DEFI-28

---

## DEFI-65 — Position Unhealthier After Liquidation

**Description:** Partial liquidation makes health factor LOWER because bonus extracts
disproportionate collateral.

**Pattern:**
```move
// VULNERABLE — no health check after
public fun partial_liquidate(pos: &mut Position, repay: u64) {
    let seize = repay + repay * BONUS_BPS / 10000;
    pos.collateral = pos.collateral - seize;
    pos.debt = pos.debt - repay;
}
// SAFE — verify health improves
public fun partial_liquidate(pos: &mut Position, repay: u64) {
    let hf_before = health_factor(pos);
    let seize = repay + repay * BONUS_BPS / 10000;
    pos.collateral = pos.collateral - seize;
    pos.debt = pos.debt - repay;
    assert!(health_factor(pos) > hf_before, E_HEALTH_NOT_IMPROVED);
}
```

**Check:** Health factor must improve after partial liquidation. Cross-ref: DEFI-54

---

## DEFI-66 — No Slippage Protection on Liquidation

**Description:** Liquidator can't specify minimum collateral received.

**Pattern:**
```move
// VULNERABLE — no minimum guarantee
public fun liquidate(pos: &mut Position, repayment: Coin<USDC>): Coin<ETH> {
    calculate_and_seize(pos, coin::value(&repayment))
}
// SAFE — liquidator specifies minimum
public fun liquidate(pos: &mut Position, repayment: Coin<USDC>, min_out: u64): Coin<ETH> {
    let c = calculate_and_seize(pos, coin::value(&repayment));
    assert!(coin::value(&c) >= min_out, E_SLIPPAGE);
    c
}
```

**Check:** Liquidation functions should accept `min_collateral_out`.

---

## DEFI-81 — Liquidation Cash Availability — Missing Pre-Check

**Description:** When a liquidation function redeems collateral ctokens to underlying
tokens in the same transaction, it must verify that the collateral reserve
has sufficient idle cash BEFORE executing the redemption. If the collateral
reserve is at high utilization (most assets borrowed out), the redemption
call will abort because `available_cash < seize_amount`, reverting the entire
liquidation transaction.

This is especially dangerous because:
1. The unhealthy position cannot be liquidated until utilization drops
2. Interest continues to accrue on the underwater position
3. The position grows toward bad debt with no remedy available
4. A malicious borrower can intentionally keep collateral reserve at
   high utilization to prevent their own liquidation

Severity is HIGH because the safety mechanism (liquidation) fails precisely
when it is needed most — at high utilization after aggressive borrowing.
Bad debt accumulates with no protocol recourse.

**Pattern:**
```move
// VULNERABLE — redeems underlying in same tx; reverts if reserve at high utilization
public fun liquidate_ctokens(
    reserve: &mut Reserve,
    position: &mut Position,
    repayment: Coin<USDC>,
    ctx: &mut TxContext,
) {
    let seize_amount = calculate_seize(position, coin::value(&repayment));
    // BUG: if available_cash < seize_amount, balance::split aborts
    let seized = balance::split(&mut reserve.underlying, seize_amount);
    transfer::public_transfer(coin::from_balance(seized, ctx), tx_context::sender(ctx));
}

// SAFE (option A) — pre-check cash availability
public fun liquidate_ctokens(
    reserve: &mut Reserve,
    position: &mut Position,
    repayment: Coin<USDC>,
    ctx: &mut TxContext,
) {
    let seize_amount = calculate_seize(position, coin::value(&repayment));
    assert!(balance::value(&reserve.underlying) >= seize_amount, E_INSUFFICIENT_CASH);
    let seized = balance::split(&mut reserve.underlying, seize_amount);
    transfer::public_transfer(coin::from_balance(seized, ctx), tx_context::sender(ctx));
}

// SAFE (option B) — liquidator receives ctokens directly, redeems separately
public fun liquidate_ctokens(
    position: &mut Position,
    repayment: Coin<USDC>,
    ctx: &mut TxContext,
): Coin<CToken> {
    let seize_amount = calculate_seize(position, coin::value(&repayment));
    // Liquidator gets ctokens; redeems for underlying in a future tx
    split_ctokens(position, seize_amount)
}
```

**Check:**
1. In the liquidation execution path, find where ctokens are converted to underlying
   (`withdraw_underlying_asset`, `balance::split`, `redeem_ctokens`, etc.)
2. Verify ONE of: (a) a pre-check exists: `assert!(available_cash >= seize_amount)`,
   (b) ctoken seizure and underlying redemption are separated — liquidator receives ctokens
   directly and redeems in a future transaction, or (c) the protocol has a bad debt write-off
   path that handles this case
3. If the liquidation redeems underlying in the same TX without (a), (b), or (c) → HIGH

---

## DEFI-83 — Close Factor Cumulative Enforcement Across Atomic Transactions

**Description:** Close factor limits the percentage of debt that can be liquidated in a single event. On chains with atomic multi-call transactions (Sui PTBs, EVM internal transactions), the close factor must be enforced against the debt balance at the START of the transaction, not recalculated after each partial liquidation.

**Pattern to flag:**
```move
// VULNERABLE: close factor recalculated on remaining (reduced) debt
public fun liquidate(market: &mut Market, obligation_id: ID, repay_amount: u64) {
    let debt = market.obligation(obligation_id).debt();
    let max_repay = debt * close_factor;  // current_debt shrinks after each call
    assert!(repay_amount <= max_repay, E_CLOSE_FACTOR_EXCEEDED);
    execute_liquidation(market, obligation_id, repay_amount);
}
```

**Check:**
1. Is the close factor checked against `original_debt_at_start_of_transaction` or `current_debt_after_previous_liquidation_in_same_tx`?
2. After one partial liquidation, does the position remain underwater? (Yes, because liquidation incentive removes more collateral VALUE than debt VALUE)
3. Can a liquidator construct an atomic transaction with N liquidation calls?
4. Calculate: with close_factor CF and N calls, total liquidated = 1 - (1-CF)^N. At CF=50%, N=3 → 87.5%. At CF=50%, N=5 → 96.9%.

**Impact:** Borrower loses significantly more collateral than the close factor intends. In extreme cases, repeated liquidation pushes positions into bad debt (zero collateral, residual debt) that is socialized across all depositors.

**Fix:** Track the obligation's debt at the start of the first liquidation in the transaction. All subsequent close factor checks reference this original snapshot:
```move
if (!obligation.has_liquidation_snapshot()) {
    obligation.set_liquidation_snapshot(current_debt);
}
let max_repay = obligation.liquidation_snapshot() * close_factor - obligation.already_liquidated_this_tx();
```

**References:** SUI-28, Compound V2 close factor design (enforced per-block but blocks are single-tx on most chains).

---

## DEFI-89 — Self-Trade Value Extraction on Unhealthy Margin Accounts

**Description:** When a margin protocol's self-match protection only compares
account IDs (not common ownership), an attacker can trade their unhealthy margin
account against a second account they control. Each round-trip at the worst
price allowed by the oracle guard moves value from the debt-backed account to the
clean account. Once the margin account is drained, liquidation removes all
remaining collateral but cannot fully repay the debt — the shortfall becomes
bad debt absorbed by lenders.

**Pattern:**
```move
// VULNERABLE — self-match check uses account ID only
fun check_self_match(maker_account_id: ID, taker_account_id: ID) {
    assert!(maker_account_id != taker_account_id, E_SELF_MATCH);
    // same owner with two accounts bypasses this
}

// Attacker flow:
// 1. Account A (margin): borrow at max leverage, risk_ratio ~1.25
// 2. Account B (normal): place maker bid at lower oracle bound
// 3. Account A sells into B at worst allowed price — no health check (DEFI-88)
// 4. Repeat until A is liquidatable
// 5. Liquidate A: out_amount = repay * (1 + bonus)
//    If collateral < debt * (1 + bonus), partial repay → residual bad debt
```

**Check:**
1. Identify what self-match protection compares — account ID only, or also owner address?
2. If account-ID-only, verify whether a margin account and a normal account owned by the
   same address can cross orders
3. Check if liquidation forces full debt repayment when all collateral is consumed —
   if not, calculate the bad debt: `1 - risk_ratio / (1 + liquidation_bonus)`
4. Cross-ref: DEFI-53 (bad debt handling), DEFI-88 (missing post-trade health check)

---

## Liquidation Economics Validation

**Before reporting ANY liquidation finding, answer these questions:**

1. **If your "fix" were applied, would liquidation still be profitable for the liquidator?**
   Calculate: `liquidator_revenue - liquidator_cost` at current market (spot) prices.
   If your fix makes liquidation unprofitable → the fix causes bad debt → your fix is
   WORSE than the "bug."

2. **Remember: seized collateral is worth its market (spot) price, not its lagging average.**
   Using spot for seize reflects reality — the liquidator sells at spot. Using EMA/TWAP
   for seize would underpay liquidators. Cross-ref: `defi-lending-design-patterns.md` DESIGN-L1.

3. **Does the finding change who benefits, or just how much?**
   A liquidation that over-seizes by 0.1% is Low severity. A liquidation that can be blocked
   entirely is High. Scale severity to actual economic impact.

4. **Can the "victim" of the liquidation mechanism avoid the situation?**
   If a borrower can maintain health factor by adding collateral or repaying, the liquidation
   mechanism working as designed is not a finding — even if the math slightly favors the
   liquidator. That's the intended incentive.

---

## Liquidation Verification Checklist

- [ ] Liquidation bonus exists and exceeds gas costs (DEFI-50)
- [ ] Minimum position size enforced (DEFI-51)
- [ ] Collateral withdrawal uses borrow threshold, not liquidation threshold (DEFI-52)
- [ ] Bad debt handling mechanism exists (DEFI-53)
- [ ] Partial liquidation improves health factor (DEFI-54)
- [ ] Bonus arithmetic uses correct decimal scaling (DEFI-55)
- [ ] Protocol fees don't make liquidation unprofitable (DEFI-56)
- [ ] Health factor includes all accrued yield/PnL (DEFI-57)
- [ ] No unbounded loops in liquidation path (DEFI-60)
- [ ] Token denylist/freeze can't block liquidation (DEFI-63)
- [ ] Interest frozen or grace period during/after pause (DEFI-64)
- [ ] Liquidator can specify minimum collateral received (DEFI-66)
- [ ] Liquidation path checks idle cash availability before redeeming underlying (DEFI-81)
- [ ] Close factor enforced per-TRANSACTION (cumulative), not per-call — PTB repeated call bypass (DEFI-83)
- [ ] Self-match protection prevents same-owner cross between margin and normal accounts (DEFI-89)

## defi/defi-math-precision.md

# DeFi Math & Precision — Move

Deep-dive patterns for arithmetic and precision vulnerabilities in Move DeFi protocols.
Move uses `u64` (max ~1.8e19) and `u128` (max ~3.4e38) — significantly smaller than
Solidity's `uint256` (max ~1.15e77), making precision issues more severe.

---

## DEFI-35 — Division Before Multiplication (DeFi Deep-Dive)

**Description:** Performing division before multiplication in financial calculations
causes precision loss that compounds across operations. In DeFi, even tiny per-operation
losses accumulate into significant fund leakage.

**Pattern:**
```move
// VULNERABLE — division before multiplication loses precision
public fun calculate_fee(amount: u64, fee_rate: u64, precision: u64): u64 {
    // If amount=1000, fee_rate=3, precision=10000:
    // (1000 / 10000) * 3 = 0 * 3 = 0 — fee completely lost
    (amount / precision) * fee_rate
}

// SAFE — multiply first, divide last
public fun calculate_fee(amount: u64, fee_rate: u64, precision: u64): u64 {
    // (1000 * 3) / 10000 = 3000 / 10000 = 0 — still rounds but less loss
    // Use u128 intermediate: (1000 * 3) / 10000 = 0 in u64 but tracks correctly at scale
    ((amount as u128) * (fee_rate as u128) / (precision as u128) as u64)
}
```

**Check:**
1. Search for any `/` operator appearing before `*` in the same expression
2. Grep: `/ .* \*` in financial calculation functions
3. Verify u128 intermediates are used for fee, share, and interest calculations
4. Cross-ref: common-move.md 2.2

---

## DEFI-36 — Rounding to Zero on Small Amounts

**Description:** Small token amounts produce zero results in reward, fee, or share
calculations. Attackers exploit this to transact for free (zero fees) or grief other
users (zero rewards distributed).

**Pattern:**
```move
// VULNERABLE — small reward rounds to zero, lost forever
public fun calculate_reward(user_stake: u64, reward_per_share: u64, precision: u64): u64 {
    // If user_stake=100, reward_per_share=5, precision=1_000_000:
    // (100 * 5) / 1_000_000 = 500 / 1_000_000 = 0
    (user_stake * reward_per_share) / precision
}

// SAFE — enforce minimum amounts, use higher precision
public fun calculate_reward(user_stake: u64, reward_per_share: u128, precision: u128): u64 {
    let reward = ((user_stake as u128) * reward_per_share) / precision;
    // Accumulate dust in a remainder tracker instead of losing it
    (reward as u64)
}
// Also enforce: assert!(user_stake >= MIN_STAKE, E_STAKE_TOO_SMALL);
```

**Check:**
1. Identify all divisions that could produce zero for realistic input ranges
2. Verify minimum amount requirements exist for deposits, stakes, and borrows
3. Check if dust/remainder is tracked or silently dropped
4. Cross-ref: DEFI-13, DEFI-32

---

## DEFI-37 — Decimal Mismatch Between Token Types

**Description:** Move tokens have configurable decimals stored in metadata. Mixing tokens
with different decimals (USDC=6, BTC=8, SUI=9, APT=8) without conversion causes
magnitude errors in value calculations.

**Pattern:**
```move
// VULNERABLE — assumes both tokens have same decimals
public fun calculate_value(amount_a: u64, price_a_in_b: u64): u64 {
    // If A has 8 decimals and B has 6 decimals:
    // 1.0 A (100_000_000) * price 50000 = 5_000_000_000_000 — wrong scale for 6-decimal token
    amount_a * price_a_in_b
}

// SAFE — normalize decimals explicitly
public fun calculate_value(
    amount_a: u64, price_a_in_b: u64,
    decimals_a: u8, decimals_b: u8, price_decimals: u8
): u64 {
    let value = (amount_a as u128) * (price_a_in_b as u128);
    let scale_adjustment = (decimals_a as u32) + (price_decimals as u32) - (decimals_b as u32);
    (value / (math::pow(10, scale_adjustment) as u128) as u64)
}
```

**Check:**
1. Identify all cross-token calculations (value, collateral ratio, swap amounts)
2. Grep: `coin::decimals`, `CoinMetadata`, `decimals` — verify these are used in math
3. Check that decimal normalization happens before comparison, not after
4. On Sui: `coin::get_decimals<T>(metadata)` — on Aptos: `coin::decimals<T>()`

---

## DEFI-38 — Unsafe Downcasting u128 to u64 in Financial Math

**Description:** Intermediate calculations use u128 for precision, then cast back to u64.
If the intermediate result exceeds `u64::MAX` (~1.8e19), the cast silently truncates
or aborts, causing incorrect financial outcomes.

**Pattern:**
```move
// VULNERABLE — u128 intermediate overflows u64 on cast
public fun calculate_shares(deposit: u64, total_supply: u64, total_assets: u64): u64 {
    let shares_u128 = (deposit as u128) * (total_supply as u128) / (total_assets as u128);
    // If total_supply is large and total_assets is small, shares_u128 > u64::MAX
    // Cast aborts in Move — DoS on all deposits
    (shares_u128 as u64)
}

// SAFE — validate before casting
public fun calculate_shares(deposit: u64, total_supply: u64, total_assets: u64): u64 {
    let shares_u128 = (deposit as u128) * (total_supply as u128) / (total_assets as u128);
    assert!(shares_u128 <= (U64_MAX as u128), E_OVERFLOW);
    (shares_u128 as u64)
}
```

**Check:**
1. Search all `as u64` casts from u128 — each is a potential truncation
2. Verify the mathematical maximum of each intermediate cannot exceed u64::MAX
3. Check if overflow causes abort (DoS) vs silent truncation (fund loss)
4. Cross-ref: common-move.md 2.4

---

## DEFI-39 — Wrong Rounding Direction

**Description:** Move integer division always truncates toward zero. In DeFi, rounding
must be protocol-favoring: round fees UP (protocol receives more), round withdrawals
DOWN (user receives less). Wrong direction leaks value from the protocol.

**Pattern:**
```move
// VULNERABLE — rounds withdrawal UP, favoring user over protocol
public fun calculate_withdrawal(shares: u64, total_assets: u64, total_shares: u64): u64 {
    // Truncation rounds down by default — correct for withdrawals
    // But if someone adds +1: (shares * total_assets + total_shares - 1) / total_shares
    // This rounds UP — user gets more than their share
    (shares * total_assets + total_shares - 1) / total_shares
}

// SAFE — round DOWN for withdrawals (protocol keeps the dust)
public fun calculate_withdrawal(shares: u64, total_assets: u64, total_shares: u64): u64 {
    (shares * total_assets) / total_shares  // natural truncation = round down
}

// SAFE — round UP for fees (protocol charges the dust)
public fun calculate_fee(amount: u64, fee_bps: u64): u64 {
    (amount * fee_bps + 9999) / 10000  // ceil division for fees
}
```

**Check:**
1. For every division: does rounding favor the protocol or the user?
2. Withdrawals/redemptions: must round DOWN (truncate)
3. Fees/interest/debt: must round UP (ceil)
4. Deposits/minting: must round DOWN (user gets fewer shares)

---

## DEFI-40 — Inverted Oracle Price Pairs

**Description:** Using price of A-in-B where B-in-A was needed. Results in inverted
calculations — if BTC/USD = 50000, accidentally using USD/BTC = 0.00002 produces
values off by a factor of 2.5 billion.

**Pattern:**
```move
// VULNERABLE — uses TOKEN/USD price to convert USD to TOKEN (inverted)
public fun usd_to_token(usd_amount: u64, token_usd_price: u64, precision: u64): u64 {
    // This calculates: usd_amount * token_usd_price — WRONG
    // Should divide by price to convert USD → TOKEN
    usd_amount * token_usd_price / precision
}

// SAFE — correct direction: divide by price to convert USD → TOKEN
public fun usd_to_token(usd_amount: u64, token_usd_price: u64, precision: u64): u64 {
    usd_amount * precision / token_usd_price
}
```

**Check:**
1. At every oracle integration: document whether price is `A/B` or `B/A`
2. Verify the math direction matches: multiply by price to go `A → B`, divide for `B → A`
3. Cross-ref: DEFI-23

---

## DEFI-41 — Time Unit Confusion (Sui ms vs Aptos seconds)

**Description:** Sui's `clock::timestamp_ms()` returns milliseconds, while Aptos's
`timestamp::now_seconds()` returns seconds. Mixing units in interest calculations,
lockup durations, or staleness checks causes 1000x errors.

**Pattern:**
```move
// VULNERABLE — Sui: using milliseconds as if they were seconds
public fun calculate_interest(principal: u64, rate_per_second: u64, last_update: u64, clock: &Clock): u64 {
    let elapsed = clock::timestamp_ms(clock) - last_update; // returns milliseconds!
    // elapsed = 60000 (1 minute in ms), but treated as 60000 seconds (16.6 hours)
    // Interest is 1000x too high
    principal * rate_per_second * elapsed / PRECISION
}

// SAFE — convert Sui ms to seconds explicitly
public fun calculate_interest(principal: u64, rate_per_second: u64, last_update_ms: u64, clock: &Clock): u64 {
    let elapsed_ms = clock::timestamp_ms(clock) - last_update_ms;
    let elapsed_seconds = elapsed_ms / 1000;
    ((principal as u128) * (rate_per_second as u128) * (elapsed_seconds as u128) / (PRECISION as u128) as u64)
}
```

**Check:**
1. On Sui: verify every `clock::timestamp_ms()` usage converts to correct unit
2. On Aptos: verify `timestamp::now_seconds()` — some Aptos code also has `now_microseconds()`
3. Check lockup/cooldown durations: `3600` could mean 3600ms (3.6s) or 3600s (1hr)
4. Cross-ref: SUI-16, common-move.md 8.4

---

## DEFI-42 — Exponentiation Precision Loss (Compound Interest)

**Description:** Compound interest via repeated multiplication loses precision at each
step. `(1 + rate)^n` computed iteratively truncates at every multiplication, causing
significant divergence from the true value over time.

**Pattern:**
```move
// VULNERABLE — iterative compounding truncates at each step
public fun compound(principal: u64, rate_bps: u64, periods: u64): u64 {
    let result = principal;
    let i = 0;
    while (i < periods) {
        result = result + (result * rate_bps / 10000); // truncation each iteration
        i = i + 1;
    };
    result
}

// SAFE — use binary exponentiation with u128 precision
public fun compound(principal: u64, rate_bps: u64, periods: u64): u64 {
    // Binary exponentiation: O(log n) multiplications, u128 intermediate
    let base = (10000 + rate_bps as u128); // 1 + rate in BPS
    let precision = 10000u128;
    let result = precision; // starts at 1.0
    let exp = periods;
    let b = base;
    while (exp > 0) {
        if (exp % 2 == 1) {
            result = result * b / precision;
        };
        b = b * b / precision;
        exp = exp / 2;
    };
    ((principal as u128) * result / precision as u64)
}
```

**Check:**
1. Search for loops with multiplication inside (`while` + `*`) — potential iterative compounding
2. Verify compound interest uses binary exponentiation or lookup tables
3. Check that u128 intermediates are used throughout the exponentiation
4. For large period counts (e.g., per-second compounding over years), verify no overflow

---

## DEFI-85 — Multiply-Before-Divide Overflow in Fixed-Point Helpers

**Description:** Fixed-point math libraries (WAD/RAY/Decimal wrappers) store scaled values and
enforce bounds internally. When `A.mul(B)` computes `(A.value * B.value) / WAD`, the
intermediate product can exceed the library's `VALUE_MAX` and abort — even though the
final result after `.div(C)` would be small. This is a hidden overflow: the calling code
looks correct (`from(x).mul(from(y)).div(from(z))`) but the helper aborts before the
division is reached.

**This bug class is HIGH/CRITICAL when:**
1. The overflowing function is called by every user-facing operation (deposit, withdraw, borrow, repay, liquidate, claim)
2. The overflow occurs before a state checkpoint (`last_update_time`, `cumulative_index`)
3. The time delta grows after each failed attempt, making recovery impossible
4. Admin recovery paths (cancel, close) also trigger the same update

**Mandatory analysis steps:**

**Step 1 — Derive helper bounds.** Open the fixed-point module. For `mul(a, b)`:
```
Internal: result = (a.value * b.value) / WAD
Bound check: result <= VALUE_MAX
Simplifies to: a.value * b.value <= VALUE_MAX * WAD
If VALUE_MAX = U64_MAX and both a, b are from(u64):
  a.value = input_a * WAD, b.value = input_b * WAD
  intermediate = input_a * WAD * input_b * WAD / WAD = input_a * input_b * WAD
  bound: input_a * input_b * WAD <= U64_MAX * WAD
  simplifies to: input_a * input_b <= U64_MAX
```

**Step 2 — Derive overflow threshold.** For the specific call site:
```
Example: float::from(total_rewards).mul(float::from(time_passed_ms))
Overflow when: total_rewards * time_passed_ms > U64_MAX (~1.844e19)

Token: USDC (6 decimals) → 500,000 USDC = 5e11 atomic units
Overflow at: time_passed_ms = U64_MAX / 5e11 = 3.69e7 ms ≈ 10.25 hours

Token: SUI (9 decimals) → 1,000 SUI = 1e12 atomic units
Overflow at: time_passed_ms = U64_MAX / 1e12 = 1.844e7 ms ≈ 5.12 hours
```

**Step 3 — Compute threshold table for realistic reward amounts:**

| Reward Amount | Token Decimals | Atomic Units | Max Inactivity Before Overflow |
|--------------|----------------|-------------|-------------------------------|
| 10,000 USDC | 6 | 1e10 | ~21.3 days |
| 100,000 USDC | 6 | 1e11 | ~2.13 days |
| 500,000 USDC | 6 | 5e11 | ~10.25 hours |
| 1,000,000 USDC | 6 | 1e12 | ~5.12 hours |
| 10,000,000 USDC | 6 | 1e13 | ~30.7 minutes |
| 1,000 SUI | 9 | 1e12 | ~5.12 hours |

**Pattern:**
```move
// VULNERABLE — mul overflows before div can normalize
let unlocked_rewards =
    float::from(pool_reward.total_rewards)
        .mul(float::from(time_passed_ms))        // aborts when product > U64_MAX
        .div(float::from(pool_reward.end_time_ms - pool_reward.start_time_ms));

// SAFE — div first, then mul (intermediate stays within bounds)
let unlocked_rewards =
    float::from(pool_reward.total_rewards)
        .div(float::from(pool_reward.end_time_ms - pool_reward.start_time_ms))
        .mul(float::from(time_passed_ms));
// (total_rewards / duration) is always <= total_rewards, so the subsequent mul
// can only overflow if time_passed > duration, which is bounded by the reward period.
```

**Check:**
1. Open EVERY fixed-point helper used by the protocol. Read `mul`, `div`, `from`. Derive the internal overflow bound.
2. For every call of the form `from(A).mul(from(B)).div(from(C))` — prove `A * B <= VALUE_MAX` OR flag it
3. Compute a threshold table using the protocol's actual token decimals and realistic amounts
4. If overflow is reachable, apply the Recoverability Matrix (common-move.md 12.1)
5. Cross-ref: common-move.md 2.6, DEFI-86

---

## DEFI-86 — Accumulator Checkpoint Liveness (Abort-Before-State-Advance)

**Description:** A periodic accumulator update function (reward index, interest accrual,
fee distribution) performs arithmetic that can abort, and the state checkpoint
(`last_update_time`, `cumulative_index`, `reward_per_share`) is written AFTER the
potentially-aborting line. Once the abort fires, the checkpoint stays stale, causing the
time delta to grow on every retry until the function becomes permanently uncallable.

**Why it matters for DeFi:** Accumulator updates are called by nearly every user-facing
operation — deposit, withdraw, borrow, repay, liquidate, claim. A stuck accumulator
freezes the entire pool.

**Pattern:**
```move
// VULNERABLE — checkpoint after abort-prone line
public fun update_pool_reward(pool: &mut Pool, clock: &Clock) {
    let now = clock::timestamp_ms(clock);
    let elapsed = now - pool.last_update_time_ms;              // grows if update fails

    let new_rewards = compute_rewards(pool.total_rewards, elapsed);  // CAN ABORT (overflow)

    pool.accumulated_rewards = pool.accumulated_rewards + new_rewards;
    pool.last_update_time_ms = now;  // <-- NEVER REACHED if compute_rewards aborts
}

// SAFE — use safe arithmetic that cannot abort, OR reorder to divide-first
public fun update_pool_reward(pool: &mut Pool, clock: &Clock) {
    let now = clock::timestamp_ms(clock);
    let elapsed = now - pool.last_update_time_ms;

    // Option A: reorder to prevent overflow (see DEFI-85)
    let rate = float::from(pool.total_rewards).div(float::from(pool.duration));
    let new_rewards = rate.mul(float::from(elapsed));

    pool.accumulated_rewards = pool.accumulated_rewards + new_rewards;
    pool.last_update_time_ms = now;
}
```

**Check:**
1. For every function that updates a cumulative accumulator or timestamp checkpoint:
   - Is there ANY arithmetic between reading `now` and writing the checkpoint?
   - Can that arithmetic abort (overflow, divide-by-zero, assertion)?
   - If it aborts, does the checkpoint remain stale?
2. If yes: trace ALL entry points that call this update:
   - User actions: deposit, withdraw, borrow, repay
   - Liquidation: liquidate, seize, ADL
   - Claims: claim_rewards, harvest
   - Admin: cancel_reward, close_pool, update_config
3. If ALL paths go through the stuck update → **permanent deadlock** → HIGH/CRITICAL
4. If some paths bypass the update → conditional deadlock → lower severity
5. Compute the time-to-overflow threshold (see DEFI-85 threshold table)
6. Cross-ref: common-move.md 12.1, DEFI-85

---

## DEFI-87 — Reward Manager Overflow Auto-Detection

**Trigger:** Any codebase containing reward distribution, liquidity mining, or incentive mechanisms with time-based unlocking.

**Pattern:** A reward manager computes unlocked rewards as `(total_rewards * time_elapsed) / duration` using a fixed-point helper. The multiply-before-divide order inside the helper causes overflow when `total_rewards * time_elapsed > VALUE_MAX`. If this computation runs inside a periodic update that gates ALL pool operations, overflow = permanent pool freeze.

**Mandatory grep patterns — run ALL of these:**
```
total_rewards.*mul.*time
time_passed.*mul.*total
unlocked.*from.*mul.*from.*div
reward.*\.mul\(.*time
update_pool_reward
update_reward_manager
update_obligation_reward
liquidity_mining.*update
```

**For every match, execute this 5-step trace:**

1. **Read the helper:** Open the fixed-point module used (e.g., `float.move`, `decimal.move`). Read `mul()`. Derive: what is the max product before abort?
2. **Compute overflow threshold:**
   ```
   For mul(from(A), from(B)) where helper bound is A * B <= U64_MAX:

   | Token     | Decimals | Reward Amount    | Atomic Units | Max time_passed before overflow |
   |-----------|----------|------------------|--------------|-------------------------------|
   | USDC      | 6        | 10,000           | 1e10         | ~21.3 days                    |
   | USDC      | 6        | 100,000          | 1e11         | ~2.13 days                    |
   | USDC      | 6        | 500,000          | 5e11         | ~10.25 hours                  |
   | USDC      | 6        | 1,000,000        | 1e12         | ~5.12 hours                   |
   | SUI       | 9        | 1,000            | 1e12         | ~5.12 hours                   |
   | SUI       | 9        | 10,000           | 1e13         | ~30.7 minutes                 |
   ```
   If ANY realistic reward configuration overflows within 30 days of inactivity → flag.

3. **Check checkpoint ordering:** Is `last_update_time_ms` (or equivalent) written AFTER the overflowing line? If yes → permanent deadlock.

4. **Trace all callers:** Does EVERY user-facing operation (deposit, withdraw, borrow, repay, liquidate, claim) call this update? Does EVERY admin recovery path (cancel_reward, close_pool) also call this update? If ALL paths trapped → no recovery → HIGH/CRITICAL.

5. **Check for admin-origin:** Is the overflow triggered by a routine admin action (adding rewards)? If the admin action is expected/routine but users are the victims → do NOT dismiss as "admin-only" (see 12.2).

**Safe patterns (do NOT flag):**
```move
// SAFE — divide first, then multiply
float::from(total_rewards).div(float::from(duration)).mul(float::from(time_passed))

// SAFE — cap time_passed to remaining duration
let time_passed = math::min(time_passed, end_time - last_update_time);

// SAFE — u256 intermediate with sufficient headroom
let unlocked = ((total_rewards as u256) * (time_passed as u256)) / (duration as u256);
```

---

## Math / Precision Verification Checklist

- [ ] All financial calculations multiply before dividing (DEFI-35)
- [ ] Minimum amounts enforced to prevent rounding-to-zero exploitation (DEFI-36)
- [ ] Cross-token calculations normalize decimals before arithmetic (DEFI-37)
- [ ] All u128→u64 casts validated against overflow (DEFI-38)
- [ ] Rounding direction favors protocol: fees round UP, withdrawals round DOWN (DEFI-39)
- [ ] Oracle price direction (A/B vs B/A) documented and verified at each use (DEFI-40)
- [ ] Time units consistent: Sui ms converted, Aptos seconds verified (DEFI-41)
- [ ] Compound interest uses binary exponentiation with u128 precision (DEFI-42)
- [ ] Fixed-point helper `mul` intermediate product cannot overflow before normalizing division (DEFI-85)
- [ ] Every accumulator checkpoint is written BEFORE or ATOMICALLY WITH potentially-aborting arithmetic (DEFI-86)
- [ ] Overflow thresholds computed with production token decimals and realistic amounts for all `from(A).mul(from(B))` calls (DEFI-85)
- [ ] Reward manager overflow: grep patterns run, threshold table computed, checkpoint ordering checked, all callers traced (DEFI-87)

## defi/defi-oracle.md

# DeFi Oracle Vulnerability Patterns (DEFI-17 to DEFI-24)

Oracle integrations are a critical attack surface in Move DeFi protocols.
Pyth and Switchboard are the primary providers on Sui and Aptos.

---

## DEFI-17 — Stale Price Data

**Description:** Oracle price consumed without checking `publish_time` (Pyth) or
`latest_round_timestamp` (Switchboard) against a max staleness threshold, enabling
arbitrage against outdated valuations.

**Pattern:**
```move
// VULNERABLE — no staleness check, price could be hours old
public fun get_token_price(price_info: &PriceInfoObject): u64 {
    (pyth::price::get_price(&pyth::price_info::get_price(price_info)) as u64)
}

// SAFE — enforce maximum staleness
const MAX_STALE_SECONDS: u64 = 60;
const E_STALE_PRICE: u64 = 1001;

public fun get_token_price_safe(price_info: &PriceInfoObject, clock: &Clock): u64 {
    let price = pyth::price_info::get_price(price_info);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < MAX_STALE_SECONDS, E_STALE_PRICE);
    (pyth::price::get_price(&price) as u64)
}
```

**Check:**
1. Every `get_price` call must have a `publish_time` / `timestamp` comparison nearby
2. Grep: `get_price` without a nearby `publish_time` or `timestamp` assertion
3. Cross-ref: DEFI-01

---

## DEFI-18 — Same Staleness Threshold for Different Feeds

**Description:** One `MAX_STALE` constant for all feeds. Volatile assets (BTC, ETH)
need tight windows (30-60 s), stablecoins need wider ones (3600 s). A universal
threshold is either too loose or too tight.

**Pattern:**
```move
// VULNERABLE — one constant for all feeds (too loose for BTC, maybe fine for USDC)
const MAX_STALE: u64 = 3600;

public fun check_price(price_info: &PriceInfoObject, clock: &Clock): u64 {
    let price = pyth::price_info::get_price(price_info);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < MAX_STALE, E_STALE_PRICE);
    (pyth::price::get_price(&price) as u64)
}

// SAFE — per-feed staleness via Table<ID, u64>
struct OracleConfig has key, store { id: UID, staleness: Table<ID, u64> }

public fun check_price_safe(
    config: &OracleConfig, feed_id: ID, price_info: &PriceInfoObject, clock: &Clock,
): u64 {
    let price = pyth::price_info::get_price(price_info);
    let max_stale = *table::borrow(&config.staleness, feed_id);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < max_stale, E_STALE_PRICE);
    (pyth::price::get_price(&price) as u64)
}
```

**Check:**
1. Look for a single `MAX_STALE` / `MAX_STALENESS` constant used across multiple feed reads
2. Grep: `const MAX_STALE` or `const STALENESS` — check if one constant or a per-feed config
3. Cross-ref: DEFI-17

---

## DEFI-19 — Oracle Decimal/Exponent Mismatch

**Description:** Pyth returns price as `i64` with `i32` exponent (price=12345,
expo=-2 = $123.45). Ignoring the exponent causes 10^N magnitude errors.

**Pattern:**
```move
// VULNERABLE — raw price without applying exponent
public fun value_in_usd(amount: u64, price_info: &PriceInfoObject): u64 {
    let price = pyth::price_info::get_price(price_info);
    // price=2950000, expo=-5 => real=$29.50, but treats 2950000 as dollar price
    amount * (pyth::price::get_price(&price) as u64)
}

// SAFE — normalize using exponent: result = amount * price * 10^(target_decimals + expo)
const TARGET_DECIMALS: u8 = 8;

public fun value_in_usd_safe(amount: u64, amt_dec: u8, price_info: &PriceInfoObject): u64 {
    let price = pyth::price_info::get_price(price_info);
    let raw = pyth::price::get_price(&price);
    assert!(raw > 0, E_NEGATIVE_PRICE);
    let expo = pyth::price::get_expo(&price); // e.g., -5
    let adj = (TARGET_DECIMALS as i32) - (amt_dec as i32) + expo;
    if (adj >= 0) { amount * (raw as u64) * math::pow(10, (adj as u8)) }
    else { amount * (raw as u64) / math::pow(10, ((-adj) as u8)) }
}
```

**Check:**
1. Every `get_price()` usage must have a corresponding `get_expo()` call
2. Grep: `get_price` without `get_expo` in the same function
3. Cross-ref: common-move.md 8.4

---

## DEFI-20 — Wrong Price Feed ID

**Description:** Pyth feed IDs are chain-specific (Sui: `PriceInfoObject` ID,
Aptos: 32-byte address). Testnet IDs differ from mainnet. Wrong feed ID prices
assets with entirely incorrect data.

**Pattern:**
```move
// VULNERABLE (Sui) — no verification of feed identity
public fun get_btc_price(price_info: &PriceInfoObject): u64 {
    // Caller can pass ANY PriceInfoObject — could be ETH/USD, not BTC/USD
    let price = pyth::price_info::get_price(price_info);
    (pyth::price::get_price(&price) as u64)
}

// VULNERABLE (Aptos) — hardcoded feed without validation
const BTC_FEED: vector<u8> = x"aabbccdd"; // could be testnet-only

public fun get_btc_price_aptos(): u64 {
    let price = pyth::get_price(BTC_FEED, timestamp::now_seconds());
    (pyth::price::get_price(&price) as u64)
}

// SAFE — registry validates feed identity at runtime
struct FeedRegistry has key, store { id: UID, feeds: Table<String, ID> }
const E_WRONG_FEED: u64 = 3001;

public fun get_price_checked(
    reg: &FeedRegistry, asset: String, price_info: &PriceInfoObject, clock: &Clock,
): u64 {
    assert!(object::id(price_info) == *table::borrow(&reg.feeds, asset), E_WRONG_FEED);
    let price = pyth::price_info::get_price(price_info);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < 60, E_STALE_PRICE);
    (pyth::price::get_price(&price) as u64)
}
```

**Check:**
1. Look for hardcoded hex addresses or object IDs used as price feed identifiers
2. Grep: `const.*FEED` or `@0x` near oracle code — check testnet vs mainnet configs
3. Cross-ref: DEFI-17, SUI-01 / APT-01

---

## DEFI-21 — Depeg Events Not Handled

**Description:** Protocol assumes wrapped/pegged asset equals underlying (wBTC=BTC,
USDC=$1). Depeg breaks this, causing incorrect valuations and exploitable arbitrage.

**Pattern:**
```move
// VULNERABLE — uses BTC/USD price for wBTC, ignores depeg
public fun wbtc_collateral_value(wbtc_amount: u64, btc_usd_info: &PriceInfoObject): u64 {
    let price = pyth::price_info::get_price(btc_usd_info);
    wbtc_amount * (pyth::price::get_price(&price) as u64) // assumes wBTC == BTC
}

// SAFE — use dedicated wBTC/USD feed + depeg circuit breaker
const MAX_DEPEG_BPS: u64 = 200; const BPS_BASE: u64 = 10000;
const E_DEPEG_DETECTED: u64 = 4001;

public fun wbtc_collateral_value_safe(
    wbtc_amount: u64, wbtc_usd_info: &PriceInfoObject, btc_usd_info: &PriceInfoObject,
): u64 {
    let wbtc_usd = (pyth::price::get_price(&pyth::price_info::get_price(wbtc_usd_info)) as u64);
    let btc_usd = (pyth::price::get_price(&pyth::price_info::get_price(btc_usd_info)) as u64);
    let ratio_bps = wbtc_usd * BPS_BASE / btc_usd;
    let dev = if (ratio_bps > BPS_BASE) { ratio_bps - BPS_BASE } else { BPS_BASE - ratio_bps };
    assert!(dev <= MAX_DEPEG_BPS, E_DEPEG_DETECTED);
    wbtc_amount * wbtc_usd
}
```

**Check:**
1. Look for wrapped/pegged tokens valued using the underlying token's oracle feed
2. Grep: `wbtc.*btc_price` or `steth.*eth_price` — wrapped asset using unwrapped feed
3. Cross-ref: DEFI-22, DEFI-01

---

## DEFI-22 — Oracle Min/Max Price Bounds

**Description:** Oracle returns extreme values (0, negative, MAX_U64) during outages.
Zero prices enable infinite borrowing; extreme prices trigger mass liquidations.

**Pattern:**
```move
// VULNERABLE — no bounds check; price could be 0 (div-by-zero) or MAX_U64 (overflow)
public fun get_collateral_ratio(debt: u64, collateral: u64, price_info: &PriceInfoObject): u64 {
    let val = (pyth::price::get_price(&pyth::price_info::get_price(price_info)) as u64);
    collateral * val / debt
}

// SAFE — enforce min/max bounds after oracle read
const MIN_PRICE: u64 = 1; const MAX_PRICE: u64 = 1_000_000_000_000;
const E_PRICE_OUT_OF_BOUNDS: u64 = 5001;

public fun get_price_bounded(price_info: &PriceInfoObject, clock: &Clock): u64 {
    let price = pyth::price_info::get_price(price_info);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < 60, E_STALE_PRICE);
    let raw = pyth::price::get_price(&price);
    assert!(raw > 0, E_NEGATIVE_PRICE);
    let val = (raw as u64);
    assert!(val >= MIN_PRICE && val <= MAX_PRICE, E_PRICE_OUT_OF_BOUNDS);
    val
}
```

**Check:**
1. Look for oracle reads flowing directly into arithmetic with no `assert!` on bounds
2. Grep: `get_price` followed by `*` or `/` without an intermediate bounds check
3. Cross-ref: DEFI-17, common-move.md 8.1 (overflow/underflow)

---

## DEFI-23 — Price Direction Confusion

**Description:** Using TOKEN_A/TOKEN_B price where TOKEN_B/TOKEN_A was needed
(e.g., ETH/USD=3000 used as USD/ETH). Calculations off by price squared.

**Pattern:**
```move
// VULNERABLE — inverted price direction
public fun eth_needed_for_usd(usd_amount: u64, eth_usd_info: &PriceInfoObject): u64 {
    let eth_per_usd = (pyth::price::get_price(&pyth::price_info::get_price(eth_usd_info)) as u64);
    // BUG: oracle returns usd_per_eth (3000), not eth_per_usd
    usd_amount * eth_per_usd // returns usd * 3000 instead of usd / 3000
}

// SAFE — ETH/USD = USD per 1 ETH. USD->ETH: divide. ETH->USD: multiply.
const PRECISION: u64 = 100_000_000;

public fun eth_needed_for_usd_safe(
    usd_amount: u64, eth_usd_price_info: &PriceInfoObject, clock: &Clock,
): u64 {
    let price = pyth::price_info::get_price(eth_usd_price_info);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < 60, E_STALE_PRICE);
    let usd_per_eth = (pyth::price::get_price(&price) as u64);
    assert!(usd_per_eth > 0, E_NEGATIVE_PRICE);
    usd_amount * PRECISION / usd_per_eth // divide to go USD -> ETH
}

/// Cross-rate: BTC in ETH = BTC_USD / ETH_USD
public fun cross_rate(btc_info: &PriceInfoObject, eth_info: &PriceInfoObject): u64 {
    let btc_usd = (pyth::price::get_price(&pyth::price_info::get_price(btc_info)) as u64);
    let eth_usd = (pyth::price::get_price(&pyth::price_info::get_price(eth_info)) as u64);
    btc_usd * PRECISION / eth_usd
}
```

**Check:**
1. Look for oracle prices used in multiplication where division was needed, or vice versa
2. Grep: variable names containing `per` — verify direction matches oracle feed definition
3. Cross-ref: DEFI-19, common-move.md 8.4

---

## DEFI-24 — Missing Circuit Breakers

**Description:** No deviation check between consecutive oracle updates. A sudden
50x spike triggers mass liquidations or unbounded borrowing immediately.

**Pattern:**
```move
// VULNERABLE — blindly accepts any price, even 100x jumps
struct PriceState has key, store { id: UID, current_price: u64 }

public fun update_price(state: &mut PriceState, price_info: &PriceInfoObject) {
    state.current_price = (pyth::price::get_price(
        &pyth::price_info::get_price(price_info)) as u64); // no deviation check
}

// SAFE — circuit breaker pauses on abnormal deviation
const MAX_DEV_BPS: u64 = 1500; const BPS: u64 = 10000; // 15% max
const E_CIRCUIT_BREAKER: u64 = 6001;

struct PriceState has key, store { id: UID, current_price: u64, is_paused: bool }

public fun update_price_safe(state: &mut PriceState, price_info: &PriceInfoObject, clock: &Clock) {
    assert!(!state.is_paused, E_CIRCUIT_BREAKER);
    let price = pyth::price_info::get_price(price_info);
    assert!(clock::timestamp_ms(clock) / 1000 - pyth::price::get_publish_time(&price) < 60, E_STALE_PRICE);
    let new_price = (pyth::price::get_price(&price) as u64);
    assert!(new_price > 0, E_NEGATIVE_PRICE);
    if (state.current_price > 0) {
        let dev = if (new_price > state.current_price) {
            (new_price - state.current_price) * BPS / state.current_price
        } else { (state.current_price - new_price) * BPS / state.current_price };
        if (dev > MAX_DEV_BPS) {
            state.is_paused = true;
            event::emit(CircuitBreakerTripped { last_price: state.current_price, new_price, deviation_bps: dev });
            abort E_CIRCUIT_BREAKER
        };
    };
    state.current_price = new_price;
}

public fun unpause(state: &mut PriceState, _admin: &AdminCap) { state.is_paused = false; }
```

**Check:**
1. Look for price storage that overwrites `last_price` with no deviation comparison
2. Grep: `current_price =` or `last_price =` near oracle reads — verify deviation check before assignment
3. Cross-ref: DEFI-22, DEFI-17

---

## Oracle Integration Verification Checklist

- [ ] **Staleness:** Every oracle read checks `publish_time` / `latest_round_timestamp` against max age (DEFI-17)
- [ ] **Per-feed staleness:** Volatile assets use tighter thresholds than stablecoins (DEFI-18)
- [ ] **Exponent handling:** Price exponent correctly applied when normalizing to protocol precision (DEFI-19)
- [ ] **Feed identity:** Feed IDs validated against on-chain registry; testnet vs mainnet verified (DEFI-20)
- [ ] **Depeg awareness:** Wrapped/pegged assets use own price feeds or have depeg breakers (DEFI-21)
- [ ] **Bounds validation:** Prices checked against min/max bounds before arithmetic (DEFI-22)
- [ ] **Price direction:** Quote direction (A/B vs B/A) documented and correctly applied (DEFI-23)
- [ ] **Circuit breakers:** Abnormal deviations trigger pause rather than immediate execution (DEFI-24)

## defi/defi-signatures.md

# DeFi Signatures & Cryptographic Verification — Move

Vulnerability patterns for signature verification in Move DeFi protocols. Covers
meta-transactions, gasless relays, off-chain authorization, multi-sig, and any
protocol that verifies cryptographic signatures on-chain.

---

## DEFI-74 — Nonce Replay Attack

**Description:** Signatures verified without tracking a nonce can be replayed after
the original transaction. An attacker replays a valid signature to execute the same
operation multiple times (e.g., approve transfer, execute trade, authorize withdrawal).

**Pattern:**
```move
// VULNERABLE — no nonce tracking, signature can be replayed
public fun execute_meta_tx(
    message: vector<u8>,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    assert!(
        ed25519::ed25519_verify(&signature, &public_key, &message),
        E_INVALID_SIGNATURE
    );
    // Signature verified — but can be submitted again!
    let action = deserialize_action(&message);
    process_action(action);
}

// SAFE — track and increment nonce per signer
public fun execute_meta_tx(
    nonce_store: &mut Table<vector<u8>, u64>,
    message: vector<u8>,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    assert!(
        ed25519::ed25519_verify(&signature, &public_key, &message),
        E_INVALID_SIGNATURE
    );
    let (action, nonce) = deserialize_action_with_nonce(&message);
    // Verify and increment nonce
    let current = if (table::contains(nonce_store, public_key)) {
        *table::borrow(nonce_store, public_key)
    } else { 0 };
    assert!(nonce == current, E_INVALID_NONCE);
    if (table::contains(nonce_store, public_key)) {
        *table::borrow_mut(nonce_store, public_key) = current + 1;
    } else {
        table::add(nonce_store, public_key, 1);
    };
    process_action(action);
}
```

**Check:**
1. Every signature verification must consume a nonce or mark the signature as used
2. Grep: `ed25519_verify`, `secp256k1_recover`, `verify_signature` — check for nonce logic
3. Alternative: store signature hash in a `Table<vector<u8>, bool>` to prevent replay

---

## DEFI-75 — Cross-Chain Signature Replay

**Description:** A signature valid on Sui can be replayed on Aptos (or vice versa) if
the signed message doesn't include a chain identifier. Same applies across testnets and
mainnets of the same chain.

**Pattern:**
```move
// VULNERABLE — message doesn't include chain ID
public fun verify_authorization(
    message: vector<u8>,  // contains: action + amount + recipient
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    // Same message + signature valid on both Sui mainnet AND Aptos mainnet
    assert!(ed25519::ed25519_verify(&signature, &public_key, &message), E_INVALID);
}

// SAFE — include chain identifier in signed message
public fun verify_authorization(
    message: vector<u8>,  // contains: chain_id + contract_address + action + amount + recipient
    signature: vector<u8>,
    public_key: vector<u8>,
    expected_chain_id: u64,
) {
    assert!(ed25519::ed25519_verify(&signature, &public_key, &message), E_INVALID);
    let (chain_id, contract_addr, _action) = deserialize_message(&message);
    assert!(chain_id == expected_chain_id, E_WRONG_CHAIN);
    // On Sui: also verify contract address matches this package ID
    // On Aptos: verify module address matches
}
```

**Check:**
1. Does the signed message include a chain identifier (chain ID or chain name)?
2. Does it include the contract/module address to prevent replay on different deployments?
3. Check: same protocol deployed on both Sui and Aptos — can signatures be shared?
4. Cross-ref: DEFI-10

---

## DEFI-76 — Missing Parameters in Signed Message

**Description:** Critical parameters not included in the signed message can be
manipulated by the transaction submitter. If the message includes `amount` but
not `recipient`, the submitter can redirect funds to any address.

**Pattern:**
```move
// VULNERABLE — message missing recipient, submitter can redirect
// Signed message: hash(action, amount, nonce)
// Attacker changes recipient to their own address — signature still valid
public fun execute_transfer(
    amount: u64,
    recipient: address,  // NOT in signed message — attacker-controlled
    nonce: u64,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    let message = bcs::to_bytes(&TransferMsg { amount, nonce }); // recipient missing!
    assert!(ed25519::ed25519_verify(&signature, &public_key, &message), E_INVALID);
    transfer_tokens(amount, recipient);
}

// SAFE — all mutable parameters included in signature
public fun execute_transfer(
    amount: u64,
    recipient: address,
    nonce: u64,
    deadline: u64,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    let message = bcs::to_bytes(&TransferMsg { amount, recipient, nonce, deadline });
    assert!(ed25519::ed25519_verify(&signature, &public_key, &message), E_INVALID);
    transfer_tokens(amount, recipient);
}
```

**Check:**
1. List ALL parameters that affect the outcome of the signed operation
2. Every such parameter must be included in the signed message
3. Common missing parameters: recipient, deadline, chain_id, contract address, token type
4. If using BCS serialization, verify field order matches between signer and verifier

---

## DEFI-77 — No Expiration on Signatures

**Description:** Signatures without a deadline/expiration are valid forever. If a
user's permission is revoked (e.g., removed from whitelist, KYC expired), old
signatures still work — granting "lifetime access" that cannot be revoked.

**Pattern:**
```move
// VULNERABLE — signature never expires
public fun claim_with_signature(
    amount: u64,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    let message = bcs::to_bytes(&ClaimMsg { amount });
    assert!(ed25519::ed25519_verify(&signature, &public_key, &message), E_INVALID);
    // This signature works forever — even after airdrop period ends
    mint_tokens(amount);
}

// SAFE — include deadline in signed message
public fun claim_with_signature(
    amount: u64,
    deadline: u64,
    signature: vector<u8>,
    public_key: vector<u8>,
    clock: &Clock,
) {
    assert!(clock::timestamp_ms(clock) <= deadline, E_SIGNATURE_EXPIRED);
    let message = bcs::to_bytes(&ClaimMsg { amount, deadline });
    assert!(ed25519::ed25519_verify(&signature, &public_key, &message), E_INVALID);
    mint_tokens(amount);
}
```

**Check:**
1. Every signed message should include a `deadline` or `expires_at` field
2. The deadline must be checked BEFORE processing the action
3. For long-lived authorizations, consider a revocation mechanism instead

---

## DEFI-78 — Unchecked Signature Verification Return Value

**Description:** Move's `ed25519::ed25519_verify` returns a `bool`. If the code
calls it but doesn't check the return value, ALL signatures pass verification.
This is a critical authentication bypass.

**Pattern:**
```move
// VULNERABLE — return value ignored, all signatures accepted
public fun verify_and_execute(
    message: vector<u8>,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    // Returns bool but not checked! Any signature passes
    ed25519::ed25519_verify(&signature, &public_key, &message);
    execute_privileged_action();
}

// SAFE — assert on return value
public fun verify_and_execute(
    message: vector<u8>,
    signature: vector<u8>,
    public_key: vector<u8>,
) {
    let valid = ed25519::ed25519_verify(&signature, &public_key, &message);
    assert!(valid, E_INVALID_SIGNATURE);
    execute_privileged_action();
}
```

**Check:**
1. Every call to signature verification must assert on the return value
2. Grep: `ed25519_verify`, `ecdsa_recover` — verify return is used in `assert!`
3. On Aptos: `ed25519::signature_verify_strict` also returns `bool`
4. Cross-ref: common-move.md 6.3 (unvalidated return values)

---

## DEFI-79 — Signature Malleability (secp256k1)

**Description:** secp256k1 ECDSA signatures have a malleability property: for any
valid signature `(r, s)`, the signature `(r, n-s)` is also valid (where `n` is the
curve order). If raw signature bytes are used as unique keys (e.g., in a replay
prevention table), the malleated signature bypasses the check.

**Pattern:**
```move
// VULNERABLE — using raw signature as replay prevention key
public fun execute_once(
    used_sigs: &mut Table<vector<u8>, bool>,
    message: vector<u8>,
    signature: vector<u8>,
) {
    assert!(!table::contains(used_sigs, signature), E_ALREADY_USED);
    // Attacker submits (r, s) — marked as used
    // Then submits (r, n-s) — different bytes, same signer, bypasses check!
    let pk = ecdsa_k1::secp256k1_ecrecover(&signature, &message, 0);
    assert!(pk == EXPECTED_SIGNER, E_WRONG_SIGNER);
    table::add(used_sigs, signature, true);
    execute_action();
}

// SAFE — normalize s-value, or use message hash as key instead
public fun execute_once(
    used_msgs: &mut Table<vector<u8>, bool>,
    message: vector<u8>,
    signature: vector<u8>,
) {
    let msg_hash = hash::sha3_256(message);
    assert!(!table::contains(used_msgs, msg_hash), E_ALREADY_USED);
    let pk = ecdsa_k1::secp256k1_ecrecover(&signature, &message, 0);
    assert!(pk == EXPECTED_SIGNER, E_WRONG_SIGNER);
    // Key by message hash, not signature — malleability doesn't matter
    table::add(used_msgs, msg_hash, true);
    execute_action();
}
// Note: ed25519 in Move uses strict verification which is NOT malleable
```

**Check:**
1. If using secp256k1: verify s-value normalization or use message hash as replay key
2. If using ed25519: `ed25519_verify` with strict mode is safe from malleability
3. Never use raw signature bytes as a unique identifier
4. Check if protocol uses `secp256k1_ecrecover` — search for `ecdsa_k1`, `secp256k1`

---

## Signature Verification Checklist

- [ ] Every signature verification tracks and consumes a nonce (DEFI-74)
- [ ] Signed messages include chain ID and contract address (DEFI-75)
- [ ] All outcome-affecting parameters are included in signed message (DEFI-76)
- [ ] Signatures include expiration deadline (DEFI-77)
- [ ] Signature verification return value is asserted, never ignored (DEFI-78)
- [ ] secp256k1 replay prevention uses message hash, not raw signature bytes (DEFI-79)

## defi/defi-slippage.md

# DeFi Slippage & MEV — Move

Deep-dive patterns for slippage protection and MEV vulnerabilities in Move DeFi protocols.
These apply to all swap, AMM, and liquidity operations on Sui and Aptos.

---

## DEFI-43 — Zero or Missing `min_amount_out`

**Description:** Swap functions that accept no minimum output parameter, or default it
to zero, allow sandwich attacks to extract nearly 100% of the swap value. This is the
single most common MEV vulnerability in DeFi.

**Pattern:**
```move
// VULNERABLE — no min_amount_out parameter at all
public entry fun swap<CoinIn, CoinOut>(
    pool: &mut Pool<CoinIn, CoinOut>,
    coin_in: Coin<CoinIn>,
    ctx: &mut TxContext
) {
    let coin_out = do_swap(pool, coin_in);
    // No check on coin_out value — sandwich extracts all value
    transfer::public_transfer(coin_out, tx_context::sender(ctx));
}

// SAFE — user specifies and enforces minimum output
public entry fun swap<CoinIn, CoinOut>(
    pool: &mut Pool<CoinIn, CoinOut>,
    coin_in: Coin<CoinIn>,
    min_amount_out: u64,
    ctx: &mut TxContext
) {
    let coin_out = do_swap(pool, coin_in);
    assert!(coin::value(&coin_out) >= min_amount_out, E_SLIPPAGE_EXCEEDED);
    transfer::public_transfer(coin_out, tx_context::sender(ctx));
}
```

**Check:**
1. Every swap/exchange entry function must have a `min_amount_out` parameter
2. Grep: `fun swap`, `fun exchange`, `fun trade` — verify slippage param exists
3. Check that `min_amount_out = 0` is rejected or warned against in documentation
4. Verify the assertion happens AFTER the swap, comparing actual output
5. Cross-ref: DEFI-07

---

## DEFI-44 — No Deadline Parameter

**Description:** Transactions without a deadline can be held by validators or
sequencers and executed at a later time when market conditions have changed
unfavorably. Unlike EVM's `block.timestamp`, Move has no implicit tx deadline.

**Pattern:**
```move
// VULNERABLE — no deadline, tx can be delayed indefinitely
public entry fun swap<A, B>(
    pool: &mut Pool<A, B>,
    coin_in: Coin<A>,
    min_out: u64,
    ctx: &mut TxContext
) {
    // Even with min_out, a delayed tx might execute when min_out
    // is far below market price — user gets worst acceptable price
    let out = do_swap(pool, coin_in);
    assert!(coin::value(&out) >= min_out, E_SLIPPAGE);
    transfer::public_transfer(out, tx_context::sender(ctx));
}

// SAFE — enforce deadline (Sui)
public entry fun swap<A, B>(
    pool: &mut Pool<A, B>,
    coin_in: Coin<A>,
    min_out: u64,
    deadline_ms: u64,
    clock: &Clock,
    ctx: &mut TxContext
) {
    assert!(clock::timestamp_ms(clock) <= deadline_ms, E_EXPIRED);
    let out = do_swap(pool, coin_in);
    assert!(coin::value(&out) >= min_out, E_SLIPPAGE);
    transfer::public_transfer(out, tx_context::sender(ctx));
}
```

**Check:**
1. All time-sensitive operations (swaps, liquidations, auctions) should have a deadline
2. On Sui: deadline checked against `clock::timestamp_ms(clock)`
3. On Aptos: deadline checked against `timestamp::now_seconds()`
4. Verify deadline is checked at the START of the function, not after state changes

---

## DEFI-45 — Hardcoded / Fixed Slippage Tolerance

**Description:** A hardcoded slippage tolerance (e.g., `const SLIPPAGE_BPS: u64 = 500`)
prevents users from setting tighter protection. During high volatility, the fixed tolerance
may be too loose (sandwich profitable). During low liquidity, it may be too tight (tx reverts,
funds stuck).

**Pattern:**
```move
// VULNERABLE — hardcoded 5% slippage
const SLIPPAGE_BPS: u64 = 500;

public fun rebalance(pool: &mut Pool, amount: u64) {
    let expected = calculate_output(pool, amount);
    let min_out = expected * (10000 - SLIPPAGE_BPS) / 10000; // always 5%
    let out = do_swap(pool, amount);
    assert!(coin::value(&out) >= min_out, E_SLIPPAGE);
    // 5% is too loose for stable pairs, too tight during volatility
}

// SAFE — user-provided or per-operation slippage
public fun rebalance(pool: &mut Pool, amount: u64, max_slippage_bps: u64) {
    assert!(max_slippage_bps <= MAX_ALLOWED_SLIPPAGE, E_SLIPPAGE_TOO_HIGH);
    let expected = calculate_output(pool, amount);
    let min_out = expected * (10000 - max_slippage_bps) / 10000;
    let out = do_swap(pool, amount);
    assert!(coin::value(&out) >= min_out, E_SLIPPAGE);
}
```

**Check:**
1. Grep: `const.*SLIPPAGE`, `const.*SLIP` — flag any hardcoded slippage values
2. Admin/keeper functions using protocol funds are especially vulnerable
3. Verify slippage cannot be set to 100% (effectively zero protection)
4. Check if hardcoded slippage can cause withdrawal failures during volatility

---

## DEFI-46 — On-Chain Self-Referential Slippage Calculation

**Description:** Calculating `min_amount_out` from the same pool state that will execute
the swap. An attacker manipulates pool state first, then the slippage calculation reflects
the manipulated state — offering zero protection.

**Pattern:**
```move
// VULNERABLE — slippage calculated from manipulable on-chain state
public fun swap_with_auto_slippage<A, B>(pool: &mut Pool<A, B>, coin_in: Coin<A>) {
    let amount_in = coin::value(&coin_in);
    // Attacker front-runs: manipulates pool reserves
    // Now quote() returns the manipulated price as "expected"
    let expected_out = quote(pool, amount_in);
    let min_out = expected_out * 95 / 100; // 5% of manipulated price = no protection
    let out = do_swap(pool, coin_in);
    assert!(coin::value(&out) >= min_out, E_SLIPPAGE);
}

// SAFE — min_amount_out comes from off-chain calculation
public fun swap<A, B>(
    pool: &mut Pool<A, B>,
    coin_in: Coin<A>,
    min_amount_out: u64,  // calculated off-chain from TWAP or external oracle
    ctx: &mut TxContext
) {
    let out = do_swap(pool, coin_in);
    assert!(coin::value(&out) >= min_amount_out, E_SLIPPAGE);
    transfer::public_transfer(out, tx_context::sender(ctx));
}
```

**Check:**
1. Identify any function that both queries a price AND executes against the same pool
2. `min_amount_out` must come from the user (off-chain) or a separate oracle (TWAP)
3. Any "auto-slippage" feature that reads from the pool being swapped is vulnerable
4. Cross-ref: DEFI-01

---

## DEFI-47 — LP Operation Slippage (Add/Remove Liquidity)

**Description:** Slippage protection implemented for swaps but missing for `add_liquidity`
and `remove_liquidity`. LP tokens minted or assets received can be sandwiched just like
swaps. Attacker skews the pool ratio before the LP operation.

**Pattern:**
```move
// VULNERABLE — add_liquidity has no slippage protection
public entry fun add_liquidity<A, B>(
    pool: &mut Pool<A, B>,
    coin_a: Coin<A>,
    coin_b: Coin<B>,
    ctx: &mut TxContext
) {
    let lp_tokens = mint_lp(pool, coin_a, coin_b);
    // No check on lp_tokens value — attacker skews pool to reduce LP minted
    transfer::public_transfer(lp_tokens, tx_context::sender(ctx));
}

// SAFE — enforce minimum LP tokens minted
public entry fun add_liquidity<A, B>(
    pool: &mut Pool<A, B>,
    coin_a: Coin<A>,
    coin_b: Coin<B>,
    min_lp_out: u64,
    ctx: &mut TxContext
) {
    let lp_tokens = mint_lp(pool, coin_a, coin_b);
    assert!(coin::value(&lp_tokens) >= min_lp_out, E_SLIPPAGE_LP);
    transfer::public_transfer(lp_tokens, tx_context::sender(ctx));
}
```

**Check:**
1. All `add_liquidity` functions must have `min_lp_out` parameter
2. All `remove_liquidity` functions must have `min_amount_a` and `min_amount_b` parameters
3. Single-sided liquidity operations are especially vulnerable — check proportional deposit
4. Cross-ref: DEFI-03

---

## DEFI-48 — Token vs USD Slippage Confusion

**Description:** Slippage set in token terms when the real risk is USD value, or vice versa.
In multi-hop swaps, intermediate token amounts may look fine but the final USD value is
significantly lower due to price movements across the hops.

**Pattern:**
```move
// VULNERABLE — slippage on intermediate hop, not final output
public fun multi_hop_swap(
    pool_ab: &mut Pool<A, B>,
    pool_bc: &mut Pool<B, C>,
    coin_a: Coin<A>,
    min_b: u64,  // only protects first hop
    ctx: &mut TxContext
) {
    let coin_b = swap(pool_ab, coin_a);
    assert!(coin::value(&coin_b) >= min_b, E_SLIPPAGE);
    let coin_c = swap(pool_bc, coin_b);
    // No slippage check on final output coin_c!
    transfer::public_transfer(coin_c, tx_context::sender(ctx));
}

// SAFE — slippage on final output
public fun multi_hop_swap(
    pool_ab: &mut Pool<A, B>,
    pool_bc: &mut Pool<B, C>,
    coin_a: Coin<A>,
    min_final_out: u64,  // protects final output
    ctx: &mut TxContext
) {
    let coin_b = swap(pool_ab, coin_a);
    let coin_c = swap(pool_bc, coin_b);
    assert!(coin::value(&coin_c) >= min_final_out, E_SLIPPAGE);
    transfer::public_transfer(coin_c, tx_context::sender(ctx));
}
```

**Check:**
1. In multi-hop swaps, slippage must protect the FINAL output, not intermediates
2. Check if intermediate slippage checks give false sense of security
3. For USD-denominated protocols, verify slippage is in USD value terms

---

## DEFI-49 — PTB Composability Sandwich (Sui-Specific)

**Description:** Sui's Programmable Transaction Blocks (PTBs) allow composing multiple
operations atomically. While PTBs make DeFi more composable, they also enable
sophisticated sandwich attacks within a single transaction block by validators.

**Pattern:**
```move
// CONTEXT — Sui PTB enables atomic multi-step operations
// A validator can construct a PTB that:
// 1. Swaps large amount in Pool to move price (front-run)
// 2. Includes victim's swap transaction
// 3. Swaps back to capture profit (back-run)
// All within the SAME transaction block

// VULNERABLE — entry function allows arbitrary composition
public entry fun swap_and_deposit<A, B>(
    pool: &mut Pool<A, B>,
    vault: &mut Vault<B>,
    coin_a: Coin<A>,
    ctx: &mut TxContext
) {
    let coin_b = do_swap(pool, coin_a);
    // Swap output deposited without slippage check
    deposit_to_vault(vault, coin_b, ctx);
}

// SAFE — enforce slippage at each composable boundary
public entry fun swap_and_deposit<A, B>(
    pool: &mut Pool<A, B>,
    vault: &mut Vault<B>,
    coin_a: Coin<A>,
    min_swap_out: u64,
    min_vault_shares: u64,
    deadline_ms: u64,
    clock: &Clock,
    ctx: &mut TxContext
) {
    assert!(clock::timestamp_ms(clock) <= deadline_ms, E_EXPIRED);
    let coin_b = do_swap(pool, coin_a);
    assert!(coin::value(&coin_b) >= min_swap_out, E_SLIPPAGE);
    let shares = deposit_to_vault(vault, coin_b, ctx);
    assert!(shares >= min_vault_shares, E_SLIPPAGE_VAULT);
}
```

**Check:**
1. On Sui: every composable entry function must enforce its own slippage protection
2. Do not rely on the "caller will check" — PTBs can bypass intermediate checks
3. Verify that shared object mutations in multi-step PTBs cannot be front-run by validators
4. Check if `public entry` functions expose unprotected intermediate states
5. Cross-ref: SUI-02, SUI-11

---

## Slippage / MEV Verification Checklist

- [ ] All swap entry functions have `min_amount_out` parameter (DEFI-43)
- [ ] Time-sensitive operations enforce a deadline parameter (DEFI-44)
- [ ] No hardcoded slippage constants used for user-facing operations (DEFI-45)
- [ ] Slippage calculations use off-chain values, not same-pool queries (DEFI-46)
- [ ] `add_liquidity` and `remove_liquidity` have slippage protection (DEFI-47)
- [ ] Multi-hop swaps protect final output, not just intermediates (DEFI-48)
- [ ] On Sui: PTB-composable functions enforce slippage at each boundary (DEFI-49)

## defi/defi-staking.md

# DeFi Staking — Vulnerability Patterns

Staking-specific vulnerability patterns for Move smart contracts. Load when auditing
protocols using `stake`, `unstake`, `reward_per_share`, `accumulator`, or `farming` logic.

---

## DEFI-11 — First Depositor Share Theft

**Description:** The first depositor manipulates the share-to-asset ratio to steal from
subsequent depositors. Attacker deposits 1 unit (1 share), donates tokens directly to
inflate price-per-share. Next depositor's shares round to 0, attacker redeems all assets.

**Pattern:**
```move
// VULNERABLE — no minimum initial shares, no dead shares burned
public fun deposit<T>(pool: &mut Pool<T>, coin: Coin<T>): u64 {
    let deposit_amount = coin::value(&coin);
    let total_balance = balance::value(&pool.balance);
    let shares = if (pool.total_shares == 0) {
        deposit_amount  // first depositor gets 1:1 shares
    } else {
        // Attack: total_balance inflated via direct donation
        // deposit_amount=5000, total_shares=1, total_balance=10000
        // shares = 5000 * 1 / 10000 = 0 -> depositor gets ZERO shares
        (deposit_amount * pool.total_shares) / total_balance
    };
    balance::join(&mut pool.balance, coin::into_balance(coin));
    pool.total_shares = pool.total_shares + shares;
    shares
}

// SAFE — burn minimum initial shares to anchor the ratio
public fun deposit<T>(pool: &mut Pool<T>, coin: Coin<T>): u64 {
    let deposit_amount = coin::value(&coin);
    let total_balance = balance::value(&pool.balance);
    let shares = if (pool.total_shares == 0) {
        assert!(deposit_amount > MIN_INITIAL_SHARES, E_INSUFFICIENT_INITIAL);
        pool.total_shares = MIN_INITIAL_SHARES; // burn dead shares
        deposit_amount - MIN_INITIAL_SHARES
    } else {
        (deposit_amount * pool.total_shares) / total_balance
    };
    assert!(shares > 0, E_ZERO_SHARES);
    balance::join(&mut pool.balance, coin::into_balance(coin));
    pool.total_shares = pool.total_shares + shares;
    shares
}
```

**Check:**
1. Is the pool's first deposit protected by burning minimum dead shares or enforcing a minimum deposit?
2. Can an attacker call `balance::join` directly to inflate the ratio?
3. Does the share calculation guard against returning 0 shares?
4. Cross-ref: DEFI-03

---

## DEFI-12 — Reward Dilution via Direct Transfer

**Description:** Sending reward tokens directly to a staking pool's balance bypasses
the reward accumulator update. The balance increases but `reward_per_share` is never
updated, causing rewards to be distributed incorrectly or silently lost.

**Pattern:**
```move
// VULNERABLE — uses pool balance for reward calculation
public fun update_rewards<T>(pool: &mut StakePool<T>) {
    let current_balance = balance::value(&pool.balance);
    // BUG: current_balance includes directly deposited tokens
    let new_rewards = (current_balance as u128) - (pool.total_staked as u128);
    if (pool.total_staked > 0) {
        pool.reward_per_share = pool.reward_per_share
            + new_rewards / (pool.total_staked as u128);
    };
}

// SAFE — separate staked/reward balances, explicit reward injection
public fun add_rewards<T>(pool: &mut StakePool<T>, reward_coin: Coin<T>) {
    let reward_amount = coin::value(&reward_coin);
    if (pool.total_staked > 0) {
        pool.reward_per_share = pool.reward_per_share
            + ((reward_amount as u128) * PRECISION) / (pool.total_staked as u128);
    };
    pool.distributed_rewards = pool.distributed_rewards + reward_amount;
    balance::join(&mut pool.reward_balance, coin::into_balance(reward_coin));
}
```

**Check:**
1. Does the protocol use `balance::value(&pool.balance)` to derive reward amounts?
2. Are staked funds and reward funds stored in separate `Balance` fields?
3. Can anyone call `balance::join` on the pool outside the intended deposit flow?
4. On Sui: can a PTB compose a direct deposit with a claim?
5. Cross-ref: DEFI-05

---

## DEFI-13 — Precision Loss in Reward Accumulator

**Description:** Move's `u64` (max ~1.8e19) and `u128` (max ~3.4e38) are smaller than
Solidity's `uint256`. When `total_staked` is large relative to `reward_amount`, the
`reward_per_share` increment rounds to 0, silently destroying rewards.

**Pattern:**
```move
// VULNERABLE — u64 arithmetic, no precision scaling
public fun update_reward_index(pool: &mut Pool, reward_amount: u64) {
    if (pool.total_staked == 0) return;
    // reward_amount=999, total_staked=1_000_000 -> increment = 0
    pool.reward_per_share = pool.reward_per_share
        + reward_amount / pool.total_staked;
}

// SAFE — multiply before divide, u128 intermediate, large PRECISION
const PRECISION: u128 = 1_000_000_000_000; // 1e12

public fun update_reward_index(pool: &mut Pool, reward_amount: u64) {
    if (pool.total_staked == 0) return;
    pool.reward_per_share = pool.reward_per_share
        + ((reward_amount as u128) * PRECISION) / (pool.total_staked as u128);
}

public fun pending_reward(pool: &Pool, user_staked: u64, user_debt: u128): u64 {
    let raw = ((user_staked as u128) * pool.reward_per_share) / PRECISION;
    ((raw - user_debt) as u64)
}
```

**Check:**
1. Is the reward accumulator stored as `u128`? `u64` overflows or rounds to 0 easily
2. Does the update formula multiply by PRECISION (>= 1e12) before dividing?
3. Does `pending_reward` correctly divide by PRECISION when computing payouts?
4. Cross-ref: common-move.md 2.2

---

## DEFI-14 — Flash Deposit/Withdraw Griefing

**Description:** Attacker performs a large flash deposit to dilute pending rewards, claims
a disproportionate share, then immediately withdraws. On Sui, PTBs enable
stake + claim + unstake in a single transaction block.

**Pattern:**
```move
// VULNERABLE — no minimum stake duration, rewards claimable immediately
struct UserStake has key, store {
    id: UID,
    amount: u64,
    reward_debt: u128,
    // No timestamp — no duration enforcement
}

public fun unstake<T>(pool: &mut Pool<T>, user: UserStake, ctx: &mut TxContext): Coin<T> {
    let UserStake { id, amount, reward_debt: _ } = user;
    object::delete(id);
    pool.total_staked = pool.total_staked - amount;
    coin::from_balance(balance::split(&mut pool.staked, amount), ctx)
}

// SAFE — enforce minimum stake duration
struct UserStake has key, store {
    id: UID,
    amount: u64,
    reward_debt: u128,
    stake_time_ms: u64,
}

public fun unstake<T>(
    pool: &mut Pool<T>, user: UserStake, clock: &Clock, ctx: &mut TxContext
): Coin<T> {
    let UserStake { id, amount, reward_debt: _, stake_time_ms } = user;
    let elapsed = clock::timestamp_ms(clock) - stake_time_ms;
    assert!(elapsed >= MIN_STAKE_DURATION_MS, E_STAKE_TOO_SHORT); // e.g., 24h
    object::delete(id);
    pool.total_staked = pool.total_staked - amount;
    coin::from_balance(balance::split(&mut pool.staked, amount), ctx)
}
```

**Check:**
1. Can `stake()` + `claim()` + `unstake()` be called in the same transaction (PTB on Sui)?
2. Is there a minimum stake duration enforced via on-chain timestamp?
3. Does reward distribution use time-weighted calculations that resist single-block manipulation?
4. Cross-ref: SUI-02, common-move.md 10.2

---

## DEFI-15 — Stale Reward Index After Distribution

**Description:** Adding new rewards without updating `reward_per_share` first causes
stale calculations. The admin `add_rewards()` function omits the index update call
that every other state-modifying function includes.

**Pattern:**
```move
// VULNERABLE — add_rewards does not update reward index
public fun stake<T>(pool: &mut Pool<T>, clock: &Clock, amount: u64) {
    update_reward_index(pool, clock); // correctly updates
    pool.total_staked = pool.total_staked + amount;
}

public fun add_rewards<T>(pool: &mut Pool<T>, reward_coin: Coin<T>, new_rate: u64) {
    // Missing: update_reward_index(pool, clock);
    // Rewards accrued at old rate since last_update_time are lost
    balance::join(&mut pool.reward_balance, coin::into_balance(reward_coin));
    pool.reward_rate = new_rate;  // rate change applied retroactively
}

// SAFE — always update index before modifying distribution parameters
public fun add_rewards<T>(
    pool: &mut Pool<T>, reward_coin: Coin<T>, new_rate: u64, clock: &Clock
) {
    update_reward_index(pool, clock); // settle accrued rewards at old rate
    balance::join(&mut pool.reward_balance, coin::into_balance(reward_coin));
    pool.reward_rate = new_rate;      // now safe to change rate
}
```

**Check:**
1. List every function that modifies `reward_rate`, `total_staked`, or distribution parameters
2. Does each call `update_reward_index()` before the modification?
3. Admin functions are the most common offenders — check `add_rewards()`, `set_reward_rate()`
4. If admin function lacks a `Clock` parameter, it physically cannot call the update
5. Cross-ref: DEFI-05

---

## DEFI-16 — Balance Caching Mismatch

**Description:** A cached balance value diverges from actual on-chain balance during
transaction execution. On Sui, `balance::value()` on a shared object can change between
PTB steps. On Aptos, `borrow_global` returns a snapshot that may differ after subsequent
operations within the same transaction.

**Pattern:**
```move
// VULNERABLE — reads balance at start, uses stale cached value after operations
public fun compound_and_withdraw<T>(vault: &mut Vault<T>, amount: u64, ctx: &mut TxContext): Coin<T> {
    let cached_balance = balance::value(&vault.balance); // cache
    // Operation 1: compound pending rewards (modifies vault.balance)
    let reward = vault.pending_rewards;
    vault.pending_rewards = 0;
    // BUG: cached_balance is stale — doesn't include compounded rewards
    let user_share = (amount * 10000) / cached_balance; // inflated share
    coin::from_balance(balance::split(&mut vault.balance, amount), ctx)
}

// SAFE — re-read balance after any operation that modifies it
public fun compound_and_withdraw<T>(vault: &mut Vault<T>, amount: u64, ctx: &mut TxContext): Coin<T> {
    let reward = vault.pending_rewards;
    vault.pending_rewards = 0;
    // Re-read AFTER compound — always use fresh value
    let current_balance = balance::value(&vault.balance);
    let user_share = (amount * 10000) / current_balance;
    coin::from_balance(balance::split(&mut vault.balance, amount), ctx)
}
```

**Check:**
1. Does any function cache `balance::value()` early and use it after balance-modifying operations?
2. On Sui: can another PTB step modify a shared object's balance between read and use?
3. Prefer using return values of balance-modifying operations over pre-cached reads
4. Cross-ref: common-move.md 6.4

---

## Staking Verification Checklist

- [ ] First depositor attack mitigated — minimum dead shares burned or virtual reserves (DEFI-11)
- [ ] Reward accounting uses dedicated reward balance, not pool's total balance (DEFI-12)
- [ ] Reward accumulator uses u128 with PRECISION >= 1e12, multiplies before dividing (DEFI-13)
- [ ] Minimum stake duration enforced via on-chain timestamp (DEFI-14)
- [ ] Every function modifying reward rate or total staked calls `update_reward_index()` first (DEFI-15)
- [ ] No stale cached balance values used after balance-modifying operations (DEFI-16)

## evidence-chains.md

# Evidence Chains

Load this file during **Phase 7 — Verify & Triage**. It provides structured evidence
templates for proving or disproving Move audit findings.

Every non-trivial finding must include at least one completed evidence template.

---

## Section 1: Data Flow Evidence Template

Trace data from source to sink. Every step must cite exact code.

```
### Data Flow: [finding title]

| Step | Location | Description | Trust Level |
|------|----------|-------------|-------------|
| Source | `file.move:NN` | Where the value originates | [see levels below] |
| Validation | `file.move:NN` | What checks the value passes through | — |
| Transform | `file.move:NN` | How the value is modified | — |
| Sink | `file.move:NN` | Where the value is consumed / stored | — |

**Attacker-controlled?** Yes / No — because [reason]
**Validation gap?** Yes / No — because [reason]
```

### Move Trust Levels

| Source | Trust Level | Rationale |
|--------|------------|-----------|
| Owned object parameter | Owner-trusted | Only the object owner can pass it in a PTB/tx |
| `&signer` (Aptos) | Signer-trusted | Transaction signer verified by runtime |
| Shared object parameter | **Untrusted** | Anyone can reference a shared object |
| `Cap`-gated parameter | Capability-holder-trusted | Only the cap holder can call |
| `clock::timestamp_ms` / `TxContext` | System-trusted | Provided by validators, not user-controllable |
| Function argument (non-object) | **Untrusted** | Caller can pass arbitrary values |
| `dynamic_field::borrow` result | Context-dependent | Trusted if parent object is owned; untrusted if shared |
| Return value from external module | **Untrusted** | Unless verified from source (tag `[PROD-SOURCE]`) |

---

## Section 2: Mathematical Bounds Proof Template

For any finding involving arithmetic overflow, precision loss, or economic thresholds.

```
### Math Proof: [finding title]

**Expression:** [the exact arithmetic expression from code, e.g., `a * b / c`]
**Location:** `file.move:NN`

**Variable bounds:**
| Variable | Type | Min | Max | Source of bound |
|----------|------|-----|-----|-----------------|
| a | u64 | 0 | 18_446_744_073_709_551_615 | type max |
| b | u64 | 0 | [realistic max from protocol] | `file.move:NN` |
| c | u64 | 1 | [realistic max] | assert at `file.move:NN` |

**Overflow check:**
- Intermediate: `a * b` max = [value] → overflows u64? Yes/No
- With realistic values: `a * b` max = [value] → overflows? Yes/No
- Trigger condition: [exact values that cause overflow]

**Precision loss check:**
- `a / c` when a < c → result = 0? Impact: [describe]
- Rounding direction: floor (favors protocol / favors user?)

**Conclusion:** Overflow/precision loss IS/IS NOT reachable with production values.
```

### Move Integer Type Reference

| Type | Bits | Max Value |
|------|------|-----------|
| u8 | 8 | 255 |
| u16 | 16 | 65,535 |
| u32 | 32 | 4,294,967,295 |
| u64 | 64 | 18,446,744,073,709,551,615 (~1.8×10¹⁹) |
| u128 | 128 | ~3.4×10³⁸ |
| u256 | 256 | ~1.15×10⁷⁷ |

---

## Section 3: Attacker Control Analysis Template

Enumerate what the attacker controls and how they exercise that control.

```
### Attacker Control: [finding title]

**Chain:** Sui / Aptos

**Control surfaces:**
| Control Type | What attacker controls | How | Constraints |
|-------------|----------------------|-----|-------------|
| PTB composition (Sui) | Call sequence, arguments | Constructs PTB with MoveCall commands | Must use `public` or `entry` functions only |
| Transaction script (Aptos) | Call sequence, arguments | Writes entry function calls | Must use `entry` or `public entry` functions |
| Object control | [which objects] | Owned: full control / Shared: read + mutate via public fns | [list constraints] |
| Signer control | Own address only | Cannot impersonate other signers | Single signer per tx (or multi-sig) |
| Type parameter control | `<T>` in generic calls | Can instantiate with any type meeting constraints | Ability constraints enforced |
| Call sequence control | Order of calls in PTB/script | Deterministic, attacker-chosen order | Within single tx only |
| Timing control | When to submit tx | Choose epoch/timestamp window | Cannot control exact consensus ordering |

**Critical question:** Does the attacker control enough surfaces simultaneously
to reach the vulnerable state AND extract value?
```

---

## Section 4: PoC Pseudocode Template

Write concrete exploit sequences, not vague descriptions.

### Sui PTB Format

```
### PoC: [finding title]

**Preconditions:**
- [Object X exists as shared object at 0x...]
- [Attacker owns Y tokens]
- [Protocol state: ...]

**PTB Sequence:**
1. MoveCall(pkg::module::function_a<TypeA>(shared_obj, arg1, arg2))
   → Returns: result_a
2. MoveCall(pkg::module::function_b(result_a, attacker_coin))
   → Returns: stolen_value
3. TransferObjects([stolen_value], attacker_address)

**Postconditions:**
- Attacker gains: [exact amount and asset]
- Protocol loses: [exact amount and asset]
- State corruption: [describe if any]

**Profit calculation:**
- Gross profit: [amount]
- Gas cost: ~[amount] SUI
- Net profit: [amount]
```

### Aptos Transaction Script Format

```
### PoC: [finding title]

**Preconditions:**
- [Resource R exists at address 0x...]
- [Attacker has account with ...]

**Transaction sequence:**
Tx 1:
  entry fun setup(signer: &signer) {
      module::function_a<TypeA>(signer, arg1, arg2);
  }

Tx 2:
  entry fun exploit(signer: &signer) {
      let value = module::function_b(signer, arg3);
      coin::deposit(signer::address_of(signer), value);
  }

**Postconditions:**
- Attacker gains: [exact amount]
- Victim loses: [exact amount]
```

---

## Section 5: Negative PoC Template

When dismissing a finding, prove it's NOT exploitable.

```
### Negative PoC: [finding title]

**Claimed vulnerability:** [what the finding alleges]

**Normal operation trace:**
1. User calls function_a(args) → [state changes]
2. Internal: assert!(condition) at line NN → passes because [reason]
3. Result: [expected behavior]

**Attempted exploit trace:**
1. Attacker calls function_a(malicious_args) → [state changes]
2. Internal: assert!(condition) at line NN → **FAILS** because [reason]
3. Transaction aborts. No state corruption.

**Precondition gap:**
The exploit requires [state X], but:
- [State X] is set only in `init()` at `file.move:NN`
- `init()` enforces [constraint] via assert at line NN
- Therefore [state X] is unreachable through valid protocol operations

**Conclusion:** Finding DISMISSED — [precondition unreachable / blocked by assert /
type system prevents / ownership model prevents]
```

---

## Section 6: Devil's Advocate Evidence Template

Structured challenge protocol. Answer 11 questions AGAINST the finding, then 2 FOR
it (to prevent false negatives from overzealous dismissal).

### Questions AGAINST the Finding (try to disprove it)

```
### Devil's Advocate: [finding title]

**Against:**
1. Does Move's type system already prevent this?
   → [answer with specific ability/constraint]

2. Does an upstream caller already validate this input?
   → [trace callers, cite file:line]

3. Is the "vulnerable" branch actually reachable?
   → [trace all paths that set the condition variable]

4. Does a downstream assert/abort already block the exploit?
   → [cite the assert, explain why it fires]

5. Does object ownership make this infeasible?
   → [Sui: owned vs shared; Aptos: signer requirement]

6. Does the attacker actually profit after gas + fees?
   → [calculate cost vs gain]

7. Can the attacker actually obtain the required capability/object?
   → [trace capability creation and distribution]

8. Is this a known-safe design pattern? (Check DESIGN-L1 to L4)
   → [cite the pattern if applicable]

9. Would my recommended fix actually change behavior?
   → [apply fix mentally, compare outcomes]

10. Am I applying a Solidity mental model to Move?
    → [list Move-specific properties that differ]

11. Does the code I'm citing actually say what I think?
    → [re-read it now, quote the exact line]
```

### Questions FOR the Finding (prevent false negatives)

```
**For:**
12. If I'm wrong about the protection, what's the worst case?
    → [describe maximum impact if the protection fails]

13. Is there a code path that BYPASSES the protection I identified?
    → [check all callers, all entry points, admin overrides]
```

### Decision

```
**Verdict:** VALID / QUESTIONABLE / DISMISSED
**Strongest argument against:** [#N — one-line summary]
**Strongest argument for:** [#12 or #13 — one-line summary]
**Confidence:** confirmed / likely / needs_review
```

## move-fp-catalog.md

# Move False Positive Catalog

Load this file on **every audit**. It prevents the most common LLM false positives
when auditing Move smart contracts on Sui and Aptos.

---

## Section 1: Rationalizations to Reject

When you catch yourself thinking one of these, STOP and apply the correction.

| # | LLM Shortcut (what you're tempted to say) | Why it's wrong in Move | Required action before reporting |
|---|-------------------------------------------|------------------------|----------------------------------|
| 1 | "No access control on this function" | Sui object ownership IS access control — if the function takes `&mut MyObject` (owned), only the owner can call it | Check whether params are owned objects; if yes, ownership is the gate |
| 2 | "Unchecked arithmetic — overflow possible" | Move aborts on overflow (DoS, not silent corruption). DoS is only a finding if attacker profits from the abort | Prove the attacker profits from the abort or that the abort permanently bricks state. **CRITICAL WARNING: Do NOT dismiss overflow in accumulator/reward/interest update functions without checking the abort-before-checkpoint pattern (12.1, DEFI-85/86). If the overflow occurs BEFORE `last_update_time` is written, it causes PERMANENT deadlock — this is the #1 missed High/Critical in Move audits.** |
| 3 | "Missing signer check" | On Sui, the function may only be callable via PTB with owned objects — the signer is implicit. On Aptos, check whether upstream callers validate `signer::address_of` | Trace ALL callers and check if an upstream gate already validates the signer |
| 4 | "This pattern looks dangerous" | Pattern recognition is not analysis. Move's type system eliminates many patterns that are dangerous in other languages | Complete a full data flow trace before claiming the pattern is exploitable |
| 5 | "Similar code was vulnerable in Solidity" | Move has no reentrancy (no dynamic dispatch), no delegatecall, no fallback functions, no storage collisions. The Solidity mental model does not transfer | Verify this specific Move instance is exploitable using Move-specific primitives |
| 6 | "The function is public so anyone can call it" | On Aptos, `public fun` is NOT a transaction entry point — only `public entry fun` and `entry fun` are. On Sui, `public fun` IS PTB-callable | Check the chain. On Aptos, verify `entry` keyword. On Sui, confirm PTB composability risk |
| 7 | "No input validation on amount parameter" | The upstream caller may construct the value from a safe source (e.g., `coin::value()`, a stored field). Not all inputs come from users | Trace the ACTUAL callers and the source of the value before claiming it's unvalidated |
| 8 | "I'll explain the exploit verbally" | If you can't write an exact PTB/transaction sequence, you're probably hallucinating the exploit. No artifact = no finding | Write the exact PTB sequence (Sui) or transaction script (Aptos) showing the exploit |
| 9 | "This is clearly critical severity" | LLMs systematically overrate severity. Our benchmark shows 25% false positive rate, mostly from inflated severity | Prove with concrete evidence: who loses money, how much, under what conditions |
| 10 | "Rapid analysis of remaining checks — all clean" | Every check gets full verification. Rushing the tail produces false negatives AND false positives | Verify each check through all steps. No batch dismissals |

---

## Section 2: Move-Specific False Positive Patterns

These patterns APPEAR vulnerable but are protected by Move's design. Do NOT report
them unless you have concrete evidence of ACTUAL harm despite the protection.

### 2A. Sui Object Model FPs

| # | Pattern that looks vulnerable | Why it's safe | When it IS a real bug |
|---|------------------------------|---------------|----------------------|
| 1 | Function takes `&mut T` with no explicit auth check | Owned objects: only the owner can pass them to a PTB. The object parameter IS the access control | When the object is `shared` — then anyone can pass it. Check `share_object` calls |
| 2 | Object created with `object::new(ctx)` — no uniqueness check | `UID` is globally unique, generated from transaction digest + creation count. Collision is cryptographically impossible | Never — this is always safe |
| 3 | Admin function has no signer check | If the function requires an `AdminCap` (owned object), only the cap holder can call it. Cap IS the signer equivalent | When the `AdminCap` has `store` ability and can be transferred to an attacker, or when it's a shared object |
| 4 | Token can be spent twice (double-spend claim) | Linear types: `Coin<T>` has no `copy`, so it cannot be duplicated. Spending consumes it | Never in standard Move — only if someone wraps a value in a struct with `copy` ability |
| 5 | Object ID collision between different types | Sui objects are typed — `Pool<SUI>` and `Pool<USDC>` cannot collide even with same ID (which is impossible anyway) | Never — this is always safe |
| 6 | `transfer::transfer` called without ownership check | `transfer::transfer` for owned objects already requires the caller to possess the object (linear type) | When using `transfer::public_transfer` on a shared object without validating the recipient |
| 7 | Wrapped object can be accessed by attacker | Wrapped objects (inside another struct) are inaccessible until unwrapped by the parent's module | When the parent module exposes an `unwrap` function without proper authorization |
| 8 | Shared object concurrent access race condition | Sui's Narwhal/Bullshark consensus serializes all accesses to a shared object within an epoch. No TOCTOU within a transaction | When the protocol relies on cross-transaction ordering (e.g., "first come first served" without explicit sequencing) |

### 2B. Move Type System FPs

| # | Pattern that looks vulnerable | Why it's safe | When it IS a real bug |
|---|------------------------------|---------------|----------------------|
| 1 | Generic function `<T>` accepts any type | Move generics are monomorphized at compile time. The type must satisfy ability constraints | When the function doesn't check `T` against a whitelist and `T` controls pricing/collateral value |
| 2 | Capability can be forged by attacker | If capability struct has only `key` (no `copy`, no `store`), it cannot be duplicated or transferred outside the module | When capability has `copy` or `store` ability — check the struct definition |
| 3 | Reentrancy via external module call | Move has no dynamic dispatch, no callbacks, no fallback functions. All calls are statically resolved | Never in standard Move — cross-module calls are deterministic. But check for state inconsistency between pre-call and post-call |
| 4 | Hot potato not enforced (flash loan repayment) | Hot potato structs (no `drop`, no `store`, no `copy`, no `key`) MUST be consumed in the same transaction. Compiler enforces this | Never — compiler guarantee. But verify the struct actually lacks all four abilities |
| 5 | Phantom type parameter creates confusion | `phantom` type parameters have no runtime effect — they're compile-time markers only | Never — they cannot affect runtime behavior |
| 6 | Function constraint bypass | Ability constraints on generics (`T: store + drop`) are compiler-enforced, not runtime-checked | Never — compiler guarantee. But verify constraints are present and correct |

### 2C. Move Abort Semantics FPs

| # | Pattern that looks vulnerable | Why it's safe | When it IS a real bug |
|---|------------------------------|---------------|----------------------|
| 1 | Arithmetic overflow in financial calculation | Move aborts on overflow — state is NOT corrupted. The transaction simply fails | When the abort permanently bricks state (abort-before-checkpoint pattern — see DEFI-85/86) or when attacker profits from the DoS. **BEFORE dismissing ANY arithmetic overflow, you MUST check: (1) Is this inside a periodic update function? (2) Is the state checkpoint written AFTER the overflow point? (3) Does the time delta grow on retry? If all three YES → PERMANENT DEADLOCK, not a false positive.** |
| 2 | Subtraction underflow on balance check | Abort prevents negative balances — this IS the protection, not the bug | When the abort prevents a legitimate user action (e.g., repayment, liquidation) that should succeed |
| 3 | `assert!` failure aborts the transaction | The abort prevents the invalid state from being written. The revert IS the protection | When the assert condition is wrong (too strict or too lenient) — check the condition logic, not the abort |
| 4 | Division by zero aborts | Abort prevents undefined behavior. State remains consistent | When zero divisor is reachable in normal operation and blocks legitimate actions |
| 5 | Multiple operations abort on same condition | "Same value, different error code" is not a vulnerability. Both produce transaction abort with identical user outcome | Never — if the outcome is identical abort, there's no incremental impact |

### 2D. PTB/Transaction Composition FPs

| # | Pattern that looks vulnerable | Why it's safe | When it IS a real bug |
|---|------------------------------|---------------|----------------------|
| 1 | Sandwich attack on Sui | Sui has no public mempool — validators batch transactions. Sandwich requires validator collusion | When the finding accounts for validator-level adversary and the economic incentive justifies it |
| 2 | Close factor bypassed by calling liquidate() twice | If close factor is enforced per-TRANSACTION (checking cumulative liquidation), repeated calls don't help | When close factor is checked per-CALL against current (shrinking) debt — then PTB repetition bypasses it (SUI-28) |
| 3 | PTB reordering attack | PTB commands execute in deterministic order as specified by the sender. There's no reordering within a PTB | Never within a single PTB — but cross-PTB ordering depends on consensus |
| 4 | Flash loan repayment bypass | Hot potato pattern (no `drop` ability) makes non-repayment a compiler error, not a runtime check | Never if hot potato is correctly implemented (verify abilities) |
| 5 | Front-running shared object access | Narwhal/Bullshark consensus serializes shared object access. Ordering is not first-come-first-served | When the protocol's correctness depends on transaction ordering within an epoch (e.g., auction end times) |

### 2E. DeFi Design Pattern FPs

| # | Pattern that looks vulnerable | Why it's safe | When it IS a real bug |
|---|------------------------------|---------------|----------------------|
| 1 | Flash loan not updating accounting fields (cash, debt) | Hot potato guarantees same-tx repayment. Decrementing cash would understate reserves during the loan window (DESIGN-L2) | When other operations READ the stale value mid-PTB and misprice shares or health |
| 2 | EMA for liquidation eligibility, Spot for seize | Liquidator sells collateral at spot price. Using EMA for seize makes liquidation unprofitable during rapid price drops (DESIGN-L1) | When there's no bounded divergence check between EMA and spot in the liquidation path |
| 3 | Blocking borrows when idle cash < reserve | Protective — ensures protocol fees are not lent out. Resolves as loans are repaid (DESIGN-L3) | When the blocking also prevents repayment or liquidation (not just new borrows) |
| 4 | Liquidation skipping rate limiters | Liquidations must proceed regardless to maintain solvency | When the skip also bypasses other critical safety checks (not just rate limits) |
| 5 | Interest rate returning 0 at low utilization | Expected from fixed-point truncation at near-zero rates | When it enables economically significant free borrowing over meaningful time periods |
| 6 | Liquidation bonus matching industry standard (5-10%) | Standard incentive range used by Compound, Aave, MakerDAO | When the bonus exceeds collateral margin or creates profitable self-liquidation |
| 7 | Interest rate kink/jump model behavior | Steep rate increase above optimal utilization is intentional — it incentivizes repayment | When the kink parameters create discontinuities that can be gamed |
| 8 | Admin parameter setting as sole finding | Admin actions are trusted unless admin is untrusted by design | When admin action is routine AND unprivileged users are later bricked (see common-move.md 12.2) |

### 2F. Documented Design Constraints Are Not Medium+ Findings

| # | Pattern that looks vulnerable | Why it's safe | When it IS a real bug |
|---|------------------------------|---------------|----------------------|
| 1 | Code behavior has explicit security warnings in code comments | The code is working as designed and documented — the developer already considered and accepted this trade-off | When there is NO structural mitigation and the footgun is easy to trigger unintentionally |
| 2 | Wrapper lacks `store` ability (prevents shared-object embedding) | Structural mitigation at the type level — integrators cannot misuse it even if they ignore docs | Never — if the type system prevents the misuse, documenting it is defense-in-depth |
| 3 | Multiple inline warnings (e.g., 3+ comments) about a known limitation | Developer has explicitly warned integrators; triggering requires intentionally ignoring documentation | When the docs warn but the API makes the dangerous path the easiest/default one |

**Rule:** If a behavior is:
1. Documented with explicit security warnings in code comments (not just README), AND
2. Has structural mitigations (e.g., no `store` ability, no public constructor for dangerous state), AND
3. Requires the integrator to intentionally ignore documentation to trigger

Then the maximum severity is **Informational**. The code is working as designed and documented.

**Counter-example:** If the documentation warns but there is NO structural mitigation and the footgun is easy to trigger, Low may be appropriate.

---

## Section 3: Self-Hallucination Check Protocol

**Mandatory:** Run this checklist after concluding each finding. Re-read the actual
source code AFTER reaching your conclusion — not before.

### 5-Point Checklist

For every finding you're about to report, answer ALL five questions:

1. **Can I name the exact file and line number?**
   - If you can only vaguely point to "somewhere in the lending module" → STOP.
   - Re-read the file and find the exact line.

2. **Can I write the exploit PTB/transaction sequence?**
   - Write it out now: `1. MoveCall(pkg::mod::fn(args))`, `2. ...`
   - If you can't write concrete steps → the exploit is probably hallucinated.

3. **Does the code I'm referencing ACTUALLY exist?**
   - Re-read the source file RIGHT NOW.
   - Check: Is the function name correct? Does it have the signature I think?
   - Check: Does the line I'm citing contain what I think it contains?
   - LLMs frequently confuse function names, parameter orders, and line numbers.

4. **Did I re-read the code AFTER reaching my conclusion?**
   - Confirmation bias: once you decide something is a bug, you see evidence
     that confirms it and ignore evidence that refutes it.
   - Re-read the function, its callers, and its callees NOW. Look for:
     - Assert conditions you missed
     - Upstream validation you didn't trace
     - Downstream checks that make the bug unreachable

5. **Am I pattern-matching on scary-looking code?**
   - "This looks like the Solidity reentrancy pattern" → Move has no callbacks
   - "This looks like an unchecked return" → Move forces handling via types
   - "This looks like an access control issue" → Check owned objects first
   - If your reasoning is "it looks like X" rather than "the data flows from A
     through B to C causing D" → your analysis is incomplete.

### Failure Protocol

If ANY of the 5 checks fails:
- Do NOT include the finding in the report
- Go back to the source code and redo the analysis from scratch
- If the finding still fails after re-analysis → mark as DISMISSED with reason
  "failed self-hallucination check #N"

## sample-finding.md

# Example Audit Output

This file shows what a high-quality finding looks like from the move-auditor skill.
Use this as a reference for output format and detail level.

---

## Audit Report — ExampleLending Protocol
**Chain:** Sui
**Date:** 2025-01-15
**Severity Summary:** 1 Critical, 2 High, 1 Medium, 2 Low, 1 Info

---

### [CRITICAL-001] Unchecked Coin Type Allows Worthless Token as Collateral

| Field      | Value |
|------------|-------|
| Severity   | Critical |
| Location   | `lending.move`, line 87, function `deposit_collateral` |
| Category   | Input Validation / Access Control |

**Description:**
The `deposit_collateral` function accepts any `CoinType` as collateral without validating
that it is an approved asset. An attacker can create a custom worthless token, deposit it
as collateral, and borrow real assets against it.

**Attack Scenario (PoC):**
1. Attacker deploys `module attacker::junk_token` and mints 1,000,000 JUNK tokens
2. Attacker calls `deposit_collateral<attacker::junk_token::JUNK>(1_000_000)`
3. Protocol accepts JUNK as collateral (no whitelist check)
4. Attacker calls `borrow<SUI>(900_000_SUI_MIST)` — protocol allows borrow up to 90% LTV
5. Attacker walks away with real SUI, leaving worthless JUNK as collateral
6. Protocol is insolvent

**Recommended Fix:**
Add a whitelist of approved collateral types enforced on-chain:
```move
// Add to protocol's shared config object
struct Config has key, store {
    id: UID,
    approved_collateral: vector<TypeName>,
    // ...
}

// Add whitelist check to deposit_collateral
public entry fun deposit_collateral<CoinType>(
    config: &Config,
    pool: &mut LendingPool,
    coin: Coin<CoinType>,
    ctx: &mut TxContext
) {
    let type_name = type_name::get<CoinType>();
    assert!(
        vector::contains(&config.approved_collateral, &type_name),
        E_COLLATERAL_NOT_APPROVED
    );
    // ... rest of function
}
```

---

### [HIGH-001] Flash Loan Enables Oracle Manipulation for Collateral Valuation

| Field      | Value |
|------------|-------|
| Severity   | High |
| Location   | `oracle.move`, line 34, function `get_price` |
| Category   | Oracle Manipulation |

**Description:**
`get_price()` returns the current spot ratio of the liquidity pool reserves:
`price = pool.reserve_b / pool.reserve_a`. This is manipulable in a single transaction
using a flash loan to temporarily skew the pool ratio, borrow against inflated collateral,
then repay the flash loan — keeping the profit.

**Attack Scenario (PoC):**
1. Attacker takes flash loan of 10,000 SUI
2. Swaps all 10,000 SUI into USDC in target pool → pool ratio now shows USDC worth 10x normal
3. Calls `borrow` using USDC as collateral — oracle reads inflated price
4. Borrows 5,000 SUI against now-"valuable" USDC
5. Swaps USDC back to SUI (restoring pool ratio)
6. Repays flash loan of 10,000 SUI
7. Net profit: ~5,000 SUI minus fees

**Recommended Fix:**
Replace spot price with a TWAP (Time-Weighted Average Price) using at least a 30-minute window:
```move
// Store price observations
struct PriceObservation has store {
    price: u128,
    timestamp: u64,
}

// Compute TWAP from stored observations
public fun get_twap_price(pool: &Pool): u128 {
    // ... compute weighted average over observations
}
```

---

### [HIGH-002] Reward Accumulator Overflow Permanently Freezes Pool

| Field      | Value |
|------------|-------|
| Severity   | High |
| Confidence | VALID (`confirmed`) |
| Location   | `rewards.move`, line 87, function `update_reward_index` |
| Category   | Arithmetic / DoS |

**Description:**
`update_reward_index` computes `reward_per_share += (elapsed_ms * rate * PRECISION) / total_shares`.
The intermediate multiplication `elapsed_ms * rate * PRECISION` overflows `u128` when the pool
is inactive for >10 hours with standard USDC reward parameters. The abort occurs BEFORE
`last_update_time_ms` is checkpointed, creating an irrecoverable deadlock — every subsequent
call to any function that touches rewards will abort at the same line.

**Attack Scenario (PoC):**
```
PTB Sequence (triggering the bug — no attacker needed, normal operation):
1. Admin calls add_reward_program<USDC>(pool, rate=1_000_000, ctx)
2. Pool operates normally for 10+ hours with no deposits/withdrawals
3. Any user calls deposit(pool, coin, ctx)
   → Internal: update_reward_index() at rewards.move:87
   → Intermediate: 36_000_000 * 1_000_000 * 1_000_000_000_000 = 3.6×10²⁵ > u128 max? No.
   → With PRECISION=10^18: 36_000_000 * 1_000_000 * 10^18 = 3.6×10³¹ → overflows u128
   → Transaction aborts. last_update_time NOT updated.
4. All subsequent operations (deposit, withdraw, claim, borrow, repay) abort identically.
5. No admin recovery path — cancel_reward also calls update_reward_index.
```

**Evidence Chain:**

| Claim | Evidence | Tag | Signal Strength |
|-------|----------|-----|-----------------|
| Intermediate multiplication overflows u128 | `rewards.move:87` — `elapsed * rate * PRECISION` | `[CODE]` | 4 (math proof) |
| Overflow values reachable in production | USDC 6 decimals, rate=10^6, PRECISION=10^18, 10h gap | `[CODE]` | 4 (concrete values) |
| Checkpoint written after abort point | `last_update_time_ms` set at line 92, abort at line 87 | `[CODE]` | 3 (call path traced) |
| All entry points call update_reward_index | deposit, withdraw, borrow, repay, claim, cancel — traced | `[CODE]` | 3 (call path traced) |

**Recoverability:** Permanent — all 6 entry points and admin cancel path trapped.

**Verification:** Passed gates: Process (all steps), Reachability (any user tx triggers),
Real Impact (all pool funds locked), PoC (concrete values), Math Bounds (proven), Move Safety (abort semantics confirmed).

**Recommended Fix:**
Checkpoint `last_update_time_ms` BEFORE the potentially-aborting arithmetic:
```move
public fun update_reward_index(pool: &mut Pool, clock: &Clock) {
    let now = clock::timestamp_ms(clock);
    let elapsed = now - pool.last_update_time_ms;
    pool.last_update_time_ms = now;  // checkpoint FIRST
    // ... then compute with overflow-safe math or capped elapsed
}
```

---

### Verified Clean Checks

- ✅ Access control: All entry functions require valid signer or capability
- ✅ Arithmetic: No overflow/underflow DoS vectors found
- ✅ Capability abilities: All capability structs have `drop` only
- ✅ Object transfer: Recipient validated as tx sender in all transfer functions
- ✅ Initialization: `init()` function is one-time-only

---

### Auditor Notes

- Test coverage is low (~40%). Core invariants are not tested. Recommend adding invariant tests before deployment.
- The protocol has a single-key upgrade authority with no timelock. Post-deployment, consider migrating to a multisig.
- All findings above must be manually verified and PoC-tested before inclusion in a final report. AI analysis may miss context.

## semantic-gap-checks.md

# Semantic Gap Checks

Use this file for protocols with:

- rewards or accumulators
- checkpoints or snapshots
- cross-module accounting
- lending / vault / staking / liquidation state

This pass is designed to catch subtle state bugs that normal pattern scans miss.

## Gap Types

- `SYNC_GAP`: one accounting variable updates, its mirror does not
- `CONDITIONAL_SKIP`: checkpoint or accumulator updates only on one branch
- `ACCUMULATION_EXPOSURE`: time-weighted or rate-weighted state can be manipulated before snapshot
- `LIFECYCLE_GAP`: one module closes state while another still depends on it
- `DUAL_SOURCE_METRIC`: the same invariant is checked against different data sources

## Workflow

For each candidate gap:

1. identify the writer path
2. identify the skipped or missing sibling path
3. identify every consumer of the stale state
4. measure persistence: same tx / next tx / unbounded / permanent
5. quantify the wrong outcome with a numeric trace

If you cannot name both:

- the writer path
- the stale consumer path

do not escalate beyond `QUESTIONABLE`.

## High-Signal Targets

Always run this pass on lending, staking, reward, vault, liquidation, and oracle-heavy protocols.

Search especially for:

- `reward_per_share`
- `accumulator`
- `checkpoint`
- `last_update`
- `index`
- `total_debt`
- `borrow_amount`
- `cash_reserve`
- `emode`
- `claimable`

## Mandatory Checks

### Mirror Variable Consistency

If two variables represent the same economic concept at different scopes, verify
every path that changes one also changes the other.

### Conditional Checkpoint Writes

For every conditional write to a checkpoint or accumulator:

- can the false branch happen in production?
- who later reads the stale value?
- does that stale read affect debt, rewards, collateral, shares, or liquidation?

### Pre-Accrual Before Config Changes

Before changing:

- interest model
- fee rate
- reward rate
- liquidation threshold

verify accrued state is settled first.

### Cross-Module Cleanup Completeness

When an entity is repaid, liquidated, closed, or deleted, verify dependent modules
clean up trackers, rewards, dynamic fields, receipts, and checkpoints.

### Deviation Reference Freshness

If a deviation or sanity check exists, verify the reference point is:

- fresh
- not trivially admin-manipulable
- not updated only on a rare path

## Output Template

```md
### Gap Summary
- Gap Type: `...`
- Stale Variable(s): `...`
- Writer Path: `module::function`
- Consumer Path: `module::function`
- Persistence: same tx / next tx / unbounded / permanent

### Numeric Trace
1. Initial state: ...
2. Gap-creating action: ...
3. Stale read: ...
4. Wrong outcome: ...
```

## sui-patterns.md

# Sui Move — Security Patterns

Sui-specific vulnerability patterns. Load this when auditing any codebase that imports
`sui::object`, `sui::transfer`, or `sui::tx_context`.

---

## Sui Mental Model

Sui's object-centric model is fundamentally different from account-based Move (Aptos).
The key concepts that create unique attack surfaces:

- **Objects** are the primary unit of storage, not global storage at addresses
- **Ownership** is tracked by the Sui runtime: objects can be owned, shared, immutable, or wrapped
- **Shared objects** require consensus; owned objects don't
- **Capability pattern** is the primary access control mechanism
- **Witness pattern** is used for one-time type initialization

Misunderstanding any of these leads to exploitable vulnerabilities.

---

## SUI-01 — Object Ownership Confusion

**Description:** Functions that accept object references without verifying the caller owns them.

**Pattern:**
```move
// VULNERABLE — accepts any Coin object regardless of ownership
public entry fun deposit(pool: &mut Pool, coin: Coin<SUI>, ctx: &mut TxContext) {
    // No check that coin belongs to tx sender
    pool::add_liquidity(pool, coin);
}
```

**Risk:** In Sui, object ownership is enforced by the runtime at the transaction level —
you can't pass an owned object you don't own. However, shared objects and wrapped objects
can create confusion. Check for:

1. Shared objects where callers can manipulate state they shouldn't
2. Functions that accept `&mut T` on a shared object without validating caller permissions
3. Hot potato patterns (structs without `drop`) that can be passed between functions unexpectedly

**Check:** For every function accepting a mutable shared object, verify there is an explicit
permission/capability check before mutation.

---

## SUI-02 — Shared Object Reentrancy / State Inconsistency

**Description:** Shared objects accessed in a partially-updated state during a PTB (Programmable Transaction Block).

**Pattern:** A PTB calls `function_A` which partially updates shared object `S`, then calls
`function_B` which reads `S` before `function_A` completes its invariant restoration.

**Risk:** While Sui doesn't have EVM reentrancy, PTBs allow chaining of function calls.
If a shared object is left in inconsistent state mid-PTB, subsequent calls in the same
PTB can observe and exploit that state.

**Check:**
- Every function that modifies a shared object should leave it in a valid state after each call
- Watch for "unlock then use" patterns where the unlock and use happen in separate PTB steps
- Flash loan implementations must enforce that loans are repaid within the same PTB

---

## SUI-03 — Witness Pattern Abuse

**Description:** The witness pattern (`struct Witness has drop {}`) is used to prove type ownership at initialization. Bugs arise when witnesses can be created without the expected constraints.

**Pattern:**
```move
// VULNERABLE — witness struct is public and copyable
public struct MY_WITNESS has copy, drop {}

// Anyone can create a witness and call privileged functions
public fun create_with_witness(w: MY_WITNESS) { ... }
```

**Risk:** If the witness type has `copy`, anyone can call privileged initialization functions
multiple times or from unexpected modules.

**Check:**
1. One-Time Witness (OTW) structs must have the exact module name in ALL_CAPS
2. OTW structs must have only `drop` ability — never `copy` or `store`
3. OTW structs must be consumed (not referenced) in the privileged function
4. The `sui::types::is_one_time_witness` check should be used where applicable

---

## SUI-04 — Transfer to Wrong Owner

**Description:** Objects transferred to an attacker-controlled address due to missing sender validation.

**Pattern:**
```move
// VULNERABLE — recipient is user-supplied
public entry fun claim_reward(
    pool: &mut Pool,
    recipient: address,  // attacker-controlled!
    ctx: &mut TxContext
) {
    let reward = calculate_reward(pool);
    transfer::public_transfer(reward, recipient);
}
```

**Check:** Functions that transfer objects or coins to an address should validate that the
recipient is the transaction sender, or that the caller has explicit permission to specify
a different recipient.

---

## SUI-05 — Wrapping and Unwrapping Attacks

**Description:** Objects can be wrapped inside other objects and become inaccessible without being destroyed. Malicious actors can trap objects.

**Pattern:**
```move
// If an NFT can be wrapped into any arbitrary struct,
// a malicious contract could wrap it and never unwrap
public entry fun wrap_nft(nft: SomeNFT, wrapper: &mut MaliciousWrapper) {
    wrapper.trapped_nft = option::some(nft);
    // nft is now trapped — original owner loses access
}
```

**Risk:** Protocol-level object wrapping that doesn't have a guaranteed unwrap path.
Flash loans that wrap the collateral in a non-unwrappable struct.

**Check:**
- Any wrapping function should have a corresponding, accessible unwrapping function
- Objects that hold other objects must provide guaranteed extraction paths
- Flash loan implementations: verify the "repay" step unwraps any wrapped collateral

---

## SUI-06 — Dynamic Field Injection

**Description:** Dynamic fields allow attaching arbitrary data to objects at runtime.
If a shared object accepts dynamic field additions from any caller, attackers can
pollute the object's field namespace.

**Pattern:**
```move
// VULNERABLE — any caller can add fields to shared object
public entry fun add_metadata(
    obj: &mut SharedProtocolObject,
    key: String,
    value: String,
    ctx: &mut TxContext
) {
    dynamic_field::add(&mut obj.id, key, value);
}
```

**Risk:**
1. Field namespace collision (overwriting existing fields)
2. Storage bloat as an attack (adding thousands of fields)
3. Polluting protocol state with attacker-controlled data

**Check:**
- Dynamic field additions to shared objects should be permissioned
- Keys should be namespaced to prevent collisions
- Removal paths should exist to prevent permanent storage bloat

---

## SUI-07 — Clock / Epoch Oracle Manipulation

**Description:** Logic that relies on `sui::clock::Clock` for time-sensitive operations.

**Pattern:**
```move
// Auction with time-based mechanics
public entry fun place_bid(
    auction: &mut Auction,
    clock: &Clock,
    bid: Coin<SUI>,
    ctx: &mut TxContext
) {
    assert!(clock::timestamp_ms(clock) < auction.end_time, E_AUCTION_ENDED);
    // ...
}
```

**Risk:** Validators can influence block timestamps by small amounts (~few hundred ms).
Epoch boundaries can be predicted. Flash loan attacks can be constructed around epoch transitions.

**Check:**
1. Time windows shorter than 1000ms are potentially manipulable by validators
2. Logic at epoch boundaries (staking rewards, interest accrual) must handle the exact boundary case
3. Avoid `clock::timestamp_ms() == exact_value` checks — always use ranges
4. Flag any "last second" scenarios where timestamp manipulation gives economic benefit
5. **`epoch_timestamp_ms()` is NOT current time.** `tx_context::epoch_timestamp_ms(ctx)` returns the timestamp of when the current epoch started — it can be up to ~24h stale. Any protocol using it for time-sensitive logic (expiry checks, price freshness, liquidation windows, lock durations) instead of `clock::timestamp_ms(clock)` is using a stale value. Grep for `epoch_timestamp_ms` and verify it is never used where current time is needed — always use `sui::clock::Clock` for real-time timestamps

---

## SUI-08 — Capability Object Theft / Forgery

**Description:** Capability objects that can be created, copied, or obtained by unauthorized parties.

**Pattern:**
```move
// VULNERABLE — AdminCap can be minted by anyone
public fun create_admin_cap(): AdminCap {
    AdminCap { id: object::new(ctx) }
}
```

**Check:**
1. Capability creation should only happen in `init()` (called once at deployment)
2. Capability structs should never have `copy` ability
3. Capability transfer should be restricted — not `public_transfer`
4. Check if `TreasuryCap` (for coins) is properly stored and access-controlled

---

## SUI-09 — Hot Potato Misuse

**Description:** Hot potato structs (no abilities) must be consumed in the same PTB. Misuse creates DoS or loss of funds.

**Pattern:**
```move
struct HotPotato { value: u64 }  // no abilities

// If the function that creates HotPotato panics before the consuming function
// is called in the PTB, the user's transaction fails and any sent funds may be locked
```

**Check:**
1. Hot potato patterns in flash loans: verify both "take" and "return" functions work correctly
2. If a hot potato is created but the transaction aborts, verify no funds are lost
3. The "repay" path for hot-potato flash loans must be accessible in the same PTB

---

## SUI-10 — Event Spoofing

**Description:** Events emitted with attacker-controlled data that downstream off-chain systems trust.

**Risk:** If a protocol's off-chain infrastructure (indexers, bridges, relayers) trusts emitted events without verification, attackers can emit fake events to trigger off-chain actions.

**Check:**
- Events emitted from privileged operations should only be reachable through privileged paths
- Off-chain systems should verify on-chain state, not just events

---

## SUI-11 — Entry Modifier Visibility Bypass

**Description:** A `public(package) entry` function is callable directly from transactions, bypassing the intended package-only visibility. The `entry` modifier overrides `public(package)` restrictions for direct invocation.

**Pattern:**
```move
// VULNERABLE — entry modifier makes this callable by anyone via transaction
public(package) entry fun emergency_withdraw(
    v: &mut Vault,
    ctx: &TxContext
) {
    v.withdrawals = v.withdrawals + 1;
    v.admin = tx_context::sender(ctx); // attacker becomes admin!
}

// SAFE — without entry, only callable from within the package
public(package) fun emergency_withdraw_secure(
    v: &mut Vault,
    ctx: &TxContext
) {
    v.withdrawals = v.withdrawals + 1;
    v.admin = tx_context::sender(ctx);
}
```

**Check:**
1. Audit every `public(package) entry` function — the `entry` modifier means anyone can call it directly
2. If a function is meant to be package-internal only, remove the `entry` modifier
3. If the `entry` modifier is needed, add explicit authorization checks (admin/capability)

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — access_control_1*

---

## SUI-12 — Caller Address as Parameter (Spoofable Sender)

**Description:** Functions that accept a caller/sender address as a parameter instead of deriving it from `TxContext`. Anyone can pass any address.

**Pattern:**
```move
// VULNERABLE — caller address is user-supplied, anyone can claim to be admin
public fun withdraw_all(
    v: &mut Vault,
    caller: address,    // attacker passes v.admin address here
) {
    assert!(caller == v.admin, E_NOT_ADMIN);
    v.balance = 0;
}

// SAFE — derive sender from TxContext, cannot be spoofed
public fun withdraw_all(
    v: &mut Vault,
    ctx: &TxContext,
) {
    let caller = tx_context::sender(ctx);
    assert!(caller == v.admin, E_NOT_ADMIN);
    v.balance = 0;
}
```

**Check:**
1. Flag any function that accepts an `address` parameter used for authorization
2. Sender identity must always come from `tx_context::sender(ctx)`
3. Especially dangerous in `public` or `public(package)` functions

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — access_control_2*

---

## SUI-13 — Phantom Type Generic Role Bypass

**Description:** Role-based capability checks using generic type parameters instead of concrete types. A user holding `RoleCap<UserRole>` can pass it where `RoleCap<ModRole>` or `RoleCap<AdminRole>` was intended.

**Pattern:**
```move
public struct RoleCap<phantom R> has key { id: UID, owner: address }
public struct UserRole has drop {}
public struct AdminRole has drop {}

// VULNERABLE — generic R accepts ANY RoleCap, not just ModRole
public fun moderator_checkout_admin<R>(
    _cap: &RoleCap<R>,          // any user with RoleCap<UserRole> passes this
    ctx: &mut TxContext,
) {
    let admin_cap = RoleCap<AdminRole> { id: object::new(ctx), owner: tx_context::sender(ctx) };
    transfer::transfer(admin_cap, tx_context::sender(ctx));
}

// SAFE — concrete type enforces only ModRole holders can call
public fun moderator_checkout_admin(
    _cap: &RoleCap<ModRole>,    // only RoleCap<ModRole> accepted
    ctx: &mut TxContext,
) {
    let admin_cap = RoleCap<AdminRole> { id: object::new(ctx), owner: tx_context::sender(ctx) };
    transfer::transfer(admin_cap, tx_context::sender(ctx));
}
```

**Check:**
1. Flag any function with generic type parameter on capability structs (e.g., `<R>` in `RoleCap<R>`)
2. Role-gated functions must use concrete types, not generics
3. Verify that `sign_up` / public minting only creates the lowest-privilege capability

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — access_control_3*

---

## SUI-14 — Table Key Collision (Duplicate Key Abort)

**Description:** Using `table::add` without checking if the key already exists causes an abort on duplicate entries. This can DoS users trying to deposit/interact a second time.

**Pattern:**
```move
// VULNERABLE — aborts on second deposit for the same user
public fun deposit(bank: &mut Bank, user: address, amount: u64) {
    table::add(&mut bank.balances, user, amount); // aborts if key exists!
}

// SAFE — insert-or-update pattern
public fun deposit(bank: &mut Bank, user: address, amount: u64) {
    if (!table::contains(&bank.balances, user)) {
        table::add(&mut bank.balances, user, amount);
    } else {
        let bal = table::borrow_mut(&mut bank.balances, user);
        *bal = *bal + amount;
    }
}
```

**Check:**
1. Every `table::add` call must be preceded by a `table::contains` check or be guaranteed first-time-only
2. Same applies to `bag::add`, `object_bag::add`, `object_table::add`
3. Also check `table::remove` / `table::borrow` without existence checks — they abort on missing keys

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — tables_1*

---

## SUI-15 — Unbounded Iteration DoS

**Description:** Loops over vectors or tables with user-controlled size. Attackers can grow the data structure until iteration exceeds gas limits, bricking the function.

**Pattern:**
```move
// VULNERABLE — iterates over entire vector, size controlled by users
public fun reward_all(lb: &mut Leaderboard) {
    let len = vector::length(&lb.scores);
    let mut i = 0;
    while (i < len) {
        let s = vector::borrow_mut(&mut lb.scores, i);
        *s = *s + 1;
        i = i + 1;
    }
}

// SAFE — paginated iteration with bounded range
public fun reward_batch(lb: &mut Leaderboard, start: u64, count: u64) {
    let len = vector::length(&lb.scores);
    let end = math::min(start + count, len);
    let mut i = start;
    while (i < end) {
        let s = vector::borrow_mut(&mut lb.scores, i);
        *s = *s + 1;
        i = i + 1;
    }
}
```

**Check:**
1. Flag any `while` or loop that iterates over a vector/table whose size is user-controlled
2. Verify there are caps on how large user-controlled collections can grow
3. Prefer paginated/batched patterns for operations on unbounded collections
4. Check `vector::push_back` calls — is the vector size capped?

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — tables_2*

---

## SUI-16 — Timestamp Unit Confusion (ms vs seconds)

**Description:** `clock::timestamp_ms()` returns milliseconds but code compares it against constants defined in seconds (or vice versa), breaking time-based locks.

**Pattern:**
```move
const LOCK_TIME_SECONDS: u64 = 10 * 24 * 60 * 60; // 10 days in seconds

// VULNERABLE — stores ms/1000 in stake, but compares raw ms against seconds constant
public entry fun stake(state: &mut StakeState, clock: &Clock, _ctx: &mut TxContext) {
    state.seconds = clock::timestamp_ms(clock) / 1000;  // converted to seconds
    // ...
}

public entry fun unstake(state: &mut StakeState, clock: &Clock, _ctx: &mut TxContext) {
    let now = clock::timestamp_ms(clock);  // raw milliseconds!
    // BUG: comparing ms against (seconds + seconds) — lock is effectively instant
    if (now >= state.seconds + LOCK_TIME_SECONDS) {
        // unlocks immediately because now_ms >> saved_seconds + lock_seconds
    }
}

// SAFE — consistent units throughout
public entry fun unstake(state: &mut StakeState, clock: &Clock, _ctx: &mut TxContext) {
    let now_seconds = clock::timestamp_ms(clock) / 1000;
    if (now_seconds >= state.stake_time_seconds + LOCK_TIME_SECONDS) {
        // correct comparison: seconds vs seconds
    }
}
```

**Check:**
1. Every use of `clock::timestamp_ms()` — verify the result is used consistently (all ms or all seconds)
2. Check constant names vs actual units (e.g., `LOCK_TIME_SECONDS` used with ms values)
3. Flag mixed arithmetic: ms values compared/added to second values
4. Verify struct field names match the units stored in them

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — time_units*

---

## SUI-17 — Hot Potato State Reset (Nested Flash Loan Attack)

**Description:** Hot potato flash loan patterns where calling `start` multiple times resets the saved snapshot, or where `finish` doesn't actually enforce the return of funds.

**Pattern:**
```move
public fun start_harvest(vault: &mut Vault, ctx: &TxContext): HarvestOp {
    vault.saved_reserves = vault.reserves;  // snapshot resets each call!
    vault.operation_in_progress = true;
    HarvestOp {}
}

// VULNERABLE — finish accepts returned_amount as parameter but doesn't deposit it
public fun finish_harvest(vault: &mut Vault, op: HarvestOp, returned_amount: u64, ctx: &TxContext) {
    let required = vault.saved_reserves * MIN_RETURN_BPS / BPS_DENOM;
    assert!(returned_amount >= required, EInsufficientReturn);
    // BUG: returned_amount is just a number — no actual funds transferred back!
    vault.operation_in_progress = false;
    let HarvestOp {} = op;
}
```

**Attack flow:**
1. Call `start_harvest` → snapshot = 1000, withdraw 900
2. Call `start_harvest` again → snapshot resets to 100 (current reserves)
3. Call `finish_harvest` with `returned_amount = 98` → passes 98% check on 100
4. Attacker keeps 900, vault lost funds

**Check:**
1. `start` function must assert no operation is already in progress (`!operation_in_progress`)
2. Hot potato struct should store the snapshot amount, not the vault
3. `finish` must verify actual token balances, not trust a user-supplied amount parameter
4. Verify the hot potato cannot be created multiple times in the same PTB

*Source: [Monethic/sui-vuln-lab](https://github.com/Monethic/sui-vuln-lab) — hot_potato*

---

## SUI-18 — Missing Object / UID Validation

**Description:** Functions that accept Sui objects without validating their UID or origin. Attackers create their own instance of the same struct type with manipulated internal values.

**Pattern:**
```move
// VULNERABLE — no validation that bank is the legitimate protocol instance
public fun mint_shares(
    bank: &mut Bank,
    amount: u64,
    ctx: &mut TxContext
) {
    // Attacker creates a fake Bank with share_price = 1
    let shares = amount * PRECISION / bank.share_price;
    // Shares minted at manipulated price
}

// SAFE — validate object ID against a registry or known ID
public fun mint_shares(
    bank: &mut Bank,
    registry: &Registry,
    amount: u64,
    ctx: &mut TxContext
) {
    assert!(object::id(bank) == registry.bank_id, E_INVALID_BANK);
    let shares = amount * PRECISION / bank.share_price;
}
```

**Check:**
1. Functions accepting objects that determine prices, rates, or permissions — is the object ID validated?
2. Can an attacker create their own instance of a shared object type and pass it?
3. Functions referencing multiple objects — verify they belong to the same protocol instance
4. Especially critical for objects used as price sources or liquidity pools

*Real audit refs: Bluefin (no UID validation, forged BankV2 — Critical),
Kuna Labs (different SupplyPool instances referenced — High)*

---

## SUI-19 — Unconditional Balance Destruction

**Description:** Calling `balance::destroy_zero()` on a balance that may not be zero, permanently destroying remaining funds.

**Pattern:**
```move
// VULNERABLE — assumes remaining balance is zero after liquidation
public fun liquidate(position: &mut Position) {
    let seized = balance::split(&mut position.collateral, liquidation_amount);
    // ... transfer seized to liquidator ...

    let leftover = balance::withdraw_all(&mut position.collateral);
    balance::destroy_zero(leftover);
    // ABORTS if any collateral remains, or DESTROYS funds if called unsafely
}

// SAFE — handle non-zero remainder
public fun liquidate(position: &mut Position, ctx: &mut TxContext) {
    let seized = balance::split(&mut position.collateral, liquidation_amount);
    // ... transfer seized to liquidator ...

    let leftover = balance::withdraw_all(&mut position.collateral);
    if (balance::value(&leftover) > 0) {
        transfer::public_transfer(
            coin::from_balance(leftover, ctx),
            position.owner
        );
    } else {
        balance::destroy_zero(leftover);
    }
}
```

**Check:**
1. Every `balance::destroy_zero()` / `coin::destroy_zero()` — is the balance **guaranteed** to be zero?
2. Common in liquidation flows where remaining collateral may not be exactly zero
3. Also check `balance::split` where the split amount could exceed the balance (causes abort)
4. Partial liquidation: remainder must be returned to the position owner

*Real audit ref: Creek Finance (unconditional destroy_zero on non-zero balances during liquidation — High)*

---

## SUI-20 — Flash Loan Receipt Pool Binding

**Description:** Sui flash loan receipts (hot potatoes) that don't bind to their originating pool via `object::id`. This is the Sui-specific variant of the generic type confusion pattern (see `common-move.md` 8.1) — on Sui, the key check is validating the pool's `ID` inside the receipt.

**Pattern:**
```move
struct FlashReceipt { pool_id: ID, amount: u64 }

// VULNERABLE — doesn't verify receipt.pool_id matches this pool
public fun repay_flash_loan<T>(
    pool: &mut Pool<T>,
    receipt: FlashReceipt,
    payment: Coin<T>,
) {
    assert!(coin::value(&payment) >= receipt.amount, E_UNDERPAY);
    balance::join(&mut pool.reserve, coin::into_balance(payment));
    let FlashReceipt { pool_id: _, amount: _ } = receipt;
    // Attacker borrows from Pool A, repays to Pool B
}

// SAFE — validate receipt belongs to this specific pool via object::id
public fun repay_flash_loan<T>(
    pool: &mut Pool<T>,
    receipt: FlashReceipt,
    payment: Coin<T>,
) {
    assert!(receipt.pool_id == object::id(pool), E_WRONG_POOL);
    assert!(coin::value(&payment) >= receipt.amount, E_UNDERPAY);
    balance::join(&mut pool.reserve, coin::into_balance(payment));
    let FlashReceipt { pool_id: _, amount: _ } = receipt;
}
```

**Check:**
1. Receipt struct must store the originating pool's `ID` (set via `object::id(pool)` at borrow time)
2. Repay function must assert `receipt.pool_id == object::id(pool)`
3. Receipt must be consumed (destructured) — not just read by reference
4. Repayment amount must account for fees
5. Can the same receipt be used across different pools in a PTB?

*Real audit refs: Cetus (repay_flash_loan doesn't verify order_id — Critical),
Dexlyn (repay_flash_swap missing pool binding — Critical)*

---

## SUI-21 — Denylist Enforcement Awareness (Validator-Level + Epoch Gap)

**Description:** Sui's regulated coin denylist (`DenyCapV2`) is enforced at the **validator level during transaction input validation**, not in Move code. This creates two auditor-relevant behaviors:

1. **Sending is blocked instantly** — a blocked user cannot submit a transaction using their regulated coins as inputs. The tx fails before Move code runs.
2. **Receiving is only blocked at next epoch (~24hrs)** — a blocked user can still receive coins until the epoch changes.

**Risk:** The epoch gap for receiving creates a dangerous window in cross-chain scenarios. If tokens are burned on a source chain and minting is attempted on destination after epoch change, funds can be lost permanently.

```move
// FALSE POSITIVE — this is NOT a bypass
// A blocked user calling transfer directly will fail at the validator level
// before this Move code ever executes
public fun transfer_coins(coin: Coin<REGULATED>, recipient: address) {
    // No denylist check needed here — validators enforce it
    transfer::public_transfer(coin, recipient);
}

// REAL RISK — cross-chain bridge: burn on source, mint on destination
// If user is blocked between burn and mint, funds are lost
public fun bridge_mint(proof: BurnProof, recipient: address, ctx: &mut TxContext) {
    // recipient was allowed when burn happened on source chain
    // but may be blocked by the time this mints on Sui (epoch changed)
    let coin = coin::mint(&mut treasury, proof.amount, ctx);
    transfer::public_transfer(coin, recipient);  // recipient now blocked = stuck
}
```

**Check:**
1. Don't flag missing denylist checks in Move code — Sui enforces at runtime/validator level
2. For regulated coins: check if the protocol handles the ~24hr receiving gap
3. Cross-chain bridges: verify the protocol handles the case where a recipient becomes blocked between source burn and destination mint
4. Flag any protocol that assumes denylist blocking is instant for both sending AND receiving

*Refs: [Sui DenyCapV2 docs](https://docs.sui.io/references/framework/sui_sui/coin#sui_coin_DenyCapV2),
[deny_list_v2.rs source](https://github.com/MystenLabs/sui/blob/main/crates/sui-types/src/deny_list_v2.rs)*

---

## SUI-22 — Dependency Upgrade Version Contagion

**Description:** When a Sui package upgrades, it changes its object version. The old package's version check fails for updated objects, breaking all callers of the old package. If your protocol is immutable and calls a dependency that upgrades, every call through the old package path fails permanently.

**Pattern:**
```move
// Your immutable protocol calls DEX v1 for liquidations
public fun liquidate(position: &mut Position, pool: &mut dex_v1::Pool) {
    let proceeds = dex_v1::swap(pool, position.collateral);
    // Works fine... until DEX upgrades to v2
    // DEX v2 updates all Pool objects to version=2
    // dex_v1::swap() checks version==1, fails
    // ALL liquidations permanently bricked
}
```

**The contagion effect:**
- If Protocol A is upgradeable, Protocol B using A must also be upgradeable
- Protocol C using B must be upgradeable
- One upgrade at the bottom forces every protocol above to centralize
- Choosing immutability (for security) becomes maximum vulnerability in this model

**Check:**
1. For every external dependency: is it upgradeable? Does it use object version checks?
2. If the audited protocol is immutable and any dependency is upgradeable → flag as **Critical** (protocol can be permanently bricked by a dependency upgrade)
3. If the protocol is upgradeable: does it have a mechanism to update dependency calls after upstream upgrades?
4. Check for version-gated function calls in dependencies (`assert!(obj.version == CURRENT_VERSION)`)
5. Evaluate the full dependency tree — contagion can be multi-level

*Ref: [Move is not perfect: The Upgrade Trap](https://medium.com/@gfusee33/move-is-not-perfect-2-the-upgrade-trap-1d2857417e37)*

---

## SUI-23 — Shared Object Version Check (Upgrade Safety)

**Description:** Shared objects must carry a `version: u64` field so upgraded code can
reject stale layouts. Without version gating, upgraded functions operate on objects
with old field layouts, causing deserialization failures or silent data corruption.

**Pattern:**
```move
// VULNERABLE — shared object has no version field
struct Pool has key {
    id: UID,
    balance: Balance<SUI>,
    fee_bps: u64,
}

// After upgrade adds a new field, existing Pool objects lack it
// borrow_global / dynamic access silently reads garbage or aborts

// SAFE — version-gated shared object
const CURRENT_VERSION: u64 = 1;

struct Pool has key {
    id: UID,
    version: u64,
    balance: Balance<SUI>,
    fee_bps: u64,
}

public fun swap(pool: &mut Pool, input: Coin<SUI>): Coin<USDC> {
    assert!(pool.version == CURRENT_VERSION, E_WRONG_VERSION);
    // ...
}

// Migration function bumps version after upgrade
public fun migrate_pool(pool: &mut Pool, cap: &AdminCap) {
    assert!(pool.version == CURRENT_VERSION - 1, E_ALREADY_MIGRATED);
    pool.version = CURRENT_VERSION;
}
```

**Check:**
1. Every shared object struct must have a `version: u64` field
2. Every `public` function taking `&mut SharedObj` must assert `obj.version == CURRENT_VERSION`
3. A migration function must exist to bump versions post-upgrade
4. Cross-ref: SUI-22 (dependency upgrade trap)

---

## SUI-24 — Publisher Object Not Secured

**Description:** The `Publisher` object (from `sui::package`) proves package authorship
and enables creating `Display` objects, claiming type ownership, and configuring
transfer policies. If not transferred to admin/governance in `init`, anyone with
access can spoof metadata or bypass royalties.

**Pattern:**
```move
// VULNERABLE — Publisher left as owned object, transferred to deployer without protection
fun init(otw: MY_MODULE, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    transfer::public_transfer(publisher, tx_context::sender(ctx));
    // Deployer's wallet key = single point of failure
}

// SAFE — Publisher stored in a governed wrapper or destroyed if unneeded
fun init(otw: MY_MODULE, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    // Option A: wrap in admin-gated object
    let gov = GovernedPublisher { id: object::new(ctx), publisher };
    transfer::share_object(gov);
    // Option B: if Display is already set up and Publisher isn't needed
    // package::burn_publisher(publisher);
}
```

**Check:**
1. Is `Publisher` transferred to a secure multisig/governance address?
2. If stored as owned object — is key compromise considered?
3. If `Publisher` is not needed post-init, is it burned via `package::burn_publisher`?

---

## SUI-25 — Dynamic Field Cleanup Before Object Deletion

**Description:** Dynamic fields attached to an object are NOT automatically removed
when the parent UID is deleted. Values in orphaned dynamic fields become permanently
inaccessible — causing permanent fund loss if they hold `Balance<T>` or `Coin<T>`.

**Pattern:**
```move
// VULNERABLE — deletes UID with dynamic fields still attached
public fun close_vault(vault: Vault) {
    let Vault { id, owner: _ } = vault;
    // dynamic_field holding Balance<SUI> is now orphaned forever
    object::delete(id);
}

// SAFE — remove all dynamic fields before deletion
public fun close_vault(vault: Vault): Balance<SUI> {
    let Vault { id, owner: _ } = vault;
    let balance = dynamic_field::remove<String, Balance<SUI>>(&mut id, b"funds".to_string());
    // Remove ALL other dynamic fields...
    object::delete(id);
    balance
}
```

**Check:**
1. Before any `object::delete(uid)`, verify ALL dynamic fields/objects are removed
2. If the set of dynamic field keys is unbounded, deletion may be impossible — flag as design risk
3. Check for `Balance<T>`, `Coin<T>`, or any value type with `store` in dynamic fields
4. Cross-ref: SUI-06 (dynamic field injection)

---

## SUI-26 — Kiosk Transfer Policy Bypass

**Description:** NFTs in a `Kiosk` are protected by `TransferPolicy` rules (royalties,
allowlist checks). If the `KioskOwnerCap` is not properly secured, or if `purchase`
is called without enforcing all policy rules, NFTs can be extracted without paying
royalties or passing allowlist checks.

**Pattern:**
```move
// VULNERABLE — KioskOwnerCap freely transferable, bypass via self-purchase
fun init(ctx: &mut TxContext) {
    let (kiosk, cap) = kiosk::new(ctx);
    transfer::public_share_object(kiosk);
    transfer::public_transfer(cap, tx_context::sender(ctx));
    // cap holder can list at 0 price and self-purchase, skipping royalty
}

// SAFE — cap stored securely, TransferPolicy enforced
fun init(ctx: &mut TxContext) {
    let (kiosk, cap) = kiosk::new(ctx);
    transfer::public_share_object(kiosk);
    // Store cap in governed wrapper — not directly transferable
    let gov = GovernedKiosk { id: object::new(ctx), cap };
    transfer::share_object(gov);
}
```

**Check:**
1. Is `KioskOwnerCap` stored securely (not freely transferable)?
2. Are ALL `TransferPolicy` rules enforced on every extraction path?
3. Can owner list at price 0 and self-purchase to bypass royalties?
4. Check `kiosk::list` and `kiosk::purchase` call patterns

---

## SUI-27 — UpgradeCap Lifecycle Mismanagement

**Description:** Two opposite risks: (a) `UpgradeCap` destroyed prematurely via
`sui::package::make_immutable` — makes the package permanently immutable before
critical bugs can be fixed; (b) upgrade policy is more permissive than necessary
(`compatible` when `additive_only` or `dep_only` suffices), allowing dangerous
changes to function signatures and struct layouts.

**Pattern:**
```move
// RISK A — premature immutability
fun init(ctx: &mut TxContext) {
    // Package can never be fixed if a critical bug is found
    package::make_immutable(upgrade_cap);
}

// RISK B — overly permissive upgrade policy
fun init(ctx: &mut TxContext) {
    // `compatible` allows changing function bodies + adding functions
    // Could weaken security checks in existing functions
    transfer::public_transfer(upgrade_cap, tx_context::sender(ctx));
}

// SAFE — restrict to minimum required policy, held by governance
fun init(ctx: &mut TxContext) {
    // Restrict to additive-only: can add new functions but not change existing ones
    package::only_additive_upgrades(&mut upgrade_cap);
    // Or even stricter: only dependency changes
    // package::only_dep_upgrades(&mut upgrade_cap);
    transfer::public_transfer(upgrade_cap, @governance_multisig);
}
```

**Check:**
1. Is `UpgradeCap` held by multisig/governance (not a single EOA)?
2. Is the upgrade policy the minimum required (`dep_only` > `additive_only` > `compatible`)?
3. If `make_immutable` is called — is the protocol mature enough? Are all dependencies also immutable?
4. Cross-ref: SUI-22 (immutable + upgradeable dep = bricking risk)

---

## SUI-28 — PTB Repeated Call Limit Bypass

**Description:** Sui PTBs (Programmable Transaction Blocks) allow calling the same function multiple times against the same shared object in a single atomic transaction. Per-call limits (close factors, rate limits, cooldowns) can be bypassed by calling the function N times, where each call re-reads the updated state and gets a fresh allowance.

**Pattern:**
```move
// VULNERABLE — close factor checked per-call, not per-transaction
public fun liquidate(market: &mut Market, obligation_id: ID, repay_amount: u64) {
    let debt = market.obligation(obligation_id).debt();
    let max_repay = debt * close_factor; // recalculated on CURRENT debt
    assert!(repay_amount <= max_repay, E_CLOSE_FACTOR_EXCEEDED);
    // ... execute liquidation, reduce debt ...
}
// Attacker calls liquidate() 5x in one PTB: each call gets 50% of REMAINING debt
// Total: 50% + 25% + 12.5% + 6.25% + 3.125% = 96.875% liquidated
```

**Risk:** Any per-call numeric limit becomes meaningless if the function can be called repeatedly in the same PTB with state persisting between calls. This is unique to Sui's PTB model — on EVM, each tx is independent.

**Check:**
1. For every function with a per-call numeric limit (close factor, max withdrawal, rate limit), verify the limit is tracked per-TRANSACTION, not per-call
2. Look for patterns where: (a) a limit is checked against current state, (b) state is modified to reduce the denominator, (c) no flag prevents re-invocation in the same PTB
3. Common vulnerable patterns: liquidation close factors, withdrawal rate limits, flash loan stacking across different assets, reward claim limits
4. Fix patterns: (a) store original state in a hot-potato that persists across calls, (b) set a per-obligation/per-asset flag that prevents repeated operations, (c) track cumulative amounts via a transaction-scoped accumulator

**Real-World Example:** CurrentSUI lending protocol — close factor of 50% bypassed via 3 liquidation calls in one PTB, achieving 87.5% total liquidation. Position pushed from recoverable to bad debt.

---

## SUI-29 — Time-Lock Window State Guarantees

**Description:** Delayed/time-locked transfer wrappers where the observation window does not guarantee state immutability or binding commitment. Two distinct sub-patterns:

**A) Mutation during delay window:**
The wrapper owner can `borrow_mut` and modify the inner object while a transfer is pending. The new owner receives a different object than what was visible when the transfer was scheduled.

```move
// INFORMATIONAL — owner can mutate inner object during pending transfer
public fun borrow_mut<T: key + store>(self: &mut DelayedWrapper<T>): &mut T {
    // No check whether a transfer is pending — owner can change inner state
    &mut self.obj
}

// SAFER — restrict mutation during active transfer
public fun borrow_mut<T: key + store>(self: &mut DelayedWrapper<T>): &mut T {
    assert!(!self.transfer_pending, EMutationDuringTransfer);
    &mut self.obj
}
```

**B) Cancellation after deadline:**
The owner can `cancel_schedule` even after the delay has fully elapsed, as long as `execute_transfer` hasn't been called. The delay provides observation time but not a binding commitment — observers cannot rely on the transfer completing.

```move
// INFORMATIONAL — cancel works even after deadline passed
public fun cancel_schedule<T: key + store>(self: &mut DelayedWrapper<T>) {
    // No check: clock::timestamp_ms(clock) < self.deadline
    self.transfer_pending = false;
    self.new_owner = @0x0;
}
```

**Check:**
1. Does the delay wrapper allow mutable access to the inner object during a pending transfer/unwrap? If yes, flag as Informational
2. Can a scheduled operation be cancelled after the deadline passes? If yes, flag as Informational — the delay is observation-only
3. Check if this behavior is documented — if documented, keep at Info; if not, upgrade to Low

**Severity guidance:** Both patterns are often intentional design choices (owner retains full custody until transfer executes). Do NOT classify above Low unless there is no documentation and downstream protocols depend on the commitment guarantee.

---

## SUI-30 — VecMap/VecSet for Unbounded Collections

**Description:** `VecMap` and `VecSet` use linear scans for every lookup, insertion, and removal — O(n) per operation. When the collection size is driven by user actions (deposits, registrations, listings), an attacker can grow it until operations exceed gas limits.

**Pattern:**
```move
// VULNERABLE — roles/balances can grow unboundedly as users are added
public struct GlobalState has key {
    id: UID,
    user_roles: VecMap<address, vector<u8>>,  // O(n) on every lookup
    user_balances: VecMap<address, u64>,       // O(n) on every deposit
}

// SAFE — Table provides O(1) lookups via dynamic fields
public struct GlobalState has key {
    id: UID,
    user_roles: Table<address, vector<u8>>,
    user_balances: Table<address, u64>,
}
```

**Rule of thumb:** VecMap/VecSet is fine for admin-bounded collections (<100 entries, e.g., supported token list). Use `Table`/`ObjectTable` for any collection populated by user actions.

**Check:**
1. Grep for `VecMap`, `VecSet` usage in shared objects
2. Trace who adds entries — if any public/entry function grows the collection → flag
3. Is there a max-size cap enforced? If not → DoS at ~1,000+ entries
4. Cross-ref: SUI-15 (unbounded iteration is a sub-problem of this)

---

## SUI-31 — Shared Object Contention (Excessive Mutable Access)

**Description:** Shared objects that require `&mut` access for read-only or query operations. Every `&mut` reference on a shared object forces consensus ordering (sequential execution), creating a bottleneck. High-TPS protocols with `&mut` on common paths become unusable under load.

**Pattern:**
```move
// VULNERABLE — read-only operation takes &mut, forces consensus ordering
public fun check_balance<T>(state: &mut GlobalState<T>, addr: address): u64 {
    *table::borrow(&state.balances, addr)
}

// VULNERABLE — emitting an event doesn't need &mut
public fun get_price(oracle: &mut Oracle): u64 {
    event::emit(PriceQueried { price: oracle.price });
    oracle.price
}

// SAFE — immutable reference allows parallel execution
public fun check_balance<T>(state: &GlobalState<T>, addr: address): u64 {
    *table::borrow(&state.balances, addr)
}

public fun get_price(oracle: &Oracle): u64 {
    oracle.price
}
```

**Check:**
1. For each shared object, count `&mut` vs `&` references across all public functions
2. If the `&mut:&` ratio exceeds 2:1 in non-admin functions → flag as design concern
3. Functions that only read state but take `&mut` → flag (should use `&`)
4. High-TPS paths (swaps, price queries, balance checks) must use `&` wherever possible

---

## SUI-32 — Blind Transfer Without Receive Logic

**Description:** `transfer::transfer` sends an object to another object's address (via `to_address()`), but the target type has no `receive` function. The transferred object becomes permanently inaccessible — it exists on-chain but cannot be extracted.

**Pattern:**
```move
// VULNERABLE — sends reward to a GameCharacter object, but GameCharacter has no receive fn
public fun send_reward(character_id: ID, reward: Reward) {
    transfer::transfer(reward, character_id.to_address());
    // reward is now stuck — GameCharacter has no way to receive it
}

// SAFE — target type implements receive logic
public fun send_reward(character_id: ID, reward: Reward) {
    transfer::transfer(reward, character_id.to_address());
}

// In the GameCharacter module:
public fun receive_reward(
    character: &mut GameCharacter,
    reward: Receiving<Reward>
): Reward {
    transfer::receive(&mut character.id, reward)
}
```

**Check:**
1. Grep for `transfer::transfer` or `transfer::public_transfer` where the recipient is `*.to_address()` or `object::id_to_address()`
2. For each such transfer: does the target type's module have a corresponding `transfer::receive` function?
3. If no receive function exists → flag as High (permanent fund/object loss)
4. Also check: does the receive function have appropriate access control?

---

## SUI-33 — Using `address` Type Where `ID` Should Be Used

**Description:** Struct fields that store object references as `address` instead of `ID`. The `address` type provides no compile-time safety that the value actually refers to an object, allows mixing up user addresses with object IDs, and loses the ability to use `object::id`-based comparisons.

**Pattern:**
```move
// VULNERABLE — pool_ref is address, could be a user address or garbage
public struct Position has key {
    id: UID,
    pool_ref: address,       // is this an object ID or a user address?
    collateral_ref: address, // same ambiguity
    owner: address,          // this one IS a user address
}

// SAFE — ID type enforces this is an object reference
public struct Position has key {
    id: UID,
    pool_id: ID,          // clearly an object reference
    collateral_id: ID,    // clearly an object reference
    owner: address,       // clearly a user address
}
```

**Check:**
1. Grep for `address` fields in structs where the field name contains `id`, `object`, `pool`, `nft`, `cap`, `registry`, `vault`
2. If the field is used with `object::id_to_address()` or compared against object IDs → should be `ID` type
3. Exception: fields that genuinely store user/sender addresses (owner, recipient, admin) should remain `address`

---

## SUI-34 — Internal Transfer Instead of Returning Object

**Description:** Functions that create an object and immediately `transfer::transfer` or `transfer::share_object` it internally, instead of returning the object. This prevents callers from composing the result in PTBs.

**Pattern:**
```move
// VULNERABLE — not composable, caller cannot use the Movie in the same PTB
public fun create_movie(_: &AdminCap, title: String, ctx: &mut TxContext) {
    let movie = Movie { id: object::new(ctx), title };
    transfer::share_object(movie);  // locked inside the function
}

// SAFE — return the object, let the caller decide what to do with it
public fun create_movie(_: &AdminCap, title: String, ctx: &mut TxContext): Movie {
    Movie { id: object::new(ctx), title }
}

// Caller composes in PTB:
// let movie = create_movie(cap, title);
// add_metadata(movie, ...);
// transfer::share_object(movie);
```

**Check:**
1. Grep for `transfer::transfer`, `transfer::public_transfer`, `transfer::share_object` inside non-`init` functions
2. If the transferred object was created in the same function → flag (should return instead)
3. Exception: `init()` functions are expected to transfer/share objects directly
4. Exception: Functions where the transfer IS the core purpose (e.g., `send_to_recipient`)

---

## SUI-35 — Batch Function Instead of PTB Loop

**Description:** Functions that accept parallel vectors (`amounts: vector<u64>, recipients: vector<address>`) for batch processing. On Sui, PTBs natively handle batching — callers can call a single-item function N times in one PTB. Batch functions add code complexity, vector length mismatch risks, and gas estimation difficulties.

**Pattern:**
```move
// ANTI-PATTERN — custom batch logic with parallel vectors
public fun mint_batch(
    cap: &MintCap,
    amounts: vector<u64>,
    recipients: vector<address>,
    ctx: &mut TxContext
) {
    let len = vector::length(&amounts);
    assert!(len == vector::length(&recipients), EMismatch);
    let mut i = 0;
    while (i < len) {
        let coin = coin::mint(cap, *vector::borrow(&amounts, i), ctx);
        transfer::public_transfer(coin, *vector::borrow(&recipients, i));
        i = i + 1;
    }
}

// PREFERRED — single-item function, caller uses PTB for batching
public fun mint_one(
    cap: &MintCap,
    amount: u64,
    recipient: address,
    ctx: &mut TxContext
) {
    let coin = coin::mint(cap, amount, ctx);
    transfer::public_transfer(coin, recipient);
}
// PTB: call mint_one N times with different args — same atomicity guarantee
```

**Check:**
1. Flag functions accepting parallel vectors (`vector<u64>` + `vector<address>`) with internal loops
2. Suggest refactoring to single-item function that callers invoke via PTB
3. Exception: operations where atomicity across all items is required (all-or-nothing batch)
4. Severity: Low (design/composability issue, not a direct vulnerability)

---

## SUI-36 — Solidity-Style Auth Patterns Instead of Capabilities

**Description:** Using `VecMap<address, role>` or similar address-based role mappings for access control instead of Move's native capability objects. This is a Solidity anti-pattern transplanted into Move — it's less secure (relies on address comparison), creates unbounded storage (VecMap grows), and misses Move's type-level access control guarantees.

**Pattern:**
```move
// ANTI-PATTERN — Solidity-style role mapping
public struct GlobalCap<phantom T> has key {
    id: UID,
    roles: VecMap<address, vector<u8>>,  // address → role bytes
}

public fun check_role<T>(cap: &GlobalCap<T>, addr: address, role: vector<u8>): bool {
    let r = vec_map::get(&cap.roles, &addr);
    *r == role
}

// PREFERRED — Move capability pattern
public struct OperatorCap has key, store { id: UID }
public struct AdminCap has key { id: UID }

public fun operate(_: &OperatorCap, state: &mut State) { /* ... */ }
public fun admin_action(_: &AdminCap, state: &mut State) { /* ... */ }
```

**Check:**
1. Flag structs with `VecMap<address, ...>` or `Table<address, ...>` fields used for role/permission tracking
2. The Move capability pattern (distinct struct per role, pass by reference) is strictly superior:
   - Type-safe (compiler enforces correct cap type)
   - O(1) (no map lookup)
   - Cannot be forged (no `copy` ability)
   - Revocable (delete the cap object)
3. Severity: Medium (security anti-pattern) if the role map is the primary auth mechanism; Low if supplementary

---

## SUI-37 — Framework Type Name Shadowing

**Description:** Defining types with names that shadow well-known Sui framework types: `CoinMetadata`, `TreasuryCap`, `Publisher`, `Display`, `TransferPolicy`, `Kiosk`, `UpgradeCap`. Integrators or downstream code may reference the wrong type.

**Pattern:**
```move
// DANGEROUS — shadows sui::coin::TreasuryCap
public struct TreasuryCap has key {
    id: UID,
    supply: u64,
}

// DANGEROUS — shadows sui::coin::CoinMetadata
public struct CoinMetadata has key {
    id: UID,
    name: String,
}

// SAFE — use distinct names
public struct TokenTreasury has key {
    id: UID,
    supply: u64,
}
```

**Check:**
1. Grep for struct names matching: `CoinMetadata`, `TreasuryCap`, `Publisher`, `Display`, `TransferPolicy`, `Kiosk`, `KioskOwnerCap`, `UpgradeCap`
2. If any of these are defined in user code (not imported from `sui::*`) → flag
3. Severity: Medium if the shadowing causes functional confusion; Low otherwise

---

## SUI-38 — Metadata/Display Frozen Before Required Fields Set

**Description:** Calling `transfer::public_freeze_object` on a `Display` or metadata object before all required fields (especially `icon_url`, `image_url`, `project_url`) are set. Once frozen, the object is permanently immutable — missing fields cannot be added.

**Pattern:**
```move
// VULNERABLE — freezes Display before setting icon_url
fun init(otw: MY_NFT, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    let mut display = display::new<MyNFT>(&publisher, ctx);
    display::add(&mut display, string::utf8(b"name"), string::utf8(b"{name}"));
    display::add(&mut display, string::utf8(b"description"), string::utf8(b"{description}"));
    // Missing: icon_url, image_url, project_url
    display::update_version(&mut display);
    transfer::public_freeze_object(display);  // permanently locked without icon
    transfer::public_transfer(publisher, tx_context::sender(ctx));
}

// SAFE — all fields set before freeze, or Display kept mutable via publisher
fun init(otw: MY_NFT, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    let mut display = display::new<MyNFT>(&publisher, ctx);
    display::add(&mut display, string::utf8(b"name"), string::utf8(b"{name}"));
    display::add(&mut display, string::utf8(b"description"), string::utf8(b"{description}"));
    display::add(&mut display, string::utf8(b"image_url"), string::utf8(b"{image_url}"));
    display::add(&mut display, string::utf8(b"project_url"), string::utf8(b"https://example.com"));
    display::update_version(&mut display);
    transfer::public_transfer(display, tx_context::sender(ctx));  // kept mutable
}
```

**Check:**
1. Find all `transfer::public_freeze_object` calls on Display objects
2. Verify all standard fields are set before freeze: `name`, `description`, `image_url`, `project_url`
3. If any fields missing before freeze → flag as Medium (irreversible)
4. If protocol needs to update Display later (e.g., for metadata changes), freezing is premature

---

## SUI-39 — Multiple Publisher Objects

**Description:** Multiple modules in the same package each call `package::claim` to create separate `Publisher` objects. This splits package authority across multiple objects, making governance harder and creating confusion about which `Publisher` controls which `Display` or `TransferPolicy`.

**Pattern:**
```move
// ANTI-PATTERN — two modules in same package each claim Publisher
// module_a.move
fun init(otw: MODULE_A, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    transfer::public_transfer(publisher, tx_context::sender(ctx));
}

// module_b.move
fun init(otw: MODULE_B, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    transfer::public_transfer(publisher, tx_context::sender(ctx));
}

// PREFERRED — single Publisher, set up Display for each type
// main.move
fun init(otw: MAIN, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);
    let display_a = display::new<TypeA>(&publisher, ctx);
    let display_b = display::new<TypeB>(&publisher, ctx);
    // ... set up both displays ...
    transfer::public_transfer(publisher, tx_context::sender(ctx));
}
```

**Check:**
1. Count `package::claim` calls across all modules in the package
2. If more than one → flag as Low (governance complexity)
3. Cross-ref: SUI-24 (Publisher security)

---

## SUI-40 — Unnecessary `public(package)` Visibility

**Description:** Functions declared `public(package)` that are only called within their own module. The broader visibility unnecessarily expands the attack surface — any future module added to the package can call these functions.

**Check:**
1. For each `public(package)` function, search all other modules in the package for calls to it
2. If no cross-module callers exist → flag as Low (should be `fun` / private)
3. Multi-package caveat: if the project has multiple packages where one depends on another, verify cross-package calls before flagging
4. Severity: Low (attack surface reduction, not a direct vulnerability)

---

## SUI-41 — NFT Stores Constant Fields (Use Display)

**Description:** NFT structs that store per-collection constants (name, description, project_url) in every instance. These fields are identical for every NFT — storing them wastes gas on mint and bloats on-chain storage. Sui's `Display` object handles collection-level metadata via templates.

**Pattern:**
```move
// ANTI-PATTERN — every NFT stores identical collection metadata
public struct MyNFT has key, store {
    id: UID,
    name: String,           // same for every NFT in collection
    description: String,    // same for every NFT in collection
    project_url: String,    // same for every NFT in collection
    serial_number: u64,     // this IS per-instance — keep it
}

// PREFERRED — only per-instance fields in struct, constants in Display template
public struct MyNFT has key, store {
    id: UID,
    serial_number: u64,
    image_url: String,  // per-instance
}
// Display template: "name" → "MyCollection #{serial_number}"
// Display template: "project_url" → "https://example.com"
```

**Check:**
1. Look for NFT structs with fields like `name`, `description`, `project_url`, `collection_name` that are set to the same value on every mint
2. If a field is constant across all instances → should be in Display template, not the struct
3. Severity: Low (gas inefficiency, not a security issue)

---

## SUI-42 — Migration Function in Non-Upgraded Package

**Description:** `migrate` or `migration` functions present in a v1 package that has never been upgraded. These functions are dead code — they cannot be called meaningfully because no version transition has occurred.

**Pattern:**
```move
// In a v1 package that has never been upgraded:
const VERSION: u64 = 1;

// DEAD CODE — no migration from v0 exists, this function is meaningless
public fun migrate(state: &mut State, cap: &AdminCap) {
    assert!(state.version == VERSION - 1, EWrongVersion);  // VERSION - 1 = 0, but no v0 exists
    state.version = VERSION;
}
```

**Check:**
1. If the package VERSION constant is 1 and the package has no upgrade history → flag migration functions as dead code
2. Severity: Low (unnecessary code complexity)
3. If the migration function has a bug (e.g., wrong version math), note it as Info

---

## SUI-43 — Transaction Digest / UID / Epoch Used as Randomness

**Description:** `tx_context::digest()` returns the 32-byte transaction hash, which developers sometimes use as a randomness source for lotteries, NFT trait generation, or game outcomes. This is **not random** — the digest is deterministic and known to the transaction sender before execution. Validators can also reorder or exclude transactions to influence outcomes. The same applies to `object::id()` bytes (derived from digest + counter) and `epoch()` / `epoch_timestamp_ms()` (coarse and publicly known).

**Pattern:**
```move
// VULNERABLE — digest is deterministic, sender knows it before execution
public entry fun mint_random_nft(ctx: &mut TxContext) {
    let digest = tx_context::digest(ctx);
    let rarity = (*vector::borrow(digest, 0) as u64) % 100;
    // Attacker simulates tx locally, only submits if rarity > 95
    mint_with_rarity(rarity, ctx);
}

// VULNERABLE — UID bytes are derived from digest, equally predictable
public entry fun lottery_draw(pool: &mut Pool, ctx: &mut TxContext) {
    let ticket = object::new(ctx);
    let id_bytes = object::uid_to_bytes(&ticket);
    let winner_index = (*vector::borrow(&id_bytes, 0) as u64) % pool.participants;
    // Predictable — attacker controls when to enter
    object::delete(ticket);
}

// VULNERABLE — epoch/timestamp are publicly known, trivially predictable
public entry fun daily_reward(state: &mut State, ctx: &TxContext) {
    let seed = tx_context::epoch(ctx) + tx_context::epoch_timestamp_ms(ctx);
    let reward_tier = seed % 5;
    // Every user in the same epoch gets the same "random" tier
}

// SAFE — use Sui's on-chain VRF (sui::random, available since v1.22)
public entry fun mint_random_nft(r: &Random, ctx: &mut TxContext) {
    let gen = random::new_generator(r, ctx);
    let rarity = random::generate_u64_in_range(&mut gen, 0, 99);
    mint_with_rarity(rarity, ctx);
}
```

**Check:**
1. Grep for `tx_context::digest` — any usage for randomness, trait generation, shuffling, or outcome selection is Critical in gaming/lottery, High in NFT minting
2. Grep for `object::uid_to_bytes` or `object::id_to_bytes` used in modulo/math for selection — same predictability issue
3. Grep for `epoch()` or `epoch_timestamp_ms()` used as seeds or selection input — all publicly known values
4. Safe alternative: `sui::random::Random` shared object passed as `r: &Random`, used with `random::new_generator` — this is Sui's on-chain VRF backed by threshold cryptography
5. If the protocol predates `sui::random` (pre-v1.22), flag the use of digest-based randomness and recommend migration

*Cross-ref: APT-20 (Aptos randomness bias via test-and-abort / undergasing — different mechanism, same bug class)*

---

## SUI-44 — `swap_remove` Silent Index Reordering

**Description:** `table_vec::swap_remove` and `vector::swap_remove` delete an element at index `i` by swapping it with the last element and popping the end. This is O(1) — no shifting — but it **silently changes the position of the last element** without any abort or warning. If contract logic assigns meaning to index positions (insertion order, priority, rank, queue position), `swap_remove` corrupts that ordering.

**Pattern:**
```move
// VULNERABLE — contract logic depends on insertion order
struct DepositQueue has key {
    id: UID,
    depositors: TableVec<address>,  // "first 3 depositors get bonus"
}

public entry fun remove_depositor(queue: &mut DepositQueue, index: u64) {
    table_vec::swap_remove(&mut queue.depositors, index);
    // If index 1 removed from [alice, bob, charlie, dave, eve]:
    //   eve moves to index 1 → [alice, eve, charlie, dave]
    //   eve now qualifies for "first 3" bonus she shouldn't get
    //   bob lost his position silently
}

// VULNERABLE — withdrawal processing assumes FIFO order
public fun process_next_withdrawal(queue: &mut WithdrawQueue): address {
    let next = *table_vec::borrow(&queue.pending, 0);
    table_vec::swap_remove(&mut queue.pending, 0);
    // Last element jumped to front — FIFO order broken
    next
}

// SAFE — order doesn't matter, just membership
struct Whitelist has key {
    id: UID,
    addresses: TableVec<address>,  // bag of addresses, no ordering semantics
}

public entry fun remove_from_whitelist(list: &mut Whitelist, index: u64) {
    table_vec::swap_remove(&mut list.addresses, index);  // fine — order is irrelevant
}
```

**Check:**
1. Grep for `swap_remove` in all modules — both `table_vec::swap_remove` and `vector::swap_remove`
2. For each usage: does the containing data structure have **index-dependent semantics**? (queues, priority lists, ranked depositors, ordered processing, "first N get X" logic)
3. If index order carries meaning → flag as Medium (silent logic corruption) or High (if it affects fund distribution or priority)
4. If the data structure is used as a set/bag where order is irrelevant → safe, no finding
5. Rule of thumb: if you'd be fine storing it in a hash set, `swap_remove` is safe. If index position matters, it's a bug.

---

## Sui Verification Checklist

- [ ] All shared object mutations are permission-gated
- [ ] No OTW structs with `copy` ability
- [ ] No unconstrained transfer-to-address functions
- [ ] All wrapped objects have guaranteed unwrap paths
- [ ] Dynamic field additions to shared objects are permissioned
- [ ] Time-sensitive logic uses >1000ms windows
- [ ] Capability creation only in `init()`
- [ ] Hot potato flash loans tested for abort-safety
- [ ] TreasuryCap access-controlled
- [ ] Events not trusted as primary source of truth by critical systems
- [ ] No `public(package) entry` functions without explicit auth checks (SUI-11)
- [ ] No address parameters used for authorization — sender derived from TxContext (SUI-12)
- [ ] No generic type params on capability-gated functions — concrete types only (SUI-13)
- [ ] All `table::add` / `bag::add` calls guarded by existence checks (SUI-14)
- [ ] No unbounded loops over user-controlled vectors/tables (SUI-15)
- [ ] Timestamp units consistent — no ms/seconds mixing (SUI-16)
- [ ] Hot potato `start` functions assert no operation already in progress (SUI-17)
- [ ] Objects used for pricing/permissions validated by UID against registry (SUI-18)
- [ ] No unconditional `balance::destroy_zero()` — check value > 0 first (SUI-19)
- [ ] Flash loan receipts validated against originating pool before repayment (SUI-20)
- [ ] No false-positive denylist findings — enforcement is validator-level, not Move code; check epoch gap for receiving (SUI-21)
- [ ] All external dependencies checked for upgradeability — immutable protocol + upgradeable dep = bricking risk (SUI-22)
- [ ] All shared objects have `version: u64` field; all public functions assert version (SUI-23)
- [ ] `Publisher` object transferred to governance or burned post-init (SUI-24)
- [ ] All dynamic fields removed before `object::delete(uid)` — no orphaned balances (SUI-25)
- [ ] `KioskOwnerCap` stored securely; `TransferPolicy` rules enforced on every extraction (SUI-26)
- [ ] `UpgradeCap` held by governance with minimum-required policy; premature immutability flagged (SUI-27)
- [ ] Per-call numeric limits (close factor, rate limits, cooldowns) enforced per-TRANSACTION not per-call — PTB repeated call bypass (SUI-28)
- [ ] Time-lock wrappers: check if inner object mutable during pending transfer, and if cancel works after deadline (SUI-29)
- [ ] No VecMap/VecSet used for user-driven unbounded collections — use Table for >100 entries (SUI-30)
- [ ] Shared object read-only functions use `&` not `&mut` — flag excessive mutable access ratio (SUI-31)
- [ ] All `transfer::transfer` to object addresses have corresponding `receive` functions on target type (SUI-32)
- [ ] Object reference fields use `ID` type, not `address` (SUI-33)
- [ ] Functions that create objects return them instead of calling transfer internally (SUI-34)
- [ ] No batch functions with parallel vectors — use single-item functions + PTB loops (SUI-35)
- [ ] No Solidity-style address-to-role mappings — use Move capability objects (SUI-36)
- [ ] No user-defined types shadowing Sui framework names: CoinMetadata, TreasuryCap, Publisher, etc. (SUI-37)
- [ ] Display/metadata objects have all required fields set before any freeze (SUI-38)
- [ ] Single Publisher object per package, not multiple (SUI-39)
- [ ] No unnecessary `public(package)` visibility — downgrade to private if no cross-module callers (SUI-40)
- [ ] NFT structs store only per-instance fields — collection constants belong in Display templates (SUI-41)
- [ ] No dead migration functions in v1 (never-upgraded) packages (SUI-42)
- [ ] No `tx_context::digest()`, `uid_to_bytes`, `epoch()`, or `epoch_timestamp_ms()` used as randomness — use `sui::random::Random` (SUI-43)
- [ ] No `swap_remove` on index-ordered data structures — only safe for unordered bags/sets (SUI-44)

## verification-policy.md

# Verification Policy

Use this file during verification and triage.

Its job is simple:

- reduce weak dismissals
- force realistic exploitability checks
- make High/Critical findings more defensible

## Core Rule

Bias against **false dismissals**.

If a finding may be real but the refutation depends on weak evidence, keep it
as `QUESTIONABLE` rather than marking it `DISMISSED`.

## Evidence Tags

Tag every decisive claim with one of these:

| Tag | Meaning | Strong enough for `DISMISSED`? |
|-----|---------|--------------------------------|
| `[CODE]` | in-scope source code | Yes |
| `[TEST]` | existing tests in the audited repo | Yes, if exact behavior is covered |
| `[MOCK]` | test helper / fake dependency | No |
| `[DOC]` | spec, README, comments | No |
| `[EXT-UNVERIFIED]` | external package behavior not verified from source | No |
| `[PROD-SOURCE]` | verified published package source | Yes |
| `[PROD-STATE]` | production on-chain package/object/config state | Yes |

Use a short evidence table for non-trivial findings:

```md
### Evidence Audit
| Claim | Evidence | Tag |
|------|----------|-----|
| `borrow` is permissionless | `sources/lending.move:112` | `[CODE]` |
| external oracle rejects stale data | test helper | `[MOCK]` |
```

## Mock Rejection Rule

If a dismissal depends on `[MOCK]`, `[DOC]`, or `[EXT-UNVERIFIED]`, do not mark
the finding `DISMISSED`.

Mark it:

- `QUESTIONABLE` if the root cause still looks technically plausible
- `OVERCLASSIFIED` if the bug is real but the claimed impact is too high

## Feasibility Gates

Before keeping any finding at High/Critical, pass both gates below.

### Gate 1: Reachability

Identify:

- attacker-accessible entry point
- intermediate call path
- required signer / object / capability / resource
- why the attacker can actually obtain or invoke each prerequisite

If the path requires a trusted admin or an unobtainable capability, reclassify it.

### Gate 2: Math Bounds

Substitute realistic ranges into the bug-triggering expression:

- token decimals
- supply / TVL
- fee / interest / reward parameters
- time windows and stale periods
- liquidation bonuses / close factors / oracle precision

If the bug requires impossible values or blocked domains, do not keep the original severity.

## Severity Discipline

Only use High/Critical if you can name:

1. attacker path
2. victim
3. broken invariant
4. harmful postcondition

If one of those is missing, downgrade or keep as `QUESTIONABLE`.

## Move-Specific Refutation Checks

When trying to dismiss a finding, explicitly test whether it is already blocked by:

- linear resource semantics
- ability constraints
- module visibility
- Sui ownership rules
- Aptos signer / capability rules
- PTB limitations on non-`public` Sui functions
- overflow abort turning silent corruption into DoS

Tie every dismissal to exact local evidence.

## Required Verifier Output

For each finding that survives triage, include:

- exact bug statement
- attacker profile
- preconditions
- postconditions
- evidence audit
- `reachability=pass/fail`
- `math_bounds=pass/fail`
- final label: `VALID`, `QUESTIONABLE`, `DISMISSED`, or `OVERCLASSIFIED`
- severity rationale

---

## Hard Evidence Requirements

For finding-type-specific evidence requirements, see `confidence-gates.md` Section 3.
A finding missing its required hard evidence is automatically capped at `needs_review`
confidence regardless of other signals.

## Confidence Gating

Findings are assigned a confidence level that constrains maximum severity:

| Confidence | Requirement | Max Severity |
|-----------|-------------|-------------|
| `confirmed` | 2+ independent corroborating signals | Critical / High / Medium / Low |
| `likely` | 1 strong signal (strength ≥ 3) | High (if signal is strong) / Medium / Low |
| `needs_review` | Pattern match only, no concrete corroboration | **Medium max** |

See `confidence-gates.md` for the full signal taxonomy and gating checklist.

