Core Contracts

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

Position debt interest is wrongly accrued

Summary

Using wrong debt balances in the function DebtToken::mint() causes position debt is accrued more than expected.

Vulnerability Details

The function DebtToken::mint() computes the accrued interest wrongly such that it uses the balance returned from balanceOf() which is the borrower's total debt included interest. This causes the result of balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index) to be higher than expected.

The problem arises when the balanceIncrease is added to the mint amount uint256 amountToMint = amount + balanceIncrease. Indeed, the system can still compute the accrued interest such that accrued_interest = scaled_balance * (new_index - last_index). So, it must not add balanceIncrease to amountToMint.

As a result, there will be more debt minted for the borrower

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
...
@> uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
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());
...
}
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
@> uint256 scaledBalance = super.balanceOf(account);
@> return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
function _update(address from, address to, uint256 amount) internal virtual override {
if (from != address(0) && to != address(0)) {
revert TransfersNotAllowed(); // Only allow minting and burning
}
@> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
@> super._update(from, to, scaledAmount);
emit Transfer(from, to, amount);
}

PoC

Add this test to file test/unit/core/pools/LendingPool/LendingPool.test.js

describe("Borrow and Repay", function () {
...
it.only("test interest accrued", async function(){
// @audit interest accrued wrongly
// borrow
const borrowAmount = ethers.parseEther("100");
await lendingPool.connect(user1).borrow(borrowAmount);
// time passed
await ethers.provider.send("evm_increaseTime", [31536000]);
await ethers.provider.send("evm_mine", []);
// borrow 1 wei to trigger DebtToken.mint()
await lendingPool.connect(user1).borrow(1n);
// update state
await lendingPool.connect(user1).updateState();
const currentBorrowRate = await getCurrentBorrowRatePercentage(lendingPool);
const wadCurrentBorrowRatePercentage = ethers.parseUnits(currentBorrowRate.toString(), 18);
const totalBorrowed = borrowAmount + 1n
// offchain computed debt
const generatedDebt = (totalBorrowed * wadCurrentBorrowRatePercentage) / BigInt(100 * 1e18);
// total debt tracked by DebtToken contract
let finalDebt = await debtToken.balanceOf(user1.address);
// compare generated debt with onchain debt with 10% error
expect(finalDebt - totalBorrowed).to.closeTo(generatedDebt, generatedDebt / 10n, "incorrect debt accrued")
})

Run the test and it failed:

0 passing (2s)
1 failing
1) LendingPool
Borrow and Repay
test interest accrued:
AssertionError: expected 6934408361917797373 to be close to 3352272727272727000 +/- 335227272727272700

Impact

  • Position accrues more debt than expected and borrowers have to pay more debt

Tools Used

Manual

Recommendations

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
- uint256 scaledBalance = balanceOf(onBehalfOf);
+ uint256 scaledBalance = super.balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
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;
+ uint256 amountToMint = amount;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}
Updates

Lead Judging Commences

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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