This inflated utilization rate then cascades through the emission rate calculations in the tick()
function:
Because the utilization rate is artificially high, the emission rate constantly increases through the updateEmissionRate()
function until it reaches maximum levels.
This will force the protocol to keep issuing the maximum of rewards even when the utilization rate is low.
First, we have to fix the following bug submitted in other reports to reproduce this PoC.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "../contracts/core/governance/boost/BoostController.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/interfaces/core/tokens/IveRAACToken.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DEToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
contract RAACMinterTest is Test {
using WadRayMath for uint256;
StabilityPool stabilityPool;
LendingPool lendingPool;
RToken rToken;
DEToken deToken;
DebtToken debtToken;
RAACMinter raacMinter;
crvUSDToken crvUSD;
RAACToken raacToken;
RAACHousePrices public raacHousePrices;
RAACNFT public raacNFT;
address owner = address(1);
address user1 = address(2);
address user2 = address(3);
address user3 = address(4);
address[] users = new address[](3);
function setUp() public {
users[0] = user1;
users[1] = user2;
users[2] = user3;
vm.label(user1, "USER1");
vm.label(user2, "USER2");
vm.label(user3, "USER3");
vm.warp(1738798039);
vm.roll(100);
vm.startPrank(owner);
_deployAndSetupContracts();
vm.stopPrank();
_mintCrvUsdTokenToUsers(10000e18);
_depositCrvUsdIntoLendingPoolForAllUsers(100e18);
_depositRTokenIntoLendingPoolForAllUsers(100e18);
_liquidateUserAndAccrueInterest(user1, 1);
_liquidateUserAndAccrueInterest(user3, 2);
_depositNftBorrowFunds(user1, 3, 500e18, false);
}
function test_getUtilizationRate_returnsIncorrectValue_leadingToPermanent_MaximumEmissionOfRewards() public {
console.log("NormalizeDebt: %e", lendingPool.getNormalizedDebt());
console.log("TotalDeposits: %e", stabilityPool.getTotalDeposits());
console.log("getUtilizationRate: %d", _getUtilizationRate());
assertGt(_getUtilizationRate(), 100, "!100");
for(uint256 i = 0; i < 20; i++) {
uint256 raacTotalSupplyBeforeEmission = raacToken.totalSupply();
uint256 emissionRateBefore = raacMinter.getEmissionRate();
_advanceInTime(1 days + 1 seconds);
raacMinter.tick();
console.log("RAAC minted during one day: %e", raacToken.totalSupply() - raacTotalSupplyBeforeEmission);
}
}
function _deployAndSetupContracts() internal {
crvUSD = new crvUSDToken(owner);
raacToken = new RAACToken(owner, 100, 50);
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
raacNFT = new RAACNFT(
address(crvUSD),
address(raacHousePrices),
owner
);
rToken = new RToken(
"RToken",
"RTK",
owner,
address(crvUSD)
);
deToken = new DEToken(
"DEToken",
"DET",
owner,
address(rToken)
);
debtToken = new DebtToken(
"DebtToken",
"DEBT",
owner
);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
0.8e27
);
raacMinter = new RAACMinter(
address(raacToken),
address(0x1234324423),
address(lendingPool),
owner
);
stabilityPool = new StabilityPool(owner);
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvUSD),
address(lendingPool)
);
raacMinter.setStabilityPool(address(stabilityPool));
lendingPool.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
}
* @dev Calculates the current system utilization rate
* @return The utilization rate as a percentage (0-100)
*/
function _getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
console.log("totalBorrowed: %e", totalBorrowed);
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}
function _liquidateUserAndAccrueInterest(address user, uint256 tokenId) internal {
_depositNftBorrowFunds(user, tokenId, 600e18, true);
vm.startPrank(owner);
lendingPool.initiateLiquidation(user);
_advanceInTime(3 days);
lendingPool.updateState();
_advanceInTime(1 seconds);
lendingPool.updateState();
deal(address(crvUSD), address(stabilityPool), lendingPool.getUserDebt(user));
stabilityPool.liquidateBorrower(user);
vm.stopPrank();
}
function _depositCrvUsdIntoLendingPoolForAllUsers(uint256 initialDeposit) internal {
for (uint i = 0; i < users.length; i++) {
vm.prank(users[i]);
lendingPool.deposit(initialDeposit);
}
}
function _depositRTokenIntoLendingPoolForAllUsers(uint256 initialDeposit) internal {
for (uint i = 0; i < users.length; i++) {
vm.prank(users[i]);
stabilityPool.deposit(initialDeposit);
}
}
function _mintCrvUsdTokenToUsers(uint256 initialBalance) internal {
for (uint i = 0; i < users.length; i++) {
_mintCrvUsdTokenToUser(initialBalance, users[i]);
}
}
function _mintCrvUsdTokenToUser(uint256 initialBalance, address user) internal {
vm.prank(owner);
crvUSD.mint(user, initialBalance);
vm.startPrank(user);
crvUSD.approve(address(raacNFT), type(uint256).max);
crvUSD.approve(address(lendingPool), type(uint256).max);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.stopPrank();
}
function _advanceInTime(uint256 time) internal {
vm.warp(block.timestamp + time);
vm.roll(block.number + 7200);
}
function _advanceInTimeAndBlocks(uint256 time, uint256 blocks) internal {
vm.warp(block.timestamp + time);
vm.roll(block.number + blocks);
}
function _advanceInTimeAndAccrueInterestInLendingPool(uint256 time) internal {
uint256 usageIndex = lendingPool.getNormalizedDebt();
_advanceInTime(time);
lendingPool.updateState();
}
function _setupHousePrice(uint256 housePrice, uint256 newValue) internal {
vm.startPrank(owner);
raacHousePrices.setHousePrice(newValue, housePrice);
vm.stopPrank();
}
function _mintNFTwithTokenId(uint256 tokenId, uint256 housePrice) internal {
raacNFT.mint(tokenId, housePrice);
raacNFT.approve(address(lendingPool), tokenId);
}
function _mintAndDepositNftInLendingPool(uint256 tokenId, uint256 housePrice) internal {
_mintNFTwithTokenId(tokenId, housePrice);
lendingPool.depositNFT(tokenId);
}
function _borrowCrvUsdTokenFromLendingPool(uint256 amount) internal {
lendingPool.borrow(amount);
}
function _depositNftBorrowFunds(address user, uint256 tokenId, uint256 nftPrice, bool makeUserLiquidatable) internal {
_setupHousePrice(nftPrice, tokenId);
vm.startPrank(user);
_mintAndDepositNftInLendingPool(tokenId, nftPrice);
_borrowCrvUsdTokenFromLendingPool(nftPrice/2);
vm.stopPrank();
if (makeUserLiquidatable) {
_advanceInTimeAndAccrueInterestInLendingPool(365 days);
_setupHousePrice(nftPrice/2, tokenId);
}
}
}
Output: Inflated utilization rate, leading to inflated emission rate.
Ran 1 test for test/RAACMinter.t.sol:RAACMinterTest
[PASS] test_getUtilizationRate_returnsIncorrectValue_leadingToPermanent_MaximumEmissionOfRewards() (gas: 609810)
Logs:
NormalizeDebt: 3.1234054078898067473481334106e28
TotalDeposits: 1.305265665501601281484e21
totalBorrowed: 3.1234054078898067473481334106e28
@> getUtilizationRate: 2392926965
totalBorrowed: 3.1234054078898067473481334106e28
RAAC minted during one day: 1.1024999999999999856e21
RAAC minted during one day: 1.1576249999999999784e21
RAAC minted during one day: 1.2155062499999999712e21
RAAC minted during one day: 1.276281562499999964e21
RAAC minted during one day: 1.3400956406249999568e21
RAAC minted during one day: 1.4071004226562499496e21
RAAC minted during one day: 1.4774554437890624424e21
RAAC minted during one day: 1.5513282159785155584e21
RAAC minted during one day: 1.628894626777441332e21
RAAC minted during one day: 1.7103393581163133968e21
RAAC minted during one day: 1.7958563260221290616e21
RAAC minted during one day: 1.8856491423232355136e21
RAAC minted during one day: 1.9799315994393972864e21
RAAC minted during one day: 1.9999999999999999944e21
RAAC minted during one day: 1.9999999999999999944e21
RAAC minted during one day: 1.9999999999999999944e21
RAAC minted during one day: 1.9999999999999999944e21
RAAC minted during one day: 1.9999999999999999944e21
RAAC minted during one day: 1.9999999999999999944e21
RAAC minted during one day: 1.9999999999999999944e21
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.21ms (1.31ms CPU time)
Modify the utilization rate calculation to use the correct debt measurement and handle decimal precision properly:
Run the PoC again. Result: The utilization rate is correctly returned(a value between 0-100):
RAAC emission is correct and properly decreased over time if the utilization ratio doesn't change.