code-423n4 / 2022-06-nibbl-findings

1 stars 0 forks source link

Gas Optimizations #228

Open code423n4 opened 2 years ago

code423n4 commented 2 years ago

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