Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSR holders could get less staked stRSR than expected #186

Open
c4-bot-2 opened this issue Aug 19, 2024 · 0 comments
Open

RSR holders could get less staked stRSR than expected #186

c4-bot-2 opened this issue Aug 19, 2024 · 0 comments
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working edited-by-warden 🤖_15_group AI based duplicate group recommendation sufficient quality report This report is of sufficient quality

Comments

@c4-bot-2
Copy link
Contributor

c4-bot-2 commented Aug 19, 2024

Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L502
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L621
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L230

Vulnerability details

Impact

The stake() function allows RSR holders to stake their RSR token for stRSR token at an exchange rate of that current time. However, the lack of slippage protection can result in users receiving less than expected when staking RSR tokens. This occurs because the stakeRate (i.e. exchangeRate) is updated within the stake functions by the _payoutRewards function. However, users typically check the exchangeRate using the exchangeRate() function before staking to understand the expected amount they will receive. Since the exchangeRate() function does not trigger _payoutRewards, it returns a value that may change if a new stakeRate is claculated (based on new added rewards), which leads to changes in the exchange rate when the actual staking occurs. This lack of a slippage protection mechanism can result in users receiving fewer stRSR tokens than anticipated.

Proof of Concept

    function _payoutRewards() internal {
        if (block.timestamp < payoutLastPaid + 1) return;
        uint48 numPeriods = uint48(block.timestamp) - payoutLastPaid;


        uint192 initRate = exchangeRate();
        uint256 payout;


        // Do an actual payout if and only if enough RSR is staked!
        if (totalStakes >= FIX_ONE) {
            // Paying out the ratio r, N times, equals paying out the ratio (1 - (1-r)^N) 1 time.
            // Apply payout to RSR backing
            // payoutRatio: D18 = FIX_ONE: D18 - FixLib.powu(): D18
            // Both uses of uint192(-) are fine, as it's equivalent to FixLib.sub().
            uint192 payoutRatio = FIX_ONE - FixLib.powu(FIX_ONE - rewardRatio, numPeriods);


            // payout: {qRSR} = D18{1} * {qRSR} / D18
            payout = (payoutRatio * rsrRewardsAtLastPayout) / FIX_ONE;
            stakeRSR += payout;
        }


        payoutLastPaid += numPeriods;
        rsrRewardsAtLastPayout = rsrRewards();


        // stakeRate else case: D18{qStRSR/qRSR} = {qStRSR} * D18 / {qRSR}
        // downcast is safe: it's at most 1e38 * 1e18 = 1e56
        // untestable:
        //      the second half of the OR comparison is untestable because of the invariant:
        //      if totalStakes == 0, then stakeRSR == 0
        @> stakeRate = (stakeRSR == 0 || totalStakes == 0)
            ? FIX_ONE
            : uint192((totalStakes * FIX_ONE_256 + (stakeRSR - 1)) / stakeRSR);


        emit RewardsPaid(payout);
        emit ExchangeRateSet(initRate, exchangeRate());
    }
  • Exchange Rate: When a user wants to stake or unstake RSR, they will typically check the exchangeRate() function to calculate the expected stRSR:

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L502

function exchangeRate() public view returns (uint192) {
    // D18{qRSR/qStRSR} = D18 * D18 / D18{qStRSR/qRSR}
    @>return (FIX_SCALE_SQ + (stakeRate / 2)) / stakeRate; // ROUND method
}

However, the exchangeRate function does not call the _payoutRewards function, which updates the stakeRate to a new value (in case of new added RSR rewards).

  • Stake Function: When a user stakes RSR tokens, the stake() function calls _payoutRewards before minting new stRSR tokens:

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L227

function stake(uint256 rsrAmount) public {
    _notZero(rsrAmount);
    @> _payoutRewards();
    mintStakes(_msgSender(), rsrAmount);
    IERC20Upgradeable(address(rsr)).safeTransferFrom(_msgSender(), address(this), rsrAmount);
}

This can change the stakeRate after the user has checked the exchangeRate, leading to them receiving fewer stRSR tokens than expected.

  • Example: As a simple example, suppose:
  1. stakeRate is set as FIX_ONE (i.e. 1e18 - 1:1), totalStakes = 500 and stakeRSR = 500
  2. Alice wants to stake 50 RSR, calls exchangeRate() function to get the exchange rate between RSR and StRSR (note: exchangeRate() returns FIX_ONE i.e. 1e18 therefore, 50 RSR = 50 StRSR).
  3. Alice calls stake() function. Assume there are new reward to distribute in the contract (200 RSR), the _payoutRewards() function re-calculates a new stakeRate. new stakeRate = (totalStakes(500) * FIX_ONE_256(1e18) + (stakeRSR(700) - 1)) / stakeRSR(700) = 7.14285714285714E+17 (note: now less than initial stakeRate of FIX_ONE (1e18))
  4. With the new stakeRate, Alice now gets StRSR amount of = stakeRate(7.14285714285714E+17) * stakeAmount(50) = ~ 35 StRSR (i.e. 15 StRSR less than expected)

Tools Used

Manual, Foundry

Recommended Mitigation Steps

Either implement a slippage protection mechanism in the stake function or update the exchangeRate function to call _payoutRewards

Reference

code-423n4/2023-12-revolutionprotocol-findings#397 (comment)

Assessed type

Other

@c4-bot-2 c4-bot-2 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Aug 19, 2024
c4-bot-1 added a commit that referenced this issue Aug 19, 2024
@c4-bot-9 c4-bot-9 changed the title Draft RSR holders could get less staked stRSR than expected Aug 19, 2024
@c4-bot-12 c4-bot-12 added the 🤖_15_group AI based duplicate group recommendation label Aug 19, 2024
howlbot-integration bot added a commit that referenced this issue Aug 21, 2024
@howlbot-integration howlbot-integration bot added the sufficient quality report This report is of sufficient quality label Aug 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working edited-by-warden 🤖_15_group AI based duplicate group recommendation sufficient quality report This report is of sufficient quality
Projects
None yet
Development

No branches or pull requests

3 participants