A Collection of Gas Optimisation Tricks

In the Ethereum smart contract world, every single wei counts! Therefore, it could be very valuable to have a specific list/collection of multiple gas optimisation tricks at hand. And that is the ultimate goal of this thread: collecting various gas optimisation tricks, without compromising any security, that can be leveraged by anyone out there who appreciates gas savings.

I take the lead here and provide the first trick: Gas Optimisations for Infinite Allowances

Various projects (e.g. Uniswap, see here using the constant `MaxUint256` from `ethers.js`) set the default value of the user's allowance to `2^256 - 1`. Now the value `2^256 - 1` can also be represented in hex as `0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`. From Ethereum's yellow paper we know that zeros are cheaper than non-zero values in the hex representation. Considering this fact, an alternative choice could be now `0x8000000000000000000000000000000000000000000000000000000000000000` or `2^255` to represent "infinity". If you do the calculations with Remix, you will see that the former costs 47'872 gas, while the latter costs 45'888 gas. If you accept that infinity can also be represented via `2^255` (instead of `2^256-1`) - and I think most projects can live with that - you can already save 1'984 gas (or 4.1%) leveraging this optimisation trick.

14 Likes

One of the main ways of optimizing gas is reducing the number of storage slots that are used in a transaction. Using smaller types than uint256 to pack multiple values into one slot is a good way to achieve that. For example, you don't need 256 bits to represent a timestamp, 64 bits are more than enough. Remember to use SafeCast to be extra sure though!

6 Likes

Pretty basic but essential, extracted from the Solidity documentation:

Compared to regular state variables, the gas costs of constant and immutable variables are much lower. For a constant variable, the expression assigned to it is copied to all the places where it is accessed and also re-evaluated each time. This allows for local optimizations. Immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed. For these values, 32 bytes are reserved, even if they would fit in fewer bytes. Due to this, constant values can sometimes be cheaper than immutable values.

There are also good resources available (I think it is a good idea to have this thread here on OpenZeppelin which is currently the main SC reference, which gathers all the tricks) such as blog posts or even better, research papers investigating this topic. A good and well structured one from Cagliari University is available at:

3 Likes

Also a beginner level one, but it adds up.

If there's a state variable you'll read from more than once in a function, it's best to cast it into memory.

``````uint256 bar;

}
``````

you'd be better off with:

``````uint256 bar;

uint256 tempBar = bar; // tempBar is in memory and cheaper to read from
}
``````
7 Likes

You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor `payable`. The following opcodes are cut out:

• `CALLVALUE`
• `DUP1`
• `ISZERO`
• `PUSH2`
• `JUMPI`
• `PUSH1`
• `DUP1`
• `REVERT`
• `JUMPDEST`
• `POP`

In Solidity, this chunk of assembly would mean the following:

``````if(msg.value != 0) revert();
``````
4 Likes

Harikrishnan Mulackal wrote a great summary about gas optimisations here. I mirror everything here to have everything in one place.

The advantages of versions `0.8.*` over `<0.8.0` are:

• Safemath by default from `0.8.0` (can be more gas efficient than some library-based safemath.)
• Low-level inliner from `0.8.2`, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra `jump` instructions and additional stack operations needed for function calls.
• Optimizer improvements in packed structs: Before `0.8.3`, storing packed structs, in some cases, used additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means an additional cost of `100` gas alongside the same unnecessary stack operations and extra deploy time costs.
• Custom errors from `0.8.4`, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings with custom errors.

Caching the length in for loops

Consider a generic example of an array `arr` and the following loop:

``````for (uint i = 0; i < arr.length; i++) {
// do something that doesn't change arr.length
}
``````

In the above case, the solidity compiler will always read the length of the array during each iteration. That is,

1. if it is a `storage` array, this is an extra `sload` operation (100 additional extra gas (EIP-2929) for each iteration except for the first),
2. if it is a `memory` array, this is an extra `mload` operation (3 additional gas for each iteration except for the first),
3. if it is a `calldata` array, this is an extra `calldataload` operation (3 additional gas for each iteration except for the first)

This extra costs can be avoided by caching the array length (in stack):

``````uint length = arr.length;
for (uint i = 0; i < length; i++) {
// do something that doesn't change arr.length
}
``````

In the above example, the `sload` or `mload` or `calldataload` operation is only called once and subsequently replaced by a cheap `dupN` instruction. Even though `mload` , `calldataload` and `dupN` have the same gas cost, `mload` and `calldataload` needs an additional `dupN` to put the offset in the stack, i.e., an extra 3 gas.

This optimization is especially important if it is a storage array or if it is a lengthy for loop.

Note that the Yul based optimizer (not enabled by default; only relevant if you are using `--experimental-via-ir` or the equivalent in standard JSON) can sometimes do this caching automatically. However, this is likely not the case in your project. Reference. Also see this.

Use `calldata` instead of `memory` for function parameters

In some cases, having function arguments in `calldata` instead of `memory` is more optimal.

Consider the following generic example:

``````contract C {
function add(uint[] memory arr) external returns (uint sum) {
uint length = arr.length;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
}
``````

In the above example, the dynamic array `arr` has the storage location `memory` . When the function gets called externally, the array values are kept in `calldata` and copied to `memory` during ABI decoding (using the opcode `calldataload` and `mstore` ). And during the for loop, `arr[i]` accesses the value in memory using a `mload` . However, for the above example, this is inefficient. Consider the following snippet instead:

``````contract C {
function add(uint[] calldata arr) external returns (uint sum) {
uint length = arr.length;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
}
``````

In the above snippet, instead of going via memory, the value is directly read from `calldata` using `calldataload` . That is, there are no intermediate memory operations that carry this value.

Gas savings : In the former example, the ABI decoding begins with copying value from `calldata` to `memory` in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduce the deployment time cost of the contract.

In short, use `calldata` instead of `memory` if the function argument is only read.

Note that in older Solidity versions, changing some function arguments from `memory` to `calldata` may cause â€śunimplemented feature errorâ€ť. This can be avoided by using a newer ( `0.8.*` ) Solidity compiler.

State variables that can be set to `immutable`

Solidity 0.6.5 introduced `immutable` as a major feature. It allows setting contract-level variables at construction time which gets stored in code rather than storage.

Consider the following generic example:

``````contract C {
/// The owner is set during construction time, and never changed afterwards.
}
``````

In the above example, each call to the function `owner()` reads from storage, using a `sload` . After EIP-2929, this costs 2100 gas cold or 100 gas warm. However, the following snippet is more gas efficient:

``````contract C {
/// The owner is set during construction time, and never changed afterwards.
address public immutable owner = msg.sender;
}
``````

In the above example, each storage read of the `owner` state variable is replaced by the instruction `push32 value` , where `value` is set during contract construction time. Unlike the last example, this costs only 3 gas.

Consider having short revert strings

Consider the following require statement:

``````// condition is boolean
// str is a string
require(condition, str)
``````

The string `str` is split into 32-byte sized chunks and then stored in memory using `mstore` , then the memory offsets are provided to `revert(offset, length)` . For chunks shorter than 32 bytes, and for low `--optimize-runs` value (usually even the default value of 200), instead of `push32 val` , where `val` is the 32-byte hexadecimal representation of the string with `0` padding on the least significant bits, the solidity compiler replaces it by `shl(value, short-value))` . Where `short-value` does not have any `0` padding. This saves the total bytes in the deploy code and therefore saves deploy time cost, at the expense of extra `6` gas during runtime. This means that shorter revert strings save deployment time costs of the contract. Note that this kind of saving is not relevant for high values of `--optimize-runs` as `push32 value` will not be replaced by an `shl(..., ...)` equivalent by the Solidity compiler.

Going back, each 32-byte chunk of the string requires an extra `mstore`. That is an additional cost for `mstore` , memory expansion costs, as well as stack operations. Note that, this runtime cost is only relevant when the revert condition is met.

Overall, shorter revert strings can save deploy time as well as runtime costs.

Note that if your contracts already allow using at least Solidity `0.8.4`, then consider using Custom errors. This is more gas efficient while allowing the developer to describe the errors in detail using NatSpec. A disadvantage to this approach is that some tooling may not have proper support for this.

The increment in for loop postcondition can be made unchecked

(This is only relevant if you are using the default solidity checked arithmetic.)

Consider the following generic for loop:

``````for (uint i = 0; i < length; i++) {
// do something that doesn't change the value of i
}
``````

In this example, the for loop postcondition, i.e., `i++` involves checked arithmetic, which is not required. This is because the value of `i` is always strictly less than `length <= 2**256 - 1`. Therefore, the theoretical maximum value of `i` to enter the for-loop body is `2**256 - 2`. This means that the `i++` in the for loop can never overflow. Regardless, the overflow checks are performed by the compiler.

Unfortunately, the Solidity optimizer is not smart enough to detect this and remove the checks. One can manually do this by:

``````for (uint i = 0; i < length; i = unchecked_inc(i)) {
// do something that doesn't change the value of i
}

function unchecked_inc(uint i) returns (uint) {
unchecked {
return i + 1;
}
}
``````

Note that itâ€™s important that the call to `unchecked_inc` is inlined. This is only possible for solidity versions starting from `0.8.2`.

Gas savings: roughly speaking this can save 30-40 gas per loop iteration. For lengthy loops, this can be significant!

EDIT from pcaversaccio: As an alternative one can also use the following:

``````for (uint256 i = 0; i < length; ) {
// do something that doesn't change the value of i
unchecked {
i++;
}
}
``````

Consider using custom errors instead of revert strings

Solidity 0.8.4 introduced custom errors. They are more gas efficient than revert strings, when it comes to deployment cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for gas savings.

3 Likes

Use `!= 0` instead of `> 0` for unsigned integer comparison

When dealing with unsigned integer types, comparisons with `!= 0` are cheaper then with `> 0` .

2 Likes

Use shift right/left instead of division/multiplication if possible

While the `DIV` / `MUL` opcode uses 5 gas, the `SHR` / `SHL` opcode only uses 3 gas. Furthermore, beware that Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting. Eventually, overflow checks are never performed for shift operations as they are done for arithmetic operations. Instead, the result is always truncated.

1 Like

1. The fallback function (and sig-less functions in Yul) save gas because they don't require a function signature to be called.
2. Negative values are more expensive in calldata because they are prepended with `0xf` bytes. E.g. `abi.encode(int(-256))` leads `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef0d4`. And from Ethereum's yellow paper we know that zeros are cheaper than non-zero values in the hex representation.

To update this with using at least 0.8.6 there is no difference in gas usage with`!= 0` or `> 0`

1 Like

What does this kind of manual packing offer over a packed struct like the one you showed?

In your code you're using unsafe casting, e.g. from `uint` to `uint8` for `bulkMultiplier`. You should somehow encode the assumption that it will always fit in a `uint8`: use a `uint8` variable for that value everywhere, and when needed to convert from a larger integer size use OpenZeppelin's `SafeCast` library.

I checked and effectively proper packed struct works at the same gas cost even lower! Thanks for the advice. It still was a nice exercise

The usage of `!=` over `<` is 3 GAS cheaper (even with Solidity version `0.8.17`). Be careful to apply it only in situations where it doesn't represent a security risk!

``````// SPDX-License-Identifier: WTFPL
pragma solidity 0.8.17;

contract ConditionTesting {
uint256 public counter;

function functionVersion1() external {
/**
* @dev Uses the opcodes `LT` & `ISZERO`; 3 + 3 = 6 GAS.
* @notice `type(uint64).max` is used as a general placeholder for illustration.
*/
if (counter < type(uint64).max) {
...
}
}

function functionVersion2() external {
/**
* @dev Uses the opcode `EQ`; = 3 GAS.
* @notice `type(uint64).max` is used as a general placeholder for illustration.
*/
if (counter != type(uint64).max) {
...
}
}
}
``````

@pcaversaccio Have you tested with `viaIR`? In my experience both operators result in the same final bytecode when using `viaIR`. That said it's still not 100% ready to use.

I quickly tested it with `0.8.17`, `--optimizer-runs 200` and `--via-ir`. My findings are the following:

The gas costs are the same.

`<` Version

`!=` Version

However, there is a difference in the opcodes as it seems:

`<` Version

`!=` Version

I just don't feel like recommending `viaIR` right now due to the recently found bugs that led to Solidity version `0.8.17` ultimately.

Can I ask what IDE/plugins you were using to get the printout in your pics?

No plugin nor IDE, just simply `forge debug` - I'm a simple man and just need my terminal . Similarly, you could use `hevm`, which is however deprecated. See the comments in my gist here on how to use it in an example.

Ah, I've never used Foundry (generally develop with Hardhat). I get a lot of that information generated using different hardhat plugins but the terminal output just looked nice. I'll have to play around with Foundry one of these days.

1 Like