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

Give us feedback!