6 minutes
EVM - Out of the Bytecode
There are many guides and videos explaining how the EVM works on the inside, but almost all of of these focus only on smart contract execution. Even though this is the most important part of the EVM and the one that most smart contract developers care about, I think it’s relevant to talk about what happens out of code execution. In this article we’ll focus on that. Some basic knowledge of the EVM is required.
Introduction
Remember that the EVM can be thought of as a system that computes the State Transition Function STF, that given a state S and a transaction T it generates a new state S’. This can be summarized as f(S, T) = S’
If we go inside this function we can think of it as three steps that that go one after another:
- Pre-execution: Transaction validation and initial state changes.
- Execution: Running bytecode and precompiles.
- Post-execution: Final state changes.
Execution only happens when there’s something to execute, whereas the other two phases occur every time; these two are the parts that happen Out of the Bytecode.
Pre-execution
Validations
At the beginning, the EVM will perform all kinds of validations on the Transaction to see if it’s invalid. If that’s the case, it doesn’t even get executed and is discarded without making any change to the world state, as if nothing ever happened.
There are multiple validations but some of the most basic ones are:
- Nonce Mismatch: The nonce of the transaction doesn’t match the current nonce of the sender.
- Insufficient Account Funds: The sender doesn’t have enough ETH to pay for the transaction’s upfront cost1.
- Sender is not an EOA: The sender is not an Externally Owned Account. Any external transaction coming from a contract will be rejected.
Initial State Changes
If the transaction is valid the EVM will then proceed to make some basic changes to the state of sender and recipient accounts, plus some other things related to gas usage. These are:
- Increment sender nonce by one: Irreversible change even if transaction gets reverted (e.g. due to running out of gas), because reverted transactions are still recorded on the blockchain. Nonces must be unique in order to avoid replay attacks.
- Subtract upfront cost from sender balance
- Transfer transaction value (ETH) to the recipient
- Consume intrinsic gas: This is the gas that’s consumed always before execution. For a simple ETH transfer it’s usually 21000, but more complex transactions may consume even more. As of Prague the cost is calculated like this.
- Warm up accounts: For gas cost reasons we qualify accounts into cold or warm. Accessing a warm account is much cheaper than accessing a cold one. At the beginning of every transaction accounts like the sender, recipient, and others are automatically warmed up. Other accounts and storage slots can be warmed up via access lists.
- Set Delegate Accounts: In case the transaction is of “Type 4” there’s additional behavior in this phase, we process something called the “authorization list”, which is a feature that basically allows EOAs to have a contract associated to them, so that when a transaction is sent to them the contract code is executed. This process is called Account Delegation. You can read more about it here.
- If it’s a CREATE transaction and an account already exists at the derived address, there’s a collision and the transaction will revert. This is virtually impossible due to how contract addresses are computed:
keccak256(rlp([sender, nonce]))[12:].
After doing all of these things we’ll then continue with contract execution. Note that in a basic ETH transfer between EOAs it’s very unlikely that any code will be executed at all, since the average user doesn’t usually have a delegated contract–at least for now.
Execution
As I said, we are not going to focus on the details of execution here. For that there already are many other amazing guides that explain how the Stack, Memory and other components of the EVM work. The most important thing that you need to know is that the final goal of executing all this code is generally making a change to the world state, some of the most relevant opcodes for this purpose are:
- SSTORE: Writes to storage. It triggers gas refunds when setting a non-zero slot to zero.
- CREATE/CREATE2: Deploys a new contract
- CALL: Performs an internal transaction
- SELFDESTRUCT: Registers a contract that will be destroyed in Post-execution and sends all its ETH to the specified address.
- LOG: Generates logs. Doesn’t change state of accounts but these are part of the transaction receipt that is finally stored in the receipt trie.
If execution is successful, the changes are applied to the state; otherwise, they’re discarded — a.k.a. reverted.
We make a distinction between a revert triggered by the REVERT opcode and the one triggered exceptionally, this one is called Exceptional Halt. The main difference between these is that the latter consumes all gas left whereas the former doesn’t.
Some errors that can trigger an Exceptional Halt are:
- Invalid Opcode: tried to execute an opcode that’s not valid.
- Out of Gas: gas used exceeded the gas limit.
- Stack Underflow: tried to pop a value out of the stack when it was empty.
A revert also undoes the value transfer that was done in the pre-execution phase. However, some actions like incrementing sender nonce and subtracting the upfront cost are irreversible.
Post-execution
Independently of the outcome of the execution we have to make a few more changes at the end.
Remember how at the beginning we decreased the balance of the sender by the upfront cost? Well, now it’s time to give back what hasn’t been consumed during execution. To simplify it we say that we give (unused_gas + gas_refunds) * gas_price back to the user. But in reality there are some constraints that don’t make the math as straightforward as this, such as gas refunds being capped to 20% of the gas used; more than that won’t be refunded to the sender. There is also a minimum amount of gas to consume on each transaction, which will affect the unused_gas variable.
Additionally, we have to pay the transaction fees to the coinbase2 address, this is known as the tip or priority fee.
Last but not least, we destroy all the contracts that executed the SELFDESTRUCT opcode during the execution phase.
-
The upfront cost is the maximum amount of ETH that the user is willing to pay for. It’s currently calculated like:
gas_limit * gas_price + value + blob_gas_cost, the last field is non-zero only in Type 3 transactions. ↩︎ -
The coinbase address is the one that gets the block fees/rewards. In Ethereum Proof of Stake is the address set by the validator. ↩︎
2025-07-07 (Last updated: 2025-07-08)