Imported from akoita/agent-toolkit (
plugins/portable/security/skills/security-smart-contracts/SKILL.md). Install upstream withnpx skills add akoita/agent-toolkit --skill security-smart-contracts. Copyright stays with the author.
Smart contract security review
On-chain code is immutable by default, publicly readable, and adversarially executed by anyone who can pay gas. Treat every deployed function as a hostile entry point and every external call as a re-entry into your own state.
This skill is a four-phase procedure: reconnaissance, a two-pass sweep, deep validation of each candidate, then a report. The phases are ordered because each one narrows the next. Do not skip to reporting from tool output.
The shared contract, restated
The security-audit skill in this plugin owns the long form of the severity
model, the evidence rules, and the report contract. The short version, which
binds this skill:
- Every tool finding is a lead, not a finding. Promote a lead only when a
written attack path exists, with a
file:lineand named functions. Anything else is either discarded or taggedtheoretical. - Evidence or silence. Never claim a component, control, role, or data flow exists without a code reference. "Probably has a check somewhere" is not a finding and not a clearance.
- Preflight, never assume. Detect each tool before using it. If it is absent, print its install command and continue with agent-native reasoning rather than aborting the review.
- Artifacts leave the working tree. Scan output contains source excerpts and exploit steps. Write it to a directory outside the repository and never attach it to a pull request by default.
- Exit codes are inconsistent across tools. Handle them per tool and make every pipeline step non-fatal, so one analyzer's failure does not truncate the review.
- Per-finding record: identifier, title, severity, vulnerability class,
file:line, affected contracts and functions, numbered attack path, preconditions, proof of concept or an explicittheoreticaltag, impact, recommendation, and confidence. - Deduplicate by
(file, line, class)across every source, tool and human alike, and keep one merged record with the union of its corroboration.
Phase 1: reconnaissance
Build a map before looking for bugs. A finding you cannot place in the value flow is a finding you cannot rank.
- Enumerate every
.solsource under the project's source root and record the pragma of each file. Many defects are version-dependent — the arithmetic story,PUSH0availability,transfergas semantics, and custom-error support all move with the compiler — so a per-file pragma table is not bookkeeping, it is an input to Phase 3. - Identify external dependencies: which library versions are vendored or installed, which are upgradeable, which are forks with local edits. A fork with edits is the highest-risk dependency shape there is; diff it.
- Map privileged roles. For every role, record who holds it, what it can call, whether it can grant itself more, and whether it is behind a timelock.
- Map value flows. For every asset, record how it enters, where it is accounted, who can move it, and how it leaves. Every deposit path must have a withdrawal path.
- Record the trust assumptions the protocol states about oracles, sequencers, bridges, relayers, keepers, and governance. Phase 3 either confirms each one is enforced in code or turns it into a finding.
- Run the Slither printers listed in
references/tooling.mdto accelerate steps 3 and 4. Treat printer output as a draft of your map, not the map.
Phase 2: sweep
Two complementary passes. Run both. Neither alone is sufficient, and the overlap between them is small.
Pass A: syntactic
Grep for known trigger patterns and record every hit with its file:line.
This pass is deliberately dumb and deliberately over-inclusive; filtering is
Phase 3's job. Trigger set:
| Pattern | Suspected class |
|---|---|
delegatecall, callcode, assembly |
delegatecall, storage, arithmetic |
.call{value:, .send(, .transfer( |
reentrancy, unchecked calls, DoS |
tx.origin |
access control |
unchecked {, downcasts such as uint128( |
arithmetic |
block.timestamp, block.number, blockhash, prevrandao |
MEV, randomness, L2 |
ecrecover, EIP712, DOMAIN_SEPARATOR, permit( |
signatures |
initialize(, initializer, _disableInitializers |
upgradeability |
_authorizeUpgrade, upgradeTo, ERC1967 |
upgradeability |
slot0(, getReserves(, latestRoundData(, getPrice |
oracles |
selfdestruct, create2, CREATE2 |
access control, metamorphic code |
for ( over a storage or calldata array |
DoS, gas griefing |
IERC20(...).transfer, .approve(, absent SafeERC20 |
ERC-20 weirdness |
_safeMint, onERC721Received, onERC1155 |
reentrancy via callbacks |
msg.value inside a loop or multicall |
msg.value replay |
payable( fallback or receive() with logic |
access control, DoS |
Also run the static analyzers here — Slither and Aderyn, per
references/tooling.md. Their output joins the same candidate list under the
same rule: lead, not finding.
Pass B: semantic
Read for what does not grep. Budget the majority of Phase 2 here.
- Cross-function and cross-contract reentrancy. A guard on one function does not protect a second function that shares the same state. Follow every external call and ask which other entry point is reachable during it.
- Missing access control on state-changing functions. Enumerate every external and public function that writes storage, moves value, or changes a role, and name the modifier or explicit check that protects each. A blank cell is a candidate.
- Inheritance and initialization order. C3 linearization decides which implementation wins; constructor and initializer order decides which state is set. Check that every parent is initialized exactly once and in the right order, and that no override silently drops a base-class check.
- Unchecked external calls. A low-level call returns
(bool, bytes)and a discardedboolis a silent failure. Check that the return value is used and that returndata is bounded. - Missing
initializeron upgradeable contracts. Aninitialize()without the modifier, or an implementation whose constructor omits_disableInitializers(), is a critical candidate on sight. - Invariants stated in comments or documentation but not enforced in code.
These convert directly into Phase 3 candidates and into the fuzzing
properties in
references/invariants-and-fuzzing.md.
Merge Pass A and Pass B into one deduplicated candidate list, each entry
carrying a file:line and a suspected class. Deduplicate by
(file, line, class).
Phase 3: deep validation
For each candidate, do the work that turns it into a finding or discards it:
- Trace the full call chain from every external entry point that reaches it.
- Follow the values of the relevant variables across contract boundaries, including through libraries, proxies, and callbacks.
- Check every modifier and require on the path, and check what the path looks like when each one is satisfied by an attacker rather than a user.
- Establish the preconditions an attacker needs, and whether they are reachable — capital is reachable via flash loan, ordering is reachable via MEV, and a "trusted" role is reachable if it is an EOA with no timelock.
- Confirm or discard. Write the reason either way; a discarded candidate with a written reason is a durable review artifact.
Rationalizations to reject
This list is the most valuable part of this skill. Each entry is a sentence that ends analysis prematurely and is wrong often enough to be dangerous.
- "The compiler is at least 0.8.0, so overflow is impossible." Checked
arithmetic covers ordinary expressions only.
uncheckedblocks, inline assembly, and type downcasts all still wrap silently —uint128(x)on an oversizeduint256is a truncation, not a revert, at every compiler version. - "It uses OpenZeppelin, so it is safe." The library is sound; the integration usually is not. The common defect is a custom function that forgot the modifier, an override that dropped a check, a hook used with the wrong assumptions, or a version whose behavior differs from the one the code was written against.
- "That function is internal, so it is not reachable." Internal functions are reached from external entry points and execute with the caller's context. Reachability is a property of the call graph, not of the visibility keyword. Enumerate the external callers before dismissing anything.
- "No ETH is involved, so reentrancy does not apply." ERC-721
_safeMintandsafeTransferFrom, ERC-1155 single and batch safe transfers, and ERC-777tokensToSend/tokensReceivedall hand control to a receiver. And read-only reentrancy needs no value transfer at all: a view function read mid-callback returns state that is momentarily inconsistent. - "It is upgradeable, so we can fix it later." Upgradeability adds a
vulnerability class rather than removing one. An
initialize()without theinitializermodifier is itself a critical finding, and a storage-layout collision cannot be fixed by the upgrade that causes it. - "The tool flagged it, so it is a finding." And its inverse, "the tools found nothing, so the code is clean." A tool result is a lead until it has a written attack path; the absence of tool results says nothing about the business-logic and access-control classes that now dominate real losses.
Phase 4: report
Emit one record per confirmed finding, then a severity summary.
### [SC-01] <title>
- Severity: High | Medium | Low | Informational
- Class: <OWASP SC id and name, or a named class from the inventory>
- Location: `src/Path.sol:142` (`Contract.functionName`)
- Also affects: <other file:line, if the same root cause>
- Attack path:
1. <attacker action, with the concrete precondition>
2. <state transition>
3. <realized impact>
- Preconditions: <capital, role, ordering, external assumption>
- Proof of concept: `test/PoC.t.sol:test_...` — fails at <assertion>
(or: `theoretical` — no PoC written, reason: ...)
- Impact: <what is lost, by whom, and bounded by what>
- Recommendation: <the specific change, not "add validation">
- Confidence: high | medium | low
- Corroboration: slither `<detector>`, aderyn `<detector>`, manual
Close with the summary table:
| Severity | Count | With PoC | Theoretical |
|---|---|---|---|
| High | |||
| Medium | |||
| Low | |||
| Informational |
Severity rubric
Use the wording competitive-audit practitioners recognize, so a severity
travels between this report and a contest or a client review without
retranslation. This is the one place the plugin departs from the five-tier
ladder in security-audit: on-chain reviews have no Critical tier, because
direct loss of funds is already the top of the scale. When a finding has to be
reported alongside off-chain findings, map High here to Critical or High there
depending on whether the loss needs any precondition at all, and leave Medium,
Low, and Informational unchanged.
- High — assets can be directly stolen, lost, or compromised; or indirectly, via a valid attack path with no hand-waving. If a step in the path is "and then the admin makes a mistake" or "assuming an unrealistic market", it is not High.
- Medium — assets are not at direct risk, but a protocol function or its availability is impacted; or value leaks under a stated external assumption the protocol itself declares.
- Low — a real defect with negligible impact, or one requiring conditions outside any stated threat model.
- Informational — code quality, missing events, documentation drift. Keep these out of the severity table's decision-making and out of any gate.
Sherlock's denial-of-service rule, applied verbatim because it removes most severity arguments: funds locked for more than a week, or availability of time-sensitive functions impacted, is Medium; both together is High.
Every High or Medium finding should carry a failing Foundry
proof-of-concept test, or be tagged explicitly as theoretical. A severity
without one of those two is not reportable. See
references/invariants-and-fuzzing.md for the harness patterns.
Standards to work from
-
OWASP Smart Contract Top 10, 2026 edition —
https://scs.owasp.org/sctop10/. The current ranking, and the one to classify findings against:ID Class SC01 Access control vulnerabilities SC02 Business logic vulnerabilities SC03 Price oracle manipulation SC04 Flash-loan-facilitated attacks SC05 Lack of input validation SC06 Unchecked external calls SC07 Arithmetic errors (rounding and precision) SC08 Reentrancy attacks SC09 Integer overflow and underflow SC10 Proxy and upgradeability vulnerabilities Access control is now #1 and reentrancy has fallen to #8. Let the effort budget reflect that: the enumeration of every state-changing function and its guard is worth more review time than another pass over external calls.
-
Solodit checklist —
https://solodit.cyfrin.io/checklist, with the machine-readable source inCyfrin/audit-checklist. Use it as the manual pass in the default pipeline; it is organized by the categories real audit reports use. -
Building Secure Contracts (Trail of Bits) —
https://secure-contracts.com. Development guidance and the reference material for Echidna, Medusa, and Slither program analysis. -
EEA EthTrust Security Levels v2, 13 December 2023 —
https://entethalliance.org/specs/ethtrust-sl/v2/. The formal successor to SWC, structured as testable requirements at three levels. A Version 3 editor's draft is published at the unversioned URL; treat v2 as the citable release. -
The SWC Registry is deprecated. Cite an
SWC-xxxidentifier only when mapping a legacy report or an older tool's output. Do not use it as a live checklist. -
DASP is dead — the site is unreachable and its content dates from 2018. Do not cite it and do not use its top-ten ordering.
Default pipeline
An ordered, copy-pasteable block for a Foundry repository. Every step is non-fatal, and artifacts land outside the working tree.
OUT="${TMPDIR:-/tmp}/sc-audit-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUT"
REPO_URL="https://github.com/ORG/REPO" # for --markdown-root
EXCL="lib/|test/|script/|mock/"
# 1. Recon
{ forge --version; slither --version; aderyn --version; } 2>&1 | tee "$OUT/versions.txt"
find . -name '*.sol' -not -path './lib/*' | xargs grep -H '^pragma solidity' \
> "$OUT/pragmas.txt" || true
forge build --sizes > "$OUT/build.txt" 2>&1 || true
slither . --print human-summary,contract-summary,vars-and-auth,entry-points,not-pausable \
> "$OUT/recon.txt" 2>&1 || true
# 2. Static analysis (run both; they disagree usefully)
slither . --checklist --markdown-root "$REPO_URL/blob/$(git rev-parse HEAD)/" \
--filter-paths "$EXCL" --exclude-dependencies > "$OUT/slither.md" 2>&1 || true
slither . --json "$OUT/slither.json" --sarif "$OUT/slither.sarif" --fail-none || true
aderyn . -x lib,test,script -o "$OUT/aderyn.json" || true
aderyn . -x lib,test,script -o "$OUT/aderyn.md" || true
solhint 'src/**/*.sol' -f sarif > "$OUT/solhint.sarif" 2>/dev/null || true
# 3. Proxy and storage checks (only when a proxy exists)
slither-check-upgradeability . ContractV1 --new-contract-name ContractV2 \
> "$OUT/upgradeability.txt" 2>&1 || true
slither-read-storage . --contract-name Contract > "$OUT/storage.txt" 2>&1 || true
# 4. Tests and coverage
forge test -vv > "$OUT/tests.txt" 2>&1 || true
forge coverage --report lcov --report-file "$OUT/lcov.info" \
--no-match-coverage "(test|script|mock)" --ir-minimum > "$OUT/coverage.txt" 2>&1 || true
# 5. Invariants and property fuzzing
FOUNDRY_PROFILE=invariant forge test --match-test invariant_ -vv \
> "$OUT/invariants.txt" 2>&1 || true
echidna . --contract InvariantHarness --test-mode assertion \
--corpus-dir "$OUT/echidna-corpus" > "$OUT/echidna.txt" 2>&1 || true
medusa fuzz --target-contracts InvariantHarness \
--corpus-dir "$OUT/medusa-corpus" > "$OUT/medusa.txt" 2>&1 || true
# 6. Symbolic and formal, best effort only
halmos --function check_ --solver-timeout-assertion 0 > "$OUT/halmos.txt" 2>&1 || true
hevm test > "$OUT/hevm.txt" 2>&1 || true
# 7. Manual pass — the part no step above performs
echo "Solodit checklist + OWASP SC Top 10 2026 + the class inventory" \
> "$OUT/manual-pass.md"
Replace Contract, ContractV1, ContractV2, InvariantHarness, and
REPO_URL with the project's own names; nothing else is project-specific.
Triage rules
- Suppress
lib/,test/,script/, andmock/everywhere, in every tool, using that tool's own path filter. A finding inside a dependency belongs to the dependency review, not this one. - Deduplicate across Slither and Aderyn by
(file, line, class). Keep one record whose corroboration field lists both detectors. Agreement raises confidence; it does not create a second finding. - Promote a lead to a finding only with a written attack path plus a
failing Foundry proof of concept, or an explicit
theoreticaltag. - Suppress
reentrancy-benignandreentrancy-eventsby default; they are usually non-issues and they crowd out the signal. - Keep Solhint's output entirely out of the findings report. It is a style gate, not an audit input.
References in this skill
| File | Contents |
|---|---|
references/vulnerability-classes.md |
The 2026 class inventory: what each class is, how to spot it, what to check |
references/tooling.md |
Install, invocation, output format, and false-positive character for every tool |
references/invariants-and-fuzzing.md |
Foundry invariants, the six invariant families, crytic/properties, Echidna, Medusa |
references/operations.md |
Upgrade safety, verification, keys and governance, monitoring, incident response, non-EVM |
When not to use this skill
- Off-chain application, API, cloud, container, or dependency security — those belong to the other skills in this plugin.
- Gas optimization, tokenomics, or economic-design review with no security question attached.
- Non-EVM chains beyond the short pointers in
references/operations.md. - A request to attack or scan a contract the user does not own or is not authorized to review.