Core Contracts

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

Incorrect RToken Minting Calculation due to Dynamic Liquidity Index in `deposit()` Function

Summary

During the deposit process in the lending protocol, the number of RTokens minted is dynamically calculated using a non-static liquidity index (reserve.liquidityIndex). This logic does not enforce a minimum receipt threshold (i.e., the minimum amount of RTokens the user must receive). As a result, if the liquidity index changes unexpectedly between state updates or due to accrued interest, a user’s minted RTokens may be lower than anticipated relative to their deposited crvUSD. This discrepancy can lead to a loss in value for the depositor.

Vulnerability Details

The deposit routine uses the current reserve.liquidityIndex to calculate the scaled amount for minting RTokens. This index is not a constant value; it is influenced by interest accrual over time. During the execution of transactions, the index might change, causing the minted token amount to differ from what the depositor would expect based on the deposited crvUSD value.

function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// Update the reserve state before the deposit
ReserveLibrary.updateReserveState(reserve, rateData);
// Perform the deposit through ReserveLibrary
@>> uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
// Rebalance liquidity after deposit
_rebalanceLiquidity();
emit Deposit(msg.sender, amount, mintedAmount);
}
function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
if (amount < 1) revert InvalidAmount();
// Update reserve interests
updateReserveInterests(reserve, rateData);
// Transfer asset from caller to the RToken contract
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender, // from
reserve.reserveRTokenAddress, // to
amount // amount
);
// 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;
}

RToken contract

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);
}

Is changing not static getNormalizedIncome()

function _update(address from, address to, uint256 amount) internal override {
// Scale amount by normalized income for all operations (mint, burn, transfer)
@>> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

The deposit function does not include a mechanism for specifying or enforcing a minimum number of RTokens to be received by a user. Without such a safeguard, fluctuations in the liquidity index can result in a scenario where the depositor ends up receiving a lower number of RTokens than anticipated, effectively losing part of the deposited value.

Impact

Depositors may experience an unintended loss of value since the number of RTokens minted does not correctly represent the intended conversion of crvUSD value.

Tools Used

Manual Review

Recommendations

Add an additional parameter to the deposit() function (and propagate it to the ReserveLibrary.deposit()) that allows the depositor to specify a minimum acceptable amount of RTokens to mint. If the calculated mintedAmount is below this threshold, the transaction should revert.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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