What Actually Happens When Calldata Hits the EVM
I’m currently open to collaborations and development projects across blockchain, smart contracts, and full-stack systems, feel free to connect if you’re building something interesting
When you call a smart contract function like
set(42), it feels simple, you pick a function, pass an argument, and click “Send”.But the Ethereum Virtual Machine (EVM) doesn’t see your function name or your variable names.
What it actually receives is a blob of raw bytes called calldata: a hex-encoded payload that encodes the function selector and arguments.In this post, we’ll peel back the layers and see what happens the moment that calldata reaches the EVM.
We’ll look at the exact opcodes that Solidity generates, understand how the EVM extracts the selector, and see how your function call is routed step by step through the contract’s dispatch logic.
What actually happens when calldata hits the EVM
In this post we understood how calldata is packed for the EVM execution and now we will understand what happens when a smart contract is called on the EVM, the Solidity function name and arguments are not what the EVM sees. Instead, the data field in the transaction is delivered as raw hex-encoded bytes, which the EVM loads into a special, read-only area called calldata.
This section breaks down what the EVM actually does the moment calldata arrives. Here’s a simple contract that was compiled with solidity compiler version 0.4.25 without optimization to minimum and basic opcodes that we can cover with ease:
pragma solidity 0.4.25;
contract Example {
uint data;
function set(uint x) public {
data = x;
}
function get() public view returns (uint) {
return data;
}
}Full compiled smart contract bytecode and opcodes:
bytecode: “0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e64cec114604e5780636057361d146076575b600080fd5b348015605957600080fd5b50606060a0565b6040518082815260200191505060405180910390f35b348015608157600080fd5b50609e6004803603810190808035906020019092919050505060a9565b005b60008054905090565b80600081905550505600a165627a7a7230582078de35703c2e2e542f27462c54cc554bfaebcea8f777f7df9e1eb1a59a3628660029”
opcodes: “PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xDF DUP1 PUSH2 0x1F PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x49 JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x2E64CEC1 EQ PUSH1 0x4E JUMPI DUP1 PUSH4 0x6057361D EQ PUSH1 0x76 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x59 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x60 PUSH1 0xA0 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x81 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x9E PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0xA9 JUMP JUMPDEST STOP JUMPDEST PUSH1 0x0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 PUSH25 0xDE35703C2E2E542F27462C54CC554BFAEBCEA8F777F7DF9E1E 0xb1 0xa5 SWAP11 CALLDATASIZE 0x28 PUSH7 0x290000000000”What happens next:
We will skip boilerplate instructions and focus on the critical opcodes involved in dispatch and execution opcodes in the process of the contract execution.
PUSH1 0x80
PUSH1 0x40
MSTOREThese first instructions initialize memory. The EVM uses position 0x40 as a free memory pointer. It stores 0x80 there as the starting offset for future dynamic memory allocations. But why store 0x80 at 0x40? This is the Solidity convention for the “free memory pointer”. The memory location 0x40 is reserved to store a pointer to the next available free space in memory. At the beginning of execution, Solidity sets this pointer to 0x80 to skip the lower memory region (which is often used for scratch variables or built-ins). So when future operations want to allocate memory, they look at mload(0x40), get 0x80, and start writing there.
CALLVALUE ; Pushes msg.value to the stack
DUP1 ; Duplicates it (so we can use it twice)
ISZERO ; Checks if msg.value == 0
PUSH2 0x0010 ; If zero, jump to the actual logic (offset 0x10)
JUMPI ; Conditional jump (if value is zero, we’re good)PUSH1 0x00 ; Set offset = 0 for revert data
DUP1 ; Set length = 0 (no error message)
REVERT ; Abort the transaction with no messageWhy it exists:
Ensures that non-payable functions revert if any ETH is sent
Prevents accidental ETH loss due to incorrect calls
Keeps contract behavior predictable and secure
This pattern is auto-generated by the Solidity compiler for all non-payable functions and appears at the very beginning of the function dispatch logic.
... ; [will not be covered here, not so important]
PUSH1 0x4 ; [top] Push constant 4 — minimum selector length
CALLDATASIZE ; [next] Push the size of calldata
LT ; Compare: is calldataSize < 4?
PUSH1 0x49 ; If true, jump to fallback
JUMPI
CALLDATALOAD ; Load the first 32 bytes of calldata onto the stack
PUSH29 0x100000000000000000000000000000000000000000000000000000000 ; Bitmask: isolates the top 4 bytes (first 4 bytes of calldata)
SWAP1 ; Swap mask and calldata so mask is on top
DIV ; Shifts the selector down by 28 bytes (removes trailing zeros)
PUSH4 0xffffffff ; 0xFFFFFFFF mask to ensure only the first 4 bytes remain
AND ; Final cleanup to get the 4-byte selector exactlyWhat’s happening:
The EVM checks if there are at least 4 bytes of calldata (minimum for a function selector).
If not → jump to fallback logic.
Otherwise → extract the selector by:
Loading the first 32 bytes of calldata,
Shifting the selector down using
DIV,Cleaning the rest with
AND.
This ensures the contract can safely and precisely identify which function to execute.
DUP1 ; Duplicate selector
PUSH4 0x2E64CEC1 ; First function selector in contract
EQ ; Is it equal?
PUSH1 0x4E
JUMPI ; If yes, jump to that function’s code
DUP1 ; Still same selector on stack
PUSH4 0x6057361D ; Second function selector
EQ
PUSH1 0x76
JUMPI ; If match, jump to other function
JUMPDEST ; If none matched...
PUSH1 0x00
DUP1
REVERT ; Revert: function not found
... ; [will not be covered here]This is the function dispatcher, it’s like a manual switch-case for function routing in bytecode. This ensures invalid or unknown function calls are safely rejected.
Summary
In this section, we unpacked what really happens when a contract function is called on the EVM from how calldata is parsed and routed, to how the ABI structures data, to what happens during nested and failed calls. With this foundation, you’ll be able to debug, reason about, and even optimize low-level smart contract behavior much more confidently.

