This error typically occurs when a function has too many local variables, and the Ethereum Virtual Machine (EVM) imposes a limit on the number of variables that can be pushed onto the stack.
Here the follows investigation that I made today.
References of some examples are given here:
Let’s start by examining a simple Solidity contract that triggers the “Stack too deep” error:
pragma solidity ^0.8.19;
contract Deep2Stack {
function performCalculation(
uint256 var1,
uint256 var2,
uint256 var3,
uint256 var4,
uint256 var5,
uint256 var6,
uint256 var7,
uint256 var8,
uint256 var9
) public pure returns (uint256) {
return var1 + var2 + var3 + var4 + var5 + var6 + var7 + var8 + var9;
}
}
The simplest method involves dividing the function logic into smaller components and using accessor functions to access variables beyond the stack size limit. By storing specific variables as state variables and implementing accessor functions, we can avoid the “Stack too deep” error:
pragma solidity ^0.8.19;
contract Deep2Stack {
uint256 value1;
uint256 value2;
function store(uint256 num1, uint256 num2) public {
value1 = num1;
value2 = num2;
}
function get_value1() public view returns (uint256) {
return value1;
}
function get_value2() public view returns (uint256) {
return value2;
}
function performCalculation(
uint256 num3,
uint256 num4,
uint256 num5,
uint256 num6,
uint256 num7,
uint256 num8,
uint256 num9,
uint256 num10
) public view returns (uint256) {
return get_value1() + get_value2() + num3 + num4 + num5 + num6 + num7 + num8 + num9 + num10;
}
}
This approach leverages contract storage but helps avoid stack size limitations.
If certain variables have default values, directly assign them within the contract to reduce the number of function parameters:
pragma solidity ^0.8.19;
contract Deep2Stack {
uint256 value1 = 10;
uint256 value2 = 20;
uint256 value3 = 30;
uint256 value4 = 40;
uint256 value5 = 50;
function performCalculation(
uint256 value6,
uint256 value7,
uint256 value8,
uint256 value9,
uint256 value10
) public view returns (uint256) {
return
value1 +
value2 +
value3 +
value4 +
value5 +
value6 +
value7 +
value8 +
value9 +
value10;
}
}
Divide the function logic into smaller functions to distribute the workload across several components, reducing the number of variables used within each function:
pragma solidity ^0.8.19;
contract Deep2Stack {
uint256 value1;
uint256 value2;
function store(uint256 num1, uint256 num2) public {
value1 = num1;
value2 = num2;
}
function get_value1() public view returns (uint256) {
return value1;
}
function get_value2() public view returns (uint256) {
return value2;
}
function performCalculation(
uint256 num3,
uint256 num4,
uint256 num5,
uint256 num6,
uint256 num7,
uint256 num8,
uint256 num9,
uint256 num10
) public view returns (uint256) {
return
get_value1() +
get_value2() +
num3 +
num4 +
num5 +
num6 +
num7 +
num8 +
num9 +
num10;
}
}
Organize related variables into a struct to enhance code organization and potentially reduce the number of variables used within a function:
pragma solidity ^0.8.19;
contract Deep2Stack {
struct DataInput {
uint256 val1;
uint256 val2;
uint256 val3;
uint256 val4;
uint256 val5;
}
DataInput inputData;
function assignDataInput(
uint256 val1,
uint256 val2,
uint256 val3,
uint256 val4,
uint256 val5
) public {
inputData.val1 = val1;
inputData.val2 = val2;
inputData.val3 = val3;
inputData.val4 = val4;
inputData.val5 = val5;
}
function performCalculation(
uint256 val6,
uint256 val7,
uint256 val8,
uint256 val9,
uint256 val10
) public view returns (uint256) {
return
(inputData.val1 +
inputData.val2 +
inputData.val3 +
inputData.val4 +
inputData.val5) +
val6 +
val7 +
val8 +
val9 +
val10;
}
}
Structs provide a cleaner way to organize and access related data.
It’s not really a solution if we need the variables but simply minimize the number of variables used in the function to comply with the EVM’s stack size limit:
pragma solidity ^0.8.19;
contract Deep2Stack {
function performCalculation(
uint256 var1,
uint256 var2,
uint256 var3,
uint256 var4,
uint256 var5,
uint256 var6,
uint256 var7,
uint256 var8
) public pure returns (uint256) {
return var1 + var2 + var3 + var4 + var5 + var6 + var7 + var8;
}
}
Consider using the --via-ir flag to first compile Solidity into Yul before optimization.
The IR codegen path can automatically move stack variables into memory, overcoming stack size limits. Both Foundry and Hardhat support this flag.
This solution requires minimal effort but may not cover every situation. It might result in clearer and more human-verifiable code to explore other approaches.
Block scoping can be a powerful tool in reducing the number of local variables.
Consider this example:
pragma solidity ^0.8.19;
contract Deep2Stack {
function performCalculation(
uint256 var1,
uint256 var2,
uint256 var3,
uint256 var4,
uint256 var5,
uint256 var6,
uint256 var7,
uint256 var8,
uint256 var9
) public pure returns (uint256) {
uint256 soma1;
{
soma1 = var1 + var2 + var3;
}
return soma1 + var4 + var5 + var6 + var7 + var8 + var9;
}
}
By utilizing block scoping, we can reduce the number of variables pushed onto the stack.
In extreme situations, entering a new execution context by making an external function call might be necessary. This brings a fresh (virtually empty) stack, albeit with some overhead.
While storage reads and writes are expensive, in certain situations, using storage might be a viable option.
Be cautious and consider gas costs.
For complex computations, consider moving some of the workload off-chain and passing only the result to the contract.
The contract then performs a simpler verification step.
Bit-packing involves using bitwise arithmetic to pack multiple values into a single variable. This can help reduce the number of variables but may sacrifice code readability.
In conclusion, the “Stack too deep” error in Solidity can be addressed using various approaches, ranging from simple restructuring to more unconventional solutions. The choice of approach depends on the specific requirements and constraints of your project.