Core Contracts

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

User can’t withdraw his NFT, make it permanently locked.

Summary

If the time passed and multiple deposits were done, if the user tries to repay all the debt, some scaledDebtBalance would be left, which wouldn't allow to withdraw the NFt

Vulnerability Details

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "lib/forge-std/src/Test.sol";
//Import which is neccessery for the LendingPool.sol
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/DebtToken.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/minters/RAACMinter/RAACMinter.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
interface ITokenERC20{
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract ArsenTest is Test{
//Actual, non-modifier contracts
RToken public rToken;
DebtToken public dToken;
RAACNFT public raacNFT;
RAACToken public raacToken;
//deploy minters
RAACMinter public raacMinter;
//Mock contracts
HousePrices public housePrices;
PriceOracle public priceOracle;
//deploy the Lending Pool
LendingPool public lendingPool;
//deploy Stability Pool
StabilityPool public stabilityPool;
address public attacker = address(8888);
address public user_1 = address(1111);
address public user_2 = address(2222);
address CRV_USD = address(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E);
string ETH_RPC_URL = "https://mainnet.infura.io/v3/2cef90f9f3f44a1fb5201d8547183d42";
function setUp() public{
uint256 ethFork = vm.createFork(ETH_RPC_URL);
vm.selectFork(ethFork);
housePrices = new HousePrices();
priceOracle = new PriceOracle();
rToken = new RToken(
"rToken",
"RTOKEN",
address(this), //init owner
CRV_USD //crvUsd addr
);
dToken = new DebtToken(
"Debt Token",
"DBTTOKEN",
address(this) //init owner
);
raacNFT = new RAACNFT(
CRV_USD, //token
address(housePrices), //house prices
address(this) //init owner
);
raacToken = new RAACToken(
address(this),
500, //initialSwapTaxRate
500 //initialBurnTaxRate
);
lendingPool = new LendingPool(
CRV_USD, //crvUsd addr
address(rToken), //rToken
address(dToken), //debtToken
address(raacNFT), //raacNFT
address(priceOracle),//priceOracle addr
100000000000000000000000000 //init prime rate
);
stabilityPool = new StabilityPool(address(this));
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
address(this)
);
stabilityPool.initialize(
address(rToken),
address(dToken),
address(raacToken),
address(raacMinter), //raacMinter
CRV_USD,
address(lendingPool)
);
//@audit TO DO - set the setParameter , like threshold, e.t.c for the Lending Pool
//set the reserve pool for RToken to be able to mint stuff
rToken.setReservePool(address(lendingPool));
//set the reserve pool for the DToken to be able to mint stuff
dToken.setReservePool(address(lendingPool));
}
//forge test --match-test test_deposit -vvvv
function test_deposit() public{
//---------------------------------
//@note #1 - DEPOSIT THE CRVusd INTO THE LENDING POOL
deal(CRV_USD, attacker, 1_000_000e18); //deal 200 crvUSD to the attacker
vm.startPrank(attacker);//start
ITokenERC20(CRV_USD).approve(address(lendingPool), 1_000_000e18);
lendingPool.deposit(1_000_000e18); //deposit 100 crvUSD into the pool
//check the balances after the deposit
assertEq(ITokenERC20(CRV_USD).balanceOf(address(rToken)), 1_000_000e18);
assertEq(rToken.balanceOf(attacker), 1_000_000e18);
vm.stopPrank();//stop
//---------------------------------
//---------------------------------
//@note #2 - DEPOSIT THE NFT INTO THE LENDING POOL AS A NORMAL USER
deal(CRV_USD, user_1, 10_000e18);
vm.startPrank(user_1); //start
ITokenERC20(CRV_USD).approve(address(raacNFT), 10_000e18);
raacNFT.mint(1, 10_000e18);
raacNFT.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
vm.stopPrank(); //stop
//---------------------------------
//---------------------------------
//@note #3 - BORROW THE FUNDS FROM THE LENDING POOL
vm.startPrank(user_1); //start
lendingPool.borrow(10_000e18);
assertEq(dToken.balanceOf(user_1), 10_000e18);
vm.stopPrank(); //stop
//(,,,,,,,uint256 totalUsageAfterBorrow) = lendingPool.getAllUserData(user_1);
//---------------------------------
//after the user_1 has borrowed the funds, activity spike has happened and many users deposited NFT and borrowed money, increasing usage index
for(uint i = 2; i < 50; i++){
address user = vm.addr(i);
deal(CRV_USD, user, 10_000e18);
vm.startPrank(user); //start
ITokenERC20(CRV_USD).approve(address(raacNFT), 10_000e18);
raacNFT.mint(i, 10_000e18);
raacNFT.approve(address(lendingPool), i);
lendingPool.depositNFT(i);
lendingPool.borrow(10_000e18);
vm.stopPrank(); //stop
}
//1 year has passed. After that, user has decided to finally repay the borrowed funds.
vm.warp(block.timestamp + 365 days);
lendingPool.updateState();
//---------------------------------
//@note #4 - REPAY THE FUNDS TO THE LENDING POOL
vm.startPrank(user_1);
ITokenERC20(CRV_USD).approve(address(lendingPool), 10_000e18);
lendingPool.repay(10_000e18);
vm.stopPrank(); //stop
//---------------------------------
//@audit however the scaledDebtBalance still has left, even if the user has repaid all debt.
assertEq(ITokenERC20(CRV_USD).balanceOf(user_1), 0);
assertGt(dToken.balanceOf(user_1), 60e18);
//What it can lead to?
//First and foremost, the user can't withdraw his NFT, leave it permanently locked.
vm.prank(user_1);
vm.expectRevert();
lendingPool.withdrawNFT(1);
}
}
//@note MOCK CONTRACTS FOR THE POC PURPOSES ONLY
contract HousePrices{
function tokenToHousePrice(uint256 _tokenId) external view returns (uint256){
return 10_000e18;
}
}
contract PriceOracle{
function getLatestPrice(uint256 _tokenId) external view returns (uint256, uint256){
return(10_000e18, block.timestamp);
}
}

Impact

Funds stuck

Tools Used

Recommendations

Ensure the scaledDebtBalance is cleared upon repayment

Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::borrow tracks debt as user.scaledDebtBalance += scaledAmount while DebtToken mints amount+interest, leading to accounting mismatch and preventing full debt repayment

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.