Connext Amarok contest - WatchPug's results

The interoperability protocol of L2 Ethereum.

General Information

Platform: Code4rena

Start Date: 08/06/2022

Pot Size: $115,000 USDC

Total HM: 26

Participants: 72

Period: 11 days

Judge: leastwood

Total Solo HM: 14

Id: 132

League: ETH

Connext

Findings Distribution

Researcher Performance

Rank: 3/72

Findings: 4

Award: $12,411.35

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: Ruhum

Also found by: 0x1f8b, WatchPug

Labels

bug
duplicate
3 (High Risk)

Awards

2338.3143 USDC - $2,338.31

External Links

Lines of code

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/PortalFacet.sol#L80-L113

Vulnerability details

  function repayAavePortal(
    address _local,
    uint256 _backingAmount,
    uint256 _feeAmount,
    uint256 _maxIn,
    bytes32 _transferId
  ) external {
    uint256 totalAmount = _backingAmount + _feeAmount; // in adopted
    uint256 routerBalance = s.routerBalances[msg.sender][_local]; // in local

    // Sanity check: has that much to spend
    if (routerBalance < _maxIn) revert PortalFacet__repayAavePortal_insufficientFunds();

    // Need to swap into adopted asset or asset that was backing the loan
    // The router will always be holding collateral in the local asset while the loaned asset
    // is the adopted asset

    // Swap for exact `totalRepayAmount` of adopted asset to repay aave
    (bool success, uint256 amountIn, address adopted) = AssetLogic.swapFromLocalAssetIfNeededForExactOut(
      _local,
      totalAmount,
      _maxIn
    );

    if (!success) revert PortalFacet__repayAavePortal_swapFailed();

    // decrement router balances
    unchecked {
      s.routerBalances[msg.sender][_local] -= amountIn;
    }

    // back loan
    _backLoan(_local, _backingAmount, _feeAmount, _transferId);
  }

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/AssetLogic.sol#L226-L250

  function swapFromLocalAssetIfNeededForExactOut(
    address _asset,
    uint256 _amount,
    uint256 _maxIn
  )
    internal
    returns (
      bool,
      uint256,
      address
    )
  {
    AppStorage storage s = LibConnextStorage.connextStorage();

    // Get the token id
    (, bytes32 id) = s.tokenRegistry.getTokenId(_asset);

    // If the adopted asset is the local asset, no need to swap
    address adopted = s.canonicalToAdopted[id];
    if (adopted == _asset) {
      return (true, _amount, _asset);
    }

    return _swapAssetOut(id, _asset, adopted, _amount, _maxIn);
  }

When the _local is canonicalToAdopted, AssetLogic.swapFromLocalAssetIfNeededForExactOut() will return the totalAmount as amountIn, and totalAmount is the total of two input values: _backingAmount + _feeAmount.

At L91, it checks for routerBalance < _maxIn, but there is no guarantee that _maxIn >= amountIn.

As a result, when repayAavePortal() is called with _backingAmount + _feeAmount > routerBalance and _local is an adopted asset, L108 will underflow, and the routerBalance can be inflated to a very high number.

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/RoutersFacet.sol#L591-L593

  // Sanity check: amount can be deducted for the router
  if (routerBalance < _amount) revert RoutersFacet__removeRouterLiquidity_insufficientFunds();

The routerBalance is used for sanity check and once it's been inflated, the router can withdraw possibly all the funds in the contract.

Recommendation

Generally, we suggest not to use unchecked in critical storage updates

    if (!success) revert PortalFacet__repayAavePortal_swapFailed();

    // decrement router balances
    s.routerBalances[msg.sender][_local] -= amountIn;

    // back loan
    _backLoan(_local, _backingAmount, _feeAmount, _transferId);
  }

#0 - ecmendenhall

2022-06-20T04:47:34Z

Duplicate of #68

#1 - LayneHaber

2022-06-21T22:27:30Z

Closing in favor of #68

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
3 (High Risk)
resolved
sponsor confirmed

Awards

8660.4234 USDC - $8,660.42

External Links

Lines of code

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/SwapUtils.sol#L1053-L1062

Vulnerability details

function withdrawAdminFees(Swap storage self, address to) internal {
  IERC20[] memory pooledTokens = self.pooledTokens;
  for (uint256 i = 0; i < pooledTokens.length; i++) {
    IERC20 token = pooledTokens[i];
    uint256 balance = self.adminFees[i];
    if (balance != 0) {
      token.safeTransfer(to, balance);
    }
  }
}

self.adminFees[i] should be reset to 0 every time it's withdrawn. Otherwise, the adminFees can be withdrawn multiple times.

The admin may just be unaware of this issue and casualty withdrawAdminFees() from time to time, and rug all the users slowly.

Recommendation

Change to:

function withdrawAdminFees(Swap storage self, address to) internal {
  IERC20[] memory pooledTokens = self.pooledTokens;
  for (uint256 i = 0; i < pooledTokens.length; i++) {
    IERC20 token = pooledTokens[i];
    uint256 balance = self.adminFees[i];
    if (balance != 0) {
      self.adminFees[i] = 0;
      token.safeTransfer(to, balance);
    }
  }
}

#1 - 0xleastwood

2022-08-05T21:42:38Z

Completely agree with the validity of this finding. Even if the admin was not malicious, the bug will still continue to withdraw additional fees which were not included as part of the swap calculations. LPs would lose considerable value as a result.

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x1f8b

Labels

bug
2 (Med Risk)
resolved
sponsor confirmed

Awards

1169.1572 USDC - $1,169.16

External Links

Lines of code

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L99-L115

Vulnerability details

For tokens with decimals larger than 18, many functions across the codebase will revert due to underflow.

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L99-L115

function getPriceFromDex(address _tokenAddress) public view returns (uint256) {
    PriceInfo storage priceInfo = priceRecords[_tokenAddress];
    if (priceInfo.active) {
      uint256 rawTokenAmount = IERC20Extended(priceInfo.token).balanceOf(priceInfo.lpToken);
      uint256 tokenDecimalDelta = 18 - uint256(IERC20Extended(priceInfo.token).decimals());
      uint256 tokenAmount = rawTokenAmount.mul(10**tokenDecimalDelta);
      uint256 rawBaseTokenAmount = IERC20Extended(priceInfo.baseToken).balanceOf(priceInfo.lpToken);
      uint256 baseTokenDecimalDelta = 18 - uint256(IERC20Extended(priceInfo.baseToken).decimals());
      uint256 baseTokenAmount = rawBaseTokenAmount.mul(10**baseTokenDecimalDelta);
      uint256 baseTokenPrice = getTokenPrice(priceInfo.baseToken);
      uint256 tokenPrice = baseTokenPrice.mul(baseTokenAmount).div(tokenAmount);

      return tokenPrice;
    } else {
      return 0;
    }
  }

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/StableSwapFacet.sol#L426

precisionMultipliers[i] = 10**uint256(SwapUtils.POOL_PRECISION_DECIMALS - decimals[i]);

Chainlink feeds' with decimals > 18 are not supported neither:

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L122-L140

function getPriceFromChainlink(address _tokenAddress) public view returns (uint256) {
    AggregatorV3Interface aggregator = aggregators[_tokenAddress];
    if (address(aggregator) != address(0)) {
      (, int256 answer, , , ) = aggregator.latestRoundData();

      // It's fine for price to be 0. We have two price feeds.
      if (answer == 0) {
        return 0;
      }

      // Extend the decimals to 1e18.
      uint256 retVal = uint256(answer);
      uint256 price = retVal.mul(10**(18 - uint256(aggregator.decimals())));

      return price;
    }

    return 0;
  }

Recommendation

Consider checking if decimals > 18 and normalize the value by div the decimals difference.

#0 - ecmendenhall

2022-06-20T14:59:35Z

Duplicate of #39

#1 - ecmendenhall

2022-06-20T15:15:49Z

I gave this a :heart: along with https://github.com/code-423n4/2022-06-connext-findings/issues/61 because these findings both identified an additional location in the StableSwap contract where the 18 decimal assumption is hardcoded.

#2 - jakekidd

2022-06-24T21:30:01Z

I gave this a ❤️ along with #61 because these findings both identified an additional location in the StableSwap contract where the 18 decimal assumption is hardcoded.

Marking as confirmed (and leaving issue open) for this reason. Would be great to merge both findings into 1 issue in the finalized audit.

#5 - 0xleastwood

2022-08-13T23:32:41Z

Marking this as the primary issue because it highlights an active part of the codebase while other issues do not. initializeSwap will not be compatible with any token with decimals greater than 18.

Chainlink's latestRoundData might return stale or incorrect results

Lines of code

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L122-L140

Vulnerability details

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L122-L140

function getPriceFromChainlink(address _tokenAddress) public view returns (uint256) {
    AggregatorV3Interface aggregator = aggregators[_tokenAddress];
    if (address(aggregator) != address(0)) {
      (, int256 answer, , , ) = aggregator.latestRoundData();

      // It's fine for price to be 0. We have two price feeds.
      if (answer == 0) {
        return 0;
      }

      // Extend the decimals to 1e18.
      uint256 retVal = uint256(answer);
      uint256 price = retVal.mul(10**(18 - uint256(aggregator.decimals())));

      return price;
    }

    return 0;
  }

On ConnextPriceOracle.sol, we are using latestRoundData, but there is no check if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation:

Recommendation

Consider adding missing checks for stale data.

For example:

(uint80 roundID, int256 answer, , uint256 timestamp, uint80 answeredInRound) = aggregator.latestRoundData();
require(answer > 0, "Chainlink price <= 0"); 
require(answeredInRound >= roundID, "Stale price");
require(timestamp != 0, "Round not complete");

#0 - ecmendenhall

2022-06-20T05:39:10Z

Duplicate of #190

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