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.
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
@> 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());
}
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 () {
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
const tokenId = 1;
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("20");
await lendingPool.connect(user1).borrow(borrowAmount);
await lendingPool.connect(user1).updateState();
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));
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
✔ 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: