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.

Now it's your turn :)!

8 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!

3 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.
So, instead of:

uint256 bar; 

function foo(uint256 someNum) external {
    someMapping[bar] = someNum;
    someArray[bar] = someNum;
}

you'd be better off with:

uint256 bar; 

function foo(uint256 someNum) external {
    uint256 tempBar = bar; // tempBar is in memory and cheaper to read from
    someMapping[tempBar] = someNum;
    someArray[tempBar] = someNum;
}
5 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();
2 Likes