The buyLoan
function in the Lender
contract does not validate the loanToken
and collateralToken
token addresses when buying a loan to ensure the loan is moved to the correct pool with the same configured token addresses. As a result, an attacker can exploit this vulnerability to both steal funds from other lenders and cripple a loan, effectively locking the loan's collateral forever and causing a loss of funds for (previous) lender.
An auctioned loan can be bought with the buyLoan
function, providing the loan id (loanId
) and the pool id (poolId
) for the pool to move the loan to.
Please note that the caller,
msg.sender
, is not validated to be the owner of the pool with the givenpoolId
. This issue is covered by another high-severity submission - "Non-pool owner can buy a loan causing the loan to be crippled and funds to be stuck". This submission assumes that themsg.sender
is the owner of the pool with the givenpoolId
.
If a pool owner buys a loan that has a different loanToken
or collateralToken
than the new pool, the loan will be correlated to the wrong pool id later on. The loan's new lender is assigned to msg.sender
in line 518, i.e., the pool owner, and both the old and the new pool will have their pool balance and outstanding loan accounting variables updated accordingly.
The destination pool with the given poolId
id does not necessarily have the same loanToken
and collateralToken
as the loan. This results in adjusting pool balances of different tokens with different values, which can be exploited to lock the loan's collateral forever and cause accounting issues.
Additionally, whenever the pool id for a loan is determined as the keccak256
hash of the loan.lender
, loan.loanToken
, and the loan.collateralToken
(e.g., by using the getPoolId
function), this pool id is potentially different than the actual pool id that was supplied to the buyLoan
call (due to the different loanToken
or collateralToken
). This determined pool id may point to a non-existent pool with zero balance and zero outstandingLoans
, thus decrementing leads to an underflow error. This griefs the following actions and causes the collateral to be locked forever:
repay
will revert in line 314 due to subtracting the loan debt from outstandingLoans
, which is 0
giveLoan
reverts in line 400
buyLoan
reverts in line 502
seizeLoan
reverts in line 575
refinance
reverts in line 633
Re-visiting the above mentioned accounting issue, an attacker can steal funds from other pool owners (lenders).
This is accomplished by creating two pools - one regular pool with a high-value loan token and a second pool with a low-value loan token. The attacker borrows funds, i.e., creates a loan, via the first pool. Then the attacker buys the loan (which has a higher-value loan token) with the second pool id. Both pools, will have their pool balances and outstanding loans accounting variables updated.
However, by having different tokens, token amounts don't reflect the same monetary values. The loan's debt is thus not covered (collateralized) anymore with the correct token by this second pool. Now the attacker and owner of the first pool withdraws the pool balance via the removeFromPool
function and steals funds from other pool owners who deposited funds for the same loan token in the Lender
contract.
Consider the following example (for simplicity and demonstration purposes, interest was omitted):
Pools:
Pool | Lender | Collateral Token | Loan Token | Pool Balance | Outstanding Loans |
---|---|---|---|---|---|
1 | Bob | USDC | WETH | 10e18 WETH |
0 |
2 | Alice (Attacker) | USDC | WETH | 0 WETH (initially 10e18 WETH, lent out) |
10e18 WETH |
3 | Alice (Attacker) | USDC | DAI | 100e18 DAI |
0 |
Total WETH in the Lender
contract: 10e18
WETH
Loans:
Loan | Collateral Token | Loan Token | Collateral | Debt | Correlated Pool |
---|---|---|---|---|---|
1 | USDC | WETH | 1 USDC |
10e18 WETH |
2 |
Alice, the attacker, starts the auction for loan 1 and buys it by calling buyLoan(..)
with the pool id of pool 3.
Please note that the loan ratio can be purposefully set very high, i.e., having little collateral for a large debt, to make the loan unattractive for other buyers. It is only important to have the same maxLoanRatio
for both of attacker's pools.
This leads to the following state:
Pools:
Pool | Lender | Collateral Token | Loan Token | Pool Balance | Outstanding Loans |
---|---|---|---|---|---|
1 | Bob | USDC | WETH | 10e18 WETH |
0 |
2 | Alice | USDC | WETH | 0 + 10e18 = 10e18 WETH |
10e18 - 10e18 = 0 WETH |
3 | Alice | USDC | DAI | 100e18 - 10e18 = 90e18 DAI |
0 + 10e18 = 10e18 WETH |
Alice's pool (pool 2) now has its pool balance back to 10e18
WETH and is able to withdraw the funds via the removeFromPool
function, effectively stealing from Bob. The Lender
contract will now have 0
WETH.
If Bob attempts to withdraw his pool balance, he will not be able to do so as the Lender
contract does not have sufficient WETH funds to cover the withdrawal and faces a loss.
Please note that validation is also missing for maxLoanRatio
as well as auctionLength
.
Loan actions can be griefed (DoS'd) resulting in stuck collateral funds
Funds from other pool owners can be stolen from the Lender
contract thus resulting in a loss of funds for them
Additionally, accounting issues may arise if the loanToken
or collateralToken
have different decimals than the tokens intended for the pool.
Manual Review
Consider adding validation for pool.loanToken
, pool.collateralToken
, maxLoanRatio
, and auctionLength
in the buyLoan
function to ensure the pool matches the loan.
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.