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

giveLoan increases the loan.debt which is multiplied by interest rate, leading to overcharging of interest

Summary

Loans charge simple interest based on the interestRate parameter in the pool.

However when a loan is transferred via giveLoan(), the debt over which the interest is charged is increased.

First totalDebt is calculated by adding the interest and protocol interest

uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;

Then the original debt is overwritten by totalDebt:

loans[loanId].debt = totalDebt;

That means that the interest in this point is calculated based on debt+lenderInterest+protocolInterest rather than the debt when the loan was initialized. This means that the debt is COMPOUNDED every time it is transferred via giveLoan() compared to the lower simpleInterest charged when the loan is never transferred.

There is a code POC below:

Proof of Concept

The below code POC compares 2 scenarios:

In secnario without_giveLoan, the borrower has a loan for 2 years, interest rate at 10%.

  • Lender makes no transfers

  • Borrower repays the loan

Result: They borrower pays 1.2x their original deposit, so end up paying 20% interest.

In scenario 2:
In secnario with_giveLoan, the borrower has a loan for 2 years, interest rate at 10%.

  • Lender gives the loan to another account, lender2, 1 year in

  • The borrower repays the laon

Result: The borrower pays 1.21x their original deposit, they paid 10% the first year and 10% of 1.1x their original deposit the second year. They have paid compound interest instead!

This can be exacerbated if there are multiple calls to giveLoan() interspersed over that period, the interest is re-compounded every time it is called.

function test_without_giveLoan_interest() public {
test_borrow();
vm.warp(block.timestamp + 365*2 days);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = keccak256(
abi.encode(
address(lender2),
address(loanToken),
address(collateralToken)
)
);
uint feeReceiverBalanceBefore = loanToken.balanceOf(address(this));
loanToken.mint(address(borrower), 1000*10**18);
uint borrowerBeforeRepay = loanToken.balanceOf(borrower);
vm.prank(borrower);
lender.repay(loanIds);
console.log(borrowerBeforeRepay - loanToken.balanceOf(borrower), "how much borrower repaid");
console.log(loanToken.balanceOf(borrower), "loan token in borrower account");
console.log(loanToken.balanceOf(address(this)) - feeReceiverBalanceBefore, "feeReceived");
}
function test_giveLoan_interest() public {
test_borrow();
vm.warp(block.timestamp + 365 days);
vm.startPrank(lender2);
Pool memory p = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = keccak256(
abi.encode(
address(lender2),
address(loanToken),
address(collateralToken)
)
);
vm.startPrank(lender1);
lender.giveLoan(loanIds, poolIds);
vm.warp(block.timestamp + 365 days);
loanToken.mint(address(borrower), 1000*10**18);
vm.stopPrank();
uint borrowerBeforeRepay = loanToken.balanceOf(borrower);
vm.prank(borrower);
lender.repay(loanIds);
console.log(borrowerBeforeRepay - loanToken.balanceOf(borrower), "how much borrower repaid");
}
[PASS] test_without_giveLoan_interest() (gas: 530208)
Logs:
120000000000000000000 how much borrower repaid
979500000000000000000 loan token in borrower account
2000000000000000000 feeReceived
[PASS] test_giveLoan_interest() (gas: 724380)
Logs:
121000000000000000000 how much borrower repaid

Impact

The loaner can giveLoan() to their other account to change their loan from Simple Interest to Compound Interest, effectively overcharging their interest

Tools Used

Foundry

Recommendations

Do not change the lender debt over which interest is charged when giveLoan() is called, while also ensuring that the debt is tracked correctly.

Support

FAQs

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