Core Contracts

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

Interest Rate Model Uses Prime Rate Instead of Optimal Rate at Optimal Utilization

Summary

The interest rate model in ReserveLibrary::calculateBorrowRate() calculates borrowing rates using the prime rate instead of the configured optimal rate,
causing borrowers to pay double the intended interest rate when utilization reaches the optimal point.

Severity

High

Vulnerability Details

The ReserveLibrary implements a two-slope interest rate model where rates should scale from base rate to optimal rate in the first slope (0-80% utilization), then from optimal rate to max rate in the second slope (80-100% utilization). However, the implementation incorrectly uses the prime rate instead of optimal rate when calculating the first slope.
The optimal rate is explicitly set to 50% of the prime rate during initialization, indicating an intended relationship between these rates. However, in calculateBorrowRate, the function uses primeRate in its calculations despite taking optimalRate as a parameter:

if (utilizationRate <= optimalUtilizationRate) {
uint256 rateSlope = primeRate - baseRate; // Uses primeRate instead of optimalRate
uint256 rateIncrease = utilizationRate.rayMul(rateSlope).rayDiv(optimalUtilizationRate);
rate = baseRate + rateIncrease;
}

This causes the first slope to target the prime rate instead of the optimal rate at the optimal utilization point, creating a steeper rate curve than intended in normal market conditions.

This is proven to be unintentional behaviour from the documentation.

| optimalRate | uint256 | Rate at optimal utilization in RAY |

And also this blogpost

The protocol offers predictable borrowing rates, with interest rates soft-pegged to half of US Prime Rate

Impact

High impact:

  1. Protocol Design Impact:

  • Breaks core interest rate model assumptions

  • Nullifies the purpose of having an optimal rate parameter

  • Documentation and implementation mismatch on fundamental mechanism

  1. Economic Impact:

  • Borrowers consistently pay double the intended interest rate

  • Distorts the intended economic incentives of the two-slope model

Likelihood

High likelihood:

  • Triggered on every borrow transaction when utilization approaches optimal level

  • Built into the core interest rate calculation

  • Affects all markets and all users equally

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 {Test, console} from "lib/forge-std/src/Test.sol";
import {ReserveLibrary} from "contracts/libraries/pools/ReserveLibrary.sol";
import {WadRayMath} from "contracts/libraries/math/WadRayMath.sol";
import {PercentageMath} from "contracts/libraries/math/PercentageMath.sol";
contract TestOptimalUtilizationRate is Test {
using WadRayMath for uint256;
using PercentageMath for uint256;
ReserveLibrary.ReserveData public reserve;
ReserveLibrary.ReserveRateData public rateData;
function setUp() public {
// Initialize rate data with more realistic values
rateData.primeRate = 1000e25; // 10% prime rate in RAY
rateData.baseRate = rateData.primeRate.percentMul(25_00); // 2.5%
rateData.optimalRate = rateData.primeRate.percentMul(50_00); // 5%
rateData.maxRate = rateData.primeRate.percentMul(400_00); // 40%
rateData.optimalUtilizationRate = WadRayMath.RAY.percentMul(80_00); // 80%
}
function testRateAtOptimalUtilization() public {
// Initialize rates
rateData.primeRate = 1000e25; // 10% interest rate
rateData.baseRate = rateData.primeRate.percentMul(25_00); // 2.5%
rateData.optimalRate = rateData.primeRate.percentMul(50_00); // 5%
rateData.maxRate = rateData.primeRate.percentMul(400_00); // 40%
rateData.optimalUtilizationRate = WadRayMath.RAY.percentMul(80_00); // 80%
// Calculate rate at optimal utilization
uint256 rate = ReserveLibrary.calculateBorrowRate(
rateData.primeRate,
rateData.baseRate,
rateData.optimalRate,
rateData.maxRate,
rateData.optimalUtilizationRate,
rateData.optimalUtilizationRate
);
// Logs show actual vs intended rates
console.log("Actual rate at optimal utilization:", rate / 1e25, "%");
console.log("Expected optimal rate:", rateData.optimalRate / 1e25, "%");
console.log("Prime rate:", rateData.primeRate / 1e25, "%");
// Proves rate equals prime rate instead of optimal rate
assertEq(rate, rateData.primeRate);
assertEq(rate.rayDiv(rateData.optimalRate), 2 * WadRayMath.RAY);
}
}
  1. Run
    forge test

  2. Output:

Ran 1 test for test-foundry/TestOptimalUtilizationRate.t.sol:TestOptimalUtilizationRate
[PASS] testRateAtOptimalUtilization() (gas: 22778)
Logs:
Actual rate at optimal utilization: 1000 %
Expected optimal rate: 500 %
Prime rate: 1000 %
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.79ms (2.80ms CPU time)

Recommendations

Modify the rate calculation in ReserveLibrary.sol to use optimalRate instead of primeRate:

function calculateBorrowRate(
uint256 primeRate,
uint256 baseRate,
uint256 optimalRate,
uint256 maxRate,
uint256 optimalUtilizationRate,
uint256 utilizationRate
) internal pure returns (uint256) {
if (utilizationRate <= optimalUtilizationRate) {
- uint256 rateSlope = primeRate - baseRate;
+ uint256 rateSlope = optimalRate - baseRate;
uint256 rateIncrease = utilizationRate.rayMul(rateSlope).rayDiv(optimalUtilizationRate);
rate = baseRate + rateIncrease;
} else {
uint256 excessUtilization = utilizationRate - optimalUtilizationRate;
uint256 maxExcessUtilization = WadRayMath.RAY - optimalUtilizationRate;
- uint256 rateSlope = maxRate - primeRate;
+ uint256 rateSlope = maxRate - optimalRate;
uint256 rateIncrease = excessUtilization.rayMul(rateSlope).rayDiv(maxExcessUtilization);
rate = primeRate + rateIncrease;
}
return rate;
}
Updates

Lead Judging Commences

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

ReserveLibrary::calculateBorrowRate uses primeRate instead of optimalRate in first slope calculation, causing borrowers to pay double the intended interest at optimal utilization

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

ReserveLibrary::calculateBorrowRate uses primeRate instead of optimalRate in first slope calculation, causing borrowers to pay double the intended interest at optimal utilization

Support

FAQs

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