Platform: Code4rena
Start Date: 15/06/2023
Pot Size: $79,800 USDC
Total HM: 14
Participants: 6
Period: 14 days
Judge: 0xean
Total Solo HM: 11
Id: 248
League: ETH
Rank: 3/6
Findings: 5
Award: $0.00
🌟 Selected for report: 2
🚀 Solo Findings: 2
🌟 Selected for report: rvierdiiev
Data not available
In case Distributor.setDistribution use, revenue from rToken RevenueTrader and rsr token RevenueTrader should be distributed. Otherwise wrong distribution will be used.
BackingManager.forwardRevenue
function sends revenue amount to the rsrTrader
and rTokenTrader
contracts, according to the distribution inside Distributor
contract. For example it can 50%/50%. In case if we have 2 destinations in Distributor: strsr and furnace, that means that half of revenue will be received by strsr stakers as rewards.
This distribution can be changed at any time.
The job of RevenueTrader
is to sell provided token for a tokenToBuy
and then distribute it using Distributor.distribute
function. There are 2 ways of auction that are used: dutch and gnosis. Dutch auction will call RevenueTrader.settleTrade
, which will initiate distribution. But Gnosis trade will not do that and user should call distributeTokenToBuy
manually, after auction is settled.
The problem that i want to discuss is next.
Suppose, that governance at the beginning set distribution as 50/50 between 2 destinations: strsr and furnace. And then later forwardRevenue
sent some tokens to the rsrTrader and rTokenTrader. Then, when trade was active to exchange some token to rsr token, Distributor.setDistribution
was set in order to make strsr share to 0, so now everything goes to Furnace only. As result, when trade will be finished in the rsrTrader and Distributor.distribute
will be called, then those tokens will not be sent to the strsr contract, because their share is 0 now.
They will be stucked inside rsrTrader.
Another problem here is that strsr holders should receive all revenue from the time, where they distribution were created. What i mean is if in time 0, rsr share was 50% and in time 10 rsr share is 10%, then BackingManager.forwardRevenue
should be called for all tokens that has surplus, because if that will be done after changing to 10%, then strsr stakers will receive less revenue.
VsCode
You need to think how to guarantee fair distribution to the strsr stakers, when distribution params are changed.
Error
#0 - c4-sponsor
2023-07-04T22:24:44Z
tbrent marked the issue as sponsor confirmed
#1 - tbrent
2023-07-04T22:25:47Z
This is a good find. The mitigation we have in mind is adding a new function to the RevenueTrader
that allows anyone to transfer a registered ERC20 back to the BackingManager
, as long as the current distribution for that tokenToBuy
is 0%.
#2 - c4-judge
2023-07-12T19:30:37Z
0xean marked the issue as satisfactory
🌟 Selected for report: rvierdiiev
Data not available
FurnaceP1.setRatio
will not update lastPayout
when called in frozen state, which means that after component will be unfrozen, melting will be incorrect.
melt
function should burn some amount of tokens from lastPayoutBal
. It depends of lastPayout
and ratio
variables. The more time has passed, the more tokens will be burnt.
When setRatio
function is called, then melt
function is tried to be executed, because new ratio is provided and it should not be used for previous time ranges.
In case if everything is ok, then lastPayout
and lastPayoutBal
will be updated, so it's safe to update ratio
now.
But it's possible that melt
function will revert in case if notFrozen
modifier is not passed. As result lastPayout
and lastPayoutBal
will not be updated, but ratio will be. Because of that, when Furnace
will be unfrozen, then melting rate can be much more, then it should be, because lastPayout
wasn't updated.
VsCode
In case of catch
case, you can update lastPayout
and lastPayoutBal
.
Error
#0 - c4-sponsor
2023-07-04T22:42:50Z
tbrent marked the issue as sponsor confirmed
#1 - c4-judge
2023-07-12T19:35:19Z
0xean marked the issue as satisfactory
🌟 Selected for report: ronnyx2017
Also found by: 0xA5DF, rvierdiiev
Data not available
Loss of staking yield for stakers when another user stakes in pause/frozen state.
Issue #148 from previous audit is present again. As i can see it was mitigated. But maybe after that new code changes were made, so this issue is present again.
VsCode
In case if you can't call payoutRewards
when frozen, then do not allow to call stake as well.
Error
#0 - c4-judge
2023-06-30T15:30:54Z
0xean marked the issue as duplicate of #24
#1 - c4-judge
2023-07-12T20:05:35Z
0xean marked the issue as satisfactory
🌟 Selected for report: ronnyx2017
Also found by: 0xA5DF, rvierdiiev
Data not available
StRSR.cancelUnstake doesn't call _payoutRewards before minting new shares. As result this rewards will be distributed for new staker as well.
If user wants to cancel his withdraw, then he can call StRSR.cancelUnstake
and mint new shares from the RSR amount he wanted to withdraw. This minting will use current stakeRate
to find out stRSR amount to mint.
The problem is that _payoutRewards
function is not called before minting. It should be because, this function increases stakeRSR
param with rewards, which then changes stakeRate
.
As result user that decided to cancel withdraw will receive part of rewards which he should not receive.
VsCode
Call _payoutRewards
, before minting new stRSR to user.
Error
#0 - tbrent
2023-07-03T17:17:09Z
Dup with #10
#1 - c4-sponsor
2023-07-04T22:21:34Z
tbrent marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-12T19:26:43Z
0xean marked the issue as duplicate of #10
#3 - c4-judge
2023-07-12T20:05:26Z
0xean marked the issue as satisfactory
🌟 Selected for report: 0xA5DF
Also found by: rvierdiiev
Data not available
Attacker can disable basket, when swapping or unregistering not basket asset.
AssetRegistry.swapRegistered and AssetRegistry.unregister are both functions that can be called by governance only. It's possible that after proposal is passed, then it can be executed by anyone. Also executor can provide as much gas with the call, as he wishes.
function swapRegistered(IAsset asset) external governance returns (bool swapped) { require(_erc20s.contains(address(asset.erc20())), "no ERC20 collision"); try basketHandler.quantity{ gas: _reserveGas() }(asset.erc20()) returns (uint192 quantity) { if (quantity > 0) basketHandler.disableBasket(); // not an interaction } catch { basketHandler.disableBasket(); } swapped = _registerIgnoringCollisions(asset); }
Here, reserve team is checking that after basketHandler.quantity
call, function still has 900000 gas, which is needed to finish basketHandler.disableBasket
call.
As you can see, in case if quantity of asset in the basket is 0, then there is no need to disable basket.
However, malicious user still can do that.
What he need to do is to call swapRegistered
with 900000 + x gas amount. Where x will be the amount that is not enough to execute basketHandler.quantity
function, which will cause out of gas error and catch block will then call basketHandler.disableBasket
. As result basket will be disabled if attacker wants it.
VsCode
You need to ensure that you send enough gas to basketHandler.quantity
function.
Error
#0 - tbrent
2023-07-03T17:09:21Z
Is this duplicate with #7?
#1 - c4-sponsor
2023-07-04T22:23:48Z
tbrent marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-12T19:30:12Z
0xean marked the issue as duplicate of #7
#3 - c4-judge
2023-07-12T20:05:15Z
0xean marked the issue as satisfactory
🌟 Selected for report: 0xA5DF
Also found by: RaymondFam, carlitox477, hihen, ronnyx2017, rvierdiiev
Data not available
BasketHandler.warmupPeriod can be changed, when basket is in warm up period, which will allow another contracts to call basket handler.
BasketHandler.isReady
function is used by another components to check if it's possible to communicate with basket handler.
https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BasketHandler.sol#L261-L265
function isReady() external view returns (bool) { return status() == CollateralStatus.SOUND && (block.timestamp >= lastStatusTimestamp + warmupPeriod); }
As you can see warmupPeriod
period is needed to wait after basket status changed.
The problem that owner can change warmupPeriod
period any time. And in case if warmup has already started before it will rewrite it.
You need to store time, where warmup will be finished instead of using warmupPeriod
check.
BackingManager currently trying to sell SOUND collateral instead of IFFY. Because of that RToken leave token that can be defaulted soon.
RecollateralizationLibP1.nextTradePair
function should find trade pair in order to make recollateralization.
It uses isBetterSurplus
function in order to compare selling different assets.
https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/RecollateralizationLib.sol#L418-L434
function isBetterSurplus( MaxSurplusDeficit memory curr, CollateralStatus other, uint192 surplusAmt ) private pure returns (bool) { // NOTE: If the CollateralStatus enum changes then this has to change! if (curr.surplusStatus == CollateralStatus.DISABLED) { return other == CollateralStatus.DISABLED && surplusAmt.gt(curr.surplus); } else if (curr.surplusStatus == CollateralStatus.SOUND) { return other == CollateralStatus.DISABLED || (other == CollateralStatus.SOUND && surplusAmt.gt(curr.surplus)); } else { // curr is IFFY return other != CollateralStatus.IFFY || surplusAmt.gt(curr.surplus); } }
As you can see in case if current status is SOUND, then any amount of DISABLED will be counted as better option to sell.
But in case if other is IFFY, then curr
will not be updated.
The idea here is to better sell DISABLED token over SOUND. But IFFY token can become DEFAULTED as well soon. That's why i believe that it should be prioritized to sell over SOUND asset. So i guess that SOUND tokens should be sold in last order.
In case if current is SOUND and other
is IFFY, then sell IFFY.
User should be able to withdraw his draft after unstakingDelay
period, but he should wait more.
When user creates new draft, then availableAt
time is calculated for him. This is a time when he should be able to withdraw his draft.
However, because of this check it's possible that user will wait more time to withdraw that draft.
This can happen when unstakingDelay
governance param was changed to smaller one.
While, i understand that should be done, because of algorithm that is used to work with drafts, this submission is about unfair waiting time.
Think, that it can't be changed, as it will break work with drafts.
#0 - c4-judge
2023-07-12T19:57:34Z
0xean marked the issue as grade-a