Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7979: Call and Return Opcodes for the EVM

A minimal specification for new EVM opcodes to support calls and returns.

Authors Greg Colvin (@gcolvin), Martin Holst Swende (@holiman), Brooklyn Zelenka (@expede), John Max Skaller <skaller@internode.on.net>
Created 2025-12-17
Discussion Link https://ethereum-magicians.org/t/eip-7951-call-and-return-opcodes-for-the-evm/24615
Requires EIP-3541

Abstract

This is the smallest possible change to the EVM to support calls and returns.

This proposal introduces three new control-flow instructions to the EVM:

  • CALLSUB destination transfers control to the destination``.
  • ENTERSUB marks a CALLSUB destination.
  • RETURNSUB returns to the PC after the most recent CALLSUB.

Code can also be prefixed with MAGIC bytes. The complete control flow of MAGIC code can be traversed in time and space linear in the size of the code, enabling better tools for validation, static analysis, and JIT and AOT compilers. Onchain, MAGIC code is validated at CREATE time to ensure that it will not execute invalid instructions, jump to invalid locations, underflow stack, or, in the absence of recursion, overflow stack.

These changes are backwards-compatible: the new instructions behave as specified whether or not they appear in MAGIC code.

Motivation

The original control-flow facilities

In 1833 Charles Babbage began the design of a steam-powered, mechanical, Turing-complete computer. Its first published description was the “Sketch of The Analytical Engine Invented by Charles Babbage”, L. F. Menabre, Bibliothque Universelle de Genve, October, 1842, No. 82. The translator, Ada Augusta, Countess of Lovelace, made extensive notes, including her famous computer program – which used conditional branches and nested loops to recursively compute Bernoulli numbers. Here we find her prescient recognition of its power:

The Analytical Engine … holds a position wholly its own … In enabling mechanism to combine together general symbols in successions of unlimited variety and extent, a uniting link is established between the operations of matter and the abstract mental processes of the most abstract branch of mathematical science.

And her insistence on its limits:

The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform. It can follow analysis; but it has no power of anticipating any analytical relations or truths. Its province is to assist us in making available what we are already acquainted with.

Alan Turing answered Lady Lovelace’s objection with a question of his own:

The majority of [minds] seem to be “subcritical” … an idea presented to such a mind will on average give rise to less than one idea in reply. A smallish proportion are supercritical. An idea presented to such a mind that may give rise to a whole “theory” consisting of secondary, tertiary and more remote ideas… we ask, “Can a machine be made to be supercritical?”

“Computing Machinery and Intelligence” A. M. Turing, Mind, Volume LIX, Issue 236, October 1950, Pages 433–460

Jumps, conditional jumps, calls, and returns

In 1945 Turing proposed jumps, conditional jumps, calls, and returns as a means of organizing the logic of the code and the design of the memory crystals for his Automatic Computing Engine:

A simple form of logical control would be a list of operations to be carried out in the order in which these are given. Such a scheme can be made to cover quite a number of jobs… and has been used in more than one machine… However, it lacks flexibility. We wish to be able to arrange that sequences of orders can divide at various points, continuing in different ways according to the outcome of the calculations to date… We also wish to be able to arrange for the splitting up of operations into subsidiary operations… To start on a subsidiary operation we need only make a note of where we left off the major operation and then apply the first instruction of the subsidiary. When the subsidiary is over we look up the note and continue with the major operation.

The other Turing machine. B. E. Carpenter , R. W. Doran. The Computer Journal, Volume 20, Issue 3, January 1977

Turing’s ACE was designed as a 32-bit RISC machine with integer and floating point operations, 32 registers, a 1024-slot return stack, and 25K of RAM on a 1-MHz bus. The Pilot ACE was for a while the world’s fastest computer.

Similar call and return facilities of various levels of complexity – from Burroughs’ baroque ALGOL support to RISC-V’s subtle JAL and JALR – have proven their worth across a long line of important machines over the last 80 years. This includes most all of the machines we have programmed or implemented: physical machines including the Burroughs 5000, CDC 7600, IBM 360, PDP-11, VAX, Motorola 68000, Sun SPARC, Intel x86s, and others, as well as virtual machines for Scheme, Forth, Pascal, Java, Wasm, and others.

The EVM control-flow facility

Unlike these machines, the Ethereum Virtual Machine does not provide operations for calls and returns. Instead, they must be synthesized using the JUMP instruction, which takes its argument on the stack. Further, the EVM provides only this jump. The EVM’s dynamic jump causes problems. First, the need to synthesize static jumps and calls with dynamic jumps wastes some space and gas, as we will show below. The much bigger problem is this: jumps that can dynamically branch to any destination in the program are a denial-of-service vulnerability that cause quadratic “path explosions” when traversing the program’s flow of control.

Traversing flow of control is a fundamentmental first step for many static analyses, including validating the flow of control, proving that programs meet their formal specifications, and, for these purposes and others, transforming code into other representations, such as control flow graphs, code for faster interpreters, and code for physical machines. Flow of control can be traversed via the standard “depth-first search” (DFS) algorithm:

1) Starting at the first instruction * step instruction by instruction. 2) When a jump or call is encountered: 1) Recursively traverse its destinations. 2) When a destination is re-encountered: * Return from that recursion.

When all jumps are static the number of steps is linear in the number of instructions: only one or two paths must be explored for each jump. With dynamic jumps every possible destination must be explored at every jump: at worst, the number of steps is quadratic in the number of instructions.

Going quadratic

For Ethereum, quadratic traversal times are an online denial-of-service vulnerability that prevents us from, in one pass, ensuring the valid use of EVM code and compiling EVM code for faster execution.

Even offline, dynamic jumps can cause static analyses of many contracts to become impractically slow, intractable or even impossible:

  • Ethereum smart contracts are distributed programs running on top of the Ethereum blockchain. Since program flaws can cause significant monetary losses and can hardly be fixed due to the immutable nature of the blockchain, there is a strong need of automated analysis tools which provide formal security guarantees. Designing such analyzers, however, proved to be challenging and error-prone.[^1]
  • The EVM language is a simple stack-based language … with one significant difference between the EVM and other virtual machine languages (like Java Bytecode or CLI for .Net programs): the use of the stack for saving the jump addresses instead of having it explicit in the code of the jumping instructions. Static analyzers need the complete control flow graph (CFG) of the EVM program in order to be able to represent all its execution paths.[^2]
  • Static analysis approaches mostly face the challenge of analysing compiled Ethereum bytecode… However, due to the intrinsic complexity of Ethereum bytecode (especially in jump resolution), static analysis encounters significant obstacles.[^3]
  • Analyzing contract binaries is vital since their sources are unavailable, involving identification comprising function entry identification and detecting its boundaries… Unfortunately, it is challenging to identify functions … due to the lack of internal function call statements.[^4]

There is an entire academic literature of complex, partial solutions to problems that this proposal renders trivial. With so much at stake on the blockchain deploying correct contracts is imperative – there is no reason for the EVM to make that job any more difficult than necessary.

Taming the EVM

To prevent control-flow traversal from “going quadratic” we must prevent the dynamic use of jumps. Onchain, most all uses of JUMP and JUMPI are preceded by a PUSH – that is, they are effectively static. This proposal ensures that in valid code JUMP and JUMPI are always used statically. Currently, the only places that jumps must be used dynamically are to support calls and returns. For that purpose we propose CALLSUB and RETURNSUB opcodes as replacements.

Specification

The key words MUST and MUST NOT in this Specification are to be interpreted as described in RFC 2119 and RFC 8174.

CALLSUB destination (0x..)

Transfers control to a subsidiary operation.

  1. Decode the destination as a UTF8-encoded positive integer.
  2. Push the current PC + 1 to the return stack.
  3. Set PC to destination.

The gas cost is mid (8).

ENTERSUB (0x..)

The destination of every CALLSUB MUST be an ENTERSUB.

RETURNSUB (0x..)

Returns control to the caller of a subsidiary operation.

  1. Pop the return stack to PC.

The gas cost is low (5).

MAGIC (0xEF....)

After this EIP has been activated code beginning with the MAGIC bytes MUST be a valid program. Execution begins immediately after the MAGIC bytes.

Notes:

  • Values popped off the return stack do not need to be validated, since they are alterable only by CALLSUB and RETURNSUB.
  • The description above lays out the semantics of these instructions in terms of a return stack. But the actual state of the return stack is not observable by EVM code or consensus-critical to the protocol. (For example, a node implementer may code CALLSUB to unobservably push PC on the return stack rather than PC + 1, which is allowed so long as RETURNSUB observably returns control to the PC + 1 location.)

  • Opcode and magic values are still to be determined.

Costs

A mid cost for CALLSUB is justified by it taking very little more work than the mid cost of JUMP – just pushing an integer to the return stack

A jumpdest cost for ENTERSUB is justified by it being, like JUMPDEST, a mere label.

A low cost for RETURNSUB is justified by needing only to pop the return stack into the PC.

Benchmarking will be needed to tell if the costs are well-balanced.

Validity

Execution is defined in the Yellow Paper as a sequence of changes in the EVM state. The conditions on valid code are preserved by state changes. At runtime, if execution of an instruction would violate a condition the execution is in an exceptional halting state and cannot continue. The Yellow Paper defines six such states.

  • State modification during a static call
  • Insufficient gas
  • More than 1024 stack items
  • Insufficient stack items
  • Invalid jump destination
  • Invalid instruction

We would like to consider EVM code valid iff no execution of the program can lead to an exceptional halting state. In practice, we must test at runtime for the first three conditions. We dont know whether we will be called statically. We dont know how much gas there will be, and we dont know how deep a recursion may go. However, we can validate that non-recursive programs do not overflow stack. All of the remaining conditions MUST be validated statically. To allow for efficient algorithms our validation does not consider the codes data and computations, only its control flow and stack use. This means we will reject programs with invalid code paths, even if those paths are not reachable.

Constraints on valid EVM code

Code beginning with MAGIC MUST be valid. Constraints on valid code MUST be validated at CREATE time, in time and space linear in the size of the code. The constraints on valid code are as follows.

1) All opcodes must be valid

  • They MUST have been defined in the Yellow Paper or a deployed EIP and
  • MUST NOT have been deprecated in a subsequent deployed EIP.
  • The INVALID opcode is valid. 2) The JUMP and JUMPI instructions MUST be preceded by a PUSH instruction. 4) The JUMP and JUMPI instructions MUST NOT address immediate data, and MUST address a JUMPDEST. 3) The CALLSUB instruction MUST NOT address immediate data, and MUST address an ENTERSUB. 5) The number of items on the data stack MUST always be positive and less than or equal to 1024. 6) The number of items on the return stack MUST always be positive and less than or equal to 1024. 7) The stack height is the absolute difference between the current stack pointer and the stack pointer at the most recent ENTERSUB;
  • the stack height MUST be the same for every PC.

The guarantee of constant stack height prevents stack underflow, breaks cycles in the control flow, ensures finite stack use for non-recursive programs, and allows virtual stack code to be directly serialized into virtual register code for faster interpretation and for one-pass compilation to machine code.

Note: .NET, the JVM, and Wasm enforce similar constraints for similar reasons.

Validation

The above is a purely semantic specification, placing no constraints on the syntax of bytecode beyond being an array of opcodes and immediate data. “Subsidiary operations” here are not contiguous sequences of bytecode. They are subgraphs of the bytecode’s full control-flow graph. The EVM is a simple state machine, where every instruction advances the state one more notch – it has no syntatic structure. We only promise that valid code will not, as it were, jam up the gears of the machine.

Rather than enforce semantic constraints via syntax – as done by higher-level languges – this proposal enforces them via validation: MAGIC code is proven valid at CREATE time. We provide an algorithm below for vaidlidating code in time and space linear in the size of the code. With no syntactic constraints and minimal semantic constraints we maximize opportunities for optimizations, include tail call elimination, multiple-entry calls, common exit handlers, arranging code blocks for optimal caching and variables for efficient register allocation, and others. Since we want to support online compilation of EVM code to native code it is crucial that the EVM code be as well optimized as possible offline.

Rationale

Why no relative jumps?

This would of course break the promise of “the smallest possible change.” EIP-4200: EOF - Static relative jumps remains available if we want its performance advantages. It proposes three new instructions:

  • RJUMP destination transfers control to PC + destination.
  • RJUMPI destination conditionally transfers control to PC + destination.
  • RJUMPV jump-table transfers control relative to an embedded jump table.

If adopted, we would want RCALLSUB as well as, or perhaps rather than CALLSUB.

Why no object format?

Again, this would break the promise of “the smallest possible change,” and EIP-3540: EOF - EVM Object Format remains available. In the presence of EOF code sections CALLSUB would be restricted to section boundaries.

Why the return-stack mechanism?

In our experience most stack machines have separate stacks for data and for returns from calls, whereas most register machines have one stack, registers for computation, and instructions to support returns from calls in coordination with the stack. The EVM is of course a stack machine, not a register machine. As an industry and a team we have substantial experience with the return-stack mechanism proposed here. It has been effectively used in many machines over the last eight decades, and has been implemented, tested, and even ready to ship in many of our clients over the last nine years.

Do we save on code size and gas?

The difference these instructions make can be seen in this very simple code for calling a routine that squares a number. The distinct opcodes make it easier for both people and tools to understand the code, and there are modest savings in code size and gas costs as well.

SQUARE:                           |       SQUARE:                      
    jumpdest       ; 1 gas        |           entersub       ; 1 gas
    dup            ; 3 gas        |           dup            : 5 gas
    mul            ; 5 gas        |           mul            ; 5 gas
    swap1          ; 3 gas        |           returnsub      ; 5 gas
    jump           ; 8 gas        |                                 
                                  |                                 
CALL_SQUARE:                      |       CALL_SQUARE:                 
    jumpdest       ; 1 gas        |           entersub       ; 1 gas
    push RTN_CALL  ; 3 gas        |           push 2         ; 3 gas          
    push 2         ; 3 gas        |           callsub SQUARE ; 8 gas
    push SQUARE    ; 3 gas        |           returnsub      ; 5 gas
    jump           ; 8 gas        |           
RTN_CALL:                         |                                 
    swap1          ; 3 gas        |                                 
    jump           ; 8 gas        |                                 
                                  |                                 
Size in bytes; 18                 |      Size in bytes: 12
Consumed gas;  49                 |      Consumed gas:  35

That’s 33% fewer bytes and 33% less gas using CALLSUB versus using JUMP. So we can see that these instructions provide a simpler, more efficient mechanism. As code becomes larger and better optimized the gains become proportionaly smaller, but code using CALLSUB always takes less space and gas than equivalent code without it.

Do we improve real-time performance?

Real time performance gains (as opposed to gas) will come from AOT and JIT compilers. Crucially, the constraint that stack depths be constant means that in MAGIC code a very fast JIT can traverse the control flow of the EVM code in one pass, generating machine code as it goes. (Wasm, the JVM and .NET share that property.) The EVM is a stack machine, but real machines are register machines. So generating virtual register code for a faster interpreter is a win. (I have seen 4X speepups on JVM code.) Generating good machine code gives orders of magnitude gains. But for most transactions storage dominates execution time, and gas counting and other overhead take their toll. So these gains would be most visible in contexts where this overhead is absent, such as for L1 precompiles and on some EVM-compatible chains.

Backwards Compatibility

These changes are backwards compatible.

  • The semantics of EVM code is not affected by whether the contract begins with MAGIC.
  • There are no changes to the semantics of existing EVM code, with the caveat that code that might have halted could execute and vice versa. Such code was always broken.
  • This proposal does not require maintaining two interpreters.

These changes do not foreclose EOF, RISC-V, or other changes: new MAGIC numbers would mark future EVMs. Neither do these changes preclude running the EVM in zero knowledge; they would more likely help.

Test Cases

** Note: these tests are known to be incorrect. **

Simple routine

This should jump into a subroutine, back out and stop.

Bytecode: 0x60045e005b5d (PUSH1 0x04, CALLSUB, STOP, JUMPDEST, RETURNSUB)

Pc Op Cost Stack RStack
0000 CALLSUB 0004 8 [] [3]
0003 STOP 0 [] []
0004 ENTERSUB 1 [] [3]
0005 RETURNSUB 5 [] []

Output: 0x Consumed gas: 14

Two levels of subroutines

This should execute fine, going into two depths of subroutines.

Bytecode: 0x6800000000000000000c5e005b60115e5d5b5d (PUSH9 0x00000000000000000c, CALLSUB, STOP, ENTERSUB, PUSH1 0x11, CALLSUB, RETURNSUB, ENTERSUB, RETURNSUB)

Pc Op Cost Stack RStack
0000 CALLSUB 0004 8 [] [0003]
0003 STOP 0 [] []
0004 ENTERSUB 8 [] [003]
0004 CALLSUB 0008 8 [] [0007,0003]
0007 RETURNSUB 5 [] []
0008 ENTERSUB 8 [] [0003]
0009 RETURNSUB 5 [] []

Consumed gas: 42

Failure 1: invalid jump

This should fail, since the given location is outside of the code-range. The code is the same as previous example, except that the pushed location is 0xffff instead of 0x0c.

Bytecode: 0xffff (PUSH9 0x01000000000000000c, CALLSUB, STOP, JUMPDEST, PUSH1 0x11, CALLSUB, RETURNSUB, JUMPDEST, RETURNSUB)

Pc Op Cost Stack RStack
0000 CALLSUB FFFF 8 [] [FFFF]
0003 RETURNSUB 5 [] []
Error: at pc=10, op=CALLSUB: invalid jump destination

Failure 2: shallow return stack

This should fail at first opcode, due to shallow return_stack

Bytecode: 0x5d5858 (RETURNSUB, PC, PC)

Pc Op Cost Stack RStack
0 RETURNSUB 5 [] []
Error: at pc=0, op=RETURNSUB: invalid retsub

Subroutine at end of code

In this example. the CALLSUB is on the last byte of code. When the subroutine returns, it should hit the ‘virtual stop’ after the bytecode, and not exit with error

Bytecode: 0x6005565b5d5b60035e (PUSH1 0x05, JUMP, BEGINSUB, RETURNSUB, JUMPDEST, PUSH1 0x03, CALLSUB)

Pc Op Cost Stack RStack
0000 PUSH1 3 [] []
0002 JUMP 8 [5] []
0005 ENTERSUB 1 [] []
0008 CALLSUB 0003 10 [] [0003]
0004 RETURNSUB 5 [] []
0009 STOP 0 [] []

Consumed gas: 27

Error: at pc=0, op=RETURNSUB: invalid retsub

Reference Implementation

** Note: this implementation is known to be incomplete and incorrect. **

The following is a pseudo-Python implementation of an algorithm for predicating code validity. An equivalent algorithm MUST be run at initialization time in space and time linear in the size of the code. This algorithm recursively traces the code, emulating its control flow and stack use and checking for violations of the rules above. It runs in time proportional to the size of the code,and the depth of recursion is proportional to the size of the code.

Validation Function

We assume that instruction validation and destination analysis has been done, and that we have some constant-time helper functions:

  • is_terminator(opcode) returns true iff opcode is a terminator.
  • previous_data(pc) returns the immediate data for the instruction before pc (usually a PUSH.)
  • immediate_size(opcode) returns the size of the immediate data for an opcode.
  • removed_items(opcode) returns the number of items removed from the data_stack by the opcode.
  • added_items(opcode) returns the number of items added to the data_stack by the opcode.
    # returns true iff code is valid
    int []stack_depths
    int []max_depths
    def validate_code(code: bytes, pc: int, sp: int, bp: int, max: int) -> int, boolean:
        while pc < len(code):

            # check stack height and return if we have been here before
            stack_depth = sp - bp
            max_depth = max + stack_depth
            if max_depth > 1024
                return max_depth, false
            if stack_depths[pc] {
                if stack_depth != stack_depths[pc]:
                    return 0, false
                if opcode == ENTERSUB:
                    return max_depths[pc], true
                else
                    return max_depth, true
                else:
                    stack_depths[pc] = stack_depth

            if is_terminator(opcode):
                return max_depth, true

            elif opcode == CALLSUB:

                # push return address and set pc to destination
                jumpdest = previous_data(pc)
                push(return_stack, pc)

                # validate and track maximum height
                max_depth, valid = validate_code(jumpdest, 0, sp - bp, max)
                if !valid:
                   return max_depth, false
                max_depths[jumpdest] = max_depth;
               
            elif opcode == RETURNSUB:

                # pop return address and check for preceding call
                pc = pop(return_stack)
                max_depth = max + stack_depth
                return max_depth, true

            if opcode == JUMP:

                # set pc to destination of jump
                pc = previous_data(pc)

            elif opcode == JUMPI:

                jumpdest = previous_data(pc)

                # recurse to validate true side of conditional
                max_depth, valid = validate_code(jumpdest, sp, bp)
                if !valid:
                    return max_depth, false

            # apply instructions to stack
            sp -= removed_items(opcode)
            if sp < 0
                return so, false
            sp += added_items(opcode)

            # Skip opcode and any immediate data 
            pc += 1 + immediate_size(opcode)

        max_depth = max + stack_depth
        if (max_depth > 1024)
            return max_depth, false
        return max_depth, true

Security Considerations

These changes introduce new flow control instructions. They do not introduce any new security considerations. This EIP is intended to improve security by validating a higher level of safety for EVM code deployed on the blockchain. The validation algorithm must use time and space linear in the size of the code so as not be a denial of service vulnerability. The algorithm here makes one linear-time, recursive pass of the bytecode, whose dep cannot exceed the number of CALLSUB and JUMPI instructions in the code.

Copyright and related rights waived via CC0.


[^1]:
   ``` csl-json
   {
     "type": "article",
     "id": 1,
     "author": [
       {
         "family": "Schneidewind",
         "given": "Clara"
       }
     ],
     "DOI": "arXiv:2101.05735",
     "title": "The Good, the Bad and the Ugly: Pitfalls and Best Practices in Automated Sound Static Analysis of Ethereum Smart Contracts.,
     "original-date": {
       "date-parts": [
         [2021, 1, 14]
       ]
     },
     "URL": "https://arxiv.org/abs/2101.05735"
   }
   ```

[^2]:
   ``` csl-json
   {
     "type": "article",
     "id": 2,
     "author": [
       {
         "family": "Albert",
         "given": "Elvira"
       }
     ],
     "DOI": "arXiv:2004.14437",
     "title": "Analyzing Smart Contracts: From EVM to a sound Control-Flow Graph.,
     "original-date": {
       "date-parts": [
         [2020, 4, 29]
       ]
     },
     "URL": "https://arxiv.org/abs/2004.14437"
   }
   ```

[3]:
   ``` csl-json
   {
     "type": "article",
     "id": 3,
     "author": [
       {
         "family": "Contro",
         "given": "Filippo"
       }
     ],
     "DOI": "arXiv:2103.09113",
     "title": "EtherSolve: Computing an Accurate Control-Flow Graph from Ethereum Bytecode.",
     "original-date": {
       "date-parts": [
         [2021, 3, 16]
       ]
     },
     "URL": "https://arxiv.org/abs/2103.09113"
   }
   ```

[^4]:
   ``` csl-json
   {
     "type": "article",
     "id": 4,
     "author": [
       {
         "family": "He",
         "given": "Jiahao"
       }
     ],
     "DOI": "arXiv:2301.12695",
     "title": "Neural-FEBI: Accurate Function Identification in Ethereum Virtual Machine Bytecode.",
     "original-date": {
       "date-parts": [
         [2023, 1, 30]
       ]
     },
     "URL": "https://arxiv.org/abs/2301.12695"
   }
   ```

Citation

Please cite this document as:

Greg Colvin (@gcolvin), Martin Holst Swende (@holiman), Brooklyn Zelenka (@expede), John Max Skaller <skaller@internode.on.net>, "EIP-7979: Call and Return Opcodes for the EVM [DRAFT]," Ethereum Improvement Proposals, no. 7979, December 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7979.