Core Contracts

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

Improper debt handling in the burn function in DebtToken contract allows for borrowers to pay less but expose them to forced liquidation

Summary

Borrowers will incure more debt tokens when at a point their borrower index is less than the reserve usage index.

Vulnerability Details

When borrowers take loan from the LendingPool contract, users receive debt token proportional to the loan they take. Borrowers who take loan when their usage index is less than the reserve usage index, interest is calculated and adds accrued debt to the amount of reserveasset being taken. Observe the mint function:

function mint(address user, address onBehalfOf, uint256 amount, uint256 index)
external
override
onlyReservePool
returns (bool, uint256, uint256)
{
...
uint256 balanceIncrease = 0;
//@audit observe how the balanceIncrease goes to add up to what is minted
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());
}

The mint function ensures to update amountToMint with the accured debt (balanceIncrease). This is not considered when borrowers repay their loan in the burn function.

function burn(address from, uint256 amount, uint256 index)
external
override
onlyReservePool
returns (uint256, uint256, uint256, uint256)
{
...
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount; //@audit the balanceIncrease is not considered (observe the mint function).
}
_userState[from].index = index.toUint128();
if (amount > userBalance) {
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}

Root Cause:

The burn function fails to properly account for the accrued interest (balanceIncrease) before burning the repayment amount. While the mint function correctly adds accrued debt, the burn function merely computes it but does not adjust the repayment amount accordingly. This can lead to incorrect debt reductions, allowing users to repay less than they actually owe.

However, another issue could arise from this where borrowers face forced liquidation due to the flaw in the burn function.

POC:

This POC confirms interest bypass when users repay debt tokens (mint/burn).

https://gist.github.com/Ephraim-nonso/070b2b7d779e5cf867b3c2ef308288ad

Impact

  • Debt underpayment exploit

  • Borrowers can be forced liquidated due to remaining debt tokens after a partial repay

Tools Used

Manual review and Foundry.

Recommendations

Consider accured interest and add to amount when users repay their debts. This is to avoid underpayment and possibility of forcibly liquidating borrowers.

function burn(address from, uint256 amount, uint256 index)
external
override
onlyReservePool
returns (uint256, uint256, uint256, uint256)
{
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
}
_userState[from].index = index.toUint128();
// ✅ Ensure total repayment accounts for accrued interest
uint256 totalDebt = userBalance + balanceIncrease;
if (amount > totalDebt) {
amount = totalDebt; // Cap amount to avoid over-burning
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

Support

FAQs

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