Uninitialized variables are assigned with the types default value.
Explicitly initializing a variable with it's default value costs unnecessary gas.
🤦 Bad:
uint256 x = 0;
bool y = false;
🚀 Good:
uint256 x;
bool y;
Caching the array length outside a loop saves reading it on each iteration, as long as the array's length is not changed during the loop.
🤦 Bad:
for (uint256 i = 0; i < array.length; i++) {
// invariant: array's length is not changed
}
🚀 Good:
uint256 len = array.length
for (uint256 i = 0; i < len; i++) {
// invariant: array's length is not changed
}
When dealing with unsigned integer types, comparisons with != 0
are cheaper
than with > 0
.
🤦 Bad:
// `a` being of type unsigned integer
require(a > 0, "!a > 0");
🚀 Good:
// `a` being of type unsigned integer
require(a != 0, "!a > 0");
TODO
Removing unused variables saves gas, especially for state variables, i.e. variables saved in storage.
Making variables constant/immutable, if possible, saves gas as all variables get replaced by the values assigned to them.
⚡️ Only valid for solidity versions <0.6.12
⚡️
Access roles marked as constant
results in computing the keccak256
operation
each time the variable is used because assigned operations for constant
variables are re-evaluated every time.
Changing the variables to immutable
results in computing the hash only once
on deployment, leading to gas savings.
🤦 Bad:
bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
🚀 Good:
bytes32 public immutable GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.
If the contract(s) in scope allow using Solidity >=0.8.4
, consider using
Custom Errors as
they are more gas efficient while allowing developers to describe the error
in detail using NatSpec.
🤦 Bad:
require(condition, "UniswapV3: The reentrancy guard. A transaction cannot re-enter the pool mid-swap");
🚀 Good (with shorter string):
// TODO: Provide link to a reference of error codes
require(condition, "LOK");
🚀 Good (with custom errors):
/// @notice A transaction cannot re-enter the pool mid-swap.
error NoReentrancy();
// ...
if (!condition) {
revert NoReentrancy();
}
A division/multiplication by any number x
being a power of 2 can be
calculated by shifting log2(x)
to the right/left.
While the DIV
opcode uses 5 gas, the SHR
opcode only uses 3 gas.
Furthermore, Solidity's division operation also includes a division-by-0
prevention which is bypassed using shifting.
🤦 Bad:
uint256 b = a / 2;
uint256 c = a / 4;
uint256 d = a * 8;
🚀 Good:
uint256 b = a >> 1;
uint256 c = a >> 2;
uint256 d = a << 3;
⚡️ Only valid for solidity versions <0.6.9
⚡️
The restriction that public
functions can not take calldata
arguments was
lifted in version 0.6.9
.
For solidity versions <0.6.9
, public
functions had to copy the arguments
to memory.
⚡️ Community sentiment suggests to not accept this optimization due to security risks ⚡️
Functions marked as payable
are slightly cheaper than non-payable
ones,
because the Solidity compiler inserts a check into non-payable
functions
requiring msg.value
to be zero.
However, keep in mind that this optimization opens the door for a whole set of security considerations involving Ether held in contracts.
A lot of times there is no risk that the loop counter can overflow.
Using Solidity's unchecked
block saves the overflow checks.
🤦 Bad:
uint len = supportedTokens.length;
for (uint i; i < len; i++) {
// ...
}
🚀 Good:
uint len = supportedTokens.length;
for (uint i; i < len; ) {
// ...
unchecked { i++; }
}
The difference between the prefix increment and postfix increment expression lies in the return value of the expression.
The prefix increment expression (++i
) returns the updated value after it's
incremented. The postfix increment expression (i++
) returns the original
value.
The prefix increment expression is cheaper in terms of gas.
Consider using the prefix increment expression whenever the return value is not needed.
Note to be careful using this optimization whenever the expression's return
value is used afterwards, e.g. uint a = i++
and uint a = ++i
result in
different values for a
.
🤦 Bad:
for (uint i; i < len; i++) {
if (i % 2 == 0) {
counter++;
}
// ...
}
🚀 Good:
for (uint i; i < len; ++i) {
if (i % 2 == 0) {
++counter;
}
// ...
}