Core Contracts

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

Incorrect collateral check in borrow function allows over-borrowing

Summary

In the borrow function, the contract attempts to ensure that a user’s collateral sufficiently covers the total debt after borrowing. However, the collateral check uses the expression userTotalDebt.percentMul(liquidationThreshold) to compute the required collateral. Since liquidationThreshold is set to 80% (expressed in basis points), applying percentMul in this manner effectively scales down the debt value, reducing the required collateral threshold. This miscalculation can allow users to borrow assets even when their collateral does not adequately cover their debt, resulting in undercollateralized positions.

Vulnerability Details

Incorrect Collateral Validation in borrow

The function includes the following check:

if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
  • Intended Logic:
    The goal is to ensure that after borrowing, the user’s collateral value is at least a specified percentage (80% in this case) of their total debt. This is meant to safeguard against over-borrowing relative to the collateral provided.

  • Issue with Calculation:
    The use of percentMul on userTotalDebt with the liquidationThreshold scales down the debt value. For example, if a user’s total debt is 100 units, applying an 80% factor would yield 80 units. This lowered requirement means that as long as the collateral exceeds this reduced threshold, the borrow operation is allowed—even if the actual debt-to-collateral ratio is unsound. In effect, the check does not enforce the intended strict collateralization standard, permitting undercollateralized borrowing.

Example Scenario:

  • User’s Collateral: 800 units

  • User’s Current Debt: 100 units

  • Borrowing Action: The user borrows an amount that increases userTotalDebt to 150 units

  • Expected Behavior: The required collateral should be 150 * 80% = 120 units

  • Actual Behavior: Due to the flawed calculation, the contract might only require a lower effective threshold, allowing the borrow even if the collateral is insufficient relative to the true risk.

Impact

  • Undercollateralized Borrowing:
    Users can borrow more than what their collateral securely supports, resulting in positions that are vulnerable to market fluctuations and rapid devaluation.

  • Increased Protocol Risk:
    Allowing undercollateralized positions heightens the risk of defaults, which could compromise the stability of the lending pool and the overall protocol.

  • Economic Exploitation:
    Malicious actors may exploit this vulnerability to maximize borrowing capacity, potentially extracting more value from the protocol than intended.

Detailed Proof-of-Concept (POC)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/LendingPool.sol"; // Assume this contract contains the updated borrow function
contract BorrowTest is Test {
LendingPool pool;
address user = address(0x1);
// Setup function to deploy the contract and simulate initial collateral
function setUp() public {
pool = new LendingPool();
// Simulate collateral: For testing, manually add NFT token IDs to user's data.
// Each NFT is assumed to be worth 100 units.
// For example, adding two NFTs gives a total collateral value of 200.
pool.userData(user).nftTokenIds.push(1);
pool.userData(user).nftTokenIds.push(2);
}
/**
* @notice Test that borrowing reverts when the collateral is insufficient.
*
* Scenario:
* - User's collateral: 200 units.
* - Borrow amount: 300 units.
* - Required collateral = 300 * 80% = 240 units (which exceeds 200).
*/
function testBorrowRevertsWhenCollateralInsufficient() public {
uint256 borrowAmount = 300;
vm.prank(user);
vm.expectRevert(LendingPool.NotEnoughCollateralToBorrow.selector);
pool.borrow(borrowAmount);
}
/**
* @notice Test that borrowing succeeds when the collateral is sufficient.
*
* Scenario:
* - User's collateral: 200 units.
* - Borrow amount: 200 units.
* - Required collateral = 200 * 80% = 160 units (which is within 200).
*/
function testBorrowSucceedsWhenCollateralSufficient() public {
uint256 borrowAmount = 200;
vm.prank(user);
pool.borrow(borrowAmount);
// Further assertions can verify updated debt balances or emitted events.
}
}

In this POC:

  • Insufficient Collateral Test: Verifies that borrowing 300 units (requiring 240 collateral) reverts when only 200 collateral is available.

  • Sufficient Collateral Test: Confirms that borrowing 200 units (requiring 160 collateral) is allowed with the available 200 collateral.

This targeted change ensures that the collateral requirement is calculated correctly, preventing undercollateralized borrow operations.

Tools Used

Manul review

Recommendations

Revise the Collateral Check:
Update the logic in the borrow function to calculate the required collateral without inadvertently reducing the debt value. For example, compute the required collateral using direct multiplication and division to maintain precision:

uint256 requiredCollateral = userTotalDebt * liquidationThreshold / PERCENTAGE_FACTOR;
if (collateralValue < requiredCollateral) {
revert NotEnoughCollateralToBorrow();
}

Ensure that the calculation accurately reflects the intended 80% collateral requirement.

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!