
If you've written a non-trivial amount of Solidity, you've almost certainly run into the "stack too deep" error. EIP-8024 is a proposal to modify the EVM and put an end to this error for good. The EIP is being considered for the upcoming Glamsterdam upgrade (expected mid-2026) and has a good chance of being included.
In this article we'll go in depth (no pun intended) into the "stack too deep" problem, how EIP-8024 addresses the underlying issue, and why the solution is not as simple as it appears at first. Along the way, we'll explore some interesting and lesser-known aspects of the EVM.
The “stack too deep” error shows up in many scenarios, but they mostly boil down to using too many local variables at the same time. To illustrate this, take the following code:
contract Example {
uint public x;
function myFunction() public {
uint x1 = 1;
uint x2 = 2;
uint x3 = 3;
uint x4 = 4;
uint x5 = 5;
uint x6 = 6;
uint x7 = 7;
uint x8 = 8;
x += x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8;
}
}This compiles fine, but if we add one variable more…
contract Example {
uint public x;
function myFunction() public {
uint x1 = 1;
uint x2 = 2;
uint x3 = 3;
uint x4 = 4;
uint x5 = 5;
uint x6 = 6;
uint x7 = 7;
uint x8 = 8;
uint x9 = 9;
x += x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9;
}
}Then solc (the official Solidity compiler) gives us an error:

There are many workarounds for this problem. Perhaps the most popular today, and the one the error message itself suggests, is to use the IR pipeline. This is an alternative compilation mode in solc which does fix the problem, but which has its own issues:
It's slow (although getting better over time)
It can result in a worse debugging experience
It still has some expressiveness limitations1
At this point, it’s likely a Vyper guy will jump out from behind you and, before you have time to ask him how he got into your house, yell “Vyper doesn’t have stack too deep errors!”. Which is true. But this comes at the cost of not being able to do cyclic calls (including recursive functions) and, in any case, rewriting your contract in another language is hardly a practical solution.
To understand why the "stack too deep" error happens, we need to look at how Solidity handles variables. The EVM has three main areas2 where values can be stored: stack, memory and storage. The stack and memory start empty in each execution. The storage, on the other hand, persists between calls.
Solidity uses these three areas differently depending on the language feature. Storage variables use the permanent storage area. Local variables, on the other hand, are ephemeral, and so they have to use either the stack or memory areas.
In the previous section, we saw how a contract with eight local variables worked fine with solc's default compilation mode (normally called the "legacy pipeline"), but adding one extra variable results in a "stack too deep" error. To better understand why this happens, let's look at what goes on at the EVM level when we have seven variables. Executing that bytecode will eventually result in the stack looking like this:

And if we have eight variables (the limit before getting the error), the contents of the stack will be:

In both examples we can see all our variables at the top of the stack, ready to be used. But if we add one variable more, the code won't compile. Why? The stack can have up to 1024 elements, surely we can keep adding values there?
Take a look at the previous images again, but this time pay attention to the instructions that were executed immediately before. With seven variables, we have a sequence of instructions that looks like DUP1 | DUP3 | ... | DUP13. With eight variables, we have a slightly longer sequence with a DUP15 at the end. You don't have to be a math genius to guess that a ninth variable would then add a DUP17 instruction. But if you check the list of EVM instructions, you’ll see that there are only 16 DUP* instructions, from DUP1 to DUP16. The same happens with SWAP* instructions. In other words, we can only access the top 16 elements3 of the stack. Anything beyond that is "too deep".
Why this particular code was generated doesn't really matter. This is a contrived example, and in a more realistic scenario the stack might be used more efficiently. But there will always be a limit to the number of elements from the stack that you can use together at a given time.
Given this limitation of the EVM stack, why not use the memory area instead? This is actually what the IR pipeline and Vyper do, but the EVM memory has a couple of peculiarities of its own that make it less than ideal for storing local variables. The details are complex (check the aside below if you want to learn more), but the bottom line is that putting variables in memory comes with a significant cost in complexity and gas usage.
The EVM memory has at least two characteristics that make it a less than ideal fit for storing local variables:
It’s not random access
It has an “infinite” length
Of course, memory is not really infinite. By this I mean that the available memory grows as it's used: each time you read or write some new part, the available memory will expand to include it. But the cost of that expansion grows with the memory size in a quadratic way. This means that using the lower part of the memory area is cheaper, which explains in turn why it doesn’t have the random access property: accessing any two areas doesn’t always have the same cost.
These characteristics affect the management of local variables in two ways. One is that the more variables you use, the more costly it will be. And this is not only about the variables in an individual function: if you have multiple (internal) calls, they will share the memory area, expanding it and increasing the gas used each time. The second problem is related to the fact that EVM memory has a beginning but not an end, preventing the normal compiler trick of using one end of the available memory for the stack and the other one for the heap.
These peculiarities of the EVM memory are the reason why the IR pipeline and Vyper have a limited expressiveness with respect to recursion and cyclic calls. Check the resources at the end if you want to learn more.
In summary, the compiler has to put local variables somewhere, and the two areas where it can do it are the stack and memory. Both have limitations: memory comes with its own trade-offs, and the stack only lets you access its top 16 elements. If we want to fix this at the EVM level, we have two possibilities:
Make memory less weird, as in EIP-7923.
Make more items in the stack accessible, as in EIP-8024, the subject of this article.
As we saw, the fact that only the top 16 items of the stack can be accessed is the underlying issue that causes "stack too deep" errors. If we raised that limit high enough, the error would go away. The most straightforward way to make that happen is by adding new instructions.
One possibility is to add new variants of the existing instructions. That is, to add new instructions like DUP17, SWAP17, DUP18, etc. This doesn't seem like a great idea. If we wanted to allow access to the top 100 elements of the stack, we would have to add 168 new instructions. Leaving aside the fact that there aren't that many available opcodes, this would be ugly and wasteful.
A better idea is to add only two new instructions, DUPN and SWAPN, that take an arbitrary N as input. Where does that input come from? There are many options, but the two most obvious are:
Take N from the stack. To duplicate the 20th element, you'd execute a PUSH1 20 instruction followed by DUPN.
Use the next byte after the instruction, like PUSH1 does. With this option, a single DUPN 20 instruction would duplicate the 20th element.
It's easy to argue that the second approach is much better: the resulting bytecode is smaller (two bytes vs three), it consumes less gas (one instruction vs two), and it's more friendly to static analysis.
Arguments encoded directly in the bytecode like that are called immediates. Right now, only the PUSH* instructions use them; every other instruction that has an input takes it from the stack. These PUSH* immediates have different lengths depending on the instruction: one byte for PUSH1 (e.g., PUSH1 0x14), two bytes for PUSH2 (e.g., PUSH2 0x1234), etc. For our two new instructions, a one-byte immediate would be enough to let us access the top 256 elements of the stack, a significant improvement over the current 16 limit.
In fact, we could even do a little better. Using DUPN with a value like 5 wouldn't make sense given that DUP5 already exists (DUPN 5 would have the same behavior). But we could define our new instructions so that the value of the immediate is not interpreted verbatim. Instead of saying that DUPN X duplicates the Xth element of the stack, we could say that it refers to the item at position X + 17: DUPN 0 would duplicate the 17th element, DUPN 1 the 18th, and so on. This way, the combination of existing DUP* instructions plus the new DUPN would let us access the top 272 elements.
This all sounds great. Sadly, things aren't that simple. To explain why, we need to take a detour and talk about jumps.
The EVM works in a simple, sequential way: it executes an instruction, moves to the next one, executes it, advances again. The only exception to this behavior is the JUMP instruction4, which takes an item from the stack and continues the execution at that position. But it can't just jump anywhere in the bytecode: the destination has to be JUMPDEST (0x5b), a special instruction that serves only as a marker for valid jump destinations.
You'd be excused for thinking that this is a simple check: read the byte at the jump position and verify that its value is 0x5b, the JUMPDEST opcode. But that doesn't work. Imagine there's a PUSH1 0x5b instruction in the code. Is its second byte a valid jump destination?

For the EVM, it's not: you get an exceptional halt and the transaction reverts.
This is where JUMPDEST analysis comes in. Before executing any code, you need to process the whole bytecode to find out which positions are valid jump destinations. This, incidentally, has to start from the beginning; you can't just look at the bytes before a 0x5b and figure out if it's a valid JUMPDEST or not.
Why can’t you figure out locally if a 0x5b byte is a JUMPDEST instruction?
You could make the following argument: since the “biggest” push instruction is PUSH32, it’s possible to determine whether some 0x5b is a valid jump destination by looking at the previous 32 bytes: check if the one immediately before is a PUSH1, or if the one before that is a PUSH2, or if the one 32 positions away is a PUSH32. If none of those is true, then it’s a valid JUMPDEST.
It’s easy to prove that this approach doesn’t work. Imagine that the last byte you check is 0x7f, which corresponds to the PUSH32 opcode.

The reason JUMPDEST analysis is relevant to this discussion is that our new instructions take immediates. Why is that a big deal? We could just include them as part of the analysis and forbid a jump to the second byte of SWAPN 0x5b. Except we can't. To understand this, let's assume that the opcode of SWAPN is going to be 0xaa and that the following bytecode is already deployed onchain:

This is a perfectly valid bytecode. There is no instruction for opcode 0xaa yet, but that doesn’t matter because we are jumping over it to a valid destination.5 But what happens if we upgrade the EVM and 0xaa becomes the opcode for SWAPN? The disassembled code now looks like this:

Oops, now we are jumping to an invalid destination, even when the underlying bytecode is the same! This is a breaking change.
Conversely, an invalid destination can become valid. In the following example, we are trying to jump to a 0x5b which is the immediate of a PUSH1 instruction:

This will result in an "invalid jump" exception today. But if we upgrade the EVM with our new instruction...

The opcode of PUSH1 becomes the immediate of SWAPN and the 0x5b becomes a valid destination.
At this point you might be wondering why there would be an invalid instruction in the middle of some code. One reason is that the EVM doesn't have separate sections of a "program" for code and data. Any arbitrary data included in the contract to be used in runtime (through an instruction like CODECOPY) has to be part of the bytecode. Since this is just data, it can easily have bytes that correspond to nonexistent instructions.
In practice, this kind of data is always placed at the end of the bytecode, not in the middle of it. But that doesn't matter. A breaking change in something like the EVM is serious, even if we speculate that the impact will be low.
And now, at last, we have all the context we need to talk about the EIP-8024 proposal.
To recap: we'd like to be able to access deeper parts of the stack to allow high-level languages like Solidity to use it more freely for local variables. To do that, we want to add two new instructions, DUPN and SWAPN, that take as their input a one-byte immediate, letting us access way more than 16 stack elements. But we need to avoid introducing breaking changes related to JUMPDEST analysis.
One possible solution is to forbid the problematic bytes: 0x5b (JUMPDEST) and the 0x60–0x7f range (PUSH1–PUSH32). We can also simplify and disallow the complete 0x5b–0x7f range, even if that includes four instructions that aren't problematic. Restricting the input range like this doesn't feel right though. A compiler would have access to more parts of the stack, but it won't be able to emit instructions like DUPN 91 and it will have to work its way around it.
This is far from ideal. We can do better. And, in fact, we've already hinted at a solution when we suggested interpreting a 0 value as 17.
Let's reframe the problem. We have a restricted range we can use for the immediate values: from 0x0 to 0x5a (0 to 90) and from 0x80 to 0xff (128 to 255). We want to map that range to a continuous range starting from 17.

One option is to define our decode function like this:
def decode(n):
assert 0 <= n <= 90 or 128 <= n <= 255
if n <= 90:
return n + 17
return n - 20Which maps our ranges this way:

But we can use an even tighter definition through modular arithmetic:
def decode(n):
assert 0 <= n <= 90 or 128 <= n <= 255
return (n + 145) % 256Which results in the following mapping:

In other words, a bytecode that includes the DUPN 128 instruction will duplicate the 17th element, and DUPN 0 will duplicate the 145th one. This second approach is the one that the EIP proposes.
The EIP also adds a new EXCHANGE instruction that lets you swap two elements deeper in the stack, instead of always involving the top element like SWAPN does.6 Similar to the other two new instructions, EXCHANGE takes as input a one-byte immediate which is only allowed to have values that don’t break JUMPDEST analysis. This single byte is used to encode two operands, M and N. The instruction then swaps the M+1 and N+1 stack items. The exact details are interesting, but this article is long enough as it is; check the resources at the end if you want to learn more.
While EIP-8024 hasn’t been officially confirmed as part of Glamsterdam, it has already been implemented by clients and tested in devnets7, meaning it has a very high chance of being included in the upgrade. If that happens, solc’s legacy pipeline will finally be able to get rid of the “stack too deep” error for good without incurring any loss of expressiveness in the language. And that’s not all: the alternative IR-pipeline, vyper, and any other compilers will also benefit from these new capabilities.
Check the EIP and the Ethereum Magicians discussion for more details about the EXCHANGE instruction and how it encodes the input in a single byte.
The EIP author has written some great explainers about adjacent topics that are completely worth reading:
Compiling for the EVM: Codegen for Stack Machines, about why other stack-based virtual machines (JVM, Wasm, CLR) don’t have the “stack too deep” error.
Spilling in the EVM, which continues the previous post, discussing the technique of moving values from stack to memory.
EVM Immediates, an Ethereum magicians post that explores the solution space for the problem of adding new instructions with immediates.
To elaborate: you can still have code that won't compile if, for example, you have a lot of local variables and they are used in a recursive function. Admittedly this is not an everyday scenario, but it shows that the solution is not perfect.
↩There are other areas, like transient storage or the return data region, but those don't really matter for this discussion.
↩Technically, the DUP* instructions access the top 16 elements while the SWAP* instructions use the top 17 items, because SWAPX swaps the 1st and X+1th elements (otherwise SWAP1 would be a no-op).
There's also JUMPI, which also jumps to some destination but only if some condition is true.
Bytes that don't correspond to opcodes of existing instructions can show up in the code, but executing them results in an exceptional halt and the transaction reverts consuming all remaining gas.
↩This is useful for compilers for reasons that go over my head.
↩Devnets are temporary, development-only testnets used by Ethereum core developers to test EIPs before they reach public testnets.
↩
If you've written a non-trivial amount of Solidity, you've almost certainly run into the "stack too deep" error. EIP-8024 is a proposal to modify the EVM and put an end to this error for good. The EIP is being considered for the upcoming Glamsterdam upgrade (expected mid-2026) and has a good chance of being included.
In this article we'll go in depth (no pun intended) into the "stack too deep" problem, how EIP-8024 addresses the underlying issue, and why the solution is not as simple as it appears at first. Along the way, we'll explore some interesting and lesser-known aspects of the EVM.
The “stack too deep” error shows up in many scenarios, but they mostly boil down to using too many local variables at the same time. To illustrate this, take the following code:
contract Example {
uint public x;
function myFunction() public {
uint x1 = 1;
uint x2 = 2;
uint x3 = 3;
uint x4 = 4;
uint x5 = 5;
uint x6 = 6;
uint x7 = 7;
uint x8 = 8;
x += x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8;
}
}This compiles fine, but if we add one variable more…
contract Example {
uint public x;
function myFunction() public {
uint x1 = 1;
uint x2 = 2;
uint x3 = 3;
uint x4 = 4;
uint x5 = 5;
uint x6 = 6;
uint x7 = 7;
uint x8 = 8;
uint x9 = 9;
x += x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9;
}
}Then solc (the official Solidity compiler) gives us an error:

There are many workarounds for this problem. Perhaps the most popular today, and the one the error message itself suggests, is to use the IR pipeline. This is an alternative compilation mode in solc which does fix the problem, but which has its own issues:
It's slow (although getting better over time)
It can result in a worse debugging experience
It still has some expressiveness limitations1
At this point, it’s likely a Vyper guy will jump out from behind you and, before you have time to ask him how he got into your house, yell “Vyper doesn’t have stack too deep errors!”. Which is true. But this comes at the cost of not being able to do cyclic calls (including recursive functions) and, in any case, rewriting your contract in another language is hardly a practical solution.
To understand why the "stack too deep" error happens, we need to look at how Solidity handles variables. The EVM has three main areas2 where values can be stored: stack, memory and storage. The stack and memory start empty in each execution. The storage, on the other hand, persists between calls.
Solidity uses these three areas differently depending on the language feature. Storage variables use the permanent storage area. Local variables, on the other hand, are ephemeral, and so they have to use either the stack or memory areas.
In the previous section, we saw how a contract with eight local variables worked fine with solc's default compilation mode (normally called the "legacy pipeline"), but adding one extra variable results in a "stack too deep" error. To better understand why this happens, let's look at what goes on at the EVM level when we have seven variables. Executing that bytecode will eventually result in the stack looking like this:

And if we have eight variables (the limit before getting the error), the contents of the stack will be:

In both examples we can see all our variables at the top of the stack, ready to be used. But if we add one variable more, the code won't compile. Why? The stack can have up to 1024 elements, surely we can keep adding values there?
Take a look at the previous images again, but this time pay attention to the instructions that were executed immediately before. With seven variables, we have a sequence of instructions that looks like DUP1 | DUP3 | ... | DUP13. With eight variables, we have a slightly longer sequence with a DUP15 at the end. You don't have to be a math genius to guess that a ninth variable would then add a DUP17 instruction. But if you check the list of EVM instructions, you’ll see that there are only 16 DUP* instructions, from DUP1 to DUP16. The same happens with SWAP* instructions. In other words, we can only access the top 16 elements3 of the stack. Anything beyond that is "too deep".
Why this particular code was generated doesn't really matter. This is a contrived example, and in a more realistic scenario the stack might be used more efficiently. But there will always be a limit to the number of elements from the stack that you can use together at a given time.
Given this limitation of the EVM stack, why not use the memory area instead? This is actually what the IR pipeline and Vyper do, but the EVM memory has a couple of peculiarities of its own that make it less than ideal for storing local variables. The details are complex (check the aside below if you want to learn more), but the bottom line is that putting variables in memory comes with a significant cost in complexity and gas usage.
The EVM memory has at least two characteristics that make it a less than ideal fit for storing local variables:
It’s not random access
It has an “infinite” length
Of course, memory is not really infinite. By this I mean that the available memory grows as it's used: each time you read or write some new part, the available memory will expand to include it. But the cost of that expansion grows with the memory size in a quadratic way. This means that using the lower part of the memory area is cheaper, which explains in turn why it doesn’t have the random access property: accessing any two areas doesn’t always have the same cost.
These characteristics affect the management of local variables in two ways. One is that the more variables you use, the more costly it will be. And this is not only about the variables in an individual function: if you have multiple (internal) calls, they will share the memory area, expanding it and increasing the gas used each time. The second problem is related to the fact that EVM memory has a beginning but not an end, preventing the normal compiler trick of using one end of the available memory for the stack and the other one for the heap.
These peculiarities of the EVM memory are the reason why the IR pipeline and Vyper have a limited expressiveness with respect to recursion and cyclic calls. Check the resources at the end if you want to learn more.
In summary, the compiler has to put local variables somewhere, and the two areas where it can do it are the stack and memory. Both have limitations: memory comes with its own trade-offs, and the stack only lets you access its top 16 elements. If we want to fix this at the EVM level, we have two possibilities:
Make memory less weird, as in EIP-7923.
Make more items in the stack accessible, as in EIP-8024, the subject of this article.
As we saw, the fact that only the top 16 items of the stack can be accessed is the underlying issue that causes "stack too deep" errors. If we raised that limit high enough, the error would go away. The most straightforward way to make that happen is by adding new instructions.
One possibility is to add new variants of the existing instructions. That is, to add new instructions like DUP17, SWAP17, DUP18, etc. This doesn't seem like a great idea. If we wanted to allow access to the top 100 elements of the stack, we would have to add 168 new instructions. Leaving aside the fact that there aren't that many available opcodes, this would be ugly and wasteful.
A better idea is to add only two new instructions, DUPN and SWAPN, that take an arbitrary N as input. Where does that input come from? There are many options, but the two most obvious are:
Take N from the stack. To duplicate the 20th element, you'd execute a PUSH1 20 instruction followed by DUPN.
Use the next byte after the instruction, like PUSH1 does. With this option, a single DUPN 20 instruction would duplicate the 20th element.
It's easy to argue that the second approach is much better: the resulting bytecode is smaller (two bytes vs three), it consumes less gas (one instruction vs two), and it's more friendly to static analysis.
Arguments encoded directly in the bytecode like that are called immediates. Right now, only the PUSH* instructions use them; every other instruction that has an input takes it from the stack. These PUSH* immediates have different lengths depending on the instruction: one byte for PUSH1 (e.g., PUSH1 0x14), two bytes for PUSH2 (e.g., PUSH2 0x1234), etc. For our two new instructions, a one-byte immediate would be enough to let us access the top 256 elements of the stack, a significant improvement over the current 16 limit.
In fact, we could even do a little better. Using DUPN with a value like 5 wouldn't make sense given that DUP5 already exists (DUPN 5 would have the same behavior). But we could define our new instructions so that the value of the immediate is not interpreted verbatim. Instead of saying that DUPN X duplicates the Xth element of the stack, we could say that it refers to the item at position X + 17: DUPN 0 would duplicate the 17th element, DUPN 1 the 18th, and so on. This way, the combination of existing DUP* instructions plus the new DUPN would let us access the top 272 elements.
This all sounds great. Sadly, things aren't that simple. To explain why, we need to take a detour and talk about jumps.
The EVM works in a simple, sequential way: it executes an instruction, moves to the next one, executes it, advances again. The only exception to this behavior is the JUMP instruction4, which takes an item from the stack and continues the execution at that position. But it can't just jump anywhere in the bytecode: the destination has to be JUMPDEST (0x5b), a special instruction that serves only as a marker for valid jump destinations.
You'd be excused for thinking that this is a simple check: read the byte at the jump position and verify that its value is 0x5b, the JUMPDEST opcode. But that doesn't work. Imagine there's a PUSH1 0x5b instruction in the code. Is its second byte a valid jump destination?

For the EVM, it's not: you get an exceptional halt and the transaction reverts.
This is where JUMPDEST analysis comes in. Before executing any code, you need to process the whole bytecode to find out which positions are valid jump destinations. This, incidentally, has to start from the beginning; you can't just look at the bytes before a 0x5b and figure out if it's a valid JUMPDEST or not.
Why can’t you figure out locally if a 0x5b byte is a JUMPDEST instruction?
You could make the following argument: since the “biggest” push instruction is PUSH32, it’s possible to determine whether some 0x5b is a valid jump destination by looking at the previous 32 bytes: check if the one immediately before is a PUSH1, or if the one before that is a PUSH2, or if the one 32 positions away is a PUSH32. If none of those is true, then it’s a valid JUMPDEST.
It’s easy to prove that this approach doesn’t work. Imagine that the last byte you check is 0x7f, which corresponds to the PUSH32 opcode.

The reason JUMPDEST analysis is relevant to this discussion is that our new instructions take immediates. Why is that a big deal? We could just include them as part of the analysis and forbid a jump to the second byte of SWAPN 0x5b. Except we can't. To understand this, let's assume that the opcode of SWAPN is going to be 0xaa and that the following bytecode is already deployed onchain:

This is a perfectly valid bytecode. There is no instruction for opcode 0xaa yet, but that doesn’t matter because we are jumping over it to a valid destination.5 But what happens if we upgrade the EVM and 0xaa becomes the opcode for SWAPN? The disassembled code now looks like this:

Oops, now we are jumping to an invalid destination, even when the underlying bytecode is the same! This is a breaking change.
Conversely, an invalid destination can become valid. In the following example, we are trying to jump to a 0x5b which is the immediate of a PUSH1 instruction:

This will result in an "invalid jump" exception today. But if we upgrade the EVM with our new instruction...

The opcode of PUSH1 becomes the immediate of SWAPN and the 0x5b becomes a valid destination.
At this point you might be wondering why there would be an invalid instruction in the middle of some code. One reason is that the EVM doesn't have separate sections of a "program" for code and data. Any arbitrary data included in the contract to be used in runtime (through an instruction like CODECOPY) has to be part of the bytecode. Since this is just data, it can easily have bytes that correspond to nonexistent instructions.
In practice, this kind of data is always placed at the end of the bytecode, not in the middle of it. But that doesn't matter. A breaking change in something like the EVM is serious, even if we speculate that the impact will be low.
And now, at last, we have all the context we need to talk about the EIP-8024 proposal.
To recap: we'd like to be able to access deeper parts of the stack to allow high-level languages like Solidity to use it more freely for local variables. To do that, we want to add two new instructions, DUPN and SWAPN, that take as their input a one-byte immediate, letting us access way more than 16 stack elements. But we need to avoid introducing breaking changes related to JUMPDEST analysis.
One possible solution is to forbid the problematic bytes: 0x5b (JUMPDEST) and the 0x60–0x7f range (PUSH1–PUSH32). We can also simplify and disallow the complete 0x5b–0x7f range, even if that includes four instructions that aren't problematic. Restricting the input range like this doesn't feel right though. A compiler would have access to more parts of the stack, but it won't be able to emit instructions like DUPN 91 and it will have to work its way around it.
This is far from ideal. We can do better. And, in fact, we've already hinted at a solution when we suggested interpreting a 0 value as 17.
Let's reframe the problem. We have a restricted range we can use for the immediate values: from 0x0 to 0x5a (0 to 90) and from 0x80 to 0xff (128 to 255). We want to map that range to a continuous range starting from 17.

One option is to define our decode function like this:
def decode(n):
assert 0 <= n <= 90 or 128 <= n <= 255
if n <= 90:
return n + 17
return n - 20Which maps our ranges this way:

But we can use an even tighter definition through modular arithmetic:
def decode(n):
assert 0 <= n <= 90 or 128 <= n <= 255
return (n + 145) % 256Which results in the following mapping:

In other words, a bytecode that includes the DUPN 128 instruction will duplicate the 17th element, and DUPN 0 will duplicate the 145th one. This second approach is the one that the EIP proposes.
The EIP also adds a new EXCHANGE instruction that lets you swap two elements deeper in the stack, instead of always involving the top element like SWAPN does.6 Similar to the other two new instructions, EXCHANGE takes as input a one-byte immediate which is only allowed to have values that don’t break JUMPDEST analysis. This single byte is used to encode two operands, M and N. The instruction then swaps the M+1 and N+1 stack items. The exact details are interesting, but this article is long enough as it is; check the resources at the end if you want to learn more.
While EIP-8024 hasn’t been officially confirmed as part of Glamsterdam, it has already been implemented by clients and tested in devnets7, meaning it has a very high chance of being included in the upgrade. If that happens, solc’s legacy pipeline will finally be able to get rid of the “stack too deep” error for good without incurring any loss of expressiveness in the language. And that’s not all: the alternative IR-pipeline, vyper, and any other compilers will also benefit from these new capabilities.
Check the EIP and the Ethereum Magicians discussion for more details about the EXCHANGE instruction and how it encodes the input in a single byte.
The EIP author has written some great explainers about adjacent topics that are completely worth reading:
Compiling for the EVM: Codegen for Stack Machines, about why other stack-based virtual machines (JVM, Wasm, CLR) don’t have the “stack too deep” error.
Spilling in the EVM, which continues the previous post, discussing the technique of moving values from stack to memory.
EVM Immediates, an Ethereum magicians post that explores the solution space for the problem of adding new instructions with immediates.
To elaborate: you can still have code that won't compile if, for example, you have a lot of local variables and they are used in a recursive function. Admittedly this is not an everyday scenario, but it shows that the solution is not perfect.
↩There are other areas, like transient storage or the return data region, but those don't really matter for this discussion.
↩Technically, the DUP* instructions access the top 16 elements while the SWAP* instructions use the top 17 items, because SWAPX swaps the 1st and X+1th elements (otherwise SWAP1 would be a no-op).
There's also JUMPI, which also jumps to some destination but only if some condition is true.
Bytes that don't correspond to opcodes of existing instructions can show up in the code, but executing them results in an exceptional halt and the transaction reverts consuming all remaining gas.
↩This is useful for compilers for reasons that go over my head.
↩Devnets are temporary, development-only testnets used by Ethereum core developers to test EIPs before they reach public testnets.
↩Does that tell you that the 0x5b at i is part of a PUSH32 immediate, and therefore an invalid jump destination? Well, no! That 0x7f byte could in turn be the immediate argument of a PUSH1 just beyond the range you checked:

And, of course, you can't even be sure whether that's actually a PUSH1 or if it's part of the immediate of an instruction before it. There’s no way around it: you have to start from the beginning.
Does that tell you that the 0x5b at i is part of a PUSH32 immediate, and therefore an invalid jump destination? Well, no! That 0x7f byte could in turn be the immediate argument of a PUSH1 just beyond the range you checked:

And, of course, you can't even be sure whether that's actually a PUSH1 or if it's part of the immediate of an instruction before it. There’s no way around it: you have to start from the beginning.

Understanding Block-Level Access Lists
Parallel execution? You’ve got to have BALs

Deterministic Deployments, Part 3: Other Approaches
Preinstalls, RIP-7740, EIP-7997, and ERC-7955

Deterministic Deployments, Part 1: The Basics
Managed keys, Nick's method, and pre-signed transactions.

Understanding Block-Level Access Lists
Parallel execution? You’ve got to have BALs

Deterministic Deployments, Part 3: Other Approaches
Preinstalls, RIP-7740, EIP-7997, and ERC-7955

Deterministic Deployments, Part 1: The Basics
Managed keys, Nick's method, and pre-signed transactions.
Technical deep dives for those navigating the turbulent waters of Ethereum.
Technical deep dives for those navigating the turbulent waters of Ethereum.
Share Dialog
Share Dialog
Franco Victorio
Franco Victorio

Subscribe to Cethology

Subscribe to Cethology
<100 subscribers
<100 subscribers
No activity yet