Ondo Finance - HChang26's results

Institutional-Grade Finance, Now Onchain.

General Information

Platform: Code4rena

Start Date: 29/03/2024

Pot Size: $36,500 USDC

Total HM: 5

Participants: 72

Period: 5 days

Judge: 3docSec

Total Solo HM: 1

Id: 357

League: ETH

Ondo Finance

Findings Distribution

Researcher Performance

Rank: 5/72

Findings: 2

Award: $3,036.83

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Breeje

Also found by: Arz, HChang26, immeas

Labels

bug
3 (High Risk)
satisfactory
:robot:_59_group
duplicate-278

Awards

3028.5531 USDC - $3,028.55

External Links

Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L278 https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L388 https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L479

Vulnerability details

Impact

OUSGInstantManager.sol operates under the assumption that the value of USDC is always precisely $1. This creates an opportunity for attackers to exploit fluctuations in the value of USDC, leading to potential instant profits or immediate losses for users.

Proof of Concept

Users have the ability to mint() OUSG by exchanging USDC, as well as to redeem() OUSG for USDC. Both functions rely on getOUSGPrice() to fetch the price feed for the SHV ETF, which is used to calculate the exchange rate between USDC and OUSG. It's important to note that the price feed returns the SHV/USD rate.

  function getOUSGPrice() public view returns (uint256 price) {
    (price, ) = oracle.getPriceData();
    require(
      price > MINIMUM_OUSG_PRICE,
      "OUSGInstantManager::getOUSGPrice: Price unexpectedly low"
    );
  }

OUSGInstantManager.sol incorrectly assumes that the value of USDC remains constant at 1 dollar. Despite USDC being a stablecoin, its value rarely matches exactly 1 dollar, with minor fluctuations being common. Throughout the lifespan of USDC, its value has fluctuated by as much as 1.0341 and 0.9701, representing an approximate deviation of ±3%.

Attackers can exploit fluctuations in the value of USDC when it falls below $1:

  1. Assume the value of USDC is $0.97.
  2. SHV/USD price feed indicates $110.
  3. The attacker mint() OUSG by exchanging 250,000 USDC.
  4. Based on the current implementation, the attacker would receive 250,000/110 = 2272.72 OUSG.
  5. However, the actual amount should have been (250,000 * 0.97) / 110 = 2204.54.
  6. The attacker receives an additional 68.17 OUSG.

Conversely, users may incur immediate losses in mint() when the value of USDC exceeds $1:

  1. Assume the value of USDC is $1.03.
  2. SHV/USD price feed indicates $110.
  3. The user mint() OUSG by exchanging 250,000 USDC.
  4. Based on the current implementation, the user would receive 250,000/110 = 2272.72 OUSG.
  5. However, the actual amount should have been (250,000 * 1.03) / 110 = 2204.54 = 2340.90.
  6. The user receives 68.18 less OUSG.

Similar vulnerabilities apply to the redeem() function. Attackers can easily exploit this assumption by flashloaning a large amount of USDC to mint() and then exchanging OUSG in an external liquidity pool for immediate profit.

While profits from this attack may be minimal when the value of USDC remains close to $1, in a black swan event where the value deviates by 2-3%, the potential for profit and loss is significantly magnified.

Tools Used

Manual Review

Consider using price feed for USDC rather than assuming it's always $1.

Assessed type

MEV

#0 - c4-pre-sort

2024-04-04T04:22:13Z

0xRobocop marked the issue as duplicate of #297

#1 - c4-judge

2024-04-09T10:04:13Z

3docSec marked the issue as satisfactory

Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L278 https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L388 https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/InstantMintTimeBasedRateLimiter.sol#L93 https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/InstantMintTimeBasedRateLimiter.sol#L120

Vulnerability details

Impact

_checkAndUpdateInstantMintLimit() and _checkAndUpdateInstantRedemptionLimit() include fees, which could lead to a potential reduction in allowable amounts by up to 1.99% in each window.

Proof of Concept

Users have the ability to instant mint() and redeem() OUSG in exchange for USDC. _checkAndUpdateInstantMintLimit() and _checkAndUpdateInstantRedemptionLimit() are responsible for limiting the amount permitted in the current window.

  function _checkAndUpdateInstantMintLimit(uint256 amount) internal {
    require(amount > 0, "RateLimit: mint amount can't be zero");

    if (
      block.timestamp >= lastResetInstantMintTime + resetInstantMintDuration
    ) {
      // time has passed, reset
      currentInstantMintAmount = 0;
      lastResetInstantMintTime = block.timestamp;
    }
    require(
      amount <= instantMintLimit - currentInstantMintAmount,
      "RateLimit: Mint exceeds rate limit"
    );

    currentInstantMintAmount += amount;
  }
  function _checkAndUpdateInstantRedemptionLimit(uint256 amount) internal {
    require(amount > 0, "RateLimit: redemption amount can't be zero");

    if (
      block.timestamp >=
      lastResetInstantRedemptionTime + resetInstantRedemptionDuration
    ) {
      // time has passed, reset
      currentInstantRedemptionAmount = 0;
      lastResetInstantRedemptionTime = block.timestamp;
    }
    require(
      amount <= instantRedemptionLimit - currentInstantRedemptionAmount,
      "RateLimit: Redemption exceeds rate limit"
    );
    currentInstantRedemptionAmount += amount;
  }

In both _mint() and _redeem(), the protocol has the option to impose fees. These fees are deducted from usdcAmountIn and usdcAmountToRedeem respectively in each function. However, these fees are factored into _checkAndUpdateInstantMintLimit() and _checkAndUpdateInstantRedemptionLimit(), potentially resulting in a reduction of the allowable amount in each window by up to 1.99%.

Tools Used

Manual Review

  function _mint(
    uint256 usdcAmountIn,
    address to
  ) internal returns (uint256 ousgAmountOut) {
    require(
      IERC20Metadata(address(usdc)).decimals() == 6,
      "OUSGInstantManager::_mint: USDC decimals must be 6"
    );
    require(
      usdcAmountIn >= minimumDepositAmount,
      "OUSGInstantManager::_mint: Deposit amount too small"
    );
-   _checkAndUpdateInstantMintLimit(usdcAmountIn);
    if (address(investorBasedRateLimiter) != address(0)) {
      investorBasedRateLimiter.checkAndUpdateMintLimit(
        msg.sender,
        usdcAmountIn
      );
    }

    require(
      usdc.allowance(msg.sender, address(this)) >= usdcAmountIn,
      "OUSGInstantManager::_mint: Allowance must be given to OUSGInstantManager"
    );

    uint256 usdcfees = _getInstantMintFees(usdcAmountIn);
    uint256 usdcAmountAfterFee = usdcAmountIn - usdcfees;
+   _checkAndUpdateInstantMintLimit(usdcAmountAfterFee);

    uint256 ousgPrice = getOUSGPrice(); 
    ousgAmountOut = _getMintAmount(usdcAmountAfterFee, ousgPrice);

    require(
      ousgAmountOut > 0,
      "OUSGInstantManager::_mint: net mint amount can't be zero"
    );

    if (usdcfees > 0) {
      usdc.transferFrom(msg.sender, feeReceiver, usdcfees);
    }
    usdc.transferFrom(msg.sender, usdcReceiver, usdcAmountAfterFee);

    emit MintFeesDeducted(msg.sender, feeReceiver, usdcfees, usdcAmountIn);

    ousg.mint(to, ousgAmountOut);
  }
  function _redeem(
    uint256 ousgAmountIn
  ) internal returns (uint256 usdcAmountOut) {
    require(
      IERC20Metadata(address(usdc)).decimals() == 6,
      "OUSGInstantManager::_redeem: USDC decimals must be 6"
    );
    require(
      IERC20Metadata(address(buidl)).decimals() == 6,
      "OUSGInstantManager::_redeem: BUIDL decimals must be 6"
    );
    uint256 ousgPrice = getOUSGPrice();
    uint256 usdcAmountToRedeem = _getRedemptionAmount(ousgAmountIn, ousgPrice);

    require(
      usdcAmountToRedeem >= minimumRedemptionAmount,
      "OUSGInstantManager::_redeem: Redemption amount too small"
    );
-   _checkAndUpdateInstantRedemptionLimit(usdcAmountToRedeem);

    if (address(investorBasedRateLimiter) != address(0)) {
      investorBasedRateLimiter.checkAndUpdateRedeemLimit(
        msg.sender,
        usdcAmountToRedeem
      );
    }

    uint256 usdcFees = _getInstantRedemptionFees(usdcAmountToRedeem);
    usdcAmountOut = usdcAmountToRedeem - usdcFees;
+   _checkAndUpdateInstantRedemptionLimit(usdcAmountOut);
    require(
      usdcAmountOut > 0,
      "OUSGInstantManager::_redeem: redeem amount can't be zero"
    );

    ousg.burn(ousgAmountIn);

    uint256 usdcBalance = usdc.balanceOf(address(this));

    if (usdcAmountToRedeem >= minBUIDLRedeemAmount) {
      _redeemBUIDL(usdcAmountToRedeem);
    } else if (usdcAmountToRedeem > usdcBalance) {
      _redeemBUIDL(minBUIDLRedeemAmount);
      emit MinimumBUIDLRedemption(
        msg.sender,
        minBUIDLRedeemAmount,
        usdcBalance + minBUIDLRedeemAmount - usdcAmountToRedeem
      );
    } else {
      emit BUIDLRedemptionSkipped(
        msg.sender,
        usdcAmountToRedeem,
        usdcBalance - usdcAmountToRedeem
      );
    }

    if (usdcFees > 0) {
      usdc.transfer(feeReceiver, usdcFees);
    }
    emit RedeemFeesDeducted(msg.sender, feeReceiver, usdcFees, usdcAmountOut);

    usdc.transfer(msg.sender, usdcAmountOut);
  }

Assessed type

Context

#0 - c4-pre-sort

2024-04-04T05:19:09Z

0xRobocop marked the issue as duplicate of #47

#1 - c4-judge

2024-04-09T09:53:14Z

3docSec changed the severity to QA (Quality Assurance)

#2 - c4-judge

2024-04-09T09:54:14Z

3docSec marked the issue as grade-b

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