Core Contracts

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

Wrong Ray math operation in `DebtToken::totalSupply()` causes total supply to decrease with interest increasing

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

  1. The LendingPool uses totalSupply to track reserve.totalUsage, which affects risk parameters

  2. The incorrect totalSupply affects liquidation calculations:

  3. 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:

  • User balance correctly shows 1100 tokens

  • Total supply incorrectly shows ~909 tokens

Proof of Concept

Create a file DebtToken.t.sol under /test/foundry/ with the following code:

// SPDX-License-Identifier: MIT
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;
// First mint at index 1.0
vm.startPrank(address(mockLendingPool));
debtToken.mint(user, user, mintAmount, RAY);
vm.stopPrank();
// Check initial state
assertEq(debtToken.balanceOf(user), mintAmount);
assertEq(debtToken.totalSupply(), mintAmount);
// Increase index to 1.1 (10% interest)
uint256 newIndex = 1.1e27;
mockLendingPool.setNormalizedDebt(newIndex);
// Calculate expected values using rayDiv/rayMul
uint256 expectedBalance = mintAmount.rayMul(newIndex);
uint256 expectedSupply = mintAmount.rayDiv(newIndex);
// Check values after interest rate increase
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

  • Manual review

  • Foundry

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:

  • Install Foundry using their installation script:

curl -L https://foundry.paradigm.xyz | bash
  • Install the hardhat-foundry plugin:

npm i --save-dev @nomicfoundation/hardhat-foundry
  • Add require("@nomicfoundation/hardhat-foundry"); to the top of your hardhat.config.js file.

For detailed setup instructions, see Hardhat + Foundry Integration Guide.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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 4 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.