EIP-7979 EVM call and return instructions (Call and Return Opcodes for the EVM)
Hegota Considered for Inclusion · EVM & execution · EIP status Review · created 2025-12-17
Adds three instructions to the EVM for subroutine calls and returns: CALLSUB, CALLDEST and RETURNSUB. It makes function calls, which were previously imitated with dynamic jumps, explicit, saving code and gas and making analysis easier.
Why it is needed
Today's EVM has no dedicated instruction for calling a function and returning from it. Compilers imitate calls and returns by combining dynamic jump (JUMP) instructions, which take their destination from the stack. This approach has two problems. First, chaining several jumps together wastes bytecode space and gas. Second, and more importantly, the return address is treated as data, so the control flow of the code is hidden from both people and tools. A tool analyzing the code must mark every possible destination, which can take time proportional to the square of the code size, and following every path in practice can take exponential time or worse. On Ethereum, worse-than-linear complexity becomes a denial-of-service (DoS) vulnerability for online static analysis such as bytecode validation and just-in-time compilation. Even offline, automated correctness proofs and formal analysis can become impractically slow or mathematically impossible.
What changes
Today
- Compilers imitate function calls and returns by combining several dynamic jumps
- The return address sits on the data stack, where code can read or modify it
- Analysis tools must trace every possible destination of a jump, which can take quadratic time or more
- A single call needs a jump, a swap and several jump destination labels, wasting code and gas
After EIP-7979
- The three instructions CALLSUB, CALLDEST and RETURNSUB make calls and returns explicit in the code
- The return address is stored only on a separate return stack, which EVM code cannot access
- Code that does not use dynamic jumps can be validated and compiled with a single linear-time check
- In a simple call example, code size drops by 29% and gas drops by 32%
Who is affected
- Compiler teams (Solidity, Vyper): They must change their code generators to emit dedicated instructions instead of synthesizing calls and returns from jumps to see any benefit. ACDE #245 made the compiler teams' intent to integrate a precondition for the inclusion decision.
- Client teams: They must implement the return stack and the three instructions. The implementation itself is simple: a call is a push and a jump, and a return is a pop and a jump.
- L2 and ZK rollups: The source measures that static control flow code can be compiled to RISC-V and proven, making the proving cost of the same contract 5x to 51x cheaper than with an interpreter approach.
- Security audit and formal verification tools: Because the call structure is visible directly in the code, linear-time analysis becomes possible, and the class of vulnerabilities in which code corrupts its own return address disappears.
In depth
CALLSUB (0xBC): call a subroutine
Pops destination from the top of the data stack, pushes the return address PC + 1 onto the return stack, and sets PC to destination. If destination is not a CALLDEST, or the return stack already holds 1024 entries, execution enters the exceptional halting state. The gas cost is mid (8). The source explains that this cost is justified because it is the mid cost of JUMP plus only the work of pushing one integer onto the return stack, and adds that the cost balance needs to be confirmed by benchmarks.
CALLDEST (0xBB): entry marker and tail calls
An instruction that marks a subroutine entry. Like JUMPDEST, it does nothing else and execution flows on to the next instruction. The destination of every CALLSUB must be a CALLDEST. Gas is jumpdest (1). CALLDEST is also a valid destination for JUMP and JUMPI. Entering by a jump does not push a return address, so a RETURNSUB inside returns to an address already on the return stack. This is allowed because of tail calls. Replacing a call made just before a return with a jump saves one instruction and one return address, and a recursion that calls itself again can run without increasing the return stack depth. Compilers use this transformation for tail calls, mutual recursion, state machines and shared epilogues.
RETURNSUB (0xBF): return
Sets PC to the return address popped from the return stack. If the return stack is empty, execution enters the exceptional halting state. Gas is low (5), on the grounds that it only has to pop the return stack into PC, which is less work than JUMP. The return stack values can be changed only by CALLSUB and RETURNSUB, so no check is needed when popping. The return stack describes semantics only: the actual state cannot be observed from EVM code and does not matter for consensus, so an implementation may store the PC as long as RETURNSUB observably returns to PC + 1. 0xBD and 0xBE are left free for the possibility of multi-branch calls and jumps.
Why a separate return stack
Register machines such as x86, ARM and RISC-V keep the return address in a link register or in a single stack mixed with data. Stack machines such as Forth, the JVM, Wasm and the CLR, by contrast, effectively use separate data and return stacks. The EVM is a stack machine, so it adopts the same proven approach. Another reason is that data stack items are 32 bytes while a jump destination needs only a byte or two. Because the return address lives only on its own stack and cannot be read, modified or moved with ordinary stack operations, the class of vulnerabilities in which code corrupts its own control flow disappears entirely. The remaining risks are checked at execution time as defined by the instructions.
Code size and gas savings
In an example that calls a simple subroutine squaring a number, the code synthesized with JUMP uses 17 bytes and 50 gas, while the code using CALLSUB uses 12 bytes and 34 gas. That is 29% fewer bytes and 32% less gas. With the JUMP approach, even the caller must synthesize its own return with swap1 and jump. The gain shrinks as code gets larger and more optimized, but code using CALLSUB always uses less space and gas than code doing the same work. Backwards compatibility is the main reason immediate arguments and code sections are not used, and EOF took the opposite path of code sections and relative jumps, which required a separate dedicated opcode for jumps between subroutines.
Execution performance and ZK proving cost
Code that does not use dynamic jumps can be checked in a single pass to confirm that the data stack depth at every instruction is the same on every execution, and code that passes can be compiled by a JIT on the fly and by an AOT compiler in linear time. The source measured the number of RISC-V instructions executed on five kernels (mul chain, call tree, xorshift, euclid, store and sum). Relative to an interpreter, a simple compiler is 19x to 51x faster, and an LLVM -O2 optimizing compiler is 33x to 971x faster. Checked on Zisk, Polygon's RISC-V zkVM, the proving cost equals exactly the number of executed instructions plus a fixed startup cost of 444 steps, so proving compiled code is 5x to 51x cheaper. However, in most transactions storage dominates execution time, so the gains show most clearly where overhead is low, such as L1 precompiles, L2s and EVM-compatible chains.
Backwards compatibility and verification implementation
The new opcodes behave the same everywhere, and the semantics of existing EVM code do not change. Only code that relied on unspecified behavior could be affected, and such code was already broken. This change does not block other changes such as ZK execution, EOF or RISC-V. In the reference implementation (based on EELS), the machine state gains a return_stack field and the JUMPDEST analysis gains a valid_call_destinations set. CALLDEST positions go into both sets, so jump and jumpi need no changes. The new exceptions are ReturnStackOverflowError and ReturnStackUnderflowError.
Status and core developer call decisions
The status of this EIP is Review, so the specification may still change. On the 09.10 ACDE #245 call, the decision was deferred and a two-week grace period was given for the Solidity and Vyper compiler teams to say whether they will actually integrate, with automatic DFI as the default if there is no progress. Later, on the 09.24 ACDE #246 call, EIP-7979 was moved to Considered for Inclusion (CFI) for Hegotá. This means the clients review it on the assumption that they will implement it.