Core Contracts

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

Incorrect Interest Accrual and Overestimated totalUsage in DebtToken::mint

Summary

The mint function in DebtToken incorrectly calculates totalUsage by dividing by the index instead of multiplying, leading to an underestimation of total debt.
Additionally, the balanceIncrease logic artificially inflates totalUsage, causing excessive interest accumulation.
This results in distorted protocol-wide debt metrics, potential borrower overcharging, incorrect lending rates, and overall protocol instability.

POC

  1. Below provided code is for testing purpose only & the original rectified version is in the Recommendations section.

  2. Replace the existing DebtToken::mint with the below code snippet.

  3. also we have used 2e27 & 4e27 just for testing purpose to show what happens if index increases. The code will still behave in same fashion if increment is small.

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override returns (bool, uint256, uint256) {
scb += amount.rayDiv(index);
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
uint256 TotalUsage = super.totalSupply();
TotalUsage = TotalUsage.rayDiv(index);
if (amount == 0) {
return (false, 0, TotalUsage);
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = super.balanceOf(user);
scaledBalance = scaledBalance.rayMul(index);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
// mitigation recommends commenting or removing the concept of balanceIncrease
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;
uint256 scaledAmount = uint256(amountToMint.toUint128()).rayDiv(index);
super._update(address(0), user, scaledAmount);
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
console.log("scaledDebtBalance :", scb);
uint256 balanceOf = super.balanceOf(user);
console.log("super.balanceOf :", balanceOf);
console.log("balanceOf :", balanceOf.rayMul(index));
uint256 Total_Usage = super.totalSupply();
console.log("super.totalSupply :", Total_Usage);
Total_Usage = TotalUsagee.rayDiv(index); // mitigation involves using rayMul instead
console.log("TotalUsage :", Total_Usage);
return (scaledBalance == 0, amountToMint, Total_Usage);
}
  1. Now paste the below snippet code in LendingPool.test.js

describe("Lords Test", function () {
it("mint rectification", async function () {
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
let user5
[user5] = await ethers.getSigners();
// Deploy DebtToken contract
const DebtToken = await ethers.getContractFactory("DebtToken");
let debtToken = await DebtToken.deploy("DebtToken", "DT", owner.address);
await debtToken.mint(user5, user5, 100, ethers.parseUnits("2", 27));
console.log("---------------------------");
await debtToken.mint(user5, user5, 100, ethers.parseUnits("4", 27));
});
});

Vulnerability Details

As console.log reveals :

Incorrect Interest Accrual in totalUsage Calculation

  • When borrowed 100 crvUSD, @2e27 index, totalUsage only increases by the borrowed amount, but interest accrual isn't properly reflected. Instead of dividing totalUsage by the ongoing index, it should be multiplied to correctly track the total debt (principal + interest).

Incorrect Approach (Dividing)

super.totalSupply = (100 * 1e27) / 2e27 = 50;
totalUsage = (super.totalSupply * 1e27) / 2e27;
totalUsage = (50 * 1e27) / 2e27 = 25; // Incorrect, underestimates debt

Correct Approach (Multiplying)

super.totalSupply = (100 * 1e27) / 2e27 = 50;
totalUsage = (super.totalSupply * 2e27) / 1e27;
totalUsage = (50 * 2e27) / 1e27 = 100; // Correct, accurately tracks total debt over time

Excessive totalUsage Due to Incorrect balanceIncrease Calculation

  • When borrowing 100 crvUSD @4e27, after a prior borrow of 100 crvUSD @2e27 & rectifying the rayDiv as mentioned above via rayMul, totalUsage is incorrectly inflated to 700. This overestimation occurs due to the improper handling of interest accrual in balanceIncrease:

balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
  • Issue: balanceIncrease applies an unnecessary multiplication with both the new and old index, then subtracts them, artificially compounding interest accrual and leading to an inflated debt amount.

Incorrect Approach (With balanceIncrease)

  • Borrow 100 @4e27, resulting in:

    • Total Borrowed: 100 @2e27 + 100 @4e27 = 200 crvUSD

    • Interest Accrued (Incorrectly Calculated): 500 crvUSD

    • Final totalUsage = 700 (Excessive and Incorrect)

Correct Approach (Without balanceIncrease)

  • Borrow 100 @4e27, leading to:

    • Total Borrowed: 200 crvUSD

    • Interest Accrued (Correctly Calculated): 100 crvUSD

    • Final totalUsage = 300 (Accurate Debt Representation)

Impact

  1. Multiplying by the index ensures totalUsage reflects both the borrowed amount and accumulated interest, preventing underestimation of utilization(a.k.a utilizationRatio), interest rates(currentUsageRate & currentLiquidityRate), and liquidity growth(usageIndex & liquidityIndex).

  2. Overestimated totalUsage inflates protocol-wide debt metrics, leading to incorrect economic assumptions, potential borrower overcharging due to excessive interest accumulation, miscalculated lending rates causing inefficiencies in capital allocation, and protocol instability as total debt does not match actual borrowed amounts.

Tools Used

Manual Review

Recommendations

  • Remove the balanceIncrease logic entirely to prevent artificial inflation of totalUsage.

  • Ensure totalUsage is updated correctly by multiplying by the ongoing index rather than dividing.

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
...
...
- uint256 balanceIncrease = 0;
- if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
- balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
- }
...
...
- uint256 amountToMint = amount + balanceIncrease;
+ uint256 amountToMint = amount;
...
}
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
- return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
+ return scaledSupply.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
Updates

Lead Judging Commences

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

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

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

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

Support

FAQs

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

Give us feedback!