Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Incorrect `RAACMinter.sol::calculateNewEmissionRate()` Due to Index vs Balance Mismatch

Summary

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.

Vulnerability Details

The issue lies in the getUtilizationRate() function in RAACMinter.sol where the calculation uses mismatched values:

function getUtilizationRate() internal view returns (uint256) {
@> uint256 totalBorrowed = lendingPool.getNormalizedDebt();
@> uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}

The flow of the issue:

  1. getNormalizedDebt() returns the usage index which:

  • Starts at 1e27 (RAY precision)

  • Grows with interest accrual

  1. getTotalDeposits() returns actual token amounts in 1e18 precision

  2. The calculation multiplies by 100 before division, further amplifying the precision error

  3. Results in utilization rates in the millions of percent

This utilization rate is then used in calculateNewEmissionRate() as an important check:

function calculateNewEmissionRate() internal view returns (uint256) {
@> uint256 utilizationRate = getUtilizationRate();
uint256 adjustment = (emissionRate * adjustmentFactor) / 100;
@> if (utilizationRate > utilizationTarget) {

Output from test:

Logs:
Initial Usage Index: 1000000000000000000000000000
Final Usage Index: 1410226029899202994928355252
Total borrowed: 1410226029899202994928355252
Total deposited: 51718750000000000000000
StabilityPool.sol::calculateUtilizationRate result: 2726721

Impact

High

  • Utilization rates consistently report >1000x the actual utilization

  • Will force emission rates to maximum values due to perceived over-utilization

  • Economic model becomes dysfunctional as emissions cannot properly respond to true utilization

Likelihood

High - The issue will consistently occur as it's a fundamental calculation error in the core economic logic.

Proof of Concept

  1. Convert the project into a foundry project, ensuring test in foundry.toml points to a designated test directory.

  2. Comment out the forking object from the hardhat.congif.cjs file:

networks: {
hardhat: {
mining: {
auto: true,
interval: 0
},
//forking: {
// url: process.env.BASE_RPC_URL,
//},
  1. Copy the following test into the directory:

// SPDX-License-Identifier: MIT
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;
// Core protocol tokens
DebtToken public debtToken;
RToken public rToken;
RAACNFT public nft;
RAACToken public raacToken;
DEToken public deToken;
// Protocol contracts
RAACHousePrices public priceOracle;
LendingPool public lendingPool;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
// Mock token
MockERC20 public mockCrvUSD;
// Test addresses
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);
// Deploy independent contracts
mockCrvUSD = new MockERC20();
priceOracle = new RAACHousePrices(protocolOwner);
raacToken = new RAACToken(
protocolOwner, // initialOwner
100, // initialSwapTaxRate - 1%
50 // initialBurnTaxRate - 0.5%
);
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
);
// Deploy StabilityPool
stabilityPool = new StabilityPool(protocolOwner);
// Deploy RAACMinter with StabilityPool address
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
protocolOwner
);
// Initialize StabilityPool
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(mockCrvUSD),
address(lendingPool)
);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
protocolOwner
);
// Set up contract connections as protocol owner
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
// Fund test accounts
mockCrvUSD.mint(borrower, 100_000e18); // For NFT purchase
mockCrvUSD.mint(lender, 500_000e18); // For lending pool liquidity
// mockCrvUSD.mint(address(stabilityPool), 1_000_000e18); // For liquidations
vm.stopPrank();
// Set up user approvals
vm.startPrank(borrower);
mockCrvUSD.approve(address(lendingPool), type(uint256).max);
mockCrvUSD.approve(address(nft), type(uint256).max);
nft.setApprovalForAll(address(lendingPool), true);
vm.stopPrank();
// Set up oracle price
vm.startPrank(protocolOwner);
priceOracle.setOracle(protocolOwner);
priceOracle.setHousePrice(1, 100_000e18); // NFT worth 100,000
vm.stopPrank();
// Add liquidity to lending pool
vm.startPrank(lender);
mockCrvUSD.approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(500_000e18);
vm.stopPrank();
}
function test_incorrectUtilizationTest() public {
// Setup scenario
vm.startPrank(borrower);
nft.mint(1, 100_000e18);
lendingPool.depositNFT(1);
uint256 borrowAmount = 50_000e18;
lendingPool.borrow(borrowAmount);
vm.stopPrank();
// Add 50000 rtoken to StabilityPool
vm.startPrank(lender);
rToken.approve(address(stabilityPool), type(uint256).max);
stabilityPool.deposit(50_000e18);
vm.stopPrank();
// Record initial state
uint256 initialUsageIndex = lendingPool.getNormalizedDebt();
console.log("Initial Usage Index:", initialUsageIndex);
// Time passes and interest accrues
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);
// NOTE: make RaacMinter.getUtilizationRate() a public function to use function directly.
// uint256 actualCalc = raacMinter.getUtilizationRate();
// console.log("actual calc: ", actualCalc);
}
}
  1. Run forge test -vv

Recommendations

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.

Current implementation divides a scalar index by the total number of deposits:

The utilization rate should be calculated by:

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACMinter::getUtilizationRate incorrectly mixes stability pool deposits with lending pool debt index instead of using proper lending pool metrics

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACMinter::getUtilizationRate incorrectly mixes stability pool deposits with lending pool debt index instead of using proper lending pool metrics

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.