Core Contracts

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

Inconsistent debt accounting between the lending pool and debt token

Target

contracts/core/pools/LendingPool/LendingPool.sol

contracts/core/tokens/DebtToken.sol

Summary

The lending pool maintains its own internal accounting of users debt balances which is updated during borrows and repays, however due to the difference in debt token mint calculation logic and the accounting within the lending pool, these values can deviate leading to situations where the lenders debt position is unduly inflated or deflated causing loss to borrowers or the protocol depending on the state of the protocol.

Vulnerability Details

When the borrow function is called on the lending pool, the lending pool passes control to the debt token contract to mint debt tokens (relative to the amount argument) to the user, within the mint function of the DebtToken contract, a check is perform to account any balance increase between the last index and the current index, if there is any increase, the increment is added to the amount passed in by the user before the _mint function is executed using the sum as the argument.

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}

DebtToken.mint

on the other hand, within the lending pool contract, the internal accounting user.scaledDebtBalance is updated by adding the scaled output of the user input amount without considering any potential increments between the old and new indices.

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);
// Mint DebtTokens to the user (scaled amount)
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
// Transfer borrowed amount to user
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
// reserve.totalUsage += amount;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
// Rebalance liquidity after borrowing
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}

LendingPool.borrow

Impact

This would introduce deviations introducing errors during interest rate, usage rate and repayment calculations causing users to overpay or underpay debts

Tools Used

Manual Review

Recommendations

Ensure that the internal debt accounting across the protocol is consistent and accurate, consider implementing fuzzing to verify this invariant.

POC

Add test script to test/unit/core/pools/LendingPool/LendingPool.test.js

describe("POC tests", function(){
it("should check accurate mint info", async function () {
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
await raacHousePrices.setHousePrice(2, ethers.parseEther("100"));
await raacHousePrices.setHousePrice(3, ethers.parseEther("100"));
const amountToPay = ethers.parseEther("200");
await crvusd.mint(user1.address, amountToPay);
await crvusd.connect(user1).approve(raacNFT.target, amountToPay);
let allowance = await crvusd.allowance(user1.address, raacNFT.target);
console.log('allowance ' + allowance);
await raacNFT.connect(user1).mint(2, ethers.parseEther("100"));
await raacNFT.connect(user1).mint(3, ethers.parseEther("100"));
await raacNFT.connect(user1).approve(lendingPool.target, 2);
await raacNFT.connect(user1).approve(lendingPool.target, 3);
await lendingPool.connect(user1).depositNFT(2);
await lendingPool.connect(user1).depositNFT(3);
const borrowAmount = ethers.parseEther("50");
await ethers.provider.send("evm_increaseTime", [1 * 24 * 60 * 60]);
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [1 * 24 * 60 * 60]);
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [1 * 24 * 60 * 60]);
await lendingPool.connect(user1).borrow(borrowAmount);
let r = await lendingPool.reserve();
console.log('Usage index : ' + r.usageIndex);
let userdatabefore = await lendingPool.userData(user1.address);
let totalsupplybefore = await debtToken.totalSupply()
await lendingPool.connect(user1).borrow(borrowAmount);
let userdataafter = await lendingPool.userData(user1.address);
let totalsupplyafter = await debtToken.totalSupply()
console.log('user scaled balance change ' + (Number(userdataafter.scaledDebtBalance) - Number(userdatabefore.scaledDebtBalance)).toString());
console.log('total supply scaled change ' + (Number(totalsupplyafter) - Number(totalsupplybefore)).toString());
});
})
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!