Cover photo

This is how smart contracts call each other under the hood

filosofiacodigo.eth

Cooldev1337 and filosofiacodigo.eth

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.

What is 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.

Anatomy of a CALL

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

gas Amount of gas to forward

2

to Address to call

3

value ETH to send (in wei)

4

argsOffset Where input starts in memory

5

argsLength Length of input data

6

retOffset Where to store return data

7

retLength Max return size

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.

What Happens in a CALL?

When one contract uses the CALL opcode to invoke another, two key pieces of data flow across:

  1. Input data (calldata): What we send to the other contract (e.g. function selector + parameters).

  2. Return data: What we get back from the other contract (if any).

Let’s See an Example

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.

post image
Step by step animation of making a contract call to the helloWorld() function (selector 0xC605F76C) at contract 0xBA5EBA11BA5EBA11BA5EBA11BA5EBA11BA5EBA11

After the CALL, the result (success or failure) will be on the top of the stack.

Error Handling with CALL

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.

Other CALL Opcodes

Here’s a quick table of the family of call-related opcodes:

Opcode

Name

Description

F1

CALL

Call another contract

F2

CALLCODE

Legacy version, now discouraged

F4

DELEGATECALL

Runs code in the context of the caller

FA

STATICCALL

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!

This is how smart contracts call each other under the hood