Smart Contract Audit skill for Codex

Every finding comes with a failing test. Codex freezes scope at a commit, maps every entry point and value flow, writes protocol invariants, then works a checklist covering reentrancy, access control, oracle manipulation, rounding, signature replay and upgrade flaws. Suspected bugs become findings only after a Foundry proof of concept on a local fork, and the report rates each by impact and likelihood with a fix review.

  • Web3 & Smart Contracts
  • $smart-contract-audit
  • 184 lines
  • 3 files
  • ~2,150 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- smart-contract-audit

Installs to ~/.agents/skills/smart-contract-audit/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $smart-contract-audit, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Performs a structured security review of Solidity smart contracts - scoping, architecture mapping, Slither and Aderyn static analysis, manual review for reentrancy, access control, oracle manipulation, rounding, signature replay and upgrade flaws, and Foundry proof-of-concept tests on local forks - then writes a severity-rated findings report. Use when asked to audit, security review or pre-launch check a contract or DeFi protocol, triage a static-analysis report, or verify fixes from a previous audit.

Use it when

  • A contract or protocol is about to hold real value and needs a security review
  • You have static-analysis output that needs triage into real issues and noise
  • An external audit's findings were fixed and the fixes need verification
  • You want invariants and PoC tests written for the riskiest functions

Use something else when

How to check the result

Each finding links a location and a PoC test that fails on the audited commit and passes on the fix commit, and the report lists scope, commit hash and what was not reviewed.

The mistake it prevents

Pasting raw Slither output as findings, or demonstrating an exploit against a live deployment instead of a local fork pinned to a block.

~/.agents/skills/smart-contract-audit/SKILL.md

--- name: smart-contract-audit description: Performs a structured security review of Solidity smart contracts - scoping, architecture mapping, Slither and Aderyn static analysis, manual review… license: MIT ---

An audit is worth something when every reported issue has a reproducible proof (a failing test or a precise call sequence), severity reflects impact times likelihood, and the report says plainly what was not reviewed. An audit reduces risk; it never certifies a contract as safe, and the report must say so.

Scope and rules first

Before reading code, confirm with the user and record in the report:

  • The exact commit hash and the files in scope (and those out of scope: libraries, mocks).
  • That the code is theirs or they are authorized to have it reviewed.
  • That proof-of-concept exploits run only on local forks or local chains. Never send transactions to a live deployment, even a “test” one on mainnet, to demonstrate a bug.
  • Known issues, previous audits, and the threat model they care about (admin trusted or not?).

If a finding affects a live deployment holding funds, stop and tell the user privately before writing anything else; disclosure decisions are theirs.

Workflow

- [ ] 1. Freeze scope: commit hash, file list, nSLOC (cloc or scc), compiler version
- [ ] 2. Build and run the existing tests; note coverage gaps
- [ ] 3. Map the system: contracts, roles, entry points, value flows, external dependencies
- [ ] 4. Write down invariants the protocol must keep (in words, then as test assertions)
- [ ] 5. Run static analysis and triage every result
- [ ] 6. Manual review, function by function, with the checklist below
- [ ] 7. Write a PoC test for each suspected issue; drop or downgrade what you cannot prove
- [ ] 8. Write the report; review fixes against the same PoCs

Step 3: map before judging

Produce a table like this for every external or public non-view function:

FunctionCallerMoves value?Calls out toState writtenGuard
depositanyoneintokenbalances, totalsnonReentrant
setOracleownerno-oracleonlyOwner

Functions that are callable by anyone, move value and call out deserve the most time. Note every external dependency: tokens (which kinds: fee-on-transfer, rebasing, ERC-777 hooks, USDT’s missing return value, blocklists), oracles, AMMs, bridges, other protocols.

Step 5: tooling

forge build && forge test -vv
forge coverage --report summary          # may need --ir-minimum on stack-too-deep code
slither . --filter-paths "lib|test|script" --exclude-dependencies
slither . --print human-summary
aderyn .                                  # writes report.md

Slither and Aderyn install via pip and cargo or their installers and need network; ask for approval in a sandbox. Treat their output as leads, not findings. Classify each result as true positive, false positive (say why) or informational. Never paste raw tool output into the report.

Step 6: manual review checklist

Go through each function against these questions. For the full catalog with vulnerable and fixed code for each class, read references/vulnerability-catalog.md when a question below raises a suspicion.

Access control

  • Is every state-changing privileged function guarded? Are initializers protected and called in the deploy script (_disableInitializers() in implementation constructors)?
  • Can roles be renounced or transferred to zero by mistake? Is there a single EOA with power to drain funds? (Centralization risk: report it even when “by design”.)

Reentrancy

  • External calls (token transfers, ETH sends, hooks in ERC-721/1155/777 receivers, callbacks) before state updates? Cross-function reentrancy through a second function sharing state?
  • Read-only reentrancy: does a view function return inconsistent values mid-call that another protocol uses as a price?

Arithmetic and accounting

  • Rounding direction in share/asset conversions; first-depositor inflation on ERC-4626 vaults (OZ 4.9+/5.x mitigates with virtual shares and _decimalsOffset).
  • Division before multiplication; precision loss with low-decimal tokens (USDC has 6).
  • unchecked blocks and downcasts (uint128(x)) without SafeCast.
  • Does sum(user balances) == total hold after every function? Write it as an invariant.

Oracles and prices

  • Spot prices from AMM reserves or balanceOf(pool) are flash-loan manipulable.
  • Chainlink: stale updatedAt, answer <= 0, wrong decimals, missing L2 sequencer check.

Tokens

  • Fee-on-transfer and rebasing tokens break amount-based accounting.
  • Missing return values (USDT) without SafeERC20. Approval race and infinite approvals.
  • Tokens with blocklists (USDC) can make a push-payment loop revert for everyone.

Signatures and replay

  • EIP-712 domain includes chainId and verifyingContract; nonce consumed; deadline checked.
  • ecrecover zero-address result and s-malleability handled (use OZ ECDSA).
  • Permit front-running: a permit call that reverts because someone already used it must not block the user’s flow (wrap in try/catch).

Denial of service and griefing

  • Unbounded loops, push payments to arbitrary addresses, reliance on exact balances (address(this).balance == x, which forced ETH breaks).

MEV and ordering

  • Missing slippage limits (minAmountOut) and deadlines on swaps and liquidations.
  • Commit-reveal needed? Can a front-runner sandwich or steal a first-come reward?

Upgradeability

  • Storage layout collisions, uninitialized implementations, missing _authorizeUpgrade guard, selfdestruct/delegatecall in implementations. See $upgradeable-contracts.

Cross-chain and L2

  • block.number semantics differ on L2s (Arbitrum returns an L1-ish number).
  • PUSH0 and other opcodes on chains with older EVMs; hard-coded chain IDs.

Step 7: proving issues

Write each PoC as a Foundry test that fails on the vulnerable code and passes after the fix. Name it after the finding ID.

function test_H01_reentrancyDrainsVault() public {
    vm.deal(address(attacker), 1 ether);
    attacker.attack{value: 1 ether}();
    // Vault started with 10 ETH from honest users.
    assertGt(address(attacker).balance, 1 ether, "attacker profited");
    assertLt(address(vault).balance, 10 ether, "honest funds lost");
}

For protocols that integrate live contracts, fork locally at a pinned block: vm.createSelectFork(vm.rpcUrl("mainnet"), 21_000_000); (needs an RPC URL and network). Pinning the block keeps the PoC reproducible.

Severity

SeverityImpactTypical example
CriticalDirect theft or permanent freezing of user funds, no preconditionsUnprotected withdrawAll
HighLoss of funds with realistic preconditions; protocol insolvencyOracle manipulation via flash loan
MediumFunds at risk under unlikely conditions; core function broken; temporary freezeBlocklisted recipient freezes a payout loop
LowMinor value leak, edge-case misbehaviour, best-practice deviation with some riskMissing zero-address check on a settable param
InformationalCode quality, gas, docsUnused variable, missing NatSpec

Severity = impact x likelihood. A Critical impact behind an owner-only call is usually Medium or “centralization risk” if the owner is trusted per the agreed threat model; say which.

Output contract

# Security review: <Project> @ <commit>

**Scope:** src/Vault.sol, src/Strategy.sol (612 nSLOC). Out of scope: lib/, script/
**Method:** manual review, Slither 0.11, Aderyn, Foundry PoCs on a local fork at block N
**Disclaimer:** time-boxed review; absence of findings is not a guarantee of security.

| ID | Title | Severity | Status |
|---|---|---|---|
| H-01 | Reentrancy in withdraw drains vault | High | Fixed in abc123 |

## H-01 Reentrancy in withdraw drains vault
**Location:** src/Vault.sol#L88-L97
**Description:** what is wrong and why, in three to six sentences.
**Impact:** who loses what, under which conditions.
**Proof of concept:** test/poc/H01.t.sol::test_H01_reentrancyDrainsVault
**Recommendation:** concrete code change.
**Fix review:** verified in commit abc123; PoC now fails to profit.

Hard rules

  • No finding without a location and either a PoC or a precise, checkable call sequence.
  • Do not inflate severity to look thorough or deflate it to please the client.
  • Re-run every PoC against the fix commit before marking an issue fixed.
  • Keep findings private to the user; never post them in public issues or PRs without consent.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Smart Contract Audit"
  short_description: "Every finding comes with a failing test"
  default_prompt: "$smart-contract-audit Audit src/ at the current commit before our mainnet launch and write a findings report"
references/vulnerability-catalog.md149 lines, 6.6 KB
# Vulnerability catalog

Read the section matching the suspicion raised by the checklist. Each entry gives the
shape of the bug, a minimal vulnerable pattern, the fix, and how to prove it.

## 1. Classic reentrancy

```solidity
// Vulnerable: balance zeroed after the call.
function withdraw() external {
    uint256 amt = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: amt}("");
    require(ok);
    balances[msg.sender] = 0;
}
```

Fix: zero the balance before the call, and add `nonReentrant`. Prove with an attacker
contract whose `receive()` calls `withdraw()` again while `address(vault).balance >= amt`.

## 2. Cross-function and cross-contract reentrancy

Two functions share state; the guard protects only one, or two contracts share a token
balance. Example: `withdraw` is `nonReentrant` but `transferShares` is not, and the
attacker calls `transferShares` from the callback to move shares that are about to be
burned. Fix: put every function that touches the shared state behind the same guard, or
finish all effects before any interaction.

## 3. Read-only reentrancy

A view such as `getPricePerShare()` reads `totalAssets / totalSupply` while a callback is
in progress and one side is already updated. Another protocol that uses this price as
collateral value can be exploited during the callback. Fix: make the view revert when the
reentrancy lock is held (OZ 5.x exposes `_reentrancyGuardEntered()`), or update both values
before calling out.

## 4. Token callback reentrancy

`_safeMint` (ERC-721), `safeTransferFrom` (ERC-1155) and ERC-777 `tokensReceived` call
into the receiver. A mint loop that checks `totalMinted < max` before `_safeMint` and
increments after it can be re-entered to mint past the cap. Fix: increment counters before
the safe call.

## 5. Missing or wrong access control

- Initializer callable by anyone on an implementation or a proxy that was deployed without
  calling it in the same transaction.
- `onlyOwner` forgotten on a setter for a fee recipient, oracle or router.
- Role admin misconfigured: `DEFAULT_ADMIN_ROLE` granted to a contract that can be called by
  anyone.

Prove by calling the function from `makeAddr("attacker")` with `vm.prank`.

## 6. ERC-4626 inflation (first depositor)

Attacker deposits 1 wei of assets for 1 share, then donates a large amount directly to the
vault. The next depositor's `assets * supply / totalAssets` rounds to zero shares and the
attacker redeems everything. Fix: virtual shares and assets (OZ 5.x `ERC4626` with a non-zero
`_decimalsOffset()`), a seeded dead-shares deposit at deployment, or a minimum-shares check.

## 7. Rounding direction

Conversions that round in the user's favour can be looped for profit, especially with
low-decimal tokens. Rule: `previewDeposit` and `previewRedeem` round down; `previewMint` and
`previewWithdraw` round up. Prove with a fuzz test that a deposit-then-redeem cycle never
returns more than was put in.

## 8. Oracle manipulation

```solidity
// Vulnerable: spot price from pool reserves.
(uint112 r0, uint112 r1, ) = pair.getReserves();
uint256 price = uint256(r1) * 1e18 / r0;
```

A flash loan moves reserves inside one transaction. Fix: Chainlink feed with staleness and
sanity checks, or a TWAP long enough to be expensive to move. PoC: fork, flash-swap to skew
reserves, call the victim function, repay.

## 9. Chainlink feed misuse

Missing checks: `answer > 0`, `updatedAt` within the feed's heartbeat plus a margin,
decimals read from `feed.decimals()` not assumed, and on L2s the sequencer uptime feed with a
grace period after restart. See the chainlink-oracles skill for the full consumer.

## 10. Signature replay and malleability

- No nonce: the same signature executes twice.
- No `chainId` in the domain: replay on another chain after a fork or on an L2 deployment.
- No `verifyingContract`: replay against a second deployment.
- Using `ecrecover` directly: returns `address(0)` on invalid signatures, so a check against
  an unset signer (`address(0)`) passes. Fix: OZ `ECDSA.recover` plus `EIP712` and `Nonces`.

## 11. Front-running and missing slippage

Swaps with `amountOutMin = 0`, liquidations without price bounds, or first-come rewards
without commit-reveal. Fix: user-supplied `minOut` and `deadline` parameters.

## 12. Denial of service

- Push payments in a loop: one reverting recipient (contract without `receive`, blocklisted
  USDC address) blocks everyone. Fix: pull payments.
- Unbounded arrays: iteration cost eventually exceeds block gas. Fix: pagination or
  mappings with counters.
- Strict equality on balances: `selfdestruct` (in the creation transaction) or coinbase
  rewards can force ETH into a contract. Fix: internal accounting.

## 13. Unsafe external token assumptions

- `transfer` returning nothing (USDT) reverts when decoded as `bool`. Fix: `SafeERC20`.
- Fee-on-transfer: credit the measured balance delta, not `amount`.
- Rebasing (stETH, AMPL): balances change without transfers. Either reject or use wrapped
  versions (wstETH).
- Tokens with more or fewer than 18 decimals break hard-coded `1e18` scaling.

## 14. Upgradeable contract flaws

- Storage collision after reordering or changing variable types.
- Implementation left uninitialized and taken over (matters most when it can
  `delegatecall` or self-destruct).
- UUPS implementation missing `_authorizeUpgrade` access control, letting anyone upgrade.
- Constructor logic in an implementation that should be in `initialize`.

## 15. Arithmetic edge cases

- Unsafe downcasts: `uint96(x)` silently truncates. Use `SafeCast`.
- `unchecked` loops whose bound can be attacker-controlled.
- Division by a value that can be zero (empty pool, zero supply) bricking a function.
- `type(int256).min` negation and `abs` overflow.

## 16. Delegatecall and proxies to user input

`address(target).delegatecall(data)` where `target` is user-controlled hands over storage
and balance. Multicall helpers using `delegatecall` with `msg.value` reuse the same ETH for
each sub-call. Fix: no user-supplied delegatecall targets; non-payable multicall or
per-call value accounting.

## 17. Governance and timelocks

Flash-loanable voting power (balance read at vote time instead of a past checkpoint),
proposals executable without a delay, or a timelock whose admin can bypass the delay. Fix:
`ERC20Votes` checkpoints with `getPastVotes`, a `TimelockController` owning the protocol.

## 18. Gas griefing and returndata bombs

An external call that returns huge returndata forces the caller to pay to copy it. When
calling untrusted contracts and ignoring the result, use assembly `call` with zero output
length or a bounded-returndata helper, and pass an explicit gas limit where a revert must
not block the rest of the transaction.

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc