code-423n4 / 2024-07-reserve-validation

0 stars 0 forks source link

RSR holders could get less staked stRSR than expected #186

Open c4-bot-2 opened 1 month ago

c4-bot-2 commented 1 month ago

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

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).

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.

  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

https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/397#issue-2051784043

Assessed type

Other