Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn’t possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block
The above line cannot underflow due the check on line 311 which ensures that the operation _initialTokenSupply - _totalSupply would only be performed if _initialTokenSupply is greater than _totalSupply
Using unchecked blocks to save gas - Increments in for loop can be unchecked
The majority of Solidity for loops increment a uint256 variable that starts at 0. These increment operations never need to be checked for over/underflow because the variable will never reach the max number of uint256 (will run out of gas long before that happens). The default over/underflow check wastes gas in every iteration of virtually every for loop . eg.
Cache the length of arrays in loops ~6 gas per iteration
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
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 (EIP-2929 2) 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):
When reading the length of an array, 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. which brings this to 6 gas
Here, I suggest storing the array’s length in a variable before the for-loop, and use it instead:
++i costs less gas compared to i++ or i += 1 (~5 gas per iteration)
++i costs less gas compared to i++ or i += 1 for unsigned integer, 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
Splitting require() statements that use && saves gas - 8 gas per &&
Instead of using the && operator in a single require statement to check multiple conditions,using multiple require statements with 1 condition per require statement will save 8 GAS per &&
The gas difference would only be realized if the revert condition is realized(met).
require(basketUpdateTime != 0 && block.timestamp >= basketUpdateTime, "NibblVaultFactory: UPDATE_TIME has not passed");
The above should be modified as follows
require(basketUpdateTime != 0, "NibblVaultFactory: UPDATE_TIME has not passed");
require(block.timestamp >= basketUpdateTime, "NibblVaultFactory: UPDATE_TIME has not passed");
require(vaultUpdateTime != 0 && block.timestamp >= vaultUpdateTime, "NibblVaultFactory: UPDATE_TIME has not passed");
ProofThe following tests were carried out in remix with both optimization turned on and off
require ( a > 1 && a < 5, "Initialized");
return a + 2;
}
Execution cost
21617 with optimization and using &&
21976 without optimization and using &&
After splitting the require statement
require (a > 1 ,"Initialized");
require (a < 5 , "Initialized");
return a + 2;
}
Execution cost
21609 with optimization and split require
21968 without optimization and using split require
Expressions for constant values such as a call to keccak256(), should use immutable rather than constant
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.
If the variable was immutable instead: the calculation would only be done once at deploy time (in the constructor), and then the result would be saved and read directly at runtime rather than being recalculated.
consequences:
Each usage of a "constant" costs ~100gas more on each access (it is still a little better than storing the result in storage, but not much..)
Since these are not real constants, they can't be referenced from a real constant environment (e.g. from assembly, or from another library )
You can (and should) attach error reason strings along with require statements to make it easier to understand why a contract call reverted. These strings, however, take space in the deployed bytecode. Every reason string takes at least 32 bytes so make sure your string fits in 32 bytes or it will become more expensive.
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
FINDINGS
Using unchecked blocks to save gas
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn’t possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block
File: NibblVault.sol line 319
The above line cannot underflow due the check on line 311 which ensures that the operation
_initialTokenSupply - _totalSupply
would only be performed if_initialTokenSupply
is greater than_totalSupply
We can therefore use unchecked blocks as follows
File:NibblVault.sol line 378
The above line cannot underflow due to the check on line 373 which ensures that
_totalSupply
is greater than_initialTokenSupply
Something similar to my proposal has already been implemented on line 23 as show below
Using unchecked blocks to save gas - Increments in for loop can be unchecked
The majority of Solidity for loops increment a uint256 variable that starts at 0. These increment operations never need to be checked for over/underflow because the variable will never reach the max number of uint256 (will run out of gas long before that happens). The default over/underflow check wastes gas in every iteration of virtually every for loop . eg.
e.g Let's work with a sample loop below.
can be written as shown below.
We can also write it as an inlined function like below.
Affected code
File: NibblVault.sol line 506
The above should be modified to:
Other Instances to modify File: NibblVault.sol line 525
File: Basket.sol line 43
File: Basket.sol line 70
File: Basket.sol line 93
see resource
Cache the length of arrays in loops ~6 gas per iteration
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
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 (EIP-2929 2) 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): When reading the length of an array, 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. which brings this to 6 gas
Here, I suggest storing the array’s length in a variable before the for-loop, and use it instead:
File: NibblVault.sol line 506
The above should be modified to
Other instances to modify File: NibblVault.sol line 525
File: Basket.sol line 43
File: Basket.sol line 70
File: Basket.sol line 93
++i costs less gas compared to i++ or i += 1 (~5 gas per iteration)
++i costs less gas compared to i++ or i += 1 for unsigned integer, 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:
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 of 2
Instances include:
File: NibblVault.sol line 506
The above line can be written as follows
Other Instances File: NibblVault.sol line 525
File: Basket.sol line 43
File: Basket.sol line 70
File: Basket.sol line 93
Splitting require() statements that use && saves gas - 8 gas per &&
Instead of using the && operator in a single require statement to check multiple conditions,using multiple require statements with 1 condition per require statement will save 8 GAS per && The gas difference would only be realized if the revert condition is realized(met).
File:NibblVaultFactory.sol line 107
The above should be modified as follows
File:NibblVaultFactory.sol line 131
File:NibblVaultFactory.sol line 149
File:NibblVaultFactory.sol line 166
Proof The following tests were carried out in remix with both optimization turned on and off
Execution cost 21617 with optimization and using && 21976 without optimization and using &&
After splitting the require statement
Execution cost 21609 with optimization and split require 21968 without optimization and using split require
Expressions for constant values such as a call to keccak256(), should use immutable rather than constant
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.
If the variable was immutable instead: the calculation would only be done once at deploy time (in the constructor), and then the result would be saved and read directly at runtime rather than being recalculated.
consequences:
Each usage of a "constant" costs ~100gas more on each access (it is still a little better than storing the result in storage, but not much..)
Since these are not real constants, they can't be referenced from a real constant environment (e.g. from assembly, or from another library )
File:AccessControlMechanism.sol line 12
File:AccessControlMechanism.sol line 13
File:AccessControlMechanism.sol line 14
File: NibblVault.sol line 51
File: EIP712Base.sol line 7
Use shorter revert strings(less than 32 bytes)
You can (and should) attach error reason strings along with require statements to make it easier to understand why a contract call reverted. These strings, however, take space in the deployed bytecode. Every reason string takes at least 32 bytes so make sure your string fits in 32 bytes or it will become more expensive.
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met. Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
File:NibblVaultFactory.sol line 48
File:NibblVaultFactory.sol line 49
Other instances to modify File:NibblVaultFactory.sol line 107
File:NibblVaultFactory.sol line 131
File:NibblVaultFactory.sol line 141
File:NibblVaultFactory.sol line 149
File:NibblVaultFactory.sol line 166
I suggest shortening the revert strings to fit in 32 bytes, or using custom errors.
Use Custom Errors instead of Revert Strings to save Gas
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
see Source
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).