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: 4/72
Findings: 2
Award: $3,092.70
🌟 Selected for report: 0
🚀 Solo Findings: 0
3028.5531 USDC - $3,028.55
https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/ousgInstantManager.sol#L685 https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/ousgInstantManager.sol#L699
The ousgInstantManager
is used to mint or redeem OUSG/rOUSG using USDC on a 1:1 ratio. To calculate the amount of OUSG we will mint or redeem we use the _getMintAmount()
and _getRedemptionAmount()
functions.
function _getMintAmount( uint256 usdcAmountIn, uint256 price ) internal view returns (uint256 ousgAmountOut) { uint256 amountE36 = _scaleUp(usdcAmountIn) * 1e18; ousgAmountOut = amountE36 / price; } function _getRedemptionAmount( uint256 ousgAmountBurned, uint256 price ) internal view returns (uint256 usdcOwed) { uint256 amountE36 = ousgAmountBurned * price; usdcOwed = _scaleDown(amountE36 / 1e18); }
So for example if we want to mint and use 105 USDC worth $1 and the price of OUSG is 105 the calculation will be:
(105e6 * 1e12) * 1e18 / 105e18 = 1e18
However the problem here is that the USDC is always priced at 1$ because we are only using the amount of USDC being sent without the current price. This will create problems when the price of USDC depegs which happened for example last year.
Users will be able to use USDC to mint OUSG as if the price was 1$
even though the price can be lower allowing the users to profit from this. Or if the price goes above 1$
, users will receive the amount of USDC when redeeming as if it was 1$ even though it is worth more.
If the price of USDC depegs the users will be able to mint OUSG using a discounted price or they will be able to receive USDC at a higher price when redeeming allowing them to profit from events like this.
function _getMintAmount( uint256 usdcAmountIn, uint256 price ) internal view returns (uint256 ousgAmountOut) { uint256 amountE36 = _scaleUp(usdcAmountIn) * 1e18; ousgAmountOut = amountE36 / price; } function _getRedemptionAmount( uint256 ousgAmountBurned, uint256 price ) internal view returns (uint256 usdcOwed) { uint256 amountE36 = ousgAmountBurned * price; usdcOwed = _scaleDown(amountE36 / 1e18); }
As you can see here the amount of OUSG that the user will mint/redeem uses the amount of USDC sent which is always priced at 1$ and it doesnt check the current price.
Manual Review
The easiest way to fix this is to get the USDC price from an oracle and revert if it is for example < 0.99$ or > 1.01$
Other
#0 - 0xRobocop
2024-04-04T04:19:41Z
The issue seems valid. The oracle used for comparison is SHV / USD
from Chainlink, found in RWAOracleExternalComparisonCheck.sol
.
#1 - c4-pre-sort
2024-04-04T04:19:46Z
0xRobocop marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-04-04T04:19:48Z
0xRobocop marked the issue as primary issue
#3 - c4-pre-sort
2024-04-05T18:09:14Z
0xRobocop marked the issue as high quality report
#4 - c4-sponsor
2024-04-05T23:35:25Z
cameronclifton (sponsor) acknowledged
#5 - cameronclifton
2024-04-05T23:36:00Z
This is fair and not explicitly called out as a known issue in the readme. I'm on the fence on whether we will employ this mitigation.
#6 - c4-judge
2024-04-09T10:03:51Z
3docSec changed the severity to 3 (High Risk)
#7 - c4-judge
2024-04-09T10:04:07Z
3docSec marked the issue as satisfactory
#8 - c4-judge
2024-04-09T10:04:34Z
3docSec marked issue #278 as primary and marked this issue as a duplicate of 278
64.1515 USDC - $64.15
The rOUSG.sol contracts has a burner role which is able to burn rOUSG from any address. The burn()
function will be used to seize rOUSG/OUSG when the address is not legally allowed to own it. The problem is that the user will likely be sanctioned before the rOUSG is seized to freeze the funds but because the _burnShares()
function calls _beforeTokenTransfer()
, the tx will revert because of the check of the from address in _beforeTokenTransfer()
which will be the sanctioned address causing the check to revert.
if (from != address(0)) { // If not minting require(_getKYCStatus(from), "rOUSG: 'from' address not KYC'd"); }
Although this finding was submitted in the previous contest a big difference here is that the KYCRegistry uses a Chainalysis sanction oracle for the checks in _beforeTokenTransfer()
. In the previous contest this could have been bypassed by the admin batch executing transactions but because we are now using the Chainalysis oracle we will not be able to simply remove the sanctioned address from the oracle, burn the tokens and add it again.
The admin wont be able to seize tokens from the sanctioned address because burning will always fail. The sanctioned address will be left with the tokens stuck in the wallet.
Add this to rOUSG.t.sol
, as you can see burning from a sanctioned address will fail.
function testBurningROUSG() public dealAliceROUSG(1e18) { assertEq(rOUSGToken.balanceOf(alice), 100e18); vm.startPrank(OUSG_GUARDIAN); rOUSGToken.grantRole(rOUSGToken.BURNER_ROLE(), OUSG_GUARDIAN); //ALice gets added to the Chainalysis sanctions oracle _restrictOUSGUser(alice); vm.expectRevert("rOUSG: 'from' address not KYC'd"); rOUSGToken.burn(alice,100e18); vm.stopPrank(); }
Foundry
Burn the tokens without the _beforeTokenTransfer()
check
Token-Transfer
#0 - c4-pre-sort
2024-04-04T05:11:28Z
0xRobocop marked the issue as duplicate of #237
#1 - c4-judge
2024-04-09T08:32:44Z
3docSec marked the issue as satisfactory