Back to blogs
Written by
Jessica Young
Published on
December 5, 2024

Mastering Solidity Gas Efficiency: 12 Tips to Tackle Rising Fees on Base and Other L2 Chains

Cyfrin’s guide to the best Solidity gas optimization techniques for Base and other L2 Chains. Reduce cost with advanced, real-world, and tested strategies.

Table of Contents

Introduction: Optimizing Gas Costs on Layer 2 Chains

Ethereum is widely recognized as the leading blockchain platform, but ongoing concerns exist about its fluctuating transaction fees. To combat this issue, a number of L2s, such as Base, have been developed with a focus on scalability and minimizing gas costs. While L2s offer significant reductions in gas fees compared to the Ethereum mainnet, smart contract developers are still responsible for prioritizing gas optimization during development. By doing so, they can enhance the user experience and create more competitive dApps.

In this guide on the best Solidity gas optimization tips and techniques, you will learn advanced, real-world, and tested strategies taught by skilled 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 and are for demonstration purposes only. In most cases, they only take into consideration runtime gas costs, as deployment costs can vary significantly based on the size of the smart contract.

In real-life scenarios, we strongly suggest that each smart contract undergo a complete, in-depth auditing process.

For all 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

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 denial of service (DoS) attacks.

Gas-efficient contracts enable faster and cheaper transactions, even under congested network conditions, improving your product and user experience.

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

  • Cost-effective
  • Efficient
  • Usable

Furthermore, improving a smart contracts’ code helps uncover potential vulnerabilities, making your protocol and its users more secure.

Note: This guide does not substitute for a thorough security review of your contracts by top smart contracts auditing firms in web3.

In summary, gas optimization should be a key focus during development. It isn't simply a “nice-to-have,” but a must-have for your smart contracts' long-term success and security. Just because you are building on an L2 with relatively low fees does not mean that your gas fees won’t be dramatically higher than your competitor’s! 

Let's delve into the most effective techniques for optimizing gas usage. 

Disclaimer: all tests in this guide are executed using Foundry with the following setup:

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

Solidity gas optimization tips

1. Minimize on-chain data

NFTs gained popularity in 2021, and with that came a growing interest in fully on-chain NFTs. Unlike traditional NFTs, which reference off-chain data such as metadata and images, on-chain NFTs store everything directly on the blockchain. These tokens are notoriously expensive to interact with, and hybrid solutions quickly became the norm when users saw the impact this had on the fees.

As a developer, it is crucial to question the necessity of recording data on-chain. Whether you are creating an NFT, a game, or a DeFi protocol, always question what data actually needs to be stored on-chain and consider the trade-offs for both options.

You can significantly reduce the gas consumption of your smart contracts by storing information off-chain, because you allocate less storage to store variables.

One practical approach is to use events to store data off-chain instead of storing data directly on-chain. Events inevitably increase transaction gas costs due to the extra emit function; however, the savings generated by not storing the information on-chain often outweigh the cost.

Let us review a smart contract that allows its users to vote `true` or `false. In the first example, we will store a user’s vote in a struct on-chain.

// 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:

Test output showing successful execution of testVotingStorageCost with gas usage and log details.

To compare, let’s look at a smart contract that does not store the information on-chain, but 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);
    }
}

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

Test output showing successful execution of testVotingEventsCost with gas usage, logs, and suite results.

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

Test output showing successful execution of testEfficiencyComparisonOnVsOffChain with gas usage details, including average gas used with storage and events, and 90% gas savings for off-chain data storage. Suite results indicate 1 test passed, 0 failed, 0 skipped.

To access off-chain data, on-chain, you can use a solution like Chainlink functions, which supports most popular L2 networks, including Base. 

Test results of gas usage, minimizing on-chain data with Solidity:

Before optimization: 23,564
After optimization: 2,274
Average reduction: 90%
Test link on GitHub.

2. Use mappings over arrays

Solidity offers two primary data structures to manage data: arrays and mappings. Arrays store a collection of items, each assigned to a specific index. Mappings, on the other hand, are key-value data structures that provide direct access to data through unique keys. 

While arrays might be useful for storing vectors and similar data, mappings are generally preferred for their gas efficiency. They are particularly well-suited for scenarios where data needs to be retrieved on demand, such as names, wallet addresses, or account balances.

To best understand the gas costs incurred when using an array or a mapping, we need to review the gas consumed by the related EVM opcodes. Opcodes are the low-level instructions that the Ethereum Virtual Machine (EVM) executes when running smart contracts, and each opcode has a gas cost. 

To retrieve a value by looping through every item of an array, we must pay for each unit of gas consumed by the associated EVM opcodes.

To illustrate this concept, here's an example illustrating 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;
            }

In the above example, we are using an array to store users' addresses and corresponding balances. To retrieve a user’s balance, we’ll have to loop through each item, determine 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];
    }
}

In this test, after substituting the array with a mapping, retrieving the data used 89% less gas!

Test result for testEfficiencyComparison showing average gas usage for array and mapping, with 89% gas savings using mapping.

Also, the cost of adding data to a mapping is 93% less than adding to an array.

Test result for testMappingVsArray showing average gas usage for adding a user with array and mapping, with 93% gas savings using mapping.

Test results of gas usage when retrieving data from mappings over arrays in Solidity:

Before optimization: 30,586
After optimization: 3,081
Average savings: 89%
Test link on GitHub.

3. Use constant and immutable to reduce smart contracts gas costs

Another tip when optimizing gas costs of your Solidity smart contracts is to use constants and immutable variables. When declaring variables as immutable or constant in Solidity, values are assigned exclusively during contract creation and become read-only thereafter. 

Unlike other variables, they do not consume storage space within the EVM. Their values are instead compiled directly into the smart contract bytecode, resulting in reduced gas costs associated with storage 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 below in our Foundry test, we’re declaring our variables maxSupply and owner without using the constant or immutable keywords. Running our test 100 times, we get an average gas cost of 112,222 units.

Test result for testRegularVariablesCost showing gas usage for regular variables on the Anvil blockchain.

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 constant keywords to our variables in our Solidity smart contract, we have optimized the average gas spent by a significant 35.89%.

Test result for testConstantImmutableCost showing gas usage for constant immutable variables on the Anvil blockchain.

Gas usage test using constant or immutable variables in Solidity:

Before optimization: 112,222
After optimization: 71,940
Average savings: 35.89%
Test link on Github.

4. Optimize unused variables

Optimizing the variables in a Solidity smart contract is an obvious gas optimization tip. However, unusable variables are often kept in the execution of smart contracts, resulting 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.

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

Test result for testInefficientContractGas on Anvil blockchain, showing gas usage for an inefficient contract.

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

Test result for testEfficientContractGas on Anvil blockchain, showing gas usage for an efficient contract.

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

Gas usage test removing unused variables in Solidity:

Before optimization: 32,513
After optimization: 27,429
Average savings: 18%
Test link on Github.

5. Solidity gas refund deleting unused variables

Deleting unused variables doesn’t mean “deleting” them—this would cause all sorts of issues with pointers in memory. It's more like assigning a default value back to a variable once a calculation is completed to avoid the data being pushed to storage.

For example, the default value for a uint variable is 0.

Let’s take a look at a simple example:

// 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, by not deleting the data variable once the function ends, we pay an average of 100,300 gas units 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
    }
}

By deleting our data variable (setting its value back to 0), we save 19% gas on average!

Test result for testGasUsageComparison, showing gas usage with and without delete, highlighting 19% gas savings.

Gas usage test deleting unused variables in Solidity:

Before optimization: 100,300
After optimization: 80,406
Average savings: 19%
Test link on Github.

6. Use fixed-sized arrays over dynamic arrays to reduce smart contract gas costs

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

However, if you need to use arrays in your contracts, it's more gas efficient to use fixed-sized arrays than dynamically sized ones because dynamically sized ones can grow indefinitely, resulting in higher gas costs.

Simply put, fixed-sized arrays have known lengths.

Table displaying two elements, n1 and n2, each 32 bytes, with values 0x01 and 0x02.

On the other hand, dynamically sized arrays can grow in size, and the EVM must keep track of its length and update it each time a new item is added.

Table displaying array length and two elements, n1 and n2, each 32 bytes, with values 0x02, 0x01, and 0x02.

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 is within the range of our fixed-size array.

Test result for testDynamicArrayGas, showing gas usage for dynamic array updates on Anvil blockchain.

Running our test 100 times results 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 is the same as before, allowing us to update the value at a specific index in our array.

The EVM now knows 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 Foundry test 100 times and using a fixed array instead of a dynamic one, we save 17.99% gas.

Test result for testFixedSizeArrayGas, showing gas usage for fixed-size array updates on Anvil blockchain.

Gas usage test using a fixed-size array in Solidity:

Before optimization: 12,541
After optimization: 10,284
Average savings: 17.99%
Test link on Github.

7. Avoid using less than 256bit variables

Using uint8 instead of uint256 in Solidity can be less efficient and potentially more costly in certain contexts, primarily due to how the EVM operates.

The EVM operates with a word size of 256 bits. Thus, 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. The result is more complex, 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, not in memory or stack operations.

Further, converting to and from uint256 for computations 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, 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 previously mentioned, using less than 256-bit int or uint variables is generally considered less efficient than 256 variables. However, 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 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 consider 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 about each storage slot in Solidity having a space of 32 bytes (equal to 256-bit), in the example above, we have to use three storage slots to store our variables:

  • 1 to store our booleana” (1 byte)
  • 1 to store our uint256b”(32 bytes)
  • 1 to store our booleanc” (1 byte).

Each storage slot used incurs a gas cost, hence we’re spending three 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, AKA “we can pack them.”

Packing the variables together allows you to lower your 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;
    }
}

Re-ordering these variables allows Solidity to pack the two boolean variables together in the same slot since they weigh less than 256-bit (32 bytes).

However, 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, which might offset any potential gas savings.

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

Test result for testEfficiencyComparisonPacked, showing gas usage for packed and non-packed variables with 13% gas savings for packed variables.

Gas usage test packing variables in Solidity:

Before optimization: 1,678
After optimization: 1,447
Average savings: 13%
Test link on Github.

9. Use external visibility modifier

In Solidity, choosing the most appropriate visibility for functions can be an effective way to optimize your smart contracts 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, public functions can be called 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.

Test result for testPublicSumGas, showing gas usage for a public function on a 200-element array using the Anvil blockchain.

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

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 will save, on average, 0.3% of gas units each call.

Gas usage test using external modifier in Solidity:

Before optimization: 495,234
After optimization: 493,693
Average savings: 0.3%
Test link on Github.

10. Cache storage to prevent re-reading the same value from storage

When you identify multiple ways to reach the same functionality, it is worth reviewing the gas consumed by the related EVM opcodes to find the most gas-efficient approach. 

An effective way to save gas is to avoid reading the same variable from storage multiple times. Reading from storage is more expensive than reading from memory. If you need to use a value multiple times, it is more gas-efficient to read it from storage once, cache it into memory, and then read it from memory all other times.

A lot of smart contract gas wastage is due to these two issues: 

1. Re-reading the same value from storage repeatedly 
2. Writing to storage unnecessarily.

In the following contract, the numbers array is stored in storage. Every time there is an iteration over the sumNumbers function’s for loop, the numbers variable is read from storage.

contract ReadStorage {
   uint256[] public numbers;

   function addNumbers(uint256[] memory newNumbers) public {
       for (uint256 i = 0; i < newNumbers.length; i++) {
           numbers.push(newNumbers[i]);
       }
   }

   function sumNumbers() public view returns (uint256) {
       uint256 sum = 0;
       for (uint256 i = 0; i < numbers.length; i++) {
           sum += numbers[i];
       }
       return sum;
   }
}

To avoid this, we can create a variable stored in memory at the start of the function, and assign it the value of the numbers variable.

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

contract CacheStorage {
   uint256[] public numbers;

   function addNumbers(uint256[] memory newNumbers) public {
       for (uint256 i = 0; i < newNumbers.length; i++) {
           numbers.push(newNumbers[i]);
       }
   }

   function sumNumbers() public view returns (uint256) {
       uint256 sum = 0;
       uint256[] memory numbersArray = numbers;
       for (uint256 i = 0; i < numbersArray.length; i++) {
           sum += numbersArray[i];
       }
       return sum;
   }
}

This reduces the sumNumbers function's gas consumption by 17%, but as the array grows, so will the number of iterations and the gas cost.

Test result for testEfficiencyComparisonCaching, showing gas usage with and without caching, with 17% gas savings due to caching.

Gas usage caching variables in Solidity:

Before optimization: 3,527
After optimization: 2,905
Average savings: 17%
Test link on Github.

11. Avoid initializing variables to default values

When you identify multiple ways to reach the same functionality, it is worth reviewing the gas consumed by the related EVM opcodes to find the most gas-efficient approach. 

When we declare a state variable without initializing them (not assigning them an initial value), they are automatically initialized to their default values when the contract is deployed. The default values are:

  • 0 for integers
  • false for booleans
  • address(0) for addresses

This costs less gas than declaring their value to be the default. 

Let us compare the different declarations that produce the same results.

uint256 number; //this costs less
uint256 number = 0; //this costs more 

bool claim; //this costs less
bool claim = false; //this costs more

address owner;  //this costs less
address owner = address(0); //this costs more

It is common to see a state variable be assigned its default value and be immediately changed upon user interaction, which is not as gas efficient. 

Let us look at two simple contracts with a state variable of type uint256. In the second example, we do not initialize the variable, so it will be assigned its default value.

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

contract WithoutInitializing {
   uint256 counter = 0;
}

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

contract WithInitializing {
   uint256 counter;
}

By not initializing the variables in our Solidity smart contract, we have optimized the average gas spent by 4%.

Test result for testEfficiencyComparisonInitialization, showing gas usage with and without initializing variables to default, with 4% gas savings by not initializing.

Gas usage test using a variables default value in Solidity:

Before optimization: 46,996
After optimization: 44,802
Average savings: 4%
Test link on Github.

12. Enable Solidity compiler optimization

Solidity comes equipped with a compiler with easy-to-modify settings to optimize your 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 then be adjusted to strike the proper balance between deployment and runtime costs.

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

  • 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 200 times.

Adjust these settings to align with your application's unique needs.

13. Bonus Solidity gas optimization tip: use Assembly*

When you compile a Solidity smart contract, the compiler transforms it into bytecodes, a series of EVM 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.

Solidity example:

// 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. 

Deploying projects on L2s such as Base will reduce the cost users will incur to use your protocol, however, it is still your responsibility as the developer to implement the Solidity gas optimization tips you've learnt in this guide. These tips can dramatically 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.