Skip to content

Instantly share code, notes, and snippets.

@hrkrshnn
Last active April 13, 2026 08:41
Show Gist options
  • Select an option

  • Save hrkrshnn/ee8fabd532058307229d65dcd5836ddc to your computer and use it in GitHub Desktop.

Select an option

Save hrkrshnn/ee8fabd532058307229d65dcd5836ddc to your computer and use it in GitHub Desktop.
Some generic writeup about common gas optimizations, etc.
** Upgrade to at least 0.8.4
Using newer compiler versions and the optimizer gives gas
optimizations and additional safety checks for free!
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).
- [[https://blog.soliditylang.org/2021/03/02/saving-gas-with-simple-inliner/][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.
- [[https://blog.soliditylang.org/2021/03/23/solidity-0.8.3-release-announcement/#optimizer-improvements][Optimizer improvements in packed structs]]: Before =0.8.3=, storing
packed structs, in some cases used an additional storage read
operation. After [[https://eips.ethereum.org/EIPS/eip-2929][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 additional cost
of =100= gas alongside the same unnecessary stack operations and
extra deploy time costs.
- [[https://blog.soliditylang.org/2021/04/21/custom-errors][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 by
custom errors.
** Caching the length in for loops
Consider a generic example of an array =arr= and the following loop:
#+begin_src solidity
for (uint i = 0; i < arr.length; i++) {
// do something that doesn't change arr.length
}
#+end_src
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 ([[https://eips.ethereum.org/EIPS/eip-2929][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):
#+begin_src solidity
uint length = arr.length;
for (uint i = 0; i < length; i++) {
// do something that doesn't change arr.length
}
#+end_src
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. [[https://forum.soliditylang.org/t/solidity-team-ama-2-on-wed-10th-of-march-2021/152/15?u=hrkrshnn][Reference]]. Also see [[https://gist.github.com/hrkrshnn/a1165fc31cbbf1fae9f271c73830fdda][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:
#+begin_src solidity :args --ir-optimized --optimize
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];
}
}
}
#+end_src
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:
#+begin_src solidity :args --ir-optimized --optimize
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];
}
}
}
#+end_src
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 carries 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 reduces the deploy 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
[[https://blog.soliditylang.org/2020/04/06/solidity-0.6.5-release-announcement/][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:
#+begin_src solidity
contract C {
/// The owner is set during contruction time, and never changed afterwards.
address public owner = msg.sender;
}
#+end_src
In the above example, each call to the function =owner()= reads from
storage, using a =sload=. After [[https://eips.ethereum.org/EIPS/eip-2929][EIP-2929]], this costs 2100 gas cold
or 100 gas warm. However, the following snippet is more gas
efficient:
#+begin_src solidity
contract C {
/// The owner is set during contruction time, and never changed afterwards.
address public immutable owner = msg.sender;
}
#+end_src
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:
#+begin_src solidity
// condition is boolean
// str is a string
require(condition, str)
#+end_src
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 saves deploy 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 a =shl(..., ...)= equivalent by the Solidity compiler.
Going back, each 32 byte chunk of the string requires an extra
=mstore=. That is, 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 [[https://blog.soliditylang.org/2021/04/21/custom-errors][Custom errors]]. This is more gas
efficient, while allowing the developer to describe the errors in
detail using [[https://docs.soliditylang.org/en/latest/natspec-format.html][NatSpec]]. A disadvantage to this approach is that, some
tooling may not have proper support for this.
** The increment in for loop post condition can be made unchecked
(This is only relevant if you are using the default solidity checked
arithmetic.)
Consider the following generic for loop:
#+begin_src solidity
for (uint i = 0; i < length; i++) {
// do something that doesn't change the value of i
}
#+end_src
In this example, the for loop post condition, 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:
#+begin_src solidity
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;
}
}
#+end_src
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!
** Consider using custom errors instead of revert strings
Solidity 0.8.4 introduced [[https://blog.soliditylang.org/2021/04/21/custom-errors/][custom errors]]. They are more gas efficient
than revert strings, when it comes to deploy cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for
gas savings.
@dpmabo

dpmabo commented Feb 23, 2022

Copy link
Copy Markdown

Great writing. These tips really help.
Thank you.

@0xsomnus

0xsomnus commented Mar 8, 2022

Copy link
Copy Markdown

Sleepy was here.

@JohnChangUK

Copy link
Copy Markdown

Great write up, appreciate this!

@nizhunt

nizhunt commented Apr 10, 2022

Copy link
Copy Markdown

Thanks for the tips!

@lakejynch

Copy link
Copy Markdown

#hari4president

@z0r0z

z0r0z commented May 3, 2022

Copy link
Copy Markdown

good stuffs

@misirov

misirov commented May 29, 2022 •

Copy link
Copy Markdown

Why

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;
    }
}

and not?

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

An extra function would take space in the overall contract code + an extra function call? What is the benefit of calling a func to return an unchecked operation?

immutable variables are replaced at deployment time, would it be the same or better to use constant which are replaced during compilation? (or does the difference even matter?)

address public immutable owner = msg.sender;

@hrkrshnn

Copy link
Copy Markdown
Author

@misirov About the for loop, both are the same.

Regarding constants: yeah, they should be the same with the optimizer. A lot of times, the constants are only known at deploy time, for example in a factory contract.

@0xMorph

0xMorph commented Jun 21, 2022

Copy link
Copy Markdown

Good tips, easy to understand as well

@Marklin2289

Copy link
Copy Markdown

love it !!

@Brivan-26

Copy link
Copy Markdown

Great tips!

@Mylifechangefast

Copy link
Copy Markdown

Good Goody

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment