Core Contracts

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

Decimal Precision Mismatch in Utilization Rate Calculation in RAACMinter.sol

Summary

The RAACMinter contract is miscalculating the utilization rate and adjusting the emissions incorrectly as a result. This is due to a decimal precision mismatch.

The getUtilizationRate() function calculates the system utilization by dividing the normalized debt (totalBorrowed) by the total deposits. However, there is a unit mismatch: the normalized debt is expressed in ray precision (1e27), while total deposits are in standard token precision (1e18). This mismatch introduces an unintended scaling factor of 1e9, which can significantly skew the computed utilization rate. The miscalculation leads to incorrect emission rate adjustments, adversely affecting the protocol’s token distribution and economic incentives.

Note that this bug can also lead to a computed utilization rate of zero, even when the stability pool has deposits!

Vulnerability Details

The issue arises because totalBorrowed is obtained via lendingPool.getNormalizedDebt(), which returns a value in ray precision (1e27), while totalDeposits is obtained via stabilityPool.getTotalDeposits(), which returns a value in standard token units (1e18). Calculation Issue:

function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt(); //@audit RAY 1e27
uint256 totalDeposits = stabilityPool.getTotalDeposits(); //@audit 1e18
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
} //@audit decimal mismatch between numerator and denominator

This effectively multiplies the true utilization ratio by 1e9 (since 1e27 / 1e18 = 1e9). This leads to an erroneously inflated (or mis-truncated) utilization rate.

Potential Exploitation: An attacker could manipulate the denominator (for example, by using flash loans to temporarily lower the total deposits) to further skew the utilization calculation. This may result in improper emission rate adjustments, reducing token emissions and adversely affecting the protocol's economic incentives.

PoC

Note: We slightly modify the contract in order to be able to see the uint256 utilizationRate used by the calculateNewEmissionRate() in RAACMinter.sol:

Step 1: a) utilizationRate becomes a state variable. Modify calculateNewEmissionsRate() by removing "uint256"

b) calculateNewEmissionRate() is no longer view

function calculateNewEmissionRate() internal returns (uint256) { //@audit removed view for POC
utilizationRate = getUtilizationRate(); //<==@audit for POC ***THIS IS NOW A STATE VARIABLE***
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;
}

Step 2: Add the state variable at the end of storage

[...]
[state variables]
[...]
uint256 public lastEmissionUpdateTimestamp;
uint256 public constant BASE_EMISSION_UPDATE_INTERVAL = 1 days;
uint256 public emissionUpdateInterval;
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UPDATER_ROLE = keccak256("UPDATER_ROLE");
bytes32 public constant EMERGENCY_SHUTDOWN_ROLE = keccak256("EMERGENCY_SHUTDOWN_ROLE");
uint256 public constant TIMELOCK_DURATION = 2 days;
mapping(bytes32 => uint256) public timeLocks;
uint256 public utilizationRate; //***@audit added state variable for POC***

Step 3: paste the following test into RAACMinter.test.js and run with:

npx hardhat test test/unit/core/minters/RAACMinter.test.js --show-stack-traces

describe.only("RAACMinter - Ray/Wad Utilization Mismatch PoC", function () {
let RAACToken, RAACMinter, MockLendingPool, MockStabilityPool;
let raacToken, raacMinter, lendingPool, stabilityPool;
let owner;
beforeEach(async function () {
[owner] = await ethers.getSigners();
// Deploy RAAC Token
RAACToken = await ethers.getContractFactory("RAACToken");
raacToken = await RAACToken.deploy(owner.address, 100, 50);
// Deploy Mock LendingPool returning 'normalized debt' in ray (1e27 precision)
MockLendingPool = await ethers.getContractFactory("MockLendingPool");
lendingPool = await MockLendingPool.deploy();
// Deploy Mock StabilityPool returning total deposits in wad (1e18 precision)
MockStabilityPool = await ethers.getContractFactory("MockStabilityPool");
stabilityPool = await MockStabilityPool.deploy(
await raacToken.getAddress()
);
// Deploy RAACMinter pointing to the two mocks
RAACMinter = await ethers.getContractFactory("RAACMinter");
raacMinter = await RAACMinter.deploy(
await raacToken.getAddress(),
await stabilityPool.getAddress(),
await lendingPool.getAddress(),
owner.address
);
// Let RAACMinter mint tokens
await raacToken.setMinter(await raacMinter.getAddress());
// Set the utilizationTarget to 70 in RAACMinter if not already (depends on constructor defaults)
// Doing it here for clarity:
await raacMinter.setUtilizationTarget(70);
});
it("should NOT change emissionRate if actual utilization is exactly 70%, but due to the bug it does", async function () {
// -----------------------------
// STEP A: Setting up "real" 70% utilization
//
// If we truly wanted 0.7 tokens borrowed vs. 1 token deposited, we'd do:
// borrowed = 0.7 * 1e18 = 700000000000000000 in wad terms
// but because the LendingPool returns 'normalizedDebt' in ray (1e27),
// we store that as: borrowedWad * 1e9 => 0.7 * 1e18 * 1e9 = 7e26.
// So:
// 7e26 in ray = 0.7 tokens
// 1e18 in wad = 1 token
// If done correctly, utilization = (0.7 * 100) / 1.0 = 70 => exactly the target.
//
// However, the minter uses the raw formula:
// (7e26 * 100) / 1e18 => 7e28 / 1e18 => 7e10 => 70,000,000,000
// which is obviously >> 70.
// => The minter sees "above target" and changes the emission rate by 5%.
// -----------------------------
// 1) Set normalized debt to 7e26 (thinking it's 0.7 tokens, in ray)
await lendingPool.mockGetNormalizedDebt("700000000000000000000000000"); // 7e26
// 2) Set total deposits to 1e18 (1 token, in wad)
await stabilityPool.mockGetTotalDeposits("1000000000000000000"); // 1e18
// Double-check the Minter's target = 70
const currentTarget = await raacMinter.utilizationTarget();
expect(currentTarget).to.equal(70);
// -----------------------------
// STEP B: Observe the emission rate before/after update
// If the math were correct, it should see usage == 70 => no change to emissionRate.
// But it will see usage ~ 70,000,000,000 => well above 70 => adjusts by ~5%.
// -----------------------------
const oldRate = await raacMinter.emissionRate();
console.log("Old emissionRate:", oldRate.toString());
// Force update
await raacMinter.updateEmissionRate();
const newRate = await raacMinter.emissionRate();
console.log("New emissionRate:", newRate.toString());
// If usage matched the target precisely, the contract logic's "if utilizationRate > utilizationTarget"
// should NOT trigger an increase. The emission rate would remain unchanged.
// But due to the mismatch (70,000,000,000 >> 70), we expect it to do a 5% bump.
expect(newRate).to.not.equal(oldRate);
//The log below confirms the bug is happening. Contract is miscalculating the utilization rate and adjusting the emissions incorrectly as a result.
const finalUtilizationRate = await raacMinter.utilizationRate();
console.log("Final Utilization Rate: ", finalUtilizationRate);
console.log("Current Target Utilization Rate:", currentTarget);
expect(finalUtilizationRate).to.not.equal(70);
console.log(
"Bug demonstration:",
"We set usage to exactly 70% if done in correct math, yet the emission rate changed anyway,",
"because the contract sees a ~70,000,000,000% utilization from mixing ray/wad units."
);
});
});

Impact

  • **Utilization is Higher (or Lower) Than Reality: **If the contract expects 70% usage (0.7 token borrowed / 1 token deposited), it might see 70,000,000,000% instead of 70%. As a result, the RAACMinter incorrectly decides the system is well above its target utilization, so it applies its 5% upward adjustment (or downward, if the mismatch indicates artificially low usage).

  • **Repeated Misalignment Over Time: **Although each emission rate update is designed to change the rate by only 5%, that 5% can still accumulate across multiple updates. If the protocol frequently re-checks utilization (e.g., once per day), the error continuously nudges the emission rate away from what a correct calculation would produce.

  • **Economic Incentives Are Distorted: **Because the minter’s core logic (increase or decrease emissions) hinges on whether utilization is above or below the target, a miscalculated utilization can keep the emission rate rising or falling inappropriately. Liquidity providers or other participants may earn rewards higher or lower than intended. This, in turn, can shift user behavior—some might deposit less, or borrow more, because the returns / costs are not in line with the protocol’s intended targets.

Tools Used

Manual review, Hardhat

Recommendations

Standardize Units: Convert either the numerator or denominator so both are in the same unit. For example, if totalBorrowed is in ray (1e27) and totalDeposits is in token units (1e18), divide totalBorrowed by 1e9 before the calculation: solidity Copy uint256 normalizedBorrowed = totalBorrowed / 1e9; return (normalizedBorrowed * 100) / totalDeposits; Alternatively, adjust totalDeposits to ray precision.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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 6 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.