Understanding EVM Bytecode, ABI, and Gas Costs
Introduction to EVM Bytecode
When we write smart contracts in Solidity or Vyper, they need to be translated into a format that the Ethereum Virtual Machine (EVM) can understand and execute. This is where bytecode comes in — the low-level machine instructions that the EVM actually processes when running smart contracts.
Understanding bytecode is like peeking under the hood of Ethereum's execution environment. While most developers won't interact with bytecode directly, having knowledge of how it works helps build more efficient smart contracts and debug complex issues.
What is EVM Bytecode?
EVM bytecode is a sequence of hexadecimal values that represent a series of opcodes (operation codes) and their parameters. Each opcode is a single byte that tells the EVM what operation to perform, such as:
- PUSH1, PUSH2, etc. - Push values onto the stack
- ADD, SUB, MUL, DIV - Arithmetic operations
- SLOAD, SSTORE - Storage operations
- CALL, DELEGATECALL - Contract interaction
- JUMPI, JUMP - Control flow
Here's an example of what a simple bytecode sequence might look like:
0x6080604052348015600f57600080fd5b5060ac8061001e6000396000f3fe...This might look like gibberish at first glance, but this is actually a compiled contract that the EVM can execute. Each pair of hexadecimal characters represents one byte, and each byte can represent an opcode or data.

The Compilation Process
The journey from Solidity to bytecode involves several steps:
- Parsing: The Solidity compiler parses the source code into an abstract syntax tree (AST)
- Analysis: The compiler performs semantic analysis and type checking
- Optimization: Various optimizations are applied to make the code more efficient
- Code Generation: The compiler generates EVM bytecode from the optimized representation
Understanding the ABI (Application Binary Interface)
While bytecode is what the EVM executes, we need a way for our applications to interact with smart contracts. This is where the ABI comes in. The ABI is a JSON format that describes the contract's functions and events in a way that external applications can understand.

What's in an ABI?
An ABI typically includes:
- Function signatures: Name, input parameters, and return types
- Function types: Whether a function is pure, view, payable, etc.
- Event definitions: The structure of events the contract can emit
Here's an example of what an ABI entry for a function might look like:
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}Function Selectors
When you call a function on a smart contract, the EVM needs to know which function you want to execute. This is done using a function selector, which is the first 4 bytes of the keccak256 hash of the function signature.
For example, the function selector for deposit(uint256) would be:
0xb6b55f25 = bytes4(keccak256("deposit(uint256)"))When calling a contract, the first 4 bytes of the calldata will be this selector, followed by the ABI-encoded function arguments.

Gas Costs and Optimization
Every operation in the EVM costs gas, which is a measure of computational effort. Users pay for this gas when they interact with smart contracts, which is why optimizing for gas efficiency is crucial in Ethereum development.
Operation Gas Costs
Different operations have different gas costs, generally reflecting how computationally expensive they are:
- SSTORE (first time): 20,000 gas - extremely expensive because it writes to permanent storage
- CALL: 700+ gas - expensive because it involves message passing between contracts
- ADD/SUB: 3 gas - relatively cheap arithmetic operations
- PUSH1: 3 gas - simple operation to push a 1-byte value onto the stack

Gas Optimization Techniques
To minimize gas costs, developers employ various techniques:
- Use memory over storage: Memory operations are much cheaper than storage operations
- Batch operations: Performing multiple operations in one transaction can save gas
- Avoid loops with unknown bounds: These can lead to unpredictable gas costs or even out-of-gas errors
- Use bytes32 instead of string when possible: Fixed-size types are cheaper to work with
- Optimize contract logic: Fewer operations mean less gas
Try the Gas Estimator Tool
This interactive tool helps you visualize and calculate gas costs for different Ethereum operations. Try it out to get a better understanding of gas prices.
Gas Estimation
Before executing a transaction, it's important to estimate how much gas it will require. This helps ensure that transactions won't fail due to out-of-gas errors and that users don't overpay for gas.
Most Ethereum client libraries provide functions for gas estimation:
// Using ethers.js
const gasEstimate = await contract.estimateGas.deposit(amount);
// Using web3.js
const gasEstimate = await contract.methods.deposit(amount).estimateGas();Practical Examples
Example 1: Analyzing Bytecode
Let's look at how we might analyze bytecode to understand what a contract does:
// This bytecode pushes the value 10 onto the stack, pushes 5, adds them, and returns the result
0x600a600501600052600160206000f3Breaking this down:
0x60: PUSH1 opcode0x0a: The value 10 in hexadecimal0x60: PUSH1 opcode0x05: The value 50x01: ADD opcode- ... and so on, with more operations to store and return the result
Example 2: ABI Encoding
If we want to call the deposit(uint256) function with the value 100, the ABI-encoded calldata would look like:
0xb6b55f25000000000000000000000000000000000000000000000000000000000000006aBreaking this down:
0xb6b55f25: The function selector for deposit(uint256)000000...006a: The ABI-encoded value 100 (0x64 in hexadecimal, padded to 32 bytes)
Advanced Topics
Contract Creation Code vs. Runtime Code
When a contract is deployed, there are actually two sets of bytecode involved:
- Creation Code: Code that runs during contract deployment and returns the runtime code
- Runtime Code: The actual code that gets stored on the blockchain and runs when the contract is called
Inline Assembly
For advanced gas optimizations, Solidity allows writing inline assembly using the assembly block:
function addAssembly(uint x, uint y) public pure returns (uint) {
assembly {
// Load arguments from calldata
let result := add(x, y)
// Return the result
mstore(0x0, result)
return(0x0, 32)
}
}This gives you direct access to EVM opcodes, which can lead to more gas-efficient code when used carefully.
Conclusion
Understanding EVM bytecode, ABI, and gas costs is essential for becoming an advanced Ethereum developer. While you might not interact with bytecode directly in your day-to-day development, having this knowledge allows you to:
- Write more gas-efficient contracts
- Debug complex issues
- Understand security vulnerabilities at a deeper level
- Optimize your application's interactions with smart contracts
As you continue your blockchain journey, I encourage you to explore these topics further. Tools like Remix and Etherscan provide bytecode explorers that can help you visualize and understand the compiled code of your contracts. Happy coding!
"To truly master Ethereum development, you must understand not just what your code does, but how it translates to the bytecode that the EVM actually executes." — Vitalik Buterin