Platform: Code4rena
Start Date: 21/06/2022
Pot Size: $30,000 USDC
Total HM: 12
Participants: 96
Period: 3 days
Judge: HardlyDifficult
Total Solo HM: 5
Id: 140
League: ETH
Rank: 33/96
Findings: 2
Award: $47.54
π Selected for report: 0
π Solo Findings: 0
π Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0x52, 0xNazgul, 0xNineDec, 0xc0ffEE, 0xf15ers, 0xkatana, BowTiedWardens, Chom, ElKu, Funen, GalloDaSballo, JC, JMukesh, JohnSmith, Lambda, Limbooo, MadWookie, MiloTruck, Nethermind, Noah3o6, Nyamcil, Picodes, PwnedNoMore, Randyyy, RoiEvenHaim, SmartSek, StErMi, Tadashi, TerrierLover, TomJ, Tomio, Treasure-Seeker, UnusualTurtle, Varun_Verma, Wayne, Waze, _Adam, apostle0x01, asutorufos, berndartmueller, c3phas, catchup, cccz, cloudjunky, codexploder, cryptphi, defsec, delfin454000, dipp, ellahi, exd0tpy, fatherOfBlocks, hansfriese, hyh, joestakey, kebabsec, kenta, masterchief, minhquanym, naps62, oyc_109, pashov, peritoflores, reassor, rfa, robee, sach1r0, saian, sashik_eth, shenwilly, simon135, slywaters, sorrynotsorry, sseefried, unforgiven, xiaoming90, ych18, zuhaibmohd, zzzitron
28.2965 USDC - $28.30
Need to use safeTransfer instead of transfer. As there are popular tokens, such as USDT that transfer/trasnferFrom method doesnβt return anything. The transfer return value has to be checked (as there are some other tokens that returns false instead revert), that means you must
Basket.sol, 94 (withdrawMultipleERC20), IERC20(_tokens[i]).transfer(msg.sender, IERC20(_tokens[i]).balanceOf(address(this))); UpgradedNibblVault.sol, 470 (withdrawMultipleERC20), IERC20(_assets[i]).transfer(_to, IERC20(_assets[i]).balanceOf(address(this))); NibblVault.sol, 517 (withdrawERC20), IERC20(_asset).transfer(_to, IERC20(_asset).balanceOf(address(this))); NibblVault.sol, 526 (withdrawMultipleERC20), IERC20(_assets[i]).transfer(_to, IERC20(_assets[i]).balanceOf(address(this))); UpgradedNibblVault.sol, 462 (withdrawERC20), IERC20(_asset).transfer(_to, IERC20(_asset).balanceOf(address(this)));
Division by 0 can lead to accidentally revert, (An example of a similar issue - https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/84)
https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/NibblVault.sol#L183 _initialTokenSupply, _initialTokenPrice might be 0
Division by 0 can lead to accidentally revert, (An example of a similar issue - https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/84)
https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/NibblVault.sol#L183 _initialTokenSupply, _initialTokenPrice might be 0
Most contracts use an init pattern (instead of a constructor) to initialize contract parameters. Unless these are enforced to be atomic with contact deployment via deployment script or factory contracts, they are susceptible to front-running race conditions where an attacker/griefer can front-run (cannot access control because admin roles are not initialized) to initially with their own (malicious) parameters upon detecting (if an event is emitted) which the contract deployer has to redeploy wasting gas and risking other transactions from interacting with the attacker-initialized contract.
Many init functions do not have an explicit event emission which makes monitoring such scenarios harder. All of them have re-init checks; while many are explicit some (those in auction contracts) have implicit reinit checks in initAccessControls() which is better if converted to an explicit check in the main init function itself. (details credit to: https://github.com/code-423n4/2021-09-sushimiso-findings/issues/64) The vulnerable initialization functions in the codebase are:
Basket.sol, initialise, 23 NibblVault.sol, initialize, 173 UpgradedNibblVault.sol, initialize, 168
The following contracts have a function that allows them an admin to change it to a different address. If the admin accidentally uses an invalid address for which they do not have the private key, then the system gets locked. It is important to have two steps admin change where the first is announcing a pending new admin and the new address should then claim its ownership. A similar issue was reported in a previous contest and was assigned a severity of medium: code-423n4/2021-06-realitycards-findings#105
AccessControlMechanism.sol IAccessControlMechanism.sol
The project is compiled with different versions of solidity, which is not recommended because it can lead to undefined behaviors.
owner param should be validated to make sure the owner address is not address(0). Otherwise if not given the right input all only owner accessible functions will be unaccessible.
NibblVault.sol.permit owner UpgradedNibblVault.sol.permit owner
external / public functions parameters should be validated to make sure the address is not 0. Otherwise if not given the right input it can mistakenly lead to loss of user funds.
Basket.sol.onERC721Received from NibblVault.sol.withdrawMultipleERC721 _to UpgradedNibblVault.sol.withdrawERC20 _to NibblVault.sol.withdrawERC1155 _asset NibblVault.sol.initialize _curator NibblVault.sol.withdrawERC721 _assetAddress
Some fee parameters of functions are not checked for invalid values. Validate the parameters:
MaliciousFactory.constructor (_feeTo) NibblVaultFactory.constructor (_feeTo) MaliciousFactory.proposeNewAdminFeeAddress (_newFeeAddress) NibblVaultFactory.proposeNewAdminFee (_newFee) MaliciousFactory.proposeNewAdminFee (_newFee)
The following requires are with empty messages. This is very important to add a message for any require. So the user has enough information to know the reason of failure.
Solidity file: BancorFormula.sol, In line 259 with Empty Require message. Solidity file: TestBancorFormula.sol, In line 259 with Empty Require message. Solidity file: MaliciousFactory.sol, In line 106 with Empty Require message. Solidity file: BancorFormula.sol, In line 188 with Empty Require message. Solidity file: BancorFormula.sol, In line 356 with Empty Require message.
Those are functions and parameters pairs that the function doesn't use the parameter. In case those functions are external/public this is even worst since the user is required to put value that never used and can misslead him and waste its time.
NibblVaultFactory.sol: function createVault parameter _initialSupply isn't used. (createVault is external) NibblVaultFactory.sol: function createBasket parameter _mix isn't used. (createBasket is public) NibblVaultFactory.sol: function createVault parameter _curator isn't used. (createVault is external) MaliciousFactory.sol: function createBasket parameter _mix isn't used. (createBasket is public) NibblVault.sol: function safeTransferETH parameter _to isn't used. (safeTransferETH is private)
Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.
https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/Basket.sol#L64 https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/Basket.sol#L87 https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/Basket.sol#L37 https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/Test/UpgradedNibblVault.sol#L453
#0 - HardlyDifficult
2022-07-01T00:31:56Z
#1 - HardlyDifficult
2022-07-04T18:59:04Z
Init frontrun
Invalid, these are created by a factory.
Otherwise good improvements suggested.
π Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 8olidity, ACai, BowTiedWardens, Chandr, Chom, ElKu, Fitraldys, Funen, IgnacioB, JC, Lambda, Limbooo, MiloTruck, Noah3o6, Nyamcil, Picodes, Randyyy, SmartSek, StErMi, TerrierLover, TomJ, Tomio, UnusualTurtle, Waze, _Adam, ajtra, c3phas, cRat1st0s, catchup, codexploder, cryptphi, defsec, delfin454000, ellahi, exd0tpy, fatherOfBlocks, hansfriese, joestakey, kebabsec, kenta, m_Rassska, minhquanym, oyc_109, pashov, reassor, rfa, robee, sach1r0, saian, sashik_eth, simon135, slywaters, ych18, ynnad, zuhaibmohd
19.2386 USDC - $19.24
Caching the array length is more gas efficient. This is because access to a local variable in solidity is more efficient than query storage / calldata / memory. We recommend to change from:
for (uint256 i=0; i<array.length; i++) { ... }
to:
uint len = array.length for (uint256 i=0; i<len; i++) { ... }
NibblVault.sol, _assets, 547 Basket.sol, _tokens, 43 Basket.sol, _tokens, 70 NibblVault.sol, _assetAddresses, 506 Basket.sol, _tokens, 93 UpgradedNibblVault.sol, _assets, 469 NibblVault.sol, _assets, 525 UpgradedNibblVault.sol, _assets, 486 UpgradedNibblVault.sol, _assetAddresses, 452
Prefix increments are cheaper than postfix increments.
Further more, using unchecked {++x} is even more gas efficient, and the gas saving accumulates every iteration and can make a real change
There is no risk of overflow caused by increamenting the iteration index in for loops (the ++i
in for (uint256 i = 0; i < numIterations; ++i)
).
But increments perform overflow checks that are not necessary in this case.
These functions use not using prefix increments (++x
) or not using the unchecked keyword:
change to prefix increment and unchecked: Basket.sol, i, 93 change to prefix increment and unchecked: NibblVault.sol, i, 547 change to prefix increment and unchecked: UpgradedNibblVault.sol, i, 469 change to prefix increment and unchecked: UpgradedNibblVault.sol, i, 486 change to prefix increment and unchecked: Basket.sol, i, 70 change to prefix increment and unchecked: Basket.sol, i, 43 change to prefix increment and unchecked: NibblVault.sol, i, 525 change to prefix increment and unchecked: NibblVault.sol, i, 506 change to prefix increment and unchecked: UpgradedNibblVault.sol, i, 452
In for loops you initialize the index to start from 0, but it already initialized to 0 in default and this assignment cost gas. It is more clear and gas efficient to declare without assigning 0 and will have the same meaning:
Basket.sol, 93 Basket.sol, 43 Basket.sol, 70 NibblVault.sol, 506 NibblVault.sol, 547 UpgradedNibblVault.sol, 486 NibblVault.sol, 525 UpgradedNibblVault.sol, 469 UpgradedNibblVault.sol, 452
Reading a storage variable is gas costly (SLOAD). In cases of multiple read of a storage variable in the same scope, caching the first read (i.e saving as a local variable) can save gas and decrease the overall gas uses. The following is a list of functions and the storage variables that you read twice:
UpgradedNibblVault.sol: SCALE is read twice in getCurrentValuation NibblVault.sol: SCALE is read twice in _chargeFeeSecondaryCurve UpgradedNibblVault.sol: primaryReserveRatio is read twice in initialize NibblVault.sol: SCALE is read twice in _chargeFee NibblVault.sol: SCALE is read twice in getCurrentValuation NibblVault.sol: minBuyoutTime is read twice in initiateBuyout UpgradedNibblVault.sol: SCALE is read twice in _chargeFee
You can change the order of the storage variables to decrease memory uses.
In UpgradedNibblVault.sol,rearranging the storage fields can optimize to: 27 slots from: 28 slots. The new order of types (you choose the actual variables): 1. uint256 2. uint256 3. uint256 4. uint256 5. uint256 6. bytes32 7. uint256 8. uint256 9. uint256 10. uint256 11. uint256 12. uint256 13. uint256 14. uint256 15. uint256 16. uint256 17. uint256 18. uint256 19. uint256 20. uint256 21. Status 22. uint 23. uint256 24. address 25. uint32 26. uint32 27. address 28. address 29. address
In NibblVault.sol,rearranging the storage fields can optimize to: 28 slots from: 29 slots. The new order of types (you choose the actual variables): 1. uint256 2. uint256 3. uint256 4. uint256 5. uint256 6. uint256 7. uint256 8. bytes32 9. uint256 10. uint256 11. uint256 12. uint256 13. uint256 14. uint256 15. uint256 16. uint256 17. uint256 18. uint256 19. uint256 20. uint256 21. uint256 22. uint256 23. Status 24. uint256 25. address 26. uint32 27. uint32 28. address 29. address 30. address
The following require messages are of length more than 32 and we think are short enough to short them into exactly 32 characters such that it will be placed in one slot of memory and the require function will cost less gas. The list:
Solidity file: NibblVaultFactory.sol, In line 49, Require message length to shorten: 33, The message: NibblVaultFactory: Invalid sender Solidity file: MaliciousFactory.sol, In line 46, Require message length to shorten: 33, The message: NibblVaultFactory: Invalid sender
Using != 0 is slightly cheaper than > 0. (see https://github.com/code-423n4/2021-12-maple-findings/issues/75 for similar issue)
BancorFormula.sol, 188: change '_connectorBalance > 0' to '_connectorBalance != 0' MaliciousFactory.sol, 45: change 'balance > 0' to 'balance != 0' TestBancorFormula.sol, 259: change '_toConnectorBalance > 0' to '_toConnectorBalance != 0' BancorFormula.sol, 219: change '_supply > 0' to '_supply != 0' NibblVaultFactory.sol, 48: change 'balance > 0' to 'balance != 0' BancorFormula.sol, 219: change '_connectorWeight > 0' to '_connectorWeight != 0' TestBancorFormula.sol, 191: change '_supply > 0' to '_supply != 0' TestBancorFormula.sol, 222: change '_supply > 0' to '_supply != 0' BancorFormula.sol, 188: change '_supply > 0' to '_supply != 0' TestBancorFormula.sol, 222: change '_connectorBalance > 0' to '_connectorBalance != 0' TestBancorFormula.sol, 222: change '_connectorWeight > 0' to '_connectorWeight != 0' TestBancorFormula.sol, 259: change '_toConnectorWeight > 0' to '_toConnectorWeight != 0' BancorFormula.sol, 219: change '_connectorBalance > 0' to '_connectorBalance != 0' BancorFormula.sol, 188: change '_connectorWeight > 0' to '_connectorWeight != 0' TestBancorFormula.sol, 259: change '_fromConnectorWeight > 0' to '_fromConnectorWeight != 0' TestBancorFormula.sol, 191: change '_connectorBalance > 0' to '_connectorBalance != 0' TestBancorFormula.sol, 259: change '_fromConnectorBalance > 0' to '_fromConnectorBalance != 0' TestBancorFormula.sol, 191: change '_connectorWeight > 0' to '_connectorWeight != 0'
You can use unchecked in the following calculations since there is no risk to overflow:
NibblVaultFactory.sol (L#125) - feeToUpdateTime = block.timestamp + UPDATE_TIME; MaliciousFactory.sol (L#134) - feeAdminUpdateTime = block.timestamp + UPDATE_TIME; NibblVaultFactory.sol (L#160) - vaultUpdateTime = block.timestamp + UPDATE_TIME; UpgradedNibblVault.sol (L#377) - buyoutEndTime = block.timestamp + BUYOUT_DURATION; MaliciousFactory.sol (L#117) - feeToUpdateTime = block.timestamp + UPDATE_TIME; MaliciousFactory.sol (L#93) - vaultUpdateTime = block.timestamp + UPDATE_TIME; NibblVaultFactory.sol (L#143) - feeAdminUpdateTime = block.timestamp + UPDATE_TIME; MaliciousFactory.sol (L#150) - vaultUpdateTime = block.timestamp + UPDATE_TIME; NibblVault.sol (L#411) - buyoutEndTime = block.timestamp + BUYOUT_DURATION; NibblVaultFactory.sol (L#101) - basketUpdateTime = block.timestamp + UPDATE_TIME;
You can inline the following functions instead of writing a specific function to save gas. (see https://github.com/code-423n4/2021-11-nested-findings/issues/167 for a similar issue.)
NibblTestNFT.sol, _baseURI, { return "https://ipfs.io/ipfs/"; } UpgradedNibblVault.sol, getMaxSecondaryCurveBalance, { return ((secondaryReserveRatio * initialTokenSupply * initialTokenPrice) / (1e18 * SCALE)); } NibblVault.sol, getMaxSecondaryCurveBalance, { return ((secondaryReserveRatio * initialTokenSupply * initialTokenPrice) / (1e18 * SCALE)); } NibblVault.sol, getCurrentValuation, { return totalSupply() < initialTokenSupply ? (secondaryReserveBalance * SCALE /secondaryReserveRatio) : ((primaryReserveBalance) * SCALE / primaryReserveRatio); } UpgradedNibblVault.sol, getCurrentValuation, { return totalSupply() < initialTokenSupply ? (secondaryReserveBalance * SCALE /secondaryReserveRatio) : ((primaryReserveBalance) * SCALE / primaryReserveRatio); }
The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.
NibblTestNFT.sol, _burn NibblVault.sol, getMaxSecondaryCurveBalance TestBancorFormula.sol, generalLog BancorFormula.sol, floorLog2 TestBancorFormula.sol, findPositionInMaxExpArray TestBancorFormula.sol, generalExp NibblTestNFT.sol, _beforeTokenTransfer BancorFormula.sol, optimalLog TestBancorFormula.sol, floorLog2 TestBancorFormula.sol, optimalExp EIP712Base.sol, getChainID BancorFormula.sol, generalExp BancorFormula.sol, generalLog BancorFormula.sol, findPositionInMaxExpArray TestBancorFormula.sol, optimalLog UpgradedNibblVault.sol, getMaxSecondaryCurveBalance BancorFormula.sol, optimalExp
We recommend not to cache msg.sender since calling it is 2 gas while reading a variable is more.
https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/Test/UpgradedNibblVault.sol#L372 https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/NibblVault.sol#L406
'transferFrom(address(this), , **)' could be replaced by the following more gas efficient 'transfer(, **)' This replacement is more gas efficient and improves the code quality.
UpgradedNibblVault.sol, 488 : IERC1155(_assets[i]).safeTransferFrom(address(this), _to, _assetIDs[i], balance, "0"); UpgradedNibblVault.sol, 446 : IERC721(_assetAddress).safeTransferFrom(address(this), _to, _assetID); UpgradedNibblVault.sol, 453 : IERC721(_assetAddresses[i]).safeTransferFrom(address(this), _to, _assetIDs[i]); Basket.sol, 37 : IERC721(_token).safeTransferFrom(address(this), _to, _tokenId); UpgradedNibblVault.sol, 480 : IERC1155(_asset).safeTransferFrom(address(this), _to, _assetID, balance, "0"); NibblVault.sol, 507 : IERC721(_assetAddresses[i]).safeTransferFrom(address(this), _to, _assetIDs[i]); NibblVault.sol, 497 : IERC721(_assetAddress).safeTransferFrom(address(this), _to, _assetID); Basket.sol, 44 : IERC721(_tokens[i]).safeTransferFrom(address(this), _to, _tokenId[i]); NibblVault.sol, 549 : IERC1155(_assets[i]).safeTransferFrom(address(this), _to, _assetIDs[i], balance, "0"); Basket.sol, 64 : IERC1155(_token).safeTransferFrom(address(this), _to, _tokenId, _balance, "0"); Basket.sol, 54 : IERC721(_token).transferFrom(address(this), _to, _tokenId); Basket.sol, 72 : IERC1155(_tokens[i]).safeTransferFrom(address(this), _to, _tokenIds[i], _balance, "0"); NibblVault.sol, 538 : IERC1155(_asset).safeTransferFrom(address(this), _to, _assetID, balance, "0");
There are places in the code (especially in for-each loops) that loads the same array element more than once. In such cases, only one array boundaries check should take place, and the rest are unnecessary. Therefore, this array element should be cached in a local variable and then be loaded again using this local variable, skipping the redundant second array boundaries check:
Basket.sol.withdrawMultipleERC20 - double load of _tokens[i] NibblVault.sol.withdrawMultipleERC20 - double load of _assets[i] UpgradedNibblVault.sol.withdrawMultipleERC20 - double load of _assets[i]
Unused state variables are gas consuming at deployment (since they are located in storage) and are a bad code practice. Removing those variables will decrease deployment gas cost and improve code quality. This is a full list of all the unused storage variables we found in your code base.
NibblVaultFactoryData.sol, UPDATE_TIME NibblVaultFactoryData.sol, feeToUpdateTime NibblVaultFactoryData.sol, vaultImplementation NibblVaultFactoryData.sol, pendingBasketImplementation NibblVaultFactoryData.sol, vaultUpdateTime
In the following files there are state variables that could be set immutable to save gas.
initialTokenPrice in NibblVault.sol minBuyoutTime in UpgradedNibblVault.sol curatorFee in NibblVault.sol fictitiousPrimaryReserveBalance in UpgradedNibblVault.sol minBuyoutTime in NibblVault.sol initialTokenPrice in UpgradedNibblVault.sol curatorFee in UpgradedNibblVault.sol fictitiousPrimaryReserveBalance in NibblVault.sol