20,000 USDC
View results
Submission Details
Severity: medium
Valid

Loss of precision in `_calculateInterest()` allows interest-free loans

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.

  • Paste this in a new file under test/.

  • Run with forge test --match-contract PoC_InterestRateTest -vv

// SPDX-License-Identifier: UNLICENSED
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); // 100 GUSD at 0.5% APR for 3 days
calculateInterest(1e6, 20, 30 minutes); // 1 USDC at 0.2% APR for 30 minutes
calculateInterest(1000e6, 5, 1 minutes); // 1000 USDC at 0.05% APR for 1 minute
}
}

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;
}

Support

FAQs

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