Summary
In the DebtToken::mint()
function, balanceOf(onBehalfOf)
is used to get the user's balance, which returns an already scaled balance. This scaled balance is then scaled again when calculating balanceIncrease
, resulting in an inflated debt amount.
Vulnerability Details
The balanceOf()
function in DebtToken
overrides the standard ERC20 implementation to return a scaled balance by multiplying the raw balance with the normalized debt index:
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
In the mint()
function, this scaled balance is used:
uint256 scaledBalance = balanceOf(onBehalfOf);
The balance is then scaled again when calculating balanceIncrease
:
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
This double scaling results in a much larger balanceIncrease
than intended, leading to users accumulating more debt than they should.
Impact
The incorrect calculation of balanceIncrease
leads to users being charged more interest than they should be, directly causing financial loss. This affects every mint operation where users have existing debt.
Tools Used
Manual Review
Proof of Concept
Add the following test case to the test/unit/core/tokens/DebtToken.test.js
file:
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs.js";
it("balanceIncrease inflated due to double scaling", async function () {
const mintAmount = ethers.parseEther("100");
const index = RAY;
await debtToken.connect(mockLendingPoolSigner).mint(user1.address, user1.address, mintAmount, index);
const userBalanceAfterFirstMint = await debtToken.balanceOf(user1.address);
expect(userBalanceAfterFirstMint).to.equal(mintAmount);
console.log("User1 balance after mint:", userBalanceAfterFirstMint.toString());
const newIndex = RAY * 11n / 10n;
await mockLendingPool.setNormalizedDebt(newIndex);
const userBalanceAfterInterest = await debtToken.balanceOf(user1.address);
const expectedBalanceAfterInterest = mintAmount * newIndex / RAY;
expect(userBalanceAfterInterest).to.equal(expectedBalanceAfterInterest);
console.log("User1 balance after interest:", userBalanceAfterInterest.toString());
const actualBalanceIncrease = userBalanceAfterInterest - userBalanceAfterFirstMint;
const calculatedBalanceIncrease = mintAmount * newIndex / RAY - mintAmount;
expect(actualBalanceIncrease).to.equal(calculatedBalanceIncrease);
await expect(debtToken.connect(mockLendingPoolSigner).mint(user1.address, user1.address, mintAmount, newIndex))
.to.emit(debtToken, "Mint")
.withArgs(anyValue, anyValue, anyValue, calculatedBalanceIncrease, newIndex);
});
Recommendations
Use super.balanceOf()
to get the unscaled balance before performing the scaling calculations:
// ... existing code ...
- uint256 scaledBalance = balanceOf(onBehalfOf);
+ uint256 scaledBalance = super.balanceOf(onBehalfOf);
// ... existing code ...