The RAACMinter contract's utilization rate calculation incorrectly compares a RAY-scaled (1e27) usage index against WAD-scaled (1e18) token balances, resulting in massively inflated utilization rates that can reach millions of percent that affect emission calculations and overall protocol economics.
The issue lies in the getUtilizationRate() function in RAACMinter.sol where the calculation uses mismatched values:
High - The issue will consistently occur as it's a fundamental calculation error in the core economic logic.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "contracts/core/tokens/DebtToken.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/DEToken.sol";
import "contracts/core/minters/RAACMinter/RAACMinter.sol";
import "contracts/libraries/math/PercentageMath.sol";
import "contracts/libraries/math/WadRayMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock", "MCK") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract MasterTest is Test {
using PercentageMath for uint256;
using WadRayMath for uint256;
DebtToken public debtToken;
RToken public rToken;
RAACNFT public nft;
RAACToken public raacToken;
DEToken public deToken;
RAACHousePrices public priceOracle;
LendingPool public lendingPool;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
MockERC20 public mockCrvUSD;
address borrower = address(0x1);
address lender = address(0x2);
address lender2 = address(0x3);
address treasury = address(0x4);
address repairFund = address(0x5);
address protocolOwner = address(0x999);
function setUp() public {
vm.warp(1000 days);
vm.startPrank(protocolOwner);
mockCrvUSD = new MockERC20();
priceOracle = new RAACHousePrices(protocolOwner);
raacToken = new RAACToken(
protocolOwner,
100,
50
);
rToken = new RToken(
"RToken",
"RTKN",
protocolOwner,
address(mockCrvUSD)
);
debtToken = new DebtToken(
"DebtToken",
"DEBT",
protocolOwner
);
deToken = new DEToken(
"DEToken",
"DETKN",
protocolOwner,
address(rToken)
);
nft = new RAACNFT(
address(mockCrvUSD),
address(priceOracle),
protocolOwner
);
lendingPool = new LendingPool(
address(mockCrvUSD),
address(rToken),
address(debtToken),
address(nft),
address(priceOracle),
1e27
);
stabilityPool = new StabilityPool(protocolOwner);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
protocolOwner
);
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(mockCrvUSD),
address(lendingPool)
);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
protocolOwner
);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
mockCrvUSD.mint(borrower, 100_000e18);
mockCrvUSD.mint(lender, 500_000e18);
vm.stopPrank();
vm.startPrank(borrower);
mockCrvUSD.approve(address(lendingPool), type(uint256).max);
mockCrvUSD.approve(address(nft), type(uint256).max);
nft.setApprovalForAll(address(lendingPool), true);
vm.stopPrank();
vm.startPrank(protocolOwner);
priceOracle.setOracle(protocolOwner);
priceOracle.setHousePrice(1, 100_000e18);
vm.stopPrank();
vm.startPrank(lender);
mockCrvUSD.approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(500_000e18);
vm.stopPrank();
}
function test_incorrectUtilizationTest() public {
vm.startPrank(borrower);
nft.mint(1, 100_000e18);
lendingPool.depositNFT(1);
uint256 borrowAmount = 50_000e18;
lendingPool.borrow(borrowAmount);
vm.stopPrank();
vm.startPrank(lender);
rToken.approve(address(stabilityPool), type(uint256).max);
stabilityPool.deposit(50_000e18);
vm.stopPrank();
uint256 initialUsageIndex = lendingPool.getNormalizedDebt();
console.log("Initial Usage Index:", initialUsageIndex);
vm.warp(block.timestamp + 365 days);
lendingPool.updateState();
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
console.log("Final Usage Index:", totalBorrowed);
uint256 totalDeposits = stabilityPool.getTotalDeposits();
console.log("Total borrowed: ", totalBorrowed);
console.log("Total deposited: ", totalDeposits);
console.log("StabilityPool.sol::calculateUtilizationRate mock result: ", (totalBorrowed * 100) / totalDeposits);
}
}
The issue stems from the way the utiliazation rate is calculated; even if the precision was 'fixed' in the current implementation, you would then get a small numerator / total deposits so the inverse problem would occur.