Your Solidity contract compiles. Your Foundry suite is green. You have a badge on the repo and a Discord full of hopeful users. Then someone drains it in a single transaction, and the post-mortem reads like a checklist you never ran: reentrancy through a state change, an oracle read that a flash loan can move, an unchecked external call, a rounding error that compounds. The myth says audits are for big protocols and small teams can rely on tests. The losses say otherwise: most Web3 incidents are not broken cryptography, they are logic mistakes that are visible in the code and the patterns — if someone looks.
This roundup is about making a large language model that someone. Not as a replacement for a human auditor, but as a reviewer that never gets tired, never assumes the happy path, and can be pointed at a specific file with a specific question. Below are twelve prompts, each with the reasoning behind it, a real usage example, and the code you feed in. Use them in order or jump to the ones that match your contract's risk profile. The goal is simple: find the expensive mistakes before the audit invoice arrives — or before you skip the audit entirely.
Start with the threat model, not the linter
A linter tells you about tx.origin and floating pragmas. It does not tell you that your withdrawal function trusts a balance that another function can inflate. Before any prompt about reentrancy or oracles, you need a map of what your contract considers valuable and who can touch it. This first prompt builds that map. More on the topic: Cryptography in Practice.
Task: Build a threat model for a Solidity contract from its source and a one-line description of its purpose.
Prompt:
You are a smart contract security reviewer. I will give you a Solidity contract and a short description of its purpose. Build a threat model with four sections: (1) assets — what value the contract holds or controls (tokens, ETH, permissions, oracle feeds); (2) actors — who can call which external functions, including unprivileged users and other contracts; (3) trust boundaries — every place where the contract trusts an external address, a return value, or a timestamp; (4) failure modes — the three most plausible ways an attacker turns those trust boundaries into profit. Do not list generic risks. For each failure mode, cite the function name and the line where the trust is granted. Output as a table with columns: Asset, Actor, Trust boundary, Failure mode, Function. More on the topic: Blockchain Security & Smart Contract Auditor.
Example result: For a simple staking contract, the model returns a table where the top failure mode is “reward calculation reads totalStaked before the deposit is recorded, allowing a same-block deposit-withdraw to claim rewards on tokens that were never at risk.” That is a concrete, testable hypothesis — not a vague warning about reentrancy.
Reentrancy lives in state changes, not in the word “call”
Most reentrancy checklists are useless because they flag every .call and miss the actual bug: an external call that happens before the contract's own state is consistent. The prompt below forces the model to trace state changes around every external interaction.
Task: Find reentrancy paths by comparing state writes with external calls.
Prompt:
Analyze this Solidity contract for reentrancy. For every external call (call, transfer, send, or a call to another contract), list: (a) the state variables read after the call returns, (b) the state variables written before the call, (c) whether the call target can re-enter any public function. Then rank the findings by exploitability: which one lets an attacker withdraw more than they deposited in a single transaction? Show the call sequence as a numbered list. If a function is protected by a nonReentrant modifier, say so and skip it only if the modifier is applied to every state-changing entry point.
Example result: On a classic vulnerable withdraw, the model returns: “withdraw() sends ETH at line 42 before setting balances[msg.sender] = 0 at line 44. The fallback of the attacker contract can call withdraw() again; balances is still the original value. Sequence: deposit 1 ETH → withdraw → re-enter → withdraw again.” That is a proof sketch you can turn into a test in minutes.
Oracles: the price you read is a price someone can move
If your contract reads a price from a pool's spot reserves, a flash loan can move that price inside one transaction. This is not a theoretical attack; it is the default failure of DeFi protocols that skip a TWAP or a chainlink feed. The prompt below asks the model to find every price read and classify how manipulable it is. More on the topic: Stop Asking LLMs to Guess CVEs.
Task: Audit oracle usage and price manipulation risk.
Prompt:
Review this Solidity contract for oracle and price manipulation risk. For every place the contract reads a price or reserves (getReserves, slot0, latestRoundData, a custom oracle call), report: the source, whether it is spot or time-weighted, who can move it within one transaction, and what the contract does with the price (collateral valuation, liquidation threshold, swap output). Then write a short attack narrative for the most manipulable read: which flash loan, which pool, which function call sequence. If the contract uses a Chainlink feed, check whether it validates updatedAt and answeredInRound.
Example result: The model flags “getPrice() reads slot0.sqrtPriceX96 directly from a Uniswap V3 pool; an attacker can swap a large amount in the same block, push the price, call borrow(), and swap back.” It then checks the Chainlink branch and notes that latestRoundData() is called but updatedAt is never compared to block.timestamp, so a stale price would be accepted after a feed outage. Both are real bugs, both are visible in code.
Access control: the function that should be internal
A missing onlyOwner on a mint function is the kind of mistake that survives code review because reviewers read the function body, not the modifier list. The prompt below makes the model enumerate every state-changing function and its caller set.
Task: Check access control on every external and public function.
Prompt:
List every external and public function in this Solidity contract. For each, state who can call it (anyone, owner, a specific role) and what state it changes. Flag any function that changes a privileged state (mints, pauses, upgrades, sets fees, changes an oracle address) but has no access modifier. Also flag functions that use tx.origin for authorization, and functions where the modifier checks msg.sender but the logic uses tx.origin. Output a table: Function, Visibility, Modifier, State changed, Risk.
Example result: A typical finding: “setFee(uint256) is external with no modifier and writes feeBps. Any address can set the fee to 10000, taking 100% of swaps.” That is a one-line fix that a green test suite will never catch, because tests call the function as the owner.
Integer rounding: where the dust becomes a drain
Solidity integer division truncates. In a vault that mints shares, truncation favors one side — and an attacker can repeat the operation until the dust adds up. The prompt below asks the model to find rounding directions and check whether they can be exploited.
Task: Audit rounding and precision in share/asset calculations.
Prompt:
Analyze the arithmetic in this Solidity contract. For every division or multiplication involving token amounts, shares, or rates, state the direction of rounding (up or down) and who benefits. Flag any place where a user can repeat an operation to accumulate rounding in their favor, and any place where a division happens before a multiplication that would lose precision. If the contract uses a share price formula, check whether the first depositor can inflate the share price to steal from later depositors (the ERC-4626 inflation attack).
Example result: On a vault, the model returns: “shares = assets * totalSupply / totalAssets rounds down, favoring the vault. But assets = shares * totalAssets / totalSupply in redeem also rounds down, so a user redeeming 1 wei repeatedly loses dust to the vault — not exploitable. However, the first deposit of 1 wei sets totalSupply = 1, and a donation of 1e18 to the vault makes the next depositor's shares round to zero.” That is the inflation attack, spelled out in terms of the contract's own lines.
Gas: the injection that empties a wallet without a bug
Gas griefing is not about stealing funds directly; it is about making a function uncallable or forcing a user to pay more than expected. A loop over an unbounded array, a transfer that forwards 2300 gas to a contract that needs more, a refund pattern that can be blocked — these are real. The prompt below looks for them.
Task: Find gas-related denial of service and unbounded loops.
Prompt:
Review this Solidity contract for gas issues that can block or grief users. Look for: loops over arrays whose length is controlled by an external caller, transfer or send calls to contracts that may need more than 2300 gas, refund patterns where one failing recipient blocks all withdrawals, and storage writes inside loops. For each finding, state the maximum realistic array length or gas cost and the function that becomes uncallable. Suggest a pull-over-push fix where applicable.
Example result: The model flags “distribute() loops over holders[] and calls transfer to each. If one holder is a contract with a fallback that reverts, the whole distribution reverts, and the array grows with every new holder, so gas cost grows until the function exceeds the block gas limit.” The fix — a withdraw() per holder — is a pattern change, not a line edit, which is exactly the kind of thing an audit should catch early.
The PoC: a failing test is worth a thousand warnings
A finding without a test is a hypothesis. Foundry makes it cheap to turn a hypothesis into a failing test that proves the exploit. The prompt below asks the model to write that test.
Task: Write a Foundry PoC test for a specific vulnerability.
Prompt:
Write a Foundry test that proves this vulnerability: [describe the finding, e.g., reentrancy in withdraw]. Use forge-std/Test.sol. Create an attacker contract that implements the exploit, deploy the vulnerable contract, fund it, and assert that the attacker's balance increases by more than the deposit. The test must fail on the current code and pass after the fix. Include the exact forge test command to run it. Do not use mocks unless the contract depends on an external oracle; in that case, use a simple mock with a settable price.
Example result: The model returns a ReentrancyAttack.t.sol file with an Attacker contract whose receive() calls back into withdraw(), a test that logs the attacker's profit, and the command forge test --match-test testReentrancy -vvv. Running it against the vulnerable contract shows the attacker walking away with 2 ETH from a 1 ETH deposit — a number that ends the debate about whether the bug is real.
Where this breaks: the limits of LLM review
The prompts above are powerful, but they are not an audit. Knowing where they fail is part of using them well.
| Situation | What the LLM does well | Where it breaks |
|---|---|---|
| Single contract, < 500 lines | Full threat model, reentrancy trace, gas review | Misses cross-contract interactions if you do not paste the dependencies |
| Proxy/upgradeable patterns | Flags storage layout issues if you provide both implementations | Cannot verify the actual deployed bytecode matches the source |
| Economic design (incentives, liquidation) | Spots obvious oracle manipulation | Cannot simulate multi-block MEV or game-theoretic equilibria |
| Assembly and Yul | Explains the intent | Frequently misreads stack order and memory layout |
| Novel math (custom curves, ZK) | Checks arithmetic direction | Cannot prove soundness; needs a specialist |
The pattern: the model is a strong reviewer of code you show it and a weak reviewer of code you don't. If your contract calls an external protocol, paste that protocol's interface and the relevant function bodies. If your math is non-standard, write the invariant as a comment and ask the model to check it. The prompt is only as good as the context you give it.
One more limit: the model does not know your deployment. A function that is safe when called by an EOA can be dangerous when called by a contract. If your users are contracts, say so in the prompt. If your contract is upgradeable, say so. The threat model changes with the deployment, and the model cannot infer it from the source alone.
Twelve prompts, one habit
The list above is not a checklist to run once before an audit. It is a habit: before you ship, ask the model the same questions an attacker will ask — where does this contract trust something it cannot verify, and what happens when that trust is broken? The twelve prompts cover the four failure modes that account for most Web3 losses (reentrancy, oracle manipulation, access control, rounding) plus the two that make them worse (gas griefing and missing tests). Run them on every PR that touches value transfer, and the audit becomes a confirmation, not a discovery.
If you want to go deeper into the oracle side, the breakdown of why a “decentralized oracle” does not save you from a flash loan attack is a good next read. And if your team writes contracts with an LLM in the loop, the prompts for catching bugs before the merge button lights up apply just as well to Solidity as to any other language. More on the topic: Red Team & Application Security.
The myth says green tests mean a safe contract. The prompts say otherwise — and they are cheaper than a post-mortem.