code-423n4 / 2022-02-nested-findings

0 stars 0 forks source link

Gas Optimizations #28

Open code423n4 opened 2 years ago

code423n4 commented 2 years ago

Gas Report

Table of Contents:

Foreword

File: NestedRecords.sol

function store()

111:     function store(
...
118:         if (amount != 0) {
119:             require(records[_nftId].reserve == _reserve, "NRC: RESERVE_MISMATCH"); //@audit records[_nftId].reserve SLOAD 1 
120:             updateHoldingAmount(_nftId, _token, amount + _amount);
121:             return;
122:         }
123:         require(records[_nftId].tokens.length < maxHoldingsCount, "NRC: TOO_MANY_TOKENS"); //@audit should be inclusive
124:         require(
125:             _reserve != address(0) && (_reserve == records[_nftId].reserve || records[_nftId].reserve == address(0)), //@audit records[_nftId].reserve SLOAD 1 & 2
126:             "NRC: INVALID_RESERVE"
127:         );
...

Cache records[_nftId].reserve

Caching this in memory can save around 1 SLOAD

Inclusive comparison

By definition, maxHoldingsCount is the The maximum number of holdings for an NFT record. Here, as an example, if maxHoldingsCount == 1 and records[_nftId].tokens.length == 1, the function will revert. I believe this check should be inclusive (like this records[_nftId].tokens.length <= maxHoldingsCount). This is both a Low-risk issue and a gas issue as < costs 3 more gas than <= due to the additional ISZERO opcode (even with the Optimizer)

function deleteAsset()

88:     function deleteAsset(uint256 _nftId, uint256 _tokenIndex) public onlyFactory {
89:         address[] storage tokens = records[_nftId].tokens;
90:         address token = tokens[_tokenIndex];
91: 
92:         require(records[_nftId].holdings[token] != 0, "NRC: HOLDING_INACTIVE");
93: 
94:         delete records[_nftId].holdings[token];
95:         tokens[_tokenIndex] = tokens[tokens.length - 1]; //@audit gas: can't underflow
96:         tokens.pop();
97:     }

Unchecked block

If tokens.length == 1, all assets would be deleted. If tokens.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 an unchecked block.

File: NestedFactory.sol

function removeOperator()

111:     function removeOperator(bytes32 operator) external override onlyOwner {
112:         uint256 operatorsLength = operators.length;
113:         for (uint256 i = 0; i < operatorsLength; i++) {
114:             if (operators[i] == operator) {
115:                 operators[i] = operators[operatorsLength - 1];  //@audit can't underflow
...

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 an unchecked block.

function destroy()

200:     function destroy(
...
211:         uint256 buyTokenInitialBalance = _buyToken.balanceOf(address(this));
212: 
213:         for (uint256 i = 0; i < tokensLength; i++) {
214:             uint256 amount = nestedRecords.getAssetHolding(_nftId, tokens[i]);
215:             reserve.withdraw(IERC20(tokens[i]), amount);
216: 
217:             _safeSubmitOrder(tokens[i], address(_buyToken), amount, _nftId, _orders[i]);
218:             nestedRecords.freeHolding(_nftId, tokens[i]);
219:         }
220: 
221:         // Amount calculation to send fees and tokens
222:         uint256 amountBought = _buyToken.balanceOf(address(this)) - buyTokenInitialBalance;//@audit can't underflow 
223:         uint256 amountFees = amountBought / 100; // 1% Fee
224:         amountBought -= amountFees; //@audit can't underflow (equivalent to "amountBought = amountBought - (amountBought / 100)")
...

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 an unchecked block.

Unchecked block (2)

As amountBought -= amountFees is equivalent to amountBought = amountBought - (amountBought / 100), the result can't underflow. Therefore, line 223 should be wrapped inside an unchecked block.

function _submitInOrders()

311:     function _submitInOrders(
...
337:         require(amountSpent <= _inputTokenAmount - feesAmount, "NF: OVERSPENT");
338: 
339:         uint256 underSpentAmount = _inputTokenAmount - feesAmount - amountSpent; //@audit can't underflow
340:         if (underSpentAmount != 0) {
341:             tokenSold.safeTransfer(_fromReserve ? address(reserve) : _msgSender(), underSpentAmount);
342:         }
343: 
344:         // If input is from the reserve, update the records
345:         if (_fromReserve) {
346:             _decreaseHoldingAmount(_nftId, address(tokenSold), _inputTokenAmount - underSpentAmount); //@audit can't underflow
347:         }
...

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 an unchecked block.

function _submitOutOrders()

357:     function _submitOutOrders(
...
365:         amountBought = _batchedOrders.outputToken.balanceOf(address(this));
...
385:             require(amountSpent <= _inputTokenAmount, "NF: OVERSPENT");
386: 
387:             uint256 underSpentAmount = _inputTokenAmount - amountSpent; //@audit can't underflow
388:             if (underSpentAmount != 0) {
389:                 _inputToken.safeTransfer(address(reserve), underSpentAmount);
390:             }
391: 
392:             _decreaseHoldingAmount(_nftId, address(_inputToken), _inputTokenAmount - underSpentAmount); //@audit can't underflow
393:         }
394: 
395:         amountBought = _batchedOrders.outputToken.balanceOf(address(this)) - amountBought; //@audit can't underflow
396:         feesAmount = amountBought / 100; // 1% Fee //@audit HIGH free stuff under 100 ? Check on Remix. That's one of Secureum's audit findings
397: 
398:         if (_toReserve) {
399:             _transferToReserveAndStore(_batchedOrders.outputToken, amountBought - feesAmount, _nftId);//@audit can't underflow
400:         }
401:     }

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 an unchecked 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 an unchecked block.

Unchecked block (4)

As amountBought - feesAmount is equivalent to amountBought - (amountBought / 100), the result can't underflow. Therefore, line 399 should be wrapped inside an unchecked block.

function _safeSubmitOrder()

435:     function _safeSubmitOrder(
...
445:             if (_amountToSpend > amounts[1]) {
446:                 IERC20(_inputToken).safeTransfer(_msgSender(), _amountToSpend - amounts[1]); //@audit should be unchecked (see L445)
447:             }
...

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()

458:     function _transferToReserveAndStore(
459:         IERC20 _token,
460:         uint256 _amount,
461:         uint256 _nftId
462:     ) private {
463:         address reserveAddr = address(reserve);
464:         uint256 balanceReserveBefore = _token.balanceOf(reserveAddr);
465: 
466:         // Send output to reserve
467:         _token.safeTransfer(reserveAddr, _amount);
468: 
469:         uint256 balanceReserveAfter = _token.balanceOf(reserveAddr);
470: 
471:         nestedRecords.store(_nftId, address(_token), balanceReserveAfter - balanceReserveBefore, reserveAddr);//@audit can't underflow
472:     }

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 an unchecked block.

function _transferInputTokens()

482:     function _transferInputTokens(
...
494:         uint256 balanceBefore = _inputToken.balanceOf(address(this));
495:         if (_fromReserve) {
496:             require(
497:                 nestedRecords.getAssetHolding(_nftId, address(_inputToken)) >= _inputTokenAmount,
498:                 "NF: INSUFFICIENT_AMOUNT_IN"
499:             );
500:             // Get input from reserve
501:             reserve.withdraw(IERC20(_inputToken), _inputTokenAmount);
502:         } else {
503:             _inputToken.safeTransferFrom(_msgSender(), address(this), _inputTokenAmount);
504:         }
505:         return (_inputToken, _inputToken.balanceOf(address(this)) - balanceBefore); //@audit can't underflow
506:     }

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 an unchecked block.

function _safeTransferWithFees()

566:     function _safeTransferWithFees(
567:         IERC20 _token,
568:         uint256 _amount,
569:         address _dest,
570:         uint256 _nftId
571:     ) private {
572:         uint256 feeAmount = _amount / 100; // 1% Fee
573:         _transferFeeWithRoyalty(feeAmount, _token, _nftId);
574:         _token.safeTransfer(_dest, _amount - feeAmount);//@audit can't underflow
575:     }

Unchecked block

As _amount - feeAmount is equivalent to _amount - (_amount / 100), the result can't underflow. Therefore, line 574 should be wrapped inside an unchecked block.

File: FeeSplitter.sol

function updateShareholder()

134:     function updateShareholder(uint256 _accountIndex, uint96 _weight) external onlyOwner {
135:         require(_accountIndex < shareholders.length, "FS: INVALID_ACCOUNT_INDEX");
136:         totalWeights = totalWeights + _weight - shareholders[_accountIndex].weight; //@audit cache
137:         require(totalWeights != 0, "FS: TOTAL_WEIGHTS_ZERO");
138:         shareholders[_accountIndex].weight = _weight;
139:         emit ShareholderUpdated(shareholders[_accountIndex].account, _weight);
140:     }

Cache totalWeights

It's possible to save around 1 SLOAD by caching totalWeights in memory, like this:

134:     function updateShareholder(uint256 _accountIndex, uint96 _weight) external onlyOwner {
135:         require(_accountIndex < shareholders.length, "FS: INVALID_ACCOUNT_INDEX");
136:         uint256 _totalWeights = totalWeights + _weight - shareholders[_accountIndex].weight;  //@audit +MSTORE
137:         require(_totalWeights != 0, "FS: TOTAL_WEIGHTS_ZERO"); //@audit +MLOAD -SLOAD
138:         totalWeights = _totalWeights; //@audit +MLOAD
139:         shareholders[_accountIndex].weight = _weight;
140:         emit ShareholderUpdated(shareholders[_accountIndex].account, _weight);
141:     }

function sendFees()

175:     function sendFees(IERC20 _token, uint256 _amount) external nonReentrant {
176:         uint256 weights;
177:         unchecked {
178:             weights = totalWeights - royaltiesWeight;
179:         }
180: 
181:         uint256 balanceBeforeTransfer = _token.balanceOf(address(this));
182:         _token.safeTransferFrom(_msgSender(), address(this), _amount); 
183: 
184:         _sendFees(_token, _token.balanceOf(address(this)) - balanceBeforeTransfer, weights); //@audit can't underflow (see L181 and L182)
185:     }

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 an unchecked block.

function sendFeesWithRoyalties()

191:     function sendFeesWithRoyalties(
...
198:         uint256 balanceBeforeTransfer = _token.balanceOf(address(this));
199:         _token.safeTransferFrom(_msgSender(), address(this), _amount);
200:         uint256 amountReceived = _token.balanceOf(address(this)) - balanceBeforeTransfer;  //@audit can't underflow
201: 
202:         uint256 royaltiesAmount = _computeShareCount(amountReceived, royaltiesWeight, totalWeights); //@audit totalWeights SLOAD 1
203: 
204:         _sendFees(_token, amountReceived, totalWeights);//@audit totalWeights SLOAD 2
...

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 an unchecked block.

Cache totalWeights

Caching this in memory can save around 1 SLOAD

function getAmountDue()

216:     function getAmountDue(address _account, IERC20 _token) public view returns (uint256) {
217:         TokenRecords storage _tokenRecords = tokenRecords[address(_token)];
218:         if (_tokenRecords.totalShares == 0) return 0;//@audit _tokenRecords.totalShares SLOAD 1
219: 
220:         uint256 totalReceived = _tokenRecords.totalReleased + _token.balanceOf(address(this));
221:         return
222:             (totalReceived * _tokenRecords.shares[_account]) /
223:             _tokenRecords.totalShares - //@audit _tokenRecords.totalShares SLOAD 2
224:             _tokenRecords.released[_account];
225:     }

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()

21:     function performSwap(
...
28:         uint256 buyBalanceBeforePurchase = buyToken.balanceOf(address(this));
29:         uint256 sellBalanceBeforePurchase = sellToken.balanceOf(address(this));
30: 
31:         bool success = ExchangeHelpers.fillQuote(sellToken, operatorStorage.swapTarget(), swapCallData);
32:         require(success, "ZEO: SWAP_FAILED");
33: 
34:         uint256 amountBought = buyToken.balanceOf(address(this)) - buyBalanceBeforePurchase; //@audit can't underflow (see L28 and L31)
35:         uint256 amountSold = sellBalanceBeforePurchase - sellToken.balanceOf(address(this));//@audit can't underflow (see L29 and L31)
36:         require(amountBought != 0, "ZeroExOperator::performSwap: amountBought cant be zero"); //@audit-info move up 1 ? Will certainly cost more gas on happy path while saving some on sad path. Not a good trade-off
37:         require(amountSold != 0, "ZeroExOperator::performSwap: amountSold cant be zero");
...

Unchecked block (1)

As the initial buyToken.balanceOf(address(this)) is <= to the final buyToken.balanceOf(address(this)): line 34 can't underflow. Therefore, it should be wrapped inside an unchecked block.

Unchecked block (2)

As the initial sellToken.balanceOf(address(this)) is <= to the final sellToken.balanceOf(address(this)): line 35 can't underflow. Therefore, it should be wrapped inside an unchecked 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:

    struct BatchedInputOrders {
        IERC20 inputToken;//@audit 20 byte
        uint256 amount; //@audit 32 byte
        Order[] orders; //@audit fully takes slots
        bool fromReserve; //@audit 1 byte
    }

to this:

    struct BatchedInputOrders {
        IERC20 inputToken;//@audit 20 byte
        bool fromReserve; //@audit 1 byte
        uint256 amount; //@audit 32 byte
        Order[] orders; //@audit fully takes slots
    }

Tightly pack struct BatchedOutputOrders

struct BatchedOutputOrders can be tightly packed to save 1 storage slot by changing the code from this:

    struct BatchedOutputOrders {
        IERC20 outputToken;//@audit 20 byte
        uint256[] amounts;//@audit 32 byte
        Order[] orders;//@audit fully takes slots
        bool toReserve;//@audit 1 byte
    }

to this:

    struct BatchedOutputOrders {
        IERC20 outputToken;//@audit 20 byte
        bool toReserve;//@audit 1 byte
        uint256[] amounts;//@audit 32 byte
        Order[] orders;//@audit fully takes slots
    }

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:

    struct BatchedOrders {
        IERC20 token;
        bool hasReserve;
        uint256[] amounts;
        Order[] orders;
    }

And then declare input and output variables with this, like: BatchedOrders[] _batchedInputOrders or BatchedOrders[] _batchedOutputOrders.

Same struct, different variables.

I suggest going from:

NestedFactory.sol:
  141:     function create(uint256 _originalTokenId, BatchedInputOrders[] calldata _batchedOrders)
  162:     function processInputOrders(uint256 _nftId, BatchedInputOrders[] calldata _batchedOrders)
  176:     function processOutputOrders(uint256 _nftId, BatchedOutputOrders[] calldata _batchedOrders)
  190:         BatchedInputOrders[] calldata _batchedInputOrders,
  191:         BatchedOutputOrders[] calldata _batchedOutputOrders
  193:         _checkMsgValue(_batchedInputOrders);
  194:         _processInputOrders(_nftId, _batchedInputOrders);
  195:         _processOutputOrders(_nftId, _batchedOutputOrders);
  268:     function _processInputOrders(uint256 _nftId, BatchedInputOrders[] calldata _batchedOrders) private {
  286:     function _processOutputOrders(uint256 _nftId, BatchedOutputOrders[] calldata _batchedOrders) private {
  313:         BatchedInputOrders calldata _batchedOrders,
  359:         BatchedOutputOrders calldata _batchedOrders,
  579:     function _checkMsgValue(BatchedInputOrders[] calldata _batchedOrders) private {

interfaces\INestedFactory.sol:
  106:     function create(uint256 _originalTokenId, BatchedInputOrders[] calldata _batchedOrders) external payable;
  111:     function processInputOrders(uint256 _nftId, BatchedInputOrders[] calldata _batchedOrders) external payable;
  116:     function processOutputOrders(uint256 _nftId, BatchedOutputOrders[] calldata _batchedOrders) external;
  120:     /// @param _batchedInputOrders The input orders to execute (first)
  121:     /// @param _batchedOutputOrders The output orders to execute (after)
  124:         BatchedInputOrders[] calldata _batchedInputOrders,
  125:         BatchedOutputOrders[] calldata _batchedOutputOrders

to

NestedFactory.sol:
  141:     function create(uint256 _originalTokenId, BatchedOrders[] calldata _batchedInputOrders)
  162:     function processInputOrders(uint256 _nftId, BatchedOrders[] calldata _batchedInputOrders)
  176:     function processOutputOrders(uint256 _nftId, BatchedOrders[] calldata _batchedOutputOrders)
  190:         BatchedOrders[] calldata _batchedInputOrders,
  191:         BatchedOrders[] calldata _batchedOutputOrders
  193:         _checkMsgValue(_batchedInputOrders);
  194:         _processInputOrders(_nftId, _batchedInputOrders);
  195:         _processOutputOrders(_nftId, _batchedOutputOrders);
  268:     function _processInputOrders(uint256 _nftId, BatchedOrders[] calldata _batchedInputOrders) private {
  286:     function _processOutputOrders(uint256 _nftId, BatchedOrders[] calldata _batchedOutputOrders) private {
  313:         BatchedOrders calldata _batchedInputOrders,
  359:         BatchedOrders calldata _batchedOutputOrders,
  579:     function _checkMsgValue(BatchedOrders[] calldata _batchedInputOrders) private {

interfaces\INestedFactory.sol:
  106:     function create(uint256 _originalTokenId, BatchedOrders[] calldata _batchedInputOrders) external payable;
  111:     function processInputOrders(uint256 _nftId, BatchedOrders[] calldata _batchedInputOrders) external payable;
  116:     function processOutputOrders(uint256 _nftId, BatchedOrders[] calldata _batchedOutputOrders) external;
  124:         BatchedOrders[] calldata _batchedInputOrders,
  125:         BatchedOrders[] calldata _batchedOutputOrders

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 for uint, false for bool, 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 with for (uint256 i; i < numIterations; ++i) {

Instances include:

abstracts\MixinOperatorResolver.sol:36:        for (uint256 i = 0; i < requiredOperators.length; i++) {
abstracts\MixinOperatorResolver.sol:55:        for (uint256 i = 0; i < requiredOperators.length; i++) {
FeeSplitter.sol:126:        for (uint256 i = 0; i < accountsLength; i++) {
FeeSplitter.sol:148:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:165:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:261:        for (uint256 i = 0; i < shareholders.length; i++) {
FeeSplitter.sol:280:        for (uint256 i = 0; i < shareholdersCache.length; i++) {
FeeSplitter.sol:318:        for (uint256 i = 0; i < shareholders.length; i++) {
NestedFactory.sol:103:        for (uint256 i = 0; i < operatorsCache.length; i++) {
NestedFactory.sol:113:        for (uint256 i = 0; i < operatorsLength; i++) {
NestedFactory.sol:153:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:213:        for (uint256 i = 0; i < tokensLength; i++) {
NestedFactory.sol:273:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:291:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:327:        for (uint256 i = 0; i < batchLength; i++) {
NestedFactory.sol:369:        for (uint256 i = 0; i < _batchedOrders.orders.length; i++) {
NestedFactory.sol:581:        for (uint256 i = 0; i < _batchedOrders.length; i++) {
NestedRecords.sol:71:            uint256 tokenIndex = 0;
NestedRecords.sol:196:        for (uint256 i = 0; i < tokensCount; i++) {
OperatorResolver.sol:40:        for (uint256 i = 0; i < namesLength; i++) {
OperatorResolver.sol:60:        for (uint256 i = 0; i < names.length; i++) {
OperatorResolver.sol:75:        for (uint256 i = 0; i < destinations.length; i++) {

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:

FeeSplitter.sol:155:                _tokens[i].safeTransfer(_msgSender(), amount);
FeeSplitter.sol:167:            _tokens[i].safeTransfer(_msgSender(), amount);
FeeSplitter.sol:182:        _token.safeTransferFrom(_msgSender(), address(this), _amount);
FeeSplitter.sol:199:        _token.safeTransferFrom(_msgSender(), address(this), _amount);
NestedFactory.sol:134:        _token.safeTransfer(owner(), amount);
NestedFactory.sol:467:        _token.safeTransfer(reserveAddr, _amount);
NestedFactory.sol:503:            _inputToken.safeTransferFrom(_msgSender(), address(this), _inputTokenAmount);
NestedFactory.sol:558:            _token.safeTransfer(_dest, _amount);
NestedFactory.sol:574:        _token.safeTransfer(_dest, _amount - feeAmount);
NestedReserve.sol:23:        _token.safeTransfer(_recipient, _amount);
NestedReserve.sol:30:        _token.safeTransfer(msg.sender, _amount);

For-Loops

++i costs less gas compared to i++

++i costs less gas compared to i++ for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration)

i++ increments i and returns the initial value of i. Which means:

uint i = 1;  
i++; // == 1 but i == 2  

But ++i returns the actual incremented value:

uint i = 1;  
++i; // == 2 and i == 2 too, so no need for a temporary variable  

In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2

Instances include:

abstracts\MixinOperatorResolver.sol:36:        for (uint256 i = 0; i < requiredOperators.length; i++) {
abstracts\MixinOperatorResolver.sol:55:        for (uint256 i = 0; i < requiredOperators.length; i++) {
FeeSplitter.sol:126:        for (uint256 i = 0; i < accountsLength; i++) {
FeeSplitter.sol:148:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:165:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:261:        for (uint256 i = 0; i < shareholders.length; i++) {
FeeSplitter.sol:280:        for (uint256 i = 0; i < shareholdersCache.length; i++) {
FeeSplitter.sol:318:        for (uint256 i = 0; i < shareholders.length; i++) {
NestedFactory.sol:103:        for (uint256 i = 0; i < operatorsCache.length; i++) {
NestedFactory.sol:113:        for (uint256 i = 0; i < operatorsLength; i++) {
NestedFactory.sol:153:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:213:        for (uint256 i = 0; i < tokensLength; i++) {
NestedFactory.sol:273:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:291:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:327:        for (uint256 i = 0; i < batchLength; i++) {
NestedFactory.sol:369:        for (uint256 i = 0; i < _batchedOrders.orders.length; i++) {
NestedFactory.sol:581:        for (uint256 i = 0; i < _batchedOrders.length; i++) {
NestedRecords.sol:78:                tokenIndex++;
NestedRecords.sol:196:        for (uint256 i = 0; i < tokensCount; i++) {
OperatorResolver.sol:40:        for (uint256 i = 0; i < namesLength; i++) {
OperatorResolver.sol:60:        for (uint256 i = 0; i < names.length; i++) {
OperatorResolver.sol:75:        for (uint256 i = 0; i < destinations.length; i++) {

I suggest using ++i instead of i++ 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:

abstracts\MixinOperatorResolver.sol:36:        for (uint256 i = 0; i < requiredOperators.length; i++) {
abstracts\MixinOperatorResolver.sol:55:        for (uint256 i = 0; i < requiredOperators.length; i++) {
FeeSplitter.sol:126:        for (uint256 i = 0; i < accountsLength; i++) {
FeeSplitter.sol:148:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:165:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:261:        for (uint256 i = 0; i < shareholders.length; i++) {
FeeSplitter.sol:280:        for (uint256 i = 0; i < shareholdersCache.length; i++) {
FeeSplitter.sol:318:        for (uint256 i = 0; i < shareholders.length; i++) {
NestedFactory.sol:103:        for (uint256 i = 0; i < operatorsCache.length; i++) {
NestedFactory.sol:113:        for (uint256 i = 0; i < operatorsLength; i++) {
NestedFactory.sol:153:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:213:        for (uint256 i = 0; i < tokensLength; i++) {
NestedFactory.sol:273:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:291:        for (uint256 i = 0; i < batchedOrdersLength; i++) {
NestedFactory.sol:327:        for (uint256 i = 0; i < batchLength; i++) {
NestedFactory.sol:369:        for (uint256 i = 0; i < _batchedOrders.orders.length; i++) {
NestedFactory.sol:581:        for (uint256 i = 0; i < _batchedOrders.length; i++) {
NestedRecords.sol:78:                tokenIndex++;
NestedRecords.sol:196:        for (uint256 i = 0; i < tokensCount; i++) {
OperatorResolver.sol:40:        for (uint256 i = 0; i < namesLength; i++) {
OperatorResolver.sol:60:        for (uint256 i = 0; i < names.length; i++) {
OperatorResolver.sol:75:        for (uint256 i = 0; i < destinations.length; i++) {

The code would go from:

for (uint256 i; i < numIterations; i++) {  
 // ...  
}  

to:

for (uint256 i; i < numIterations;) {  
 // ...  
 unchecked { ++i; }  
}  

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:

abstracts\MixinOperatorResolver.sol:36:        for (uint256 i = 0; i < requiredOperators.length; i++) {
abstracts\MixinOperatorResolver.sol:55:        for (uint256 i = 0; i < requiredOperators.length; i++) {
FeeSplitter.sol:148:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:165:        for (uint256 i = 0; i < _tokens.length; i++) {
FeeSplitter.sol:261:        for (uint256 i = 0; i < shareholders.length; i++) {
FeeSplitter.sol:280:        for (uint256 i = 0; i < shareholdersCache.length; i++) {
FeeSplitter.sol:318:        for (uint256 i = 0; i < shareholders.length; i++) {
NestedFactory.sol:103:        for (uint256 i = 0; i < operatorsCache.length; i++) {
NestedFactory.sol:369:        for (uint256 i = 0; i < _batchedOrders.orders.length; i++) {
NestedFactory.sol:581:        for (uint256 i = 0; i < _batchedOrders.length; i++) {
OperatorResolver.sol:60:        for (uint256 i = 0; i < names.length; i++) {
OperatorResolver.sol:75:        for (uint256 i = 0; i < destinations.length; i++) {

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:

abstracts\OwnableProxyDelegation.sol:56:        require(newOwner != address(0), "Ownable: new owner is the zero address");
operators\ZeroEx\ZeroExOperator.sol:36:        require(amountBought != 0, "ZeroExOperator::performSwap: amountBought cant be zero");
operators\ZeroEx\ZeroExOperator.sol:37:        require(amountSold != 0, "ZeroExOperator::performSwap: amountSold cant be zero");
NestedFactory.sol:444:            require(amounts[1] <= _amountToSpend, "NestedFactory::_safeSubmitOrder: Overspent"); 

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/:

Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.

Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).

Instances include:

abstracts\MixinOperatorResolver.sol:76:        require(_foundAddress.implementation != address(0), string(abi.encodePacked("MOR: MISSING_OPERATOR: ", name)));
abstracts\MixinOperatorResolver.sol:100:            require(tokens[0] == _outputToken, "OH: INVALID_OUTPUT_TOKEN");
abstracts\MixinOperatorResolver.sol:101:            require(tokens[1] == _inputToken, "OH: INVALID_OUTPUT_TOKEN");
abstracts\OwnableFactoryHandler.sol:21:        require(supportedFactories[msg.sender], "OFH: FORBIDDEN");
abstracts\OwnableFactoryHandler.sol:28:        require(_factory != address(0), "OFH: INVALID_ADDRESS");
abstracts\OwnableFactoryHandler.sol:36:        require(supportedFactories[_factory], "OFH: NOT_SUPPORTED");
abstracts\OwnableProxyDelegation.sol:25:        require(!initialized, "OFP: INITIALIZED");
abstracts\OwnableProxyDelegation.sol:26:        require(StorageSlot.getAddressSlot(_ADMIN_SLOT).value == msg.sender, "OFP: FORBIDDEN");
abstracts\OwnableProxyDelegation.sol:40:        require(owner() == _msgSender(), "Ownable: caller is not the owner");
abstracts\OwnableProxyDelegation.sol:56:        require(newOwner != address(0), "Ownable: new owner is the zero address");
operators\Flat\FlatOperator.sol:18:        require(amount != 0, "FO: INVALID_AMOUNT");
operators\ZeroEx\ZeroExOperator.sol:32:        require(success, "ZEO: SWAP_FAILED");
operators\ZeroEx\ZeroExOperator.sol:36:        require(amountBought != 0, "ZeroExOperator::performSwap: amountBought cant be zero");
operators\ZeroEx\ZeroExOperator.sol:37:        require(amountSold != 0, "ZeroExOperator::performSwap: amountSold cant be zero");
FeeSplitter.sol:94:        require(_weth != address(0), "FS: INVALID_ADDRESS");
FeeSplitter.sol:103:        require(msg.sender == weth, "FS: ETH_SENDER_NOT_WETH");
FeeSplitter.sol:111:        require(_weight != 0, "FS: WEIGHT_ZERO");
FeeSplitter.sol:123:        require(accountsLength != 0 && accountsLength == _weights.length, "FS: INPUTS_LENGTH_MUST_MATCH");
FeeSplitter.sol:135:        require(_accountIndex < shareholders.length, "FS: INVALID_ACCOUNT_INDEX");
FeeSplitter.sol:137:        require(totalWeights != 0, "FS: TOTAL_WEIGHTS_ZERO");
FeeSplitter.sol:153:                require(success, "FS: ETH_TRANFER_ERROR");
FeeSplitter.sol:196:        require(_royaltiesTarget != address(0), "FS: INVALID_ROYALTIES_TARGET");
FeeSplitter.sol:306:        require(amountToRelease != 0, "FS: NO_PAYMENT_DUE");
FeeSplitter.sol:316:        require(_weight != 0, "FS: ZERO_WEIGHT");
FeeSplitter.sol:317:        require(_account != address(0), "FS: INVALID_ADDRESS");
FeeSplitter.sol:319:            require(shareholders[i].account != _account, "FS: ALREADY_SHAREHOLDER");
NestedAsset.sol:34:        require(_address == ownerOf(_tokenId), "NA: FORBIDDEN_NOT_OWNER");
NestedAsset.sol:44:        require(_exists(_tokenId), "URI query for nonexistent token");
NestedAsset.sol:78:        require(_exists(_replicatedTokenId) && tokenId != _replicatedTokenId, "NA: INVALID_REPLICATED_TOKEN_ID");
NestedAsset.sol:111:        require(bytes(tokenURI(_tokenId)).length == 0, "NA: TOKEN_URI_IMMUTABLE");
NestedFactory.sol:54:        require(
NestedFactory.sol:78:        require(nestedAsset.ownerOf(_nftId) == _msgSender(), "NF: CALLER_NOT_OWNER");
NestedFactory.sol:86:        require(block.timestamp > nestedRecords.getLockTimestamp(_nftId), "NF: LOCKED_NFT");
NestedFactory.sol:101:        require(operator != bytes32(""), "NF: INVALID_OPERATOR_NAME");
NestedFactory.sol:104:            require(operatorsCache[i] != operator, "NF: EXISTENT_OPERATOR");
NestedFactory.sol:126:        require(address(_feeSplitter) != address(0), "NF: INVALID_FEE_SPLITTER_ADDRESS");
NestedFactory.sol:148:        require(batchedOrdersLength != 0, "NF: INVALID_MULTI_ORDERS");
NestedFactory.sol:207:        require(_orders.length != 0, "NF: INVALID_ORDERS");
NestedFactory.sol:208:        require(tokensLength == _orders.length, "NF: INPUTS_LENGTH_MUST_MATCH");
NestedFactory.sol:209:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:243:        require(assetTokensLength > _tokenIndex, "NF: INVALID_TOKEN_INDEX");
NestedFactory.sol:245:        require(assetTokensLength > 1, "NF: UNALLOWED_EMPTY_PORTFOLIO");
NestedFactory.sol:246:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:270:        require(batchedOrdersLength != 0, "NF: INVALID_MULTI_ORDERS");
NestedFactory.sol:271:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:288:        require(batchedOrdersLength != 0, "NF: INVALID_MULTI_ORDERS");
NestedFactory.sol:289:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:317:        require(batchLength != 0, "NF: INVALID_ORDERS");
NestedFactory.sol:337:        require(amountSpent <= _inputTokenAmount - feesAmount, "NF: OVERSPENT");
NestedFactory.sol:363:        require(batchLength != 0, "NF: INVALID_ORDERS");
NestedFactory.sol:364:        require(_batchedOrders.amounts.length == batchLength, "NF: INPUTS_LENGTH_MUST_MATCH");
NestedFactory.sol:385:            require(amountSpent <= _inputTokenAmount, "NF: OVERSPENT");
NestedFactory.sol:418:        require(success, "NF: OPERATOR_CALL_FAILED");
NestedFactory.sol:444:            require(amounts[1] <= _amountToSpend, "NestedFactory::_safeSubmitOrder: Overspent");
NestedFactory.sol:489:            require(address(this).balance >= _inputTokenAmount, "NF: INVALID_AMOUNT_IN");
NestedFactory.sol:496:            require(
NestedFactory.sol:556:            require(success, "NF: ETH_TRANSFER_ERROR");
NestedFactory.sol:586:        require(msg.value == ethNeeded, "NF: WRONG_MSG_VALUE");
NestedRecords.sol:53:        require(_maxHoldingsCount != 0, "NRC: INVALID_MAX_HOLDINGS");
NestedRecords.sol:92:        require(records[_nftId].holdings[token] != 0, "NRC: HOLDING_INACTIVE");
NestedRecords.sol:119:            require(records[_nftId].reserve == _reserve, "NRC: RESERVE_MISMATCH");
NestedRecords.sol:123:        require(records[_nftId].tokens.length < maxHoldingsCount, "NRC: TOO_MANY_TOKENS");
NestedRecords.sol:124:        require(
NestedRecords.sol:140:        require(_timestamp > records[_nftId].lockTimestamp, "NRC: LOCK_PERIOD_CANT_DECREASE");
NestedReserve.sol:22:        require(_recipient != address(0), "NRS: INVALID_ADDRESS");
OperatorResolver.sol:27:        require(_foundOperator.implementation != address(0), reason);
OperatorResolver.sol:39:        require(namesLength == destinations.length, "OR: INPUTS_LENGTH_MUST_MATCH");
OperatorResolver.sol:57:        require(names.length == operatorsToImport.length, "OR: INPUTS_LENGTH_MUST_MATCH");

I suggest replacing revert strings with custom errors.

maximebrugel commented 2 years ago

File: NestedRecords.sol

« Cache records[_nftId].reserve » (Acknowledged)

« Inclusive comparison » (Disputed)

Need to be strictly, if equal it will be exceed the limit after storing. But it could work with « + 1 ».

« function deleteAsset() »

« Unchecked block » (Acknowledged)

File: NestedFactory.sol

« removeOperator Unchecked block » (Acknowledged)

No unchecked blocks for admin functions.

« function destroy() Unchecked block (1) » (Disputed)

Don’t use unchecked when smart contract state is involved.

« function destroy() Unchecked block (2) » (Confirmed)

« function _submitInOrders() Unchecked block 1 & 2 » (Confirmed)

« function _submitOutOrders() Unchecked block 1 & 2 » (Confirmed)

« function _submitOutOrders() Unchecked block 3 » (Disputed)

Don’t use unchecked when smart contract state is involved.

« function _submitOutOrders() Unchecked block 4 » (Confirmed)

« function _safeSubmitOrder() Unchecked block » (Confirmed)

« function _transferToReserveAndStore() Unchecked block » (Disputed)

Don’t use unchecked when smart contract state is involved.

« function _transferInputTokens() Unchecked block » (Disputed)

Don’t use unchecked when smart contract state is involved.

« function _safeTransferWithFees() Unchecked block » (Confirmed)

File: FeeSplitter.sol

« function updateShareholder() : Cache `totalWeights » (Acknowledged)

« function sendFees() Unchecked block » (Disputed)

Don’t use unchecked when smart contract state is involved.

« function sendFeesWithRoyalties() Unchecked block » (Disputed)

Don’t use unchecked when smart contract state is involved.

« function sendFeesWithRoyalties() totalWeights » (Confirmed)

« function getAmountDue() Cache _tokenRecords.totalShares » (Confirmed)

« function _addShareholder() private function inlined » (Acknowledged)

Better readability in a private function and it’s an admin function.

File: ZeroExOperator.sol

« function performSwap() unchecked block 1 & 2 » (Disputed)

Don’t use unchecked when smart contract state is involved.

File: INestedFactory.sol

« Tightly pack struct BatchedInputOrders » (Disputed)

Increasing after testing.

« Tightly pack struct BatchedOutputOrders » (Disputed)

Increasing after testing.

« Only use 1 struct » (Disputed)

We don’t want an uint256 array for inputs.

General recommendations

« Amounts should be checked for 0 before calling a transfer » (Disputed)

No optimizations. [image:DFFADFE9-F216-4393-9AD7-7F5537B63B51-22545-0000C5BBF01A7052/8ED5AC0E-10BB-4671-ABE3-630BEAE0A364.png]

« ++I costs less gas compared to i++ & Increments can be unchecked » (Disputed)

Already surfaced => unchecked { ++I } is more gas efficient than I++ for loops · Issue #25 · code-423n4/2021-11-nested-findings · GitHub

« An array’s length should be cached to save gas in for-loops » (Disputed)

Already surfaced in first audit.

« Reduce the size of error messages (Long revert Strings) » (Duplicated)

« Use Custom Errors instead of Revert Strings to save Gas » ()

Already surfaced code-423n4/2021-11-nested-findings#14

harleythedogC4 commented 2 years ago

My personal judgments:

  1. "Cache records[_nftId].reserve". Valid and small-optimization.
  2. "Inclusive comparison". Already discussed in the QA report, this is invalid in both. Invalid.
  3. "Unchecked block". Unchecked blocks were disputed in other gas reports since they were already discussed in the previous contest (sponsor chose to not implement them for code clarity). This applies to all. Invalid.
  4. "Cache totalWeights". Valid and small-optimization.
  5. "Cache totalWeights". Valid and small-optimization.
  6. "Cache _tokenRecords.totalShares". Valid and small-optimization.
  7. "A private function used only once can get inlined". Valid and small-optimization.
  8. "Tightly pack struct BatchedInputOrders". I am going to disagree with the sponsor here, in theory there is an optimization here, and obviously I can't reproduce the sponsor's claim of the gas costs actually increasing. Valid and small-optimization.
  9. "Tightly pack struct BatchedOutputOrders". Valid and small-optimization.
  10. "Only use 1 struct". Will agree with sponsors dispute. Invalid.
  11. "No need to explicitly initialize variables with default values". Was raised in a similar context in previous contest. Invalid.
  12. "Amounts should be checked for 0 before calling a transfer". This increases gas in most cases. Invalid.
  13. "++i costs less gas compared to i++". Already surfaced in last contest. Invalid.
  14. "Increments can be unchecked". Already surfaced in last contest. Invalid.
  15. "An array's length should be cached to save gas in for-loops". Already surfaced in last contest. Invalid.
  16. "Reduce the size of error messages (Long revert Strings)". Valid and small-optimization.
  17. "Use Custom Errors instead of Revert Strings to save Gas". Already raised in previous contest. Invalid.
harleythedogC4 commented 2 years ago

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.