modifier setPoolsAndBorrow() {
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
});
vm.prank(lender1);
lender.setPool(pool1);
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
});
vm.prank(lender2);
lender.setPool(pool2);
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: keccak256(abi.encode(lender1, address(loanToken), address(collateralToken))),
debt: 100e18,
collateral: 100e18
});
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);
console.log("\t", "Pool 1 outstanding loans after borrow:", pool1OutstandingLoansAfterBorrow);
Refinance[] memory refinances = new Refinance[](1);
refinances[0] = Refinance({
loanId: 0,
poolId: keccak256(abi.encode(lender2, address(loanToken), address(collateralToken))),
debt: 200e18,
collateral: 200e18
});
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 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.
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.
Make sure the balance of the pool is updated once.