Uninitialized uint variables are assigned with a default value of 0.
Thus, in for-loops, explicitly initializing an index with 0 costs unnecesary gas. For example, the following code:
for (uint256 i = 0; i < length; ++i) {
can be changed to:
for (uint256 i; i < length; ++i) {
Consider declaring the following lines without explicitly setting the index to 0:
contracts/AxelarGateway.sol:
207: for (uint256 i = 0; i < symbols.length; i++) {
contracts/auth/AxelarAuthWeighted.sol:
69: for (uint256 i = 0; i < weightsLength; ++i) {
98: for (uint256 i = 0; i < signatures.length; ++i) {
For-Loops: Cache array length outside of loops
Reading an array's length at each iteration has the following gas overheads:
storage arrays incur a Gwarmaccess (100 gas)
memory arrays use mload (3 gas)
calldata arrays use calldataload (3 gas)
Caching the length changes each of these to a DUP<N> (3 gas), and gets rid of the extra DUP<N> needed to store the stack offset. This would save around 3 gas per iteration.
For example:
for (uint256 i; i < arr.length; ++i) {}
can be changed to:
uint256 len = arr.length;
for (uint256 i; i < len; ++i) {}
Consider making the following change to these lines:
contracts/AxelarGateway.sol:
207: for (uint256 i = 0; i < symbols.length; i++) {
contracts/deposit-service/AxelarDepositService.sol:
114: for (uint256 i; i < refundTokens.length; i++) {
168: for (uint256 i; i < refundTokens.length; i++) {
204: for (uint256 i; i < refundTokens.length; i++) {
contracts/auth/AxelarAuthWeighted.sol:
17: for (uint256 i; i < recentOperators.length; ++i) {
98: for (uint256 i = 0; i < signatures.length; ++i) {
contracts/gas-service/AxelarGasService.sol:
123: for (uint256 i; i < tokens.length; i++) {
For-Loops: Index increments can be left unchecked
From Solidity v0.8 onwards, all arithmetic operations come with implicit overflow and underflow checks.
In for-loops, as it is impossible for the index to overflow, index increments can be left unchecked to save 30-40 gas per loop iteration.
For example, the code below:
for (uint256 i; i < numIterations; ++i) {
// ...
}
can be changed to:
for (uint256 i; i < numIterations;) {
// ...
unchecked { ++i; }
}
Consider making the following change to these lines:
contracts/AxelarGateway.sol:
195: for (uint256 i; i < adminCount; ++i) {
207: for (uint256 i = 0; i < symbols.length; i++) {
292: for (uint256 i; i < commandsLength; ++i) {
contracts/deposit-service/AxelarDepositService.sol:
114: for (uint256 i; i < refundTokens.length; i++) {
168: for (uint256 i; i < refundTokens.length; i++) {
204: for (uint256 i; i < refundTokens.length; i++) {
contracts/auth/AxelarAuthWeighted.sol:
17: for (uint256 i; i < recentOperators.length; ++i) {
69: for (uint256 i = 0; i < weightsLength; ++i) {
98: for (uint256 i = 0; i < signatures.length; ++i) {
101: for (; operatorIndex < operatorsLength && signer != operators[operatorIndex]; ++operatorIndex) {}
116: for (uint256 i; i < accounts.length - 1; ++i) {
contracts/gas-service/AxelarGasService.sol:
123: for (uint256 i; i < tokens.length; i++) {
Arithmetics: ++i costs less gas compared to i++ or i += 1
++i costs less gas compared to i++ or i += 1 for unsigned integers, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.
i++ increments i and returns the initial value of i. Which means:
uint i = 1;
i++; // == 1 but i == 2
But ++i returns the actual incremented value:
uint i = 1;
++i; // == 2 and i == 2 too, so no need for a temporary variable
In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2, thus it costs more gas.
The same logic applies for --i and i--.
Consider using ++i instead of i++ or i += 1 in the following instances:
contracts/AxelarGateway.sol:
207: for (uint256 i = 0; i < symbols.length; i++) {
contracts/deposit-service/AxelarDepositService.sol:
114: for (uint256 i; i < refundTokens.length; i++) {
168: for (uint256 i; i < refundTokens.length; i++) {
204: for (uint256 i; i < refundTokens.length; i++) {
contracts/gas-service/AxelarGasService.sol:
123: for (uint256 i; i < tokens.length; i++) {
Visibility: public functions can be set to external
Calls to external functions are cheaper than public functions. Thus, if a function is not used internally in any contract, it should be set to external to save gas and improve code readability.
Consider changing following functions from public to external:
xc20/contracts/XC20Wrapper.sol:
40: function contractId() public pure returns (bytes32) {
contracts/deposit-service/AxelarDepositService.sol:
241: function contractId() public pure returns (bytes32) {
contracts/deposit-service/DepositBase.sol:
41: function wrappedToken() public view returns (address) {
Unnecessary initialization of variables with default values
Uninitialized variables are assigned with a default value depending on its type:
uint: 0
bool: false
address: address(0)
Thus, explicitly initializing a variable with its default value costs unnecesary gas. For example, the following code:
bool b = false;
address c = address(0);
uint256 a = 0;
can be changed to:
uint256 a;
bool b;
address c;
Consider declaring the following lines without explicitly setting a value:
Use calldata instead of memory for read-only arguments in external functions
When an external function with a memory array is called, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length).
Using calldata directly helps to save gas as values are read directly from calldata using calldataload, thus removing the need for such a loop in the contract code during runtime execution.
Also, structs have the same overhead as an array of length one.
Consider changing the following from memory to calldata:
When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
However, this does not apply to storage values as using reduced-size types might be beneficial to pack multiple elements into a single storage slot. Thus, where appropriate, use uint256/int256 and downcast when needed.
Consider using uint256/int256 instead of bool for the following:
keccak256() should only need to be called on a specific string literal once
The result of keccak256() should be saved to an immutable variable, and the variable used instead. If the hash is being used as a part of a function selector, the cast to bytes4 should also only be done once.
Instances of keccak256() that can be saved to an immutable variable:
++i
costs less gas compared toi++
ori += 1
public
functions can be set toexternal
calldata
instead ofmemory
for read-only arguments in external functionsuints
/ints
smaller than 32 bytes (256 bits) incurs overheadabi.encode()
is less efficient thanabi.encodePacked()
internal
functions only called once can be inlined to save gaskeccak256()
should only need to be called on a specific string literal onceGas Report
For-loops: Index initialized with default value
Uninitialized
uint
variables are assigned with a default value of0
.Thus, in for-loops, explicitly initializing an index with
0
costs unnecesary gas. For example, the following code:can be changed to:
Consider declaring the following lines without explicitly setting the index to
0
:For-Loops: Cache array length outside of loops
Reading an array's length at each iteration has the following gas overheads:
mload
(3 gas)calldataload
(3 gas)Caching the length changes each of these to a
DUP<N>
(3 gas), and gets rid of the extraDUP<N>
needed to store the stack offset. This would save around 3 gas per iteration.For example:
can be changed to:
Consider making the following change to these lines:
For-Loops: Index increments can be left unchecked
From Solidity v0.8 onwards, all arithmetic operations come with implicit overflow and underflow checks.
In for-loops, as it is impossible for the index to overflow, index increments can be left unchecked to save 30-40 gas per loop iteration.
For example, the code below:
can be changed to:
Consider making the following change to these lines:
Arithmetics:
++i
costs less gas compared toi++
ori += 1
++i
costs less gas compared toi++
ori += 1
for unsigned integers, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.i++
incrementsi
and returns the initial value ofi
. Which means:But
++i
returns the actual incremented value:In the first case, the compiler has to create a temporary variable (when used) for returning
1
instead of2
, thus it costs more gas.The same logic applies for
--i
andi--
.Consider using
++i
instead ofi++
ori += 1
in the following instances:Visibility:
public
functions can be set toexternal
Calls to
external
functions are cheaper thanpublic
functions. Thus, if a function is not used internally in any contract, it should be set toexternal
to save gas and improve code readability.Consider changing following functions from
public
toexternal
:Unnecessary initialization of variables with default values
Uninitialized variables are assigned with a default value depending on its type:
uint
:0
bool
:false
address
:address(0)
Thus, explicitly initializing a variable with its default value costs unnecesary gas. For example, the following code:
can be changed to:
Consider declaring the following lines without explicitly setting a value:
Use
calldata
instead ofmemory
for read-only arguments in external functionsWhen an external function with a
memory
array is called, theabi.decode()
step has to use a for-loop to copy each index of thecalldata
to thememory
index. Each iteration of this for-loop costs at least 60 gas (i.e.60 * <mem_array>.length
).Using calldata directly helps to save gas as values are read directly from
calldata
usingcalldataload
, thus removing the need for such a loop in the contract code during runtime execution.Also, structs have the same overhead as an array of length one.
Consider changing the following from
memory
tocalldata
:Usage of
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadAs seen from here:
However, this does not apply to storage values as using reduced-size types might be beneficial to pack multiple elements into a single storage slot. Thus, where appropriate, use
uint256
/int256
and downcast when needed.Consider using
uint256
/int256
instead ofbool
for the following:abi.encode()
is less efficient thanabi.encodePacked()
Instances where
abi.encodePacked()
should be used rather thanabi.encode()
:internal
functions only called once can be inlined to save gasNot inlining costs 20 to 40 gas because of two extra
JUMP
instructions and additional stack operations needed for function calls.Consider inlining the following
internal
functions:keccak256()
should only need to be called on a specific string literal onceThe result of
keccak256()
should be saved to an immutable variable, and the variable used instead. If the hash is being used as a part of a function selector, the cast tobytes4
should also only be done once.Instances of
keccak256()
that can be saved to an immutable variable: