Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
scope: addMerkleTree()
numTrees is read twice
MerkleDropFactory.sol:59
MerkleDropFactory.sol:61
MerkleEligibility.sol
scope: addGate()
numGates is read twice
MerkleEligibility.sol:49
MerkleEligibility.sol:50
MerkleResistor.sol
scope: addMerkleTree()
numTrees is read twice
MerkleResistor.sol:99
MerkleResistor.sol:100
MerkleVesting.sol
scope: addMerkleRoot()
numTrees is read twice
MerkleVesting.sol:71
MerkleVesting.sol:72
TOOLS USED
Manual Analysis
MITIGATION
cache these storage variables in memory
Calldata instead of memory for RO function parameters
PROBLEM
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory.
Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
Hardcode the state variable with its initial value instead of writing it during contract deployment with constructor parameters.
Custom Errors
IMPACT
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here
Custom errors are defined using the error statement
Replace require and revert statements with custom errors.
For instance, in MerkleEligibility.sol:
Replace
require(msg.sender == gateMaster, "Only gatemaster may call this.");
with
if (msg.sender != gateMaster) {
revert IsNotGateMaster(msg.sender);
}
and define the custom error in the contract
error IsNotGateMaster(address _address);
Default value initialization
IMPACT
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type).
Explicitly initializing it with its default value is an anti-pattern and wastes gas.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
MerkleDropFactory.sol:17: uint public numTrees = 0;
MerkleEligibility.sol
MerkleEligibility.sol:31: uint public numGates = 0;
PermissionlessBasicPoolFactory.sol:115: uint i = 0;
PermissionlessBasicPoolFactory.sol:141: uint i = 0;
PermissionlessBasicPoolFactory.sol:168: uint i = 0;
PermissionlessBasicPoolFactory.sol:224: uint i = 0;
PermissionlessBasicPoolFactory.sol:249: uint i = 0;
PermissionlessBasicPoolFactory.sol:266: uint i = 0;
VoterID.sol
VoterID.sol:69: uint public numIdentities = 0;
TOOLS USED
Manual Analysis
MITIGATION
Remove explicit initialization for default values.
Event emitting of local variable
PROBLEM
When emitting an event, using a local variable instead of a storage variable saves gas.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
MerkleDropFactory.sol:61
MerkleResistor.sol
MerkleResistor.sol:122
MerkleVesting.sol
MerkleVesting.sol:72
TOOLS USED
Manual Analysis
MITIGATION
The storage variable is read multiple times in the function and it is recommended to cache it into memory, then passing the cached variable in the emit statement, as explained in the cache paragraph
Prefix increments
IMPACT
Prefix increments are cheaper than postfix increments.
change variable++ to ++variable, and variable += 1 to ++variable.
Unnecessary computation
IMPACT
When emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
Gas Report
Table of Contents
Caching storage variables in memory to save gas
IMPACT
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
scope:
addMerkleTree()
numTrees
is read twiceMerkleEligibility.sol
scope:
addGate()
numGates
is read twiceMerkleResistor.sol
scope:
addMerkleTree()
numTrees
is read twiceMerkleVesting.sol
scope:
addMerkleRoot()
numTrees
is read twiceTOOLS USED
Manual Analysis
MITIGATION
cache these storage variables in memory
Calldata instead of memory for RO function parameters
PROBLEM
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
scope:
withdraw()
MerkleEligibility.sol
scope:
isEligible()
scope:
passThruGate()
MerkleIdentity.sol
scope:
withdraw()
scope:
isEligible()
scope:
verifyMetadata()
MerkleLib.sol
scope:
verifyProof()
MerkleResistor.sol
scope:
initialize()
MerkleVesting.sol
scope:
initialize()
PermissionlessBasicPoolFactory.sol
scope:
addPool()
VoterID.sol
scope:
createIdentityFor()
scope:
setTokenURI()
scope:
safeTransferFrom()
TOOLS USED
Manual Analysis
MITIGATION
Replace
memory
withcalldata
Comparison Operators
IMPACT
In the EVM, there is no opcode for
>=
or<=
. When using greater than or equal, two operations are performed:>
and=
.Using strict comparison operators hence saves gas
PROOF OF CONCEPT
Instances include:
FixedPricePassThruGate.sol
MerkleDropFactory.sol
MerkleIdentity.sol
MerkleResistor.sol
MerkleVesting.sol
SpeedBumpPriceGate.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace
<=
with<
, and>=
with>
. Do not forget to increment/decrement the compared variableHowever, if
1
is negligible compared to the value of the variable, we can omit the increment.Constructor parameters should be avoided when possible
IMPACT
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters.
PROOF OF CONCEPT
Instances include:
MerkleEligibility.sol
MerkleIdentity.sol
PermissionlessBasicPoolFactory.sol
VoterID.sol
TOOLS USED
Manual Analysis
MITIGATION
Hardcode the state variable with its initial value instead of writing it during contract deployment with constructor parameters.
Custom Errors
IMPACT
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here
Custom errors are defined using the error statement
PROOF OF CONCEPT
Instances include:
FixedPricePassThruGate.sol
MerkleDropFactory.sol
MerkleEligibility.sol
MerkleIdentity.sol
MerkleResistor.sol
MerkleVesting.sol
PermissionlessBasicPoolFactory.sol
SpeedBumpPriceGate.sol
VoterID.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace require and revert statements with custom errors.
For instance, in
MerkleEligibility.sol
:Replace
with
and define the custom error in the contract
Default value initialization
IMPACT
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
MerkleEligibility.sol
MerkleLib.sol
MerkleResistor.sol
MerkleVesting.sol
PermissionlessBasicPoolFactory.sol
VoterID.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove explicit initialization for default values.
Event emitting of local variable
PROBLEM
When emitting an event, using a local variable instead of a storage variable saves gas.
PROOF OF CONCEPT
Instances include:
MerkleDropFactory.sol
MerkleResistor.sol
MerkleVesting.sol
TOOLS USED
Manual Analysis
MITIGATION
The storage variable is read multiple times in the function and it is recommended to cache it into memory, then passing the cached variable in the
emit
statement, as explained in the cache paragraphPrefix increments
IMPACT
Prefix increments are cheaper than postfix increments.
PROOF OF CONCEPT
Instances include:
MerkleLib.sol
PermissionlessBasicPoolFactory.sol
TOOLS USED
Manual Analysis
MITIGATION
change
variable++
to++variable
, andvariable += 1
to++variable
.Unnecessary computation
IMPACT
When emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
PROOF OF CONCEPT
Instances include:
VoterID.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace
with