EIP-140: REVERT — failing cheaply, and saying why
Before Byzantium, a contract that hit a bad condition had one blunt option: throw — which undid its work but also torched every drop of the caller's remaining gas and returned no explanation. REVERT keeps the undo, gives the gas back, and lets the contract return an error. Every require("reason"), custom error, and try/catch you've ever written descends from it.
- the EVM, gas & the call model
- state changes
Byzantium, October 2017. A big fork — it brought the cryptographic precompiles that made on-chain zero-knowledge proofs possible and a call type that can’t touch state. But the change that every Solidity developer feels daily is smaller and more humble: a new way to fail. Up to here, a contract that detected a bad condition — an overdrawn balance, a failed permission check — could only throw, and throwing was a scorched-earth act: it undid the work, but it also burned every unit of gas the caller had left and handed back not one byte of explanation. EIP-140 introduces REVERT, and with it, failure becomes cheap, precise, and legible. Let’s derive it.
The problem: aborting cost everything and said nothing
A contract call either succeeds and commits its state changes, or it aborts and they must be undone — you never want a half-applied transfer. Before Byzantium the only way to abort was to throw The pre-Byzantium way to abort: execute an invalid instruction (or Solidity's throw), which reverted the frame's state changes but consumed all remaining gas and returned no data. — typically by hitting an invalid opcode. That did undo the state, which is correct. But it came with two brutal side effects: it consumed all the caller’s remaining gas, and it returned no data at all. So a perfectly reasonable require(balance >= amount) that simply wasn’t met would burn the user’s entire gas budget and leave them staring at a bare “transaction failed” with no reason.
→ Step 2: keep the undo, drop the punishment, add a reason.
REVERT: three fixes in one opcode
REVERT The Byzantium opcode 0xfd. It halts the current call, reverts all of that frame's state changes atomically, returns the unused gas to the caller, and returns a data blob (offset, length) — used to carry an error reason. (opcode 0xfd) does exactly what we asked for, three things at once. It reverts all state changes made in the current call frame — atomically, as if the frame never ran. It refunds the remaining gas instead of burning it, so an expected failure costs only the gas actually used up to that point. And it returns data — an arbitrary (offset, length) blob from memory — which is where an error string or a structured error code rides back out.
- 1 every SSTORE, balance change, and log in this frame is reverted — atomic
- 2 the unused gas goes back to the caller; only gas already spent is lost
- 3 a memory blob bubbles up — the error string or custom-error data
→ Step 3: let the caller catch it.
The payoff: a failure the caller can catch and read
Because REVERT returns data and doesn’t nuke the transaction wholesale, a sub-call that reverts comes back to its caller as an ordinary failed call carrying its reason — not a chain-killing detonation. The callee’s own state changes are undone, control returns to the caller, and the caller gets to decide: propagate the error up, or handle it. That is precisely the machinery behind the error-handling you write every day.