AI agent skill for Solidity/EVM smart-contract security audits — reentrancy, access control, oracle manipulation & more, grounded in the SWC Registry. Works with Claude Code, Codex, Cursor. By Viprasol Tech.
SaferSkills independently audited smart-contract-audit (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
Activate this skill when the user:
asks for a security review, audit, or "is this safe?".
wrong with it or how it could be exploited.
reentrancy / oracle / access-control bugs.
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.
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.
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
funds, or full takeover of privileged control. Must be fixed before any deployment. (e.g., unprotected selfdestruct, reentrancy draining the vault, uninitialized proxy implementation.)
constrained conditions, or requiring a specific (but reachable) state. (e.g., oracle manipulation via spot price, missing access control on a sensitive setter.)
value extraction (MEV). Or a High-impact issue gated behind a trusted role.
or issues needing implausible preconditions.
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.
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.
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).
.call, .transfer, token transfer, ERC-721/1155safeTransfer* hooks, ERC-777 tokensReceived) that happens before state (balances, shares, flags) is updated. Look for state writes after .call{value:}.
withdraw() sends ETH, then sets balance[msg.sender] = 0. Thereceiver's receive() calls withdraw() again with the old non-zero balance, looping until the contract is drained (classic "The DAO").
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");
}solc < 0.8.0 without SafeMath; any unchecked { } block; manualcasts (uint256 → uint128) that truncate; a - b where b may exceed a.
from nothing; a downcast silently truncates a fee, accounting drift.
solc >= 0.8.x (checked by default). Only use unchecked when youhave proven bounds (e.g., loop counters). Use OZ SafeCast for downcasts.
setters, selfdestruct) callable by anyone, or by the wrong role.
onlyOwner /role check; public/external init/setX/upgradeTo; default-visibility functions (pre-0.5 implicitly public).
setOracle() to point the price feed at anattacker-controlled contract, then drains via mispricing.
Ownable/AccessControl modifiers; prefer Ownable2Step toavoid transferring ownership to a wrong/dead address; use a timelock + multisig for high-power roles. Verify every privileged path is gated.
getReserves,balanceOf of a pool), a single-block reading, or an oracle without freshness checks.
reads that ignore updatedAt/answeredInRound/answer <= 0; using totalSupply/balanceOf as a price.
manipulated price, repay the flash loan, keep the difference (Harvest, Cheese Bank, bZx pattern).
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).
capital atomically to satisfy any check that assumes "you can't have that much".
a single transaction; governance that snapshots at block.number of the vote.
proposal or drain via mispricing, repay — all atomically (Beanstalk).
block; add commit-reveal / timelocks to governance; never trust intra-tx balances as proof of stake.
.call/.send/.delegatecall and non-reverting ERC-20transfer/transferFrom whose boolean result is ignored.
token.transfer(...) with no require; (bool ok,)=addr.call(...)where ok is unused; tokens like USDT that don't return a bool.
nothing), but the contract credits the user anyway — accounting is now wrong.
safeTransfer, safeTransferFrom,forceApprove) which handles non-standard tokens; always require the success of low-level calls.
them for profit. Includes sandwich attacks on swaps and approval/setApproval races.
amountOutMin/deadline);auctions/claims decided by tx ordering; approve race condition.
minOut; a bot front-runs to move price, letsthe victim swap at a bad rate, then back-runs — sandwiching the trade.
amountOutMin) and deadline;use commit-reveal for sensitive ordering; prefer increaseAllowance/ forceApprove over bare approve; consider private mempools/MEV protection.
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.
delegatecall(userInput); proxy + implementation with mismatchedstate variable ordering; non-EIP-1967 storage slots; an implementation with its own constructor-set state.
delegatecalled by a proxy declares address owner inslot 0, overwriting the proxy's own slot 0 — attacker becomes owner (Parity wallet freeze). Or delegatecall to user input that calls selfdestruct.
reserved slots and storage gaps (uint256[50] __gap). Never delegatecall to untrusted addresses. Keep storage layout append-only across upgrades.
use an initialize(). If it's callable twice, or the implementation itself is left uninitialized, anyone can seize it.
initialize() without the initializer modifier or withoutdisabling re-init; implementation contract not initialized / _disableInitializers() missing in its constructor; _authorizeUpgrade unprotected.
initialize() on the unguarded implementation,becomes its owner, then upgradeToAndCall → selfdestructs the implementation, bricking every proxy that delegates to it (Parity-style).
Initializable with the initializer modifier; call_disableInitializers() in the implementation's constructor; protect _authorizeUpgrade with onlyOwner/role.
a tweaked-but-valid (r,s,v).
ecrecover without a nonce, without `chainId`, without adomain separator (EIP-712), without an expiry; not checking s is in the lower half-order; not checking recovered address != address(0).
execute the action repeatedly; or replayed on a forked chain.
chainIdand 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.
tx.origin for auth instead of msg.sender.require(tx.origin == owner).calls the target — tx.origin is still the victim, so the check passes and the attacker acts as the victim.
tx.origin only for thenarrow "is this an EOA" heuristic (and even that is discouraged post-EIP-3074/7702).
selfdestruct/SELFDESTRUCT (or a withdraw-all) reachable by anyone.selfdestruct(...) not gated by a role; arbitrary delegatecallthat could reach one; "kill"/"destroy" functions.
and bricking dependents.
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.
single failing recipient in a push-payment loop blocks everyone (DoS with revert).
for over a dynamic array that anyone can grow; distributing fundsby iterating recipients with transfer; external calls inside loops.
out-of-gases; or a contract recipient that reverts on receive freezes a distribution.
paginate loops; cap array growth; isolate failing transfers so one can't block the rest.
ERC-4626-style vaults are vulnerable to a first-depositor share-inflation attack.
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.
the vault to inflate share price so the next depositor's deposit rounds down to 0 shares — the attacker redeems and steals it.
math; seed initial shares / use virtual shares & assets (OZ ERC4626 _decimalsOffset) or a dead-shares mint to neutralize the inflation attack.
block.timestamp, blockhash, block.prevrandao/difficulty,block.number, or keccak256 of those used to pick winners/mint rarity.
same block predicts or biases the outcome and always wins.
with economic bonds. Never derive value-bearing randomness from block fields.
storage pointers aliasing slot 0.
public state thatshould be private/internal; a local struct/array storage variable declared without assignment.
owner),hijacking it; or a publicly-exposed internal function is abused.
storage references or use memory explicitly. Modern solc warns on these.
malicious code, drain via a "rescue" function.
mint by owner with no cap; pause with no time bound;upgradeable with no timelock; rescueTokens that can take user funds.
privileged actions; consider renouncing or constraining upgradeability.
balance; assuming 18 decimals; assuming transfer returns a bool / reverts.
amount, but thecontract credits the full amount → insolvency; a rebasing token changes balances out from under the accounting.
balanceBefore/balanceAfter for deposits; use SafeERC20;read decimals(); explicitly allow-list supported tokens; document unsupported token types.
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.transfer/transferFrom/approve not via SafeERC20.block.timestamp, blockhash, block.prevrandao, now used for randomness ortight time logic.
ecrecover without nonce / chainId / EIP-712 / s-malleability / zero-address check.for/while over a user-growable array; external calls inside loops.onlyOwner/role modifier on initialize, upgradeTo, setX, mint,pause, withdraw, rescue.
^) or solc < 0.8.0 without SafeMath.unchecked { } blocks — verify the bounds are actually safe.__gap; implementation without_disableInitializers().
getReserves, pool balanceOf) used as an oracle.updatedAt / answer <= 0 / round completeness.payable fallback/receive with side effects; assert used for inputvalidation (consumes all gas pre-0.8 / signals invariant break).
console.log/debug left in.Produce a single, well-structured report. Use this exact skeleton.
# 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>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.
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.
A short, separate, non-security section (clearly labeled lower priority):
storage reads cached into memory inside loops; ++i in unchecked loopincrements; 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.
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.
findings are useless to fixers.
exploitability (e.g., depends on an out-of-scope contract), say so and rate likelihood accordingly — don't inflate.
as stated, flag the assumption.
give a clean, minimal, idiomatic fix.
static coverage, Mythril for symbolic paths, Echidna/Foundry for property-based fuzzing and invariants. Recommend running them.
contract is "safe to deploy."
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.