Core Contracts

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

`DebtToken::mint()` mints incorrect amount leading to DOS of `LendingPool::repay()`

Summary

RAAC protocol's DebtToken takes from AAVE's idea of using self-interest-bearing tokens by applying indexes to minted/burned amounts. However, the current implementation is flawed, as when the token is minted, the amount to mint is inflated with a double-scaled balanceIncrease value, which is intended to serve as an event emission input. This over-inflation leads to a mismatch between the internal user.scaledDebtBalance variable in LendingPool and the burned amount of DebtTokens when repaying, resulting in an underflow error DOS-ing the LendingPool::repay() function.

Vulnerability Details

Let's look at RAAC's DebtToken mint:

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());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}

And let's look at AAVE's scaled token minting:

function _mintScaled(
address caller,
address onBehalfOf,
uint256 amount,
uint256 index
) internal returns (bool) {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT);
uint256 scaledBalance = super.balanceOf(onBehalfOf);
uint256 balanceIncrease = scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].additionalData);
_userState[onBehalfOf].additionalData = index.toUint128();
_mint(onBehalfOf, amountScaled.toUint128());
uint256 amountToMint = amount + balanceIncrease;
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0);
}

From the above, we can see that there are numerous inconsistencies:

  1. When taking the scaledBalance, RAAC uses the balanceOf function of the DebtToken and then does rayMul on it again to calculate the balanceIncrease. This is incorrect as balanceOf returns the already normalized amount, leading to double scaling (this is a different issue leading to invalid event emission).

  2. RAAC then goes to add the balanceIncrease to the amount that is meant to be minted, where this balanceIncrease is meant to be used as an event input, to indicate how much increase there is due to the changes in borrowing indexes.

  3. Combining both points from above results in an over-inflated DebtToken mint, which does not match with what is added to the internal user.scaledDebtBalance in LendingPool::borrow(). Let's analyze:

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
__SNIP__
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
// 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; // @audit - actual minted amount is larger than scaledAmount
// reserve.totalUsage += amount;
reserve.totalUsage = newTotalSupply;
__SNIP__
}

4 The actual minted amount differs from what is added to user.scaledDebtBalance, which means that the internal debt storage is now lower than the actual balance of debt tokens.

5 Now, when the user wants to fully repay, an underflow will happen, as it will try to deduct the actual burned tokens from the lowered internal storage:

function _repay(uint256 amount, address onBehalfOf) internal {
__SNIP__
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer reserve assets from the caller (msg.sender) to the reserve
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
@> user.scaledDebtBalance -= amountBurned; // @audit - this will underflow, as the amount burned will be the actual amount of burned tokens
__SNIP__
}

Impact

Users cannot repay debt due to DoS.

Tools Used

Manual review

Recommendations

Do not add the balance increase to the amount that is going to be minted when borrowing.

Updates

Lead Judging Commences

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