Summary
The user deposits rToken to stability pool and receive RAAC token as reward. Malicious user can drain RAAC rewards of the pool by invoking StabilityPool.withdraw()
with tiny withdrawal amounts multiple times.
Vulnerability Details
In the StabilityPool.withdraw()
function, it calculates the amount of rewards which user will receive with calculateRaacRewards(msg.sender)
.
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);
}
In StabilityPool.calculateRaacRewards()
function, it calculates the amount of reward with the total deposit amount of user, not withdrawal amount: (totalRewards * userDeposit) / totalDeposits
. This allows user to receive reward for total deposit amount regardless to withdraw amount - deCRVUSDAmount
.
Therefore, attacker can invoke withdraw()
with tiny amount of deCRVUSDAmount
multiple times and will receive full reward for total deposit amount every time.
function calculateRaacRewards(address user) public view returns (uint256) {
@> uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
@> return (totalRewards * userDeposit) / totalDeposits;
}
Attack path:
Attacker deposits certain amount of rToken to stability pool.
Attacker invokes withdraw()
with tiny withdrawal amount several times.
Each time, he will receive rewards for total deposit amount and will drain the reward of the pool.
Impact
The RAAC reward of the stability pool can be drained.
Tools Used
Manual Review
Recommendations
Add param withdraw amount to the StabilityPool.calculateRaacRewards()
function and calculates the reward amount based on withdraw amount.
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);
+ uint256 raacRewards = calculateRaacRewards(msg.sender, deCRVUSDAmount);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
...
}
- function calculateRaacRewards(address user) public view returns (uint256) {
+ function calculateRaacRewards(address user, uint256 amount) public view returns (uint256) {
...
}