Summary
The mint
function in DebtToken.sol
incorrectly adds the balanceIncrease
to the amount
when minting new tokens. This results in double-counting of interest accrual since the balanceIncrease
is already accounted for in the user's balance through the index scaling mechanism.
Vulnerability Details
In the mint
function, after calculating the balanceIncrease
(which represents accrued interest), the function adds it to the original amount
to create amountToMint
:
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());
}
This is incorrect because the balanceIncrease
is already accounted for in the user's balance through the index scaling mechanism. The balance increase due to interest accrual is automatically reflected when the user's balance is queried through balanceOf()
, which scales the stored balance by the current index.
Impact
This vulnerability leads to users being minted more debt tokens than they should receive, effectively double-counting the interest accrual. This results in users owing more debt than they actually borrowed, which could lead to:
The impact is HIGH severity because it directly affects the core accounting mechanism of the protocol and results in direct loss of funds for users through excess debt.
Tools Used
Manual Review
Proof of Concept
Add the following test case to the test/unit/core/tokens/DebtToken.test.js
file:
it("debt mint bigger amount", async function () {
const mintAmount = ethers.parseEther("100");
const index = RAY;
await debtToken.connect(mockLendingPoolSigner).mint(user1.address, user1.address, mintAmount, index);
{
const userBalance = await debtToken.balanceOf(user1.address);
expect(userBalance).to.equal(mintAmount);
console.log("User1 balance after mint:", userBalance.toString());
}
const newIndex = RAY * 11n / 10n;
await mockLendingPool.setNormalizedDebt(newIndex);
{
const userBalance = await debtToken.balanceOf(user1.address);
const expectedBalance = mintAmount * newIndex / RAY;
expect(userBalance).to.equal(expectedBalance);
console.log("User1 balance after interest:", userBalance.toString());
}
await debtToken.connect(mockLendingPoolSigner).mint(user1.address, user1.address, mintAmount, newIndex);
{
const userBalance = await debtToken.balanceOf(user1.address);
const accumulatedDebt = mintAmount * newIndex / RAY;
const expectedNewBalance = accumulatedDebt + mintAmount;
console.log("User1 balance after second mint:", userBalance.toString());
console.log("Expected balance:", expectedNewBalance.toString());
console.log("Accumulated debt:", accumulatedDebt.toString());
console.log("Minted amount:", mintAmount.toString());
expect(userBalance).to.gt(expectedNewBalance);
}
});
Recommendations
Remove the addition of balanceIncrease
when minting new tokens. The mint function should simply mint the requested amount:
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
// ... existing code ...
- _mint(onBehalfOf, amountToMint.toUint128());
+ _mint(onBehalfOf, amount.toUint128());
// ... existing code ...
}