Core Contracts

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

Incorrect interest compounding in DebtToken::mint leads to over-inflated debt balances

Description

The DebtToken::mint function incorrectly includes accrued interest (balanceIncrease) in new debt token mints, creating a compounding error where interest is calculated on already-accrued interest. This occurs because:

  1. balanceOf() returns actual debt (scaled balance × current index)

  2. scaledBalance in mint operation uses this inflated value

  3. Subsequent interest calculations compound on the already interest-adjusted balance (e.g functions that update reverse interests before minting debt tokens like LendingPool::borrow)

Proof of Concept

  1. Initial Borrow

    • User borrows 100 tokens @ 1.0 RAY index

    • Actual debt: 100 × 1.0 = 100

    • Raw scaled balance: 100 (stored)

  2. Interest Accrual

    • Index increases to 1.1 RAY

    • Existing debt updates to: 100 × 1.1 = 110

  3. Second Borrow

  • User borrows another 100 tokens @ 1.1 RAY index

// Flawed calculation:
scaledBalance = balanceOf(user) = 110
balanceIncrease = 110 × (1.1 - 1.0) = 11
amountToMint = 100 (new) + 11 = 111
  • New debt: 110 + 111 = 221

Test case to demonstrate vulnerability:

In DebtToken.test.js, add this test and run npx hardhat test --grep "mints debt with compounded interest error"

it("mints debt with compounded interest error", async function () {
const initialAmount = ethers.parseEther("100");
const initialIndex = RAY;
// First borrow
await debtToken
.connect(mockLendingPoolSigner)
.mint(user1.address, user1.address, initialAmount, initialIndex);
// Increase index by 10%
const newIndex = (RAY * 11n) / 10n;
await mockLendingPool.setNormalizedDebt(newIndex);
// Second borrow with new index
const borrowAmount = ethers.parseEther("100");
await debtToken
.connect(mockLendingPoolSigner)
.mint(user1.address, user1.address, borrowAmount, newIndex);
// Expected: existing debt (100 * 1.1) + new debt (100 * 1.1) = 220
const expectedDebt = (initialAmount * 11n) / 10n + borrowAmount;
const actualDebt = await debtToken.balanceOf(user1.address);
// Actual debt incurred more than expected debt
// Actual=221e18 vs Expected=210e18
expect(actualDebt).to.be.gt(expectedDebt);
});

Impact

Critical Severity

  • Debt balances grow 5.2% faster than intended in this scenario (221 vs 210)

  • Compounding effect worsens with repeated borrows/rate changes

  • Protocol accounting becomes unreliable, risking insolvency

Recommendation

Option 1: Use raw scaled balances for interest calculation

- uint256 scaledBalance = balanceOf(onBehalfOf);
+ uint256 scaledBalance = super.balanceOf(onBehalfOf);
if (_userState[onBehalfOf].index < index) {
- balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
+ balanceIncrease = scaledBalance.rayMul(index - _userState[onBehalfOf].index);
}

Option 2: Omit inclusion of balanceIncrease

- // Remove balanceIncrease calculation entirely
- 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;
Updates

Lead Judging Commences

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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

Give us feedback!