Attacker can become loan.lender by buying an auctioned loan on behalf of another lender's pool, permanently bricking the ability to repay that loan in the future.
Lender.buyLoan() never validates that msg.sender is the lender of the pool, so an attacker who is not a lender can call Lender.buyLoan() to buy a loan on behalf of a lender's pool. This appears to be by design so by itself this is not a bug but the next point is the key:
In L518 loans[loanId].lender gets set to msg.sender (the attacker's address), which will brick Lender.repay() as the call to getPoolId uses loan.lender to find the correct pool, which will fail as the attacker has become loan.lender and the attacker is not a lender of any pool.
Proof of concept: first add the following helper function to Lender.sol:
Then add the PoC to test/Lender.t.sol:
Loan is permanently bricked and can never be repaid as attacker has become loan.lender.
Manual
Two options:
Lender.buyLoan() can validate that msg.sender is the pool's lender. This would stop anyone from matching an auctioned loan with a pool, so according to the system design this is not a great option,
Change L518 to set loans[loanId].lender to pools[poolId].lender instead of msg.sender. This preserves the system design that anyone can match an auctioned loan with a pool so this is the better option.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.