WEB3DEV Español

Cover image for Guía completa de Aave Flash Loan Arbitrage usando Hardhat
Gabriella Alexandra Martinez Viloria
Gabriella Alexandra Martinez Viloria

Posted on

Guía completa de Aave Flash Loan Arbitrage usando Hardhat

Image description

¿Estás listo para profundizar en el mundo del arbitraje de préstamo rápido usando Aave y Hardhat? Si estás buscando aprovechar el poder de las finanzas descentralizadas (DeFi) para sacar provecho de las discrepancias de precios, estás en el lugar correcto. En este tutorial paso a paso, te guiaremos por el proceso de configurar y ejecutar el arbitraje de préstamo rápido usando el protocolo de Aave y el entorno de desarrollo de Hardhat.

Image description

Prerrequisitos

Antes de embarcarnos en esta vía emocionante, asegúrate de tener los siguientes prerrequisitos:

  1. Entendimiento Sólido de la Blockchain y los Contratos Inteligentes: deberías tener un conocimiento sólido de la tecnología blockchain y cómo los contratos inteligentes funcionan.

  2. Conocimiento Ethereum y de Hardhat: es vital estar familiarizado con el entorno de desarrollo de Hardhat. Si eres nuevo con Hardhat, considera leer primero los documentos oficiales.

  3. Node.js y npm: asegúrate que tienes Node.js y npm (Administrador de Paquetes de Nodo, Node Packcage Manager) instalado en tu máquina.

Ahora que ya tenemos nuestros prerrequisitos arreglados, ¡vamos a comenzar configurando nuestro proyecto y profundizar en el fascinante mundo del arbitraje de préstamo rápido!

Configurando el Proyecto

Paso 1: Inicia un Nuevo Proyecto Hardhat

Abre tu terminal y navega al directorio de tu proyecto. Ejecuta los siguientes comandos:

npm install --save-dev hardhat
npx hardhat
Enter fullscreen mode Exit fullscreen mode

Sigue los prompts para crear un nuevo proyecto Hardhat. Elige la configuración por defecto para simplificar las cosas.

Paso 2: Instala las Dependencias

Necesitamos instalar algunas dependencias adicionales para nuestro proyecto. Abre tu terminal y ejecuta los siguientes comandos:

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
Enter fullscreen mode Exit fullscreen mode

Paso 3: Estructura del Proyecto

El directorio de tu proyecto debería tener la siguiente estructura:

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

Crea un archivo .env, añade ambos SEPOLIA_RPC_URL y PRIVATE_KEY por sus credenciales como a continuación:

SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/....
PRIVATE_KEY=....
Enter fullscreen mode Exit fullscreen mode

Abre hardhat.config.js y actualízalo con los siguientes detalles:

 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: {
               // // Si quieres hacer un forking, descomenta este
               // 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, // aquí estará por defecto toma la primera cuenta como desplegador
               1: 0, // similarmente en la mainnet tomará la primera cuenta como desplegador. Nota que, dependiendo de cómo la red de harhat está configurada, la cuenta 0 en la red puede ser diferente que otra
           },
           player: {
               default: 1,
           },
       },
       solidity: {
           compilers: [
               {
                   version: "0.8.7",
               },
               {
                   version: "0.8.10",
               },
           ],
       },
       mocha: {
           timeout: 500000, // 500 máximo de segundos para ejecutar las pruebas
       },
   }
Enter fullscreen mode Exit fullscreen mode

Adicionalmente, crea un nuevo archivo llamado helper-hardhat-config.js en el directorio raíz y añade los siguientes detalles necesarios, tomando en cuenta que estamos usando la red de pruebas de Sepolia para todas las direcciones guardadas:

  1. PoolAddressesProvider,
  2. daiAddress,
  3. usdcAddress

Aquí está cómo el archivo debería verse:

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

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

module.exports = {
 networkConfig
}
Enter fullscreen mode Exit fullscreen mode

Luego de todos los ajustes que hicimos arriba, aquí está cómo la estructura de nuestro proyecto debería verse:

- 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
Enter fullscreen mode Exit fullscreen mode

Paso 4: Contratos

En este tutorial, estaremos trabajando con dos contratos inteligentes:

  1. Dex.sol: este contrato simula un intercambio descentralizado donde las oportunidades de arbitraje suceden.
  2. FlashLoanArbitrage.sol: este contrato maneja los préstamos rápidos y las operaciones de arbitraje.

Vamos a profundizar en estos contratos y entender cada línea de código. Primero, exploraremos 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;

   // índices de cambio
   uint256 dexARate = 90;
   uint256 dexBRate = 100;

   // mantiene el rastreo del saldo dai de los individuos
   mapping(address => uint256) public daiBalances;

   // mantiene el rastreo del saldo USDC de los individuos
   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 {}
}
Enter fullscreen mode Exit fullscreen mode

Lee la Explicación del Contrato Dex:

El contrato Dex.sol simula un intercambio descentralizado. Profundicemos en las características claves>

  • Contrato Dex: el contrato principal define el las variables del almacenamiento por el dueño, las direcciones DAI y USDC y las instancias de IERC20 para DAI y USDC.
  • Depósitos Token: las funciones depositUSDC y depositDAIpermite a los usuarios depositar tokens DAI y USDC, actualizando sus saldo.
  • Intercambio de Token: las funciones buyDAI y sellDAI simulan el intercambio de token, comprando DAI con USDC y vendiendo DAI por USDC basado en las tasas de intercambio.
  • Rastreo del Saldo: el contrato rastrea los saldos individuales del usuario para los mapeos del DAI y USDC.
  • Retiro del Token: la función withdraw permite al propietario del contrato retirar tokens del contrato.

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;
   // dirección del contrato Dex
   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);
   }

   /**
       Esta función se llama luego que tu contrato ha recibido la cantidad del préstamo rápido
    */
   function executeOperation(
       address asset,
       uint256 amount,
       uint256 premium,
       address initiator,
       bytes calldata params
   ) external override returns (bool) {
       //
       // Este contrato ahora tiene los fondos pedidos.
       // Tu lógica va aquí.
       //

       // Operación de arbitraje
       dexContract.depositUSDC(1000000000); // 1000 USDC
       dexContract.buyDAI();
       dexContract.depositDAI(dai.balanceOf(address(this)));
       dexContract.sellDAI();

       // Al final de la lógica de arriba, este contrato debe
       // la cantidad prestada rápidamente y los premiums.
       // Por lo tanto, asegúrate que tu contrato tiene suficiente para pagar
       // estas cantidades.

       // Aprueba la asignación por el contrato Pool para *pujar* la cantidad que se debe
       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 {}
}
Enter fullscreen mode Exit fullscreen mode

Lee la Explicación del Contrato FlashLoanArbitrage:

El contrato FlashLoanArbitrage.sol es el centro de nuestra estrategia de arbitraje. Utiliza los préstamos rápidos de Aave para ejecutar los intercambios rentables. Analicemos los aspectos claves del contrato:

  • Importar e Interfaces: importa los contratos requeridos y las interfaces desde Aave y OpenZeppelin. Esto incluye: FlashLoanSimpleReceiverBase, IPoolAddressesProvider y IERC20.
  • Interfaz IDex: define la interfaz para el intercambio descentralizado (DEX) donde el arbitraje toma lugar. Métodos como depositUSDC, depositDAI, buyDAI y sellDAI son definidos.
  • Contrato FlashLoanArbitrage: el contrato principal, heredado desde, inicializa las direcciones y contratos para DAI, USDC y el DEX. Implementa la función executeOperation que ejecuta la operación de arbitraje, comprando el DAI en una tasa baja y vendiéndola en una mayor tasa.
  • Ejecución de Préstamo Rápido: la operación de arbitraje se ejecuta dentro de la función executeOperation. Los fondos son depositados, el DAI es comprado, depositado y luego vendido.
  • Pago del Préstamo: el contrato paga la cantidad del préstamo rápido además del premium a Aave, aprobando el contrato Pool para empujar la cantidad que se debe.
  • Pedido de Préstamo Rápido: la función requestFlashLoan inicia el préstamo rápido llamando flashLoanSimple desde el contrato POOL.
  • Aprobación y Asignación del Token: las funciones como approveUSDC, approveDAI, allowanceUSDC y allowanceDAI están incluídos para aprobar los tokens y revisar las asignaciones para el DEX.
  • Consulta y Retiro del Saldo: la función getBalance revisa el saldo del token. La función withdraw permite al propietario del contrato retirar los tokens.

Paso 4: Despliega los Contratos Inteligentes

Desplegar tus contratos inteligentes es el siguiente paso crucial. Ahora, vamos a ver los script del despliegue.

00-deployDex.js

El script del despliegue para el contrato Dex.sol:

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"]
Enter fullscreen mode Exit fullscreen mode

01-deployFlashLoanArbitrage.js

El script de despliegue para el contrato FlashLoanArbitrage.sol:

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"]
Enter fullscreen mode Exit fullscreen mode

Comencemos desplegando el contrato Dex.sol:

yarn hardhat deploy --tags dex --network sepolia
Enter fullscreen mode Exit fullscreen mode

Image description

La dirección del contrato Dex es “0x81EA031a86EaD3AfbD1F50CF18b0B16394b1c076”, el cual es añadido al contrato FlashLoanArbitrage.

Luego, desplegamos FlashLoanArbitrage.sol, ejecutando el comando de abajo:

yarn hardhat deploy --tags FlashLoanArbitrage --network sepolia
Enter fullscreen mode Exit fullscreen mode

Image description

Los outputs FlashLoanArbitrage de las direcciones del contrato de abajo: “0xc30b671E6d94C62Ee37669b229c7Cd9Eab2f7098

Paso 5: Probando los Contratos Inteligentes

Ahora, vamos a enviar un mensaje a los contratos usando Remix IDE, pero para ser más específicos aquí es donde el Arbitraje de Préstamo Rápido:

// índices de intercambo
uint256 dexARate = 90;
uint256 dexBRate = 100;
Enter fullscreen mode Exit fullscreen mode

Aquí compramos 1DAI en 0.90 y lo vendemos a 100.00.
Cuando copias el código a Remix IDE, considera cambiar los importes de abajo en ambos contratos:

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";
Enter fullscreen mode Exit fullscreen mode

Añade liquidez a Dex.sol:

  1. USDC 1500
  2. DAI 1500

Image description

Image description

Aprueba:

  1. USDC 1000000000
  2. DAI 1200000000000000000000

Image description

Image description

Pedido de Préstamo: USDC (6 decimales):

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

Image description

Veamos nuestra transacción en etherscan:

https://miro.medium.com/v2/resize:fit:720/format:webp/1*CjiefK_EUQrrWP6p3EwkOQ.png
Enter fullscreen mode Exit fullscreen mode

Aquí está la explicación de las transiciones:

  1. Transfiere 1000 USDC desde el contrato aave LendingPool para el contrato FlashLoanArbitrage,
  2. Deposita 1000 USDC desde el contrato FlashLoanArbitrage al contrato Dex,
  3. Compra DAI desde el contrato Dex al contrato FlashLoanArbitrage,
  4. Deposita la cantidad de DAI en el contrato Dex,
  5. Transfiere 1,111.1111 USDC desde el contrato Dex a FlashLoanArbitrage
  6. Paga 1000 USDC + 0.05% de la tasa de préstamo rápido (1000.5 USDC)

Luego de una transacción exitosa, volveremos a revisar nuestro saldo el cual se incrementa a 110.611100 USDC

Image description

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

Conclusión

¡Felicitaciones! Te has embarcado en un recorrido emocionante para explorar el arbitraje de préstamos rápidos usando Aave y Hardhat. En este tutorial, aprendiste cómo configurar tu proyecto, entender contratos inteligentes, desplegarlos y probarlos usando REMIX IDE. Con tu conocimiento recién ganado, ahora estás equipado para explorar y experimentar más en el mundo de las finanzas descentralizadas. ¡Feliz codeo y descubre las oportunidades de arbitraje!

Este artículo es una traducción de Galien Dev, hecha por Gabriella Martínez. Puedes encontrar el artículo original aquí
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)