Summary
The RAACMinter::getUtilizationRate() function incorrectly compares a normalized debt index against total deposits amount in StabilityPool, resulting in incorrect emission rate calculations and reward distributions.
Vulnerability Details
The getUtilizationRate() function is designed to calculate system utilization by comparing borrowed amounts to deposited amounts. However, there is a critical unit mismatch in the calculation:
function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}
The issue arises because:
getNormalizedDebt() returns a normalized debt index used for interest calculations
getTotalDeposits() returns the actual token amount deposited
These incompatible units are directly compared in the utilization calculation
The less the utilization rate is, the less the emission rate will be.
RaacMinter::calculateNewEmissionRate():
function calculateNewEmissionRate() internal view returns (uint256) {
uint256 utilizationRate = getUtilizationRate();
uint256 adjustment = (emissionRate * adjustmentFactor) / 100;
if (utilizationRate > utilizationTarget) { have more utilization
uint256 increasedRate = emissionRate + adjustment;
uint256 maxRate = increasedRate > benchmarkRate ? increasedRate : benchmarkRate;
return maxRate < maxEmissionRate ? maxRate : maxEmissionRate;
@> } else if (utilizationRate < utilizationTarget) {
uint256 decreasedRate = emissionRate > adjustment ? emissionRate - adjustment : 0;
uint256 minRate = decreasedRate < benchmarkRate ? decreasedRate : benchmarkRate;
return minRate > minEmissionRate ? minRate : minEmissionRate;
}
return emissionRate;
}
Which will also lead to less rewards being minted for the depositors in the StabilityPool.
RaacMinter::tick():
function tick() external nonReentrant whenNotPaused {
if (emissionUpdateInterval == 0 || block.timestamp >= lastEmissionUpdateTimestamp + emissionUpdateInterval) {
updateEmissionRate();
}
uint256 currentBlock = block.number;
uint256 blocksSinceLastUpdate = currentBlock - lastUpdateBlock;
if (blocksSinceLastUpdate > 0) {
@> uint256 amountToMint = emissionRate * blocksSinceLastUpdate;
if (amountToMint > 0) {
excessTokens += amountToMint;
lastUpdateBlock = currentBlock;
raacToken.mint(address(stabilityPool), amountToMint);
emit RAACMinted(amountToMint);
}
}
}
Proof of Concept
Assume the lending pool has a normalized debt index of 1.5e18 (representing 150% interest accrual)
The stability pool has 1000e18 tokens deposited
Current calculation: (1.5e18 * 100) / 1000e18 = 0.15%
This severely understates the actual utilization rate, which should be based on borrowed amounts
Impact
Incorrect utilization rate calculations lead to wrong emission rate adjustments, which will lead to wrong amount of rewards minted for the depositors in the StabilityPool.
Users will receive less rewards then they should, because the total supply of the DebtToken is much larger than the usage index.
Recommendations
Use the total supply of the DebtToken, instead of the usage index.
function getUtilizationRate() internal view returns (uint256) {
- uint256 totalBorrowed = lendingPool.getNormalizedDebt();
+ uint256 totalBorrowed = lendingPool.debtToken().totalSupply();
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}
Note that this fix might require updates in some interfaces.