Bug Report
Summary
A vulnerability exists in the calculateLiquidityRate
function of the ReserveLibrary.sol
contract, where the function returns 0
when totalDebt
is zero. This results in the currentLiquidityRate
being zero, which prevents the liquidityIndex
from growing. As a consequence, depositors do not earn any interest on their deposits when there are no borrowings in the protocol.
Vulnerability Details
The calculateLiquidityRate
function is responsible for calculating the interest rate earned by depositors (currentLiquidityRate
). However, it returns 0
when totalDebt
is zero, as shown below:
function calculateLiquidityRate(
uint256 utilizationRate,
uint256 usageRate,
uint256 protocolFeeRate,
uint256 totalDebt
) internal pure returns (uint256) {
if (totalDebt < 1) {
return 0;
}
uint256 grossLiquidityRate = utilizationRate.rayMul(usageRate);
uint256 protocolFeeAmount = grossLiquidityRate.rayMul(protocolFeeRate);
uint256 netLiquidityRate = grossLiquidityRate - protocolFeeAmount;
return netLiquidityRate;
}
This behavior causes the liquidityIndex
to remain stagnant, preventing depositors from earning interest when there are no borrowings.
ImpactDepositors Earn No Interest When `totalDebt` is Zero
Depositors Earn No Interest: When totalDebt
is zero, depositors do not earn any interest on their funds.
Protocol Reputation Damage: Users may lose trust in the protocol if they realize they are not earning interest.
Economic Imbalance: The protocol's economic model relies on a balance between lenders and borrowers. If lenders are not incentivized, the protocol may struggle to attract liquidity.
Tools Used
Foundry: Used to write and execute the test case that demonstrates the issue.
Manual Code Review: Identified the vulnerability by analyzing the calculateLiquidityRate
function and its usage in the protocol.
Proof of Concept (PoC)
Below is a Foundry test that demonstrates the issue:
Test Code
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/LendingPool.sol";
import "../src/ReserveLibrary.sol";
import "../src/WadRayMath.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract LendingPoolTest is Test {
LendingPool public lendingPool;
ERC20 public reserveAsset;
address public user = address(0x123);
function setUp() public {
reserveAsset = new ERC20("Reserve Asset", "RA");
lendingPool = new LendingPool(
address(reserveAsset),
address(0),
address(0),
address(0),
address(0),
1e27
);
reserveAsset.mint(user, 1000e18);
}
function testDepositEarnsNoInterestWhenTotalDebtIsZero() public {
vm.prank(user);
reserveAsset.approve(address(lendingPool), 100e18);
vm.prank(user);
lendingPool.deposit(100e18);
vm.warp(block.timestamp + 365 days);
vm.prank(user);
lendingPool.withdraw(100e18);
uint256 userBalance = reserveAsset.balanceOf(user);
assertEq(userBalance, 1000e18, "User did not earn any interest");
}
}
Recommendations
Introduce a minimum liquidity rate in the calculateLiquidityRate
function to ensure that depositors earn interest even when totalDebt
is zero. Here’s the modified function:
function calculateLiquidityRate(
uint256 utilizationRate,
uint256 usageRate,
uint256 protocolFeeRate,
uint256 totalDebt
) internal pure returns (uint256) {
uint256 minLiquidityRate = 0.01 * 1e27;
if (totalDebt < 1) {
return minLiquidityRate;
}
uint256 grossLiquidityRate = utilizationRate.rayMul(usageRate);
uint256 protocolFeeAmount = grossLiquidityRate.rayMul(protocolFeeRate);
uint256 netLiquidityRate = grossLiquidityRate - protocolFeeAmount;
return netLiquidityRate > minLiquidityRate ? netLiquidityRate : minLiquidityRate;
}