Platform: Code4rena
Start Date: 11/11/2021
Pot Size: $50,000 USDC
Total HM: 9
Participants: 27
Period: 7 days
Judge: alcueca
Total Solo HM: 5
Id: 53
League: ETH
Rank: 16/27
Findings: 2
Award: $562.85
🌟 Selected for report: 5
🚀 Solo Findings: 0
🌟 Selected for report: GiveMeTestEther
Also found by: pauliax
389.9738 USDC - $389.97
GiveMeTestEther
The storage variable totalWeights can be set 0 by onlyOwner and therefore we would have a division by zero in the function "_computeShareCount" https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L268
-Assumption we have one shareholder with weight S1 > 0 and royaltiesWeight > 0. -With the function updateShareholder the onlyOwner sets the S1 of our shareholder to 0. - updateShareholder: https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L166 - require(_totalWeights > 0, "FeeSplitter: TOTAL_WEIGHTS_ZERO") condition is met because _totalWeights is the sum of all shareholder weights + royaltiesWeight, and royaltiesWeight is > 0
Manual Analysis
At the end of the function setRoyaltiesWeight check for 0 weight with a require: require(totalWeights > 0, "FeeSplitter: TOTAL_WEIGHTS_ZERO"); https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L94
10.4335 USDC - $10.43
GiveMeTestEther
Reason strings for reverts that don't fit into 32 bytes will increase contract creation gas and and also the gas during runtime if a require condition is not met (at least one additional mstore + othe overhead).
2021-11-nested\contracts\FeeSplitter.sol: row 74: require(_msgSender() == weth, "FeeSplitter: ETH_SENDER_NOT_WETH"); 2021-11-nested\contracts\FeeSplitter.sol: row 157: require(_royaltiesTarget != address(0), "FeeSplitter: INVALID_ROYALTIES_TARGET_ADDRESS"); 2021-11-nested\contracts\FeeSplitter.sol: row 167: require(_accountIndex + 1 <= shareholders.length, "FeeSplitter: INVALID_ACCOUNT_INDEX"); 2021-11-nested\contracts\NestedAsset.sol: row 34: require(supportedFactories[_msgSender()], "NestedAsset: FORBIDDEN_NOT_FACTORY"); 2021-11-nested\contracts\NestedAsset.sol: row 40: require(_address == ownerOf(_tokenId), "NestedAsset: FORBIDDEN_NOT_OWNER"); 2021-11-nested\contracts\NestedAsset.sol: row 115: require(bytes(tokenURI(_tokenId)).length == 0, "NestedAsset: TOKEN_URI_IMMUTABLE"); 2021-11-nested\contracts\NestedAsset.sol: row 142: require(supportedFactories[_factory] == true, "NestedAsset: ALREADY_NOT_SUPPORTED"); 2021-11-nested\contracts\NestedBuybacker.sol: row 52: require(_burnPercentage <= 1000, "NestedBuybacker::constructor: Burn part to high"); 2021-11-nested\contracts\NestedBuybacker.sol: row 82: require(_burnPercentage <= 1000, "NestedBuybacker::setBurnPart: Burn part to high"); 2021-11-nested\contracts\NestedFactory.sol: row 56: require(nestedAsset.ownerOf(_nftId) == _msgSender(), "NestedFactory: Not the token owner"); 2021-11-nested\contracts\NestedFactory.sol: row 64: require(block.timestamp > nestedRecords.getLockTimestamp(_nftId), "NestedFactory: The NFT is currently locked"); 2021-11-nested\contracts\NestedFactory.sol: row 90: require(i > 0, "NestedFactory::removeOperator: Cant remove non-existent operator"); 2021-11-nested\contracts\NestedFactory.sol: row 97: require(address(reserve) == address(0), "NestedFactory::setReserve: Reserve is immutable"); 2021-11-nested\contracts\NestedFactory.sol: row 104: require(address(_feeSplitter) != address(0), "NestedFactory::setFeeSplitter: Invalid feeSplitter address"); 2021-11-nested\contracts\NestedFactory.sol: row 117: require(_orders.length > 0, "NestedFactory::create: Missing orders"); 2021-11-nested\contracts\NestedFactory.sol: row 134: require(_orders.length > 0, "NestedFactory::addTokens: Missing orders"); 2021-11-nested\contracts\NestedFactory.sol: row 150: require(_orders.length > 0, "NestedFactory::swapTokenForTokens: Missing orders"); 2021-11-nested\contracts\NestedFactory.sol: row 169: require(_orders.length > 0, "NestedFactory::sellTokensToNft: Missing orders"); 2021-11-nested\contracts\NestedFactory.sol: row 170: require(_sellTokensAmount.length == _orders.length, "NestedFactory::sellTokensToNft: Input lengths must match"); 2021-11-nested\contracts\NestedFactory.sol: row 189: require(_orders.length > 0, "NestedFactory::sellTokensToWallet: Missing orders"); 2021-11-nested\contracts\NestedFactory.sol: row 220: require(_orders.length > 0, "NestedFactory::destroy: Missing orders"); 2021-11-nested\contracts\NestedFactory.sol: row 221: require(tokens.length == _orders.length, "NestedFactory::destroy: Missing sell args"); 2021-11-nested\contracts\NestedFactory.sol: row 262: require(assetTokensLength > _tokenIndex, "NestedFactory::withdraw: Invalid token index"); 2021-11-nested\contracts\NestedFactory.sol: row 264: require(assetTokensLength > 1, "NestedFactory::withdraw: Can't withdraw the last asset"); 2021-11-nested\contracts\NestedFactory.sol: row 391: require(success, "NestedFactory::_submitOrder: Operator call failed"); 2021-11-nested\contracts\NestedFactory.sol: row 470: require(holding.amount >= _inputTokenAmount, "NestedFactory:_transferInputTokens: Insufficient amount"); 2021-11-nested\contracts\NestedFactory.sol: row 475: require(msg.value == _inputTokenAmount, "NestedFactory::_transferInputTokens: Insufficient amount in"); 2021-11-nested\contracts\NestedRecords.sol: row 171: require(_maxHoldingsCount > 0, "NestedRecords: INVALID_MAX_HOLDINGS"); 2021-11-nested\contracts\OperatorResolver.sol: row 43: require(names.length == destinations.length, "OperatorResolver::importOperators: Input lengths must match"); 2021-11-nested\contracts\libraries\OperatorHelpers.sol: row 51: require(tokens[0] == _outputToken, "OperatorHelpers::getDecodeDataAndRequire: Wrong output token"); 2021-11-nested\contracts\libraries\OperatorHelpers.sol: row 52: require(tokens[1] == _inputToken, "OperatorHelpers::getDecodeDataAndRequire: Wrong input token"); 2021-11-nested\contracts\operators\Flat\FlatOperator.sol: row 18: require(amount > 0, "FlatOperator::commitAndRevert: Amount must be greater than zero"); 2021-11-nested\contracts\operators\ZeroEx\ZeroExOperator.sol: row 38: require(success, "ZeroExOperator::commitAndRevert: 0x swap failed");
Python script
Shorten the reason strings to max 32 bytes
#0 - maximebrugel
2021-11-18T14:05:25Z
Duplicated : #14
🌟 Selected for report: GiveMeTestEther
Also found by: pants
47.7068 USDC - $47.71
GiveMeTestEther
The loop index increments can be written as unchecked { ++i } instead of simply i++ to save gas. Two reasons:
see discussion here https://github.com/ethereum/solidity/issues/10695
Visual Code Studio
replace i++ with unchecked { ++i }
#0 - adrien-supizet
2021-11-18T12:01:44Z
As already discussed here in another audit https://github.com/code-423n4/2021-10-pooltogether-findings/issues/6#issuecomment-942583925
We are not going to fix this as it reduces clarity
🌟 Selected for report: GiveMeTestEther
Also found by: WatchPug
47.7068 USDC - $47.71
GiveMeTestEther
Calculation of the weights in the function "setRoyaltiesWeight" of FeeSplitter.sol (row 95) can be done more gas efficient.
function setRoyaltiesWeight(uint256 _weight) public onlyOwner { totalWeights -= royaltiesWeight; royaltiesWeight = _weight; totalWeights += _weight; }
can be rewritten as
function setRoyaltiesWeight(uint256 _weight) public onlyOwner { totalWeights = totalWeights - royaltiesWeight + _weight; royaltiesWeight = _weight; }
=> write only once to the storage of totalWeights.
Visual Studio Code
see above
#0 - maximebrugel
2021-11-17T16:36:03Z
Before:
After:
🌟 Selected for report: GiveMeTestEther
Also found by: defsec
47.7068 USDC - $47.71
GiveMeTestEther
Functions in the same contract with the same access modifier (e.g. onlyOwner or onlyFactory) have have a mix of public and external visibility. Set their visibility to external to save gas.
Affected contracts (see ):
Visial Studio Code + Solidity Visual Developer (Plugin)
Set the visibility to external to save gas.
Extract from Solidity Visual Developer (Plugin) of the Contracts and visibility:
Contract | Type | Bases | ||
---|---|---|---|---|
└ | Function Name | Visibility | Mutability | Modifiers |
NestedRecords | Implementation | Ownable | ||
└ | <Constructor> | Public ❗️ | 🛑 | NO❗️ |
└ | createRecord | Public ❗️ | 🛑 | onlyFactory |
└ | updateHoldingAmount | Public ❗️ | 🛑 | onlyFactory |
└ | getAssetTokens | Public ❗️ | NO❗️ | |
└ | freeHolding | Public ❗️ | 🛑 | onlyFactory |
└ | store | External ❗️ | 🛑 | onlyFactory |
└ | getAssetHolding | External ❗️ | NO❗️ | |
└ | setFactory | External ❗️ | 🛑 | onlyOwner |
└ | updateLockTimestamp | External ❗️ | 🛑 | onlyFactory |
└ | setMaxHoldingsCount | External ❗️ | 🛑 | onlyOwner |
└ | getAssetReserve | External ❗️ | NO❗️ | |
└ | getAssetTokensLength | External ❗️ | NO❗️ | |
└ | getLockTimestamp | External ❗️ | NO❗️ | |
└ | setReserve | External ❗️ | 🛑 | onlyFactory |
└ | removeNFT | External ❗️ | 🛑 | onlyFactory |
└ | deleteAsset | Public ❗️ | 🛑 | onlyFactory |
└ | freeToken | Private 🔐 | 🛑 | |
NestedFactory | Implementation | INestedFactory, ReentrancyGuard, Ownable, MixinOperatorResolver, Multicall | ||
└ | <Constructor> | Public ❗️ | 🛑 | MixinOperatorResolver |
└ | <Receive Ether> | External ❗️ | 💵 | NO❗️ |
└ | resolverAddressesRequired | Public ❗️ | NO❗️ | |
└ | addOperator | External ❗️ | 🛑 | onlyOwner |
└ | removeOperator | External ❗️ | 🛑 | onlyOwner |
└ | setReserve | External ❗️ | 🛑 | onlyOwner |
└ | setFeeSplitter | External ❗️ | 🛑 | onlyOwner |
└ | create | External ❗️ | 💵 | nonReentrant |
└ | addTokens | External ❗️ | 💵 | nonReentrant onlyTokenOwner |
└ | swapTokenForTokens | External ❗️ | 🛑 | nonReentrant onlyTokenOwner isUnlocked |
└ | sellTokensToNft | External ❗️ | 🛑 | nonReentrant onlyTokenOwner isUnlocked |
└ | sellTokensToWallet | External ❗️ | 🛑 | nonReentrant onlyTokenOwner isUnlocked |
└ | destroy | External ❗️ | 🛑 | nonReentrant onlyTokenOwner isUnlocked |
└ | withdraw | External ❗️ | 🛑 | nonReentrant onlyTokenOwner isUnlocked |
└ | increaseLockTimestamp | External ❗️ | 🛑 | onlyTokenOwner |
└ | unlockTokens | External ❗️ | 🛑 | onlyOwner |
└ | _submitInOrders | Private 🔐 | 🛑 | |
└ | _submitOutOrders | Private 🔐 | 🛑 | |
└ | _submitOrder | Private 🔐 | 🛑 | |
└ | _safeSubmitOrder | Private 🔐 | 🛑 | |
└ | _transferToReserveAndStore | Private 🔐 | 🛑 | |
└ | _transferInputTokens | Private 🔐 | 🛑 | |
└ | _handleUnderSpending | Private 🔐 | 🛑 | |
└ | _transferFeeWithRoyalty | Private 🔐 | 🛑 | |
└ | _decreaseHoldingAmount | Private 🔐 | 🛑 | |
└ | _safeTransferAndUnwrap | Private 🔐 | 🛑 | |
└ | _safeTransferWithFees | Private 🔐 | 🛑 | |
└ | _calculateFees | Private 🔐 | ||
FeeSplitter | Implementation | Ownable, ReentrancyGuard | ||
└ | <Constructor> | Public ❗️ | 🛑 | NO❗️ |
└ | <Receive Ether> | External ❗️ | 💵 | NO❗️ |
└ | getAmountDue | Public ❗️ | NO❗️ | |
└ | setRoyaltiesWeight | Public ❗️ | 🛑 | onlyOwner |
└ | setShareholders | Public ❗️ | 🛑 | onlyOwner |
└ | releaseToken | Public ❗️ | 🛑 | nonReentrant |
└ | releaseTokens | External ❗️ | 🛑 | NO❗️ |
└ | releaseETH | External ❗️ | 🛑 | nonReentrant |
└ | sendFees | External ❗️ | 🛑 | nonReentrant |
└ | sendFeesWithRoyalties | External ❗️ | 🛑 | nonReentrant |
└ | updateShareholder | External ❗️ | 🛑 | onlyOwner |
└ | totalShares | External ❗️ | NO❗️ | |
└ | totalReleased | External ❗️ | NO❗️ | |
└ | shares | External ❗️ | NO❗️ | |
└ | released | External ❗️ | NO❗️ | |
└ | findShareholder | External ❗️ | NO❗️ | |
└ | _sendFees | Private 🔐 | 🛑 | |
└ | _addShares | Private 🔐 | 🛑 | |
└ | _releaseToken | Private 🔐 | 🛑 | |
└ | _addShareholder | Private 🔐 | 🛑 | |
└ | _computeShareCount | Private 🔐 | ||
NestedAsset | Implementation | ERC721Enumerable, Ownable | ||
└ | <Constructor> | Public ❗️ | 🛑 | ERC721 |
└ | tokenURI | Public ❗️ | NO❗️ | |
└ | originalOwner | Public ❗️ | NO❗️ | |
└ | mint | Public ❗️ | 🛑 | onlyFactory |
└ | mintWithMetadata | External ❗️ | 🛑 | onlyFactory |
└ | backfillTokenURI | External ❗️ | 🛑 | onlyFactory onlyTokenOwner |
└ | burn | External ❗️ | 🛑 | onlyFactory onlyTokenOwner |
└ | setFactory | External ❗️ | 🛑 | onlyOwner |
└ | removeFactory | External ❗️ | 🛑 | onlyOwner |
└ | _setTokenURI | Internal 🔒 | 🛑 |
Legend
Symbol | Meaning |
---|---|
🛑 | Function can modify state |
💵 | Function is payable |
#0 - adrien-supizet
2021-11-23T16:52:45Z
Affected contracts (see ):
NestedRecords:
createRecord
can be internal if I'm not mistaken, it does not seem used by the Factory, nor the frontendNestedFactory ☑️
FeeSplitter:
setShareholders
and setRoyaltiesWeight
can be external, and we could duplicate the code in the calls from the constructor. Unnecessary as these functions will be very rarely called.NestedAsset:
mint
can be external if we duplicate the code in mintWithMetadata
and use the same internal function _mint
#1 - maximebrugel
2021-12-20T14:42:53Z
Note :
createRecord
has been removed.setRoyaltiesWeight
& setShareholders
can't be external since they're called from the constructor.The resolution will be focus on the mint
function.
🌟 Selected for report: GiveMeTestEther
19.3212 USDC - $19.32
GiveMeTestEther
FeeSplitter.so: totalWeights is the sum of shareholder weights and royaltiesWeight, therefore a subtraction of a shareholder weight or royaltiesWeight can be done unchecked because we can't underflow and save gas.
Manual Analysis
https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L143 can be rewritten as:
function sendFees(IERC20 _token, uint256 _amount) external nonReentrant { _sendFees(_token, _amount, unchecked {totalWeights - royaltiesWeight}); }
https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L169 can be written as unchecked { _totalWeights -= shareholders[_accountIndex].weight; }
#0 - maximebrugel
2021-11-16T16:42:38Z
The recommended mitigation can't be implemented, because the unchecked
block can't be used as an expression :
Please check => https://github.com/ethereum/solidity/issues/10698
However, it can be implemented that way :
function sendFees(IERC20 _token, uint256 _amount) external nonReentrant { uint256 weights; unchecked { weights = totalWeights - royaltiesWeight; } _sendFees(_token, _amount, weights); }
Before :
After :
#1 - adrien-supizet
2021-11-17T12:56:50Z
Issue acknowledged. Still need to decide if we want to implement it, regarding the maintainability of the protocol.
#2 - maximebrugel
2021-11-22T16:30:03Z
As mentioned in #214, we can add unchecked
in :
if (_inputTokenAmounts[i] - amountSpent > 0) { _transferFeeWithRoyalty(_inputTokenAmounts[i] - amountSpent, _inputToken, _nftId); //unchecked }
#3 - maximebrugel
2021-12-02T13:49:19Z
As mentioned in #214, we can add
unchecked
in :if (_inputTokenAmounts[i] - amountSpent > 0) { _transferFeeWithRoyalty(_inputTokenAmounts[i] - amountSpent, _inputToken, _nftId); //unchecked }
Resolved by https://github.com/NestedFinance/nested-core-lego/pull/28