Summary
When borrowing from the Lending Pool, users get minted debt tokens. The DebtToken.mint()
function includes a logic to check whether usageIndex has increase since the last minting/burning of debt tokens for (1) this user. If it has, the function calculates the balance increase for the user (2), mints the difference and updates _userState.index
(3).
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
...
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());
...
}
DebtToken.sol#136
Vulnerability Details
The problem is that balanceIncrease
is not accounted for when burning debt tokens. This creates an inconsistency, where users have less debt tokens than they should have. (See example at PoC)
function burn(
address from,
uint256 amount,
uint256 index
...
uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, amount.toUint128());
...
}
DebtToken.sol#181
Impact
Users will have less debt than they should have, which can lead to loss of funds for the protocol and users.
Proof Of Concept
Let's say we have the following scenario:
(Note: the following numbers are for demonstration purposes only)
usageIndex is 1.15e27
_userState[user].index
is 1.1e27
user has 500e18 debt tokens
user burns 50e18 debt tokens (repays debt)
some time passes and usageIndex
is now 1.35e27
user mints 50e18 debt tokens (borrows)
balanceIncrease
is calculated as
_userState[user].index
is updated to 1.15e27
user will have debt tokens
If the balanceIncrease
was accounted for when burning debt tokens, the user would've had debt tokens
Click to reveal PoC
Place the following test case in LendingPool.test.js
below describe("Borrow and Repay"
:
it.only("should show balanceIncrease isn't accounted", async function () {
const borrowAmount = ethers.parseEther("25");
await lendingPool.connect(user1).borrow(borrowAmount);
const debtAmount = await debtToken.balanceOf(user1.address);
const usageIndex = await lendingPool.getNormalizedDebt();
console.table({ debtAmount, usageIndex });
await ethers.provider.send("evm_increaseTime", [1 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine");
await lendingPool.connect(user1).updateState();
const newDebtAmount = await debtToken.balanceOf(user1.address);
const newUsageIndex = await lendingPool.getNormalizedDebt();
const repaymentAmount = ethers.parseEther("5");
await lendingPool.connect(user1).repay(repaymentAmount);
const repayDebtAmount = await debtToken.balanceOf(user1.address);
expect(newDebtAmount - repayDebtAmount).to.be.eq(repaymentAmount);
expect(usageIndex).to.be.lt(newUsageIndex);
console.table({
newDebtAmount,
newUsageIndex,
repaymentAmount,
delta: newDebtAmount - repayDebtAmount,
});
});
Recommendation
Update DebtToken.burn()
to account for balanceIncrease
when burning debt tokens.
function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
uint256 userBalance = balanceOf(from); // edin pyt umnojeno
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
+ if (balanceIncrease > 0) {
+ _mint(from, balanceIncrease.toUint128());
+ }
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}