How To Perform Custom Ethereum Flash Loans Using Solidity (ERC 3156 Standard)

dan

What will I learn?

  • Connecting and using an Ethereum testnet
  • What is a flash loan (ERC 3156)
  • Flash loan interfaces
  • Flash loan wrappers
  • Using a flash loan borrower contract

Requirements

  • Any chrome-based browser that allows Chrome extensions
  • Metamask browser extension
  • Internet connection (for using testnets)

Difficulty

  • Intermediary

Introduction

The DeFi ecosystem on Ethereum is ever-growing, it is however limiting to be able to only interact with each contract individually. Luckily there are solutions, and if you know the code you can customize it accordingly to your needs and do more advanced single-transaction operations. As long as you understand the standards from this post, you can build yourself multi-protocol smart contracts.

Flashloans are a powerful tool, which can be used to take advantage of different fees and interests from different protocols. In practice, you can use it immediately for two clear and good reasons:

  • If you find a lending platform with interest rates that are higher than some other, you could use the knowledge of these tools to borrow from the cheaper ones, and then lend what you have borrowed to the more expensive ones. Being an arbitrator and profiting from the interest differences between contracts.

  • Leverage your position in a single transaction, by borrowing a token from a contract and selling it at market price for, then supplying the proceed back to the contract to increase your collateral and allow you to borrow more than you would be able to, if you did only one deposit in a single regular transaction.

  • Avoiding Ethereum fees, because in this standard not only you are able to do many operations faster, but avoid you having to make multiple transactions, which can be prohibitively expensive.

This tool can be used either for good or evil. Some developers do use the knowledge to implement flash loans solutions to drain the liquidity of a contract. However, the complexity of doing so is very high, and the developer also needs a very big startup capital, the more capital you start with the less risky it is, because even when using leveraged positions, you still have the risk of being liquidated.

Relevant resources

Getting started

To get started, let us use the [Kovan Ethereum testnet][1]. Any testnet would do the job, even a local one, however, the advantage of using an already deployed testnet is that some protocols may already be deployed, while you would have to deploy everything by hand if you were to test locally. Studying on the main Ethereum blockchain is prohibitively expensive, so that is already out of question, the Kovan testnet has test versions of multiple well-known Ethereum smart contracts and is already baked in Metamask.

So to proceed, you just need to have Metamask installed, and to switch to the Kovan network:

Now, intuitively, to interact with the blockchain we are going to need gas, which is paid by the native testnet version of Ethereum. For Kovan you have a few examples, as Kovan is controlled by a few peers, some of them are allowed to distribute free tokens so that you can test code without spending real money:

  • https://faucet.metamask.io/
  • https://gitter.im/kovan-testnet/faucet
  • https://enjin.io/software/kovan-faucet

Once you have claimed the tokens to use as gas to deploy, you are ready to start testing.

What is ERC 3156

You can follow the discussions about ERC 3156 on [their EIP page][2], by doing this you keep yourself up to date with how the standard evolves, and get to know about possible vulnerabilities from the smart contract that may show up in the future, which is something that we, as Solidity developers, must keep in the back of our heads at all times.

ERC 3156 is motivated by giving flexibility and liquidity to its users. A flash loan is when you make multiple operations within a single transaction. Those operations can be, for example: – Taking a loan, in a protocol, using the loan taken to sell it for something else, and lend what you have taken – Transfer of debt within multiple contracts, by allowing you to take a loan on a contract “x” and sending it as collateral for another contract “y”. – Attacks, because whenever there is code it can be exploited. There have been cases of flash loans being used to exploit and destroy the liquidity of small protocols.

Lender Interface

An interface is the building ground, the interfaces allow for other contracts to understand how yours work, without having to know the code. Interfaces do not, by themselves, execute code nor functions, they just describe how a contract/protocol should be implemented, and how others can use it, by adopting patterns on what are the functions and parameters it receives.

Flashloans are a tool that allows you to leverage your position, and increase your exposure, at the risk of being liquidated. At the core of the ERC 3156 contract, are two main interfaces, called “lender” and “receiver”. Taking it directly from the specification, this is how the lender interface looks like.

Lender Interface Code

pragma solidity ^0.7.0 || ^0.8.0; 
import "./IERC3156FlashBorrower.sol";
//the import above means there is another contract, that we will discusse later in this very post

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);

}

Lender Interface Code Explained

As we can see, the lender interface by itself is composed of three core functions, to which we will get to soon, but first, let briefly understand them:

maxFlashLoan is the maximum amount of tokens that someone can take on a flashloan. Also, it is used to tell when a token is not support (or does not have liquidity) by returning a zero. Ideally, it reflects, at most, the maximum safe amount that can be loaned, but in some cases, it can be changed for more or less safe, in terms of having enough collateral to not be immediately liquidated.

flashFee is optional and returns to the caller how much of a fee will be charged for the transaction, it is supposed to be paid in the token in which the loan was taken. If an operation is not allowed, either by having a huge fee or for trying to loan an unsupported token, the function must [revert][3].

flashLoan is going to be the function that is called to, in fact, lend to the contract, and can be used in flashloans to leverage by, for example, using it to add more collateral, either to increase or decrease your leverage.

A requirement of this standard is that you pass a callback function whenever you call the flashloan.

Implementation varies, but in the end, the goal is to guarantee the loan to be executed as expected.

require(
      receiver.onFlashLoan(msg.sender, token, amount, fee, data) == keccak256("ERC3156FlashBorrower.onFlashLoan"),
      "IERC3156: Callback failed"
  );

Borrower interface

On the other side, to do a flashloan, it is required for you to not just lend (add collateral), but to also take a loan at the same time, which is the whole point of the standard.

The interface for the borrower is:

pragma solidity ^0.7.0 || ^0.8.0;`

interface IERC3156FlashBorrower {

    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32);
}

There are a few requirements at stake for the flashloan to be successful: – You must approve the flashloan smart contract to spend your ERC20 balance. It is very common on DeFi for contracts to do that because this allows the Solidity code to programmatically send and receive tokens in your name. Since the flashloan standard is a smart contract, you need to, on the ERC20 tokens you want to use, approve manually for your flashloan contract to use your balance for you. – You must, logically, have enough balance for the contract to move in your name.

Approving a contract to spend from an ERC20

Whenever you want a contract to be able to make transactions using a third party token, such as a flash loan smart contract doing flash loans using an ERC20 token you own, you must, on the ERC20 contract approve. Let us first take a look at how that process would work for a contract that was already deployed on the testnet, in this case, Uniswap and USDC

This can be seen on Defi, when, for instance, using Uniswap. If you did not yet approve the Solidity code to use the funds from your balance, it will require you to do so:

But that will only allow you for Uniswap to spend your ERC20 tokens. If you want to manually do it, you can head into the token smart contract page on Etherscan, and manually approve from there, so you know exactly the address of the contract that you are approving and how much.

In this example, on Kovan testnet, I am using USDC, and I want to approve the protocol Compound to mint cUSDC from my USDC Kovan testnet balance, so I head into the original token contract, where I do have a balance, and check the [USDC contract code on Etherscan][4] itself.

From there I can find that I can write directly to the country, by using metamask.

Over there we can see the “approve” function, default to all ERC20 tokens, and that it takes 2 parameters as input, “_spender” and “amount”.

_spender is the contract that we do want to allow to use our balance for that token. and amount is how much we want to allow them to spend in total. Each time the contract uses your balance, this number decreases.

There is a technique that allows you to “approve unlimitedly” but it is not really unlimited, it is just a very big number, the biggest number the Ethereum Virtual Machine (EVM) allows! In decimals, it is exactly: 115792089237316195423570985008687907853269984665640564039457584007913129639935

So save that, and whenever you want to approve a contract to use a specific ERC20 token indefinitely, you set the allowance to that huge number!

Complete ERC 3156 code

Now, with the interfaces ready, we can implement our own code. ERC standards define what are the functions and parameters so that each contract understands how to talk to each other, but the final and definitive code can be changed by the programmer, especially if/when new vulnerabilities or improvements are discovered.

Here, we will implement the code for the Lender and Borrow interface we discussed earlier, and then implement the reference flash loan code, all in one go to make it easier to understand, however, if you are using Remix, you can, and probably should, separate them in different files, here, only the Lender and Borrower interfaces are separated on the same directory.

pragma solidity ^0.8.0;

//interfaces discussed above
import "./IERC3156FlashBorrower.sol";
import "./IERC3156FlashLender.sol";

//interface for our contract to know how does an ERC20 looks like
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

//The borrower implementation
contract FlashBorrower is IERC3156FlashBorrower {
    enum Action {NORMAL, OTHER}
    IERC3156FlashLender lender;
    constructor (IERC3156FlashLender lender_) {
        lender = lender_;
    }

    /// @dev ERC-3156 Flash loan callback
    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");
        (Action action) = abi.decode(data, (Action));
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }

    /// @dev Initiate a flash loan
    function flashBorrow(address token, uint256 amount) public {
        bytes memory data = abi.encode(Action.NORMAL);
        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);
    }
}

//The Lender implementation
contract FlashLender is IERC3156FlashLender {
    bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
    mapping(address => bool) public supportedTokens;
    uint256 public fee; //  1 == 0.0001 %.

    constructor(address[] memory supportedTokens_, uint256 fee_) {
        for (uint256 i = 0; i < supportedTokens_.length; i++) {
            supportedTokens[supportedTokens_[i]] = true;
        }
        fee = fee_;
    }

    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external override returns(bool) {
        require(supportedTokens[token], "FlashLender: Unsupported currency");
        uint256 fee = _flashFee(token, amount);
        require(IERC20(token).transfer(address(receiver), amount),"FlashLender: Transfer failed");
        require(receiver.onFlashLoan(msg.sender, token, amount, fee, data) == CALLBACK_SUCCESS,"FlashLender: Callback failed");
        require(IERC20(token).transferFrom(address(receiver), address(this), amount + fee),"FlashLender: Repay failed");
        return true;
    }

    function flashFee(address token, uint256 amount) external view override returns (uint256) {
        require(supportedTokens[token],"FlashLender: Unsupported currency");
        return _flashFee(token, amount);
    }

    function _flashFee(address token,uint256 amount) internal view returns (uint256) {
        return amount * fee / 10000;
    }

    function maxFlashLoan(address token) external view override returns (uint256) {
        return supportedTokens[token] ? IERC20(token).balanceOf(address(this)) : 0;
    }
}

Using a flashloan smart contract

You have to use the flashloan on already deployed smart contracts, preferably deployments with good liquidity and testnet versions, to avoid liquidation and high fees from Ethereum while still in development.

Such examples of well known smart contracts that adopt the ERC3156 standard are: – DyDx ExchangeUniswapAaveYield

So, by using Kovan testnet, on a deployed smart contract (you can deploy your own), where we will take our loan, that is, we will call the flashBorrow function.

This specific contract allows you to choose a lender contract (that we have implemented), we could choose ours or an already deployed one. Our borrower contract always uses the same Lender contract, which is a little bit less flexible. Here, we can just “plug and play”.

So, for the parameters: – 0xeBe2432d4b8C59F33674F6076ddeE8643B8039d1 as the lender contract (if you want, you already know how to deploy your own) – 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa as the ERC20 token we want to lend (DAI) – The value you decide based on how much leverage you want to use, and what is your balance. Note that all values here are in the minimum fraction of the token, so, if the token has 18 decimals digits, 1 token is exactly 1000000000000000000

The flash part on this is that, until you run out of gas/margins or fees, you can do anything when your contract is called back. Here we can see that by doing a flash loan of DAI the borrow contract was called back one, and then called another contract:

It all happened inside the onFlashLoan function on the borrower contract, and there is where you can personalize what to do with the loan, when that function is called, you could, for instance, send those tokens to yet another contract, that has higher rates, and could even be used maliciously, by people that steal the liquidity from protocols, because after you call a remote flashLoan, this is the function that is called back.

Conclusion

You can see that flash loans are complicated operations, and there are contracts already deployed to make it easier for you. However, you can, and should, come back to this guide, and read the official discussion for the ERC3156 standard, to know about the most up-to-date security standards and improvements. And remember the mission-critical part of our contracts are the onFlashLoan functions, which are called back from the contracts we are interacting with, and allow for more logic to be executed within the same transaction.

Also, remember to always test on a testnet before deploying on the main Ethereum blockchain, even if you have to do some deployments by yourself, it ends up cheaper than using the mainnet.

More great articles

How much blockchain developers REALLY make?

According to Hired, blockchain developers can command salaries between 140,000 USD to 180,000 USD. Sounds amazing, right? But is it…

Read Story

How To Become a Blockchain Developer: Step-by-Step Plan

Do you want to become a become Blockchain developer? With higher salaries, reports of Ethereum Dapps becoming very successful overnight,…

Read Story

The Ultimate Resource List to Learn Ethereum Dapps & Solidity Smart Contracts

LAST UPDATED IN MARCH 2019 When I first started to learn Ethereum a few years ago, there were almost no…

Read Story

Never miss a minute

Get great content to your inbox every week. No spam.

    Only great content, we don’t share your email with third parties.
    Arrow-up