Core Contracts

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

Incorrect Scaling in RToken::mint Function

Summary

The RToken::mint function fails to properly implement scaling as indicated by its comments, leading to incorrect values being returned and tokens being minted with unscaled amounts.

Details

LendingPool::deposit calls ReserveLibrary::deposit which in turn calls RToken::mint function. Just before calling mint function, we are informed by the comment above it that the scaling is handled inside RToken.

@> // Mint RToken to the depositor (scaling handled inside RToken)
(
bool isFirstMint,
uint256 amountScaled,
uint256 newTotalSupply,
uint256 amountUnderlying
) = IRToken(reserve.reserveRTokenAddress).mint(
address(this), // caller
depositor, // onBehalfOf
amount, // amount
reserve.liquidityIndex // index
);
amountMinted = amountScaled;
// Update the total liquidity and interest rates
updateInterestRatesAndLiquidity(reserve, rateData, amount, 0);
emit Deposit(depositor, amount, amountMinted);
return amountMinted;

In RToken::mint function there are a couple of issues:

/**
* @notice Mints RToken to a user
* @param caller The address initiating the mint
* @param onBehalfOf The recipient of the minted tokens
* @param amountToMint The amount of tokens to mint (in underlying asset units)
* @param index The liquidity index at the time of minting
* @return A tuple containing:
* - bool: True if this is the first mint for the recipient, false otherwise
* - uint256: The amount of scaled tokens minted
* - uint256: The new total supply after minting
* - uint256: The amount of underlying tokens minted
*/
function mint(
address caller,
address onBehalfOf,
uint256 amountToMint,
uint256 index
)
external
override
onlyReservePool
returns (bool, uint256, uint256, uint256)
{
if (amountToMint == 0) {
return (false, 0, 0, 0);
}
uint256 amountScaled = amountToMint.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0; //? unused??
if (
_userState[onBehalfOf].index != 0 &&
_userState[onBehalfOf].index < index
) {
balanceIncrease =
scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
_mint(onBehalfOf, amountToMint.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
}
  • amountScaled is defined but is not being used apart from this check

if (amountScaled == 0) revert InvalidAmount();
  • scaledBalance and balanceIncrease are defined, but are not used. They just take space inside the function but do not change the state or are returned in any way:

uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0; //? unused??
if (
_userState[onBehalfOf].index != 0 &&
_userState[onBehalfOf].index < index
) {
balanceIncrease =
scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].index);
}

I mean, balanceIncrease is defined, and then calculated in the if clause, but after the calculation nothing is done with it. It’s just there, taking space in the code block, doing nothing. But let’s continue.

  • these are the return values from the mint function

_mint(onBehalfOf, amountToMint.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
  • Now, first of all, remember that in the ReserveLibrary::deposit function that called this RToken::mint function, we had this comment indicating that some scaling had to be done :

*// Mint RToken to the depositor (scaling handled inside RToken)*

Now let’s compare two codeblocks to understand it better:

From ReserveLibrary::deposit
// Mint RToken to the depositor (scaling handled inside RToken)
(
bool isFirstMint,
uint256 amountScaled,
uint256 newTotalSupply,
uint256 amountUnderlying
) = IRToken(reserve.reserveRTokenAddress).mint(
address(this), // caller
depositor, // onBehalfOf
amount, // amount
reserve.liquidityIndex // index
);
// Update the total liquidity and interest rates
updateInterestRatesAndLiquidity(reserve, rateData, amount, 0);
emit Deposit(depositor, amount, amountMinted);
-----------
from RToken::mint
uint256 amountScaled = amountToMint.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
...
...
_userState[onBehalfOf].index = index.toUint128();
_mint(onBehalfOf, amountToMint.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
return (isFirstMint, amountToMint, totalSupply(), amountScaled);

In the returns in ReserveLibrary, we see that it returns and uses uint256 amountScaled, but it never returned the scaled amount from RToken::mint, it just returned the amount that is passed in the params. No scaling ever happened or returned there.

So, wrong values are minted.

Impact:

  • Incorrect accounting of deposited amounts throughout the protocol

  • Mismatch between actual minted tokens and what the system believes was minted

  • Potential economic exploits if users can receive more tokens than they should

  • Integrity issues with protocol's state and total supply calculations

Recommended Fix:

  1. Correct the return values to match what the caller expects, or adjust the caller to expect the values in the correct order.

  2. Use the scaled amount for minting:

  3. Clean up or properly utilize the balanceIncrease calculation.

Updates

Lead Judging Commences

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

RToken::mint doesn't return data in the right order, making the protocol emit wrong events

RToken::mint should mint the amountScaled not the amountToMint

RToken::mint calculates balanceIncrease (interest accrued since last interaction) but never mints it, causing users to lose earned interest between deposits

The balanceIncrease is the interest that has already accrued on the user's existing scaledBalance since their last interaction. It's not something you mint as new tokens in the _mint function.

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

RToken::mint doesn't return data in the right order, making the protocol emit wrong events

RToken::mint should mint the amountScaled not the amountToMint

RToken::mint calculates balanceIncrease (interest accrued since last interaction) but never mints it, causing users to lose earned interest between deposits

The balanceIncrease is the interest that has already accrued on the user's existing scaledBalance since their last interaction. It's not something you mint as new tokens in the _mint function.

Support

FAQs

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