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;
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();
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
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;