erc-3156 ready for review (#3181)

This ERC provides standard interfaces and processes for flash lenders and borrowers, allowing for flash loan integration without a need to consider each particular implementation.
This commit is contained in:
Alberto Cuesta Cañada 2021-01-15 03:50:45 +00:00 committed by GitHub
parent c1eee1623f
commit c949c89665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 217 additions and 118 deletions

View File

@ -111,7 +111,7 @@ You can see above how internally the user-selected account is transformed into a
## Rationale
While the current model of getting user consent on a per-action basis has high security, there are huge usability gains to be had by getting more general user consent which can cover broad categories of usage, which can be expressed in a more human-readable way. This pattern has a variety of benefits to offer different functions within a web3 wallet.
While the current model of getting user consent on a per-action basis has high security, there are huge usability gains to be had bo getting more general user consent which can cover broad categories of usage, which can be expressed in a more human-readable way. This pattern has a variety of benefits to offer different functions within a web3 wallet.
The `eth_sendTransaction` method itself could be a restricted method (requested by default with the `provider.enable()` method), and the user could at sign-in time decide whether they wanted to require confirmations, approve all transactions, or only approve transactions to a certain contract, or up to a certain token limit, for example. By restricting this method by default, wallets could prevent sites from spamming the user with popups.

View File

@ -1,9 +1,9 @@
---
eip: 3156
title: Flash Loans
author: Alberto Cuesta Cañada (@albertocuestacanada), Fiona Kobayashi (@fifikobayashi), fubuloubu (@fubuloubu)
author: Alberto Cuesta Cañada (@albertocuestacanada), Fiona Kobayashi (@fifikobayashi), fubuloubu (@fubuloubu), Austin Williams (@onewayfunction)
discussions-to: https://ethereum-magicians.org/t/erc-3156-flash-loans-review-discussion/5077
status: Draft
status: Review
type: Standards Track
category: ERC
created: 2020-11-15
@ -31,75 +31,146 @@ A flash lending feature integrates two smart contracts using a callback pattern.
### Lender Specification
A `lender` smart contract implementing a flash lending feature MUST implement a `flashSupply` function:
A `lender` MUST implement the IERC3156FlashLender interface.
```
function flashSupply(address token) external external returns (uint256);
interface IERC3156FlashLender {
function maxFlashAmount(
IERC20 token
) external view returns (uint256);
function flashFee(
IERC20 token,
uint256 amount
) external view returns (uint256);
function flashLoan(
IERC3156FlashBorrower receiver,
IERC20 token,
uint256 amount,
bytes calldata data
) external;
}
```
The `flashSupply` function MUST return the maximum loan possible for `token`. If a `token` is not currently supported `flashSupply` MUST return 0, instead of reverting.
A `lender` smart contract implementing a flash lending feature MUST implement a `flashFee` function:
```
function flashFee(address token, uint256 amount) external external returns (uint256);
```
The `maxFlashAmount` function MUST return the maximum loan possible for `token`. If a `token` is not currently supported `maxFlashAmount` MUST return 0, instead of reverting.
The `flashFee` function MUST return the fee charged for a loan of `amount` `token`. If the loan cannot be executed `flashFee` MUST revert.
A `lender` smart contract implementing a flash lending feature MUST implement a `flashLoan` function:
The `flashLoan` function MUST include a callback to the `onFlashLoan` function in a `IERC3156FlashBorrower` contract.
```
function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external {
function flashLoan(
IERC3156FlashBorrower receiver,
IERC20 token,
uint256 amount,
bytes calldata data
) external {
...
FlashBorrowerLike(receiver).onFlashLoan(msg.sender, token, amount, fee, data);
receiver.onFlashLoan(msg.sender, token, amount, fee, data);
...
}
```
The `flashLoan` function MUST execute the equivalent of an `ERC20.transfer` operation before calling `FlashBorrowerLike(receiver).onFlashLoan(...)`.
The `flashLoan` function MUST transfer `amount` of `token` to `receiver` before the callback to the borrower.
The lender contract MAY `mint` the tokens lended, instead of executing a `transfer` of tokens it holds.
The `flashLoan` function MUST include `msg.sender` as the `sender` to `onFlashLoan`.
The `flashLoan` function MUST verify that the tokens lended were returned, and MUST NOT take them from the `receiver`.
The `flashLoan` function MUST NOT modify the `token`, `amount` and `data` parameter received, and MUST pass them on to `onFlashLoan`.
The `receiver` MUST take an action to return `amount + fee` tokens and allow the transaction to resolve.
After the callback, the `flashLoan` function MUST take the `amount + fee` `token` from the `receiver`, or revert if this is not successful.
If the `flashLoan` used tokens generated by a `mint`, they SHOULD be the target of a `burn` before the end of the transaction.
The *batch flash loans* extension is OPTIONAL for ERC-3156 smart contracts. This allows flash loans to be composed of several ERC20 tokens.
A `lender` offering batch flash loans MUST implement the IERC3156BatchFlashLender interface.
```
interface IERC3156BatchFlashLender is IERC3156FlashLender {
function batchFlashLoan(
IERC3156BatchFlashBorrower receiver,
IERC20[] tokens,
uint256[] amounts,
bytes calldata data
) external;
}
```
The `batchFlashLoan` function MUST revert if the length of the `amounts` and `tokens` arrays differ.
The `batchFlashLoan` function MUST include a callback to the `onBatchFlashLoan` function in a `IERC3156BatchFlashBorrower` contract.
```
function batchFlashLoan(
IERC3156BatchFlashBorrower receiver,
IERC20[] calldata token,
uint256[] calldata amount,
bytes calldata data
) external {
...
receiver.onBatchFlashLoan(msg.sender, tokens, amounts, fees, data);
...
}
```
For each `token` in `tokens`, the `batchFlashLoan` function MUST transfer `amounts[i]` of `tokens[i]` to `receiver` before the callback to the borrower.
The `batchFlashLoan` function MUST include `msg.sender` as the `sender` to `onBatchFlashLoan`.
The `batchFlashLoan` function MUST include a `fees` argument to `onBatchFlashLoan` with the fee to pay for each individual `token` and `amount` lent.
The `batchFlashLoan` function MUST NOT modify the `tokens`, `amounts` and `data` parameters received, and MUST pass them on to `onBatchFlashLoan`.
After the callback, for each `token` in `tokens`, the `batchFlashLoan` function MUST take the `amounts[i] + fees[i]` of `tokens[i]` from the `receiver`, or revert if this is not successful.
If a fee is charged, the contract implementing `flashLoan` MAY use it in any desired way (e.g. the fee can be burned or transferred to any other party).
### Receiver Specification
A `receiver` of flash mints MUST implement an `onFlashLoan` callback:
A `receiver` of flash loans MUST implement the IERC3156FlashBorrower interface with an `onFlashLoan` callback:
```
interface FlashBorrowerLike {
function onFlashLoan(address sender, address token, uint256 amount, uint256 fee, bytes calldata) external;
interface IERC3156FlashBorrower {
function onFlashLoan(
IERC3156FlashLender sender,
IERC20 token,
uint256 amount,
uint256 fee,
bytes calldata data
) external;
}
```
On the callback execution the `receiver` MUST have received `amount` tokens of the `token` ERC20 contract from the caller. The `receiver` can trust that `sender` is the account that initiated the flash loan in the caller. For the transaction to not revert, `receiver` MUST send `amount + fee` of `token` to the caller. Before that, the `receiver` can implement any logic it desires.
For the transaction to not revert, `receiver` MUST approve `amount + fee` of `token` to be taken by `msg.sender` before the end of `onFlashLoan`.
The *batch flash loans* extension is OPTIONAL for ERC-3156 smart contracts. This allows flash loans to be composed of several ERC20 tokens.
```
interface IERC3156BatchFlashBorrower {
function onBatchFlashLoan(
IERC3156BatchFlashLender sender,
IERC20[] calldata tokens,
uint256[] calldata amounts,
uint256[] calldata fees,
bytes calldata data
) external;
}
```
For the transaction to not revert, for each `token` in `tokens`, `receiver` MUST approve `amounts[i] + fees[i]` of `tokens[i]` to be taken by `msg.sender` before the end of `onFlashLoan`.
## Rationale
The interfaces described in this ERC have been chosen as to cover the known flash lending use cases, while allowing for safe and gas efficient implementations.
`flashSupply(address token)`
`flashSupply` returns zero on unsupported contracts to allow for borrowers to discover the lending services offered by an ERC-3156 compliant lender.
`flashFee(address token, uint256 amount)`
`flashFee` reverts on unsupported contracts, because returning a numerical value would be incorrect.
`flashFee` reverts on unsupported tokens, because returning a numerical value would be incorrect.
`flashLoan(address receiver, uint256 amount, bytes calldata data)`
`flashLoan(address receiver, address token, uint256 amount, bytes calldata data)`
`flashLoan` has been chosen as descriptive enough, unlikely to clash with other functions in the lender, and including both the use cases in which the tokens lended are held or minted by the lender.
`receiver` is taken as a parameter to allow `flashLoan` to be called by EOAs, as opposed to the pattern in which `onFlashLoan` is called on `msg.sender`. This allows the lender to inform the `receiver` which address called `flashLoan`. This particular setup allows the `receiver` to implement an account based platform.
`receiver` is taken as a parameter to allow flexibility on the implementation of separate loan initiators and receivers.
Existing flash lenders (Aave, dYdX and Uniswap) all provide flash loans of several token types from the same contract (LendingPool, SoloMargin and UniswapV2Pair). Providing a `token` parameter in both the `flashLoan` and `onFlashLoan` functions matches closely the observed functionality.
A `bytes calldata data` parameter is included for the caller to pass arbitrary information to the `receiver`, without impacting the utility of the `flashMint` standard.
A `bytes calldata data` parameter is included for the caller to pass arbitrary information to the `receiver`, without impacting the utility of the `flashLoan` standard.
`onFlashLoan(msg.sender, amount, fee, data)`
@ -109,68 +180,61 @@ A `sender` will often be required in the `onFlashLoan` function, which the lende
The `amount` will be required in the `onFlashLoan` function, which the lender took as a parameter. An alternative implementation which would embed the `amount` in the `data` parameter by the caller would require an additional mechanism for the receiver to verify its accuracy, and is not advisable.
A `fee` will often be calculated in the `flashMint` function, which the `receiver` must be aware of for repayment. Passing the `fee` as a parameter instead of appended to `data` is simple and effective.
A `fee` will often be calculated in the `flashLoan` function, which the `receiver` must be aware of for repayment. Passing the `fee` as a parameter instead of appended to `data` is simple and effective.
The `amount + fee` are pulled from the `receiver` to allow the `lender` to implement other functionality that depend on `token` balances, without having to lock it for the duration of a flash loan.
## Backwards Compatibility
No backwards compatibility issues identified.
## Test Cases
The test cases of the reference implementation are available from the [ERC20Flash repository](https://github.com/albertocuestacanada/ERC20Flash/tree/main/test).
## Implementation
The reference implementations included inline can also be found at the [ERC20Flash repository](https://github.com/albertocuestacanada/ERC20Flash).
Of note are the ERC-3156 wrappers for existing flash lenders, also be found at the [ERC20Flash repository](https://github.com/albertocuestacanada/ERC20Flash).
Other implementations include [WETH10](https://github.com/WETH10/WETH10) and [MakerDAO MIP-25](https://github.com/hexonaut/dss-flash/pull/18).
### Flash Borrower Reference Implementation
```
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.7.5;
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "../interfaces/IERC20.sol";
contract FlashBorrower {
function onFlashLoan(address sender, address token, uint256 value, uint256 fee, bytes calldata) external {
// do something with the tokens received
IERC20(token).transfer(msg.sender, value.add(fee));
}
function flashBorrow(IERC3156FlashLender lender, IERC20 token, uint256 amount) public {
uint256 _allowance = token.allowance(address(this), lender);
uint256 _fee = lender.flashFee(token, amount);
uint256 _repayment = amount + _fee;
token.approve(lender, _allowance + _repayment);
lender.flashLoan(this, token, amount, data);
}
}
```
### Flash Mint Reference Implementation
```
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.7.5;
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import { IERC3156FlashBorrower, IERC3156FlashLender } from "../interfaces/IERC3156.sol";
import "./ERC20.sol";
import "../interfaces/IERC3156FlashBorrower.sol";
import "../interfaces/IERC3156FlashLender.sol";
/**
* @author Alberto Cuesta Cañada
* @dev Extension of {ERC20} that allows flash minting.
*/
contract ERC20FlashMinter is ERC20, IERC3156FlashLender {
using SafeMath for uint256;
contract FlashMinter is ERC20, IERC3156FlashLender {
uint256 public fee;
uint256 public fee; // Percentage charged on the amount, in bps
/**
* @param fee_ The divisor that will be applied to the `amount` of a `loan`, with the result charged as a `fee`.
*/
constructor (string memory name, string memory symbol, uint256 fee_) ERC20(name, symbol) {
constructor (
string memory name,
string memory symbol,
uint256 fee_
) ERC20(name, symbol) {
fee = fee_;
}
@ -179,7 +243,9 @@ contract ERC20FlashMinter is ERC20, IERC3156FlashLender {
* @param token The loan currency.
* @return The amount of `token` that can be borrowed.
*/
function flashSupply(address token) external view override returns (uint256) {
function maxFlashAmount(
IERC20
) external view override returns (uint256) {
return type(uint256).max;
}
@ -189,24 +255,38 @@ contract ERC20FlashMinter is ERC20, IERC3156FlashLender {
* @param amount The amount of tokens lent.
* @return The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function flashFee(address token, uint256 amount) external view override returns (uint256) {
require(token == address(this), "FlashMinter: unsupported loan currency");
function flashFee(
IERC20 token,
uint256 amount
) external view override returns (uint256) {
require(
token == this,
"FlashMinter: unsupported loan currency"
);
return _flashFee(token, amount);
}
/**
* @dev Loan `amount` tokens to `receiver`, which needs to return them plus a 0.1% fee to this contract within the same transaction.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address sender, uint256 amount, uint256 fee, bytes calldata)` interface.
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the ERC3156 callback.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata data)` interface.
* @param token The loan currency. Must match the address of this contract.
* @param amount The amount of tokens lent.
* @param data A data parameter to be passed on to the `receiver` for any custom use.
*/
function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external override {
require(token == address(this), "FlashMinter: unsupported loan currency");
uint256 _fee = _flashFee(token, amount);
_mint(receiver, amount);
IERC3156FlashBorrower(receiver).onFlashLoan(msg.sender, token, amount, _fee, data);
_burn(address(this), amount.add(_fee));
function flashLoan(
IERC3156FlashBorrower receiver,
IERC20 token,
uint256 amount,
bytes calldata data
) external override {
require(token == this, "FlashMinter: unsupported loan currency");
uint256 fee = _flashFee(token, amount);
_mint(address(receiver), amount);
receiver.onFlashLoan(msg.sender, token, amount, fee, data);
uint256 _allowance = allowance(address(receiver), address(this));
require(_allowance >= (amount + fee), "FlashMinter: Flash loan repayment not approved");
_approve(address(receiver), address(this), _allowance - (amount + fee));
_burn(address(receiver), amount + fee);
}
/**
@ -215,39 +295,42 @@ contract ERC20FlashMinter is ERC20, IERC3156FlashLender {
* @param amount The amount of tokens lent.
* @return The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function _flashFee(address token, uint256 amount) internal view returns (uint256) {
return fee == type(uint256).max ? 0 : amount.div(fee);
function _flashFee(
IERC20,
uint256 amount
) internal view returns (uint256) {
return fee == amount * fee / 10000;
}
}
```
### Flash Loan Reference Implementation
```
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.7.5;
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import { IERC3156FlashBorrower, IERC3156FlashLender } from "../interfaces/IERC3156.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/IERC3156FlashBorrower.sol";
import "../interfaces/IERC3156FlashLender.sol";
/**
* @author Alberto Cuesta Cañada
* @dev Extension of {ERC20} that allows flash lending.
* @dev Contract that allows flash lending of any ERC20 tokens it owns.
*/
contract FlashLender is IERC3156FlashLender {
using SafeMath for uint256;
mapping(address => bool) public supportedTokens;
uint256 public fee;
mapping(IERC20 => bool) public supportedTokens;
uint256 public fee; // Percentage charged on the amount, in bps
/**
* @param supportedTokens_ Token contracts supported for flash lending.
* @param fee_ The divisor that will be applied to the `amount` of a `loan`, with the result charged as a `fee`.
*/
constructor(address[] memory supportedTokens_, uint256 fee_) {
constructor(
IERC20[] memory supportedTokens_,
uint256 fee_)
{
for (uint256 i = 0; i < supportedTokens_.length; i++) {
supportedTokens[supportedTokens_[i]] = true;
}
@ -255,12 +338,32 @@ contract FlashLender is IERC3156FlashLender {
}
/**
* @dev The amount of currency available to be lended.
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
* @param receiver The contract receiving the tokens, needs to implement the IERC3156FlashBorrower interface.
* @param token The loan currency.
* @return The amount of `token` that can be borrowed.
* @param amount The amount of tokens lent.
* @param data A data parameter to be passed on to the `receiver` for any custom use.
*/
function flashSupply(address token) external view override returns (uint256) {
return supportedTokens[token] ? IERC20(token).balanceOf(address(this)) : 0;
function flashLoan(
IERC3156FlashBorrower receiver,
IERC20 token,
uint256 amount,
bytes calldata data
) external override {
require(
supportedTokens[token],
"FlashLender: Unsupported currency"
);
uint256 fee = _flashFee(token, amount);
require(
token.transfer(address(receiver), amount),
"FlashLender: Transfer failed"
);
receiver.onFlashLoan(msg.sender, token, amount, fee, data);
require(
token.transferFrom(address(receiver), address(this), amount + fee),
"FlashLender: Repay failed"
);
}
/**
@ -269,59 +372,55 @@ contract FlashLender is IERC3156FlashLender {
* @param amount The amount of tokens lent.
* @return The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function flashFee(address token, uint256 amount) external view override returns (uint256) {
function flashFee(
IERC20 token,
uint256 amount
) external view override returns (uint256) {
require(supportedTokens[token], "FlashLender: Unsupported currency");
return _flashFee(token, amount);
}
/**
* @dev Loan `amount` tokens to `receiver`, which needs to return them plus a 0.1% fee to this contract within the same transaction.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address sender, uint256 amount, uint256 fee, bytes calldata)` interface.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @param data A data parameter to be passed on to the `receiver` for any custom use.
*/
function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external override {
require(supportedTokens[token], "FlashLender: Unsupported currency");
IERC20 currency = IERC20(token);
uint256 _fee = _flashFee(token, amount);
uint256 balanceTarget = currency.balanceOf(address(this)).add(_fee);
currency.transfer(receiver, amount);
IERC3156FlashBorrower(receiver).onFlashLoan(msg.sender, token, amount, _fee, data);
require(currency.balanceOf(address(this)) >= balanceTarget, "FlashLender: unpaid loan");
}
/**
* @dev The fee to be charged for a given loan. Internal function with no checks.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @return The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function _flashFee(address token, uint256 amount) internal view returns (uint256) {
return fee == type(uint256).max ? 0 : amount.div(fee);
function _flashFee(
IERC20 token,
uint256 amount
) internal view returns (uint256) {
return amount * fee / 10000;
}
/**
* @dev The amount of currency available to be lended.
* @param token The loan currency.
* @return The amount of `token` that can be borrowed.
*/
function maxFlashAmount(
IERC20 token
) external view override returns (uint256) {
return supportedTokens[address(token)] ? token.balanceOf(address(this)) : 0;
}
}
```
## Security Considerations
### Verification of callback arguments
The arguments on the `onFlashLoan` callback can be divided in two groups, that require different checks before they can be trusted to be genuine.
The arguments of `onFlashLoan` are expected to reflect the conditions of the flash loan, but cannot be trusted unconditionally. They can be divided in two groups, that require different checks before they can be trusted to be genuine.
0. No arguments can be assumed to be genuine without some kind of verification. `sender`, `token` and `value` refer to a past transaction that might not have happened if the caller of `onFlashLoan` decides to lie. `fee` might be false or calculated incorrectly. `calldata` is not expected to have been verified or manipulated by the caller.
0. No arguments can be assumed to be genuine without some kind of verification. `sender`, `token` and `value` refer to a past transaction that might not have happened if the caller of `onFlashLoan` decides to lie. `fee` might be false or calculated incorrectly. `data` might have been manipulated by the caller.
1. To trust that the value of `sender`, `token`, `value` and `fee` are genuine a reasonable pattern is to verify that the `onFlashLoan` caller is in a whitelist of verified flash lenders. Since often the caller of `flashLoan` will also be receiving the `onFlashLoan` callback this will be trivial. In all other cases flash lenders will need to be approved if the arguments in `onFlashLoan` are to be trusted.
2. To trust that the value of `data` is genuine, in addition to the check in point 1, it is recommended to implement the `flashLoan` caller to be also the `onFlashLoan` receiver. With this pattern, checking in `onFlashLoan` that `sender` is the current contract is enough to trust that the contents of `data` are genuine.
### Flash lending security considerations
#### Example - Transfer from receiver
An implementation that allows flash lending to an arbitrary target, and that also takes the flash loaned amount from such target at the end of the transaction can be used to drain assets of a smart contract that trades a pair of assets based on internal balances.
1. The attacker triggers a flash loan of 1 million DAI to an AMM trading DAI/ETH.
2. The attacker sells 1000 ETH to the AMM trading pair, obtaining a larger amount of DAI than the pre-transaction price would have returned.
3. The flash lender burns the 1 million DAI (plus possibly a fee) from the receiver (AMM trading pair), which bears the loss of having sold DAI to the attacker at an artificially depressed price.
The key takeaway being that smart contracts trading on balances should not give blanket transfer approvals to smart contracts with flash loan features, unless they can be certain of their implementation.
#### Automatic approvals for untrusted borrowers
Including in `onFlashLoan` the approval for the `lender` to take the `amount + fee` needs to be combined with a mechanism to verify that the borrower is trusted, such as those described above. An even safer approach is to implement the approval before the `flashLoan` is executed.
### Flash minting external security considerations