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?