Summary
Users of the RAAC protocol can deposit in the stability pool to get RAAC rewards minted to them.
As per the code
uint8 public rTokenDecimals;
uint8 public deTokenDecimals;
rTokenDecimals and deTokenDecimals could be different in the future. This creates inconsistencies in the calculation of the tokens for deposit -> withdraw sequence.
Vulnerability Details
Let's look at this scenario:
rTokenDecimals = 6;
deTokenDecimals = 18;
Alice deposits 100e6 rcrvUSD amount:
function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
_update();
rToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
deToken.mint(msg.sender, deCRVUSDAmount);
userDeposits[msg.sender] += amount;
_mintRAACRewards();
emit Deposit(msg.sender, amount, deCRVUSDAmount);
}
* @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) {
uint256 scalingFactor = 10**(18 + deTokenDecimals - rTokenDecimals);
return (rcrvUSDAmount * scalingFactor) / getExchangeRate();
}
Alice gets minted (100e6 * 1e30) / 1e18 = 100e18 dCRVUSD
Alice then withdraws immediatly her 100e18 dCRVUSD:
function calculateRcrvUSDAmount(uint256 deCRVUSDAmount) public view returns (uint256) {
uint256 scalingFactor = 10**(18 + rTokenDecimals - deTokenDecimals);
return (deCRVUSDAmount * getExchangeRate()) / scalingFactor;
}
* @notice Gets the current exchange rate between rToken and deToken.
* @return Current exchange rate.
*/
function getExchangeRate() public view returns (uint256) {
return 1e18;
}
* @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);
uint256 raacRewards = calculateRaacRewards(msg.sender);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}
calculateRcrvUSDAmount will return = (100e18 * 1e18)/ 1e6 = 100e30
100e30 rcrvUSD which is more than enough to drain the protocol from its rTokens.
Impact
Draining protocol
Tools Used
Manual review
Recommendations
When calculating rcrvUSDAmount switch exchange rate and scalingFactor in the return statement.