ERC-3156 Flash loan standard (Flash Loans)

Final · Vaults & DeFi · created 2020-11-15

Defines in one place the functions and execution procedure of a flash loan, in which tokens are borrowed without collateral and repaid within the same transaction. It makes the lending contract and the borrowing contract fit together in the same way even when they are different implementations.

Why it is needed

A flash loan is a loan in which tokens are borrowed without collateral but must be repaid before the same transaction ends. Before this standard, the interface and usage differed from one protocol to another among those that built this feature first. In some, the lender takes the principal and the fee, while in others the borrower must send them back directly. Some let the borrower repay in a token different from the one borrowed, and some bundle trading, deposits, and flash loans into a single entry point while others keep them separate. Some mint their own token in any amount and lend it without a fee. The original text expected these differences to grow in the future, and developers who want to integrate multiple flash loans take on that much technical debt. This standard reduces that cost by fixing one set of functions and procedures that the lending side and the borrowing side must follow.

In depth

The two roles and the callback structure

This standard defines a flash loan as a process in which two contracts fit together through a callback. The lending side is called the LENDER and the borrowing side is called the RECEIVER. The LENDER MUST implement the IERC3156FlashLender interface, and the RECEIVER MUST implement the IERC3156FlashBorrower interface. After the LENDER's flashLoan sends the tokens, it calls the RECEIVER's onFlashLoan, and inside that function the RECEIVER does its work with the borrowed tokens and then prepares to repay. If the principal and the fee do not come back before the transaction ends, the entire transaction is reverted. The receiver is taken as an argument so that the party calling flashLoan and the receiver of the tokens can be separate. In line with the existing practice of lending multiple tokens from one contract, the token argument is also included in both functions.

Rules for maxFlashLoan and flashFee

maxFlashLoan(token) must return the maximum amount that can be borrowed in that token. If the token is not supported, it must return 0 rather than revert. flashFee(token, amount) must return the fee to be paid on top of the principal when borrowing amount. If the token is not supported, it must revert. The original explains why the two functions behave differently: returning a fee number for an unsupported token would itself be false information, so reverting is correct. The reference implementation sets the fee variable so that 1 equals 0.01% and computes the fee as amount * fee / 10000.

The order flashLoan must follow

flashLoan(receiver, token, amount, data) must satisfy all of the following. First, it sends amount of token to the receiver before the callback. Second, it calls the receiver's onFlashLoan with msg.sender as the initiator. Third, it passes the received token, amount and data through unchanged. Fourth, the fee argument must equal flashFee(token, amount). Fifth, it checks that the return value of onFlashLoan equals keccak256("ERC3156FlashBorrower.onFlashLoan") and reverts if it does not. Sixth, once the callback finishes, it pulls amount + fee of token from the receiver and reverts if that fails. If everything succeeds, it returns true.

What the RECEIVER does in onFlashLoan

The RECEIVER implements onFlashLoan(initiator, token, amount, fee, data). To keep the transaction from reverting, it must approve the LENDER (msg.sender) to take amount + fee of token before this function ends. On success it must return the hash keccak256("ERC3156FlashBorrower.onFlashLoan"). The reference implementation FlashBorrower first checks that msg.sender is a known lender and that the initiator is itself, then decodes data with abi.decode to branch on which action to perform. The flashBorrow function that starts a loan computes the fee in advance with flashFee, approves the existing allowance plus amount + fee, and then calls flashLoan.

Why the LENDER pulls the funds

Instead of the RECEIVER sending the principal and fee to the LENDER (push), the standard has the LENDER take them with transferFrom (pull). This way, if the LENDER has other features that also use transferFrom, it does not need to lock them during the flash loan. A push design is possible, but every other LENDER feature would then have to be rewritten around transfer. The original chose the pull structure because it is simpler and more widely used. In the same spirit, initiator, amount and fee are passed as separate arguments rather than inside data. If the caller put them in data, the RECEIVER would need a separate mechanism to verify that they are correct, so this is not recommended. The function names flashLoan and onFlashLoan were chosen because they are unlikely to collide with other functions and their meaning is clear.

Reference implementations: flash mint and flash loan

The original provides two LENDER reference implementations. FlashMinter extends ERC20 and lends its own token. maxFlashLoan returns type(uint256).max - totalSupply(). flashLoan calls _mint of amount to the receiver, invokes the callback, checks that the allowance is at least amount + fee, and then calls _burn for that amount. It reverts if token is not its own address. FlashLender lends ERC20 tokens from its holdings that appear in the supportedTokens list. It sends them with transfer and, after the callback, collects amount + fee with transferFrom. maxFlashLoan returns its own balance for a supported token and 0 otherwise. Both implementations compare the callback return value with the CALLBACK_SUCCESS constant and revert if they differ. The standard is finalized with Final status, and no backwards compatibility issues were identified.

Things to watch out for

Original ERC · ← ERCs