Core Contracts

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

`DebtToken::mint` - accumulated interest is issued twice to the borrowers balance

Summary

DebtToken::mint mints an amount of tokens equivalent to the borrowed amount plus any potential interest accumulated from previous borrow positions. Accumulated interest shouldn't be minted to the borrower's account;

Vulnerability Details

DebtToken, as well as RToken are interest-bearing tokens that represents a user's share in the underlying deposited/ borrowed assets.
These are rebasing tokens, which means they are growing in balance, not in value. The ratio at which they grow in balance is given by the pool's usageIndex and liquidityIndex respectively.

When an user borrows from LendingPool, DebtToken::mint is called and amount of underlying pool's asset is transferred to borrower:

//LendingPool.sol
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(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);
...
}

In DebtToken::mint the amountToMint is calculated as the sum of borrowed assets and the interest accumulated from the user's previous borrow positions.

The accumulated interest shouldn't be issued (minted) to user's balance; instead, it is already reflected in borrower's balance by the fact that scaledBalance is multiplied by the _reservePool).getNormalizedDebt() (debtIndex).

//DebtToken.sol
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);
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()); // @audit wrong amount of debt is minted
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
@> return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}

Impact

More debt is issued to user's balance which will also accrue interest. Borrowers will have to repay more than principal borrowed and accumulated interest.

Tools Used

Recommendations

amount of assets borrowed should be passed to _mint function.

- _mint(onBehalfOf, amountToMint.toUint128());
+ _mint(onBehalfOf, amount.toUint128());
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month 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.