Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QA Report #61

Open
code423n4 opened this issue Jun 18, 2022 · 10 comments
Open

QA Report #61

code423n4 opened this issue Jun 18, 2022 · 10 comments
Assignees
Labels
bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") valid

Comments

@code423n4
Copy link
Contributor

Overview

Risk Rating Number of issues
Low Risk 6
Non-Critical Risk 4

Table of Contents

Low Risk Issues

1. Known vulnerabilities exist in currently used @openzeppelin/contracts version

As some known vulnerabilities exist in the current @openzeppelin/contracts version, consider updating package.json with at least @openzeppelin/[email protected] here:

https://github.com/code-423n4/2022-06-nested/blob/b4a153c943d54755711a2f7b80cbbf3a5bb49d76/package.json#L65

        "@openzeppelin/contracts": "^4.3.2",

While vulnerabilities are known, the current scope isn't affected (this might not hold true for the whole solution)

2. Missing address(0) checks

Consider adding an address(0) check for immutable variables:

operators/Yearn/YearnCurveVaultOperator.sol:23:    address public immutable eth;
operators/Yearn/YearnCurveVaultOperator.sol:26:    IWETH private immutable weth;
operators/Yearn/YearnCurveVaultOperator.sol:29:    Withdrawer private immutable withdrawer;
utils/NestedAssetBatcher.sol:19:    INestedAsset public immutable nestedAsset;
utils/NestedAssetBatcher.sol:20:    INestedRecords public immutable nestedRecords;
Withdrawer.sol:14:    IWETH public immutable weth;

3. OwnableProxyDelegation.initialize() is front-runnable in the solution

I suggest adding some access control or atomically initializing the contract:

https://github.com/code-423n4/2022-06-nested/blob/b4a153c943d54755711a2f7b80cbbf3a5bb49d76/contracts/abstracts/OwnableProxyDelegation.sol#L24-L32

File: OwnableProxyDelegation.sol
24:     function initialize(address ownerAddr) external {
25:         require(ownerAddr != address(0), "OPD: INVALID_ADDRESS");
26:         require(!initialized, "OPD: INITIALIZED");
27:         require(StorageSlot.getAddressSlot(_ADMIN_SLOT).value == msg.sender, "OPD: FORBIDDEN");
28: 
29:         _setOwner(ownerAddr);
30: 
31:         initialized = true;
32:     }

4. Use a constant instead of duplicating the same string

abstracts/OwnableProxyDelegation.sol:25:        require(ownerAddr != address(0), "OPD: INVALID_ADDRESS");
abstracts/OwnableProxyDelegation.sol:57:        require(newOwner != address(0), "OPD: INVALID_ADDRESS");
governance/TimelockControllerEmergency.sol:229:        require(targets.length == values.length, "TimelockController: length mismatch");
governance/TimelockControllerEmergency.sol:230:        require(targets.length == datas.length, "TimelockController: length mismatch");
governance/TimelockControllerEmergency.sol:319:        require(targets.length == values.length, "TimelockController: length mismatch");
governance/TimelockControllerEmergency.sol:320:        require(targets.length == datas.length, "TimelockController: length mismatch");
governance/TimelockControllerEmergency.sol:334:        require(isOperationReady(id), "TimelockController: operation is not ready");
governance/TimelockControllerEmergency.sol:342:        require(isOperationReady(id), "TimelockController: operation is not ready");
libraries/CurveHelpers/CurveHelpers.sol:28:        revert("CH: INVALID_INPUT_TOKEN");
libraries/CurveHelpers/CurveHelpers.sol:48:        revert("CH: INVALID_INPUT_TOKEN");
libraries/CurveHelpers/CurveHelpers.sol:68:        revert("CH: INVALID_INPUT_TOKEN");
libraries/StakingLPVaultHelpers.sol:108:        require(success, "SDCSO: CURVE_RM_LIQUIDITY_FAILED");
libraries/StakingLPVaultHelpers.sol:138:        require(success, "SDCSO: CURVE_RM_LIQUIDITY_FAILED");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:52:        require(amountToDeposit != 0, "BLVO: INVALID_AMOUNT");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:97:        require(amount != 0, "BLVO: INVALID_AMOUNT");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:54:        require(router != address(0), "BLVO: INVALID_VAULT");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:99:        require(router != address(0), "BLVO: INVALID_VAULT");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:187:        require(pair.factory() == biswapRouter.factory(), "BLVO: INVALID_VAULT");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:97:        require(amount != 0, "BLVO: INVALID_AMOUNT");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:52:        require(amountToDeposit != 0, "BLVO: INVALID_AMOUNT");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:271:        require(reserveA > 1000, "BLVO: PAIR_RESERVE_TOO_LOW");
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:272:        require(reserveB > 1000, "BLVO: PAIR_RESERVE_TOO_LOW");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:54:        require(router != address(0), "BLVO: INVALID_VAULT");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:99:        require(router != address(0), "BLVO: INVALID_VAULT");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:269:        require(reserveA > 1000, "BLVO: PAIR_RESERVE_TOO_LOW");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:270:        require(reserveB > 1000, "BLVO: PAIR_RESERVE_TOO_LOW");
operators/Beefy/BeefyVaultOperator.sol:41:        require(amount != 0, "BVO: INVALID_AMOUNT");
operators/Beefy/BeefyVaultOperator.sol:83:        require(amount != 0, "BVO: INVALID_AMOUNT");
operators/Beefy/BeefyVaultOperator.sol:96:        require(tokenAmount != 0, "BVO: INVALID_AMOUNT");
operators/Beefy/BeefyVaultOperator.sol:43:        require(address(token) != address(0), "BVO: INVALID_VAULT");
operators/Beefy/BeefyVaultOperator.sol:85:        require(address(token) != address(0), "BVO: INVALID_VAULT");
operators/Yearn/YearnCurveVaultOperator.sol:70:        require(amount != 0, "YCVO: INVALID_AMOUNT");
operators/Yearn/YearnCurveVaultOperator.sol:121:        require(amount != 0, "YCVO: INVALID_AMOUNT");
operators/Yearn/YearnCurveVaultOperator.sol:164:        require(amount != 0, "YCVO: INVALID_AMOUNT");
operators/Yearn/YearnCurveVaultOperator.sol:212:        require(amount != 0, "YCVO: INVALID_AMOUNT");
operators/Yearn/YearnCurveVaultOperator.sol:260:        require(amount != 0, "YCVO: INVALID_AMOUNT");
operators/Yearn/YearnCurveVaultOperator.sol:73:        require(pool != address(0), "YCVO: INVALID_VAULT");
operators/Yearn/YearnCurveVaultOperator.sol:123:        require(pool != address(0), "YCVO: INVALID_VAULT");
operators/Yearn/YearnCurveVaultOperator.sol:167:        require(pool != address(0), "YCVO: INVALID_VAULT");
operators/Yearn/YearnCurveVaultOperator.sol:215:        require(pool != address(0), "YCVO: INVALID_VAULT");
operators/Yearn/YearnCurveVaultOperator.sol:263:        require(pool != address(0), "YCVO: INVALID_VAULT");
NestedFactory.sol:160:        require(_entryFees != 0, "NF: ZERO_FEES");
NestedFactory.sol:168:        require(_exitFees != 0, "NF: ZERO_FEES");
NestedFactory.sol:161:        require(_entryFees <= 10000, "NF: FEES_OVERFLOW");
NestedFactory.sol:169:        require(_exitFees <= 10000, "NF: FEES_OVERFLOW");
NestedFactory.sol:191:        require(batchedOrdersLength != 0, "NF: INVALID_MULTI_ORDERS");
NestedFactory.sol:312:        require(batchedOrdersLength != 0, "NF: INVALID_MULTI_ORDERS");
NestedFactory.sol:330:        require(batchedOrdersLength != 0, "NF: INVALID_MULTI_ORDERS");
NestedFactory.sol:250:        require(_orders.length != 0, "NF: INVALID_ORDERS");
NestedFactory.sol:359:        require(batchLength != 0, "NF: INVALID_ORDERS");
NestedFactory.sol:406:        require(batchLength != 0, "NF: INVALID_ORDERS");
NestedFactory.sol:251:        require(tokensLength == _orders.length, "NF: INPUTS_LENGTH_MUST_MATCH");
NestedFactory.sol:407:        require(_batchedOrders.amounts.length == batchLength, "NF: INPUTS_LENGTH_MUST_MATCH");
NestedFactory.sol:252:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:289:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:313:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:331:        require(nestedRecords.getAssetReserve(_nftId) == address(reserve), "NF: RESERVE_MISMATCH");
NestedFactory.sol:379:        require(amountSpent <= _inputTokenAmount - feesAmount, "NF: OVERSPENT");
NestedFactory.sol:428:            require(amountSpent <= _inputTokenAmount, "NF: OVERSPENT");
NestedFactory.sol:495:            require(amounts[1] <= _amountToSpend, "NF: OVERSPENT");
OperatorResolver.sol:39:        require(namesLength == destinations.length, "OR: INPUTS_LENGTH_MUST_MATCH");
OperatorResolver.sol:57:        require(names.length == operatorsToImport.length, "OR: INPUTS_LENGTH_MUST_MATCH");

5. Funds can be locked

There isn't a withdraw mechanism and several payable methods are implemented:

  • BeefyZapBiswapLPVaultOperator.sol:
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:51:    ) external payable returns (uint256[] memory amounts, address[] memory tokens) {
  • YearnCurveVaultOperator.sol:
operators/Yearn/YearnCurveVaultOperator.sol:69:    ) external payable returns (uint256[] memory amounts, address[] memory tokens) {
operators/Yearn/YearnCurveVaultOperator.sol:120:    ) external payable returns (uint256[] memory amounts, address[] memory tokens) {
operators/Yearn/YearnCurveVaultOperator.sol:163:    ) external payable returns (uint256[] memory amounts, address[] memory tokens) {
operators/Yearn/YearnCurveVaultOperator.sol:211:    ) external payable returns (uint256[] memory amounts, address[] memory tokens) {
operators/Yearn/YearnCurveVaultOperator.sol:259:    ) external payable returns (uint256[] memory amounts, address[] memory tokens) {

6. A magic number should be documented and explained. Use a constant instead

Similar issue in the past: here

  • 1:
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:240:            1,
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:251:            1,
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:252:            1,
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:240:            1,
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:251:            1,
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:252:            1,
  • 10000:
NestedFactory.sol:378:        feesAmount = (amountSpent * entryFees) / 10000; // Entry Fees
NestedFactory.sol:443:            feesAmount = (amountBought * (_toReserve ? entryFees : exitFees)) / 10000;

I suggest using constant variables as this would make the code more maintainable and readable while costing nothing gas-wise (constants are replaced by their value at compile-time).

Non-Critical Issues

1. It's better to emit after all processing is done

contracts/governance/TimelockControllerEmergency.sol:
  374      function updateDelay(uint256 newDelay) external virtual {
  375          require(msg.sender == address(this), "TimelockController: caller must be timelock");
  376:         emit MinDelayChange(_minDelay, newDelay);
  377          _minDelay = newDelay;
  378      }

2. Typos

  • datas vs data
abstracts/MixinOperatorResolver.sol:81:    /// @dev Build the calldata (with safe datas) and call the Operator
  • setted vs set
- abstracts/OwnableProxyDelegation.sol:17:    /// @dev True if the owner is setted
+ abstracts/OwnableProxyDelegation.sol:17:    /// @dev True if the owner is set
  • liquitiy vs liquidity
libraries/StakingLPVaultHelpers.sol:21:    /// @param pool The Curve pool to add liquitiy in
libraries/StakingLPVaultHelpers.sol:52:    /// @param pool The Curve pool to add liquitiy in
libraries/StakingLPVaultHelpers.sol:85:    /// @param pool The Curve pool to remove liquitiy from
libraries/StakingLPVaultHelpers.sol:115:    /// @param pool The Curve pool to remove liquitiy from
  • WITHDRAWED vs WITHDREW or WITHDRAWN
operators/Beefy/lp/BeefyZapBiswapLPVaultOperator.sol:108:        require(vaultAmount == amount, "BLVO: INVALID_AMOUNT_WITHDRAWED");
operators/Beefy/lp/BeefyZapUniswapLPVaultOperator.sol:108:        require(vaultAmount == amount, "BLVO: INVALID_AMOUNT_WITHDRAWED");
operators/Beefy/BeefyVaultOperator.sol:95:        require(vaultAmount == amount, "BVO: INVALID_AMOUNT_WITHDRAWED");
NestedFactory.sol:51:    /// @dev Fees when funds are withdrawed
NestedFactory.sol:639:    /// @return The withdrawed amount from the reserve
  • dont vs don't
NestedFactory.sol:477:    /// @dev Call the operator to submit the order but dont stop if the call to the operator fail.
  • transfered vs transferred
NestedFactory.sol:534:    /// @return Token transfered (in case of ETH)

3. Adding a return statement when the function defines a named return variable, is redundant

While not consuming more gas with the Optimizer enabled: using both named returns and a return statement isn't necessary. Removing one of those can improve code clarity.

Affected code:

contracts/governance/TimelockControllerEmergency.sol:
  119:     function isOperation(bytes32 id) public view virtual returns (bool pending) {
  126:     function isOperationPending(bytes32 id) public view virtual returns (bool pending) {
  133:     function isOperationReady(bytes32 id) public view virtual returns (bool ready) {
  141:     function isOperationDone(bytes32 id) public view virtual returns (bool done) {
  149:     function getTimestamp(bytes32 id) public view virtual returns (uint256 timestamp) {
  158:     function getMinDelay() public view virtual returns (uint256 duration) {

contracts/libraries/CurveHelpers/CurveHelpers.sol:
  21:     ) internal view returns (uint256[2] memory amounts) {
  41:     ) internal view returns (uint256[3] memory amounts) {
  61:     ) internal view returns (uint256[4] memory amounts) {
  85:     ) internal returns (bool success) {

4. public functions not called by the contract should be declared external instead

governance/OwnerProxy.sol:16:    function execute(address _target, bytes memory _data) public payable onlyOwner returns (bytes memory response) {
@code423n4 code423n4 added bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax labels Jun 18, 2022
code423n4 added a commit that referenced this issue Jun 18, 2022
@obatirou obatirou self-assigned this Jun 21, 2022
@Yashiru
Copy link
Collaborator

Yashiru commented Jun 22, 2022

It's better to emit after all processing is done (Confirmed)

Quality assurance confirmed

@obatirou
Copy link
Collaborator

3. OwnableProxyDelegation.initialize() is front-runnable in the solution (disputed)

Already surfaced in previous audit

@obatirou
Copy link
Collaborator

5. Funds can be locked (disputed)

They are not supposed to be called outside of a delegateCall context

@obatirou
Copy link
Collaborator

obatirou commented Jun 24, 2022

1. Known vulnerabilities exist in currently used @openzeppelin/contracts version (duplicate)

Duplicate in QA report #73

@obatirou
Copy link
Collaborator

4. public functions not called by the contract should be declared external instead (duplicate)

Duplicate in QA report #76

This was referenced Jun 24, 2022
@Yashiru
Copy link
Collaborator

Yashiru commented Jun 24, 2022

6. A magic number should be documented and explained. Use a constant instead (Duplicated)

Duplicated of #76 at 5. constants should be defined rather than using magic numbers

@obatirou
Copy link
Collaborator

3. Adding a return statement when the function defines a named return variable, is redundant (confirmed)

Confirmed

Missed occurrences found in QA report #64

TimelockControllerEmergency.sol, hashOperation
TimelockControllerEmergency.sol, hashOperationBatch

@Yashiru
Copy link
Collaborator

Yashiru commented Jun 24, 2022

2. Typos (Duplicated)

Duplicated of #45 at Typos

@Yashiru Yashiru mentioned this issue Jun 24, 2022
@Yashiru
Copy link
Collaborator

Yashiru commented Jun 24, 2022

@Yashiru Yashiru added the sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") label Jun 27, 2022
@jack-the-pug
Copy link
Collaborator

L-1: Known vulnerabilities exist in currently used @openzeppelin/contracts version

Valid, upgrading is suggested.

L-2: Missing address(0) checks

Non-critical.

L-3: OwnableProxyDelegation.initialize() is front-runnable in the solution

Valid and surfaced in previous audit.

L-4: Use a constant instead of duplicating the same string

Non-critical. Prefer not making changes.

L-5: Funds can be locked

Invalid.

L-6: A magic number should be documented and explained. Use a constant instead

Valid, best practices, make changes when you see fit.

N-1: It's better to emit after all processing is done

The emit is done earlier to save gas, no?

N-2: Typos

Valid.

N-3: Adding a return statement when the function defines a named return variable, is redundant

Non-critical. Prefer not making changes.

N-4: public functions not called by the contract should be declared external instead

Valid, but no need to make changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") valid
Projects
None yet
Development

No branches or pull requests

4 participants