EIP-3156: Creating a standard for Flash Loans
A new standard for flash loans unifying the interface + wrappers for existing ecosystems
As we've discussed last week, flash loans are a commonly used pattern for hacks. But what exactly are they and how are they implemented in the contracts? As of right now each protocol has its own way of implementing flash loans.
With EIP-3156 we will get a standardized interface. The standard was recently finalized.
What are Flash Loans?
In a regular loan, you would apply for it first, then wait to be either approved or rejected. And then pay back the loan over a defined time frame with defined interest rates. A flash loan is special as none of this applies here. No one has to apply, meaning everyone will be approved.
The catch?
You have to payback the loan in the same transaction as when you received it. This makes perfect sense from the lender's point of view. If you pay back the loan immediately, there is no risk for the lender. So he can make this lending available to anyone who wants it.
You might wonder what good is a loan you have to pay back in the same transaction?
This certainly limits your options a lot, but thanks to the power of Ethereum we can still do a lot. Most commonly you can use it for arbitraging while massively reducing your own capital requirements. No need to have millions of dollars in several accounts to maximize profits. Instead take a flash loan, arbitrage as much as possible in one transaction, and then pay back the loan at the end of the transaction.
But also as mentioned it's often used for hacks. If you find a bug in a smart contract, flash loans allow you to leverage the bug. Instead of just getting a few thousands dollars, you can end up with millions of profit. And then paying back the loan at the end will be an easy task.
Update: Another good example use case brought up by Alberto himself was refinancing a debt. 'Imagine that you have locked ETH in Aave and borrowed Dai, and you are paying 10% interest on your debt. You have used the Dai to buy a house, and won't be repaying it for 10 years. Now, Compound offers to lend Dai, with ETH collateral, charging only 9% interest. With a flash loan of Dai you repay your debt in Aave, retrieve your ETH collateral, which you put in Compound. You borrow the Dai at 9% in Compound and repay the flash loan. Presto, your refinanced your 10% Aave loan into a 9% Compound loan.'
What is the EIP-3156 Flash Loan standard?
Many protocols have existing flash loan implementations, those include dYdX flash loans, Aave flash loans and Uniswap flash loans. Unfortunately the interface is different for all these. This is not only bad for the user of such flash loans, having to learn how to use a flash loan in each ecosystem newly. But it's also bad for security when everyone is trying to design a secure mechanism on their own.
That's why we have standards. EIP-3156 is designed to support various different underlying mechanisms for repaying the loan.
EIP-3156 specification
IERC3156FlashLender
The lender interface must be implemented by services wanting to provide a flash loan. The functions maxFlashLoan
and flashFee
are pretty self-explanatory.
With the flashLoan
function, you will be able to execute the flash loan. The receiver address must be a contract implementing the borrower interface. Any arbitrary data may be passed in addition to the call.
The only requirement for the implementation details of the function are that you have to call the onFlashLoan
callback from the receiver:
require(
receiver.onFlashLoan(msg.sender, token, amount, fee, data)
== keccak256("ERC3156FlashBorrower.onFlashLoan"),
"IERC3156: Callback failed"
);
After the callback, the flashLoan
function must take the amount + fee token from the receiver, or revert if this is not successful.
interface IERC3156FlashLender {
function maxFlashLoan(
address token
) external view returns (uint256);
function flashFee(
address token,
uint256 amount
) external view returns (uint256);
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool);
}
IERC3156FlashBorrower
The borrower interface consists only of the onFlashLoan
callback function.
For the transaction to not revert, inside the onFlashLoan
the contract must approve amount + fee of the token to be taken by msg.sender
.
interface IERC3156FlashBorrower {
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32);
}
Note that the token is agnostic to its underlying standard. In the following example we will use ERC-20, but other standards could be used as well.
EIP-3156 example implementation
In our example we will implement a flashLoan feature inside an ERC-20 contract. The flash loan capability will be minting new tokens as required and just burning them afterwards again.
Borrower Example
function flashBorrow(
address token,
uint256 amount,
bytes memory data
) public {
uint256 allowance = IERC20(token).allowance(
address(this),
address(lender)
);
uint256 fee = lender.flashFee(token, amount);
uint256 repayment = amount + fee;
IERC20(token).approve(
address(lender),
allowance + repayment
);
lender.flashLoan(this, token, amount, data);
}
We'll implement a flashBorrow
function. The lender
is a fixed IERC3156FlashLender contract defined on deployment.
We first approve the token to the lender for the total repayment amount. The repayment is calculated as the loan amount + fee. We can get the fee by calling flashFee
on the lender.
Lastly we can execute the flashLoan
function.
Now of course we will also need the onFlashLoan
callback function in our borrower. In our example we
- verify the sender is actually the lender
- verify the initiator for the flash loan was actually our contract
- return the pre-defined hash to verify a successful flash loan
We could further implement additional logic here based on the passed data field if required.
Now we can go into the lender implementation where we will execute the actual flash loan logic.
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override returns(bool) {
require(
msg.sender == address(lender),
"FlashBorrower: Untrusted lender"
);
require(
initiator == address(this),
"FlashBorrower: Untrusted loan initiator"
);
// optionally check data here if wanted
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
Lender Example
function maxFlashLoan(
address token
) external view override returns (uint256) {
return type(uint256).max - totalSupply();
}
Let's begin with the maxFlashLoan
function. Usually this would be something like return token.balanceOf(address(this))
.
But since we are minting any required tokens, we can allow flash loans up to the point where totalSupply
would be overflowing.
Next we will implement the flashFee
function to return 0.1% of the total loan amount.
This will be called by the borrower to repay the required amount as well as in the lender to check if the repayment was done for the full amount.
function flashFee(
address token,
uint256 amount
) public view override returns (uint256) {
require(
token == address(this),
"FlashMinter: Unsupported currency"
);
uint256 fee = 1000; // 0.1 %.
return amount * fee / 10000;
}
Now lastly we can implement the actual flashLoan
function. Inside we will
- mint the requested amount to the borrower
- execute the
onFlashLoan
callback and verify its return value - check if the repayment is approved
- reduce the allowance by the repayment amount and burn it
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external override returns (bool) {
require(token == address(this), "FlashMinter: Unsupported currency");
uint256 fee = flashFee(token, amount);
_mint(address(receiver), amount);
bytes32 CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
require(
receiver.onFlashLoan(msg.sender, token, amount, fee, data) == CALLBACK_SUCCESS,
"FlashMinter: Callback failed"
);
uint256 _allowance = allowance(address(receiver), address(this));
require(_allowance >= (amount + fee), "FlashMinter: Repay not approved");
_approve(address(receiver), address(this), _allowance - (amount + fee));
_burn(address(receiver), amount + fee);
return true;
}
You can check out the full example at https://github.com/albertocuestacanada/ERC3156 which further includes some examples for how to use the data field which I left out for simplicity reasons.
EIP-3156 wrappers for existing flash loans
Of which the dYdX wrapper is already deployed to the mainnet.
Have you used a flash loan before? Let me know in the comments.
Solidity Developer