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: 38/72
Findings: 1
Award: $8.28
π Selected for report: 0
π Solo Findings: 0
π 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
Risk | Title |
---|---|
[L-01] | Funds will become inaccessible when USDC blacklists a user |
[L-02] | Investors can't instant redeem when buidl token is paused |
[L-03] | No price validation check in rOUSG |
[L-04] | rOUSG is not erc20 compliant |
[L-05] | Incorrect check in _redeemBUIDL |
[L-06] | Minting will get DoSed if the usdcReceiver got blocklisted |
If an investor is blacklisted by the USDC contract, they will be unable to redeem their OUSG/ROUSG tokens.
Some ERC-20 tokens like for example USDC (which is used by the system) have the functionality to blacklist specific addresses, so that they are no longer able to transfer and receive tokens. Sending funds to these addresses will lead to a revert. The issue is that the _redeem function transfers funds to the msg.sender:
usdc.transfer(msg.sender, usdcAmountOut);
Therefore, if an investor is blacklisted by the USDC address, they will be unable to redeem their OUSG/ROUSG tokens.
Consider allowing investors to specify a receiver address.
The OUSGInstantManager contract is designed to facilitate instant mints and instant redemptions of OUSG and rOUSG. However, investors are unable to redeem their OUSG/rOUSG when the buidl token is paused.
Certain ERC-20 tokens, such as buidl (utilized within the system), possess the capability to pause token transfers in emergencies. This functionality is exemplified here:
function pause() public onlyTransferAgentOrAbove whenNotPaused { paused = true; emit Pause(); }
The issue arises when the buidl token is paused, rendering OUSGInstantManager unable to fulfill its instant redemption commitments. Consequently, this situation detrimentally impacts the protocol's reputation.
Consider documenting scenarios where OUSGInstantManager may fail to provide instant redemption.
rOUSG lacks validation for the price returned by the oracle. Consequently, if the price unexpectedly drops to a low value, transactions won't revert, potentially resulting in loss of funds for investors due to oracle price manipulation.
The OUSGInstantManager contract validates the price returned by the oracle as follows:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); require( price > MINIMUM_OUSG_PRICE, "OUSGInstantManager::getOUSGPrice: Price unexpectedly low" ); }
The OUSGInstantManager.getOUSGPrice function reverts if the price is unexpectedly low (i.e., when the price is less than 105e18).
However, the rOUSG token lacks a similar sanity check, as seen here:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); }
Consequently, if the returned price is too low due to price manipulation, the transaction will succeed, resulting in a loss of funds for investors.
Consider adding a similar check for the rOUSG contract:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); require( price > 105e18, "OUSGInstantManager::getOUSGPrice: Price unexpectedly low" ); }
The rOUSG contract does not adhere to EIP-20 specifications.
EIP-20 state that:
Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned!
The rOUSG interacts with the OUSG token assuming it will always revert instead of returning false. While the current implementation of OUSG does revert instead of returning false, the OUSG contract is upgradeable, meaning the implementation can change in the future, breaking the rOUSG integration with EIP-20.
Consider checking the returned value and reverting if false is returned. For example:
require(ousg.transferFrom(msg.sender, address(this), _OUSGAmount), "Transfer failed");
The _redeemBUIDL function within OUSGInstantManager contains an incorrect check that triggers the buidlRedeemer.redeem function even when there is "Insufficient BUIDL balance." This leads to unnecessary wastage of investors' gas fees.
The _redeemBUIDL function contains a check to ensure there is sufficient BUIDL balance in the contract. However, it erroneously checks the wrong value:
function _redeemBUIDL(uint256 buidlAmountToRedeem) internal { require( buidl.balanceOf(address(this)) >= minBUIDLRedeemAmount, "OUSGInstantManager::_redeemBUIDL: Insufficient BUIDL balance" ); uint256 usdcBalanceBefore = usdc.balanceOf(address(this)); // ok buidl.approve(address(buidlRedeemer), buidlAmountToRedeem); // ok buidlRedeemer.redeem(buidlAmountToRedeem); require( usdc.balanceOf(address(this)) == usdcBalanceBefore + buidlAmountToRedeem, "OUSGInstantManager::_redeemBUIDL: BUIDL:USDC not 1:1" ); }
The _redeemBUIDL function is called when usdcAmountToRedeem >= minBUIDLRedeemAmount, as shown here:
if (usdcAmountToRedeem >= minBUIDLRedeemAmount) { // amount of USDC needed is over minBUIDLRedeemAmount, do a BUIDL redemption // to cover the full amount _redeemBUIDL(usdcAmountToRedeem);
For example, if usdcAmountToRedeem is 300k USDC, and minBUIDLRedeemAmount is 250k BUIDL, _redeemBUIDL is called with usdcAmountToRedeem. The issue arises because it checks if the OUSGInstantManager BUIDL balance is more than minBUIDLRedeemAmount instead of usdcAmountToRedeem. Consequently, if there were 260k BUIDL in the OUSGInstantManager, the check ensuring sufficient balance would pass even when there is an insufficient balance, leading to wasted investor gas in a no-op situation.
Consider updating the _redeemBUIDL function check as follows:
function _redeemBUIDL(uint256 buidlAmountToRedeem) internal { require( buidl.balanceOf(address(this)) >= buidlAmountToRedeem, "OUSGInstantManager::_redeemBUIDL: Insufficient BUIDL balance" ); uint256 usdcBalanceBefore = usdc.balanceOf(address(this)); // ok buidl.approve(address(buidlRedeemer), buidlAmountToRedeem); // ok buidlRedeemer.redeem(buidlAmountToRedeem); require( usdc.balanceOf(address(this)) == usdcBalanceBefore + buidlAmountToRedeem, "OUSGInstantManager::_redeemBUIDL: BUIDL:USDC not 1:1" ); }
OUSGInstantManager minting will get DoSed if the usdcReceiver got blocklisted by USDC.
Some ERC-20 tokens like for example USDC (which is used by the system) have the functionality to blacklist specific addresses, so that they are no longer able to transfer and receive tokens. Sending funds to these addresses will lead to a revert. Currently, the usdcReceiver address in the OUSGInstantManager contract is immutable and cannot be changed:
address public immutable usdcReceiver;
If the usdcReceiver address is blacklisted by USDC, attempts to transfer tokens to it will always revert, potentially leading to a permanent DoS during minting:
usdc.transferFrom(msg.sender, usdcReceiver, usdcAmountAfterFee);
Consider making the usdcReceiver address mutable and implementing a function to update it in case of blacklisting by USDC.
#0 - c4-pre-sort
2024-04-05T05:32:57Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-04-05T20:08:07Z
0xRobocop marked the issue as high quality report
#2 - cameronclifton
2024-04-05T22:30:00Z
[L-01] Funds will become inaccessible when USDC blacklists a user -> known and invalid. [L-02] Investors canβt instant redeem when buidl token is paused -> known and invalid. [L-03] No price validation check in rOUSG -> The oracle returning a low price doesn't carry any risk right? If someone mints say 1 OUSG -> 10,000 shares -> 100 rOUSG, then say the OUSG oracle is returning $1, their balanceOf will just return 1 rOUSG, and if they unwrap it they will still just get 1 OUSG back (not more). It's not really an attack vector as it would be for the OUSGInstantManager. People would be concerned about their rOUSG balance would be changing drastically, but the amount of OUSG they are entitled to does not change and the contract would still function normally. Since the price is just aesthetic I don't think we need this check. [L-04] rOUSG is not erc20 compliant -> Any upgradeable ERC20 contract could be upgraded to break ERC20 compliance. Ondo Finance controls default admin of both OUSG and rOUSG so this would not be a valid concern IMO [L-05] Incorrect check in _redeemBUIDL -> Great catch. [L-06] Minting will get DoSed if the usdcReceiver got blocklisted -> known and invalid
Acking as one of these are valid.
#3 - c4-sponsor
2024-04-05T22:30:04Z
cameronclifton (sponsor) acknowledged
#4 - c4-judge
2024-04-10T07:02:45Z
3docSec marked the issue as grade-b
#5 - 0xbtk
2024-04-12T10:37:58Z
Hey @3docSec, I believe that my L5 is a duplicate of #306. Can you please take a look?
#6 - 3docSec
2024-04-13T13:01:57Z
Tricky one.
Yes, it's close indeed, because it's quite clear you identified the right scenario.
You however did not identify the right root cause which is that the contract does not make a partial BUIDL + partial USDC redemption to fulfill the request, and this is proved by the fact that your fix doesn't address #306, because, with your fix, the #306 scenario will fail earlier but still fail.
#7 - 0xbtk
2024-04-13T13:12:46Z
You however did not identify the right root cause which is that the contract does not make a partial BUIDL + partial USDC redemption to fulfill the request, and this is proved by the fact that your fix doesn't address #306, because, with your fix, the #306 scenario will fail earlier but still fail.
@3docSec I reported it in a different issue and it is marked as unsatisfactory, please see #235.