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();
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:
Utilization Rate Corruption:
function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
uint256 totalDeposits = stabilityPool.getTotalDeposits();
return (totalBorrowed * 100) / totalDeposits;
}
Forces Emission Rate Increases:
function calculateNewEmissionRate() internal view returns (uint256) {
uint256 utilizationRate = getUtilizationRate();
uint256 adjustment = (emissionRate * adjustmentFactor) / 100;
if (utilizationRate > utilizationTarget) {
uint256 increasedRate = emissionRate + adjustment;
uint256 maxRate = increasedRate > benchmarkRate ? increasedRate : benchmarkRate;
return maxRate < maxEmissionRate ? maxRate : maxEmissionRate;
}
}
Affects Minting Schedule:
function tick() external nonReentrant whenNotPaused {
if (block.timestamp >= lastEmissionUpdateTimestamp + emissionUpdateInterval) {
updateEmissionRate();
}
uint256 amountToMint = emissionRate * blocksSinceLastUpdate;
raacToken.mint(address(stabilityPool), amountToMint);
}
Impact:
The vulnerability breaks core protocol mechanics:
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
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:
Initial State:
RAY = 1e27
HALF_RAY = 0.5e27
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%
Utilization Calculation (using proper rayMul):
utilizationRate = (getNormalizedDebt() * 100) / totalDeposits
c = (a * b + HALF_RAY) / RAY
= (1.1e27 * 100 + 0.5e27) / 1e27
= (1.1e29 + 0.5e27) / 1e27
= 1100%
Emission Rate Progression (with rayMul):
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
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
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:
Use correct total debt tracking:
function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.reserve.totalUsage;
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}