Platform: Code4rena
Start Date: 15/06/2022
Pot Size: $35,000 USDC
Total HM: 1
Participants: 36
Period: 3 days
Judge: Jack the Pug
Total Solo HM: 1
Id: 137
League: ETH
Rank: 6/36
Findings: 2
Award: $236.66
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xNazgul
Also found by: 0xDjango, 0xFar5eer, 0xf15ers, BowTiedWardens, Chom, Dravee, IllIllI, Meera, MiloTruck, PierrickGT, TerrierLover, _Adam, cccz, codexploder, cryptphi, delfin454000, fatherOfBlocks, hansfriese, joestakey, oyc_109, simon135
153.5218 USDC - $153.52
Few vulnerabilities were found examining the contracts. The main concerns are with:
Some of the function comments are missing function parameters or returns
Non-Critical
Parameters missing a natspec comment include:
uint256 reserveA
uint256 reserveB
IBiswapRouter02 router
uint256 swapAmount\
uint256 reserveA
uint256 reserveB
IUniswapV2Router02 router
uint256 swapAmount
bytes32 id
bytes32 id
bytes32 id
bytes32 id
bytes32 id
address target,uint256 value,bytes calldata data,bytes32 predecessor,bytes32 salt
address[] calldata targets,uint256[] calldata values,bytes[] calldata datas,bytes32 predecessor,bytes32 salt
address target,uint256 value,bytes calldata data,bytes32 predecessor,bytes32 salt,uint256 delay
address[] calldata targets,uint256[] calldata values,bytes[] calldata datas,bytes32 predecessor,bytes32 salt,uint256 delay
bytes32 id, uint256 delay
bytes32 id
address target,uint256 value,bytes calldata data,bytes32 predecessor,bytes32 salt
address target,uint256 value,bytes calldata data
address[] calldata targets,uint256[] calldata values,bytes[] calldata datas,bytes32 predecessor,bytes32 salt
bytes32 id, bytes32 predecessor
bytes32 id
bytes32 id,uint256 index,address target,uint256 value,bytes calldata data
uint256 newDelay
Manual Analysis
Add a comment for these parameters
It is best practice to use constant variables rather than literal values to make the code easier to understand and maintain.
Non-Critical
Instances include:
10000
10000
10000
10000
10000
10000
Manual Analysis
Define constant variables for the literal values aforementioned.
Events should use indexed fields
Non-Critical
Instances include:
event CacheUpdated(bytes32 name, IOperatorResolver.Operator destination)
event VaultAdded(address vault, address tokenOrZapper)
event VaultRemoved(address vault)
event VaultAdded(address vault, CurvePool pool)
event VaultRemoved(address vault)
event MinDelayChange(uint256 oldDuration, uint256 newDuration)
Manual Analysis
Add indexed fields to these events so that they have the maximum number of indexed fields possible.
For readability, it is best to use scientific notation (e.g 10e5
) rather than decimal literals(100000
) or exponentiation(10**5
)
Non-Critical
Instances include:
10000
10000
10000
10000
10000
10000
Manual Analysis
Replace the numbers aforementioned with their scientific notation
There are some typos/misspelt words in the contracts.
Non-Critical
Instances include:
Manual Analysis
Replace with WITHDRAWN
constructors should check the address written in an immutable address variable is not the zero address
Low
Instances include:
eth = _eth
weth = IWETH(_weth)
withdrawer = _withdrawer
Manual Analysis
Add a zero address check for these parameters.
Some functions have the payable
modifier but their logic does not make use of msg.value
. These contracts do not have any way to withdraw ETH, meaning any ETH sent would get locked.
Low
Instances include:
scope: performSwap
scope: deposit
scope: deposit
scope: deposit
scope: deposit
scope: withdraw128
scope: withdraw256
Manual Analysis
There should be a require(0 == msg.value)
in these functions to ensure no Ether is being sent.
#0 - obatirou
2022-06-23T08:29:01Z
In our opinion it is not better for readability
#1 - obatirou
2022-06-23T14:32:57Z
They need to be payable because they are called trough a delegateCall from a payable function.
#2 - Yashiru
2022-06-24T13:23:02Z
Duplicated of #76 at 5. constants should be defined rather than using magic numbers
#3 - obatirou
2022-06-24T14:00:10Z
Duplicate from https://github.com/code-423n4/2022-06-nested-findings/issues/11#issuecomment-1165618378
#4 - Yashiru
2022-06-24T14:12:40Z
Duplicated of #45 at Typos
#5 - Yashiru
2022-06-24T15:00:21Z
Duplicated of #61 at 2. Missing address(0) checks
#6 - obatirou
2022-06-24T15:59:42Z
Missed occurences:
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xKitsune, 0xNazgul, 0xkatana, Chom, ElKu, JC, Meera, MiloTruck, Picodes, PierrickGT, SooYa, TerrierLover, UnusualTurtle, Waze, _Adam, asutorufos, c3phas, delfin454000, fatherOfBlocks, joestakey, minhquanym, oyc_109, robee, sach1r0, simon135
83.1436 USDC - $83.14
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.
Instances include:
scope: _transferFeeWithRoyalty()
feeSplitter
is read 3 timesManual Analysis
cache these storage variables in memory
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.
Instances include:
scope: fillQuote()
scope: execute()
Manual Analysis
Replace memory
with calldata
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
Instances include:
_entryFees <= 10000
_entryFees <= 10000
amountSpent <= _inputTokenAmount - feesAmount
amountSpent <= _inputTokenAmount
amounts[1] <= _amountToSpend
address(this).balance >= _inputTokenAmount
nestedRecords.getAssetHolding(_nftId, address(_inputToken)) >= _inputTokenAmount
vaultAmount >= minVaultAmount
amountToDeposit >= depositedAmount
tokenAmount >= minTokenAmount
vaultAmount >= minVaultAmount
amountToDeposit >= depositedAmount
tokenAmount >= minTokenAmount
Manual Analysis
Replace <=
with <
, and >=
with >
. Do not forget to increment/decrement the compared variable
example:
-vaultAmount >= minVaultAmount; +vaultAmount > minVaultAmount - 1;
However, if 1
is negligible compared to the value of the variable, we can omit the increment.
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters. With the compilers parameters in hardhat.config.ts
, deployment costs approximately 400
more gas per variable written via a constructor parameter.
Instances include:
nestedAsset = _nestedAsset; nestedRecords = _nestedRecords; reserve = _reserve; feeSplitter = _feeSplitter; weth = _weth; withdrawer = _withdrawer
tokenTransferProxy = _tokenTransferProxy
augustusSwapper = _augustusSwapper
eth = _eth
withdrawer = _withdrawer
nestedFactory = _nestedFactory
resolver = _resolver
Manual Analysis, hardhat
Hardcode storage variables with their initial value instead of writing it during contract deployment with constructor parameters.
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.
Instances include:
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
Manual Analysis
Remove explicit initialization for default values.
X += Y costs 22
more gas than X = X + Y. This can mean a lot of gas wasted in a function call when the computation is repeated n
times (loops)
Instances include:
amountBought -= amountFees
amountSpent += _submitOrder(address(tokenSold),_batchedOrders.orders[i].token,_nftId,_batchedOrders.orders[i],true // always to the reserve)
ethNeeded += _batchedOrders[i].amount
Manual Analysis
use X = X + Y
instead of X += Y
(same with -
)
Require statements including conditions with the &&
operator can be broken down in multiple require statements to save gas.
Instances include:
require(vaultAmount != 0 && vaultAmount >= minVaultAmount, "BVO: INVALID_AMOUNT_RECEIVED")
require(vaultAmount != 0 && vaultAmount >= minVaultAmount, "BLVO: INVALID_AMOUNT_RECEIVED")
require(depositedAmount != 0 && amountToDeposit >= depositedAmount, "BLVO: INVALID_AMOUNT_DEPOSITED")
require(vaultAmount != 0 && vaultAmount >= minVaultAmount, "BLVO: INVALID_AMOUNT_RECEIVED")
require(depositedAmount != 0 && amountToDeposit >= depositedAmount, "BLVO: INVALID_AMOUNT_DEPOSITED")
require(_tokenTransferProxy != address(0) && _augustusSwapper != address(0), "PSO: INVALID_ADDRESS")
Manual Analysis
Break down the statements in multiple require statements.
-require(_tokenTransferProxy != address(0) && _augustusSwapper != address(0), "PSO: INVALID_ADDRESS"); +require(_tokenTransferProxy != address(0)) +require(_augustusSwapper != address(0));
You can also improve gas savings by using custom errors
Revert strings cost more gas to deploy if the string is larger than 32 bytes. It costs 9,500
gas upon deployment per string exceeding that 32-byte size.
Revert strings exceeding 32 bytes include:
TimelockController: length mismatch
TimelockController: length mismatch
TimelockController: operation already scheduled
TimelockController: insufficient delay
TimelockController: operation cannot be cancelled
TimelockController: length mismatch
TimelockController: length mismatch
TimelockController: operation is not ready
TimelockController: missing dependency
TimelockController: operation is not ready
TimelockController: underlying transaction reverted
TimelockController: caller must be timelock
Manual Analysis
Write the error strings so that they do not exceed 32 bytes. For further gas savings, consider also using custom errors.
A division by 2 can be calculated by shifting one to the right. While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.
Instances include:
uint256 halfInvestment = investmentA / 2
uint256 halfInvestment = investmentA / 2
Manual Analysis
-investmentA / 2; +investmentA >> 1;
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 one bool closer to an address
Instances include:
address private _owner; @audit - slot 1 /// @dev Storage slot with the proxy admin (see TransparentUpgradeableProxy from OZ) bytes32 internal constant _ADMIN_SLOT = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); @audit - slot 2 /// @dev True if the owner is setted bool public initialized; @audit - slot 3
Manual Analysis
Place initialized
after _owner
to save one storage slot
address private _owner; @audit - slot 1 /// @dev True if the owner is setted +bool public initialized; /// @dev Storage slot with the proxy admin (see TransparentUpgradeableProxy from OZ) bytes32 internal constant _ADMIN_SLOT = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); @audit - slot 2
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. This is particularly true in for loops, as it saves some gas at each iteration.
Instances include:
i++
i++
i++
i++
i++
i++
i++
i++
i++
Manual Analysis
Place the arithmetic operations in an unchecked
block
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.
Instances include:
Manual Analysis
Replace
address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner)
with
emit OwnershipTransferred(_owner_, newOwner) _owner = newOwner;
#0 - Yashiru
2022-06-22T16:20:17Z
#1 - Yashiru
2022-06-23T09:15:06Z
Indeed, here we can do it to optimize gas consumption, but we prefer, for readability reasons, to emit the events after doing the storage update.
#2 - Yashiru
2022-06-24T12:15:15Z
Duplicated of #89 at Use Shift Right/Left instead of Division/Multiplication
#3 - Yashiru
2022-06-24T12:31:24Z
Duplicated of #75 at Using calldata instead of memory for read-only arguments in external functions saves gas.
#4 - obatirou
2022-06-24T15:21:52Z
https://github.com/code-423n4/2022-06-nested-findings/issues/62#issuecomment-1165547704
#5 - obatirou
2022-06-24T15:50:46Z
https://github.com/code-423n4/2022-06-nested-findings/issues/29#issuecomment-1165702145
#6 - maximebrugel
2022-06-24T15:50:54Z
The address _ADMIN_SLOT
is a constant, store in the bytecode. So the bool
and address
variables are already packed.
#7 - Yashiru
2022-06-24T15:54:05Z
Duplicated of #2 at For loop optimizaion
Duplicated of #2 at For loop optimizaion
#8 - Yashiru
2022-06-24T16:19:16Z
Gas optimization confirmed.