Summary
The prime rate is being initialized in LendingPool.sol constructor via -
rateData.primeRate = uint256(_initialPrimeRate);
and throughout the whole protocol, same value is being used. In ReserveLibrary.sol::calculateBorrowRate() function too -
function calculateBorrowRate(
uint256 primeRate,
uint256 baseRate,
uint256 optimalRate,
uint256 maxRate,
uint256 optimalUtilizationRate,
uint256 utilizationRate
) internal pure returns (uint256) {
if (primeRate <= baseRate || primeRate >= maxRate || optimalRate <= baseRate || optimalRate >= maxRate) {
revert InvalidInterestRateParameters();
}
uint256 rate;
if (utilizationRate <= optimalUtilizationRate) {
uint256 rateSlope = primeRate - 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 rateIncrease = excessUtilization.rayMul(rateSlope).rayDiv(maxExcessUtilization);
rate = primeRate + rateIncrease;
}
return rate;
}
As per RAACPrimeRateOracle doc [https://docs.raac.io/core/oracles/RAACPrimeRateOracle] -
Purpose
Fetch real-time prime rate data from off-chain sources
Securely update prime rates in the LendingPool contract
Provide a trusted prime rate feed for the lending protocol
Track historical prime rate updates with timestamps
But the prime rate being used in ReserveLibrary.sol is constant.
Vulnerability Details
in current implementation prime rate is not adjusting according to market supply/demand.
more importantly, it can adjust to discourage over-borrowing. In case of liquidity shortage.
Impact
This can lead to plenty of issues -
In case of high volatile NFT or RWA assets, the static value of prime rate can give incorrect borrow rate amount, causing loss to protocol or user.
dyanamic prime rates , adjusts very well with risk-free rate changes, but in current implemetation prime rate is static.
LendingPool.sol can be drained out, due to liquidity shortage. If dynamic prime rate is being used it will discourage borrower to borrow further hence preventing draining.
Tools Used
Manual
Recommendations
There is getPrimeRate() function in RAACPrimeRateOracle.sol unused, that can be leveraged to make improvemnt in current implementation :-
RAACPrimeRateOracle.sol::getPrimeRate() ->
function getPrimeRate() external view returns (uint256) {
return lastPrimeRate;
}
Improved code ->
function calculateBorrowRate(
uint256 primeRate,
uint256 baseRate,
uint256 optimalRate,
uint256 maxRate,
uint256 optimalUtilizationRate,
uint256 utilizationRate
) internal pure returns (uint256) {
+ uint256 primeRate = primeRate > raacPrimeRateOracle.getPrimeRate()
+ ? primeRate
+ : raacPrimeRateOracle.getPrimeRate();
if (primeRate <= baseRate || primeRate >= maxRate || optimalRate <= baseRate || optimalRate >= maxRate) {
revert InvalidInterestRateParameters();
}
uint256 rate;
if (utilizationRate <= optimalUtilizationRate) {
uint256 rateSlope = primeRate - 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 rateIncrease = excessUtilization.rayMul(rateSlope).rayDiv(maxExcessUtilization);
rate = primeRate + rateIncrease;
}
return rate;
}
In the proved code the prime is being fetched from RAACPrimeRateOracle, if getPrimeRate() is greater than primeRate use fetched value, and if it's less then use the initial primeRate value.