A Complete Guide to Aave Flash Loan Arbitrage using Hardhat
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:
- Solid Understanding of Blockchain and Smart Contracts: You should have a solid grasp of blockchain technology and how smart contracts work.
- 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.
- 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:
Dex.sol
: This contract simulates a decentralized exchange where arbitrage opportunities occur.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
anddepositDAI
functions allow users to deposit USDC and DAI tokens, updating their balances. - Token Exchange:
buyDAI
andsellDAI
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
, andIERC20
. - IDex Interface: Define the interface for the decentralized exchange (DEX) where arbitrage takes place. Methods like
depositUSDC
,depositDAI
,buyDAI
, andsellDAI
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 callingflashLoanSimple
from the POOL contract. - Token Approval and Allowance: Functions like
approveUSDC
,approveDAI
,allowanceUSDC
, andallowanceDAI
are included for approving tokens and checking allowances for the DEX. - Balance Inquiry and Withdrawal: The
getBalance
function checks the balance of a token. Thewithdraw
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:
USDC 1500
DAI 1500
Approve:
USDC 1000000000
DAI 1200000000000000000000
Request Loan — USDC (6 decimal):
0xda9d4f9b69ac6C22e444eD9aF0CfC043b7a7f53f,1000000000 // 1,000 USDC
Let’s view our transaction on ehterscan
Here’s the transtions explanation:
- Transfer
1000 USDC
from the aaveLendingPool
contract toFlashLoanArbitrage
contract, - Deposit
1000 USDC
from theFlashLoanArbitrage
contract toDex
contract, - Purchase of DAI From
Dex
contract toFlashLoanArbitrage
contract, - Deposit of the DAI amount in the
Dex
contract, - Transfer
1,111.1111 USDC
fromDex
contract toFlashLoanArbitrage
- 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!