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:
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 {
mockLendingPool.setNormalizedDebt(RAY);
uint256 initialMintAmount = 100e18;
console.log("Initial mint amount:", initialMintAmount);
vm.prank(address(mockLendingPool));
debtToken.mint(user1, user1, initialMintAmount, RAY);
uint256 initialBalance = debtToken.balanceOf(user1);
console.log("Initial balance:", initialBalance);
assertEq(initialBalance, initialMintAmount, "Initial balance should be 100e18");
uint256 newIndex = RAY * 110 / 100;
mockLendingPool.setNormalizedDebt(newIndex);
uint256 balanceAfterInterestAccrual = debtToken.balanceOf(user1);
console.log("Balance after interest accrual:", balanceAfterInterestAccrual);
uint256 calculatedBalanceAfterInterestAccrual = initialMintAmount * 110 / 100;
assertEq(balanceAfterInterestAccrual, calculatedBalanceAfterInterestAccrual, "Pre-mint scaled balance should be 110e18");
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);
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());
}