Skip to content

Instantly share code, notes, and snippets.

@helderjnpinto
Last active January 26, 2024 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save helderjnpinto/484f011bd467a829fe800e118b529f02 to your computer and use it in GitHub Desktop.
Save helderjnpinto/484f011bd467a829fe800e118b529f02 to your computer and use it in GitHub Desktop.
Solidity stack too deep errors and solutions.

Solving “Stack too deep” Errors in Solidity EVM

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:

https://coinsbench.com/demystifying-the-stack-too-deep-error-in-solidity-understanding-its-causes-and-solutions-d707ec3bcac9

The Challenge

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;
    }
}

Approach 1: Utilize Accessor Functions

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.

Approach 2: Set Default Values

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;
    }
}

Approach 3: Break Down the Function Logic

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;
    }
}

Approach 4: Implement a Struct Datatype

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.

Approach 5: Use Fewer Variables

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;
    }
}

Approach 6: Compile Sol with IR Codegen

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.

Approach 7: Block Scoping

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.

Approach 8: Unusual Solutions — Entering a New Execution Context

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.

Approach 9: Unusual Solutions — Using Storage (Temporarily)

While storage reads and writes are expensive, in certain situations, using storage might be a viable option.

Be cautious and consider gas costs.

Approach 10: Unusual Solutions — Off-chain Computation

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.

Approach 11: Unusual Solutions — Bit-packing

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment