A Complete Guide to Aave Flash Loan Arbitrage using Hardhat

Galien Dev
Coinmonks
9 min readAug 23, 2023

--

Are you ready to dive into the world of flash loan arbitrage using Aave and Hardhat? If you’re looking to leverage the power of decentralized finance (DeFi) to profit from price discrepancies, you’re in the right place. In this step-by-step tutorial, we’ll guide you through the process of setting up and executing flash loan arbitrage using the Aave protocol and the Hardhat development environment.

Prerequisites

Before we embark on this exciting journey, make sure you have the following prerequisites in place:

  1. Solid Understanding of Blockchain and Smart Contracts: You should have a solid grasp of blockchain technology and how smart contracts work.
  2. Ethereum and Hardhat Knowledge: Familiarity with Ethereum and the Hardhat development environment is essential. If you’re new to Hardhat, consider going through their official documentation first.
  3. Node.js and npm: Ensure you have Node.js and npm (Node Package Manager) installed on your machine.

Now that we have our prerequisites sorted, let’s get started with setting up our project and diving into the fascinating world of flash loan arbitrage!

Setting Up the Project

Step 1: Initialize a New Hardhat Project

Open your terminal and navigate to your desired project directory. Run the following commands:

npm install --save-dev hardhat
npx hardhat

Follow the prompts to create a new Hardhat project. Choose the default settings for simplicity.

Step 2: Install Dependencies

We’ll need to install some additional dependencies for our project. Open your terminal and run the following commands:

yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers @nomiclabs/hardhat-etherscan @nomiclabs/hardhat-waffle chai ethereum-waffle hardhat hardhat-contract-sizer hardhat-deploy hardhat-gas-reporter prettier prettier-plugin-solidity solhint solidity-coverage dotenv
yarn add @aave/core-v3

Step 3: Project Structure

Your project directory should now have the following structure:

- contracts/
- FlashLoanArbitrage.sol
- Dex.sol
- deploy/
- 00-deployDex.js
- 01-deployFlashLoanArbitrage.js
- scripts/
- test/
- hardhat.config.js
- package.json
- README.md

create .env file, add both SEPOLIA_RPC_URL and PRIVATE_KEY by your proper credentials as follows:

SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/....
PRIVATE_KEY=....

Open hardhat.config.js, and update it with the details below:

    require("@nomiclabs/hardhat-waffle")
require("hardhat-deploy")
require("dotenv").config()

/**
* @type import('hardhat/config').HardhatUserConfig
*/

const SEPOLIA_RPC_URL =
process.env.SEPOLIA_RPC_URL || "https://eth-sepolia.g.alchemy.com/v2/YOUR-API-KEY"
const PRIVATE_KEY = process.env.PRIVATE_KEY || "0x"

module.exports = {
defaultNetwork: "hardhat",
networks: {
hardhat: {
// // If you want to do some forking, uncomment this
// forking: {
// url: MAINNET_RPC_URL
// }
chainId: 31337,
},
localhost: {
chainId: 31337,
},
sepolia: {
url: SEPOLIA_RPC_URL,
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
// accounts: {
// mnemonic: MNEMONIC,
// },
saveDeployments: true,
chainId: 11155111,
},
},
namedAccounts: {
deployer: {
default: 0, // here this will by default take the first account as deployer
1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
},
player: {
default: 1,
},
},
solidity: {
compilers: [
{
version: "0.8.7",
},
{
version: "0.8.10",
},
],
},
mocha: {
timeout: 500000, // 500 seconds max for running tests
},
}

Additionally, create a new file called helper-hardhat-config.js in the root directory and add the following needed details keeping in ming that we’re using the Sepolia test network for all save addresses:
1. PoolAddressesProvider,
2. daiAddress,
3. usdcAddress

Here’s how this file should look:

const { ethers } = require('hardhat');

const networkConfig = {
default: {
name: 'hardhat',
},
11155111: {
name: 'sepolia',
PoolAddressesProvider: '0x0496275d34753A48320CA58103d5220d394FF77F',
daiAddress:'0x68194a729C2450ad26072b3D33ADaCbcef39D574',
usdcAddress:'0xda9d4f9b69ac6C22e444eD9aF0CfC043b7a7f53f',
},
};

module.exports = {
networkConfig
}

After all the adjustments made above, here’s how our project structure should look:

- contracts/
- FlashLoanArbitrage.sol
- Dex.sol
- deploy/
- 00-deployDex.js
- 01-deployFlashLoanArbitrage.js
- scripts/
- test/
-.env
- hardhat.config.js
-helper-hardhat-config
- package.json
- README.md

Step 4: Contracts

In this tutorial, we’ll be working with two smart contracts:

  1. Dex.sol: This contract simulates a decentralized exchange where arbitrage opportunities occur.
  2. FlashLoanArbitrage.sol: This contract handles flash loans and arbitrage operations.

Let’s dive into these contracts and understand each line of code. First, we’ll explore FlashLoanArbitrage.sol.

Dex.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import {IERC20} from "@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol";

contract Dex {
address payable public owner;

IERC20 private dai;
IERC20 private usdc;

// exchange rate indexes
uint256 dexARate = 90;
uint256 dexBRate = 100;

// keeps track of individuals' dai balances
mapping(address => uint256) public daiBalances;

// keeps track of individuals' USDC balances
mapping(address => uint256) public usdcBalances;

constructor(address _daiAddress, address _usdcAddress) {
owner = payable(msg.sender);
dai = IERC20(_daiAddress);
usdc = IERC20(_usdcAddress);
}

function depositUSDC(uint256 _amount) external {
usdcBalances[msg.sender] += _amount;
uint256 allowance = usdc.allowance(msg.sender, address(this));
require(allowance >= _amount, "Check the token allowance");
usdc.transferFrom(msg.sender, address(this), _amount);
}

function depositDAI(uint256 _amount) external {
daiBalances[msg.sender] += _amount;
uint256 allowance = dai.allowance(msg.sender, address(this));
require(allowance >= _amount, "Check the token allowance");
dai.transferFrom(msg.sender, address(this), _amount);
}

function buyDAI() external {
uint256 daiToReceive = ((usdcBalances[msg.sender] / dexARate) * 100) *
(10**12);
dai.transfer(msg.sender, daiToReceive);
}

function sellDAI() external {
uint256 usdcToReceive = ((daiBalances[msg.sender] * dexBRate) / 100) /
(10**12);
usdc.transfer(msg.sender, usdcToReceive);
}

function getBalance(address _tokenAddress) external view returns (uint256) {
return IERC20(_tokenAddress).balanceOf(address(this));
}

function withdraw(address _tokenAddress) external onlyOwner {
IERC20 token = IERC20(_tokenAddress);
token.transfer(msg.sender, token.balanceOf(address(this)));
}

modifier onlyOwner() {
require(
msg.sender == owner,
"Only the contract owner can call this function"
);
_;
}

receive() external payable {}
}

Read the Dex Contract Explanation:

The Dex.sol contract simulates a decentralized exchange. Let's dive into its key features:

  • Dex Contract: The main contract defines storage variables for the owner, DAI and USDC addresses, and instances of IERC20 for DAI and USDC.
  • Token Deposits: depositUSDC and depositDAI functions allow users to deposit USDC and DAI tokens, updating their balances.
  • Token Exchange: buyDAI and sellDAI functions simulate token exchanges, buying DAI with USDC and selling DAI for USDC based on exchange rates.
  • Balance Tracking: The contract tracks individual user balances for DAI and USDC with mappings.
  • Token Withdrawal: The withdraw function enables the contract owner to withdraw tokens from the contract.

FlashLoanArbitrage.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import {FlashLoanSimpleReceiverBase} from "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPoolAddressesProvider} from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol";

interface IDex {
function depositUSDC(uint256 _amount) external;

function depositDAI(uint256 _amount) external;

function buyDAI() external;

function sellDAI() external;
}

contract FlashLoanArbitrage is FlashLoanSimpleReceiverBase {
address payable owner;
// Dex contract address
address private dexContractAddress =
0x81EA031a86EaD3AfbD1F50CF18b0B16394b1c076;

IERC20 private dai;
IERC20 private usdc;
IDex private dexContract;

constructor(address _addressProvider,address _daiAddress, address _usdcAddress)
FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider))
{
owner = payable(msg.sender);

dai = IERC20(_daiAddress);
usdc = IERC20(_usdcAddress);
dexContract = IDex(dexContractAddress);
}

/**
This function is called after your contract has received the flash loaned amount
*/
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
//
// This contract now has the funds requested.
// Your logic goes here.
//

// Arbirtage operation
dexContract.depositUSDC(1000000000); // 1000 USDC
dexContract.buyDAI();
dexContract.depositDAI(dai.balanceOf(address(this)));
dexContract.sellDAI();

// At the end of your logic above, this contract owes
// the flashloaned amount + premiums.
// Therefore ensure your contract has enough to repay
// these amounts.

// Approve the Pool contract allowance to *pull* the owed amount
uint256 amountOwed = amount + premium;
IERC20(asset).approve(address(POOL), amountOwed);

return true;
}

function requestFlashLoan(address _token, uint256 _amount) public {
address receiverAddress = address(this);
address asset = _token;
uint256 amount = _amount;
bytes memory params = "";
uint16 referralCode = 0;

POOL.flashLoanSimple(
receiverAddress,
asset,
amount,
params,
referralCode
);
}

function approveUSDC(uint256 _amount) external returns (bool) {
return usdc.approve(dexContractAddress, _amount);
}

function allowanceUSDC() external view returns (uint256) {
return usdc.allowance(address(this), dexContractAddress);
}

function approveDAI(uint256 _amount) external returns (bool) {
return dai.approve(dexContractAddress, _amount);
}

function allowanceDAI() external view returns (uint256) {
return dai.allowance(address(this), dexContractAddress);
}

function getBalance(address _tokenAddress) external view returns (uint256) {
return IERC20(_tokenAddress).balanceOf(address(this));
}

function withdraw(address _tokenAddress) external onlyOwner {
IERC20 token = IERC20(_tokenAddress);
token.transfer(msg.sender, token.balanceOf(address(this)));
}

modifier onlyOwner() {
require(
msg.sender == owner,
"Only the contract owner can call this function"
);
_;
}

receive() external payable {}
}

Read the FlashLoanArbitrage Contract Explanation:

The FlashLoanArbitrage.sol contract is the core of our arbitrage strategy. It utilizes Aave flash loans to execute profitable trades. Let's break down the key aspects of the contract:

  • Imports and Interfaces: Import the required contracts and interfaces from Aave and OpenZeppelin. These include FlashLoanSimpleReceiverBase, IPoolAddressesProvider, and IERC20.
  • IDex Interface: Define the interface for the decentralized exchange (DEX) where arbitrage takes place. Methods like depositUSDC, depositDAI, buyDAI, and sellDAI are defined.
  • FlashLoanArbitrage Contract: The main contract, inheriting from, initializes addresses and contracts for DAI, USDC, and the DEX. It implements the executeOperation function that executes the arbitrage operation, buying DAI at a lower rate and selling it at a higher rate.
  • Flash Loan Execution: The arbitrage operation is executed within the executeOperation function. Funds are deposited, DAI is bought, deposited, and then sold.
  • Loan Repayment: The contract repays the flash loan amount plus the premium to Aave by approving the Pool contract to pull the owed amount.
  • Flash Loan Request: The requestFlashLoan function initiates the flash loan by calling flashLoanSimple from the POOL contract.
  • Token Approval and Allowance: Functions like approveUSDC, approveDAI, allowanceUSDC, and allowanceDAI are included for approving tokens and checking allowances for the DEX.
  • Balance Inquiry and Withdrawal: The getBalance function checks the balance of a token. The withdraw function allows the contract owner to withdraw tokens.

Step 5: Deploying Smart Contracts

Deploying your smart contracts is the next crucial step. Let’s take a look at the deployment scripts.

00-deployDex.js

The deployment script for the Dex.sol contract:

const { network } = require("hardhat")
const { networkConfig } = require("../helper-hardhat-config")

module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments
const { deployer } = await getNamedAccounts()
const chainId = network.config.chainId
const arguments = [networkConfig[chainId]["daiAddress"],networkConfig[chainId]["usdcAddress"]]

const dex = await deploy("Dex", {
from: deployer,
args: arguments,
log: true,
})

log("Dex contract deployed at : ", dex.address)
}

module.exports.tags = ["all", "dex"]

01-deployFlashLoanArbitrage.js

The deployment script for the FlashLoanArbitrage.sol contract:

const { network } = require("hardhat")
const { networkConfig } = require("../helper-hardhat-config")

module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments
const { deployer } = await getNamedAccounts()
const chainId = network.config.chainId
const arguments = [networkConfig[chainId]["PoolAddressesProvider"],networkConfig[chainId]["daiAddress"],networkConfig[chainId]["usdcAddress"]]

const dex = await deploy("FlashLoanArbitrage", {
from: deployer,
args: arguments,
log: true,
})

log("FlashLoanArbitrage contract deployed at : ", dex.address)
}

module.exports.tags = ["all", "FlashLoanArbitrage"]

Let’s start by deploying the Dex.sol contract:

yarn hardhat deploy --tags dex --network sepolia

The Dex contract address is “0x81EA031a86EaD3AfbD1F50CF18b0B16394b1c076” which is added to the FlashLoanArbitrage contract

Then we deploy FlashLoanArbitrage.sol by running the command below:

yarn hardhat deploy --tags FlashLoanArbitrage --network sepolia

which outputs the FlashLoanArbitrage contract address below:
0xc30b671E6d94C62Ee37669b229c7Cd9Eab2f7098

Step 5: Testing Smart Contracts

Let’s now text out the contracts using Remix IDE, but to be more specific here’s where the Flash Loan Arbitrage:


// exchange rate indexes
uint256 dexARate = 90;
uint256 dexBRate = 100;

Here, we buy 1DAI at 0.90 and sell it at 100.00 .
When coping the code to Remix IDE, consider change the imports below in both contracts accordingly:

import {IERC20} from "@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol";
import {FlashLoanSimpleReceiverBase} from "https://github.com/aave/aave-v3-core/blob/master/contracts/flashloan/base/FlashLoanSimpleReceiver.sol";
import {IPoolAddressesProvider} from "https://github.com/aave/aave-v3-core/blob/master/contracts/interfaces/IPoolAddressesProvider.sol";

Add liquidity to Dex.sol:

  1. USDC 1500
  2. DAI 1500

Approve:

  1. USDC 1000000000
  2. DAI 1200000000000000000000

Request Loan — USDC (6 decimal):

  1. 0xda9d4f9b69ac6C22e444eD9aF0CfC043b7a7f53f,1000000000 // 1,000 USDC

Let’s view our transaction on ehterscan

Here’s the transtions explanation:

  1. Transfer 1000 USDC from the aave LendingPool contract toFlashLoanArbitrage contract,
  2. Deposit 1000 USDC from the FlashLoanArbitrage contract to Dex contract,
  3. Purchase of DAI From Dex contract to FlashLoanArbitrage contract,
  4. Deposit of the DAI amount in the Dex contract,
  5. Transfer 1,111.1111 USDC from Dex contract to FlashLoanArbitrage
  6. Rapay 1000 USDC + 0.05% of flashloan fee (1000.5 USDC)

After a successfull tranaction, we’ll chek back our balalnce wich increases up to 110.611100 USDC

GitHub repository: https://github.com/Muhindo-Galien/Aave-Flash-Loan-Arbitrage

Conclusion

Congratulations! You’ve embarked on an exciting journey to explore flash loan arbitrage using Aave and Hardhat. In this tutorial, you learned how to set up your project, understand smart contracts, deploy them and test them using REMIX IDE. With your newly gained knowledge, you’re now equipped to explore and experiment further in the world of decentralized finance. Happy coding and discovering arbitrage opportunities!

--

--

Galien Dev
Coinmonks

Software Engineer & Tech Writer | Smart Contract Developer|Node.js|React.js Reach out: 📨galiencodes13@gmail.com, or https://www.linkedin.com/in/muhindo-galien/