Core Contracts

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

Title: `DebtToken::mint` calculates wrong mint amount if there is a previous mint with different index

Summary

The DebtToken::mint function incorrectly calculates the amount of debt tokens to mint when a user has an existing debt position with a previous liquidity (usage) index. In cases where the index has improved since the last mint, the additional balance ("balanceIncrease") is overcalculated—due to re‐scaling of an already scaled balance—resulting in the user’s debt being inflated

Vulnerability Details

The problematic code in the mint function is as follows:

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
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());
}

When a user mints for the first time, their debt is scaled by the current index. However, on subsequent mints after an index change, the function recalculates a balance increase by using the user’s prior scaled balance. Because the balanceOf already returns a value scaled by the current normalized debt (index) from the lending pool, this additional computation results in a double scaling effect. The corresponding test case shows that the user’s final debt balance is higher than expected:

Proof Of Concept
Add the following in DebtToken.test.js

it("mints wrong amount of tokens when the liquidity index of the previous mint from the user has changed", async function () {
const mintAmount = ethers.parseEther("50");
const index = RAY; // Initial index
console.log("Attempting to mint", mintAmount.toString(), "tokens with index", index.toString());
const tx = await debtToken.connect(mockLendingPoolSigner).mint(user1.address, user1.address, mintAmount, index);
await tx.wait();
const balance = await debtToken.balanceOf(user1.address);
console.log("User balance after mint:", balance.toString());
expect(balance).to.equal(mintAmount);
const index2 = index * ethers.getBigInt("2");
await mockLendingPool.setNormalizedDebt(index2);
const tx2 = await debtToken.connect(mockLendingPoolSigner).mint(user1.address, user1.address, mintAmount, index2);
await tx2.wait();
// Due to the redundant scaling, the resulting balance is greater than expected.
const balance2 = await debtToken.balanceOf(user1.address);
console.log("User balance after mint:", balance2.toString());
expect(balance2).to.greaterThan((mintAmount * ethers.getBigInt("2")) + mintAmount);
});

Impact

  • Debt Inflation: Users’s accrued debt is overestimated. This can lead to unjustified debt obligations and may impact liquidation calculations.

  • Economic Imbalance: By inflating individual debt balances, the protocol's risk parameters and overall health metrics may be adversely affected.

Tools Used

  • Manual Code Review

  • Unit Testing (Foundry/Hardhat as demonstrated in DebtToken.test.js)

Recommendations

It is recommended to remove the redundant recalculation of the balance increase for users with an existing mint. Since the user’s balance is already scaled by the current normalized debt (index), the extra computation is unnecessary. One potential mitigation is to simply mint exactly the amount provided for subsequent mints, without adding a recalculated balance increase. For example, consider the following patch:

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
// Remove redundant recalculation of balance increase. The user’s balance is already scaled.
uint256 scaledBalance = balanceOf(onBehalfOf);
- bool isFirstMint = scaledBalance == 0;
-
- uint256 balanceIncrease = 0;
- 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;
+ // Simply update the user's index to the current index and mint the provided amount.
+ _userState[onBehalfOf].index = index.toUint128();
+ uint256 amountToMint = amount;
Updates

Lead Judging Commences

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

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

inallhonesty Lead Judge 4 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.