Core Contracts

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

DebtToken overinflates balance after first mint

Summary

A vulnerability has been identified in the DebtToken contract's mint function where debt amounts are incorrectly scaled, leading to a double-scaling issue when calculating user balances. This occurs because the contract applies scaling both during minting and balance retrieval, resulting in users receiving more debt tokens than intended.

Vulnerability Details

In DebtToken#mint, the user's debt balance is fetched using balanceOf(), which scales the balance with the current index, thereby accounting for interest accrual. After this, the mint function incorrectly again applies scaling to the already-scaled balance. A balanceIncrease variable is populated and unnecessarily added to the mint amount, thereby overinflating the user's debt.

Here is a PoC showing a failing test because the amount of debt has been inflated above the expected amount after the second mint:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "contracts/core/tokens/DebtToken.sol";
contract DebtTokenDoubleMintTest is Test {
DebtToken public debtToken;
MockLendingPoolDebtToken public mockLendingPool;
address public owner;
address public user1;
uint256 constant RAY = 1e27;
function setUp() public {
owner = makeAddr("owner");
user1 = makeAddr("user1");
mockLendingPool = new MockLendingPoolDebtToken();
vm.prank(owner);
debtToken = new DebtToken("DebtToken", "DT", owner);
vm.prank(owner);
debtToken.setReservePool(address(mockLendingPool));
}
function test_ProveDoubleScalingBug() public {
// Set the initial index to 1
mockLendingPool.setNormalizedDebt(RAY); // Initial index = 1
// Initial mint at index 1
uint256 initialMintAmount = 100e18;
console.log("Initial mint amount:", initialMintAmount);
vm.prank(address(mockLendingPool));
debtToken.mint(user1, user1, initialMintAmount, RAY);
// Verify balance equals mint amount
uint256 initialBalance = debtToken.balanceOf(user1);
console.log("Initial balance:", initialBalance);
assertEq(initialBalance, initialMintAmount, "Initial balance should be 100e18");
// Simulate interest accrual by increasing the index by 10%
uint256 newIndex = RAY * 110 / 100; // 1.1 * RAY
mockLendingPool.setNormalizedDebt(newIndex);
// Fetch balance after interest accrual
uint256 balanceAfterInterestAccrual = debtToken.balanceOf(user1);
console.log("Balance after interest accrual:", balanceAfterInterestAccrual);
// Calculate expected balance after interest accrual
uint256 calculatedBalanceAfterInterestAccrual = initialMintAmount * 110 / 100;
// Verify balance after interest accrual corresponds to the expected value
assertEq(balanceAfterInterestAccrual, calculatedBalanceAfterInterestAccrual, "Pre-mint scaled balance should be 110e18");
// Mint a second time (now at the new index)
uint256 secondMintAmount = 50e18;
console.log("Second mint amount:", secondMintAmount);
vm.prank(address(mockLendingPool));
(bool isFirst, uint256 amountMinted, uint256 newSupply) =
debtToken.mint(user1, user1, secondMintAmount, newIndex);
// Fetch final balance
uint256 finalBalance = debtToken.balanceOf(user1);
console.log("Final balance should be:", balanceAfterInterestAccrual + secondMintAmount);
console.log("Final balance is:", finalBalance);
assertEq(finalBalance, balanceAfterInterestAccrual + secondMintAmount, "Final balance should be equal to preMintScaledBalance + secondMintAmount");
}
}
contract MockLendingPoolDebtToken {
uint256 private _normalizedDebt;
function setNormalizedDebt(uint256 normalizedDebt) external {
_normalizedDebt = normalizedDebt;
}
function getNormalizedDebt() external view returns (uint256) {
return _normalizedDebt;
}
}

console output:

Initial mint amount: 100000000000000000000

Initial balance: 100000000000000000000

Balance after interest accrual: 110000000000000000000

Second mint amount: 50000000000000000000

Final balance should be: 160000000000000000000

Final balance is: 171000000000000000000

The test file can be added to /test and run with forge test -vv after adding Foundry to the project:

https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry

Impact

After the user's initial mint, if the index is incremented and the user attempts to mint again, this will result in the user's debt being inflated above the correct amount.

Tools Used

Foundry (can be easily added to the project by following this guide: https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry)

Recommendations

Modify the mint function to mint the amount specified and update the user's index to the current index. This will yield the correct amount of debt. This modified function will cause the PoC test to pass successfully.

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
_userState[onBehalfOf].index = index.toUint128();
_mint(onBehalfOf, amount.toUint128());
emit Transfer(address(0), onBehalfOf, amount);
emit Mint(user, onBehalfOf, amount, amount, index);
return (scaledBalance == 0, amount, totalSupply());
}
Updates

Lead Judging Commences

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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