Flash Loans and how to hack them: a walk through of ERC 3156
Updated: Nov 18
Flash loans are loans between smart contracts that must be repaid in the same transaction. This article describes the ERC 3156 flash loan specification as well as the ways flash lenders and borrowers can be hacked. Suggested security exercises are provided at the end.
Below is an extremely simple example of a flash loan.
If the borrower does not pay back the loan, the require statement with the message “flash not paid back” will cause the entire transaction to revert.
Only contracts can work with flashloans
An EOA wallet cannot call a function to get the flash loan and then transfer the tokens back in a single transaction. Integration with a flash loan requires a separate smart contract.
Flash loans do not need collateral
If a flash loan is implemented properly (big if!), then there is no risk of the loan not being paid back, because a revert or failed require statement will cause the transaction to fail, and the Ether will not transfer.
What are flashloans used for?
The most common use case for a flash loan is to do an arbitrage trade. For example, if Ether is trading for $1,200 in one pool and $1,300 in another DeFi application, it would be desirable to buy the Ether in the first pool and sell it in the second pool for a $100 profit. However, you need to money to buy the Ether in the first place. A flash loan is the ideal solution for it, as you don’t need $1,200 lying around. You can borrow $1,200 of Ether, sell it for $1,300, and pay back the $1,200 keeping a $100 profit for yourself (minus fees).
For regular DeFi loans, they typically require some kind of collateral. For example, if you were borrowing $10,000 in stable coins, you would need to deposit $15,000 of Ether as collateral.
If your stable coins loan had a 5% interest and you wanted to refinance with another lending smart contract at 4%, you would need to
pay back the $10,000 in stable coins
withdraw the $15,000 Ether collateral
deposit the $15,000 Ether collateral into the other protocol
borrow $10,000 in stable coins again at the lower rate
This would be problematic if you had the $10,000 tied up in some other application. With a flashloan, you can do steps 1-4 without using any of your own stable coins.
In the example above, the borrower was using $15,000 of Ether as collateral. But suppose the protocol is offering a lower collateralization ratio using wBTC (wrapped bitcoin)? The borrower could use a flash loan and a similar set of steps outline above to swap out the collateral instead of the principal.
In the context of DeFi loans, if the collateral falls below a certain threshold, then the collateral can get liquidated — forcibly sold to cover the cost of the loan. In the example above, if the value of the Ether was to drop to $12,000, then the protocol might allow someone to purchase the Ether for $11,500 if they first pay back the $10,000 loan.
A liquidator could use a flash loan to pay off the $10,000 stable coin loan and receive $11,500. They would then sell this on another exchange for stable coins, and then pay back the flash loan.
Increase yield for other DeFi applications
Uniswap and AAVE earn depositors’ money through trading fees or lending interest. But since they have such a large amount of capital in one place, they can make additional money by also offering flash loans. This increases the efficiency of capital since the same capital now has more uses.
Hacking Smart Contracts
Flash loans are probably most famous for their use by black hat hackers to exploit protocols. The primary attack vectors for flash loans are price manipulation and governance (vote) manipulation. Used on DeFi applications with inadequate defense, flash loans allow attackers to heavily buy up an asset increasing its price, or acquiring a bunch of voting tokens to push through a governance proposal.
The following is a list of flash loan hacks for the curious. Vulnerability is two-sided however. A flash lending and flash borrowing contract can also be vulnerable to losing money if not implemented properly.
Examples of Flash Loan Hacks
Flash loan attacks are one of the most common exploits, presumably because developers coming from a web2 background aren’t accustomed to it. Here are some of the more notorious examples.
Using flash loans to hack protocols is a separate topic, this article focuses on insecure implementations of flash lending and borrowing contracts.
ERC3156 seeks to standardize the interface for getting flash loans. Although the workflow is straightforward, the exact implementation details need to be tied down, for example, should we call the function “getFlashLoan,” “onFlashLoan,” or something else? And then what parameters should it accept?
ERC3156 Receiver Specification
The first aspect of the standard is the interface the borrower needs to implement, which is shown below. The borrower only needs to implement one function.
We describe the function arguments here
This is the address that initiated the flash loan. You probably want some kind of validation here so that untrusted addresses are not initiating flashloans on your contract. Usually, the address would be you, but you shouldn’t assume that!
The function onFlashLoan is expected to be called by the flash loan contract, not the initiator. You should check msg.sender is the flash loan contract inside the onFlashLoan() function because this function is external and anyone can call it.
Initiator is not msg.sender or the flash loan contract. It is the address that triggered the flash lending contract to call the receiver’s onFlashLoan function.
This is the address of the ERC20 token you are borrowing. Contracts offering flash loans will usually hold several tokens they can flash loan out. The ERC3156 flash loan standard does not support flash loaning native Ether, but this can be implemented by flash loaning WETH and having the borrower unwrap the WETH. Because the borrowing contract is not necessarily the contract that called the flash loaner, the borrowing contract may need to be told what token is being flash lent.
Fee is how much of the token needs to be paid as a fee for the loan. It is denominated in absolute amount, not percentages.
If your flash loan receiving contract isn’t hard coded to take a particular action when receiving a flash loan, you can parameterize its behavior with the data parameter. For example, if your contract is arbitraging trading pools, then you would specify which pools to trade with.
The contract must return keccak256("ERC3156FlashBorrower.onFlashLoan") for reasons we will discuss later.
Reference implementation of the borrower
This has been modified from the code in the ERC 3156 spec to make the snippet smaller. Note that this contract is still placing perfect trust into the flash lender. If the flash lender were somehow compromised, the contract below could be exploited through feeding it bogus amount and fee and initiator data. If the lender is immutable, this isn’t a concern, but it could be an attack vector if the lender is upgradeable.
ERC3156 Lender Specification
Below is the interface for the lender specified by ERC3156
The arguments in the interface above have the same meaning as described in the previous section, so it won’t be repeated here.
The flashLoan() function needs to accomplish a few important operations:
Someone might call flashLoan() with a token the flash loan contract does not support. This should be checked for.
Someone might call flashLoan() with an amount that is larger than maxFlashLoan. This also should be checked for
data is simply forwarded to the caller.
More importantly, flashLoan() must transfer the tokens to the receiver and transfer them back. It should not rely on the borrower transferring the tokens back for repayment. The rational for this will be discussed in the next section. We have copied the reference implementation which can be found in the EIP 3156 Spec, here to emphasize the important parts:
Note that the reference implementation is assuming that the ERC20 tokens return true on success, which not all do, so use the SafeTransfer library if using non-compliant ERC20 tokens.
Access control and input validation for borrower
The borrowing smart contract must have the controls in place to only allow the flash lender contract to be the caller of onFlashLoan(). Otherwise, some actor other than the flash lender can call onFlashLoan() and cause unexpected behavior.
Furthermore, anyone can call flashloan() with an arbitrary borrower as the target and pass arbitrary data. To ensure the data is not malicious, a flash loan receiver contract should only allow a restricted set of initiators.
Reentrance locks are very important
ERC 3156 by definition cannot follow the check effects pattern to prevent reentrancy. It has to notify the borrower it has received the tokens (make an external call), then transfer the tokens back. As such, nonReentrant locks should be added to the contract.
It is important that the lender is the one transferring the tokens back or that reentrancy locks are in place.
In the above implementations, the lender transfers the tokens back from the borrower. The borrower does not transfer the loans to the lender. This is important to avoid “side entrances” where the borrower deposits money into the protocol as a lender. Now the pool sees it’s balance has returned to what it was before, but the borrower suddenly has become a lender with a large deposit.
UniswapV2’s flash loan does not transfer the tokens back after the flash loan finishes. However, it uses a reentrancy lock to ensure that the borrower cannot “pay back the loan” by depositing it back into the protocol as if they were a lender.
For the borrower, ensure only flash lender contract can call onFlashLoan
The flash lender is hardcoded to only call the receiver’s onFlashLoan() function and nothing else. If a borrower had a way to specify which function the flash lender would call, then the flash loan could be manipulated into transferring other tokens in it’s possession (by calling ERC20.transfer) or granting approval to it’s token balance to a malicious address.
Because such actions require an explicit call to an ERC20 transfer or approve, this can’t happen if the flash lender can only call onFlashLoan().
This exploit happened in the real world, here Rekt News documents an MEV bot getting hacked.
Using token.balanceOf(address(this)) can be manipulated
In the implementation above, we do not use balanceOf(address(this)) except to determine the maximum flash loan size. This can be altered by someone else directly transferring tokens to the contract, interfering with the logic. The way we know the flash loan was paid back is because the lender transferred back the loan amount + fee. There are valid ways to use balanceOf(address(this)) to check repayment, but this must be combined with reentrancy checks to avoid paying back the loan as a deposit.
Why the flash borrower needs to return keccak256("ERC3156FlashBorrower.onFlashLoan");
This handles the situation where a contract (not the flash lender contract) with a fallback function has given approval to the flash lending contract. Someone could repeatedly initiate a flashloan with that contract as a recipient. Then the following would happen:
The victim contract gets a flashloan
The victim contract gets called with onFlashLoan() and the fallback function is triggered but does not revert. The fallback function responds to any function call that doesn't match the rest of the functions in the contract, so it will respond to a onFlashLoan() call.
The flash lender withdraws tokens from the borrower + fee
If this operation happens in a loop, the victim contract with the fallback will get drained. The same could happen with an EOA wallet, since calling a wallet address with onFlashLoan does not revert.
Checking that the onFlashLoan function does not revert isn’t good enough. The flash lender also checks that the value keccack256("ERC3156FlashBorrower.onFlashLoan") is returned so that it knows the borrower intended to borrow the tokens and also pay back the fee.
Practice Problems Related to Flash Loans
The following problems from DamnVulnerableDeFi and Mr Steal Yo Crypto can help you practice the attack vectors described above. One of the best way to understand flash loans is to learn what not to do when implementing them.
Naive Receiver (your goal is to drain the borrower, not necessarily steal their funds)
Brush up on your knowledge of ERC 4626 and then practice these problems
Unstoppable (this one is a bit harder, so do it last. Your goal is to brick the contract, not steal the funds).
Flash Loaner (from Mr Steal Yo Crypto. Make sure you understand ERC 4626)
All of these problems are related to hacking the lender or the borrower, not using a flash loan to hack something else.
Learn more with RareSkills
This material is part of our advanced Solidity Bootcamp. Please see the program to learn more.