Summary
The DebtToken contract incorrectly scales down the total supply by dividing it with the normalized debt index instead of multiplying. This results in a lower-than-expected total supply, and make totalUsage(totalDept) less than it should be which affects the calculation of the utilization rate and interest rates in the LendingPool.
Vulnerability Details
The totalSupply function in the DebtToken contract scales down the total supply using rayDiv with ILendingPool(_reservePool).getNormalizedDebt(). This operation should instead multiply the total supply by the normalized debt index to reflect the correct scaled total supply, similar to how the balance is scaled.
The LendingPool uses the DebtToken total supply to compute totalUsage, which is then used to calculate the utilization rate. Since totalLiquidity is the asset amount in deposited andreserve.totalUsage should also give total dept amount without scaling. An underestimated total supply leads to an underestimated utilization rate. Since the utilization rate is a key factor in determining interest rates, an underestimated utilization rate results in lower interest rates than intended.
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
* @notice Returns the scaled total supply
* @return The total supply (scaled by the usage index)
*/
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
}
function borrow(){
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply
}
uint256 utilizationRate = calculateUtilizationRate(reserve.totalLiquidity, reserve.totalUsage);
function calculateUtilizationRate(uint256 totalLiquidity, uint256 totalDebt) internal pure returns (uint256) {
if (totalLiquidity < 1) {
return WadRayMath.RAY;
}
uint256 utilizationRate = totalDebt.rayDiv(totalLiquidity + totalDebt).toUint128();
return utilizationRate;
}
it("should show total supply is less than balance", async function () {
const mintAmount = ethers.parseEther("100");
const index = ethers.getBigInt("1100000000000000000000000000");
await mockLendingPool.setNormalizedDebt(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);
const receipt = await tx.wait();
console.log("Mint transaction successful, gas used:", receipt.gasUsed.toString());
const balance = await debtToken.balanceOf(user1.address);
console.log("User balance after mint:", balance.toString());
expect(balance).to.equal(mintAmount);
const scaledBalance = await debtToken.scaledBalanceOf(user1.address);
console.log("Scaled balance after mint:", scaledBalance.toString());
const totalSupply = await debtToken.totalSupply();
const totalSupplyScaled = await debtToken.scaledTotalSupply();
console.log("Total supply after mint:", totalSupply.toString());
console.log("Scaled total supply after mint:", totalSupplyScaled.toString());
expect(balance).to.be.greaterThan(totalSupply, "User balance is unexpectedly higher than total supply");
expect(mintAmount).to.be.greaterThan(totalSupply, "User balance is unexpectedly higher than total supply");
});
Impact
Lenders and the protocol may experience financial loss due to lower interest rates, as the utilization rate is underestimated.
User balances and total supply doesnt match
Tools Used
Manual
Recommendations
Modify the totalSupply function in the DebtToken contract to multiply the total supply by the normalized debt index instead of dividing
return scaledSupply.rayMul(ILendingPool(_reservePool).getNormalizedDebt());