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.
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:
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:
stakeRate is set as FIX_ONE (i.e. 1e18 - 1:1), totalStakes = 500 and stakeRSR = 500
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).
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))
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
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 allowsRSR
holders to stake theirRSR
token forstRSR
token at an exchange rate of that current time. However, the lack of slippage protection can result in users receiving less than expected when stakingRSR
tokens. This occurs because thestakeRate
(i.e.exchangeRate
) is updated within thestake
functions by the_payoutRewards
function. However, users typically check theexchangeRate
using theexchangeRate()
function before staking to understand the expected amount they will receive. Since theexchangeRate()
function does not trigger_payoutRewards
, it returns a value that may change if a newstakeRate
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 fewerstRSR
tokens than anticipated.Proof of Concept
Payout Reward Function: This function payouts rewards (adds to
stakeRSR
) and updates thestakeRate
. https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L593-L627exchangeRate()
function to calculate the expectedstRSR
:https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L502
However, the
exchangeRate
function does not call the_payoutRewards
function, which updates thestakeRate
to a new value (in case of new addedRSR
rewards).RSR
tokens, thestake()
function calls_payoutRewards
before minting newstRSR
tokens:https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L227
This can change the
stakeRate
after the user has checked theexchangeRate
, leading to them receiving fewerstRSR
tokens than expected.stakeRate
is set asFIX_ONE
(i.e.1e18 - 1:1
),totalStakes
= 500 andstakeRSR
= 500RSR
, callsexchangeRate()
function to get the exchange rate betweenRSR
andStRSR
(note:exchangeRate()
returnsFIX_ONE
i.e.1e18
therefore, 50RSR
= 50StRSR
).stake()
function. Assume there are new reward to distribute in the contract (200RSR
), the_payoutRewards()
function re-calculates a newstakeRate
. newstakeRate
=(totalStakes(500) * FIX_ONE_256(1e18) + (stakeRSR(700) - 1)) / stakeRSR(700)
=7.14285714285714E+17
(note: now less than initialstakeRate
ofFIX_ONE
(1e18
))stakeRate
, Alice now getsStRSR
amount of =stakeRate(7.14285714285714E+17) * stakeAmount(50) = ~ 35 StRSR
(i.e. 15StRSR
less than expected)Tools Used
Manual, Foundry
Recommended Mitigation Steps
Either implement a slippage protection mechanism in the
stake
function or update theexchangeRate
function to call_payoutRewards
Reference
https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/397#issue-2051784043
Assessed type
Other