Back to blogs
Written by
Vasiliy Gualoto
Published on
April 28, 2024

11 Advanced Solidity Gas Optimization Tips

In this guide on the best Solidity gas optimization techniques, you will learn 11 advanced, real-world, and tested strategies

Table of Contents

Optimising the gas costs of your solidity smart contracts can save more than 90% in transactions to you and your users, making your protocol: more scalable, cheaper, and successful long term.

In this guide on the best Solidity gas optimization tips and techniques, you will learn 11 advanced, real-world, and tested strategies taught by top-notch web3 developers to reduce the gas costs of your smart contracts.

Keep in mind, that the examples in this guide come from really simple contracts, are for demonstration purposes only, and in most cases, only take into consideration runtime Gas costs, as deployment costs can greatly vary based on the size of the smart contract.

In real-life scenarios, we strongly suggest each smart contract should go through a complete and in-depth auditing process.

For all the examples and tests in this article, you can refer to the Github gas optimization tips repository.

The image shows a table with the top solidity gas optimisation tips and the average gas saved for each
Solidity gas optimisation tips infographic

Before getting started with this web3 development guide, let’s quickly refresh why gas optimization is important!

The importance of Solidity gas optimization

Gas optimization is crucial for developers, users, and the long-term success of projects and protocols. Efficiently optimizing the gas of your smart contracts, will make your protocol more cost-effective, and scalable while reducing security risks such as DoS.

Gas-efficient contracts improve the usability of your product and your UX, enabling faster and cheaper transactions even under congested network conditions.

Simply put, optimising the gas costs makes Solidity smart contracts, protocols, and projects:

  • Cost-effective
  • Efficient
  • Usable

While improving adoption and giving a competitive advantage to the more efficient ones.

On top of this, improving smart contracts’ code helps uncover potential vulnerabilities, making your protocol and its users, more secure.

Note: This guide doesn’t substitute in any way a thorough security review done by top smart contracts auditing firms in web3.

In summary, gas optimization should be a key focus during development, as it isn't just a nice-to-have but a must-have for the long-term success and security of a smart contract.

Without further ado, let's delve into the most effective techniques for optimizing gas usage.

Disclaimer: all the tests in this guide are done using Foundry and the following setup:

  • Solidity version: ^0.8.13;
  • Local Blockchain Node: Anvil
  • Command used: forge test
  • Optimization runs: 100

Every test has been run 100 times, and all the results in this guide are the average of results between all tests.

Solidity Gas Optimization Tips

1. Minimize on-chain data

As a developer, it is crucial to question the necessity of recording on a chain all user data, NFT game statistics, or any other extensive information Solidity smart contracts might handle.

By allocating less storage to store variables, you can significantly reduce the gas consumption of your smart contracts.

One effective approach to achieve this is: using events to store data off-chain instead of storing data directly on-chain.

Using events will inevitably increase the gas cost for each transaction due to the extra emit function. However, the savings gained from not storing the information on-chain outweigh this cost.

Take this Contract into consideration, which pushes all the data to a struct each time the vote the function is executed.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientVotingStorage {
    struct Vote {
        address voter;
        bool choice;
    }

    Vote[] public votes;

    function vote(bool _choice) external {
        votes.push(Vote(msg.sender, _choice));
    }
}

Testing the vote function using Foundry over 100 times we get these results:

Solidity gas optimization by minimizing on-chain data

On the other hand, we have another smart contract that is not going to store the information on-chain but instead just emits an event each time the vote function is called.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EfficientVotingEvents {
    event Voted(address indexed voter, bool choice);

    function vote(bool _choice) external {
        emit Voted(msg.sender, _choice);
    }
}

As you can see, just by minimizing on-chain data, we saved an outstanding 90.34% gas on average.

If you want to get your off-chain stored data you can then use something like Chainlink functions.

Solidity minimizing on-chain data Test:

Gas usage before optimization: 23,553

Gas usage after optimization: 2,274

Gas usage average reduction: 90.34%

2. Use Mappings over Arrays

In Solidity, mappings serve as an excellent tool for establishing relationships between multiple pieces of information. When it comes to representing lists of data, Solidity offers two data types: arrays and mappings.

Arrays store a collection of items, each assigned to a specific index. On the other hand, mappings are key-value data structures that provide direct access to data through unique keys.

While arrays might be useful to store vectors and similar data, mappings are generally recommended to use whenever possible, especially when used to store items that need to be retrieved on demand such as names or wallets and balances.

To understand why, we need to remember that even if read functions in Solidity smart contracts are free when called from an EOA, they still incur costs when called as part transactions, and are charged based on the gas consumed. If to retrieve a value, we need to loop through every item of an array, we’ll need to pay for each gas consumed by the related EVM opcodes.

To illustrate this concept, here's an example showcasing the use of arrays and their equivalent mappings in Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract UsingArray {
    struct User {
        address userAddress;
        uint256 balance;
    }

    User[] public users;

    function addUser(address _user, uint256 _balance) public {
        users.push(User(_user, _balance));
    }

    // Function to simulate user retrieval as would be required in an array
    function getBalance(address _user) public view returns (uint256) {
        for (uint256 i = 0; i < users.length; i++) {
            if (users[i].userAddress == _user) {
                return users[i].balance;
            }
        }
        return 0;
    }
}

In the above example, we are using an array to store the addresses of users and their corresponding balances. When we need to retrieve the user’s balance, we’ll have to loop through each item, look if the userAddress matches the _userAddress argument, and if it matches, return the balance.

Messy, right?

Instead, we can use a mapping to directly access the balance of a particular user without having to iterate through all the elements in the array:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract UsingMapping {
    mapping(address => uint256) public userBalances;

    function addUser(address _user, uint256 _balance) public {
        userBalances[_user] = _balance;
    }

    // Function to fetch user balance directly from mapping
    function getBalance(address _user) public view returns (uint256) {
        return userBalances[_user];
    }
}

By replacing the array with a mapping, we save on gas costs as we no longer need to loop through all the elements to fetch the data we need.

In this test, just by substituting the array with a mapping, we have optimized our gas by 93% on average.

optimise smart contracts gas cost by using mappings vs arrays - retrieval function results

And this also applies to the retrieval function which has an outstanding saving of 89% in gas cost.

optimise smart contracts gas cost by using mappings vs arrays

Solidity mappings over arrays test:

Gas usage before optimization: 30,586

Gas usage after optimization: 3,081

Gas saved: 89%

Test link on Github.

3. Use Constant and Immutable to reduce smart contracts gas costs

Another tip when optimising the gas costs of your Solidity smart contracts, is using constants and immutable variables as, unlike other variables, do not consume storage space within the Ethereum Virtual Machine (EVM).

Their values are instead compiled directly into the smart contract bytecode, resulting in reduced gas costs associated with storage operations.

When declaring variables as "immutable" or constant in Solidity, values are assigned exclusively during contract creation and become read-only thereafter, this makes immutable and constant variables more cost-effective to access than regular state variables - as they reduce the gas required for SLOAD operations.

Take into consideration this example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientRegularVariables {
    uint256 public maxSupply;
    address public owner;

    constructor(uint256 _maxSupply, address _owner) {
        maxSupply = _maxSupply;
        owner = _owner;
    }
}

As you can see here, we’re declaring our variables maxSupply and owner without using the constant or immutable keywords. Running our test 100 times, we’ll get an average gas cost of 112,222 units:

image shows what happens by not using constant and immutable to reduce the  smart contracts gas cost

As maxSupply and owner are known values and aren’t planned to be changed, we can declare a maximum supply and an owner for our smart contract that does not consume any storage space.

Let’s add the constant and immutable keywords, slightly changing the declaration:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ConstantImmutable {
    uint256 public constant MAX_SUPPLY = 1000;
    address public immutable owner;

    constructor(address _owner) {
        owner = _owner;
    }
}

By simply adding the immutable and const keywords to our variables in our Solidity smart contract - we now have optimized the average gas spent by a significant 35.89%.

image shows what happens by using constant and immutable as another solidity gas optimisation tip

Solidity constant vs immutable Test:

Gas usage before optimization: 112,222

Gas usage after optimization: 71,940

Gas saved: 35.89%

4. Optimise Unused Variables

Optimizing the variables in a Solidity smart contract is one of those Solidity gas optimization tips that sound obvious. The truth is that it happens a lot that not useful variables are kept in the execution of smart contracts, ending up in avoidable gas costs.

Take a look at the following example of a bad use of variables:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientContract {
    uint public result;
    uint private unusedVariable = 10;

    function calculate(uint a, uint b) public {
        result = a + b; // Simple operation to use as a test
        // This next line alters the state unnecessarily, wasting gas.
        unusedVariable = unusedVariable + a;
    }
}

In this contract, unusedVariable is declared and manipulated in the calculate function, but it is never used anywhere else, neither in the same function nor elsewhere in the contract.

Let’s see how much gas that unused variable is costing us:

Image of test of code with unused variables

Let’s now optimize our contract, by removing the unusedVariable:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EfficientContract {
    uint public result;

    function calculate(uint a, uint b) public {
        result = a + b; // Only the necessary operation is performed
    }
}

image of test with code implementing the solidity gas optimisation tips to remove unused variables

As you can see, just by removing one single unused variable, in our smart contract, we’ve been able to reduce our smart contract gas costs by an average of 18%

Solidity optimize unused variables test:

Gas usage before optimization: 32,513

Gas usage after optimization: 27,429

Gas saved: 18%

5. Solidity Gas refund deleting unused variables

Deleting unused variables doesn’t mean “deleting” them, as it would cause all sorts of issues to pointers in memory - it's more like assigning a default value back to a variable, that when done, grants you a 15,000 units Gas refund.

For example, issuing a delete to a uint variable simply sets the variable's value to 0.

Let’s take a look at a really simple example where we have a variable call data which can store a uint :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract WithoutDelete {
    uint public data;

    function skipDelete() public {
        data = 123; // Example operation
        // Here we're not using delete
    }
}

In this case, we’re not deleting our “data” variable once the function ends, paying a total of 100,300 gas units on average - just to assign that variable to data.

Now let’s see what happens when we use the delete keyword:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract WithDelete {
    uint public data;

    function useDelete() public {
        data = 123; // Example operation
        delete data; // Reset data to its default value
    }
}

Just by deleting our “data” variable, that is: setting its value back to 0, we’re saving 19% Gas on average!

image of test showing solidity code where unused variables were not deleted

Solidity deletes unused variables test:

Gas usage before optimization: 100,300

Gas usage after optimization: 80,406

Gas saved: 19%

6. Use Fixed sized Arrays over Dynamics to reduce your smart contract gas costs

As mentioned earlier, to optimize the gas of your Solidity smart contracts you should use mappings whenever possible.

However, if you find yourself in the need of using arrays in your contracts, it's better to do it by trying to use fixed-sized arrays and avoid dynamically sized ones, as they can grow indefinitely, resulting in higher gas costs.

Simply put, statically sized arrays have known lengths, hence when the EVM needs to store one, it doesn’t need to keep the length of the array readily available in the storage:

Rapresentation of how fixed arrays are stored in storage

On the other hand, dynamically sized arrays can grow in size, hence the EVM needs to keep track of and update their length every time a new item is added:

Rapresentation of how dinamically sized arrays are stored in storage

Let’s take a look at the following code where we declare a dynamically sized array and update it through the updateArray function:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract DynamicArray {
    uint256[] public dynamicArray;

    constructor() {
        dynamicArray = [1, 2, 3, 4, 5]; // Initialize the dynamic array
    }

    function updateArray(uint256 index, uint256 value) public {
        require(index < dynamicArray.length, "Index out of range");
        dynamicArray[index] = value;
    }
}

Notice that we use a require statement to ensure that the supplied index isn't out of the range of our fixed-size array.

Running our test 100 times will result in 12,541 gas units spent on average.

Now, let’s modify our array to be of fixed size 5:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract FixedArrayContract {
	uint256[5] public fixedArray;
	    constructor() {
	        fixedArray = [1, 2, 3, 4, 5]; // Initialize the fixed-size array
	    }
	    function updateArray(uint256 index, uint256 value) public {
	        require(index < fixedArray.length, "Index out of range");
	        fixedArray[index] = value;
	    }
}

In this example, we define a fixed-size array of length 5 of type uint256. The updateArray function, the same as before, allows us to update the value at a specific index of our array.

The EVM will now know that the state variable fixedArray is of size 5, and will allocate 5 slots to it, without having to store its length in storage.

Running the same test 100 times, just by using a fixed array instead of a dynamic one, we have saved 17.99% of gas costs.

Optimize unused variables test:

Gas usage before optimization: 12,541

Gas usage after optimization: 10,284

Gas saved: 17.99%

7. Avoid using lower than 256bit variables

Using uint8 instead of uint256 in Solidity can be less efficient and potentially more costly in certain contexts, primarily due to the way the Ethereum Virtual Machine (EVM) operates.

The EVM operates with a word size of 256 bits. This means that operations on 256-bit integers (uint256) are generally the most efficient, as they align with the EVM's native word size. When you use smaller integers like uint8, Solidity often needs to perform additional operations to align these smaller types with the EVM's 256-bit word size. This can result in more complex and less efficient code.

While using smaller types like uint8 can be beneficial for optimizing storage (since multiple uint8 variables can be packed into a single 256-bit storage slot), this benefit is typically seen only in storage and not in memory or stack operations.

On top of it, for computations, the conversion to and from uint256 can negate the storage savings.

uint8 public a = 12;
uint256 public b = 13;
uint8 public c = 14;

// It can lead to inefficiencies and increased gas costs
// due to the EVM's optimization for 256-bit operations.

In summary, while using uint8 may seem like a good way to save space and potentially reduce costs, but in practice, it can lead to inefficiencies and increased gas costs due to the EVM's optimization for 256-bit operations.

uint256 public a = 12;
uint256 public b = 14;
uint256 public c = 13;

// Better solution

You can create transactions that invoke a function f(uint8 x) with a raw byte argument of 0xff000001 and 0x00000001. Both are supplied to the contract and will appear as the number 1 to x. However, msg.data will differ in each case. Therefore, if your code implements things like keccak256(msg.data) running any logic, you will get different results.

8. Pack smaller than 256-bit variables together

As we said before, using lower than 256-bit int or uint variables is generally considered less efficient than 256 variables, but there are situations where you’re forced to use smaller types, such as when using booleans that weigh 1 byte, or 8-bit.

In these cases, by declaring your state variables with the storage space in mind, Solidity will allow you to pack them, and store them all in the same slot.

Note: The benefit of packing variables is typically seen only in storage and not in memory or stack operations.

Let’s take into consideration the following example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract NonPackedVariables {
    bool public a = true;
    uint256 public b = 14;
    bool public c = false;

    function setVariables(bool _a, uint256 _b, bool _c) public {
        a = _a;
        b = _b;
        c = _c;
    }
}

Considering what we’ve said before, that each storage slot in Solidity has a space of 32 bytes equal to 256-bit, in the example above we’ll have to use 3 storage slots to store our variables:

  • 1 to store our boolean “a” (1 byte)
  • 1 to store our uint256 “b”(32 bytes)
  • 1 to store our boolean “c” (1 byte).

Each storage slot used incurs a gas cost, hence we’re spending 3 times that cost.

Given that the combined size of the two boolean variables is 16 bits, which is 240 bits less than a single storage slot's capacity, we can instruct Solidity to store variables "a" and "c" in the same slot, also said: “we can pack them”.

Packing the variables together allows you to lower your smart contract deployment gas costs by reducing the number of slots required to store state variables.

We can pack these variables together by re-ordering their declarations as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract PackedVariables {
    bool public a = true;
    bool public c = false;
    uint256 public b = 14;

    function setVariables(bool _a, bool _c, uint256 _b) public {
        a = _a;
        c = _c;
        b = _b;
    }
}

Solidity will pack the two boolean variables together in the same slot as they weigh less than 256-bit or 32 bytes.

That said, keep in mind that we're still potentially wasting storage space. The EVM operates on 256-bit words and will have to perform operations to normalize smaller-sized words. This might offset any potential gas savings.

Running our test over 100 iterations we get an average of 13% optimization.

Variables packing test:

Gas usage before optimization: 1,678

Gas usage after optimization: 1,447

Gas saved: 13%

9. Use External Visibility Modifier

In Solidity, choosing the most appropriate visibility for functions can be an effective measure to optimize you smart contract gas consumption. Specifically, using the external visibility modifier can be more gas-efficient than public.

The reason has to do with how public functions handle arguments, and how the data is passed to these functions.

External functions can read from calldata, a read-only, temporary area in the EVM storing function call parameters. Using calldata is more gas-efficient for external calls because it avoids copying data from the transaction data to memory.

On the other hand, functions declared as public can be called both internally (from within the contract) and externally. When called externally, they behave similarly to external functions, with parameters passed in the transaction data. However, when called internally, the parameters are passed in memory, not in calldata.

Simply put, since public functions need to support both internal and external calls, they cannot be restricted to only accessing calldata.

Consider the following Solidity contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract PublicSum {
    function calculateSum(
        uint[] memory numbers
    ) public pure returns (uint sum) {
        for (uint i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
    }


This function calculates the sum of an array of numbers. Because the function is public, it has to accept an array from memory which, if large, can be costly in terms of gas.

Now let's modify this function by making it external:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ExternalSum {
    function calculateSum(
        uint[] calldata numbers
    ) external pure returns (uint sum) {
        for (uint i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
    }
}

Contract link.

By changing the function to external, we can now accept arrays from calldata, making it more gas-efficient when dealing with large arrays.

This highlights the importance of properly using visibility modifiers in your Solidity smart contracts to optimize gas usage.

In this case, modifying your Solidity function modifiers leads to saving on average 0.3% Gas units each call.

Optimize unused variables test:

Gas usage before optimization: 495,234

Gas usage after optimization: 493,693

Gas saved: 0.3%

Test link on Github.

10. Enable Solidity Compiler Optimization

Solidity comes equipped with a compiler with easy-to-modify settings to optimize your codebase compiled code.

Consider the Solidity compiler as a wizard's spell book, and your intelligent manipulation of its options can create potions of optimization that significantly reduce gas usage.

The --optimize option is one such spell you can cast.

When enabled, it performs several hundred runs, streamlining your bytecode and translating it into a leaner version that consumes less gas.

The compiler can be adjusted to strike a balance between deployment cost and runtime cost.

For instance, using the --runs command lets you define the estimated number of executions your contract will have.

  • Higher number: the compiler optimizes for cheaper gas costs during contract execution.
  • Lower number: the compiler optimizes for cheaper gas costs during contract deployment.

solc --optimize --runs=200 GasOptimized.sol

By using the --optimize flag and specifying --runs=200, we instruct the compiler to optimize the code to reduce gas consumption during contract executions by running the incrementCount function around 200 times.

Make sure to adjust these settings based on your application's unique needs.

11. Extra Solidity Gas Optimization tip: use Assembly*

When you compile a Solidity smart contract, the compiler will transform it into bytecodes, a series of EVM (Ethereum Virtual Machine) opcodes.

By using assembly, you can write code that operates at a level closely aligned with opcodes.

While it may not be the easiest task to write code at such a low level, the advantage lies in the ability to manually optimize the opcodes, thereby outperforming Solidity bytecode in certain scenarios.

This level of optimization allows for greater efficiency and effectiveness in contract execution.

In a simple example with two functions intended to add two numbers, one using plain solidity and the other one using assembly we have small differences but the assembly one is still cheaper.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract InefficientAddSolitiy {
    // Standard Solidity function to add two numbers
    function addSolidity(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }
}

Now implementing assembly:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EfficientAddAssembly {
    // Function using assembly to add two numbers
    function addAssembly(
        uint256 a,
        uint256 b
    ) public pure returns (uint256 result) {
        assembly {
            result := add(a, b)
        }
    }
}

We want to make an honorific mention to Huff, which allows us to write assembly with prettier syntax.

Note: Even if using Assembly might help you optimize the Gas cost of your smart contracts, it might also lead to insecure code. We strongly recommend having your contracts reviewed by smart contract security experts before deploying.

Conclusion

Optimizing gas usage in Solidity is essential for creating cost-effective, high-performing, and sustainable Solidity smart contracts. By implementing the Solidity gas optimization tips you've learned in this guide, you can reduce transaction costs, improve scalability, and enhance the overall efficiency of your contracts.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.