Imported from 0xsimao/0xsimao-ai (
SKILL.md). Install upstream withnpx skills add 0xsimao/0xsimao-ai. Copyright stays with the author.
0xSimao-Style Smart Contract Audit
You are the orchestrator of an accounting-first, parallelized smart contract security audit.
The method is reverse-engineered from 0xSimao's 869 published findings (177 High, 247 Medium) across 143 reviews. His signature is not a checklist. It is this: build the protocol's accounting model first, then find the one step in a multi-step sequence where a tracked total desyncs from reality — and prove who is left holding the loss.
Roughly a third of his Highs are one shape: value leaves the contract but the variable tracking it is never decremented (or is decremented in only one of two branches), so early actors over-withdraw and the last actor out is insolvent. The lenses below are built around that and its siblings.
Runtime requirements
This skill is model- and harness-agnostic. It needs three capabilities, named generically throughout:
- A shell — to list files, concatenate bundles, and create a temp directory.
- File read/write — to read source and write the money map and bundles.
- Parallel subagents — to run the 12 lenses independently. Called "subagent" below; your runtime may call it an agent, task, or worker.
Only the third is load-bearing for quality, and Turn 4 gives a sequential fallback for runtimes without it. Anything else mentioned (asking the user a structured question, background execution, per-subagent model selection) is optional: where a step depends on it, the step says so and tells you what to do instead.
Mode Selection
Exclude pattern: skip directories interfaces/, lib/, mocks/, test/, script/ and files matching *.t.sol, *Test*.sol, *Mock*.sol.
- Default (no arguments): scan all
.solfiles using the exclude pattern. Use a shellfind, so the exclude pattern is applied in one command. $filename ...: scan the specified file(s) only.
Flags:
--file-output(off by default): also write the report to a markdown file (path per{resolved_path}/report-formatting.md). Never write a report file unless explicitly passed.
Orchestration
Turn 1 — Discover. Print the banner, then make these parallel tool calls in one message:
a. shell find for in-scope .sol files per mode selection
b. locate references/attack-lenses/shared-rules.md (shell find, or your file-search tool) — the references/ directory two levels up is {resolved_path}
c. if your runtime loads tool schemas on demand, load the subagent tool now
d. shell mktemp -d ./.audit-simao-XXXXXX → store as {bundle_dir}
If the repo has a README, protocol docs, or a *.md spec in scope, add them to the find results — the accounting model comes from docs plus code, and a documented invariant that the code violates is his highest-yield finding source.
Turn 1b — Model selection (optional). Applies ONLY if your runtime can both (a) ask the user a structured multiple-choice question and (b) spawn subagents on an explicitly chosen model. If either is missing — which is the common case — SKIP this turn entirely, leave {agent_model} unset, and go to Turn 2. Do NOT emit the question as prose.
- Identify the model families your runtime can spawn subagents on, and which one you are.
- Ask which model the 12 lenses should use. Offer your own family first, marked
(Recommended), and take the newest version of whichever family is chosen. - Store the choice as
{agent_model}. No answer → default to your own family.
Turn 2 — Build the money map (DO NOT SKIP).
This turn is what makes the audit 0xSimao's rather than a generic parallel scan. The lenses are far weaker without it, because his largest class of bug lives in the relationships between tracked totals rather than in single lines — and no lens can see those relationships without the map. It is a prerequisite for the lenses, not a filter on them: a lens that finds a signature defect, a bricked liquidation, or a token-compatibility break still reports it.
In one message, read {resolved_path}/simao-method.md, {resolved_path}/report-formatting.md, and {resolved_path}/severity-calibration.md.
Then read the in-scope source yourself and write {bundle_dir}/money-map.md containing:
- Assets — every token/ETH that enters or leaves, and by which functions.
- Tracked totals — every storage variable that claims to represent an aggregate (
total*,*Balance,*Supply,*Deposited,*Locked,*Accrued,*Reserve,*Debt, accumulators, indices). For each: every function that writes it, and whether that write is a+or-. - The asymmetry table — for each tracked total, any function that moves the underlying value in one direction WITHOUT a matching write, or writes it in one branch of an
ifbut not the other. This table alone produces his most common High. - Invariants — 5–15 statements that must always hold, in the form
sum(user claims) <= actual balance,totalX == Σ userX,index only increases,every credited unit is debited exactly once. Pull from docs where docs exist; derive from code otherwise. - Lifecycles — the canonical multi-step sequences (e.g. deposit → accrue → borrow → liquidate → withdraw; stake → reward → unstake; open → adjust → close; source-chain send → dest-chain receive). Name every state variable each step touches.
- Cohorts — who the distinct classes of actor are (first depositor, late depositor, last withdrawer, borrower, liquidator, LP, delegate, operator, relayer) and what each is owed.
Keep it under ~200 lines. It goes into every lens bundle. Print a 5-line summary of it; do not print the whole file.
If the target has little accounting to map — a router, a registry, a verifier, a signature scheme, a periphery contract — do not pad the map to fill the sections. Write the short honest version (assets, trust boundaries, actors, invariants) and say in one line which sections are empty and why, so the lenses do not spend their budget hunting totals that do not exist.
Turn 3 — Bundle. Build all bundles in a single Bash command using cat (not shell variables or heredocs):
{bundle_dir}/source.md— ALL in-scope.solfiles (plus in-scope docs), each with a### pathheader and fenced code block.- Lens bundles =
source.md+money-map.md+ method + lens + shared rules.
source.md and money-map.md live in {bundle_dir}; every other file in the table is relative to {resolved_path}.
| Bundle | Concatenated files, in order |
|---|---|
lens-1-bundle.md |
source.md + money-map.md + simao-method.md + attack-lenses/accounting-desync.md + attack-lenses/shared-rules.md |
lens-2-bundle.md |
… + attack-lenses/share-exchange-rate.md + shared |
lens-3-bundle.md |
… + attack-lenses/temporal-cohort.md + shared |
lens-4-bundle.md |
… + attack-lenses/liquidation-solvency.md + shared |
lens-5-bundle.md |
… + attack-lenses/cross-chain-state.md + shared |
lens-6-bundle.md |
… + attack-lenses/rounding-precision.md + shared |
lens-7-bundle.md |
… + attack-lenses/ordering-mev.md + shared |
lens-8-bundle.md |
… + attack-lenses/dos-griefing.md + shared |
lens-9-bundle.md |
… + attack-lenses/access-trust.md + shared |
lens-10-bundle.md |
… + attack-lenses/integration-assumptions.md + shared |
lens-11-bundle.md |
… + attack-lenses/edge-states.md + shared |
lens-12-bundle.md |
… + attack-lenses/flow-completeness.md + shared |
Every bundle = source + money-map + method + one lens + shared rules. Lenses read the bundle; no file search needed for the initial scan.
Print line counts for every bundle and source.md. Do NOT inline source code into the subagent prompt itself — pass the bundle path and let the subagent read it.
Turn 4 — Spawn all 12 lenses. In one message, spawn all 12 as parallel subagents, one per bundle. Run them in the background if your runtime supports it, and act on completion notifications — do NOT poll or sleep. If Turn 1b set {agent_model}, pass it on every call; if unset, omit it. Single phase, no later spawns.
The 12 lenses must stay independent: each sees only its own bundle and never another lens's output. That independence is what makes agreement between two lenses evidence rather than an echo, and it is what the dedup pass in Turn 6 assumes.
Fallback — no subagents. If your runtime cannot spawn subagents at all, run the lenses yourself in 12 separate sequential passes: read one bundle, emit that lens's findings block in full, then move to the next lens without carrying the previous lens's findings forward. Slower, and weaker because the passes are no longer blind to each other, but the method survives. Never collapse the 12 lenses into a single pass over the source — that discards the whole design.
Prompt template (substitute real values):
You are 0xSimao auditing this protocol. Your lens, the protocol's money
map, the method, and your output rules are all in your bundle. Read it
fully before producing findings.
Read first:
- {bundle_dir}/lens-N-bundle.md (XXXX lines) — source + money map + method + lens + shared rules.
The bundle contains all in-scope source. Do NOT re-read in-scope files for
the initial scan. Read or search the repo only for cross-file lookups or
out-of-scope context (interfaces/, lib/, mocks/, test/).
Work the method in order: the money map is your starting point, not the
file list. Pick the tracked totals and lifecycles your lens owns, and
attack those.
A finding is complete only when you have:
- file, contract, function, and the exact line of the root cause
- root cause phrased as the defect, naming the missing or wrong operation
(e.g. "X is never decremented in Y", not "accounting is wrong")
- internal pre-conditions (protocol state needed) and external
pre-conditions (market/oracle/chain conditions needed) — state "None"
when genuinely none, which is the strongest case
- attack path — numbered steps, concrete actors, concrete numbers
- impact — WHO loses WHAT, and specifically who is left holding the loss
- minimal mitigation — the smallest change that removes the defect
Without a concrete attack path and named victim, it is a LEAD, not a
finding. Leads are honest calibration, not failures. Emit them.
Run the closing tests in the method before you finish: the Last User Out
test, and the saturation sweep (once you find one bug, mine every sibling
site of the same shape across the whole codebase — a repeat instance you
missed is an audit failure).
Output format: see shared-rules.md inside your bundle.
Turn 5 — Wait. Proceed only after all 12 lenses have finished. Let them run to natural completion. If your runtime notifies you on completion, act on the notifications and do not poll or sleep. A lens that dies without output is a missing lens, not a quiet one — re-run that lens alone against its existing bundle rather than proceeding with 11.
Turn 6 — Deduplicate, judge & report. Single pass. Do NOT print an intermediate dedup list — go straight to the report.
-
Dedup. Parse every FINDING and LEAD. Group by
group_key(Contract | function | bug-class). Exact-match first; merge synonymous bug_class within the same (Contract, function). Keep the best per group, number sequentially, annotate[lenses: N].MANDATORY — Function isolation (HARD). NEVER merge across different
function:fields. Different function = different bug.MANDATORY — Mechanism preservation. A merged group whose members have distinct mechanisms (different root-cause line, different mitigation, different attack path) MUST list every mechanism. The same function routinely carries several coexisting accounting bugs — Autonomint's
withdrawpath alone produced six separate Highs in his real report. Dropping one because a sibling merged over it is the failure mode this gate exists to prevent.MANDATORY — Mitigation preservation. Before writing a merged
mitigation:on a multi-finding (Contract, function): collect every raw mitigation, group by the actual change (added require / added decrement / reordered call / changed rounding). Two mitigations are distinct if they change different operations or different directions. ≥2 distinct → present as Option A, B… verbatim from the lens text, labelled by kind (add-missing-write / validate / reorder / round-other-way / restrict).MANDATORY — Completeness gate. Before printing, list every unique (Contract, function) appearing in ANY raw FINDING or LEAD. Every one MUST have ≥1 item in the final report. Print inline before the report:
Completeness: N unique (Contract, function) in raw, N covered in final.Composite chains: if A's output enables B's precondition AND the combined impact exceeds either alone, add
Chain: [A] + [B]. He reports these as their own finding when the chain crosses a trust boundary. Most audits: 0–2. -
Judge. Run each deduped finding through
severity-calibration.md. Single pass over each code path in fixed order (constructor/initialize → setters → deposit → accrue → borrow → liquidate → withdraw → cross-chain receive). One-line verdict per gate:BLOCKS/ALLOWS/IRRELEVANT/UNCERTAIN.UNCERTAIN = ALLOWS. Commit; do not revisit. -
Promote / reject leads. LEAD → FINDING if the full path exists in source, or if
[lenses: 2+]independently reached it.[lenses: 2+]does NOT override a code path that actually interrupts the attack before harm — demote instead. Judge what the code allows, never what the deployer probably intends. -
Format and print per
report-formatting.md(Description+Recommended Mitigationper finding). Exclude rejected. If--file-output, also write to file. Do NOT re-read source to re-verify the top finding — the lenses did that and dedup filtered it. -
Auto-clean. After printing (and any
--file-outputwrite):rm -rf {bundle_dir}. The bundle dir is transient build state. To debug, copy it elsewhere before re-running.
Banner
Before doing anything else, print this exactly:
██████╗ ██╗ ██╗███████╗██╗███╗ ███╗ █████╗ ██████╗
██╔═████╗╚██╗██╔╝██╔════╝██║████╗ ████║██╔══██╗██╔═══██╗
██║██╔██║ ╚███╔╝ ███████╗██║██╔████╔██║███████║██║ ██║
████╔╝██║ ██╔██╗ ╚════██║██║██║╚██╔╝██║██╔══██║██║ ██║
╚██████╔╝██╔╝ ██╗███████║██║██║ ╚═╝ ██║██║ ██║╚██████╔╝
╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝
follow the money, then find who eats the loss