Core Contracts

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

Incorrect Utilization Rate Calculation Forces Protocol to Maximum Emission Rate, Breaking Economic Model

Link to Affected Code:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/minters/RAACMinter/RAACMinter.sol#L241-L246

function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt(); //@audit wrong, returns index not debt(totalUsage)
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}

Description:
The RAACMinter's emission control system is fundamentally broken due to using getNormalizedDebt() which returns the normalized debt index (~1e27) instead of reserve.totalUsage for utilization calculation. This cascades through the entire emission control mechanism:

  1. Utilization Rate Corruption:

function getUtilizationRate() internal view returns (uint256) {
// Returns index (~1.1e27) instead of actual debt
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
uint256 totalDeposits = stabilityPool.getTotalDeposits(); // Returns actual deposits (1e18)
return (totalBorrowed * 100) / totalDeposits; // Massive decimal mismatch
}
  1. Forces Emission Rate Increases:

function calculateNewEmissionRate() internal view returns (uint256) {
uint256 utilizationRate = getUtilizationRate(); // Returns inflated rate
uint256 adjustment = (emissionRate * adjustmentFactor) / 100;
// Always true due to inflated utilization
if (utilizationRate > utilizationTarget) {
uint256 increasedRate = emissionRate + adjustment;
uint256 maxRate = increasedRate > benchmarkRate ? increasedRate : benchmarkRate;
return maxRate < maxEmissionRate ? maxRate : maxEmissionRate;
}
}
  1. Affects Minting Schedule:

function tick() external nonReentrant whenNotPaused {
if (block.timestamp >= lastEmissionUpdateTimestamp + emissionUpdateInterval) {
updateEmissionRate(); // Forces rate increase due to inflated utilization
}
uint256 amountToMint = emissionRate * blocksSinceLastUpdate;
raacToken.mint(address(stabilityPool), amountToMint); // Mints at max rate
}

Impact:
The vulnerability breaks core protocol mechanics:

  1. Emission Control Failure:

  • System always sees utilization > 1000% due to decimal mismatch

  • Emission rate increases by 5% each update until maximum

  • Cannot decrease even at 0% actual utilization

  • Reaches maxEmissionRate (2000 RAAC/day) permanently

  1. Economic Model Breakdown:

  • Dynamic emission system becomes static at maximum rate

  • Protocol loses ability to adjust to market conditions

  • Incentive mechanism becomes ineffective

  • Hyperinflationary token emissions

Proof of Concept:
Detailed calculation showing inevitable progression to maximum emissions:

  1. Initial State:

// Constants
RAY = 1e27
HALF_RAY = 0.5e27
// Initial Values
Actual Debt: 100 crvUSD (100e18)
Normalized Debt Index: 1.1e27 (10% interest)
Total Deposits: 100 crvUSD (100e18)
Initial Emission Rate: 100 RAAC/day
Adjustment Factor: 5%
Utilization Target: 70%
  1. Utilization Calculation (using proper rayMul):

utilizationRate = (getNormalizedDebt() * 100) / totalDeposits
// Using actual rayMul implementation:
c = (a * b + HALF_RAY) / RAY
= (1.1e27 * 100 + 0.5e27) / 1e27
= (1.1e29 + 0.5e27) / 1e27
= 1100% // Still massively inflated vs 70% target
  1. Emission Rate Progression (with rayMul):

// Each update triggers the increase path due to inflated utilization
Day 1:
- utilizationRate = 1100% > 70% target
- adjustment = (100 * 5%) = 5 RAAC/day
- newRate = 100 + 5 = 105 RAAC/day
Day 2:
- utilizationRate still 1100% > 70% target
- adjustment = (105 * 5%) = 5.25 RAAC/day
- newRate = 105 + 5.25 = 110.25 RAAC/day
Day 3:
- adjustment = (110.25 * 5%) = 5.51 RAAC/day
- newRate = 110.25 + 5.51 = 115.76 RAAC/day
...continues compound growth until Day 93
  1. End State:

  • Protocol hits maxEmissionRate (2000 RAAC/day)

  • Cannot decrease because:

    • getNormalizedDebt() always returns ~1e27 index

    • Makes utilization appear >1000%

    • Forces emission increase path

  • Economic controls permanently broken

  • No relationship between actual utilization and emissions

  1. System Impact Flow:

getUtilizationRate()
-> Returns inflated rate due to index vs amount mismatch
-> calculateNewEmissionRate() always sees utilization > target
-> updateEmissionRate() keeps increasing
-> tick() mints at maximum rate
-> Protocol loses emission control

Recommended Mitigation:

  1. Use correct total debt tracking:

function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.reserve.totalUsage; // Use actual debt
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}
Updates

Lead Judging Commences

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

RAACMinter::getUtilizationRate incorrectly mixes stability pool deposits with lending pool debt index instead of using proper lending pool metrics

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

RAACMinter::getUtilizationRate incorrectly mixes stability pool deposits with lending pool debt index instead of using proper lending pool metrics

Support

FAQs

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

Give us feedback!