Core Contracts

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

Incorrect Debt Calculation Due to Double Scaling in `LendignPool::_repay` Function

Summary

The _repay function in the LendingPool contract incorrectly calculates the user's debt by double-scaling the debt value. The balanceOf function in the DebtToken contract already returns a scaled debt value, but the _repay function further scales it by dividing by the usageIndex. This results in an incorrect debt calculation, leading to improper repayment amounts and potential financial discrepancies in the protocol.

Vulnerability Details

The _repay function calls IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf) to retrieve the user's debt.

function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
UserData storage user = userData[onBehalfOf];
// Update reserve state before repayment
ReserveLibrary.updateReserveState(reserve, rateData);
// Calculate the user's debt (for the onBehalfOf address)
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
// If amount is greater than userDebt, cap it at userDebt
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount;
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex);
// Burn DebtTokens from the user whose debt is being repaid (onBehalfOf)
// is not actualRepayAmount because we want to allow paying extra dust and we will then cap there
(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;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit Repay(msg.sender, onBehalfOf, actualRepayAmount);
}

The balanceOf function in the DebtToken contract already scales the debt by multiplying the user's scaled balance by the usageIndex:

function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}

This means the returned value is already scaled.

In the _repay function, the scaled debt is incorrectly divided by the usageIndex again:

uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex); // Double scaling occurs here

This results in an incorrect userScaledDebt value, leading to improper repayment calculations, the actualRepayAmount and scaledAmount are derived from the incorrectly scaled debt, causing further inaccuracies in the repayment logic.

Example

  • User's scaled balance: 100 tokens

  • Current usageIndex: 1.1e27 (10% interest)

  • User's debt (balanceOf): 100 * 1.1 = 110 tokens (already scaled)

  1. Repayment Call:

    • User calls repay(110) to repay their debt.

  2. Incorrect Calculation:

    • userDebt is retrieved as 110 tokens (already scaled).

    • userScaledDebt is calculated as 110 / 1.1 = 100 tokens (incorrectly scaled again).

    • actualRepayAmount is capped at 100 tokens instead of 110 tokens.

  3. Result:

    • The user repays only 100 tokens instead of the full 110 tokens, leaving 10 tokens of residual debt.

// Step 1: Retrieve user's debt (already scaled)
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf); // Returns 110 tokens
// Step 2: Incorrectly scale the debt again
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex); // 110 / 1.1 = 100 tokens
// Step 3: Calculate repayment amount
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount; // Capped at 100 tokens
// Step 4: Burn tokens based on incorrect amount
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex); // 100 / 1.1 ≈ 90.90 tokens

Impact

Users may underpay their debt, leaving residual balances that should have been cleared.

Tools Used

Manual Review

Recommendation

Remove the redundant scaling in the _repay function. The userDebt value returned by balanceOf is already scaled and should not be divided by the usageIndex again.

Updates

Lead Judging Commences

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

LendingPool::_repay double scales the debt

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

LendingPool::_repay double scales the debt

Support

FAQs

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

Give us feedback!