Core Contracts

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

Repayment Allowed After Grace Period Leading to User Fund Loss

Summary

The RAAC protocol's lending pool allows users to repay their debt even after the liquidation grace period has elapsed, creating a scenario where users can be tricked into repaying debt when they can no longer recover their NFT collateral.

Vulnerability Details

In the _repay function of the LendingPool contract, there are no checks to verify if:

The grace period has elapsed

function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
UserData storage user = userData[onBehalfOf];
// No checks for liquidation status or grace period
// Proceeds with repayment logic...
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
// ... rest of repayment logic
}

This means users can still repay their debt even when:

  • block.timestamp > liquidationStartTime[onBehalfOf] + liquidationGracePeriod

Impact

  • Users lost their funds by repaying and NFT will be locked in the pool

##POC

describe("Liquidation", function () {
beforeEach(async function () {
const depositAmount = ethers.parseEther("100");
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);
const tokenId = 1;
await raacNFT.connect(user3).approve(lendingPool.target, tokenId);
await lendingPool.connect(user3).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("80");
await lendingPool.connect(user3).borrow(borrowAmount);
});
it("should Allow user to repay after grace period locking NFT permanently", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await lendingPool.updateState();
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
const depositAmount = ethers.parseEther("50");
await stabilityPool.connect(user2).deposit(depositAmount);
await raacHousePrices.setHousePrice(1, ethers.parseEther("70"));
await lendingPool.initiateLiquidation(user3.address);
expect(await lendingPool.isUnderLiquidation(user3.address)).to.be.true;
expect(await lendingPool.liquidationStartTime(user3.address) > 0).to.be.true;
await ethers.provider.send("evm_increaseTime", [259300]);
await ethers.provider.send("evm_mine");
// user 3 repay after grace period
const repayAMount = ethers.parseEther("90");
await crvusd.mint(user3.address, repayAMount);
await crvusd.connect(user3).approve(lendingPool.target, repayAMount);
await lendingPool.connect(user3).repay(repayAMount);
// user 3 try to close liquidation after repayment and after grace period
await expect(
lendingPool.connect(user3).closeLiquidation()
).to.be.revertedWithCustomError(lendingPool, "GracePeriodExpired");
// user 3 try to withdraw nft after repayment and after grace period
await expect(
lendingPool.connect(user3).withdrawNFT(1)
).to.be.revertedWithCustomError(lendingPool, "CannotWithdrawUnderLiquidation");
const liquidationGracePeriod = await lendingPool.liquidationGracePeriod();
console.log("liquidationGracePeriod: ", liquidationGracePeriod.toString());
const liquidationAmount = ethers.parseEther("100");
await crvusd.mint(stabilityPool.target, liquidationAmount);
// should fail when liquidation is not initiated
await expect(
stabilityPool.liquidateBorrower(user3.address)
).to.be.revertedWithCustomError(stabilityPool, "InvalidAmount");
expect(await raacNFT.balanceOf(stabilityPool.target)).to.equal(0);
});
});

Tools Used

Manual & Hardhat

Recommendations

function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
// Add checks for liquidation status
if (isUnderLiquidation[onBehalfOf] &&
block.timestamp > liquidationStartTime[onBehalfOf] + liquidationGracePeriod) {
revert CannotRepayAfterGracePeriod();
}
// Rest of repayment logic...
}
Updates

Lead Judging Commences

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

A borrower can LendingPool::repay to avoid liquidation but might not be able to call LendingPool::closeLiquidation successfully due to grace period check, loses both funds and collateral

Support

FAQs

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