Core Contracts

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

Storing Scaled Debt in `LendingPool.borrow` Function Leads to Inaccurate Debt Tracking

Summary

The borrow function in the LendingPool contract incorrectly stores the scaled down debt balance instead of the actual amount borrowed. This discrepancy between the recorded debt and the real debt can lead to several severe issues, including inaccurate user debt calculations, potential for manipulation, and complications during liquidations.

Vulnerability Details

The borrow function calculates a scaled amount for internal accounting but then uses this scaled amount to update the user's debt balance. This is incorrect. The user's debt should reflect the actual amount borrowed, not a scaled-down version.

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ... other checks ...
// ... calculations ...
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex); // Scaled amount
// ... minting DebtTokens ...
user.scaledDebtBalance += scaledAmount; // INCORRECT: Should be the actual amount borrowed
reserve.totalUsage = newTotalSupply;
// ... other code ...
}
  1. Incorrect Debt Storage: The user.scaledDebtBalance is updated with scaledAmount, which is the actual amount borrowed divided by the reserve.usageIndex. This means the user's recorded debt is significantly lower than the actual amount they owe.

  2. Inconsistent Accounting: This discrepancy creates inconsistency in the accounting of the user's debt. While the getUserDebt function attempts to rectify this by multiplying the scaled balance by the current reserve.usageIndex, it multiples with wrong user debt.

Impact

  • Inaccurate Debt Calculations: The most immediate impact is that the stored debt is incorrect. While getUserDebt attempts to compensate, any other function or external system relying on user.scaledDebtBalance directly will receive incorrect information.

  • Potential for Manipulation: This discrepancy can be exploited. Because the user's recorded debt is lower than their actual debt, they might be able to interact with other parts of the protocol (or other protocols) in ways that are detrimental to the system's overall health. For example, they might be able to borrow more than they should be allowed to.

  • Complications during Liquidations: Liquidations become significantly more complex. Because the recorded debt is incorrect, the liquidation process may not function as intended, potentially allows liquidatable users to escape for liquidaton and leading to losses for the protocol.

Proof of Concept

  1. Alice borrows 100 units of an asset.

  2. The reserve.usageIndex is 1.1 * 10**27.

  3. The borrow function calculates scaledAmount = 100 / 1.1 = 90.91 (approximately).

  4. Alice's user.scaledDebtBalance is updated to 90.91.

  5. Alice's actual debt is 100, but the protocol records it as 90.91.

  6. Later, when Alice wants to repay or checked for liquidation threshold, the protocol will consider her debt as 90.91 instead of 100.

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. Run the test using forge test --mt test_FortisAudits_IncorrectDebtStorageInLendingPool -vvvv.

function test_FortisAudits_IncorrectDebtStorageInLendingPool() 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);
console.log("Alice borrows a total of %d USD from Lending Pool", price * 2);
(uint256 debt,,) = lendingPool.userData(anon);
console.log("Alice underlying debt stored in Lending Pool: %d USD", debt);
vm.stopPrank();
}

Logs before the fix:

[PASS] test_FortisAudits_IncorrectDebtStorageInLendingPool() (gas: 1173343)
Logs:
Alice borrows a total of 100000000000000000000000 USD from Lending Pool
Alice underlying debt stored in Lending Pool: 99862347365215030851562 USD

Logs after the fix:

[PASS] test_FortisAudits_IncorrectDebtStorageInLendingPool() (gas: 1172609)
Logs:
Alice borrows a total of 100000000000000000000000 USD from Lending Pool
Alice underlying debt stored in Lending Pool: 100000000000000000000000 USD

Recommended Mitigation

To mitigate this issue, store the actual amount borrowed in user.scaledDebtBalance, not the scaled-down version. Remove rayDiv in the borrow function. This ensures that the user's debt balance accurately reflects the amount they owe, preventing potential issues with debt calculations, liquidations, and other protocol interactions.

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 ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.