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
- Do not trust callback arguments as given: The arguments of onFlashLoan cannot be trusted without verification. If the caller lies, initiator, token and amount can point to a transaction that never happened, fee can be miscalculated, and data can be tampered with. To trust initiator, token, amount and fee, check that the caller of onFlashLoan is in a list of verified LENDERs. When the party that called flashLoan also receives the callback, this check is simple. To trust data as well, additionally check that the initiator is an address you trust.
- Leftover approvals can let funds drain: The safest approach is to approve only amount + fee before running flashLoan. A RECEIVER that keeps a standing approval to a particular LENDER, or that approves inside onFlashLoan, must also have a mechanism to check that the initiator is trustworthy. Suppose a contract with a non-reverting fallback function, or an EOA, has approved a LENDER and has not used the approval yet. If the LENDER does not check the onFlashLoan return value, someone can call flashLoan with that address as the receiver and drain up to the approved amount or the balance. For this reason the LENDER must always verify the returned hash and revert if it differs.
- Interest rate attack using flash mint: Lending protocols that have no interest rate caps or floors and do not adjust fixed rates in response to liquidity changes are at risk. The original's example is a scenario in which an attacker flash mints 1 quintillion STAB, deposits it together with ETH collateral to push the rate down to 0.00001%, then borrows 1 million STAB at that rate and closes the flash mint, effectively obtaining an interest-free loan permanently. Rate caps and floors, and readjustment in response to short-term liquidity changes, are needed.
- Arithmetic overflow and underflow: If a flash mint provider does not cap the amount that can be minted in one transaction, anyone can mint 2^256-1. Protocols that receive flash minted tokens must be able to handle this amount, either by using a compiler with overflow protection built into the bytecode or by adding explicit checks.
- Combining flash mint with other features: Combining flash mint with business features on the same platform easily produces unexpected results. The original's example is a contract that flash lends its own token and, when a user burns that token, borrows from a third party. If someone flash mints a very large amount of FOO and converts it to BAR, the lender borrows up to its limit from the underwriter. The attacker can then raise the underwriter's interest rate, leave the lender undercollateralized, and liquidate it for profit.