Core Contracts

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

Risk of liquidation due to flawed debt repayment logic

Summary

In DebtToken.burn(), the amount in underlying units is compared against the borrower's DebtToken balance which is a scaled value. This results in incorrect debt repayment contrary to what the user intended to do.

Vulnerability Details

The protocol overides the normal _update() as follows for DebtToken:

function _update(address from, address to, uint256 amount) internal virtual override {
>> if (from != address(0) && to != address(0)) {
revert TransfersNotAllowed();
}
// @audit Only allow minting and burning
>> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
super._update(from, to, scaledAmount);
emit Transfer(from, to, amount);
}

This function is designed to first scale the amount into scaledAmount via the rayDiv function based on the current usageIndex. Notice that _update() is an internal function invoked in functions such as _mint() and _burn() which here are used to mint and burn DebtTokens to/from users respectively.

Now, when a user intends to borrow, they specify the amount of reserve tokens they need. DebtToken.mint() is then invoked to mint this user debt tokens in exchange:

// @audit amount is scaled as done in _update
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
---SNIP---

As seen above, the amount is first scaled via the rayDiv(index) as done by the _update() and as such, when minting is done, the amount of debt tokes received by the user is amountScaled and they supply amount of reserve tokens.

On the other hand, during repayment, a user specifies the amount of reserve tokens they wish to send.
However, the following is done in DebtToken.burn():

// @audit-info Borrower's debt token balance is retrieved
uint256 userBalance = balanceOf(from);
---SNIP---
// @audit-issue amount of reserve tokens to be supplied is compared against debt token balance
>> if(amount > userBalance){
amount = userBalance;
}
// @audit-issue amount scaled here
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
// @audit-issue Incorrect amount of debt tokens burned
_burn(from, amount.toUint128());
// @audit-issue `amount` returned here is not what the user wanted to repay
return (amount, totalSupply(), amountScaled, balanceIncrease);

Issue breakdown:

  • The amount of reserve tokens to be supplied is is in underlying units.

  • User's debt token balance is a scaled value which was performed in _update() during minting

  • The function however, compares these two values which is not correct.

Scenario:

Lets say that with the current usageIndex, 400 units of reserve tokens translate to 200 debt tokens

  • A user has 300 debt tokens

  • As such, if they piad 400 units of reserve tokens, they would burn 200 debt tokens and be left with 100 debt tokens

  • However, the 400 they wish to pay back is compared against their 300 debt tokens and since this is greater, the amount is set to 300.

  • Then amountScaled becomes 150

  • The function the calls _burn(300) and since the _update() is invoked during burning, this only burns 150 debt tokens and they are left with 150 more.

Now notice the return statement:

return (amount, totalSupply(), amountScaled, balanceIncrease);

Back to _repay() function, these returned values are used as shown here:

// @audit-info Return values fetched
>> (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer reserve assets from the caller (msg.sender) to the reserve
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;

The values are matched in this manner:

amountScaled => amount
amountBurned => amountScaled

Notice that the function pulls amountScaled i.e the amount that was reset based on user's debt token balance (200) and user's debt reduced by amountBurned which is 150.

Impact

The user was willing to pay 400 reserve tokens which could have cleared 200 units of their debt but the currect implementation forces them to pay less and clear only a small portion of their debt. This results in repeated attempts at debt repayment to finally achieve a desired outcome.

Risk of liquidation:

A user has a grace period within which they should clear their debt and opt out of liquidation train. However, instead of having their debt paid in one tx, the system prevents them. This only increases their chances of getting liquidated if the grace period runs out before they opt out.

Tools Used

Manual Review

Recommendations

Compare user's debt token balance against amountScaled then update the amount to be supplied by the user:

// @audit Retrieve Borrower's debt token balance
uint256 userBalance = balanceOf(from);
---SNIP---
- if(amount > userBalance){
- amount = userBalance;
- }
// @audit scale the amount to be supplied
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
// @audit Check this aginst user's debt token balance and Update the amountScaled and amount to be supplied
+ if(amountScaled > userBalance){
+ amountScaled = userBalance;
+ amount = amountScaled.rayMul(index);
+ }
// @audit This will burn the correct amount
_burn(from, amount.toUint128());
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.