Core Contracts

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

User will have less Debt due to lack of accounting balanceIncrease when burning Debt Tokens

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;
// @audit (1) checks if the usageIndex has increased since the last minting/burning of debt tokens
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
// @audit (2) calculates the balance increase
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
// @audit (3) updates index,
_userState[onBehalfOf].index = index.toUint128();
// @audit (3) mints amount + balanceIncrease
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;
// @audit again checks if the usageIndex has increased
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
// @audit again calculates
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
// @audit IMPORTANT: updates index, but does not mint the balanceIncrease (or subtract it from the amount for burning if possible), which allows funds to be lost
_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)

    • balanceIncrease is calculated as

    • _userState[user].index is updated to 1.15e27

  • 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 });
// Some time passes, so usageIndex increases
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);
// we can see that after repayment exactly the amount difference is = repaymentAmount
// in other words, the balanceIncrease isn't accounted for in the debt calculation
expect(newDebtAmount - repayDebtAmount).to.be.eq(repaymentAmount);
// we can see that the usageIndex has increased
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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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