Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Incorrect DebtToken totalSupply Scaling Breaks Interest Rate Calculations

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()); // @audit should be rayMul
}

This scaled total supply is then used in multiple critical protocol functions:

  1. In LendingPool::finalizeLiquidation:

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
// ...
reserve.totalUsage = newTotalSupply; // @audit uses incorrectly scaled totalSupply
// ...
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
}
  1. In LendingPool::borrow:

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ...
reserve.totalUsage = newTotalSupply; // @audit uses incorrectly scaled totalSupply
// ...
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
}
  1. 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; // @audit uses incorrectly scaled totalSupply
uint256 utilizationRate = calculateUtilizationRate(reserve.totalLiquidity, reserve.totalUsage);
// These rates are all calculated with incorrect utilization
rateData.currentUsageRate = calculateBorrowRate(
rateData.primeRate,
rateData.baseRate,
rateData.optimalRate,
rateData.maxRate,
rateData.optimalUtilizationRate,
utilizationRate
);
rateData.currentLiquidityRate = calculateLiquidityRate(
utilizationRate,
rateData.currentUsageRate,
rateData.protocolFeeRate,
totalDebt
);
}

POC

// SPDX-License-Identifier: MIT
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; // Setting the normalized debt to 1.1 in ray
}
}
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));
// Mint some initial supply - using the mint function with index
vm.startPrank(address(lendingPool));
debtToken.mint(user1, user1, 1000e18, WadRayMath.RAY);
vm.stopPrank();
}
function test_incorrect_total_supply_scaling() public {
// Get the total supply
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));
// This will pass because totalSupply is incorrectly scaled down
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);
// Calculate utilization rates
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:

  1. Utilization rates are severely underreported

  2. Interest rates are miscalculated

  3. Liquidation thresholds may not trigger when they should

  4. Protocol's economic model is fundamentally broken

Tools Used

  • Manual code review

Recommendations

  1. 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());
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!