Users can excessively borrow without providing sufficient collateral due to an incorrect validation within the borrow() function. The collateral value is improperly compared to the user's debt after borrowing, allowing the account to become under-collateralized without triggering a revert.
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePricesMock} from "contracts/mocks/core/primitives/RAACHousePricesMock.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {MockFunctionsRouter} from "contracts/mocks/core/oracles/MockFunctionsRouter.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {MockVeToken} from "contracts/mocks/core/tokens/MockVeToken.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
contract SetupContract is Test {
address public user1;
address public user2;
address public user3;
uint256 public currentBlockTimestamp = 1000 days;
address public treasury;
address public repairFund;
LendingPool public lendingPool;
crvUSDToken public _crvUSDToken;
RToken public rToken;
MockVeToken public veRToken;
DebtToken public debtToken;
RAACHousePricesMock public raacHousePrices;
RAACNFT public raacNFT;
RAACHousePriceOracle public raacHousePriceOracle;
FeeCollector public feeCollector;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
DEToken public deToken;
RAACToken public raacToken;
uint256 public constant INITIAL_PRIME_RATE = 1e26;
function setUp() external {
vm.warp(currentBlockTimestamp);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
stabilityPool = new StabilityPool(address(this));
veRToken = new MockVeToken();
_crvUSDToken = new crvUSDToken(address(this));
rToken = new RToken("rtoken", "rtk", address(this), address(_crvUSDToken));
raacToken = new RAACToken(address(this), 0, 0);
deToken = new DEToken("deToken", "detk", address(this), address(rToken));
debtToken = new DebtToken("debtToken", "dtk", address(this));
raacHousePrices = new RAACHousePricesMock();
raacNFT = new RAACNFT(address(_crvUSDToken), address(raacHousePrices), address(this));
raacHousePriceOracle = new RAACHousePriceOracle(
address(new MockFunctionsRouter()), bytes32(bytes("fun-ethereum-mainnet-1")), address(this)
);
feeCollector = new FeeCollector(address(rToken), address(veRToken), treasury, repairFund, address(this));
lendingPool = new LendingPool(
address(_crvUSDToken),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(_crvUSDToken),
address(lendingPool)
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
_crvUSDToken.mint(user1, 10000e18);
_crvUSDToken.mint(user2, 100e18);
_crvUSDToken.mint(user3, 1000e18);
}
function testDepositNFTandBorrow() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
vm.stopPrank();
raacHousePrices.setTokenPrice(1, 100e18);
vm.startPrank(user3);
_crvUSDToken.approve(address(raacNFT), 100e18);
raacNFT.mint(1, 100e18);
raacNFT.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
lendingPool.borrow(120e18);
vm.stopPrank();
}
Here the user deposits a nft worth 100 crvUSD, but is able to borrow assets valued at 120.
This vulnerability allows users to borrow more than they should, leaving their accounts under-collateralized. This could destabilize the protocol, allowing users to drain funds or expose liquidity providers to significant losses.
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();
}