Summary
The DebtToken contract has an arithmetic error in its totalSupply function that causes it to always return a scaled-down value instead of scaling it up, leading to incorrect utilization rate calculations throughout the protocol. This affects interest rate calculations, liquidations and index calulation.
Vulnerability Details
Source
The issue lies in the incorrect scaling direction in totalSupply:
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
}
This scaled total supply is then used in multiple critical protocol functions:
In LendingPool::finalizeLiquidation:
function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
}
In LendingPool::borrow:
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
}
The incorrect scaling propagates to interest rate calculations in ReserveLibrary:
function updateInterestRatesAndLiquidity(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 liquidityAdded,
uint256 liquidityTaken
) internal {
uint256 totalLiquidity = reserve.totalLiquidity;
uint256 totalDebt = reserve.totalUsage;
uint256 utilizationRate = calculateUtilizationRate(reserve.totalLiquidity, reserve.totalUsage);
rateData.currentUsageRate = calculateBorrowRate(
rateData.primeRate,
rateData.baseRate,
rateData.optimalRate,
rateData.maxRate,
rateData.optimalUtilizationRate,
utilizationRate
);
rateData.currentLiquidityRate = calculateLiquidityRate(
utilizationRate,
rateData.currentUsageRate,
rateData.protocolFeeRate,
totalDebt
);
}
POC
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import {console} from "forge-std/console.sol";
contract MockLendingPool {
function getNormalizedDebt() external pure returns (uint256) {
return 1.1e27;
}
}
contract DebtTokenPoc is Test {
using WadRayMath for uint256;
DebtToken debtToken;
address owner;
address user1;
MockLendingPool lendingPool;
function setUp() public {
lendingPool = new MockLendingPool();
owner = address(this);
user1 = makeAddr("user1");
debtToken = new DebtToken("DebtToken", "DT", owner);
debtToken.setReservePool(address(lendingPool));
vm.startPrank(address(lendingPool));
debtToken.mint(user1, user1, 1000e18, WadRayMath.RAY);
vm.stopPrank();
}
function test_incorrect_total_supply_scaling() public {
uint256 totalSupply = debtToken.totalSupply();
uint256 scaledTotalSupply = debtToken.scaledTotalSupply();
console.log("Scaled total supply: %s", scaledTotalSupply);
console.log("Total supply (incorrectly scaled down): %s", totalSupply);
console.log("Total supply (should be scaled up): %s", scaledTotalSupply.rayMul(1.1e27));
assertFalse(totalSupply > scaledTotalSupply, "Total supply should be larger than scaled supply");
}
function test_utilization_rate_impact() public {
uint256 totalLiquidity = 1000e18;
uint256 incorrectTotalDebt = debtToken.totalSupply();
uint256 correctTotalDebt = debtToken.scaledTotalSupply().rayMul(1.1e27);
uint256 incorrectUtilization = (incorrectTotalDebt * 1.1e27) / totalLiquidity;
uint256 correctUtilization = (correctTotalDebt * 1.1e27) / totalLiquidity;
console.log("Incorrect utilization rate: %s", incorrectUtilization);
console.log("Correct utilization rate: %s", correctUtilization);
assertTrue(incorrectUtilization < correctUtilization, "Utilization rate is underreported");
}
}
Impact of Incorrect Scaling
For example, with a normalizedDebt of 1.1:
Actual total supply: 1000 tokens
Current incorrect scaling: 1000 / 1.1 ≈ 909.09 tokens
Correct scaling should be: 1000 * 1.1 = 1100 tokens
This means:
Utilization rates are severely underreported
Interest rates are miscalculated
Liquidation thresholds may not trigger when they should
Protocol's economic model is fundamentally broken
Tools Used
Recommendations
Fix the scaling direction in DebtToken::totalSupply:
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());
}