Core Contracts

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

Wrong debt tokens accounting in borrow() causes repay() to revert in certain cases

Summary

When borrowing from the Lending Pool, the function calls DebtToken.mint() in order to mint the debt tokens to the user.

(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);

LendingPool.sol#353

If we take a look at DebtToken.mint()'s implementation, we can see that theres logic for calculating a balance increase for the user if the usageIndex has increased from before.

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#155

Vulnerability Details

The problem is that balanceIncrease is accounted for when updating the user's scaledDebtBalance in the LendingPool contract.

šŸ“Œ uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
​
// Mint DebtTokens to the user (scaled amount)
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
​
// Transfer borrowed amount to user
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
​
šŸ“Œ user.scaledDebtBalance += scaledAmount;

LendingPool.sol#353

Impact

This creates an inconsistency between DebtToken.balanceOf(user) and LendingPool.user.scaledDebtBalance which makes repay() revert when trying to repay the whole debt.

Infact even the user specifies the exact amount of dept to repay (user.scaledDebtBalance), the function will burn all of user's debt tokens but leave him with scaledDebtBalance > 0;

Proof Of Concept

Click to reveal PoC Place the following test case in `LendingPool.test.js` below `describe("Borrow and Repay"`:
it.only("should showcase inconsistency", async function () {
const borrowAmount = ethers.parseEther("25");
​
// User1 borrows 25e18 tokens (Any amount works!)
await lendingPool.connect(user1).borrow(borrowAmount);
​
let debtAmount = await debtToken.balanceOf(user1.address);
let scaledUsage = await lendingPool.getUserDebt(user1.address);
let delta = debtAmount - scaledUsage;
console.log("User1 debt, scaledUsage, delta on first borrow");
console.table({ debtAmount, scaledUsage, delta });
​
// Some time passes, so usageIndex increases
await ethers.provider.send("evm_increaseTime", [1 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine");
​
// User1 borrows 25e18 tokens again
await lendingPool.connect(user1).borrow(borrowAmount);
debtAmount = await debtToken.balanceOf(user1.address);
scaledUsage = await lendingPool.getUserDebt(user1.address);
delta = debtAmount - scaledUsage;
console.log("User1 debt, scaledUsage, delta on second borrow");
console.table({ debtAmount, scaledUsage, delta });
​
// User tries to repay all of his debt at once but fails.
await expect(
lendingPool.connect(user1).repay(debtAmount)
).to.be.revertedWithPanic(0x11);
console.log("Repaying all debt at once failed as expected");
​
await lendingPool.connect(user1).repay(debtAmount - delta);
​
debtAmount = await debtToken.balanceOf(user1.address);
scaledUsage = await lendingPool.getUserDebt(user1.address);
delta = debtAmount - scaledUsage;
console.log("User1 debt, scaledUsage, delta after repaying all but delta");
console.table({ debtAmount, scaledUsage, delta });
​
expect(delta).to.be.gt(0);
expect(delta).to.be.gt(scaledUsage);
});

Logs:

User1 debt, scaledUsage, delta on first borrow
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ (index) │ Values │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ debtAmount │ 25000000000000000000n │
│ scaledUsage │ 25000000000000000000n │
│ delta │ 0n │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
User1 debt, scaledUsage, delta on second borrow
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ (index) │ Values │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ debtAmount │ 50003585486944660850n │
│ scaledUsage │ 50001792679196224555n │
│ delta │ 1792807748436295n │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
Repaying all debt at once failed as expected
User1 debt, scaledUsage, delta after repaying all but delta
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ (index) │ Values │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ debtAmount │ 1792807748436295n │
│ scaledUsage │ 0n │
│ delta │ 1792807748436295n │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
āœ” should showcase inconsistency (1163ms)

Tools Used

Manual review

Recommendations

Update the following line in LendingPool.sol#borrow()

- user.scaledDebtBalance += scaledAmount;
+ user.scaledDebtBalance += amountMinted;
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::borrow tracks debt as user.scaledDebtBalance += scaledAmount while DebtToken mints amount+interest, leading to accounting mismatch and preventing full debt repayment

Support

FAQs

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

Give us feedback!