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.
In particular, in for loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is high
PROOF OF CONCEPT
Instances include:
IndexLogic.sol
scope: mint()
registry is read (2 + assets.length + inactiveAssets.length) times:
number of reads depending on length of assets and inactiveAssets as it is in a for loop
assets.length() is read (assets.length) times:
number of reads depending on length of assets as it is in a for loop
IndexLogic.sol:39
inactiveAssets.length() is read (inactiveAssets.length) times:
number of reads depending on length of inactiveAssets as it is in a for loop
IndexLogic.sol:60
vTokenFactory is read (assets.length + inactiveAssets.length) times:
number of reads depending on length of assets and inactiveAssets as it is in a for loop
IndexLogic.sol:47
IndexLogic.sol:63
scope: burn()
registry is read (2 + assets.length + inactiveAssets.length) times:
number of reads depending on length of assets and inactiveAssets as it is in a for loop
inactiveAssets.length() is read (assets.length + inactiveAssets.length) times:
number of reads depending on length of assets and inactiveAssets as it is in a for loop
IndexLogic.sol:125
vTokenFactory is read (assets.length + inactiveAssets.length) times:
number of reads depending on length of assets and inactiveAssets as it is in a for loop
IndexLogic.sol:131
ManagedIndexReweightingLogic.sol
scope: reweight()
registry is read (3 + _updatedAssets.length) times:
number of reads depending on length of _updatedAssets as it is in a for loop
assets.length() is read (assets.length) times:
number of reads depending on length of assetsas it is in a for loop
ManagedIndexReweightingLogic.sol:38
vTokenFactory is read (assets.length + _updatedAssets.length + _inactiveAssets.length) times:
number of reads depending on length of assets, _updatedAssets and _inactiveAssets as it is in a for loop
assets.length() is read (assets.length) times:
number of reads depending on length of assetsas it is in a for loop
TopNMarketCapReweightingLogic.sol:37
vTokenFactory is read (assets.length + diff.assetCount + _inactiveAssets.length) times:
number of reads depending on length of assets, diff.assetCount and _inactiveAssets as it is in a for loop
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
if (msg.sender != factory) {
revert IsNotFactory(msg.sender);
}
and define the custom error in the contract
error IsNotFactory(address _address);
Dead code
IMPACT
Functions that are not used should be removed as they increase the gas cost during deployment
PROOF OF CONCEPT
Instances include:
IndexLibrary.sol
IndexLibrary.sol:24: //amountInAsset() is never used in the contracts.
TOOLS USED
Manual Analysis
MITIGATION
Remove amountInAsset()
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.
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked block will save gas
PROOF OF CONCEPT
Instances include:
BaseIndex.sol
BaseIndex.sol:78: i bounded by _assets.length, overflow check unnecessary
IndexLogic.sol
IndexLogic.sol:39: i bounded by assets.length, overflow check unnecessary
IndexLogic.sol:60: i bounded by inactiveAssets.length, overflow check unnecessary
IndexLogic.sol:102: i bounded by assets.length, overflow check unnecessary
IndexLogic.sol:125: i bounded by assets.length+inactiveAssets.length, overflow check unnecessary
ManagedIndex.sol
ManagedIndex.sol:30: i bounded by _assets.length, overflow check unnecessary
ManagedIndexReweightingLogic.sol
ManagedIndexReweightingLogic.sol:38: i bounded by assets.length, overflow check unnecessary
ManagedIndexReweightingLogic.sol:50: i bounded by _updatedAssets.length, overflow check unnecessary
ManagedIndexReweightingLogic.sol:96: i bounded by _inactiveAssets.length, overflow check unnecessary
TopNMarketCapIndex.sol
TopNMarketCapIndex.sol:48: i bounded by _assets.length, overflow check unnecessary
TopNMarketCapIndex.sol:49: i bounded by _assets.length_ underflow check unnecessary
TopNMarketCapReweightingLogic.sol
TopNMarketCapReweightingLogic.sol:37: i bounded by assets.length, overflow check unnecessary
TopNMarketCapReweightingLogic.sol:51: i bounded by diff.assetCount, overflow check unnecessary
TopNMarketCapReweightingLogic.sol:104: i bounded by _inactiveAssets.length, overflow check unnecessary
TrackedIndex.sol
TrackedIndex.sol:35: i bounded by _assets.length, overflow check unnecessary
TrackedIndexReweightingLogic.sol
TrackedIndexReweightingLogic.sol:37: i bounded by assets.length, overflow check unnecessary
TrackedIndexReweightingLogic.sol:66: i bounded by assets.length, overflow check unnecessary
UniswapV2PathPriceOracle.sol
UniswapV2PathPriceOracle.sol:34: i bounded by path.length, overflow check unnecessary
UniswapV2PathPriceOracle.sol:49: i bounded by path.length, overflow check unnecessary
TOOLS USED
Manual Analysis
MITIGATION
Place the arithmetic operations in an unchecked block
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.
In particular, in
for
loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is highPROOF OF CONCEPT
Instances include:
IndexLogic.sol
scope:
mint()
registry
is read (2 + assets.length + inactiveAssets.length) times: number of reads depending on length ofassets
andinactiveAssets
as it is in a for loopassets.length()
is read (assets.length) times: number of reads depending on length ofassets
as it is in a for loopinactiveAssets.length()
is read (inactiveAssets.length) times: number of reads depending on length ofinactiveAssets
as it is in a for loopvTokenFactory
is read (assets.length + inactiveAssets.length) times: number of reads depending on length ofassets
andinactiveAssets
as it is in a for loopscope:
burn()
registry
is read (2 + assets.length + inactiveAssets.length) times: number of reads depending on length ofassets
andinactiveAssets
as it is in a for loopinactiveAssets.length()
is read (assets.length + inactiveAssets.length) times: number of reads depending on length ofassets
andinactiveAssets
as it is in a for loopvTokenFactory
is read (assets.length + inactiveAssets.length) times: number of reads depending on length ofassets
andinactiveAssets
as it is in a for loopManagedIndexReweightingLogic.sol
scope:
reweight()
registry
is read (3 + _updatedAssets.length) times: number of reads depending on length of_updatedAssets
as it is in a for loopassets.length()
is read (assets.length) times: number of reads depending on length ofassets
as it is in a for loopvTokenFactory
is read (assets.length + _updatedAssets.length + _inactiveAssets.length) times: number of reads depending on length ofassets
,_updatedAssets
and_inactiveAssets
as it is in a for loopTopNMarketCapReweightingLogic.sol
scope:
reweight()
registry
is read (2 + diff.assetCount) times: number of reads depending on length of_updatedAssets
as it is in a for loopassets.length()
is read (assets.length) times: number of reads depending on length ofassets
as it is in a for loopvTokenFactory
is read (assets.length + diff.assetCount + _inactiveAssets.length) times: number of reads depending on length ofassets
,diff.assetCount
and_inactiveAssets
as it is in a for loopTrackedIndexReweightingLogic.sol
scope:
reweight()
registry
is read (3 + assets.length) times: number of reads depending on length ofassets
as it is in a for loopassets.length()
is read (2 * assets.length) times: number of reads depending on length ofassets
as it is in a for loopvTokenFactory
is read (2 * assets.length) times: number of reads depending on length ofassets
as it is in a for loopUniswapV2PathPriceOracle.sol
scope:
refreshedAssetPerBaseInUQ()
path.length
is read (path.length - 1) times: number of reads depending on length ofpath
as it is in a for loopscope:
lastAssetPerBaseInUQ()
path.length
is read (path.length - 1) times: number of reads depending on length ofpath
as it is in a for loopvToken.sol
scope:
_transferAsset()
asset
is read twice:TOOLS USED
Manual Analysis
MITIGATION
cache these storage variables in memory
Comparisons with zero for unsigned integers
IMPACT
>0
is less gas efficient than!0
if you enable the optimizer at 10k AND you’re in a require statement. Detailed explanation with the opcodes herePROOF OF CONCEPT
Instances include:
IndexLogic.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace
> 0
with!0
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:
ManagedIndexReweightingLogic.sol
UniswapV2PathPriceOracle.sol
UniswapV2PriceOracle.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace
<=
with<
, and>=
with>
. Do not forget to increment/decrement the compared variableexample:
When the comparison is with a constant storage variable, you can also do the increment in the storage variable declaration
example:
and
However in this case, the 1 second difference is negligible compared to the value of
MIN_UPDATE_INTERVAL
, so the following is the best option:and
MIN_UPDATE_INTERVAL
remains unchangedCustom 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:
BaseIndex.sol
ChainlinkPriceOracle.sol
IndexLogic.sol
ManagedIndex.sol
ManagedIndexReweightingLogic.sol
PhuturePriceOracle.sol
TopNMarketCapIndex.sol
TopNMarketCapReweightingLogic.sol
TrackedIndex.sol
TrackedIndexReweightingLogic.sol
UniswapV2PathPriceOracle.sol
UniswapV2PriceOracle.sol
vToken.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace require and revert statements with custom errors.
For instance, in
TopNMarketCapIndex.sol
:Replace
with
and define the custom error in the contract
Dead code
IMPACT
Functions that are not used should be removed as they increase the gas cost during deployment
PROOF OF CONCEPT
Instances include:
IndexLibrary.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove
amountInAsset()
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:
UniswapV2PathPriceOracle.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove explicit initialization for default values.
Prefix increments
IMPACT
Prefix increments are cheaper than postfix increments.
PROOF OF CONCEPT
Instances include:
UniswapV2PathPriceOracle.sol
FullMath.sol
TOOLS USED
Manual Analysis
MITIGATION
change
i++
to++i
inUniswapV2PathPriceOracle.sol
.change
result++
to++result
inFullMath.sol
Require instead of &&
IMPACT
Require statements including conditions with the
&&
operator can be broken down in multiple require statements to save gas.PROOF OF CONCEPT
Instances include:
ManagedIndexReweightingLogic.sol
TOOLS USED
Manual Analysis
MITIGATION
Unchecked arithmetic
IMPACT
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an
unchecked
block will save gasPROOF OF CONCEPT
Instances include:
BaseIndex.sol
IndexLogic.sol
ManagedIndex.sol
ManagedIndexReweightingLogic.sol
TopNMarketCapIndex.sol
TopNMarketCapReweightingLogic.sol
TrackedIndex.sol
TrackedIndexReweightingLogic.sol
UniswapV2PathPriceOracle.sol
TOOLS USED
Manual Analysis
MITIGATION
Place the arithmetic operations in an
unchecked
blockUnnecessary computation
IMPACT
Inlining computation can save gas
PROOF OF CONCEPT
Instances include:
IndexLogic.sol
TOOLS USED
Manual Analysis
MITIGATION
Unnecessary check
IMPACT
we can remove if statements where unnecessary to save gas.
PROOF OF CONCEPT
BaseIndex.sol
line 38,
minAmountInBase = type(uint).max
, hence the condition check line 51 is redundant.TOOLS USED
Manual Analysis
MITIGATION
Remove the
if
condition check