Core Contracts

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

Linear Scaling in `LendingPool.getUserDebt` Returns Inaccurate Debt for Compound Interest Debt

Summary

While the LendingPool contract's getUserDebt function attempts to calculate a user's current debt by scaling up the stored debt balance with the current usage index, this linear scaling is fundamentally inaccurate for debts that accrue compound interest. Using the DebtToken's balanceOf function will provide the accurate debt amount.

Vulnerability Details

The getUserDebt function calculates the user's debt by multiplying the stored scaledDebtBalance by the current reserve.usageIndex. This approach assumes a linear interest model. However, lending protocols typically use compound_ interest.

function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
return user.scaledDebtBalance.rayMul(reserve.usageIndex); // INCORRECT: Linear scaling for compound interest
}
  1. Linear vs. Compound Interest: Linear interest accrues only on the principal amount. Compound interest accrues on both the principal and the accumulated interest. The reserve.usageIndex reflects the accumulated interest, but using it in a linear calculation does not accurately capture the effect of compounding.

  2. Inaccurate Debt Calculation: Because of the difference between linear and compound interest, the getUserDebt function will consistently underestimate the user's actual debt. The longer the debt is outstanding, the greater the discrepancy will be.

Impact

  • Liquidations: Liquidations may be triggered prematurely or delayed because the system relies on an incorrect debt value.

  • Borrowing Power: Users' borrowing power calculations, which are based on their debt, will be incorrect, potentially allowing them to borrow more than they should.

  • Accounting and Reporting: The protocol's overall accounting and reporting will be flawed.

  • Potential for Exploitation: This discrepancy can be exploited. Because the reported debt is lower than the actual debt, malicious users might be able to strategically interact with the protocol to their advantage. For instance, they might be able to avoid liquidation for longer than they should.

Proof of Concept

  1. Alice borrows 100 units of an asset.

  2. The loan accrues compound interest over time.

  3. The reserve.usageIndex increases to reflect the accrued interest.

  4. getUserDebt calculates Alice's debt using linear scaling, resulting in a value slightly lower than her actual debt.

  5. This difference, while seemingly small initially, grows over time due to the compounding effect.

Proof Of Code:

  1. Use this guide to intergrate foundry into your project: foundry

  2. Create a new file FortisAudits.t.sol in the test directory.

  3. Add the following gist code to the file: Gist Code

  4. Fix the wrong user debt storing in the LendingPool contract (separate report submitted for this issue)

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ... other checks ...
// ... calculations ...
- uint256 scaledAmount = amount.rayDiv(reserve.usageIndex); // Scaled amount
// 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; // INCORRECT: Should be the actual amount borrowed
+ user.scaledDebtBalance += amount; // CORRECT: Store the actual amount borrowed
reserve.totalUsage = newTotalSupply;
// ... other code ...
}

5.Run the test using forge test --mt test_FortisAudits_WrongUserDebtReturned -vvvv.

function test_FortisAudits_WrongUserDebtReturned() public {
address lp = makeAddr("lp");
uint256 price = 50_000e18;
uint256 tokenId = 1;
_reserveAsset.mint(lp, price * 2);
rwaToken.mint(anon, price * 2);
// owner setting up the lending pool and minting the tokens
vm.startPrank(initialOwner);
raacHouse.setOracle(initialOwner);
raacHouse.setHousePrice(tokenId, price);
raacHouse.setHousePrice(tokenId+1, price);
debtToken.setReservePool(address(lendingPool));
vm.stopPrank();
// Lp deposits the reserve asset
vm.startPrank(lp);
_reserveAsset.approve(address(lendingPool), price * 2);
lendingPool.deposit(price * 2);
vm.stopPrank();
// Anon deposits the RWA token and mints the NFTs
vm.startPrank(anon);
rwaToken.approve(address(raacNFT), price * 2);
raacNFT.mint(tokenId, price);
raacNFT.mint(tokenId+1, price);
raacNFT.setApprovalForAll(address(lendingPool), true);
// Deposit
lendingPool.depositNFT(1);
lendingPool.depositNFT(2);
// Borrow
lendingPool.borrow(price);
skip(14 days);
lendingPool.borrow(price);
uint256 debt = lendingPool.getUserDebt(anon);
console.log("Alice scaled debt stored in Lending Pool: %d USD", debt);
uint256 debt2 = debtToken.balanceOf(anon);
console.log("Alice actual scaled debt stored in Debt Token: %d USD", debt2);
assert(debt != debt2);
vm.stopPrank();
}

Logs before the fix:

[PASS] test_FortisAudits_WrongUserDebtReturned() (gas: 1163748)
Logs:
Alice scaled debt stored in Lending Pool: 100276065291865898746361 USD
Alice actual scaled debt stored in Debt Token: 100276446352092763765664 USD

Logs after the fix:

[PASS] test_FortisAudits_WrongUserDebtReturned() (gas: 1166345)
Logs:
Alice scaled debt stored in Lending Pool: 100276446352092763765664 USD
Alice actual scaled debt stored in Debt Token: 100276446352092763765664 USD

Recommended Mitigation

The most accurate way to determine a user's current debt is to directly query the DebtToken contract's balanceOf function. This function already incorporates the compound interest calculation.By using the DebtToken's balanceOf function, the getUserDebt function will accurately reflect the user's current debt, including the effect of compound interest. This change is crucial for the stability and proper functioning of the lending protocol.

function getUserDebt(address userAddress) public view returns (uint256) {
- UserData storage user = userData[userAddress];
- return user.scaledDebtBalance.rayMul(reserve.usageIndex); // INCORRECT: Linear scaling for compound interest
+ return IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(userAddress); // Correct: Use DebtToken's balanceOf
}
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!