Summary
The DebtToken::totalSupply()
function incorrectly uses rayDiv
instead of rayMul
when scaling by the interest rate index. This causes the total debt supply to decrease when interest rates increase, leading to severe accounting issues in the protocol.
Vulnerability Details
The DebtToken::totalSupply()
function is meant to return the total debt including accrued interest. However, it divides by the interest rate index instead of multiplying:
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
}
This is inconsistent with balanceOf()
which correctly uses multiplication:
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
Impact
The incorrect scaling in DebtToken::totalSupply()
causes the total debt to decrease when interest rates increase, which is the opposite of what should happen. This affects critical protocol mechanisms:
The LendingPool
uses totalSupply
to track reserve.totalUsage
, which affects risk parameters
The incorrect totalSupply
affects liquidation calculations:
The totalSupply
is used in gauge and boost calculations therefore reward rates will be calculated incorrectly
For example, with 1000 tokens borrowed and 10% interest:
Proof of Concept
Create a file DebtToken.t.sol
under /test/foundry/
with the following code:
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {WadRayMath} from "../../contracts/libraries/math/WadRayMath.sol";
import {MockLendingPoolDebtToken} from "../../contracts/mocks/core/pools/MockLendingPoolDebtToken.sol";
contract DebtTokenTest is Test {
using WadRayMath for uint256;
DebtToken public debtToken;
MockLendingPoolDebtToken public mockLendingPool;
address public user = address(1);
uint256 constant RAY = 1e27;
function setUp() public {
mockLendingPool = new MockLendingPoolDebtToken();
mockLendingPool.setNormalizedDebt(RAY);
debtToken = new DebtToken("Debt Token", "DT", address(this));
debtToken.setReservePool(address(mockLendingPool));
vm.deal(address(mockLendingPool), 100 ether);
}
function testTotalSupplyBug() public {
uint256 mintAmount = 1000e18;
vm.startPrank(address(mockLendingPool));
debtToken.mint(user, user, mintAmount, RAY);
vm.stopPrank();
assertEq(debtToken.balanceOf(user), mintAmount);
assertEq(debtToken.totalSupply(), mintAmount);
uint256 newIndex = 1.1e27;
mockLendingPool.setNormalizedDebt(newIndex);
uint256 expectedBalance = mintAmount.rayMul(newIndex);
uint256 expectedSupply = mintAmount.rayDiv(newIndex);
assertEq(debtToken.balanceOf(user), expectedBalance);
assertEq(debtToken.totalSupply(), expectedSupply);
console.log("\nAfter interest rate increase:");
console.log("Balance (correct):", expectedBalance / 1e18);
console.log("Supply (wrong):", expectedSupply / 1e18);
}
}
Tools Used
Recommendations
Change rayDiv
to rayMul
in the DebtToken::totalSupply()
function:
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
- return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
+ return scaledSupply.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
Appendix: Running Foundry Tests
The POC uses Foundry in a Hardhat project. To reproduce:
curl -L https://foundry.paradigm.xyz | bash
npm i --save-dev @nomicfoundation/hardhat-foundry
For detailed setup instructions, see Hardhat + Foundry Integration Guide.