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

Stealing any loan opening for auction through others' lending pool

Summary

An attacker can steal any loan opening for auction for free by executing the Lender::buyLoan() and inputting anyone else's poolId as an argument.

Vulnerability Details

Root cause: the buyLoan() lacks verification that a caller (msg.sender) must be the new pool's lender.

Therefore, an attacker (which can be anyone, including a current pool's lender) can execute the buyLoan() to force anyone else's lending pool to pay the loan's total debt for the attacker.

Subsequently, the forced pool has to pay for the debt but the attacker will become a new lender for free.

function buyLoan(uint256 loanId, bytes32 poolId) public {
...
// if they do have a big enough pool then transfer from their pool
@> _updatePoolBalance(poolId, pools[poolId].poolBalance - totalDebt); //@audit the forced pool has to pay for the debt
pools[poolId].outstandingLoans += totalDebt;
...
// update the loan with the new info
@> loans[loanId].lender = msg.sender; //@audit the attacker will become a new lender
loans[loanId].interestRate = pools[poolId].interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].debt = totalDebt;
...
}
  • The forced pool has to pay for the debt: https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L489

  • The attacker will become a new lender: https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L518

Once a borrower repays the loan, the attacker address will be used to compute the poolId. The computed poolId will point to the attacker's pool. This way, the attacker can steal the loan principal (loan.debt) and interest (lenderInterest).

function repay(uint256[] calldata loanIds) public {
for (uint256 i = 0; i < loanIds.length; i++) {
...
bytes32 poolId = getPoolId(
@> loan.lender,
loan.loanToken,
loan.collateralToken
);
// update the pool balance
_updatePoolBalance(
@> poolId,
@> pools[poolId].poolBalance + loan.debt + lenderInterest
);
...
}
}
  • The attacker address will be used to compute the poolId: https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L304

  • The computed poolId will point to the attacker's pool: https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L311-L312

Impact

An attacker can steal any loan opening for auction for free by executing the buyLoan() and inputting anyone else's poolId as an argument. This vulnerability is considered a high-risk issue.

Tools Used

Manual Review

Recommendations

I recommend verifying that a caller (msg.sender) of the buyLoan() must be the new pool's lender, as shown below.

function buyLoan(uint256 loanId, bytes32 poolId) public {
...
+ if (msg.sender != pools[poolId].lender) revert Unauthorized();
...
// if they do have a big enough pool then transfer from their pool
_updatePoolBalance(poolId, pools[poolId].poolBalance - totalDebt);
pools[poolId].outstandingLoans += totalDebt;
...
// update the loan with the new info
loans[loanId].lender = msg.sender;
loans[loanId].interestRate = pools[poolId].interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].debt = totalDebt;
...
}

Support

FAQs

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