Solidity Gas Optimization Tips taken from various sources (listed in the #credits section below). Make pull requests to contribute gas alpha.
Check if arithmetic operations can be achieved using bitwise operators, if yes, implement the same operations using bitwise operators and compare the gas consumed. Usually bitwise logic will be cheaper (ofc we cannot use bitwise everywhere, there are some limitations)
Note: Bit shift <<
and >>
operators are not among the arithmetic ones, and thus don’t revert on overflow.
Code example:
- uint256 alpha = someVar / 256;
- uint256 beta = someVar % 256;
+ uint256 alpha = someVar >> 8;
+ uint256 beta = someVar & 0xff;
- uint 256 alpha = delta / 2;
- uint 256 beta = delta / 4;
- uint 256 gamma = delta * 8;
+ uint 256 alpha = delta >> 1;
+ uint 256 beta = delta >> 2;
+ uint 256 gamma = delta << 3;
The public
visibility modifier is equivalent to external
plus internal
. In other words, both public
and external
can be called from outside your contract (like MetaMask), but of these two, only public
can be called from other functions inside your contract.
Because public
grants more access than external
(and is costlier than the latter), the general best practice is to prefer external
. Then you can consider switching to public
if you fully understand the security and design implications.
Code Example:
pragma solidity 0.8.10;
contract Test {
string message = "Hello World";
// Execution cost: 24527
function test() public view returns (string memory){
return message;
}
//Execution cost: 24505
function test2() external view returns (string memory){
return message;
}
}
If the variable is not set/initialized, it is assumed to have a default value (0, false, 0x0, etc., depending on the data type). If you explicitly initialize it with its default value, you are just wasting gas.
uint256 foo = 0;//bad, expensive
uint256 bar;//good, cheap
It is considered a best practice to append the error reason string with a require
statement. But these strings take up space in the deployed bytecode. Each reason string needs at least 32 bytes, so make sure your string complies with 32 bytes, otherwise it will become more expensive.
Code Example:
require(balance >= amount, "Insufficient balance");//good
require(balance >= amount, "Too bad, it appears, ser you are broke, bye bye"; //bad
We should avoid unnecessary/redundant checks to save some extra gas.
Code Example:
Bad Code:
require(balance>0, "Insufficient balance");
if (balance>0){ // this check is redundant
. . . //some action
}
Good Code:
require(balance>0, "Insufficient balance");
. . . //some action
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier-to-read code and better coverage reports.
Code Example:
pragma solidity 0.8.10;
contract NestedIfTest {
//Execution cost: 22334 gas
function funcBad(uint256 input) public pure returns (string memory) {
if (input<10 && input>0 && input!=6){
return "If condition passed";
}
}
//Execution cost: 22294 gas
function funcGood(uint256 input) public pure returns (string memory) {
if (input<10) {
if (input>0){
if (input!=6){
return "If condition passed";
}
}
}
}
}
Using multiple require statements is cheaper than using &&
multiple check combinations. There are more advantages, such as easier-to-read code and better coverage reports.
pragma solidity 0.8.10;
contract MultipleRequire {
// Execution cost: 21723 gas
function bad(uint a) public pure returns(uint256) {
require(a>5 && a>10 && a>15 && a>20);
return a;
}
// Execution cost: 21677 gas
function good(uint a) public pure returns(uint256) {
require(a>5);
require(a>10);
require(a>15);
require(a>20);
return a;
}
}
Set appropriate function visibilities, as internal
functions are cheaper to call than public
functions. There is no need to mark a function as public
if it is only meant to be called internally.
// The following function will only be called internally
- function _func() public {...}
+ function _func() internal {...}
When you call a public
function of the library, the bytecode of the function will not become part of your contract, so you can put complex logic in the library while keeping the contract scale small.
The call to the library is made through a delegate call, which means that the library can access the same data and the same permissions as the contract. This means that it is not worth doing for simple tasks.
Reading and writing local variables is cheap, whereas reading and writing state variables that are stored in contract storage is expensive.
function badCode() external {
for(uint256 i; i < myArray.length; i++) { // state reads
myCounter++; // state reads and writes
}
}
function goodCode() external {
uint256 length = myArray.length; // one state read
uint256 local_mycounter = myCounter; // one state read
for(uint256 i; i < length; i++) { // local reads
local_mycounter++; // local reads and writes
}
myCounter = local_mycounter; // one state write
}
A common gas optimization is “packing structs” or “packing storage slots”. This is the action of using smaller types like uint128 and uint96 next to each other in contract storage. When values are read or written in contract storage a full 256 bits are read or written. So if you can pack multiple variables within one 256-bit storage slot then you are cutting the cost to read or write those storage variables in half or more.
// Unoptimized
struct MyStruct {
uint256 myTime;
address myAddress;
}
//Optimized
struct MyStruct {
uint96 myTime;
address myAddress;
}
In the above a myTime
and myAddress
state variables take up 256
bits so both values can be read or written in a single state read or write.
Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.
It is better to use uint256
and bytes32
than using uint8 for example. While it seems like uint8
will consume less gas than uint256 it is not true, since the Ethereum virtual Machine(EVM) will still occupy 256
bits, fill 8 bits with the uint variable and fill the extra bites with zeros.
Code Example:
pragma solidity ^0.8.1;
contract SaveGas {
uint8 resulta = 0;
uint resultb = 0;
// Execution cost: 73446 gas
function UseUint() external returns (uint) {
uint selectedRange = 50;
for (uint i=0; i < selectedRange; i++) {
resultb += 1;
}
return resultb;
}
// Execution cost: 84175 gas
function UseUInt8() external returns (uint8){
uint8 selectedRange = 50;
for (uint8 i=0; i < selectedRange; i++) {
resulta += 1;
}
return resulta;
}
}
Short-circuiting is a strategy we can make use of when an operation makes use of either ||
or &&
. This pattern works by ordering the lower-cost operation first so that the higher-cost operation may be skipped (short-circuited) if the first operation evaluates to true.
// f(x) is low cost
// g(y) is expensive
// Ordering should go as follows
f(x) || g(y)
f(x) && g(y)
Libraries are often only imported for a small number of uses, meaning that they can contain a significant amount of code that is redundant to your contract. If you can safely and effectively implement the functionality imported from a library within your contract, it is optimal to do so.
External calls are expensive, therefore, fewer external gas == fewer gas cost.
Code Example:
// Unoptimized Code
for(uint i=0; i<n; i++){
. . . //some stuff here
}
//Optimized Code
for(uint i; i<n;){
. . . //some stuff here
unchecked { ++i; }
}
Arithmetic computations cost gas, so it is recommended to avoid repeated computations.
Code Example:
// Unoptimized code:
for (uint i=0;i<length;i++) {
tokens[i] += limit * price;
}
// Optimized code:
uint local = limit * price;
for (uint i=0;i<length;i++) {
tokens[i] += local;
}
There is no point of leaving dead lines in code. Those lines are never going to execute, but they will take place in bytecode, it is better to get rid of them.
Code Example:
function deadCode(uint x) public pure {
if(x <1) {
if(x> 2) {
return x;
}
}
}
Since there's no garbage collection, you have to throw away unused data yourself.
Code Example:
//Using delete keyword
delete myVariable;
//Or assigning the value 0 if integer
myInt = 0;
One line to swap two variables without writing a function or temporary variable that needs more gas.
Code Example:
(hello, world) = (world, hello);
Choosing the perfect Data location is essential. You must know these:
-
storage - variable is stored on the blockchain. It's a persistent state variable. Costs Gas to define it and change it.
-
memory - temporary variable declared inside a function. No gas for declaring. But costs gas for changing memory variables (less than storage)
-
calldata - like memory but non-modifiable and only available as an argument of external functions
Also, it is important to note that, If not specified data location, then it storage by default.
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];
}
}
}
// Unoptimized code:
contract C {
/// The owner is set during construction time and never changed afterward.
address public owner = msg.sender;
}
// Optimized code:
contract C {
/// The owner is set during construction time and never changed afterward.
address public immutable owner = msg.sender;
}
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();
Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks for free.
When dealing with unsigned integer types, comparisons with != 0
are cheaper than with > 0
.
The code of modifiers is inlined inside the modified function, thus adding up size and costing gas. Limit the modifiers. Internal functions are not inlined but are called as separate functions. They are slightly more expensive at run time, but save a lot of redundant bytecode in deployment, if used more than once.
In Solidity, Boolean variables are stored as uint8 (unsigned integer of 8 bits). However, only 1 bit would be enough to store them. If you need up to 32 Booleans together, you can just follow the Packing Variables pattern. If you need more, you will use more slots than actually need.
Pack Booleans in a single uint256 variable. To this purpose, create functions that pack and unpack the Booleans into and from a single variable. The cost of running these functions is cheaper than the cost of extra Storage.
Solidity provides only two data types to represent a list of data: arrays and maps. Mappings are cheaper, while arrays are packable and iterable.
In order to save gas, it is recommended to use mappings to manage lists of data, unless there is a need to iterate or it is possible to pack data types. This is useful both for Storage and Memory. You can manage an ordered list with a mapping using an integer index as a key.
Whenever it is possible to set an upper bound on the size of an array, use a fixed-size array instead of a dynamic one.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Code Example:
// Revert Strings
contract C {
address payable owner;
function withdraw() public {
require(msg.sender == owner, "Unauthorized");
//..
}
}
//Custom errors
error Unauthorized();
contract C {
address payable owner;
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
//..
}
}
- https://eip2535diamonds.substack.com/p/smart-contract-gas-optimization-with
- https://blog.birost.com/a?ID=00950-e4e25dc9-573d-4332-8262-41131961734f
- https://marduc812.com/2021/04/08/how-to-save-gas-in-your-ethereum-smart-contracts/
- https://betterprogramming.pub/how-to-write-smart-contracts-that-optimize-gas-spent-on-ethereum-30b5e9c5db85
- https://mudit.blog/solidity-gas-optimization-tips/
- http://www.cs.toronto.edu/~fanl/papers/gas-brain21.pdf
- https://gist.github.com/tqmvt/dc7059357bbe8e1a75cf17795ff21e46