Core Contracts

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

Incorrect amount used in `RToken::_mint` and `RToken::burn` functions

Summary

In RToken::mint and RToken::burn, the amount being minted and burned is incorrect because it is denominated in units of the underlying asset instead of RTokens. This discrepancy leads to errors in the minting and burning process.

Additionally, the balanceIncrease isn't added to the amount minted so the interest accrued are not gotten by the user.

/**
* @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;
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); // @audit
}

The amount to be minted should be in RToken units, not the underlying asset units. This discrepancy leads to errors in the minting process.

Furthermore, the amount being burned in _burn is incorrect because the amount passed in _burn is denominated in units of the underlying asset.

/**
* @notice Burns RToken from a user and transfers underlying asset
* @param from The address from which tokens are burned
* @param receiverOfUnderlying The address receiving the underlying asset
* @param amount The amount to burn (in underlying asset units)
* @param index The liquidity index at the time of burning
* @return A tuple containing:
* - uint256: The amount of scaled tokens burned
* - uint256: The new total supply after burning
* - uint256: The amount of underlying asset transferred
*/
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
@> _burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
@> return (amount, totalSupply(), amount); // @audit wrong return value
}

Impact

  • This leads to incorrect mint and burn amounts

  • It returns the wrong value which could mislead integrators about the actual amount of RTokens minted / burned, potentially causing accounting errors in protocols integrating with this contract

Tools Used

Manual Review

Recommendations

amountScaled should be used in the _mint function instead of amountToMint to ensure that the correct amount of RTokens is minted and the correct values are returned in the function.

/**
* @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;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
+ amountToMint += balanceIncrease;
+ amountScaled = amountToMint.rayDiv(index);
- _mint(onBehalfOf, amountToMint.toUint128());
+ _mint(onBehalfOf, amountScaled.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
- return (isFirstMint, amountToMint, totalSupply(), amountScaled);
+ return (isFirstMint, amountScaled, totalSupply(), amountToMint);
}

amountScaled should be used in the _burn function instead of amount to ensure that the correct amount of RTokens is burned and the correct values are returned in the function.

/**
* @notice Burns RToken from a user and transfers underlying asset
* @param from The address from which tokens are burned
* @param receiverOfUnderlying The address receiving the underlying asset
* @param amount The amount to burn (in underlying asset units)
* @param index The liquidity index at the time of burning
* @return A tuple containing:
* - uint256: The amount of scaled tokens burned
* - uint256: The new total supply after burning
* - uint256: The amount of underlying asset transferred
*/
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
- _burn(from, amount.toUint128());
+ _burn(from, amountScaled.toUint128());
if (receiverOfUnderlying != address(this)) {
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amountScaled, totalSupply(), amount); // @audit wrong return value
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::burn incorrectly burns amount (asset units) instead of amountScaled (token units), breaking token economics and interest-accrual mechanism

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::burn incorrectly burns amount (asset units) instead of amountScaled (token units), breaking token economics and interest-accrual mechanism

Appeal created

fromeo_016 Submitter
about 1 month ago
inallhonesty Lead Judge
about 1 month ago
fromeo_016 Submitter
29 days ago
inallhonesty Lead Judge
29 days ago
inallhonesty Lead Judge 22 days ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::burn incorrectly burns amount (asset units) instead of amountScaled (token units), breaking token economics and interest-accrual mechanism

RToken::mint should mint the amountScaled not the amountToMint

Support

FAQs

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