Core Contracts

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

Users can miss rewards due to Stale UR Data in RAACMinter's Emission Rate Calculation

Summary

The updateEmissionRate() function determines the new emission rate based on the utilization rate (UR). However, it fetches the UR without first updating the lending pool state to account for accrued interest. This means that getNormalizedDebt() (which uses reserve.usageIndex) could be outdated at the time of calculation.

Vulnerability Details

Emission Rate Update Trigger

function tick() external nonReentrant whenNotPaused {
if (emissionUpdateInterval == 0 || block.timestamp >= lastEmissionUpdateTimestamp + emissionUpdateInterval) {
updateEmissionRate();
}
// ... minting logic
}

The tick() function serves two purposes:

  • Updates emission rate based on time interval

  • Mints tokens to stability pool

Emission Rate Calculation:

function updateEmissionRate() public whenNotPaused {
if (emissionUpdateInterval > 0 && block.timestamp < lastEmissionUpdateTimestamp + emissionUpdateInterval) {
revert EmissionUpdateTooFrequent();
}
// @audit - Gets utilization rate without updating lending pool state
uint256 newRate = calculateNewEmissionRate();
emissionRate = newRate;
lastEmissionUpdateTimestamp = block.timestamp;
}
  1. Stale Data Usage

function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt(); // Uses potentially stale data
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}

getNormalizedDebt() relies on reserve.usageIndex, which is only updated when updateReserveInterests() is called.

Since updateReserveInterests() is not called before fetching the utilization rate, the calculation may be using outdated data.

function updateReserveInterests(ReserveData storage reserve, ReserveRateData storage rateData) internal {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
// ... updates indices
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
}

This update is not triggered before calculating the utilization rate.

Impact

Users interacting with the stability pool will miss rewards because the _update() in stability pool calls the tick() in the RAACMinter contractthat did not update the state of the lending pool before getting the UR leading to Incorrect Emission Rates

Tools Used

Manual

Recommendations

Update the updateEmissionRate()

function updateEmissionRate() public whenNotPaused {
if (emissionUpdateInterval > 0 && block.timestamp < lastEmissionUpdateTimestamp + emissionUpdateInterval) {
revert EmissionUpdateTooFrequent();
}
lendingPool.updateState(); // update here
uint256 newRate = calculateNewEmissionRate();
emissionRate = newRate;
lastEmissionUpdateTimestamp = block.timestamp;
emit EmissionRateUpdated(newRate);
}
Updates

Lead Judging Commences

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

LendingPool::getNormalizedIncome() and getNormalizedDebt() returns stale data without updating state first, causing RToken calculations to use outdated values

Support

FAQs

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