Writing Solidity with AI: The Bottleneck Moved to Reading
A model writes Solidity that compiles and usually works. The hard part is no longer producing the code, it is judging what came back, because plausible code is the hardest kind to check.
TL;DR
- Yes, a model writes Solidity that compiles and usually works. The bottleneck moved. It is no longer typing, it is judging what came back.
- A model produces plausible code, and plausible is the hardest kind to check, because nothing looks wrong. A syntax error announces itself. A different design decision does not.
- The loop is three moves: say what you want, read what came back, make it prove itself. Only the first feels like work, and it is the least important.
- A good request names the interface, the units, what must revert, what must not be in the file, what it may assume. Pointing at a real file beats any adjective.
- You cannot supervise code you cannot read, so reading got more valuable the moment writing got cheap.
Can AI write Solidity?
Yes. Ask a current coding assistant for an ERC20 token and you get back forty or so lines that compile, deploy and move balances correctly. The code is not wrong. The difficulty is that it is not obviously right either, and telling those two apart is now the whole job.
The usual framing is a fight about whether the output is good or bad. It is neither. It is a draft embodying decisions, made silently and fast, by something with no access to your requirements beyond the sentence you typed. That is not new to software. What is new to Solidity is that a questionable decision in a web app gets patched on Tuesday, and one in a deployed contract never does.
Why is AI-generated Solidity hard to review?
Because it is plausible. Plausible code is harder to check than bad code, since bad code fails loudly and plausible code fails silently, months later, in a way nobody predicted. Nothing catches your eye. Every line reads like something a competent developer would write, because statistically that is exactly what it is.
Consider what your eye is trained to do with code. It hunts the jarring thing: the misspelled name, the mismatched bracket, the obviously missing check. That reflex works against human error, because human error looks like error.
A model rarely makes that mistake. It produces a coherent, conventional-looking file differing from what you would have written in three or four places you never thought to look. None is a bug. Each is a decision, and decisions stay invisible until you set them beside an alternative. Which is why this demands reading rather than proofreading: you are hunting choices, not faults.
What is the loop for writing Solidity with an assistant?
Three moves, in order. Say what you want, with enough specificity that a stranger could check the result against it. Read what came back line by line, as unfamiliar code rather than as your own. Then make it prove itself, with cases you chose rather than ones it suggested.
Most people do the first move at length and the other two barely at all, which inverts the value. Writing the request feels like the work because it is where you type. It is also where a model tolerates the most vagueness, filling any gap you leave with a reasonable guess.
The second move is where the value sits. Reading code you did not write is a specific skill with a method that does not care what produced the code: the line-by-line method for reading a smart contract applies unchanged.
The third exists because reading has a ceiling: you can read a function carefully and still be wrong about it at a boundary. The useful tests are the ones you thought of while reading, not the ones a model volunteered alongside the code it is grading itself on.
What does AI-written Solidity get different from OpenZeppelin?
Set a model's token next to the reference implementation and the differences are not mistakes. They are decisions: where balances get written, what a refusal carries with it, and whether anyone can extend the contract later. All three stay invisible until you have both files open.
Here is transfer from what an assistant returned when asked for a simple ERC20 token. This block is model output. It comes from no deployed contract and has no upstream repository, which is why it is labelled.
function transfer(address to, uint256 value) public returns (bool) {
require(to != address(0), "MyToken: transfer to the zero address");
require(balanceOf[msg.sender] >= value, "MyToken: insufficient balance");
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
Seven lines, readable, and it works. Now the same job in OpenZeppelin's ERC20, the file most deployed tokens inherit from:
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
Four lines, three of which hand the work elsewhere. Follow _transfer for the refusals:
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
And _update is where a balance is finally allowed to move:
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
Three differences, and not one of them is a fault.
Balances move in two places rather than through one funnel. The model writes balanceOf directly inside transfer, so transferFrom writes it again, and so would minting and burning. The reference routes every balance change through _update: one place where supply and balances can change, one place to look.
Refusals carry a string rather than values. "MyToken: insufficient balance" says something was insufficient. ERC20InsufficientBalance(from, fromBalance, value) says who, how much they had and how much they wanted, as data a caller can act on. Both refuse the transfer. Only one is usable by the contract calling you. See require and revert.
Nothing is marked virtual, so nothing can extend it. The reference marks transfer and _update virtual, the word permitting a child contract to replace them. Without it, the only way to add a fee, a pause or a cap is to edit the file itself. See Solidity inheritance.
For a token nobody will extend, the model's version may be the better answer: shorter, flatter, readable in one sitting. That is the point. These are trade-offs somebody has to make deliberately, and right now nobody did, because you never said which way you wanted them.
What should you put in a Solidity prompt?
Five things, every time. The interface or standard to match. The units and their types. What must revert, and with what. What must not be in the file. What it may assume about callers and about the chain. Everything you leave out is a decision you have delegated.
The interface or standard. Stronger than any adjective. "Make it standard" is not checkable. "Match IERC20 exactly as declared at this commit, and add nothing beyond it" is checkable by anyone in a minute. A file is unambiguous in a way a description never is.
The units and their types. Wei or whole tokens, basis points or a fixed-point fraction, and what width each is. Unit confusion produces no compiler error. It produces a number off by ten thousand that looks completely normal.
What must revert. Enumerate the refusals you require and whether they carry data. Left unstated, you get whichever refusals are conventional, which may be all the ones you needed or most of them.
What must not be in the file. The one people skip, and the one that matters most. No owner. No pause. No upgrade path. No mint after deployment. An unrequested privileged role is not a bug, it is a decision you never made.
What it may assume. Whether callers are trusted, whether an external token is well behaved, whether a value comes from a source you control. Assumptions stated in the request become assumptions you can check.
Write all five and the request stops being a wish and becomes a specification. Its value is not better code, but something concrete to check the code against.
Do you still need to learn Solidity if AI writes it?
More than before, and the reason is structural rather than sentimental. Every one of the three moves in the loop except the typing requires you to read Solidity. You cannot specify what you cannot describe, you cannot review what you cannot read, and you cannot test what you do not understand.
What models remove is not the need to learn the language but the need to learn the toolchain first. A working contract can be in front of you in seconds, where it used to take a day of setup, and what stands between you and understanding it is now vocabulary rather than infrastructure. That usefully inverts the traditional curriculum, and it is the argument in learning Solidity by reading it, made urgent by tools generating more code than anyone can carefully write. Generation is free. Judgment is not, and it is entirely a reading skill.
Whether the assistant is Claude Code, Codex, or whatever replaces both, this does not change, because it is a fact about code review rather than about any tool.
Delete the line and ask what breaks
- Delete
virtualfrom the reference'stransferand nobody can extend the token. The model's version never had the word, so deleting it changes nothing. An absence looks identical to a deliberate choice. - Inline
_updateintotransferand the contract behaves identically today. You lose the one place every balance change must pass, which is where a reviewer looks first. - Replace
ERC20InsufficientBalance(from, fromBalance, value)with a plain string and every test still passes. The contract that wanted to know how short the balance was cannot find out. - Remove the zero-address check from the model's
transferand tokens go where nobody controls them. Nothing errors. The supply just becomes permanently smaller than it claims.
Related questions
Can ChatGPT or Claude write a smart contract that works? Yes, routinely, for common contract shapes. Working and being right for your situation are different claims. The output encodes decisions about extensibility, refusals, privileged roles and units that were never in your request.
Is AI-generated Solidity safe to deploy? Not without review, and not because models are bad at Solidity. Deployment is irreversible and the code embeds choices nobody stated. Treat it as code from a contractor you have never met.
What is the best prompt for generating Solidity? The one naming a real file to match, stating units and types, enumerating required refusals, listing what must be absent, declaring what may be assumed. Pointing at an interface beats describing one.
Does AI replace the need to learn Solidity? No. It replaces the need to learn the toolchain before the language, a genuine improvement. Reviewing generated code needs the same fluency as reviewing anyone's, and it arrives in larger volumes.
Why does AI-generated code look correct even when it is not right for me? Because it is trained on conventional, competent code, so it produces conventional, competent code. The failure mode is not error, it is a reasonable default applied to a situation you never described.
Should I use AI for reviewing as well as for writing? Writing and reviewing are different tasks with different failure modes, and reviewing has its own method. How to review AI-generated Solidity covers that side.
Where to go next
This shift makes the easy half easier and leaves the hard half where it was. Producing Solidity is close to free. Knowing whether it makes the decisions you wanted is unchanged, and now must happen more often, on more code than anyone can read.
The response is not better prompts, although you should write them. It is getting good enough at reading that a generated file stops being an object of faith. The course below sets a model's token beside a real one and walks the difference line by line, which is the exercise that makes invisible decisions visible.
Tagged