Open code423n4 opened 2 years ago
records[_nftId].reserve
» (Acknowledged)Need to be strictly, if equal it will be exceed the limit after storing. But it could work with « + 1 ».
No unchecked blocks for admin functions.
Don’t use unchecked when smart contract state is involved.
Don’t use unchecked when smart contract state is involved.
Don’t use unchecked when smart contract state is involved.
Don’t use unchecked when smart contract state is involved.
Don’t use unchecked when smart contract state is involved.
Don’t use unchecked when smart contract state is involved.
_tokenRecords.totalShares
» (Confirmed)Better readability in a private function and it’s an admin function.
Don’t use unchecked when smart contract state is involved.
Increasing after testing.
Increasing after testing.
We don’t want an uint256 array for inputs.
No optimizations. [image:DFFADFE9-F216-4393-9AD7-7F5537B63B51-22545-0000C5BBF01A7052/8ED5AC0E-10BB-4671-ABE3-630BEAE0A364.png]
Already surfaced => unchecked { ++I } is more gas efficient than I++ for loops · Issue #25 · code-423n4/2021-11-nested-findings · GitHub
Already surfaced in first audit.
Already surfaced code-423n4/2021-11-nested-findings#14
My personal judgments:
Now, here is the methodology I used for calculating a score for each gas report. I first assigned each submission to be either small-optimization (1 point), medium-optimization (5 points) or large-optimization (10 points), depending on how useful the optimization is. The score of a gas report is the sum of these points, divided by the maximum number of points achieved by a gas report. This maximum number was 10 points, achieved by #67.
The number of points achieved by this report is 8 points. Thus the final score of this gas report is (8/10)*100 = 80.
Gas Report
Table of Contents:
records[_nftId].reserve
totalWeights
totalWeights
_tokenRecords.totalShares
BatchedInputOrders
BatchedOutputOrders
++i
costs less gas compared toi++
Foreword
Storage-reading optimizations
Unchecking arithmetics operations that can't underflow/overflow
@audit
tagsFile: NestedRecords.sol
function store()
Cache
records[_nftId].reserve
Caching this in memory can save around 1 SLOAD
Inclusive comparison
By definition,
maxHoldingsCount
is theThe maximum number of holdings for an NFT record
. Here, as an example, ifmaxHoldingsCount == 1
andrecords[_nftId].tokens.length == 1
, the function will revert. I believe this check should be inclusive (like thisrecords[_nftId].tokens.length <= maxHoldingsCount
). This is both a Low-risk issue and a gas issue as<
costs 3 more gas than<=
due to the additionalISZERO
opcode (even with the Optimizer)function deleteAsset()
Unchecked block
If
tokens.length == 1
, all assets would be deleted. Iftokens.length == 0
, line 90 would've thrown an error and trigger a revert. As it's impossible for line 95 to underflow, it should be wrapped inside anunchecked
block.File: NestedFactory.sol
function removeOperator()
Unchecked block
Line 115 can't underflow due to
operatorsLength > 0
(the for-loop wouldn't iterate otherwise). Therefore, line 115 should be wrapped inside anunchecked
block.function destroy()
Unchecked block (1)
As
buyTokenInitialBalance
is<=
to the final_buyToken.balanceOf(address(this))
, line 222 can't underflow. Therefore, line 222 should be wrapped inside anunchecked
block.Unchecked block (2)
As
amountBought -= amountFees
is equivalent toamountBought = amountBought - (amountBought / 100)
, the result can't underflow. Therefore, line 223 should be wrapped inside anunchecked
block.function _submitInOrders()
Unchecked block (1)
Line 339 can't underflow due to the require statement line 337. Therefore, line 339 should be wrapped inside an
unchecked
block.Unchecked block (2)
As
underSpentAmount = _inputTokenAmount - feesAmount - amountSpent
(line 339):_inputTokenAmount >= underSpentAmount
. Therefore, line 346 can't underflow and should be wrapped inside anunchecked
block.function _submitOutOrders()
Unchecked block (1)
Line 387 can't underflow due to the require statement line 385. Therefore, line 387 should be wrapped inside an
unchecked
block.Unchecked block (2)
As
underSpentAmount = _inputTokenAmount - amountSpent
:_inputTokenAmount >= underSpentAmount
. Therefore, line 392 can't underflow and should be wrapped inside anunchecked
block.Unchecked block (3)
As the initial
_batchedOrders.outputToken.balanceOf(address(this))
line 365 is<=
to the final_batchedOrders.outputToken.balanceOf(address(this))
line 395: line 395 can't underflow. Therefore, line 395 should be wrapped inside anunchecked
block.Unchecked block (4)
As
amountBought - feesAmount
is equivalent toamountBought - (amountBought / 100)
, the result can't underflow. Therefore, line 399 should be wrapped inside anunchecked
block.function _safeSubmitOrder()
Unchecked block
Line 446 can't underflow due to the require statement line 445. Therefore, line 446 should be wrapped inside an
unchecked
block.function _transferToReserveAndStore()
Unchecked block
As the initial
_token.balanceOf(reserveAddr)
is<=
to the final_token.balanceOf(reserveAddr)
: line 471 can't underflow. Therefore, line 471 should be wrapped inside anunchecked
block.function _transferInputTokens()
Unchecked block
As the initial
_inputToken.balanceOf(address(this))
is<=
to the final_inputToken.balanceOf(address(this))
: line 505 can't underflow. Therefore, it should be wrapped inside anunchecked
block.function _safeTransferWithFees()
Unchecked block
As
_amount - feeAmount
is equivalent to_amount - (_amount / 100)
, the result can't underflow. Therefore, line 574 should be wrapped inside anunchecked
block.File: FeeSplitter.sol
function updateShareholder()
Cache
totalWeights
It's possible to save around 1 SLOAD by caching
totalWeights
in memory, like this:function sendFees()
Unchecked block
As the initial
_token.balanceOf(address(this))
is<=
to the final_token.balanceOf(address(this))
: line 184 can't underflow. Therefore, it should be wrapped inside anunchecked
block.function sendFeesWithRoyalties()
Unchecked block
As the initial
_token.balanceOf(address(this))
is<=
to the final_token.balanceOf(address(this))
: line 200 can't underflow. Therefore, it should be wrapped inside anunchecked
block.Cache
totalWeights
Caching this in memory can save around 1 SLOAD
function getAmountDue()
Cache
_tokenRecords.totalShares
Caching this in memory can save around 1 SLOAD
function _addShareholder()
A private function used only once can get inlined
As this private function is only used once line 127 in function setShareholders(), it can get inlined to save some gas.
File: ZeroExOperator.sol
function performSwap()
Unchecked block (1)
As the initial
buyToken.balanceOf(address(this))
is<=
to the finalbuyToken.balanceOf(address(this))
: line 34 can't underflow. Therefore, it should be wrapped inside anunchecked
block.Unchecked block (2)
As the initial
sellToken.balanceOf(address(this))
is<=
to the finalsellToken.balanceOf(address(this))
: line 35 can't underflow. Therefore, it should be wrapped inside anunchecked
block.File: INestedFactory.sol
Storage
Tightly pack struct
BatchedInputOrders
struct BatchedInputOrders
can be tightly packed to save 1 storage slot by changing the code from this:to this:
Tightly pack struct
BatchedOutputOrders
struct BatchedOutputOrders
can be tightly packed to save 1 storage slot by changing the code from this:to this:
Only use 1 struct
In my opinion, the structs here are used in an unintended way: it's not up to a struct to carry the input/output concept here.
It's possible to use only 1 struct for the whole logic, as such:
And then declare input and output variables with this, like:
BatchedOrders[] _batchedInputOrders
orBatchedOrders[] _batchedOutputOrders
.Same struct, different variables.
I suggest going from:
to
General recommendations
Variables
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (
0
foruint
,false
forbool
,address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.As an example:
for (uint256 i = 0; i < numIterations; ++i) {
should be replaced withfor (uint256 i; i < numIterations; ++i) {
Instances include:
I suggest removing explicit initializations for default values.
Comparisons
Amounts should be checked for 0 before calling a transfer
Checking non-zero transfer values can avoid an expensive external call and save gas.
Places I suggest adding a non-zero-value check:
For-Loops
++i
costs less gas compared toi++
++i
costs less gas compared toi++
for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration)i++
incrementsi
and returns the initial value ofi
. 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 of2
Instances include:
I suggest using
++i
instead ofi++
to increment the value of an uint variable.Increments can be unchecked
In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
ethereum/solidity#10695
Instances include:
The code would go from:
to:
The risk of overflow is inexistant for a
uint256
here.An array's length should be cached to save gas in for-loops
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Here, I suggest storing the array's length in a variable before the for-loop, and use it instead:
Errors
Reduce the size of error messages (Long revert Strings)
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.
Revert strings > 32 bytes are here:
I suggest shortening the revert strings to fit in 32 bytes, or that using custom errors as described next.
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)
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Custom errors are defined using the
error
statement, which can be used inside and outside of contracts (including interfaces and libraries).Instances include:
I suggest replacing revert strings with custom errors.