13 techniques for optimizing gas costs in a smart contract
November 1st, 2024

Intro

Developers always need to make sure they optimize smart contracts for gas usage. Contracts that aren't well optimized can end up using a lot of gas.

What is the problem with that?

Each unit of used gas costs Ether. The more gas a smart contract uses, the more it’ll cost the user to execute that contract. Plus it’ll slow down the entire network, waste computational resources and make the overall ecosystem less efficient.

In this article I want to share some tricks developers should remember to reduce the amount of gas they use to make smart contracts cheaper and more efficient.


This is a full list of techniques I am going to talk about:

  • Use calldata instead of memory

  • Packing variables

  • Delete unused variables

  • Not shrinking variables unless you pack them together

  • Use events instead of storing data on chain

  • Use libraries

  • Use short-circuiting rules

  • Avoid unnecessary variable assignments

  • Avoid doing operations on storage

  • Use fixed size arrays

  • Use mapping instead of arrays

  • Use bytes type instead of string type

  • Use external modifier instead of public modifier if you can


Use calldata instead of memory for function parameters

In Solidity, variables can be stored in different locations. The location affects how the variable is accessed and used. The two most relevant locations for function parameters are memory and calldata.

How using calldata instead of memory helps reducing gas costs?

  • Memory is a mutable location, which means it costs gas to read from and write to. When you declare a variable in memory, Solidity will allocate a space in the contract's memory to store the variable's value.

  • Calldata is an immutable location, it is the data that is passed into a function from the function call itself. It is a read-only location, which means you can only read the data, not modify it.

Reading from calldata is much cheaper than reading from memory because you don't have to pay the gas cost of copying the data.

Let’s look at the example, it will be easier to understand.

The Test1 contract (optimized version) below uses a uint[] calldata parameter. This means that the function will directly access the data passed in from the function call, without needing to copy the entire array into memory:

optimized version
optimized version

In a non-optimized version below (using memory) the function parameter numbers is declared as uint[] memory . This means that the entire array will be copied from the caller's memory into the function's memory, which costs gas:

non-optimized version
non-optimized version

The tradeoff here is that calldata is read-only, so you can't modify the parameters vs memory allows you to mutate the data. But in many cases, you don't even need to modify the function parameters so it will work.


Packing variables

In Solidity, when you compile and deploy a smart contract, a storage structure is created based on the order you declare your state variables. Each storage slot in Ethereum is 32 bytes.

1 slot = 32 bytes = 256 bits

By carefully ordering your variables, you can pack multiple smaller variables into a single storage slot and save gas as a result. For example, in the contract below the variables are efficiently packed:

optimized version
optimized version

The uint8 variables x and y together use only 16 bits, so they can share a slot with the uint128 variable w, which uses 128 bits. The uint256 variable z occupies its own slot.

So as a result we have 4 variables that occupy 2 slots. This decreases the gas costs for storage operations. In contrast, an unoptimized version below uses 4 full storage slots storing the same information:

non-optimized version
non-optimized version

Delete unused variables

When a variable is no longer needed, using the delete keyword is more gas-efficient than manually resetting it to its default value.

Why is it more efficient ? The reason is in how the Ethereum Virtual Machine (EVM) handles storage operations.

The delete keyword is optimized to clear storage slots, potentially triggering refunds for unused storage vs assigning a value, even if it's the default value, is treated as a regular SSTORE operation, which costs gas.

For example in a function below the delete operation resets x to its default value of 0. This operation is gas-efficient:

optimized version
optimized version

The operation below is not gas-efficient because it explicitly setting x = 0:

non-optimized version
non-optimized version

Not shrinking variables unless you pack them together

You might think that using smaller variable types like uint8 instead of the default uint (which is uint256) leads to gas savings. But this is not true. Using the default uint is generally more gas-efficient for standalone variables.

This is because Solidity needs to perform type conversion when working with smaller types, which needs additional gas costs.

For example, uint x = 10; is more efficient than uint8 x = 10;

The EVM operates on 256-bit words, so using smaller types requires extra operations. The exception is when you have a bunch of smaller values and you can pack them. So use the default uint for independent variables unless you're specifically packing multiple variables together in storage.


Use events instead of storing data on chain

Another trick is to use events instead of storing data directly on the blockchain.

When you emit an event, you're essentially logging information that can be accessed off-chain, rather than storing it in the contract's state. This approach is cheaper. Writing to storage is one of the most expensive operations.

For example, a contract below is writing to storage. It’ll cost more gas to deploy and execute updateValue() :

non-optimized version
non-optimized version

In contrast, a contract below uses events instead of storage. updateValue() function emits this event instead of storing the value. It will cost less gas to deploy and execute updateValue() :

optimized version
optimized version

Use libraries

Using libraries is particularly valuable in larger projects with multiple smart contracts.

When several contracts share similar functionality, replicating the same code across each contract leads to unnecessary deployment costs.

It also breaks DRY (Don't Repeat Yourself) principle. What you can do to reduce gas costs is to extract shared functionality into a library that can be accessed by all contracts.

The library approach is more gas-efficient because the library code is deployed only once and then reused by multiple contracts.

For example, below is an unoptimized version where doMath() function is implemented in 3 separate contracts:

non-optimized version
non-optimized version

What you can do to make it optimized is to create a separate library contract that can be accessed by all contracts:

optimized version
optimized version

Since deployment costs are directly correlated with contract size, reducing duplicate code through libraries will lead to gas savings and it is also a great way for creating modular and maintainable contracts.


Use short-circuiting rules

This trick takes advantage of how logical operators (&& and ||) evaluate conditions.

With the && (AND) operator, if the left side evaluates to false, the right side won't be executed at all because the overall result must be false.

Similarly, with the || (OR) operator, if the left side is true, the right side won't be executed because the result must be true.

Let’s look at the example below.

check1() is more likely to return true and is potentially simpler to compute than check2(). The testOptimized() function is more gas efficient because it puts check1() first in the logical AND operation. If check1() returns false, the contract will skip executing check2() entirely, saving gas.

So as a best practice, you should order your conditions by placing cheaper, more likely-to-short-circuit conditions first, followed by more expensive or less likely conditions.


Avoid unnecessary variable assignments

In Solidity, when state variables are declared without an explicit assignment, they are automatically initialized to their default values (0 for uint, false for bool, address(0) for address, etc.).

The contract below is more gas-efficient because it leverages Solidity's default initialization behavior:

optimized version
optimized version

While the contract below unnecessarily spends gas to explicitly write the same default values to storage:

non-optimized version
non-optimized version

So it is cheaper to just leave an assignment blank instead of assigning it to zero.


Avoid doing operations on storage

Minimizing direct operations on storage variables is another trick for optimizing gas usage. Storage operations are one of the most expensive operations in terms of gas costs, so it's important to minimize them whenever possible.

Below there is the inefficient approach. Each iteration of the loop performs a storage operation, which is expensive in terms of gas:

non-optimized version
non-optimized version

Below is the optimized version.

It caches the storage variable in memory using a local variable (_count), performs all operations on this memory variable, and only then updates the storage variable once at the end. This pattern (called storage caching) replaces multiple storage operations with a single one. to reduce gas costs.

optimized version
optimized version

Use fixed size arrays

When possible, use fixed-size arrays instead of dynamic arrays because they require less gas for storage operations. For example, if you know an array will always contain exactly 10 elements, using uint[10] is more gas-efficient than uint[].


Use mapping instead of arrays when possible

Mappings are generally more gas-efficient than arrays when you need to store and access key-value pairs, as they don't require iteration and have constant-time lookup.

For example, consider a simple user balance tracking system. Instead of saving userIds and balances in arrays like below:

non-optimized version
non-optimized version

Save them in a mapping data structure:

optimized version
optimized version

Use bytes type instead of string type

Using bytes instead of string is more gas-efficient when dealing with raw data, as string requires additional encoding and decoding operations.


Use external modifier instead of public modifier if you can

When you write functions in your smart contract, you can mark them as either public or external.

A public function can be called by anyone: other contracts AND functions inside your contract. An external function can only be called by other contracts or users, but NOT by functions inside your contract.

When you know the function will only be called from outside your contract use external instead of public because it's cheaper.


Subscribe to Diana
Receive the latest updates directly to your inbox.
Nft graphic
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from Diana

Skeleton

Skeleton

Skeleton