Optimizing gas usage of your ethereum smart contracts — the ultimate guide
By: Vlad Kostanda
Tags: Distributed Ledgers, Smart Contracts, Gas In Ethereum, Blockchain, Ethereum

Writing to Storage and Memory.

According to appendices G and H of Ethereum Yellow Paper: a formal specification of Ethereum, a programmable blockchain, the most expensive location where you can save your data is the persistent area called Storage. Saving one slot that is a word of 256 bits to Storage (SSTORE) is 20,000 gas when you initially set it from zero to non-zero. And 5,000 gas is spent when an already used Storage slot is rewritten. Reading a Storage slot using SLOAD takes 200 gas.

Compared to Storage, there is a less expensive non-persistent area called Memory where the cost of storing data is a polynomial function of the data size, linear for small amounts, and quadratic for larger. It’s significantly less for small variables compared to Storage.

Knowing this, there are several recommendations to follow:

Use writing and reading Storage variables carefully. In most cases doing preliminary calculations in local variables and storing the final value to Storage is more cost-effective than keeping updating constantly a Storage variable

Try adjusting the location of your variables playing with the keywords “storage” and “memory”. Depending on the size and number of copying operations between Storage and Memory, switching to memory may give or give not any improvements. All this because of varying memory costs. So it’s not that obvious, and every case has to be considered individually.

Pack several blocks of information into one Storage slot if they are smaller than a word of 32 bytes. This can give significant savings. Specifically if according to the application logic, they are usually updated and accessed together. For example, a structure of 2 uint128 can be stored in one slot in a mapping instead of storing them separately. This can save up to twice of gas.

Free Storage slots by zeroing corresponding variables as soon as you don’t need them anymore. This will refund 15000 of gas. The fact gas is refunded is even used in a project that aims to “tokenize” the gas by occupying slots at moments of relatively low gas price and freeing the slots when gas price is high. This gas difference can be then consumed by any smart contract decreasing the total costs of a call.

Structure packing

Additionally to combining several individual variables in a structure, so they take one 256 bits word, it’s possible to optimize gas consumption by ordering struct members and Storage variables, so they fit wording multiplicity and are packed tightly. This can significantly improve writing and reading of the structures.

Exceptions handling

Use “require” for all runtime conditions validations that can’t be prevalidated on the compile time. And “assert” should be used only for static validations that normally never fail in a properly functioning code. A failing “assert” consumes all the gas available to the call, while “require” doesn’t consume any.

Calling a contract from another one

Such operations as calling one contract or library from another are conducted with gas overhead. And it’s not just costs of updating context, copying data and direct costs of CALL, DELEGATECALL, and similar opcodes. In our observations of asm instructions execution, we could see that on every external contract invocation, a heavy opcode is used, EXTCODESIZE, with a gas cost of 700. So every contract call you make will cost at least this value. Originally the fee for this instruction was significantly smaller, just 20, but after a spam attack was carried against ethereum network that resulted in notable blocks generation slowdown, this instruction price was rebalanced to 700. It’s indeed quite consuming computationally and calculates the size of the code associated with an address. In the case of an external contract method call, it’s simply used to validate there’s indeed a contract at the specified address.

When the EXTCODESIZE repricing was approved, in our opinion, it wasn’t considered carefully enough that the opcode is used that frequently.

The only option we see to optimize the code in this regard is minimizing the number of calls to other contracts and libraries.

For example, we observed an architecture with a universal storage contract responsible for all persistent data of many other contracts. This was used for upgradeability. And every read and write operation was associated with this storage contract call, creating huge overhead. The only recommendation for that case was packing together multiple read or write requests into case-specific methods of the storage contract, so they are either read or updated all at the same time, and less storage contract calls are performed. In the long term, such architecture should be updated to a proxy pattern with a data-storage contract delegating calls to implementation contracts.

Short-circuiting

According to short-circuiting rules, it’s reasonable to set the order of operands in logic expressions, logic OR and logic AND, in a way that computationally heavy operations aren’t performed if the final result of the expression is already known. This means:

Low-cost operand with a high probability of “true” should go first in “or” expression (||)

Low-cost operand with a high probability of “false” should go first in “and” expression (&&)

Depending on call frequency and other parameters, this small detail can save a lot.

Loops

Expensive operations in a loop, independent loops that can be combined into one, and some other loops-related cases that can be identified on the algorithmic level in your code can be optimized to save some gas and make the life of the entire ethereum community easier.

Methods and modifiers

Public / External

If you are sure you won’t call a method of your contract from its other methods, but only externally, it’s good to make such a method external, not public. This is because a public method that will also allow calls from the same contract, has to copy parameters to memory before performing a jump to the method instructions. While an external method doesn’t expect such calls and is optimized by direct reading of parameters from calldata.

An external method f() can still be called from the same contract through a message call as this.f(), but this is unreasonable as will use extra gas.

Internal function calls

Calling functions internally is optimizing the execution because this doesn’t need memory clearing and is just a jump to the method code in the same flow.

Methods Naming

Public and external methods that are exported from a contract are stored in a virtual methods table where they are searched by their ID. The further a method is on the list, the more gas is spent to iterate and find it. Every step is 22 gas. Not that much but still worth considering if you expect your method will be called very often.

A method ID is constructed using hash function keccack256 of the method signature, and the first 4 bytes are taken. Knowing this, it’s possible to rename your exported methods so they are sorted in the table the way you want.

This online tool can be used to rename exported methods and order them as needed.

Events

According to Appendix G of the Yellow Paper, emitting events is gas-consuming as they generate LOG instructions that take gas depending on data size and amount of topics. So emit events only where really needed to simplify interaction with offchain parts of the software, not for debugging or logging purpose.

Using fixed-size bytes arrays

For arrays of bytes and strings that are shorter than 32, it’s recommended to use fixed-size byte arrays, bytes1 to bytes32 instead of arbitrary-length arrays bytes[] and string.

Input size of a method call

If your function has a lot of parameters and their effective size is much smaller than 256 bits used for every parameter, it’s possible to pack those parameters and extract later with bit masks. According to appendix G of Ethereum Yellow Paper: a formal specification of Ethereum, a programmable blockchain, with 68 gas spent for every nonzero byte and 4 gas for zero bytes, this non-standard technique can be used if efficiency is critical. However, operations to extract the values are also adding instructions, so these experiments need profiling in every specific case.

Hash functions

Appendices E and G of the above yellow paper say Ethereum supports 3 hash functions:

keccak256 that defaults to SHA3

sha256

ripemd160

Keccak256 is available as native instructions and is the cheapest hash function and the recommended one. While 2 others are implemented through precompiled contracts and are more expensive. More specifically:

keccak256: 30 gas + 6 gas for each word of input data

sha256: 60 gas + 12 gas for each word of input data

ripemd160: 600 gas + 120 gas for each word of input data

So if you don’t have specific reasons to select another hash function, just use keccak256

Removing useless code

A general recommendation is to remove all the variables and code that won’t be used, either unreachable or not having business value. This will not just save the costs of contract deployment but will also optimize future execution. Splitting a contract that combines independent entities into several contracts can help both with gas and architecture.

Compilation optimization

Use optimization and set the counter to high values or leave the default 200. Setting it to 1 can be useful in a rare case when it’s important to optimize contract deployment, but not subsequent methods call.

Readonly methods vs. transactions

You are not paying for constant functions that aren’t transactions. But this doesn’t mean they aren’t consuming gas, they do. Just it’s free when executed on the local EVM. However, if a constant function is called in a transaction, all the gas matters. So keep an eye on the constant methods too.

Offchain transactions

An alternative approach to save your users from paying transactions fee is to collect their offchain signed transactions and publish those by the backend. This can be very useful in different multisig defi scenarios and custodian solutions. In this case, the missing signatures can be collected from all involved parties offchain, and only the final transaction is published.

Of course, we have to verify signatures onchain using ecrecover. Its costs are mentioned in the yellow paper as 3000 gas.

In some other cases, it may be helpful to do the inverse — the backend is applying its signature to assure the data isn’t manipulated later, but it’s the user who is publishing the tx and paying for gas.