Summary
Incorrect formula in RAACMinter causes it to mint max amount of emission RAAC tokens each tick. Because of that users will receive more than expected rewards.
Vulnerability Details
RAACMinter is the contract that mints and distributes RAAC tokens based on emmission schedule. Each time user deposits or withdraws from/to the StabilityPool contract, it triggers a tick in the RAACMinter contract (see _update and _minRAACRewards fns in StabilityPool contract).
If we go to the tick fn in RAACMinter, we can see that if enough time has passed from the last emission, we call updateEmissionRate fn.
function tick() external nonReentrant whenNotPaused {
if (emissionUpdateInterval == 0 || block.timestamp >= lastEmissionUpdateTimestamp + emissionUpdateInterval) {
updateEmissionRate();
}
In the updateEmissionRate it first calculates the utilization rate, then using that utilization rate it determines how much the new rate should be, based on utilization target.
If we go into how getUtilizationRate is calculated, it appears that the formula is totalBorrowed * 100 / totalDeposits.
The problem is that totalBorrowed is the index of the RToken, not the scaled amount.
This causes a problem and makes it so that the function will always return extremely high utilization rate.
To demontrate that i've created this test:
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("RAACMinter Vulnerabilities", function () {
let RAACToken, RAACMinter, MockLendingPool, MockStabilityPool;
let raacToken, raacMinter, lendingPool, stabilityPool;
let owner, user1;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
RAACToken = await ethers.getContractFactory("RAACToken");
raacToken = await RAACToken.deploy(owner.address, 100, 50);
MockLendingPool = await ethers.getContractFactory("MockLendingPool");
lendingPool = await MockLendingPool.deploy();
MockStabilityPool = await ethers.getContractFactory("MockStabilityPool");
stabilityPool = await MockStabilityPool.deploy(await raacToken.getAddress());
RAACMinter = await ethers.getContractFactory("RAACMinter");
raacMinter = await RAACMinter.deploy(
await raacToken.getAddress(),
await stabilityPool.getAddress(),
await lendingPool.getAddress(),
owner.address
);
await raacToken.setMinter(await raacMinter.getAddress());
});
it('will mint max emmision rate always because of broken getUtilizationRate formula', async () => {
await lendingPool.mockGetNormalizedDebt(ethers.parseEther("1000000000000000000000000000"));
await stabilityPool.mockGetTotalDeposits(ethers.parseEther("10000"));
const initialEmissionRate = await raacMinter.emissionRate();
console.log(initialEmissionRate);
await raacMinter.updateEmissionRate();
const newEmissionRate = await raacMinter.emissionRate();
expect(newEmissionRate).to.equal(145833333333333332n);
for (let i = 0; i < 20; i++) {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.updateEmissionRate();
}
});
});
And in order to see how the mission rate changes, import "hardhat/console.sol"; has to be put at the top of RAACMinter.sol and console.log must be put to track the changes increaseRate and maxRate. Here is how the new calculateNewEmissionRate looks like:
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;
+ console.log('increasedRate', increasedRate, 'maxRate', maxRate);
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;
}
If you run the tests, we will see the following logs:
Compiled 2 Solidity files successfully (evm target: paris).
RAACMinter Vulnerabilities
138888888888888888n
increasedRate 145833333333333332 maxRate 145833333333333332
increasedRate 153124999999999998 maxRate 153124999999999998
increasedRate 160781249999999997 maxRate 160781249999999997
increasedRate 168820312499999996 maxRate 168820312499999996
increasedRate 177261328124999995 maxRate 177261328124999995
increasedRate 186124394531249994 maxRate 186124394531249994
increasedRate 195430614257812493 maxRate 195430614257812493
increasedRate 205202144970703117 maxRate 205202144970703117
increasedRate 215462252219238272 maxRate 215462252219238272
increasedRate 226235364830200185 maxRate 226235364830200185
increasedRate 237547133071710194 maxRate 237547133071710194
increasedRate 249424489725295703 maxRate 249424489725295703
increasedRate 261895714211560488 maxRate 261895714211560488
increasedRate 274990499922138512 maxRate 274990499922138512
increasedRate 288740024918245437 maxRate 288740024918245437
increasedRate 291666666666666665 maxRate 291666666666666665
increasedRate 291666666666666665 maxRate 291666666666666665
increasedRate 291666666666666665 maxRate 291666666666666665
increasedRate 291666666666666665 maxRate 291666666666666665
increasedRate 291666666666666665 maxRate 291666666666666665
increasedRate 291666666666666665 maxRate 291666666666666665
✔ will mint max emmision rate always because of broken getUtilizationRate formula (1009ms)
As it can be seen, after each emission, the emissionRate always increases with maximum amount, until it hits the maximum allowed emission rate.
Now thats established, we can say that each time updateEmissionRate is called, which is each time enough time has passed since last emission. updateEmissionRate is called from the tick fn which is called from withdraw and deposit from the stability pool.
If we go back to the tick fn, once the emission is updated, it checks when was the last time it minted new raac tokens. Based on the emission from the calculation before we know that this will very fast reach max emission rate, causing unexpected inflation.
Until now, its proven that there is unnecesarry inflation of the RAAC token. With that understanding we can see how it affects the rewards each user receives.
If we go to the StabilityPool, we can see that on withdraw, its calculated the amount of rcrvUSDAmount the sender should receive and the amount of additional raacRewards.
Raac rewards are calculated with the following formula: totalRewards * userDeposits / totalDeposits.
Because there is inflated amount of raacTokens, totalRewards will be higher than expected. Because totalRewards are higher, the user will receive more than expected rewards.
Impact
Unexpected inflation of RAAC tokens,. Users receive more than expected RAAC rewards.
Tools Used
Manual review, unit tests
Recommendations
N/A