🎓 A1Learner
🔍
0 of 45 lessons completed

Solidity Flash Loans Concepts

A flash loan is a loan that is borrowed, used, and repaid inside one single transaction. If the repayment plus the fee does not arrive before the transaction ends, the whole transaction reverts.

A good receiver repays the loan plus the fee

The pool lends 1000 and charges a fee of 10. The good receiver returns 1010, so the pool ends with 10010.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(uint256 initialSupply) {
        balanceOf[msg.sender] = initialSupply;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "not enough");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "not enough");
        require(allowance[from][msg.sender] >= amount, "not allowed");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

interface IFlashReceiver {
    function onFlashLoan(uint256 amount, uint256 fee) external;
}

contract FlashPool {
    ToyToken public token;

    constructor(ToyToken token_) {
        token = token_;
    }

    function feeFor(uint256 amount) public pure returns (uint256) {
        return amount / 100;
    }

    function flashLoan(uint256 amount, IFlashReceiver receiver) public {
        uint256 before = token.balanceOf(address(this));
        require(amount <= before, "too much");
        uint256 fee = feeFor(amount);
        token.transfer(address(receiver), amount);
        receiver.onFlashLoan(amount, fee);
        require(token.balanceOf(address(this)) >= before + fee, "not repaid");
    }
}

contract GoodReceiver is IFlashReceiver {
    ToyToken public token;
    FlashPool public pool;

    constructor(ToyToken token_, FlashPool pool_) {
        token = token_;
        pool = pool_;
    }

    function onFlashLoan(uint256 amount, uint256 fee) external override {
        token.transfer(address(pool), amount + fee);
    }
}

contract BadReceiver is IFlashReceiver {
    function onFlashLoan(uint256, uint256) external override {
    }
}

A bad receiver that does not repay reverts the whole loan

The bad receiver keeps the borrowed tokens and returns nothing. The pool check fails, so the outer loan call reverts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(uint256 initialSupply) {
        balanceOf[msg.sender] = initialSupply;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "not enough");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "not enough");
        require(allowance[from][msg.sender] >= amount, "not allowed");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

interface IFlashReceiver {
    function onFlashLoan(uint256 amount, uint256 fee) external;
}

contract FlashPool {
    ToyToken public token;

    constructor(ToyToken token_) {
        token = token_;
    }

    function feeFor(uint256 amount) public pure returns (uint256) {
        return amount / 100;
    }

    function flashLoan(uint256 amount, IFlashReceiver receiver) public {
        uint256 before = token.balanceOf(address(this));
        require(amount <= before, "too much");
        uint256 fee = feeFor(amount);
        token.transfer(address(receiver), amount);
        receiver.onFlashLoan(amount, fee);
        require(token.balanceOf(address(this)) >= before + fee, "not repaid");
    }
}

contract GoodReceiver is IFlashReceiver {
    ToyToken public token;
    FlashPool public pool;

    constructor(ToyToken token_, FlashPool pool_) {
        token = token_;
        pool = pool_;
    }

    function onFlashLoan(uint256 amount, uint256 fee) external override {
        token.transfer(address(pool), amount + fee);
    }
}

contract BadReceiver is IFlashReceiver {
    function onFlashLoan(uint256, uint256) external override {
    }
}

A loan larger than the pool balance reverts

The pool holds 10000 tokens. A loan of 10001 is more than the pool holds, so it reverts before any tokens move.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(uint256 initialSupply) {
        balanceOf[msg.sender] = initialSupply;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "not enough");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "not enough");
        require(allowance[from][msg.sender] >= amount, "not allowed");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

interface IFlashReceiver {
    function onFlashLoan(uint256 amount, uint256 fee) external;
}

contract FlashPool {
    ToyToken public token;

    constructor(ToyToken token_) {
        token = token_;
    }

    function feeFor(uint256 amount) public pure returns (uint256) {
        return amount / 100;
    }

    function flashLoan(uint256 amount, IFlashReceiver receiver) public {
        uint256 before = token.balanceOf(address(this));
        require(amount <= before, "too much");
        uint256 fee = feeFor(amount);
        token.transfer(address(receiver), amount);
        receiver.onFlashLoan(amount, fee);
        require(token.balanceOf(address(this)) >= before + fee, "not repaid");
    }
}

contract GoodReceiver is IFlashReceiver {
    ToyToken public token;
    FlashPool public pool;

    constructor(ToyToken token_, FlashPool pool_) {
        token = token_;
        pool = pool_;
    }

    function onFlashLoan(uint256 amount, uint256 fee) external override {
        token.transfer(address(pool), amount + fee);
    }
}

contract BadReceiver is IFlashReceiver {
    function onFlashLoan(uint256, uint256) external override {
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(uint256 initialSupply) {
        balanceOf[msg.sender] = initialSupply;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "not enough");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "not enough");
        require(allowance[from][msg.sender] >= amount, "not allowed");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

interface IFlashReceiver {
    function onFlashLoan(uint256 amount, uint256 fee) external;
}

contract FlashPool {
    ToyToken public token;

    constructor(ToyToken token_) {
        token = token_;
    }

    function feeFor(uint256 amount) public pure returns (uint256) {
        return amount / 100;
    }

    function flashLoan(uint256 amount, IFlashReceiver receiver) public {
        uint256 before = token.balanceOf(address(this));
        require(amount <= before, "too much");
        uint256 fee = feeFor(amount);
        token.transfer(address(receiver), amount);
        receiver.onFlashLoan(amount, fee);
        require(token.balanceOf(address(this)) >= before + fee, "not repaid");
    }
}

contract GoodReceiver is IFlashReceiver {
    ToyToken public token;
    FlashPool public pool;

    constructor(ToyToken token_, FlashPool pool_) {
        token = token_;
        pool = pool_;
    }

    function onFlashLoan(uint256 amount, uint256 fee) external override {
        token.transfer(address(pool), amount + fee);
    }
}

contract BadReceiver is IFlashReceiver {
    function onFlashLoan(uint256, uint256) external override {
    }
}

Key points

  • Flash loan: A flash loan is a loan borrowed, used, and repaid inside one transaction.
  • Atomicity: Atomicity means the whole transaction succeeds together or reverts together.
  • Fee: The fee is the extra amount the borrower must repay on top of the borrowed amount.
  • Receiver: The receiver is the contract that gets the loan and must repay it in the same call.
  • Revert: A missing repayment makes the pool check fail and reverts the entire loan.
  • Restatement: Borrow, use, and repay all happen in one transaction, or nothing happens at all.

In this example, the pool holds 10000 tokens and a good receiver holds 50. Each loan of 1000 costs a fee of 10, so after two loans the pool holds 10020 and the receiver keeps 30. A bad loan attempted after the good loans still reverts and leaves the pool balance untouched.

Note

Real flash loans need a real pool with real liquidity, and this lesson shows only the pattern on a toy pool.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

A flash loan borrower does not repay inside the same transaction. What happens?