All articles
Web3 FoundationsSeptember 16, 202610 min read

How to Review AI-Generated Solidity: A Reading Checklist

A repeatable seven-pass reading checklist for Solidity a model wrote for you, using nothing but your eyes: the state, the signatures, the claimed interface, undefined names, comments, error messages, and the decisions it made silently.

By Carlos (Bloqarl)

TL;DR

  • Reviewing generated Solidity is the reading method you would use on any contract, run deliberately rather than by feel. No tool required.
  • Read the state before the functions. Anything it remembers that you never asked for is a decision it made on your behalf.
  • Every comment, error message, and claimed interface is a claim, not a fact, and each is checkable against the lines underneath it. Put the interface it says it implements beside the real one: a missing returns (bool) is invisible in prose and obvious side by side.
  • The honest limit: this catches what is visible in the text, and says nothing about what the code does when it runs.

How do you review Solidity that an AI wrote?

You read it, in a fixed order, before you compile it. Read the state, read the signatures, hold the claimed interface beside the real one, trace every name you do not recognise, treat comments and error messages as claims, then ask what it decided without telling you.

Seven passes, none needing an editor, a compiler, or a test runner. They are the passes from how to read a smart contract with one difference in emphasis: generated code is fluent. The trap is not that models write nonsense, because they rarely do. It is that plausible code passes a glance, and this is how you refuse to glance.

Every generated block below is marked as model output: not deployed, not from any repository. The real code is real, quoted from OpenZeppelin.

What should you read first in a contract a model gave you?

The state, before any function. State variables are the contract's whole world model, so a fact absent from the list does not exist to it. In generated code the list does a second job: anything it remembers that you never asked for is a decision the model made silently.

Here is the top of a file produced from a request for a fixed supply token. It is model output, not a deployed contract.

contract SimpleToken {
    string public name = "Simple";
    string public symbol = "SMP";
    uint8 public decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

Read it first as a list of things the contract knows: two labels, a decimals figure, a running total, a balance table, a nested table of permissions. Now read it as a list of decisions. Nobody asked for eighteen decimals; the model chose the convention. Nobody asked for an allowance table; the model decided this token supports spending on someone else's behalf, a second mechanism with its own rules.

Notice what is absent too. No owner, no pauser, no fee recipient. You can establish that this contract has no privileged role without reading a function, purely from what its memory lacks. See state variables in Solidity.

How do you read the signatures it wrote?

One function at a time, in file order, answering three questions and nothing else: who can call this, does it change anything, what does it hand back. You are not reading bodies yet. You are building a table of what the contract offers, because bodies only make sense against it.

Visibility first. public and external mean anyone; internal and private mean only code inside the contract or its descendants. Generated code leans towards public because public always compiles, so a function you assumed was plumbing may be a front door. See visibility.

Then mutability, where view promises the compiler the function only reads. Then the return, the one people skim past and the pass that pays: write the signature out and compare it to what callers expect back. For standard interfaces that expectation is published, so you can put it on the table beside it.

How do you check it implements the interface it claims?

Open the real interface and compare it declaration by declaration. A contract saying it implements IERC20 is a sentence in a comment, not a promise the compiler checked, unless the contract actually inherits the interface. Line the two files up and the differences stop being subtle.

Here is the real thing, OpenZeppelin's IERC20, documentation left out:

interface IERC20 {
    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address to, uint256 value) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 value) external returns (bool);

    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

And the two matching lines from the generated file, bodies left off, again model output:

    function transfer(address to, uint256 value) public returns (bool) {

    function approve(address spender, uint256 value) public {

transfer matches: same name, same arguments, same bool coming back. approve does not. The standard says returns (bool), this one returns nothing, and any contract calling approve and reading a result fails to compile against it.

That difference is invisible in prose, and the model's own summary said the token implements the interface exactly. Side by side it takes two seconds. It is also why is matters: contract SimpleToken is IERC20 would have been refused by the compiler, while a contract naming the standard only in a comment compiles happily. See interface vs abstract contract.

How do you trace a name you do not recognise?

Three places, in order. The contract line says what it inherits, the import lines say which files came in, and the file itself holds whatever is defined locally. A name in none of the three is defined nowhere, and generated code produces exactly that more often than you would expect.

A function from the same generated file. Model output, not a deployed contract.

    function burn(uint256 value) public {
        balanceOf[msg.sender] -= value;
        totalSupply -= value;
        _afterBurn(msg.sender, value);
        emit Transfer(msg.sender, address(0), value);
    }

Three of those names resolve at once. balanceOf and totalSupply are in the state list you read first, and Transfer is an event declared in the file. _afterBurn is none of those. It is declared nowhere, there are no imports, and the contract line inherits nothing, so the name points at nothing. The leading underscore makes it look like a familiar internal hook, and that look is why it survives a skim.

The habit matters more than the instance: every identifier resolves somewhere, and you should be able to name where. See inheritance in Solidity for what is and import bring in.

Why should you read every comment as a claim?

Because a comment is text the compiler ignores, so nothing keeps it honest. Hand-written comments drift out of date. A generated comment and the body under it came from one prompt together, so a disagreement between them tells you the model held two incompatible ideas at once.

The same burn function, with the documentation the model wrote above it:

    /// Burns `value` tokens from the caller. The supply is fixed at construction,
    /// so `totalSupply` is left unchanged here.
    function burn(uint256 value) public {
        balanceOf[msg.sender] -= value;
        totalSupply -= value;

The comment says totalSupply is left unchanged. The second line of the body changes totalSupply. One is what the code does, the other is what the model believed it wrote, and nothing in the file says which was intended.

The same applies inside a refusal, where only the condition is ever enforced:

        require(balanceOf[msg.sender] >= value, "SimpleToken: amount exceeds allowance");

The condition checks a balance. The message says allowance. Those are different tables holding different things, so the sentence a caller receives describes a check that never ran. Read every refusal against the expression to its left, not the story in quotation marks. See require and revert.

What did the model decide for you without saying so?

Ask the question explicitly, because silent decisions do not announce themselves. A prompt is a paragraph and a contract is a specification, so the gap between them gets filled with defaults you never chose and will not notice unless you go looking for them.

Four questions cover most of that gap. Is the operation all-or-nothing, or does it quietly do part of the job when it cannot do all of it? Which token or address is involved, when your request named a kind rather than a specific one? Who may call each function, given that a prompt rarely says? And what happens at zero, the value nobody describes and every contract eventually receives. Every answer sits in text you have read.

Now the honest close. Everything here catches what is visible in the text. It does not tell you what the code does when it runs. A file can pass all seven passes and still be wrong in ways that appear only on execution. That is what tests are for, at a higher standard than most people apply: a test that would pass whether or not the code is right has proved nothing. Ask of every test what it would report if the function underneath were broken. If the answer is "the same thing", it is decoration.

Delete the line and ask what breaks

  • Delete returns (bool) from a transfer declaration and the contract still compiles alone, while every caller written against the standard stops compiling against it. The break lands on someone else's machine.
  • Delete is IERC20 from a contract claiming to implement the standard and you lose the only mechanical check that its signatures match. What remains is a comment, and comments are not checked.
  • Delete the line defining an internal hook and the contract calling it will not build. That is the pleasant case: the compiler catches names, so a missing one costs minutes.
  • Delete the message from a require and enforcement is unchanged, because only the condition ever ran. You lose the explanation a caller receives, which is why a wrong message is worth fixing and never worth believing.

Related questions

Can an AI write safe smart contract code? It writes code that compiles and usually does roughly what was asked. Whether it behaves correctly under every input is not something fluency predicts, and the answers come from reading the output and testing it.

Do I still need to learn Solidity if a model writes it for me? Yes, and reading is the part you need most. You are the reviewer of everything it produces, and nobody reviews a language they cannot read.

Why does generated code look more correct than it is? Because it comes from a system optimised for plausible text, and plausible code carries every marker of finished work: consistent naming, tidy comments, familiar structure. Those markers are what a skim checks for.

What is the fastest check on a generated contract? Open the interface it claims to implement and compare signatures line by line. Under a minute, nothing installed, and it catches a mismatch that is hard to see in running prose.

Should I trust the tests an AI writes alongside the contract? Read them with one question: what would this report if the function it covers were wrong? Generated tests often assert that a call did not fail rather than that it did the right thing, and a test passing either way proves nothing.

Is reviewing generated code different from reviewing a colleague's code? The method is identical, the distribution of mistakes is not. A colleague rarely invents a function that does not exist or writes a comment contradicting the line beneath it. A model does both while sounding certain.

Where to go next

The checklist is short enough to keep in your head: state, signatures, claimed interface against the real one, unresolved names, comments, refusals, silent decisions. In that order, most of what a model gets wrong is visible before you open a terminal.

If the reading is still effortful, go back to the method on real code with how to read a smart contract, because reviewing output is that method pointed at a new source. For the other half, where code proves itself rather than looks right, there is Your First 90 Days with Solidity, a free, guided course by the security firm Zealynx. Its closing act puts a model's answer on the board beside the real interface and walks the lens down it, then asks the harder question: what would this test have said if the contract were wrong?

Tagged

SoliditySmart ContractsLearn to Code