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: 65/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
The ROUSG.burn()
function allows admin
to burn rOUSG
tokens from any _account
thereby getting OUSG
in return.
The ROUSG.unwrap()
on the other hand is called by users to unwrap their rOUSG
tokens.
However, these functions do not have any type of slippage control
since the amount of tokens
received by the user is determined by an interaction with an oracle
, meaning that the amount received in return may vary indefinitely while the request is waiting to be executed.
Also the users will have no defense against price manipulation attacks, if they were to be found after the protocol's deployment.
ROUSG.burn()
function burn( address _account, uint256 _amount ) external onlyRole(BURNER_ROLE) { uint256 ousgSharesAmount = getSharesByROUSG(_amount); if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER) revert UnwrapTooSmall(); _burnShares(_account, ousgSharesAmount); ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER ); emit Transfer(address(0), msg.sender, getROUSGByShares(ousgSharesAmount)); emit TransferShares(_account, address(0), ousgSharesAmount); }
The ousgSharesAmount
to be burned is determined by the getSharesByROUSG()
function of the same contract:
function getSharesByROUSG( uint256 _rOUSGAmount ) public view returns (uint256) { return (_rOUSGAmount * 1e18 * OUSG_TO_ROUSG_SHARES_MULTIPLIER) / getOUSGPrice(); }
As can be observed, this function uses oracle
interaction through the division by getOUSGPrice()
. The getOUSGPrice()
function queries the external oracle
for the asset price:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); }
To determine the amount of OUSG
to be received after share burning, the ousgSharesAmount
calculated above is divided by OUSG_TO_ROUSG_SHARES_MULTIPLIER
:
ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER );
This means that the user has no way to predict how many OUSG
they will get back at the moment of burning shares
, as the price could be updated while the request is in the mempool.
ROUSG.unwrap()
function unwrap(uint256 _rOUSGAmount) external whenNotPaused { require(_rOUSGAmount > 0, "rOUSG: can't unwrap zero rOUSG tokens"); uint256 ousgSharesAmount = getSharesByROUSG(_rOUSGAmount); if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER) revert UnwrapTooSmall(); _burnShares(msg.sender, ousgSharesAmount); ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER ); emit Transfer(msg.sender, address(0), _rOUSGAmount); emit TransferShares(msg.sender, address(0), ousgSharesAmount); }
ousgSharesAmount
is detamined by calling getSharesByROUSG()
:
function getSharesByROUSG( uint256 _rOUSGAmount ) public view returns (uint256) { return (_rOUSGAmount * 1e18 * OUSG_TO_ROUSG_SHARES_MULTIPLIER) / getOUSGPrice(); }
which also uses getOUSGPrice()
in division as above. This invokes the external oracle
to get the asset price:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); }
And finally to determine the amount of OUSG
to be received after unwrapping, the ousgSharesAmount
calculated above is divided by OUSG_TO_ROUSG_SHARES_MULTIPLIER
:
ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER );
Similarly, the user has no way to predict how many OUSG
they will get back at the moment of unwrapping, as the price could be updated while the request is in the mempool.
Manual Review
Add additional parameter that could be added in these functions, to decide the minimum amount of tokens to be received
, with a relative check after burning/unwrapping.
Oracle
#0 - c4-pre-sort
2024-04-05T02:41:16Z
0xRobocop marked the issue as duplicate of #156
#1 - c4-judge
2024-04-09T08:03:12Z
3docSec marked the issue as satisfactory
#2 - c4-judge
2024-04-11T06:43:12Z
3docSec marked the issue as not a duplicate
#3 - c4-judge
2024-04-11T06:43:16Z
3docSec marked the issue as primary issue
#4 - c4-judge
2024-04-11T06:44:08Z
3docSec marked the issue as selected for report
#5 - Brivan-26
2024-04-11T13:12:40Z
I'm quoting from the warden's description:
However, these functions do not have any type of slippage control since the amount of tokens received by the user is determined by an interaction with an oracle, meaning that the amount received in return may vary indefinitely while the request is waiting to be executed.
I believe this is wrong, the amount to receive will never vary indefinitely as mentioned, because Oracle doesn't allow %1 price change when updating the price. So the user may still view the current OUSG price and can still determine the outcome of the transaction.
This means that the user has no way to predict how many OUSG they will get back at the moment of burning shares, as the price could be updated while the request is in the mempool.
This is very unlikely to happen, the price is updated once every 23 hours at best scenario. So an unwrap/burn transaction encounter the oracle::setPrice
in the mempool is very unlikely. Even if such rare cases happen, the price won't change too much as the oracle doesn't allow a change slippage of %1
#6 - 3docSec
2024-04-11T15:05:05Z
āļø this is a very good point.
In summary:
All of the above give factual proof that slippage is negligible in this context. šÆ @Brivan-26
#7 - 3docSec
2024-04-11T15:12:47Z
Update, it's actually 2% in RWAOracleExternalComparisonCheck
but the argument stands.
#8 - c4-judge
2024-04-11T15:17:25Z
3docSec changed the severity to QA (Quality Assurance)
#9 - c4-judge
2024-04-11T15:17:31Z
3docSec marked the issue as grade-b
#10 - c4-judge
2024-04-11T15:18:13Z
3docSec marked the issue as not selected for report
#11 - 0xbtk
2024-04-11T18:13:46Z
Hey @3docSec, 2% may seem small, but it's not negligible. A significant number of users are likely to utilize both redeemRebasingOUSG and mintRebasingOUSG, with a minimum requirement of 50k for redemption and 100k for minting. Hence, a 2% loss translates to 1k during redemption and 2k during minting, posing a non-trivial risk of asset loss, warranting a medium severity i believe.