Core Contracts

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

Double Interest Calculation in `DebtToken::mint()`

Summary

The mint() function in DebtToken incorrectly applies interest twice, leading to inflated debt token balances and potential user losses.

Vulnerability Details

Initially, the project intended to accumulate user interest using _userState[onBehalfOf].index. However, the current implementation instead uses getNormalizedDebt() to compute interest. As highlighted in the marked (@>) lines, the balanceIncrease amount is still added to amountToMint, effectively applying interest twice when minting new debt tokens for the user.

// DebtToken::mint()
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
// SNIP...
@> uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
@> balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
@> _userState[onBehalfOf].index = index.toUint128();
@> uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());
// SNIP...
}
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
@> return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}

Poc

Add the following test case to test/unit/core/pools/LendingPool/LendingPool.test.js and execute it:

describe("DebtToken mint()", function () {
beforeEach("Simulate real-world interest rates", async function () {
// user2 deposits 1000e18 crvusd
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
// user1 deposits NFT
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
// user1 borrows 20e18 crvusd
const borrowAmount = ethers.parseEther("20");
await lendingPool.connect(user1).borrow(borrowAmount);
await lendingPool.connect(user1).updateState();
// After 365 days
await ethers.provider.send("evm_increaseTime", [365 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.connect(user1).updateState();
});
it("output:", async function () {
console.log("user1 debtToken balance:", await debtToken.balanceOf(user1.address));
// User1 borrows another 20e18 crvusd
const borrowAmount = ethers.parseEther("20");
await lendingPool.connect(user1).borrow(borrowAmount);
await lendingPool.connect(user1).updateState();
console.log("user1 debtToken balance:", await debtToken.balanceOf(user1.address));
});
});

output:

LendingPool
DebtToken mint()
Promise { <pending> }
user1 debtToken balance: 20525536117137245878n
user1 debtToken balance: 41064881647788219951n # ❌ Interest is double calculated
✔ output:

Impact

The double-counting of interest leads to incorrect debt token balances, resulting in excessive debt accumulation for users. This miscalculation can cause financial losses and disrupt expected protocol behavior.

Tools Used

Manual Review

Recommendations

Remove the redundant balanceIncrease calculation to prevent double interest accumulation:

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);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
- if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
- balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
- }
_userState[onBehalfOf].index = index.toUint128();
- uint256 amountToMint = amount + balanceIncrease;
+ uint256 amountToMint = amount; // ✅ Prevent double-counting interest
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}

test again:

LendingPool
DebtToken mint()
Promise { <pending> }
user1 debtToken balance: 20525536117137245878n
user1 debtToken balance: 40525536117137245878n
✔ output:
Updates

Lead Judging Commences

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