Solidity Gotchas — Part 4: Stack too deep

Simon Palmer
3 min readApr 21, 2022

--

Remember… the EVM is a stack machine, not a register machine.

oops

Practically speaking, the EVM stack is a maximum of 16 deep. Every variable you create will get pushed onto the stack, that includes function parameters and locally declared variables. As a result you quickly hit practical limits on how much you can pass into a function, and how much complexity you can build into a single function.

If you do something to make the stack too deep the compiler will fail you. Here’s an example…

function veryDeepStack(
uint16 a_1,
uint16 a_2,
uint16 a_3,
uint16 a_4,
uint16 a_5,
uint16 a_6,
uint16 a_7,
uint16 a_8,
uint16 a_9,
uint16 a_10,
uint16 a_11,
uint16 a_12,
uint16 a_13,
uint16 a_14,
uint16 a_15,
uint16 a_16,
uint16 a_17
)
public
returns (bool) {
return false;
}
CompilerError: Stack too deep, try removing local variables.
--> project:/contracts/deepstack.sol:55:5:
|
55 | function veryDeepStack(
| ^ (Relevant source part starts here and spans across multiple lines).

A variant of that error you may encounter is…

CompilerError: Stack too deep when compiling inline assembly: Variable headStart is 1 slot(s) too deep inside the stack.

They both amount to the same thing. You have filled the stack and you are going to have to change your design to fix it.

On balance this is good, because it causes a depth of thought about the architecture of your code that you wouldn’t bother with without these constraints. Ultimately you are likely to end up with tighter and better factored code as a result. Stack limitations, plus a good library strategy, will steer you towards very cleanly architected code.

Why the constraint?

The EVM is a 256 bit word machine. Its stack has a maximum size of 1024 slots. Every time you pop something onto the stack you use up one of the top slots. Once you get past 16 the compiler will throw an error.

This doesn’t mean 16 variables exactly because it depends on what you are declaring and how wide it is. You may well bump into this constraint with fewer variables but more complex types, such as amapping.

Structs to the rescue

It is perfectly possible to encounter business problems where this limitation can’t be solved just be refactoring. Fortunately there is a neat solution and that’s to use struct.

If you pack your data into a struct and pass that into your function, you only use a single stack slot…

struct veryDeepStruct{
uint16 a_1;
uint16 a_2;
uint16 a_3;
uint16 a_4;
uint16 a_5;
uint16 a_6;
uint16 a_7;
uint16 a_8;
uint16 a_9;
uint16 a_10;
uint16 a_11;
uint16 a_12;
uint16 a_13;
uint16 a_14;
uint16 a_15;
uint16 a_16;
uint16 a_17;
}
function notVeryDeepStack(veryDeepStruct memory a_lot)
public
returns (bool) {
return false;
}

Design your data

This goes hand in hand with factoring code for libraries. Think carefully about how your data groups together and consider making domain related structs which contain data that is commonly used together.

A good direction to consider that from can be the code. If you find you are passing the same 3 parameters to a bunch of functions, perhaps there is enough commonality between the code and the data that it factors into its own struct and library. Maybe even its own contract.

That’s all on this for now although there is a lot of excellent reading out there about the EVM stack. Here are a few articles I learned a lot from:

This towardsblockchain article on backstage at the EVM

That’s all for now on this topic. I will add as I learn more.

--

--

Simon Palmer

CTO in the wild. I’ve been in the software industry for approximately forever. Let me know if you need help, especially if you are grappling with blockchain.