Nested Finance contest - pauliax's results

The one-stop Defi app to build, manage and monetize your portfolio.

General Information

Platform: Code4rena

Start Date: 10/02/2022

Pot Size: $30,000 USDC

Total HM: 5

Participants: 24

Period: 3 days

Judge: harleythedog

Total Solo HM: 3

Id: 86

League: ETH

Nested Finance

Findings Distribution

Researcher Performance

Rank: 4/24

Findings: 3

Award: $2,466.86

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: IllIllI

Also found by: 0x1f8b, hyh, pauliax

Labels

duplicate
2 (Med Risk)

Awards

1024.836 USDC - $1,024.84

External Links

This issue has been created to upgrade a QA report submission to a medium severity finding. From pauliax:

function rebuildCache() in MixinOperatorResolver does not delete removed operators from operatorCache. resolverOperatorsRequired return current active operators, so it will not contain removed operators, e.g. operator was removed by calling removeOperator in the factory, then rebuildCache is called, and the cache will still contain this removed operator, and it will be possible to callOperator on this operator.

#0 - harleythedogC4

2022-03-03T01:02:58Z

And this is a duplicate of #38.

Findings Information

🌟 Selected for report: pauliax

Also found by: 0xliumin, Dravee, GreyArt, IllIllI, Omik, ShippooorDAO, WatchPug, bobi, csanuragjain, gzeon, kenzo, rfa, robee, samruna

Labels

bug
QA (Quality Assurance)
sponsor acknowledged

Awards

758.213 USDC - $758.21

External Links

  • Function releaseTokens uses weth, not eth when comparing against a native asset. if the token address is weth, it unwraps and sends the native asset to the user:
  if (address(_tokens[i]) == weth)
    IWETH(weth).withdraw(amount);
    (bool success, ) = _msgSender().call{ value: amount }("");
    require(success, "FS: ETH_TRANFER_ERROR");

Releasing weth token should be left as a valid option if the user prefers wrapped ERC20 eth, and I think for this native purpose there is a not used storage variable named ETH:

  address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

Based on my assumptions, the intention was:

  if (address(_tokens[i]) == ETH)
    ...

or if you do not want to implement this change, then at least remove this unused variable to save some gas. However, the issue is small, because the user can always retrieve weth by using another function named releaseTokensNoETH.

  • This was mentioned in the Red4Sec audit (NFSC09), but it wasn't fixed here: OwnableProxyDelegation is Context, but still uses msg.sender, not _msgSender():
  require(StorageSlot.getAddressSlot(_ADMIN_SLOT).value == msg.sender, "OFP: FORBIDDEN");
  • function rebuildCache() in MixinOperatorResolver does not delete removed operators from operatorCache. resolverOperatorsRequired return current active operators, so it will not contain removed operators, e.g. operator was removed by calling removeOperator in the factory, then rebuildCache is called, and the cache will still contain this removed operator, and it will be possible to callOperator on this operator.

  • Consider introducing an upper limit for _timestamp in updateLockTimestamp, e.g. max 1 year from current block timestamp, otherwise it may be possible to accidentally lock the token forever.

  • if removeFactory has this check:

  require(supportedFactories[_factory], "OFH: NOT_SUPPORTED"); 

then I think addFactory should have an analogous check:

  require(!supportedFactories[_factory], "OFH: ALREADY_SUPPORTED"); 
  • The revert message is a bit missleading here:
  require(assetTokensLength > 1, "NF: UNALLOWED_EMPTY_PORTFOLIO");
  • NestedFactory has a function unlockTokens that lets admin rescue any ERC20 token. Consider also adding support for rescuing the native asset (e.g. ETH).

#0 - maximebrugel

2022-02-15T14:43:54Z

« Function releaseTokens uses weth, not eth (…) » (Duplicated)

https://github.com/code-423n4/2022-02-nested-findings/issues/15

« OwnableProxyDelegation is Context, but still uses msg.sender, not _msgSender() (…) » (Acknowledged)

No meta-transaction support for this admin function.

« Function rebuildCache() in MixinOperatorResolver does not delete removed operators from operatorCache (…) » ( Duplicated)

https://github.com/code-423n4/2022-02-nested-findings/issues/18

« Consider introducing an upper limit for _timestamp in updateLockTimestamp (…) » (Acknowledged)

We are not sure about an upper limit to set.

« (…) addFactory should have an analogous check » (Disputed)

No need for a require as long as supportedFactories[_factory] = true does not disrupt the protocol state.

« The revert message is a bit misleading here » (Disputed)

I don’t really know what is misleading. You can’t withdraw the last token and keep an empty portfolio.

« adding support for rescuing the native asset » (Acknowledged)

We will fix this issue by adding a require in the receive function. Also, the user can't send more ETH than needed with the _checkMsgValue function.

#1 - harleythedogC4

2022-03-03T01:44:37Z

My personal judgements:

  1. "Function releaseTokens uses weth". This is a gas optimization. Will keep it in mind when scoring #67. For here, Invalid.
  2. "OwnableProxyDelegation is Context". Valid and very-low-critical.
  3. "Function rebuildCache() in MixinOperatorResolver does not delete removed operators from operatorCache". This has been upgraded to medium severity in #77. Will not contribute to QA score.
  4. "Consider introducing an upper limit for _timestamp in updateLockTimestamp". I think this is a good idea. Valid and low-critical.
  5. "addFactory should have an analogous check". Just a consistency suggestion, valid and non-critical.
  6. "The revert message is a bit missleading here". Warden doesn't explain enough why it is misleading. Invalid.
  7. "Consider also adding support for rescuing the native asset". Valid and low-critical.

#2 - harleythedogC4

2022-03-03T02:31:10Z

Now, here is the methodology I used for calculating a score for each QA report. I first assigned each submission to be either non-critical (1 point), very-low-critical (5 points) or low-critical (10 points), depending on how severe/useful the issue is. The score of a QA report is the sum of these points, divided by the maximum number of points achieved by a QA report. This maximum number was 26 points, achieved by https://github.com/code-423n4/2022-02-nested-findings/issues/66.

The number of points achieved by this report is 26 points. Thus the final score of this QA report is (26/26)*100 = 100.

Findings Information

Awards

683.8064 USDC - $683.81

Labels

bug
G (Gas Optimization)
sponsor confirmed

External Links

  • function transfer in NestedReserve is never used and can only be called by the factory (onlyFactory), so consider removing it because I think the factory uses a withdraw function from the Reserve.

  • Currently never used:

  function setReserve onlyFactory

You can remove it to save some gas, or leave it if it was intended for future use with other factories.

    function unlockTokens(IERC20 _token) external override onlyOwner {
        ...
        _token.safeTransfer(owner(), amount);
  • There are several functions that call _checkMsgValue. This function is quite expensive as it iterates over all the _batchedOrders and is only relevant when the inputToken is ETH. Later the callers will have to iterate over all the _batchedOrders again anyway, so I think this function should be refactored to significantly reduce gas. My suggestion: because processInputOrders and processInputAndOutputOrders both call _processInputOrders, the logic from _checkMsgValue could be moved to _processInputOrders. function create then can be refactored to re-use _processInputOrders. I see 2 discrepancies here: _fromReserve is always false when _submitInOrders is called from create (could be solved if _processInputOrders takes extra parameter), and _processInputOrders has this extra line:
  require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");

but this could be solved if you first mint the NFT and then invoke _processInputOrders from create.

  • function withdraw calls nestedRecords twice:
 uint256 assetTokensLength = nestedRecords.getAssetTokensLength(_nftId);
 ...
  address token = nestedRecords.getAssetTokens(_nftId)[_tokenIndex];

I think it could just substitute these links by first fetching all the tokens, and then calculating the length itself instead of making 2 external calls for pretty much the same data.

  • Could use 'unchecked' maths here, as underflow is not possible:
   if (_amountToSpend > amounts[1]) {
      IERC20(_inputToken).safeTransfer(_msgSender(), _amountToSpend - amounts[1]);
    }

#0 - maximebrugel

2022-02-17T14:11:42Z

"You can remove it to save some gas, or leave it if it was intended for future use with other factories" (Acknowledged)

Consider using EnumerableSet to store operators (Acknowledged)

"Could just use msg.sender and do not call an owner() function here" (Confirmed)

_checkMsgValue refactoring (Disputed)

"Could be". You need to pre-calculate the amount of ETH needed to check msg.value in a simple way.

"function withdraw calls nestedRecords twice" (Acknowledged)

"Could use 'unchecked' maths here, as underflow is not possible" (Confirmed)

#1 - harleythedogC4

2022-03-13T03:47:19Z

My personal judgments:

  1. "function transfer in NestedReserve is never used". Valid and medium-optimization.
  2. "EnumerableSet to store them". Valid and small-optimization.
  3. "Could just use msg.sender". Valid and small-optimization.
  4. "_checkMsgValue refactoring". The idea of refactoring the reserve check to be in the combined function is valid. Valid and small-optimization.
  5. "withdraw calls nestedRecords twice". Valid and small-optimization.
  6. "Could use 'unchecked' here". This was disputed in other gas reports as this already surfaced in the first contest. To be fair, this should be marked as invalid too. Invalid.

#2 - harleythedogC4

2022-03-13T03:48:57Z

Also, #66 has the additional issue: 7. "remove this unused variable to save some gas (ETH)". Valid and small-optimization.

#3 - harleythedogC4

2022-03-13T06:22:32Z

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 10 points. Thus the final score of this gas report is (10/10)*100 = 100.

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