Nibbl contest - robee's results

NFT fractionalization protocol with guaranteed liquidity and price based buyout.

General Information

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

Nibbl

Findings Distribution

Researcher Performance

Rank: 33/96

Findings: 2

Award: $47.54

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

transfer return value of a general ERC20 is ignored

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

  1. Check the transfer return value Another popular possibility is to add a whiteList. Those are the appearances (solidity file, line number, actual line):

Code instances:

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

Div by 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)

Code instances:

https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/NibblVault.sol#L183 _initialTokenSupply, _initialTokenPrice might be 0

Div by 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)

Code instances:

https://github.com/code-423n4/2022-06-nibbl/tree/main/contracts/NibblVault.sol#L183 _initialTokenSupply, _initialTokenPrice might be 0

Init frontrun

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:

Code instances:

Basket.sol, initialise, 23 NibblVault.sol, initialize, 173 UpgradedNibblVault.sol, initialize, 168

Two Steps Verification before Transferring Ownership

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

Code instances:

AccessControlMechanism.sol IAccessControlMechanism.sol

Solidity compiler versions mismatch

The project is compiled with different versions of solidity, which is not recommended because it can lead to undefined behaviors.

Code instance:

Not verified owner

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.

Code instances:

NibblVault.sol.permit owner UpgradedNibblVault.sol.permit owner

Not verified input

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.

Code instances:

Basket.sol.onERC721Received from NibblVault.sol.withdrawMultipleERC721 _to UpgradedNibblVault.sol.withdrawERC20 _to NibblVault.sol.withdrawERC1155 _asset NibblVault.sol.initialize _curator NibblVault.sol.withdrawERC721 _assetAddress

Does not validate the input fee parameter

Some fee parameters of functions are not checked for invalid values. Validate the parameters:

Code instances:

MaliciousFactory.constructor (_feeTo) NibblVaultFactory.constructor (_feeTo) MaliciousFactory.proposeNewAdminFeeAddress (_newFeeAddress) NibblVaultFactory.proposeNewAdminFee (_newFee) MaliciousFactory.proposeNewAdminFee (_newFee)

Require with empty message

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.

Code instances:

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.

Never used parameters

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.

Code instances:

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)

Check transfer receiver is not 0 to avoid burned money

Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.

Code instances:

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.

Caching array length can save gas

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++) { ... }

Code instances:

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

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:

Code instances:

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

Unnecessary index init

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:

Code instances:

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

Storage double reading. Could save SLOAD

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:

Code instances:

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

Rearrange state variables

You can change the order of the storage variables to decrease memory uses.

Code instances:

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

Short the following require messages

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:

Code instances:

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

Use != 0 instead of > 0

Using != 0 is slightly cheaper than > 0. (see https://github.com/code-423n4/2021-12-maple-findings/issues/75 for similar issue)

Code instances:

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'

Use unchecked to save gas for certain additive calculations that cannot overflow

You can use unchecked in the following calculations since there is no risk to overflow:

Code instances:

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;

Consider inline the following functions to save gas

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

Code instances

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); }

Inline one time use functions

The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.

Code instances:

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

Do not cache msg.sender

We recommend not to cache msg.sender since calling it is 2 gas while reading a variable is more.

Code instances:

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

Change transferFrom to transfer

'transferFrom(address(this), , **)' could be replaced by the following more gas efficient 'transfer(, **)' This replacement is more gas efficient and improves the code quality.

Code instances:

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");

Unnecessary array boundaries check when loading an array element twice

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:

Code instances:

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

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.

Code instances:

NibblVaultFactoryData.sol, UPDATE_TIME NibblVaultFactoryData.sol, feeToUpdateTime NibblVaultFactoryData.sol, vaultImplementation NibblVaultFactoryData.sol, pendingBasketImplementation NibblVaultFactoryData.sol, vaultUpdateTime

State variables that could be set immutable

In the following files there are state variables that could be set immutable to save gas.

Code instances:

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
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter