Summary
The code allows for rtokens and token flexibility but this creates a bug when both decimals are not the same causing users to lose funds also if the scale is reduced on the other hand will allow one user to take more rewards than they are entitled to and other users will not be able to withdraw their funds.
Vulnerability Details
According to the code
uint8 public rTokenDecimals;
uint8 public deTokenDecimals;
Allowing for a flexible decimal creates a situation that causes the Reward calculation to fail to accurately track rewards
When a user calls to withdraw
* @notice Allows a user to withdraw their rToken and RAAC rewards.
* @param deCRVUSDAmount Amount of deToken to redeem.
*/
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
@audit>> uint256 raacRewards = calculateRaacRewards(msg.sender);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
Based on the reward calculation we take the Rtoken balance and divide by the CRVUSD total supply but this is wrong as rtoken and detoken are allowed to have flexible decimals hence the value returned when both tokens are not both the same will be wrong
* @notice Calculates the pending RAAC rewards for a user.
* @param user Address of the user.
* @return Amount of RAAC rewards.
*/
function calculateRaacRewards(address user) public view returns (uint256) {
@audit>> rtoken >> uint256 userDeposit = userDeposits[user];
@audit>> scaled rtoken to crvusd >> uint256 totalDeposits = deToken.totalSupply();
@audit>> uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
@audit>> return (totalRewards * userDeposit) / totalDeposits;
}
Rtokens are always scaled to get CRVUSD
* @notice Calculates the amount of deToken to mint for a given rToken deposit.
* @param rcrvUSDAmount Amount of rToken deposited.
* @return Amount of deToken to mint.
*/
function calculateDeCRVUSDAmount(uint256 rcrvUSDAmount) public view returns (uint256) {
@audit>> uint256 scalingFactor = 10**(18 + deTokenDecimals - rTokenDecimals);
@audit>> return (rcrvUSDAmount * scalingFactor) / getExchangeRate();
}
* @notice Calculates the amount of rToken to return for a given deToken redemption.
* @param deCRVUSDAmount Amount of deToken to redeem.
* @return Amount of rToken to return.
*/
function calculateRcrvUSDAmount(uint256 deCRVUSDAmount) public view returns (uint256) {
@audit>> uint256 scalingFactor = 10**(18 + rTokenDecimals - deTokenDecimals);
@audit>> return (deCRVUSDAmount * getExchangeRate()) / scalingFactor;
}
If the scaling factor is larger or smaller we either send lower rewards or just try and send higher which reverts withdrawals.
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
@audit>> if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}
Impact
The possibility of occurring is low as both tokens need to be flexible and have different decimals but when it occur the impact is high as users will either lose their share of the reward or won't be able to make any withdrawals.
Tools Used
Manual review
Recommendations
Scale the userdeposit in the raac reward calculation
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
++ uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(userDeposit);
---------------------------------------------------
++ return (totalRewards * deCRVUSDAmount) / totalDeposits;