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

Using forged/fake lending pools to steal any loan opening for auction

Summary

An attacker can steal any loan opening for auction by executing the Lender::buyLoan() and specifying the poolId parameter to a forged/fake pool. This vulnerability can even cause the protocol to become insolvent because an attacker moves out the stolen loans' loanToken and/or collateralToken tokens.

Vulnerability Details

Root cause: the buyLoan() lacks verification that the loan's loanToken and collateralToken must be identical to the new pool.

Therefore, an attacker can buy the loan using another pool of different loanToken/collateralToken pair.

To elaborate on this vulnerability, assume that a loan of 1 WETH (loanToken) / 2000 USDC (collateralToken) is opened for auction. An attacker can execute the buyLoan() to buy the loan by pointing to a pool of DAI (loanToken) / USDC (collateralToken). In this way, the attacker can steal the loan's debt of $2000+ using small DAI tokens (totalDebt = 1 + lenderInterest + protocolInterest).

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 balance subtraction from a forged pool
pools[poolId].outstandingLoans += totalDebt;
...
// update the loan with the new info
@> loans[loanId].lender = msg.sender; //@audit the attacker becomes a new lender
loans[loanId].interestRate = pools[poolId].interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].debt = totalDebt;
...
}
  • Balance subtraction from a forged pool: https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L489

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

Impact

An attacker can steal any loan opening for auction by executing the buyLoan() and specifying the poolId parameter to a forged pool. The forged pool can even point to a pair of fake loanToken and collateralToken, which has worth $0.

This vulnerability can even cause the protocol to become insolvent because an attacker moves out the stolen loans' loanToken and/or collateralToken tokens. Hence, I consider this vulnerability a high-risk issue.

Tools Used

Manual Review

Recommendations

I recommend verifying that the loan's loanToken and collateralToken must be identical to the new pool, as shown below.

function buyLoan(uint256 loanId, bytes32 poolId) public {
...
+ if (loan.loanToken != pools[poolId].loanToken) revert TokenMismatch();
+ if (loan.collateralToken != pools[poolId].collateralToken) revert TokenMismatch();
...
// 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.