Summary
Lender.sol calculates the interest rate on loans with insufficient decimal precision. This rounds down the accrued interest and protocol fee and in some configurations allows for interest-free and fee-free borrows.
Vulnerability Details
The function _calculateInterest calculates the interest rate as such:
function _calculateInterest(
Loan memory l
) internal view returns (uint256 interest, uint256 fees) {
uint256 timeElapsed = block.timestamp - l.startTimestamp;
interest = (l.interestRate * l.debt * timeElapsed) / 10000 / 365 days;
fees = (lenderFee * interest) / 10000;
interest -= fees;
}
The function uses Solidity's truncating integer division and returns 0 when l.interestRate * l.debt * timeElapsed < 315_360_000_000. For sufficiently low debt the calculated interest and fees will be 0. For low-decimal coins such as GUSD, EURS (2-decimal) there are realistic scenarios in which a non-trivial amount is lent out interest-free (see concrete exampels in PoC).
Instead, the calculation for interest and fees should always round up. Users should be charged at least 1 wei.
Impact
High - interest-free loans are allowed
PoC
The following test provides 3 real-world examples for this bug.
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import {Loan} from "../src/utils/Structs.sol";
contract LenderHarness is Lender {
function calculateInterest(
Loan memory l
) public view returns (uint256 interest, uint256 fees) {
return _calculateInterest(l);
}
}
contract PoC_InterestRateTest is Test {
LenderHarness public harness;
function setUp() public {
harness = new LenderHarness();
}
function calculateInterest(uint256 debt, uint256 interestRate, uint256 elapsedTime) private {
Loan memory l;
l.startTimestamp = 1;
l.interestRate = interestRate;
l.debt = debt;
vm.warp(elapsedTime);
(uint256 interest, uint256 fees) = harness.calculateInterest(l);
console.log("For debt=%e, interestRate=%sbps, elapsedTime=%ss", debt, interestRate, elapsedTime);
console.log("Result: interest=%s, fees=%s", interest, fees);
console.log("---");
assertEq(interest, 0);
assertEq(fees, 0);
}
function test_PoC() public {
calculateInterest(100e2, 50, 3 days);
calculateInterest(1e6, 20, 30 minutes);
calculateInterest(1000e6, 5, 1 minutes);
}
}
Tools Used
Manual review, foundry
Recommendations
The best way is to use rounding up math when calculating interest rate and fees. For example:
function _calculateInterest(
Loan memory l
) internal view returns (uint256 interest, uint256 fees) {
uint256 timeElapsed = block.timestamp - l.startTimestamp;
interest = (l.interestRate * l.debt * timeElapsed + 10000 * 365 days - 1) / 10000 / 365 days;
fees = (lenderFee * interest + 9999) / 10000;
interest -= fees;
}