We reject abstraction
We believe in bytecode
Bytecode is only true form
Let’s code with bytes now
Welcome back to Bytecode Tuesday, where we pull back the curtain on what your smart contract is really doing under the hood. Last week, we looked at functions, how they're organized in bytecode and how the EVM routes execution. This week, we take on one of the most powerful opcodes in the EVM toolbox: CALL
.
CALL lets your smart contract talk to other contracts (or even itself) on the Ethereum blockchain.
Do you want to interact with an ERC20 token? Or invoke another contract’s function? Or even delegate work across contracts? You can do that with the CALL
opcode.
But CALL
isn’t just one thing, it’s a low-level instruction with a lot of parameters. Think of it like dialing a phone number where you also have to specify how much credit you’re willing to spend, what data to send, and where to store the response.
CALL
's bytecode is F1
but before the EVM can run it, you need to set up the stack with 7 arguments, in this exact order (from top to bottom of the stack):
Stack Position (top to bottom) | Meaning |
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
Once you push all that onto the stack, CALL consumes it and tries to make the call. If it succeeds, it pushes 1 onto the stack. If it fails, it pushes 0.
When one contract uses the CALL opcode to invoke another, two key pieces of data flow across:
Input data (calldata): What we send to the other contract (e.g. function selector + parameters).
Return data: What we get back from the other contract (if any).
Let’s say we want to call another contract with no ETH, send 4 bytes of data (a function selector), and expect 32 bytes back.
Here’s the bytecode to make that happen:
PUSH1 0x20 // return data length = 32
PUSH0 // return data offset = 0
PUSH1 0x04 // call data length = 4
PUSH0 // call data offset = 0
PUSH0 // value to send = 0
PUSH20 <address> // the contract address
PUSH2 0xFFFF // gas = 65535 (for example)
CALL
We assume the calldata (like a function selector) is already stored in memory at offset 0x00
.
And the actual bytecode form would be: 60205F60045F5F73<address>61FFFFF1
.
helloWorld()
function (selector 0xC605F76C
) at contract 0xBA5EBA11BA5EBA11BA5EBA11BA5EBA11BA5EBA11
After the CALL
, the result (success or failure) will be on the top of the stack.
Unlike Solidity, which reverts on failed external calls by default, raw CALL
does not. If the call fails, you just get 0 on the stack and it’s up to you to decide what to do.
So if you want to revert on error, you must explicitly check.
Here’s a quick table of the family of call-related opcodes:
Opcode | Name | Description |
|
| Call another contract |
|
| Legacy version, now discouraged |
|
| Runs code in the context of the caller |
|
| Like CALL but disallows state modification |
The CALL
opcode is low-level and flexible. It exposes everything gas, calldata, return handling, and value transfers. While Solidity wraps it in nice syntax, EVM developers get to shape the whole interaction exactly how they want.
See you next Tuesday for our last Bytecode Tuesday release!