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.
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.
However, when 1 is negligible compared to the variable (with is the case here as the variable is in the order of 10**16), it is not necessary to increment.
Custom Errors
PROBLEM
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 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.
Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage.
By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.
address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).
As bool type variables are of size 1 byte, there's a slot here that can get saved by moving them closer to an address
PROOF OF CONCEPT
Instances include:
VaultStorage.sol
VaultStorage.sol:11:
address public pool;
uint256 public totalDebt;
bool public strategyActive;
TOOLS USED
Manual Analysis
MITIGATION
Place strategyActive after pool to save one storage slot
address public pool;
+bool public strategyActive;
uint256 public totalDebt;
Unchecked arithmetic
PROBLEM
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:
LiquidityPool.sol
LiquidityPool.sol:751: underlyingBalance - underlyingToWithdraw; //because of the condition line 745, the underflow check is unnecessary
BkdEthCvx.sol
BkdEthCvx.sol:83: uint256 requiredUnderlyingAmount = amount - underlyingBalance; //because of the condition line 76, the underflow check is unnecessary
BkdTriHopCvx.sol
BkdTriHopCvx.sol:181: uint256 requiredUnderlyingAmount = amount - underlyingBalance; //because of the condition line 174, the underflow check is unnecessary
CvxMintAmount.sol
CvxMintAmount.sol:24: uint256 remaining = _CLIFF_COUNT - currentCliff; //because of the condition line 21, the underflow check is unnecessary
Vault.sol
Vault.sol:24: uint256 remaining = _CLIFF_COUNT - currentCliff; //because of the condition line 21, the underflow check is unnecessary
Vault.sol:125: uint256 requiredWithdrawal = amount - availableUnderlying_; //because of the condition line 122, the underflow check is unnecessary
Vault.sol:130: uint256 newTarget = (allocated - requiredWithdrawal) //because of the condition line 127, the underflow check is unnecessary
Vault.sol:141: uint256 totalUnderlyingAfterWithdraw = totalUnderlying - amount; //because of the condition line 122, the underflow check is unnecessary
Vault.sol:440: waitingForRemovalAllocated = _waitingForRemovalAllocated - withdrawn; //because of the condition line 437, the underflow check is unnecessary
Vault.sol:444: uint256 profit = withdrawn - allocated; //because of the condition line 443, the underflow check is unnecessary
Vault.sol:452: allocated -= withdrawn; //because of the condition line 443, the underflow check is unnecessary
Vault.sol:591: uint256 profit = allocatedUnderlying - amountAllocated; //because of the condition line 589, the underflow check is unnecessary
Vault.sol:595: profit -= currentDebt; //because of the condition line 593, the underflow check is unnecessary
Vault.sol:600: currentDebt -= profit; //because of the condition line 593, the underflow check is unnecessary
Vault.sol:605: uint256 loss = amountAllocated - allocatedUnderlying; //because of the condition line 603, the underflow check is unnecessary
Vault.sol:784: uint256 withdrawAmount = allocatedUnderlying - target; //because of the condition line 782, the underflow check is unnecessary
Vault.sol:790: uint256 depositAmount = target - allocatedUnderlying; //because of the condition line 788, the underflow check is unnecessary
StakerVault.sol
StakerVault.sol:164: uint256 srcTokensNew = srcTokens - amount; //because of the condition line 153, the underflow check is 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
PROBLEM
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:
AaveHandler.sol
scope:
topUp()
weth
is read twicelendingPool
is read 4 timesCompoundHandler.sol
scope:
_getAccountBorrowsAndSupply()
comptroller
is read (2 +assets.length
) times. Number of read depends on the length ofassets
as it is in a for loopCTokenRegistry.sol
scope:
_isCTokenUsable()
comptroller
is read 3 timesTopUpAction.sol
scope:
resetPosition()
addressProvider
is read twicescope:
execute()
addressProvider
is read 3 timesBkdEthCvx.sol
scope:
_withdraw()
vault
is read twiceBkdTriHopCvx.sol
scope:
_withdraw()
vault
is read twiceConvexStrategyBase.sol
scope:
addRewardToken()
_strategySwapper
is read twicescope:
harvestable()
crvCommunityReserveShare
is read twice_rewardTokens.length()
is read_rewardTokens.length()
times. Number of read depends on the length of_rewardsTokens
as it is in a for loopscope:
_harvest()
_rewardTokens.length()
is read_rewardTokens.length()
times. Number of read depends on the length of_rewardsTokens
as it is in a for loopscope:
_sendCommunityReserveShare()
cvxCommunityReserveShare
is read twiceVault.sol
scope:
_handleExcessDebt()
reserve
is read 3 timesscope:
_handleExcessDebt()
totalDebt
is read twiceVault.sol
scope:
stakeFor()
token
is read 4 timesscope:
unStakeFor()
token
is read 4 timesTOOLS 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:
RoleManager.sol
scope:
hasAnyRole()
AaveHandler.sol
scope:
topUp()
CompoundHandler.sol
scope:
topUp()
TopUpAction.sol
scope:
getHealthFactor()
TopUpKeeperHelper.sol
scope:
canExecute()
scope:
_canExecute()
scope:
_positionToTopup()
scope:
_shortenTopups()
Erc20Pool.sol
scope:
initialize()
EthPool.sol
scope:
initialize()
LiquidityPool.sol
scope:
initialize()
LpToken.sol
scope:
initialize()
TOOLS USED
Manual Analysis
MITIGATION
Replace
memory
withcalldata
Comparisons with zero for unsigned integers
PROBLEM
>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:
TopUpAction.sol
scope:
register()
scope:
execute()
TopUpActionFeeHandler.sol
scope:
claimKeeperFeesForPool()
LiquidityPool.sol
scope:
updateDepositCap()
scope:
calcRedeem()
scope:
redeem()
Vault.sol
scope:
withdrawFromReserve()
BkdLocker.sol
scope:
depositFees()
TOOLS USED
Manual Analysis
MITIGATION
Replace
> 0
with!0
Comparison Operators
PROBLEM
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:
TopUpAction.sol
TopUpActionFeeHandler.sol
ChainLinkOracleProvider.sol
EthPool.sol
BkdEthCvx.sol
BkdTriHopCvx.sol
ConvexStrategyBase.sol
StrategySwapper.sol
CvxMintAmount.sol
Preparable.sol
Vault.sol
VaultReserve.sol
BkdLocker.sol
Controller.sol
CvxCrvRewardsLocker.sol
GasBank.sol
StakerVault.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:
However, when
1
is negligible compared to the variable (with is the case here as the variable is in the order of10**16
), it is not necessary to increment.Custom Errors
PROBLEM
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:
RoleManager.sol
AaveHandler.sol
CompoundHandler.sol
TopUpAction.sol
TopUpActionFeeHandler.sol
ChainLinkOracleProvider.sol
Erc20Pool.sol
EthPool.sol
LiquidityPool.sol
PoolFactory.sol
BkdTriHopCvx.sol
ConvexStrategyBase.sol
StrategySwapper.sol
Preparable.sol
Erc20Vault.sol
Vault.sol
VaultReserve.sol
AddressProvider.sol
BkdLocker.sol
Controller.sol
CvxCrvRewardsLocker.sol
GasBank.sol
LpToken.sol
StakerVault.sol
SwapperRegistry.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace require statements with custom errors.
Default value initialization
PROBLEM
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:
RoleManager.sol
CompoundHandler.sol
CTokenRegistry.sol
TopUpAction.sol
TopUpActionKeeperHandler.sol
LiquidityPool.sol
ConvexStrategyBase.sol
Vault.sol
BkdLocker.sol
Controller.sol
CvxCrvRewardsLocker.sol
StakerVault.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove explicit initialization for default values.
Prefix increments
PROBLEM
Prefix increments are cheaper than postfix increments.
PROOF OF CONCEPT
Instances include:
RoleManager.sol
CompoundHandler.sol
CTokenRegistry.sol
TopUpAction.sol
TopUpActionKeeperHandler.sol
ConvexStrategyBase.sol
BkdLocker.sol
Controller.sol
StakerVault.sol
TOOLS USED
Manual Analysis
MITIGATION
change
variable++
to++variable
Redundant code
IMPACT
Redundant code should be avoided as it costs unnecessary gas
PROOF OF CONCEPT
Instances include:
Preparable.sol
We can update
currentAddresses[key]
after emitting the event to save the gas of the declaration ofoldValue
:TOOLS USED
Manual Analysis
MITIGATION
see Proof of Concept for mitigation steps.
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:
TopUpAction.sol
ConvexStrategyBase.sol
SwapperRegistry.sol
TOOLS USED
Manual Analysis
MITIGATION
Breakdown each condition in a separate
require
statement (though require statements should be replaced with custom errors)Tight Variable Packing
PROBLEM
Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage. By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.
address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).
As bool type variables are of size 1 byte, there's a slot here that can get saved by moving them closer to an address
PROOF OF CONCEPT
Instances include:
VaultStorage.sol
TOOLS USED
Manual Analysis
MITIGATION
Place
strategyActive
afterpool
to save one storage slotUnchecked arithmetic
PROBLEM
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:
LiquidityPool.sol
BkdEthCvx.sol
BkdTriHopCvx.sol
CvxMintAmount.sol
Vault.sol
StakerVault.sol
TOOLS USED
Manual Analysis
MITIGATION
Place the arithmetic operations in an
unchecked
block