Smart Contract Security Audit
A structured methodology for reviewing Solidity / Vyper / EVM smart contracts and
producing a professional, severity-rated security report. This skill turns an
agent into a disciplined first-pass auditor: it scopes the target, walks a
comprehensive vulnerability checklist grounded in the SWC Registry and real
DeFi exploit history, and emits findings with exploit scenarios, proof-of-concept
sketches, and concrete fixes.
This skill produces an AI-assisted review. It is a powerful pre-audit and
triage tool, but it is not a substitute for a professional human audit.
See the disclaimer at the end.
When to Activate
Activate this skill when the user:
- Shares Solidity or Vyper source code (a file, a snippet, or a whole repo) and
asks for a security review, audit, or "is this safe?".
- Provides a deployed contract address (mainnet or testnet) and asks what is
wrong with it or how it could be exploited.
- Links a GitHub repo / Foundry / Hardhat project of contracts.
- Asks for a pre-deployment checklist, a "DeFi audit", or help finding
reentrancy / oracle / access-control bugs.
- Is reviewing a protocol upgrade (proxy, UUPS, Diamond) for storage or
initialization issues.
If you only have a contract address and no source, first try to obtain the
verified source (e.g., from a block explorer) or the bytecode. If neither is
available, state that clearly and limit the review to what the ABI / observable
behavior allows.
Step 1: Scope & Context
Before reading a single line for bugs, establish context. Ask for — or infer and
state your assumptions about — each of the following. The right finding severity
depends entirely on this context.
| Question |
Why it matters |
| Target chain(s) (Ethereum, Arbitrum, Base, BSC, Polygon, L2s…) |
Gas semantics, opcode availability (PUSH0, MCOPY), reorg depth, and precompiles differ. L2 sequencers add downtime/oracle-staleness risks. |
Compiler / solc version & pragma |
<0.8.0 has no built-in overflow checks. Specific versions have known compiler bugs (e.g., ABI head/tail, storage write removal). A floating pragma ^0.8.x is itself a finding. |
| Framework (Foundry, Hardhat, Truffle) & libraries (OpenZeppelin, Solmate, Solady) and their versions |
Pin to known-vuln library versions; confirm SafeERC20, ReentrancyGuard, Ownable2Step etc. are the audited ones. |
| Is it upgradeable? (Transparent proxy, UUPS, Diamond/EIP-2535, Beacon) |
Adds storage-layout, initializer, and _authorizeUpgrade attack surface. |
| Value at risk / TVL |
Drives likelihood × impact. A bug in a contract holding $50M is Critical even if exploitation is fiddly. |
| Trust model |
Who is privileged (owner, multisig, timelock, DAO)? Is the owner an EOA? Centralization is a finding when users are told it's "decentralized". |
| External dependencies |
Oracles (Chainlink, Uniswap TWAP, custom), bridges, other protocols composed with. |
| Which contracts are in scope |
Don't audit Oz's ERC20.sol; focus on the protocol's own logic and its integration points. |
| Tests / coverage / prior audits |
Existing audit reports and invariants tell you where to dig deeper, not where to relax. |
Record the answers in a Scope block at the top of the report. State all
assumptions explicitly — an assumption that turns out wrong is itself worth
flagging.
Step 2: Severity Model
Rate every finding on Impact × Likelihood. Use this matrix and resolve ties
toward the higher severity when funds are involved.
|
Likelihood: High |
Likelihood: Medium |
Likelihood: Low |
| Impact: High (funds drained / frozen, total loss of control) |
Critical |
High |
Medium |
| Impact: Medium (partial loss, griefing, temporary DoS, unfair value extraction) |
High |
Medium |
Low |
| Impact: Low (minor/cosmetic, requires implausible conditions) |
Medium |
Low |
Informational |
Severity definitions
- Critical — Direct, practical theft or permanent freeze of user/protocol
funds, or full takeover of privileged control. Must be fixed before any
deployment. (e.g., unprotected
selfdestruct, reentrancy draining the vault,
uninitialized proxy implementation.)
- High — Loss of funds or critical functionality under realistic but
constrained conditions, or requiring a specific (but reachable) state. (e.g.,
oracle manipulation via spot price, missing access control on a sensitive
setter.)
- Medium — Limited or conditional loss, griefing, recoverable DoS, or unfair
value extraction (MEV). Or a High-impact issue gated behind a trusted role.
- Low — Minor issues, best-practice deviations with real but small impact,
or issues needing implausible preconditions.
- Informational — No direct security impact: code quality, gas, style,
missing events, documentation/centralization disclosure.
Also note likelihood modifiers: is the trigger permissionless? Does it need
a flash loan (cheap) vs. a large capital position (expensive)? Does it require
winning a race / specific block conditions? Account for these explicitly.
Step 3: Vulnerability Checklist (by class)
Walk every class below against the in-scope code. For each, the format is:
what it is → how to spot it → exploit scenario → fix/pattern. SWC IDs are noted
where the Smart Contract Weakness Classification applies.
3.1 Reentrancy (SWC-107)
- What: An external call hands control to an attacker contract that re-enters
the calling function before its state updates complete. Variants:
single-function, cross-function (shared state), cross-contract, and
read-only reentrancy (a view function returns stale state mid-callback,
poisoning an integrating protocol).
- Spot it: An external call (
.call, .transfer, token transfer, ERC-721/1155
safeTransfer* hooks, ERC-777 tokensReceived) that happens before state
(balances, shares, flags) is updated. Look for state writes after .call{value:}.
- Exploit:
withdraw() sends ETH, then sets balance[msg.sender] = 0. The
receiver's receive() calls withdraw() again with the old non-zero balance,
looping until the contract is drained (classic "The DAO").
- Fix: Checks-Effects-Interactions — update all state before the external
call. Add a
nonReentrant guard (OpenZeppelin ReentrancyGuard). For
read-only reentrancy, guard view functions or have integrators avoid trusting
state during callbacks. Prefer pull-over-push withdrawals.
// BEFORE (vulnerable)
function withdraw() external {
uint256 bal = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: bal}(""); // interaction first
require(ok);
balances[msg.sender] = 0; // effect too late
}
// AFTER (checks-effects-interactions + guard)
function withdraw() external nonReentrant {
uint256 bal = balances[msg.sender];
balances[msg.sender] = 0; // effect first
(bool ok, ) = msg.sender.call{value: bal}(""); // interaction last
require(ok, "transfer failed");
}
3.2 Integer Overflow / Underflow (SWC-101)
- What: Arithmetic wraps around its type bounds.
- Spot it:
solc < 0.8.0 without SafeMath; any unchecked { } block; manual
casts (uint256 → uint128) that truncate; a - b where b may exceed a.
- Exploit: A balance subtraction underflows to a huge number, minting value
from nothing; a downcast silently truncates a fee, accounting drift.
- Fix: Use
solc >= 0.8.x (checked by default). Only use unchecked when you
have proven bounds (e.g., loop counters). Use OZ SafeCast for downcasts.
3.3 Access Control / Missing Modifier (SWC-105, SWC-106)
- What: Sensitive functions (mint, pause, upgrade, withdraw, parameter
setters,
selfdestruct) callable by anyone, or by the wrong role.
- Spot it: State-changing or privileged functions with no
onlyOwner /
role check; public/external init/setX/upgradeTo; default-visibility
functions (pre-0.5 implicitly public).
- Exploit: Anyone calls
setOracle() to point the price feed at an
attacker-controlled contract, then drains via mispricing.
- Fix: Apply
Ownable/AccessControl modifiers; prefer Ownable2Step to
avoid transferring ownership to a wrong/dead address; use a timelock + multisig
for high-power roles. Verify every privileged path is gated.
3.4 Oracle / Price Manipulation
- What: Pricing off a manipulable source — a DEX spot price (
getReserves,
balanceOf of a pool), a single-block reading, or an oracle without freshness
checks.
- Spot it: Spot price reads from Uniswap/Curve pools used directly; Chainlink
reads that ignore
updatedAt/answeredInRound/answer <= 0; using
totalSupply/balanceOf as a price.
- Exploit: Flash-loan-skew the pool's reserves in one tx, borrow/redeem at the
manipulated price, repay the flash loan, keep the difference (Harvest, Cheese
Bank, bZx pattern).
- Fix: Use a manipulation-resistant oracle: Chainlink with staleness +
min/max + L2 sequencer-uptime checks, or a Uniswap V3 TWAP over a meaningful
window. Never price off single-block spot. Validate Chainlink rounds:
require(answer > 0 && updatedAt != 0 && block.timestamp - updatedAt < MAX_AGE).
3.5 Flash-Loan Attacks
- What: Not a bug per se, but an amplifier — attackers borrow unbounded
capital atomically to satisfy any check that assumes "you can't have that much".
- Spot it: Logic that trusts balances, voting power, LP share, or price within
a single transaction; governance that snapshots at
block.number of the vote.
- Exploit: Borrow tokens to acquire majority voting power, pass a malicious
proposal or drain via mispricing, repay — all atomically (Beanstalk).
- Fix: Use TWAP/checkpointed balances; snapshot voting power at a past
block; add commit-reveal / timelocks to governance; never trust intra-tx
balances as proof of stake.
3.6 Unchecked External Call Return Values (SWC-104)
- What: Low-level
.call/.send/.delegatecall and non-reverting ERC-20
transfer/transferFrom whose boolean result is ignored.
- Spot it:
token.transfer(...) with no require; (bool ok,)=addr.call(...)
where ok is unused; tokens like USDT that don't return a bool.
- Exploit: A "transfer" silently fails (or a weird token reverts/returns
nothing), but the contract credits the user anyway — accounting is now wrong.
- Fix: Use OpenZeppelin
SafeERC20 (safeTransfer, safeTransferFrom,
forceApprove) which handles non-standard tokens; always require the success
of low-level calls.
3.7 Front-Running / MEV (SWC-114)
- What: Transactions are public in the mempool; searchers reorder/sandwich
them for profit. Includes sandwich attacks on swaps and approval/setApproval
races.
- Spot it: Swaps/mints without slippage protection (
amountOutMin/deadline);
auctions/claims decided by tx ordering; approve race condition.
- Exploit: User swaps with no
minOut; a bot front-runs to move price, lets
the victim swap at a bad rate, then back-runs — sandwiching the trade.
- Fix: Enforce user-supplied slippage (
amountOutMin) and deadline;
use commit-reveal for sensitive ordering; prefer increaseAllowance/
forceApprove over bare approve; consider private mempools/MEV protection.
3.8 Delegatecall & Proxy Storage Collisions (SWC-112)
- What:
delegatecall runs target code in the caller's storage/context.
If the proxy and implementation disagree on storage layout, writes corrupt the
wrong slots; delegatecall to attacker-controlled code is game over.
- Spot it:
delegatecall(userInput); proxy + implementation with mismatched
state variable ordering; non-EIP-1967 storage slots; an implementation with its
own constructor-set state.
- Exploit: A library
delegatecalled by a proxy declares address owner in
slot 0, overwriting the proxy's own slot 0 — attacker becomes owner
(Parity wallet freeze). Or delegatecall to user input that calls selfdestruct.
- Fix: Use audited proxy patterns (OZ Transparent / UUPS) with EIP-1967
reserved slots and storage gaps (
uint256[50] __gap). Never delegatecall
to untrusted addresses. Keep storage layout append-only across upgrades.
3.9 Uninitialized Proxy / Initializer Issues
- What: Upgradeable contracts can't use constructors for proxy state, so they
use an
initialize(). If it's callable twice, or the implementation itself is
left uninitialized, anyone can seize it.
- Spot it:
initialize() without the initializer modifier or without
disabling re-init; implementation contract not initialized / _disableInitializers()
missing in its constructor; _authorizeUpgrade unprotected.
- Exploit: Attacker calls
initialize() on the unguarded implementation,
becomes its owner, then upgradeToAndCall → selfdestructs the implementation,
bricking every proxy that delegates to it (Parity-style).
- Fix: Use OZ
Initializable with the initializer modifier; call
_disableInitializers() in the implementation's constructor; protect
_authorizeUpgrade with onlyOwner/role.
3.10 Signature Replay & Malleability (SWC-117, SWC-121, SWC-122)
- What: A signed message is accepted more than once, on another chain, or with
a tweaked-but-valid
(r,s,v).
- Spot it:
ecrecover without a nonce, without chainId, without a
domain separator (EIP-712), without an expiry; not checking s is in the lower
half-order; not checking recovered address != address(0).
- Exploit: A meta-transaction / permit signature with no nonce is replayed to
execute the action repeatedly; or replayed on a forked chain.
- Fix: Use EIP-712 typed data with a domain separator binding
chainId
and contract address; track per-signer nonces; include a deadline; use OZ
ECDSA.recover (rejects malleable s and zero address). Consider EIP-2612
permit from a vetted library.
3.11 tx.origin Authentication (SWC-115)
- What: Using
tx.origin for auth instead of msg.sender.
- Spot it:
require(tx.origin == owner).
- Exploit: Victim is phished into calling a malicious contract; that contract
calls the target —
tx.origin is still the victim, so the check passes and the
attacker acts as the victim.
- Fix: Authenticate with
msg.sender. Reserve tx.origin only for the
narrow "is this an EOA" heuristic (and even that is discouraged post-EIP-3074/7702).
3.12 Unprotected selfdestruct / Ether Withdrawal (SWC-106)
- What:
selfdestruct/SELFDESTRUCT (or a withdraw-all) reachable by anyone.
- Spot it:
selfdestruct(...) not gated by a role; arbitrary delegatecall
that could reach one; "kill"/"destroy" functions.
- Exploit: Anyone destroys the contract, forwarding its balance to themselves
and bricking dependents.
- Fix: Gate behind
onlyOwner/multisig+timelock, or remove it. Note post-Dencun
(EIP-6780) selfdestruct only fully deletes within the same tx as creation —
don't rely on its old semantics either.
3.13 Denial of Service: Unbounded Loops & Gas Griefing (SWC-128, SWC-113)
- What: Loops over user-growable arrays can exceed the block gas limit; a
single failing recipient in a push-payment loop blocks everyone (DoS with
revert).
- Spot it:
for over a dynamic array that anyone can grow; distributing funds
by iterating recipients with transfer; external calls inside loops.
- Exploit: Attacker registers thousands of entries so the loop always
out-of-gases; or a contract recipient that reverts on receive freezes a
distribution.
- Fix: Pull-over-push (each user withdraws their own share); bound or
paginate loops; cap array growth; isolate failing transfers so one can't block
the rest.
3.14 Rounding / Precision Loss & First-Depositor (Vault Inflation)
- What: Integer division truncates; ordering of mul/div changes results.
ERC-4626-style vaults are vulnerable to a first-depositor share-inflation attack.
- Spot it:
a / b * c (divide before multiply); share = assets * supply / totalAssets with a zero/one-wei starting supply; fee math that rounds in the
user's favor.
- Exploit: Attacker mints 1 wei of shares, then "donates" a large amount to
the vault to inflate share price so the next depositor's deposit rounds down to
0 shares — the attacker redeems and steals it.
- Fix: Multiply before dividing; round against the user for protocol-favoring
math; seed initial shares / use virtual shares & assets (OZ ERC4626
_decimalsOffset)
or a dead-shares mint to neutralize the inflation attack.
3.15 Bad Randomness (SWC-120)
- What: On-chain "randomness" from predictable values.
- Spot it:
block.timestamp, blockhash, block.prevrandao/difficulty,
block.number, or keccak256 of those used to pick winners/mint rarity.
- Exploit: A miner/validator or a contract computing the same value in the
same block predicts or biases the outcome and always wins.
- Fix: Use a verifiable randomness source (Chainlink VRF) or commit-reveal
with economic bonds. Never derive value-bearing randomness from block fields.
3.16 Default / Incorrect Visibility & Uninitialized Storage (SWC-100, SWC-108, SWC-109)
- What: Functions/state with unintended visibility; uninitialized local
storage pointers aliasing slot 0.
- Spot it: Missing visibility specifiers (older solc);
public state that
should be private/internal; a local struct/array storage variable
declared without assignment.
- Exploit: An uninitialized storage pointer writes to slot 0 (often
owner),
hijacking it; or a publicly-exposed internal function is abused.
- Fix: Always specify visibility; mark sensitive state non-public; initialize
storage references or use
memory explicitly. Modern solc warns on these.
3.17 Centralization & Privileged-Role Risk
- What: Owner/admin can rug — mint infinitely, pause withdrawals, upgrade to
malicious code, drain via a "rescue" function.
- Spot it: EOA owner;
mint by owner with no cap; pause with no time bound;
upgradeable with no timelock; rescueTokens that can take user funds.
- Exploit: A compromised or malicious key drains/freezes the protocol.
- Fix: Multisig + timelock for admin; document powers honestly; cap/limit
privileged actions; consider renouncing or constraining upgradeability.
3.18 ERC-20 Integration Pitfalls (fee-on-transfer, rebasing, return values, decimals)
- What: Assuming all ERC-20s behave "normally."
- Spot it: Accounting on the input amount instead of the measured received
balance; assuming 18 decimals; assuming
transfer returns a bool / reverts.
- Exploit: A fee-on-transfer token delivers less than
amount, but the
contract credits the full amount → insolvency; a rebasing token changes
balances out from under the accounting.
- Fix: Measure
balanceBefore/balanceAfter for deposits; use SafeERC20;
read decimals(); explicitly allow-list supported tokens; document unsupported
token types.
Step 4: Red-Flags Quick Scan
Before (and alongside) the deep pass, grep the codebase for these instant danger
signs. Any hit is at minimum a question, often a finding:
tx.origin used for authorization.
delegatecall / callcode — especially to a non-constant / user-supplied address.
selfdestruct / suicide not behind a strict access check.
.call{value:} / .call( whose return value is unused, or before state updates.
- Token
transfer/transferFrom/approve not via SafeERC20.
block.timestamp, blockhash, block.prevrandao, now used for randomness or
tight time logic.
ecrecover without nonce / chainId / EIP-712 / s-malleability / zero-address check.
- Unbounded
for/while over a user-growable array; external calls inside loops.
- Missing
onlyOwner/role modifier on initialize, upgradeTo, setX, mint,
pause, withdraw, rescue.
- Floating pragma (
^) or solc < 0.8.0 without SafeMath.
unchecked { } blocks — verify the bounds are actually safe.
- Proxy without EIP-1967 slots / storage
__gap; implementation without
_disableInitializers().
- DEX spot price (
getReserves, pool balanceOf) used as an oracle.
- Chainlink read ignoring
updatedAt / answer <= 0 / round completeness.
- Divide-before-multiply; vault share math with no first-deposit protection.
payable fallback/receive with side effects; assert used for input
validation (consumes all gas pre-0.8 / signals invariant break).
- Hardcoded addresses, secrets, or private keys;
console.log/debug left in.
Step 5: Output Format
Produce a single, well-structured report. Use this exact skeleton.
5.1 Header & Scope
# Security Review: <Project / Contract name>
**Reviewer:** AI-assisted audit (smart-contract-audit skill)
**Date:** <date>
**Commit / Address:** <hash or 0x…>
**Chain / solc:** <e.g., Ethereum mainnet, solc 0.8.24>
**Scope:** <files / contracts in scope>
**Out of scope / assumptions:** <list>
5.2 Findings Summary Table
A one-glance table, sorted by severity:
| ID |
Title |
Severity |
Location |
| C-01 |
Reentrancy in withdraw() drains vault |
Critical |
Vault.sol:142 |
| H-01 |
Missing onlyOwner on setRewardRate() |
High |
Staking.sol:88 |
| M-01 |
Spot price used for collateral valuation |
Medium |
Lending.sol:210 |
| L-01 |
Floating pragma ^0.8.0 |
Low |
*.sol |
| I-01 |
Missing event on parameter change |
Informational |
Config.sol:55 |
Use ID prefixes C-/H-/M-/L-/I-. Include a count line, e.g.
Totals: 1 Critical · 1 High · 1 Medium · 1 Low · 1 Informational.
5.3 Per-Finding Detail
For each finding:
### [C-01] Reentrancy in `withdraw()` drains the vault
- **Severity:** Critical (Impact: High × Likelihood: High)
- **Location:** `Vault.sol:142-150`
- **SWC:** SWC-107
- **Description:** The external ETH transfer occurs before `balances[msg.sender]`
is zeroed, allowing a malicious receiver to re-enter `withdraw()` and withdraw
repeatedly against a stale balance.
- **Impact:** Complete drain of all ETH held by the contract.
- **Proof of Concept (sketch):**
1. Attacker deposits 1 ETH.
2. Attacker's contract `receive()` calls `Vault.withdraw()` again.
3. Loop continues until `Vault` balance is 0; attacker withdraws ~all funds.
- **Recommendation:** Apply checks-effects-interactions (zero the balance before
the transfer) and add OpenZeppelin `nonReentrant`. (Show before/after.)
- **References:** SWC-107; OpenZeppelin ReentrancyGuard; "The DAO" 2016.
Order findings by severity (Critical → Informational). Keep PoCs to the
minimal sketch needed to make the bug undeniable; don't write weaponized exploits.
5.4 Gas & Optimization Notes
A short, separate, non-security section (clearly labeled lower priority):
storage reads cached into memory inside loops; ++i in unchecked loop
increments; calldata over memory for external array params; immutable/
constant for never-changing values; custom errors over require strings;
packing storage variables; avoiding redundant SLOADs. Note each only if it
actually applies.
5.5 Disclaimer (always include verbatim spirit)
Disclaimer. This is an AI-assisted security review, not a professional
audit. It may produce false positives and, more importantly, miss real
vulnerabilities — especially complex economic, cross-contract, and
protocol-design issues. Automated and AI reviews do not replace a manual audit
by a qualified firm, formal verification, comprehensive testing/fuzzing, and a
bug bounty. Do not deploy to mainnet or move real value based on this review
alone. Always complement with tooling — Slither (static analysis),
Mythril (symbolic execution), and Echidna/Foundry invariant fuzzing
— and an independent professional audit.
Working Notes for the Agent
- Be specific. Cite exact file:line locations and the offending snippet. Vague
findings are useless to fixers.
- Be honest about confidence. If something is suspicious but you can't confirm
exploitability (e.g., depends on an out-of-scope contract), say so and rate
likelihood accordingly — don't inflate.
- Don't hallucinate APIs. If you're unsure a library function exists or behaves
as stated, flag the assumption.
- Think like an attacker, report like an engineer. Find the worst case, then
give a clean, minimal, idiomatic fix.
- Reference real tooling as complements, not competitors: Slither for fast
static coverage, Mythril for symbolic paths, Echidna/Foundry for property-based
fuzzing and invariants. Recommend running them.
- Never deploy advice. Always end with the disclaimer; never tell a user a
contract is "safe to deploy."
1---2name: smart-contract-audit3description: Audit Solidity/EVM smart contracts for security vulnerabilities — reentrancy, access control, oracle manipulation, and more. Grounded in the SWC Registry and real DeFi exploit patterns. Outputs severity-rated findings with exploit scenarios and fixes. Use for smart-contract security review, DeFi audits, or pre-deployment checks.4---56# Smart Contract Security Audit78A structured methodology for reviewing Solidity / Vyper / EVM smart contracts and9producing a professional, severity-rated security report. This skill turns an10agent into a disciplined first-pass auditor: it scopes the target, walks a11comprehensive vulnerability checklist grounded in the **SWC Registry** and real12DeFi exploit history, and emits findings with exploit scenarios, proof-of-concept13sketches, and concrete fixes.1415> This skill produces an **AI-assisted review**. It is a powerful pre-audit and16> triage tool, but it is **not** a substitute for a professional human audit.17> See the disclaimer at the end.1819---2021## When to Activate2223Activate this skill when the user:2425- Shares Solidity or Vyper source code (a file, a snippet, or a whole repo) and26 asks for a security review, audit, or "is this safe?".27- Provides a deployed **contract address** (mainnet or testnet) and asks what is28 wrong with it or how it could be exploited.29- Links a GitHub repo / Foundry / Hardhat project of contracts.30- Asks for a **pre-deployment checklist**, a "DeFi audit", or help finding31 reentrancy / oracle / access-control bugs.32- Is reviewing a protocol upgrade (proxy, UUPS, Diamond) for storage or33 initialization issues.3435If you only have a contract **address** and no source, first try to obtain the36verified source (e.g., from a block explorer) or the bytecode. If neither is37available, state that clearly and limit the review to what the ABI / observable38behavior allows.3940---4142## Step 1: Scope & Context4344Before reading a single line for bugs, establish context. Ask for — or infer and45state your assumptions about — each of the following. The right finding severity46depends entirely on this context.4748| Question | Why it matters |49|---|---|50| **Target chain(s)** (Ethereum, Arbitrum, Base, BSC, Polygon, L2s…) | Gas semantics, opcode availability (`PUSH0`, `MCOPY`), reorg depth, and precompiles differ. L2 sequencers add downtime/oracle-staleness risks. |51| **Compiler / `solc` version** & `pragma` | `<0.8.0` has no built-in overflow checks. Specific versions have known compiler bugs (e.g., ABI head/tail, storage write removal). A floating `pragma ^0.8.x` is itself a finding. |52| **Framework** (Foundry, Hardhat, Truffle) & libraries (OpenZeppelin, Solmate, Solady) and their **versions** | Pin to known-vuln library versions; confirm `SafeERC20`, `ReentrancyGuard`, `Ownable2Step` etc. are the audited ones. |53| **Is it upgradeable?** (Transparent proxy, UUPS, Diamond/EIP-2535, Beacon) | Adds storage-layout, initializer, and `_authorizeUpgrade` attack surface. |54| **Value at risk / TVL** | Drives likelihood × impact. A bug in a contract holding $50M is Critical even if exploitation is fiddly. |55| **Trust model** | Who is privileged (owner, multisig, timelock, DAO)? Is the owner an EOA? Centralization is a finding when users are told it's "decentralized". |56| **External dependencies** | Oracles (Chainlink, Uniswap TWAP, custom), bridges, other protocols composed with. |57| **Which contracts are in scope** | Don't audit Oz's `ERC20.sol`; focus on the protocol's own logic and its integration points. |58| **Tests / coverage / prior audits** | Existing audit reports and invariants tell you where to dig deeper, not where to relax. |5960Record the answers in a **Scope** block at the top of the report. State all61assumptions explicitly — an assumption that turns out wrong is itself worth62flagging.6364---6566## Step 2: Severity Model6768Rate every finding on **Impact × Likelihood**. Use this matrix and resolve ties69toward the higher severity when funds are involved.7071| | **Likelihood: High** | **Likelihood: Medium** | **Likelihood: Low** |72|---|---|---|---|73| **Impact: High** (funds drained / frozen, total loss of control) | **Critical** | **High** | **Medium** |74| **Impact: Medium** (partial loss, griefing, temporary DoS, unfair value extraction) | **High** | **Medium** | **Low** |75| **Impact: Low** (minor/cosmetic, requires implausible conditions) | **Medium** | **Low** | **Informational** |7677**Severity definitions**7879- **Critical** — Direct, practical theft or permanent freeze of user/protocol80 funds, or full takeover of privileged control. Must be fixed before any81 deployment. (e.g., unprotected `selfdestruct`, reentrancy draining the vault,82 uninitialized proxy implementation.)83- **High** — Loss of funds or critical functionality under realistic but84 constrained conditions, or requiring a specific (but reachable) state. (e.g.,85 oracle manipulation via spot price, missing access control on a sensitive86 setter.)87- **Medium** — Limited or conditional loss, griefing, recoverable DoS, or unfair88 value extraction (MEV). Or a High-impact issue gated behind a trusted role.89- **Low** — Minor issues, best-practice deviations with real but small impact,90 or issues needing implausible preconditions.91- **Informational** — No direct security impact: code quality, gas, style,92 missing events, documentation/centralization disclosure.9394Also note **likelihood modifiers**: is the trigger permissionless? Does it need95a flash loan (cheap) vs. a large capital position (expensive)? Does it require96winning a race / specific block conditions? Account for these explicitly.9798---99100## Step 3: Vulnerability Checklist (by class)101102Walk **every** class below against the in-scope code. For each, the format is:103*what it is → how to spot it → exploit scenario → fix/pattern*. SWC IDs are noted104where the Smart Contract Weakness Classification applies.105106### 3.1 Reentrancy (SWC-107)107- **What:** An external call hands control to an attacker contract that re-enters108 the calling function before its state updates complete. Variants:109 single-function, cross-function (shared state), cross-contract, and110 **read-only reentrancy** (a view function returns stale state mid-callback,111 poisoning an integrating protocol).112- **Spot it:** An external call (`.call`, `.transfer`, token transfer, ERC-721/1155113 `safeTransfer*` hooks, ERC-777 `tokensReceived`) that happens **before** state114 (balances, shares, flags) is updated. Look for state writes after `.call{value:}`.115- **Exploit:** `withdraw()` sends ETH, then sets `balance[msg.sender] = 0`. The116 receiver's `receive()` calls `withdraw()` again with the old non-zero balance,117 looping until the contract is drained (classic "The DAO").118- **Fix:** **Checks-Effects-Interactions** — update all state *before* the external119 call. Add a `nonReentrant` guard (OpenZeppelin `ReentrancyGuard`). For120 read-only reentrancy, guard view functions or have integrators avoid trusting121 state during callbacks. Prefer **pull-over-push** withdrawals.122123```solidity124// BEFORE (vulnerable)125function withdraw() external {126 uint256 bal = balances[msg.sender];127 (bool ok, ) = msg.sender.call{value: bal}(""); // interaction first128 require(ok);129 balances[msg.sender] = 0; // effect too late130}131132// AFTER (checks-effects-interactions + guard)133function withdraw() external nonReentrant {134 uint256 bal = balances[msg.sender];135 balances[msg.sender] = 0; // effect first136 (bool ok, ) = msg.sender.call{value: bal}(""); // interaction last137 require(ok, "transfer failed");138}139```140141### 3.2 Integer Overflow / Underflow (SWC-101)142- **What:** Arithmetic wraps around its type bounds.143- **Spot it:** `solc < 0.8.0` without SafeMath; any `unchecked { }` block; manual144 casts (`uint256` → `uint128`) that truncate; `a - b` where `b` may exceed `a`.145- **Exploit:** A balance subtraction underflows to a huge number, minting value146 from nothing; a downcast silently truncates a fee, accounting drift.147- **Fix:** Use `solc >= 0.8.x` (checked by default). Only use `unchecked` when you148 have **proven** bounds (e.g., loop counters). Use OZ `SafeCast` for downcasts.149150### 3.3 Access Control / Missing Modifier (SWC-105, SWC-106)151- **What:** Sensitive functions (mint, pause, upgrade, withdraw, parameter152 setters, `selfdestruct`) callable by anyone, or by the wrong role.153- **Spot it:** State-changing or privileged functions with no `onlyOwner` /154 role check; `public`/`external` `init`/`setX`/`upgradeTo`; default-visibility155 functions (pre-0.5 implicitly public).156- **Exploit:** Anyone calls `setOracle()` to point the price feed at an157 attacker-controlled contract, then drains via mispricing.158- **Fix:** Apply `Ownable`/`AccessControl` modifiers; prefer `Ownable2Step` to159 avoid transferring ownership to a wrong/dead address; use a timelock + multisig160 for high-power roles. Verify **every** privileged path is gated.161162### 3.4 Oracle / Price Manipulation163- **What:** Pricing off a manipulable source — a DEX **spot** price (`getReserves`,164 `balanceOf` of a pool), a single-block reading, or an oracle without freshness165 checks.166- **Spot it:** Spot price reads from Uniswap/Curve pools used directly; Chainlink167 reads that ignore `updatedAt`/`answeredInRound`/`answer <= 0`; using168 `totalSupply`/`balanceOf` as a price.169- **Exploit:** Flash-loan-skew the pool's reserves in one tx, borrow/redeem at the170 manipulated price, repay the flash loan, keep the difference (Harvest, Cheese171 Bank, bZx pattern).172- **Fix:** Use a manipulation-resistant oracle: Chainlink with staleness +173 min/max + L2 sequencer-uptime checks, or a Uniswap V3 **TWAP** over a meaningful174 window. Never price off single-block spot. Validate Chainlink rounds:175 `require(answer > 0 && updatedAt != 0 && block.timestamp - updatedAt < MAX_AGE)`.176177### 3.5 Flash-Loan Attacks178- **What:** Not a bug per se, but an **amplifier** — attackers borrow unbounded179 capital atomically to satisfy any check that assumes "you can't have that much".180- **Spot it:** Logic that trusts balances, voting power, LP share, or price within181 a single transaction; governance that snapshots at `block.number` of the vote.182- **Exploit:** Borrow tokens to acquire majority voting power, pass a malicious183 proposal or drain via mispricing, repay — all atomically (Beanstalk).184- **Fix:** Use TWAP/checkpointed balances; snapshot voting power at a **past**185 block; add commit-reveal / timelocks to governance; never trust intra-tx186 balances as proof of stake.187188### 3.6 Unchecked External Call Return Values (SWC-104)189- **What:** Low-level `.call`/`.send`/`.delegatecall` and non-reverting ERC-20190 `transfer`/`transferFrom` whose boolean result is ignored.191- **Spot it:** `token.transfer(...)` with no `require`; `(bool ok,)=addr.call(...)`192 where `ok` is unused; tokens like USDT that **don't return a bool**.193- **Exploit:** A "transfer" silently fails (or a weird token reverts/returns194 nothing), but the contract credits the user anyway — accounting is now wrong.195- **Fix:** Use OpenZeppelin **`SafeERC20`** (`safeTransfer`, `safeTransferFrom`,196 `forceApprove`) which handles non-standard tokens; always `require` the success197 of low-level calls.198199### 3.7 Front-Running / MEV (SWC-114)200- **What:** Transactions are public in the mempool; searchers reorder/sandwich201 them for profit. Includes sandwich attacks on swaps and approval/setApproval202 races.203- **Spot it:** Swaps/mints without slippage protection (`amountOutMin`/deadline);204 auctions/claims decided by tx ordering; `approve` race condition.205- **Exploit:** User swaps with no `minOut`; a bot front-runs to move price, lets206 the victim swap at a bad rate, then back-runs — sandwiching the trade.207- **Fix:** Enforce user-supplied **slippage** (`amountOutMin`) and **deadline**;208 use commit-reveal for sensitive ordering; prefer `increaseAllowance`/209 `forceApprove` over bare `approve`; consider private mempools/MEV protection.210211### 3.8 Delegatecall & Proxy Storage Collisions (SWC-112)212- **What:** `delegatecall` runs target code in the **caller's** storage/context.213 If the proxy and implementation disagree on storage layout, writes corrupt the214 wrong slots; `delegatecall` to attacker-controlled code is game over.215- **Spot it:** `delegatecall(userInput)`; proxy + implementation with mismatched216 state variable ordering; non-EIP-1967 storage slots; an implementation with its217 own constructor-set state.218- **Exploit:** A library `delegatecall`ed by a proxy declares `address owner` in219 slot 0, overwriting the proxy's own slot 0 — attacker becomes owner220 (Parity wallet freeze). Or `delegatecall` to user input that calls `selfdestruct`.221- **Fix:** Use audited proxy patterns (OZ Transparent / UUPS) with **EIP-1967**222 reserved slots and storage **gaps** (`uint256[50] __gap`). Never `delegatecall`223 to untrusted addresses. Keep storage layout append-only across upgrades.224225### 3.9 Uninitialized Proxy / Initializer Issues226- **What:** Upgradeable contracts can't use constructors for proxy state, so they227 use an `initialize()`. If it's callable twice, or the implementation itself is228 left uninitialized, anyone can seize it.229- **Spot it:** `initialize()` without the `initializer` modifier or without230 disabling re-init; implementation contract not initialized / `_disableInitializers()`231 missing in its constructor; `_authorizeUpgrade` unprotected.232- **Exploit:** Attacker calls `initialize()` on the unguarded implementation,233 becomes its owner, then `upgradeToAndCall` → `selfdestruct`s the implementation,234 bricking every proxy that delegates to it (Parity-style).235- **Fix:** Use OZ `Initializable` with the `initializer` modifier; call236 `_disableInitializers()` in the implementation's constructor; protect237 `_authorizeUpgrade` with `onlyOwner`/role.238239### 3.10 Signature Replay & Malleability (SWC-117, SWC-121, SWC-122)240- **What:** A signed message is accepted more than once, on another chain, or with241 a tweaked-but-valid `(r,s,v)`.242- **Spot it:** `ecrecover` without a **nonce**, without **`chainId`**, without a243 domain separator (EIP-712), without an expiry; not checking `s` is in the lower244 half-order; not checking recovered address `!= address(0)`.245- **Exploit:** A meta-transaction / permit signature with no nonce is replayed to246 execute the action repeatedly; or replayed on a forked chain.247- **Fix:** Use **EIP-712** typed data with a domain separator binding `chainId`248 and contract address; track per-signer **nonces**; include a deadline; use OZ249 `ECDSA.recover` (rejects malleable `s` and zero address). Consider EIP-2612250 `permit` from a vetted library.251252### 3.11 tx.origin Authentication (SWC-115)253- **What:** Using `tx.origin` for auth instead of `msg.sender`.254- **Spot it:** `require(tx.origin == owner)`.255- **Exploit:** Victim is phished into calling a malicious contract; that contract256 calls the target — `tx.origin` is still the victim, so the check passes and the257 attacker acts as the victim.258- **Fix:** Authenticate with **`msg.sender`**. Reserve `tx.origin` only for the259 narrow "is this an EOA" heuristic (and even that is discouraged post-EIP-3074/7702).260261### 3.12 Unprotected selfdestruct / Ether Withdrawal (SWC-106)262- **What:** `selfdestruct`/`SELFDESTRUCT` (or a withdraw-all) reachable by anyone.263- **Spot it:** `selfdestruct(...)` not gated by a role; arbitrary `delegatecall`264 that could reach one; "kill"/"destroy" functions.265- **Exploit:** Anyone destroys the contract, forwarding its balance to themselves266 and bricking dependents.267- **Fix:** Gate behind `onlyOwner`/multisig+timelock, or remove it. Note post-Dencun268 (EIP-6780) `selfdestruct` only fully deletes within the same tx as creation —269 don't rely on its old semantics either.270271### 3.13 Denial of Service: Unbounded Loops & Gas Griefing (SWC-128, SWC-113)272- **What:** Loops over user-growable arrays can exceed the block gas limit; a273 single failing recipient in a push-payment loop blocks everyone (DoS with274 revert).275- **Spot it:** `for` over a dynamic array that anyone can grow; distributing funds276 by iterating recipients with `transfer`; external calls inside loops.277- **Exploit:** Attacker registers thousands of entries so the loop always278 out-of-gases; or a contract recipient that reverts on receive freezes a279 distribution.280- **Fix:** **Pull-over-push** (each user withdraws their own share); bound or281 paginate loops; cap array growth; isolate failing transfers so one can't block282 the rest.283284### 3.14 Rounding / Precision Loss & First-Depositor (Vault Inflation)285- **What:** Integer division truncates; ordering of mul/div changes results.286 ERC-4626-style vaults are vulnerable to a first-depositor share-inflation attack.287- **Spot it:** `a / b * c` (divide before multiply); share = `assets * supply /288 totalAssets` with a zero/one-wei starting supply; fee math that rounds in the289 user's favor.290- **Exploit:** Attacker mints 1 wei of shares, then "donates" a large amount to291 the vault to inflate share price so the next depositor's deposit rounds down to292 **0 shares** — the attacker redeems and steals it.293- **Fix:** Multiply before dividing; round **against** the user for protocol-favoring294 math; seed initial shares / use virtual shares & assets (OZ ERC4626 `_decimalsOffset`)295 or a dead-shares mint to neutralize the inflation attack.296297### 3.15 Bad Randomness (SWC-120)298- **What:** On-chain "randomness" from predictable values.299- **Spot it:** `block.timestamp`, `blockhash`, `block.prevrandao`/`difficulty`,300 `block.number`, or `keccak256` of those used to pick winners/mint rarity.301- **Exploit:** A miner/validator or a contract computing the same value in the302 same block predicts or biases the outcome and always wins.303- **Fix:** Use a verifiable randomness source (**Chainlink VRF**) or commit-reveal304 with economic bonds. Never derive value-bearing randomness from block fields.305306### 3.16 Default / Incorrect Visibility & Uninitialized Storage (SWC-100, SWC-108, SWC-109)307- **What:** Functions/state with unintended visibility; uninitialized local308 storage pointers aliasing slot 0.309- **Spot it:** Missing visibility specifiers (older solc); `public` state that310 should be `private`/`internal`; a local `struct`/`array` storage variable311 declared without assignment.312- **Exploit:** An uninitialized storage pointer writes to slot 0 (often `owner`),313 hijacking it; or a publicly-exposed internal function is abused.314- **Fix:** Always specify visibility; mark sensitive state non-public; initialize315 storage references or use `memory` explicitly. Modern `solc` warns on these.316317### 3.17 Centralization & Privileged-Role Risk318- **What:** Owner/admin can rug — mint infinitely, pause withdrawals, upgrade to319 malicious code, drain via a "rescue" function.320- **Spot it:** EOA owner; `mint` by owner with no cap; `pause` with no time bound;321 upgradeable with no timelock; `rescueTokens` that can take user funds.322- **Exploit:** A compromised or malicious key drains/freezes the protocol.323- **Fix:** Multisig + timelock for admin; document powers honestly; cap/limit324 privileged actions; consider renouncing or constraining upgradeability.325326### 3.18 ERC-20 Integration Pitfalls (fee-on-transfer, rebasing, return values, decimals)327- **What:** Assuming all ERC-20s behave "normally."328- **Spot it:** Accounting on the **input** amount instead of the measured received329 balance; assuming 18 decimals; assuming `transfer` returns a bool / reverts.330- **Exploit:** A **fee-on-transfer** token delivers less than `amount`, but the331 contract credits the full `amount` → insolvency; a **rebasing** token changes332 balances out from under the accounting.333- **Fix:** Measure `balanceBefore`/`balanceAfter` for deposits; use `SafeERC20`;334 read `decimals()`; explicitly allow-list supported tokens; document unsupported335 token types.336337---338339## Step 4: Red-Flags Quick Scan340341Before (and alongside) the deep pass, grep the codebase for these instant danger342signs. Any hit is at minimum a question, often a finding:343344- `tx.origin` used for authorization.345- `delegatecall` / `callcode` — especially to a non-constant / user-supplied address.346- `selfdestruct` / `suicide` not behind a strict access check.347- `.call{value:}` / `.call(` whose return value is unused, or before state updates.348- Token `transfer`/`transferFrom`/`approve` **not** via `SafeERC20`.349- `block.timestamp`, `blockhash`, `block.prevrandao`, `now` used for randomness or350 tight time logic.351- `ecrecover` without nonce / `chainId` / EIP-712 / `s`-malleability / zero-address check.352- Unbounded `for`/`while` over a user-growable array; external calls inside loops.353- Missing `onlyOwner`/role modifier on `initialize`, `upgradeTo`, `setX`, `mint`,354 `pause`, `withdraw`, `rescue`.355- Floating pragma (`^`) or `solc < 0.8.0` without SafeMath.356- `unchecked { }` blocks — verify the bounds are actually safe.357- Proxy without EIP-1967 slots / storage `__gap`; implementation without358 `_disableInitializers()`.359- DEX **spot** price (`getReserves`, pool `balanceOf`) used as an oracle.360- Chainlink read ignoring `updatedAt` / `answer <= 0` / round completeness.361- Divide-before-multiply; vault share math with no first-deposit protection.362- `payable` fallback/`receive` with side effects; `assert` used for input363 validation (consumes all gas pre-0.8 / signals invariant break).364- Hardcoded addresses, secrets, or private keys; `console.log`/debug left in.365366---367368## Step 5: Output Format369370Produce a single, well-structured report. Use this exact skeleton.371372### 5.1 Header & Scope373```374# Security Review: <Project / Contract name>375**Reviewer:** AI-assisted audit (smart-contract-audit skill)376**Date:** <date>377**Commit / Address:** <hash or 0x…>378**Chain / solc:** <e.g., Ethereum mainnet, solc 0.8.24>379**Scope:** <files / contracts in scope>380**Out of scope / assumptions:** <list>381```382383### 5.2 Findings Summary Table384A one-glance table, sorted by severity:385386| ID | Title | Severity | Location |387|----|-------|----------|----------|388| C-01 | Reentrancy in `withdraw()` drains vault | Critical | `Vault.sol:142` |389| H-01 | Missing `onlyOwner` on `setRewardRate()` | High | `Staking.sol:88` |390| M-01 | Spot price used for collateral valuation | Medium | `Lending.sol:210` |391| L-01 | Floating pragma `^0.8.0` | Low | `*.sol` |392| I-01 | Missing event on parameter change | Informational | `Config.sol:55` |393394Use ID prefixes `C-/H-/M-/L-/I-`. Include a count line, e.g.395`Totals: 1 Critical · 1 High · 1 Medium · 1 Low · 1 Informational`.396397### 5.3 Per-Finding Detail398For **each** finding:399400```401### [C-01] Reentrancy in `withdraw()` drains the vault402- **Severity:** Critical (Impact: High × Likelihood: High)403- **Location:** `Vault.sol:142-150`404- **SWC:** SWC-107405- **Description:** The external ETH transfer occurs before `balances[msg.sender]`406 is zeroed, allowing a malicious receiver to re-enter `withdraw()` and withdraw407 repeatedly against a stale balance.408- **Impact:** Complete drain of all ETH held by the contract.409- **Proof of Concept (sketch):**410 1. Attacker deposits 1 ETH.411 2. Attacker's contract `receive()` calls `Vault.withdraw()` again.412 3. Loop continues until `Vault` balance is 0; attacker withdraws ~all funds.413- **Recommendation:** Apply checks-effects-interactions (zero the balance before414 the transfer) and add OpenZeppelin `nonReentrant`. (Show before/after.)415- **References:** SWC-107; OpenZeppelin ReentrancyGuard; "The DAO" 2016.416```417418Order findings by severity (Critical → Informational). Keep PoCs to the419minimal sketch needed to make the bug undeniable; don't write weaponized exploits.420421### 5.4 Gas & Optimization Notes422A short, separate, **non-security** section (clearly labeled lower priority):423- `storage` reads cached into `memory` inside loops; `++i` in `unchecked` loop424 increments; `calldata` over `memory` for external array params; `immutable`/425 `constant` for never-changing values; custom errors over `require` strings;426 packing storage variables; avoiding redundant SLOADs. Note each only if it427 actually applies.428429### 5.5 Disclaimer (always include verbatim spirit)430> **Disclaimer.** This is an **AI-assisted security review**, not a professional431> audit. It may produce false positives and, more importantly, **miss real432> vulnerabilities** — especially complex economic, cross-contract, and433> protocol-design issues. Automated and AI reviews do not replace a manual audit434> by a qualified firm, formal verification, comprehensive testing/fuzzing, and a435> bug bounty. **Do not deploy to mainnet or move real value based on this review436> alone.** Always complement with tooling — **Slither** (static analysis),437> **Mythril** (symbolic execution), and **Echidna**/**Foundry invariant fuzzing**438> — and an independent professional audit.439440---441442## Working Notes for the Agent443444- **Be specific.** Cite exact file:line locations and the offending snippet. Vague445 findings are useless to fixers.446- **Be honest about confidence.** If something is suspicious but you can't confirm447 exploitability (e.g., depends on an out-of-scope contract), say so and rate448 likelihood accordingly — don't inflate.449- **Don't hallucinate APIs.** If you're unsure a library function exists or behaves450 as stated, flag the assumption.451- **Think like an attacker, report like an engineer.** Find the worst case, then452 give a clean, minimal, idiomatic fix.453- **Reference real tooling as complements,** not competitors: Slither for fast454 static coverage, Mythril for symbolic paths, Echidna/Foundry for property-based455 fuzzing and invariants. Recommend running them.456- **Never deploy advice.** Always end with the disclaimer; never tell a user a457 contract is "safe to deploy."