Core Contracts

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

Incorrect Total Supply Scaling in `DebtToken` Contract Leads to Inconsistent Balance Representation

Summary:

The totalSupply() function in the DebtToken contract incorrectly scales the total supply down by the normalized debt index instead of scaling it up. This results in a total supply value that is lower than the sum of all user balances, violating a fundamental principle of ERC20 tokenomics.

Vulnerability Details:

The DebtToken contract is designed to track user debt balances, with interest accrual reflected by the _usageIndex and normalized debt index from the associated LendingPool. The balanceOf() function correctly scales user balances up by the normalized debt index to reflect accrued interest. However, the totalSupply() function scales the total supply down by the normalized debt index.

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
// @audit should scale up the total supply <--- INCORRECT SCALING
return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt()); // Scaled down instead of up
}

The total supply should also reflect the accrued interest, meaning it needs to be scaled up, not down. The correct implementation should use rayMul (multiplication), not rayDiv (division).

Impact:

The incorrect total supply scaling has the following negative consequences:

  • Inconsistent Balance Representation: The core issue is that totalSupply() will be less than the sum of all user balances (balanceOf(user)), which contradicts the basic principles of ERC20 tokens. This will create a fundamental mismatch between the reported total supply and the actual aggregate user balances.

  • Integration Issues: Protocols and dApps that integrate with DebtToken may rely on the totalSupply() value for calculations or other logic. The incorrect scaling can lead to unexpected behavior and errors in these integrations.

  • Inconsistent state: This inconsistent state could potentially be leveraged in more complex attacks or exploits, particularly if other protocols rely on the incorrect totalSupply() value.

Proof of Concept:

  1. Alice borrows 100 units of the underlying asset. This results in 100 debt tokens being minted to Alice.

  2. The normalized debt index increases to 1.1 (representing 10% accrued interest).

  3. Alice's balanceOf() will correctly show a scaled balance of 110 (100 x 1.1).

  4. However, totalSupply() will incorrectly show a scaled total supply of approximately 90.9 (100 / 1.1), which is less than Alice's balance.

  5. This discrepancy violates the expected relationship between total supply and individual balances.

Proof of Code:

  1. Use this guide to intergrate foundry into your project: foundry

  2. Create a new file FortisAudits.t.sol in the test directory.

  3. Add the following gist code to the file: Gist Code

  4. Run the test using forge test --mt test_FortisAudits_IncorrectTotalSupplyScaling -vvvv.

function test_FortisAudits_IncorrectTotalSupplyScaling() public {
address lp = makeAddr("lp");
uint256 price = 50_000e18;
uint256 tokenId = 1;
_reserveAsset.mint(lp, price * 2);
rwaToken.mint(anon, price * 2);
// owner setting up the lending pool and minting the tokens
vm.startPrank(initialOwner);
raacHouse.setOracle(initialOwner);
raacHouse.setHousePrice(tokenId, price);
raacHouse.setHousePrice(tokenId+1, price);
debtToken.setReservePool(address(lendingPool));
vm.stopPrank();
// Lp deposits the reserve asset
vm.startPrank(lp);
_reserveAsset.approve(address(lendingPool), price * 2);
lendingPool.deposit(price * 2);
vm.stopPrank();
// Anon deposits the RWA token and mints the NFTs
vm.startPrank(anon);
rwaToken.approve(address(raacNFT), price * 2);
raacNFT.mint(tokenId, price);
raacNFT.mint(tokenId+1, price);
raacNFT.setApprovalForAll(address(lendingPool), true);
// Deposit
lendingPool.depositNFT(1);
lendingPool.depositNFT(2);
// Borrow
lendingPool.borrow(price);
skip(14 days);
lendingPool.borrow(price);
uint256 totalSupply = debtToken.scaledTotalSupply();
uint256 balance = debtToken.scaledBalanceOf(anon);
uint256 scaledTotal = debtToken.totalSupply();
console.log("Scaled Total Supply: %d", scaledTotal);
uint256 scaledBalance = debtToken.balanceOf(anon);
console.log("Scaled User Balance: %d", scaledBalance);
assert(scaledTotal != scaledBalance);
vm.stopPrank();
}

Logs before the fix:

Logs:
Scaled Total Supply: 99725073695387326585093
Scaled User Balance: 100276446352092763765664

Logs after the fix:

Logs:
Scaled Total Supply: 100276446352092763765664
Scaled User Balance: 100276446352092763765664

Tools Used:

  • Manual code review

Recommended Mitigation:

The totalSupply() function should be corrected to scale the total supply up by the normalized debt index using rayMul. This will ensure that the total supply correctly reflects the accrued interest and is consistent with the sum of all user balances.

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
- return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt()); // Incorrect: Scaling down
+ return scaledSupply.rayMul(ILendingPool(_reservePool).getNormalizedDebt()); // Corrected: Scaling up
}
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!