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
Rank: 5/72
Findings: 2
Award: $3,036.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
3028.5531 USDC - $3,028.55
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
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.
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:
mint()
OUSG by exchanging 250,000 USDC.Conversely, users may incur immediate losses in mint()
when the value of USDC exceeds $1:
mint()
OUSG by exchanging 250,000 USDC.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.
Manual Review
Consider using price feed for USDC rather than assuming it's always $1.
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
🌟 Selected for report: immeas
Also found by: 0xAkira, 0xCiphky, 0xGreyWolf, 0xJaeger, 0xMosh, 0xabhay, 0xlemon, 0xmystery, 0xweb3boy, Aamir, Abdessamed, Aymen0909, Breeje, DanielArmstrong, DarkTower, Dots, EaglesSecurity, FastChecker, HChang26, Honour, IceBear, JC, K42, Krace, MaslarovK, Omik, OxTenma, SAQ, Shubham, Stormreckson, Tigerfrake, Tychai0s, VAD37, ZanyBonzy, albahaca, arnie, ast3ros, asui, b0g0, bareli, baz1ka, btk, caglankaan, carrotsmuggler, cheatc0d3, dd0x7e8, grearlake, igbinosuneric, jaydhales, kaden, kartik_giri_47538, m4ttm, ni8mare, niser93, nonn_ac, oualidpro, pfapostol, pkqs90, popeye, radev_sw, samuraii77, slvDev, zabihullahazadzoi
8.2807 USDC - $8.28
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
_checkAndUpdateInstantMintLimit()
and _checkAndUpdateInstantRedemptionLimit()
include fees, which could lead to a potential reduction in allowable amounts by up to 1.99% in each window.
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%.
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); }
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