9 ways to optimise your gas usage in Solidity

Why gas optimisation matters for smart contract development and how to approach it.

brainbot
6 min readFeb 10, 2023
Gas station
Photo by Juan Carballo Diaz on Unsplash

Gas optimisation matters

Optimising the gas usage of smart contracts is a substantial aspect when it comes to developing successful decentralised applications. This has mainly financial reasons since every unit of gas comes with a (dynamic) price and even though the gas price on Ethereum is bearable at the moment (~35 gwei at the time of writing), it is very volatile and can change rapidly based on market conditions. In the 2021 bull run, users had to pay up to 20$ for simple transactions when gas prices moved around 200 gwei and more.

High gas costs affect both sides of your project’s balance sheet: First, they ruin user experience and potentially scare away people from using your application or even cryptocurrencies at all, reducing revenue. Second, depending on how much gas executing your smart contracts requires, your costs will surge unless you increase fees, which will again affect your revenue.

To make it short, gas optimisation matters. For that very reason, we do not only look at potential vulnerabilities when we conduct code audits for our clients, but also provide recommendations on how to reduce gas spending. Through those audits as well as our own development efforts for various projects, we have piled up significant experience and tips on how to optimise gas usage, of which we will share some in this post for you to inherit.

You can learn more about the smart contract code audits or the development support we provide to our clients here:

A brief introduction to gas

If you are asking yourself what gas is, this section is meant to give you a brief overview. If you are familiar with the concept of gas, you can skip to the next chapter.

Executing code on the blockchain requires computational efforts, for which you have to pay a fee. On Ethereum (and most other blockchains) this fee is called gas. The amount of gas you need to pay depends on the computational resources required for executing the smart contract. The price of gas then depends on the current demand in the network and the value of Ether, Ethereum’s native token.

While one cannot influence the price of Ether or the demand on the network, reducing the need for computational resources is a common approach. Gas optimisation thus aims to reduce the cost of executing smart contract code by streamlining the code according to the way it is processed on-chain.

If you want to get a more in-depth understanding, we recommend to stop by the explainer page on ethereum.org, which additionally provides a list of further readings.

9 ways to optimise gas usage

This is probably the part you’re here for. Each way will introduced briefly, followed by an example. Let’s jump in!

1. Use small variables for storage

Storage is tightly packed, meaning two variables following each other that can fit in a word (256 bits in the EVM) will be made into a single storage slot. As such if you use variables smaller than 256 bits, you may be able to declare them in a way to save storage slots. This can be especially relevant when defining structs that are going to be used multiple times.

contract Storage {
uint256 a;
uint256 b;
function inefficient(uint256 _a, uint256 _b) external {
a = _a;
b = _b;
}
uint128 c;
uint128 d;
function efficient(uint128 _c, uint128 _d) external {
c = _c;
d = _d;
}
}

Tightly pack your structs:

struct fourWords {
uint256 a;
uint128 b;
uint256 c;
uint128 d;
}
struct threeWords {
uint256 a;
uint256 c;
uint128 b;
uint128 d;
}

2. Don’t use too small variables for storage

The problem with using smaller variable for storage is that when the variables are loaded and stored, they need to be rescaled to and from 256 bits EVM words that the EVM manipulates. This costs gas on every load / store.

The tradeoff is between saving storage costs or computation costs. Variables that are frequently read and stored should be written in a full word when possible.

OpenZeppelin uses uint(1) and uint(2) instead of a true/false boolean for a frequently used storage variable. As documented, they avoid using 0 to avoid the gas refund that usually comes when bringing a storage variable from non-zero to zero. As the gas refunded in a transaction is limited by a fraction of the total transaction gas, this potentially saves gas.

contract Storage2 {
uint128 a = 1;
uint256 b = 2;
function inefficient(uint128 _a) external {
a += _a;
}
function efficient(uint256 _b) external {
b += _b;
}
}

3. Reading storage multiple times

Accessing storage costs 2100 gas for the first read and 100 gas per additional read (cold SLOAD / warm storage read). If you need to access the same storage variable multiple times, you can put it in memory and read it from there instead. This should cost 3 gas (MSTORE) and 3 additional gas per read (MLOAD). It is particularly remarkable when using the length of a storage array in a for loop

contract Gas {

uint256[] arrayOfNumbers = [1, 2, 3, 4, 5];
function forLoop() external {
// costs 2100 gas + 100 gas per iteration
for(uint256 i = 0; i < arrayOfNumbers.length; i++) {
}
}
function forLoopEfficient() external {
// costs 3 gas + 3 gas per iteration
uint256 arrayLength = arrayOfNumbers.length;
for(uint256 i = 0; i < arrayLength; i++) {
}
}
}

4. Use calldata instead of memory arguments

Similarly to using memory instead of storage when possible, arguments of external functions should be left in calldata instead of memory to reduce gas costs. Defining arguments as memory will require the arguments to be loaded into memory, adding an additional MSTORE as well as an additional MLOAD every time it is read. This is only required if you need to modify the argument of the function inside the function. The gas saved by using calldatainstead of memory is proportional to the size of argument and depends on the usage of memory in the rest of the function (see memory expansion).

function inefficient(uint256[] memory a) external {
}
function efficient(uint256[] calldata a) external {
}

5. Use immutable or constant instead of storage

To save storage costs, you can declare variables that you set only once as immutable or constant instead. An immutable variable is set only in the constructor and does not use storage. This will save 20000 gas on deployment as well as 2100 gas whenever the immutable is read + 100 gas per additional read in the same transaction.

contract Immutable {
uint256 a;
uint256 immutable b;
constructor(uint _b) {
a = _b; // 20k gas
b = _b; // 0 gas
}
function inefficient() external returns (uint) {
return a; // 2100 gas
}
function efficient() external returns (uint) {
return b; // 0 gas
}
}

6. Use custom errors

Instead of reverting with a string, in solidity 0.8.4 and up you can use custom errors to save gas. The custom error will revert with the signature of the selector of the declared error in memory, while a revert with a string will revert with the selector of Error(string)and the ABI-encoded string in memory. Gas is also saved on deployment by not having to save the string.

The following example saves about 252 gas on execution.

contract Error {
error Test();
function inefficient() external {
revert("Test");
}
function efficient() external {
revert Test();
}
}

7. Use ++i instead of i++

It costs 5 less gas to use ++i and --i than i++ and i--. This is especially relevant in for loops.

function inefficient() external {
for (uint256 i = 0; i < 10; i++);
}
function efficient() external {
for (uint256 i = 0; i < 10; ++i);
}
}

8. Don’t compare to boolean literals

It saves about 15 gas to replace if (x == true) with if (x) and if (x == false) with if (!x).

bool constant x = false;
function inefficient() external {
if (x == true) {
revert();
}
}
function efficient() external {
if (x) {
revert();
}
}

9. Make functions payable

Payable functions cost about 21 less gas to be called than non-payable function. This is due to having to check the message value in non-payable function and revert if it is not 0. Contracts with payable functions are also cheaper to deploy.

function inefficient() external {
}
function efficient() external payable {
}

Stay up to date for part 2!

We are planning to publish further articles on gas optimisation in the future. If you liked this post, we recommend to follow our medium profile to stay up to date on any new publications.

About the author

This blog post was created by Côme du Crest, Developer & Auditor at brainbot technologies AG. For questions, please reach out to services@brainbot.com and mention the keyword „gas optimisation“.

--

--

brainbot

Creating the core building blocks that enable broad adoption of public blockchains with a focus on Ethereum.