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

Pool balance is updated twice during refinance

Summary

During the execution of Lender::refinance a borrower will refinance a loan to a new pool, but the debt of the borrower is deducted twice (#L.636, #L.698) from the new pool with no apparent reason.

Vulnerability Details

Assume a user interacts with pool1 which has a balance of 1000 loan tokens to borrow 100 loan tokens and deposits 100 collateral tokens. Later the borrower decides to refinance the loan to pool2, which also has a balance of 1000 loan tokens, because the lending terms are better, the borrower decides to increment the debt and the collateral to 200 tokens each.

modifier setPoolsAndBorrow() {
// create pool1
Pool memory pool1 = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100e18,
poolBalance: 1000e18,
maxLoanRatio: 2e18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
// set pool1
vm.prank(lender1);
lender.setPool(pool1);
// create pool2
Pool memory pool2 = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100e18,
poolBalance: 1000e18,
maxLoanRatio: 3e18,
auctionLength: 1 days,
interestRate: 900,
outstandingLoans: 0
});
// set pool2
vm.prank(lender2);
lender.setPool(pool2);
// prepare borrows
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: keccak256(abi.encode(lender1, address(loanToken), address(collateralToken))),
debt: 100e18,
collateral: 100e18
});
// borrow from pool1
vm.prank(borrower);
lender.borrow(borrows);
_;
}
function testDeductsTwiceFromPoolBalance() public setPoolsAndBorrow {
(,,,, uint256 pool1BalanceAfterBorrow,,,, uint256 pool1OutstandingLoansAfterBorrow) =
lender.pools(keccak256(abi.encode(lender1, address(loanToken), address(collateralToken))));
console.log("\n", "\t", "Pool 1 balance after borrow:", pool1BalanceAfterBorrow); // 900000000000000000000 ~ 9000
console.log("\t", "Pool 1 outstanding loans after borrow:", pool1OutstandingLoansAfterBorrow);
// prepare refinances
Refinance[] memory refinances = new Refinance[](1);
refinances[0] = Refinance({
loanId: 0,
poolId: keccak256(abi.encode(lender2, address(loanToken), address(collateralToken))),
debt: 200e18,
collateral: 200e18
});
// refinance loan 0 from pool1 to pool2
vm.prank(borrower);
lender.refinance(refinances);
(,,,, uint256 pool1BalanceAfterRefinance,,,,) =
lender.pools(keccak256(abi.encode(lender1, address(loanToken), address(collateralToken))));
console.log("\t", "Pool 1 balance after refinance:", pool1BalanceAfterRefinance);
(,,,, uint256 loanDebt,,,,,) = lender.loans(0);
(,,,, uint256 pool2BalanceAfterRefinance,,,, uint256 pool2outstandingLoansAfterRefinance) =
lender.pools(keccak256(abi.encode(lender2, address(loanToken), address(collateralToken))));
console.log("\t", "Loan 0 debt:", loanDebt);
console.log("\t", "Outstanding loans of pool 2 after refinance", pool2outstandingLoansAfterRefinance);
console.log("\t", "Pool 2 balance after refinance:", pool2BalanceAfterRefinance);
}

The results are shown below.

Running 1 test for test/audit/unit/LenderTest.t.sol:LenderTest
[PASS] testDeductsTwiceFromPoolBalance() (gas: 870423)
Logs:
Pool 1 balance after borrow: 900000000000000000000
Pool 1 outstanding loans after borrow: 100000000000000000000
Pool 1 balance after refinance: 1000000000000000000000
Loan 0 debt: 200000000000000000000
Outstanding loans of pool 2 after refinance 200000000000000000000
Pool 2 balance after refinance: 600000000000000000000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.00ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

After the borrow the balance and outstanding loans of pool1 are 900 and 100 tokens respectively. These values are correct.

After the borrower refinanced the loan, the debt of loan 0 and outstanding loan of pool2 increased to 200 tokens, but the balance of pool2 was decreased to 600 tokens, instead of 800 tokens.

Impact

One of the invariants of the protocol is that the sum of loan tokens of each pool should be equal to the balance of loan tokens in the protocol at all times, but this invariant is broken by deducting twice the new debt from the balance of the new pool.

Also the function can be potentially un-callable due to possible underflow caused by the double deduction. If the user would've tried to refinance to a debt of 600 tokens, then Lender::refinance would've reverted.

Tools Used

VS Code and Foundry

Recommendations

Make sure the balance of the pool is updated once.

Support

FAQs

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