Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 7 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

FlashArbAI is a decentralized finance (DeFi) arbitrage bot that leverages Balancer V2 flash loans to identify and execute arbitrage opportunities across decentralized exchanges (DEXs). The project integrates with the Goat Framework and utilizes the Eliza AI agent plugin to provide a conversational interface for users. Users can interact with the AI agent to discover supported token pairs, monitor arbitrage opportunities, and execute trades when profitable opportunities arise.

The core of the project is the `Arbitrage` smart contract, which handles flash loans, token swaps, and profit calculations. The contract is deployed on the Mode Sepolia Testnet and supports DEXs that share the same interface as Uniswap V3.
The core of the project is the `Arbitrage` smart contract, which handles flash loans, token swaps, and profit calculations. The contract is deployed and supports DEXs that share the same interface as Uniswap V3.

## Features

Expand Down Expand Up @@ -46,16 +46,6 @@ The `Arbitrage` contract is the core of the FlashArbAI project. It implements th
- `_amountOut`: Minimum amount of output tokens expected.
- `_fee`: Pool fee for the swap.

#### Events

- **`TokensSwapped`**:
- Emitted when a token swap is executed.
- Parameters:
- `tokenIn`: Address of the input token.
- `tokenOut`: Address of the output token.
- `amountIn`: Amount of input tokens swapped.
- `minAmountOut`: Minimum amount of output tokens received.

## Setup Instructions

### Prerequisites
Expand Down Expand Up @@ -84,50 +74,28 @@ The `Arbitrage` contract is the core of the FlashArbAI project. It implements th
forge build
```

4. Deploy the contract to the Mode Sepolia Testnet:
4. Deploy the contract, here is an example for Sepolia Testnet:
```bash
forge script script/DeployArbitrage.s.sol:DeployArbitrage <MODE_SEPOLIA_RPC_URL> --private-key <PRIVATE_KEY> --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.mode.network/api/
forge script script/DeployArbitrage.s.sol:DeployArbitrage <SEPOLIA_RPC_URL> --private-key <PRIVATE_KEY> --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.network/api/
```

### Configuration

1. Update the foundry.toml file with your Mode Sepolia Testnet RPC URL and private key.

2. Configure the Eliza AI agent plugin in the Goat Framework to interact with the Arbitrage contract.
1. Update the foundry.toml file with your network RPC URL.

## Usage
### Interacting with the AI Agent
1. Start the Goat Framework with the Eliza AI agent plugin.
2. Use the conversational interface to query supported token pairs:
```
User: What token pairs are supported for arbitrage?
Eliza: Supported token pairs are: ETH/USDC, USDC/DAI, WBTC/ETH.
```

3. Monitor arbitrage opportunities:
```
User: Are there any arbitrage opportunities for ETH/USDC?
Eliza: Yes, there is an arbitrage opportunity for ETH/USDC. Would you like to execute the trade?
```

4. Execute the trade:
```
User: Execute the trade.
Eliza: Executing trade... Trade completed successfully. Profit: 0.5 ETH.
```

### Executing Trades Programmatically
1. Call the `executeTrade` function on the Arbitrage contract using Foundry:
```bash
cast send <CONTRACT_ADDRESS> "executeTrade(address[],address[],address[],uint24,uint256)" \
"[<ROUTER1>, <ROUTER2>]" "[<QUOTER1>, <QUOTER2>]" "[<TOKEN1>, <TOKEN2>]" <FEE> <FLASH_AMOUNT> \
--rpc-url <MODE_SEPOLIA_RPC_URL> --private-key <PRIVATE_KEY>
--rpc-url <SEPOLIA_RPC_URL> --private-key <PRIVATE_KEY>
```

2. Monitor the TokensSwapped event to track trade execution and profits:
```bash
cast logs --from-block <START_BLOCK> --to-block <END_BLOCK> --address <CONTRACT_ADDRESS> \
--topic "TokensSwapped(address,address,uint256,uint256)" --rpc-url <MODE_SEPOLIA_RPC_URL>
--topic "TokensSwapped(address,address,uint256,uint256)" --rpc-url <SEPOLIA_RPC_URL>
```

## Testing
Expand Down Expand Up @@ -155,5 +123,4 @@ This project is licensed under the MIT License. See the `LICENSE` file for detai

## Acknowledgments
- Balancer Labs for the Balancer V2 flash loan functionality.
- Uniswap Labs for the Uniswap V3 integration.
- Goat Framework and Eliza AI agent for the conversational interface.
- Uniswap Labs for the Uniswap V3 integration.
2 changes: 1 addition & 1 deletion script/DeployArbitrage.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ contract DeployArbitrage is Script {

function setUp() public {
helperConfig = new HelperConfig();
modeConfig = helperConfig.getModeSepoliaConfig();
modeConfig = helperConfig.getSepoliaETHConfig();
}

function run() public returns (FlashArbitrage) {
Expand Down
166 changes: 66 additions & 100 deletions src/FlashArbitrage.sol
Original file line number Diff line number Diff line change
@@ -1,144 +1,110 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;

import "@balancer/balancer-v2-monorepo/pkg/interfaces/contracts/vault/IVault.sol";
import "@balancer/balancer-v2-monorepo/pkg/interfaces/contracts/vault/IFlashLoanRecipient.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol";
import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import {IQuoterV2} from "@uniswap/v3-periphery/contracts/interfaces/IQuoterV2.sol";

/**
* @title FlashArbitrage
* @author FlashArbAI
* @notice Advanced DEX arbitrage executor leveraging Balancer flash loans
* @dev Implements cross-DEX arbitrage strategies using flash loans and optimized swap paths
*/
contract FlashArbitrage is IFlashLoanRecipient {
/// @notice Balancer V2 Vault for flash loan operations
IVault private constant BALANCER_VAULT = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);

address public strategist;

/// @notice Defines parameters for a complete arbitrage operation
struct ArbStrategy {
address[] dexRouters; // Addresses of DEX routers to use
address[] priceQuoters; // Price quoter contracts for each DEX
address[] tradingPath; // Token addresses in trading sequence
uint24 poolFee; // Trading fee tier (3000 = 0.3%)
}
contract Arbitrage is IFlashLoanRecipient {
IVault private constant VAULT = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);

address public owner;

event ArbitrageExecuted(address sourceToken, address targetToken, uint256 inputAmount, uint256 minimumReturn);
struct Trade {
address[] routerPath;
address[] tokenPath;
uint24 fee;
}

constructor() {
strategist = msg.sender;
owner = msg.sender;
}

/**
* @notice Initiates an arbitrage operation with flash loan
* @param _dexRouters Array of DEX router addresses for the arbitrage path
* @param _priceQuoters Array of price quoter addresses for each DEX
* @param _tradingPath Array of token addresses in the arbitrage sequence
* @param _poolFee Fee tier for the pools to use
* @param _loanSize Size of the flash loan to initiate
*/
function executeArbitrage(
address[] memory _dexRouters,
address[] memory _priceQuoters,
address[] memory _tradingPath,
uint24 _poolFee,
uint256 _loanSize
) external {
bytes memory strategyData = abi.encode(
ArbStrategy({
dexRouters: _dexRouters,
priceQuoters: _priceQuoters,
tradingPath: _tradingPath,
poolFee: _poolFee
})
);
function executeTrade(address[] memory _routerPath, address[] memory _tokenPath, uint24 _fee, uint256 _flashAmount)
external
{
bytes memory data = abi.encode(Trade({routerPath: _routerPath, tokenPath: _tokenPath, fee: _fee}));

// Configure flash loan parameters
IERC20[] memory loanTokens = new IERC20[](1);
loanTokens[0] = IERC20(_tradingPath[0]);
// Token to flash loan, by default we are flash loaning 1 token.
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(_tokenPath[0]);

uint256[] memory loanAmounts = new uint256[](1);
loanAmounts[0] = _loanSize;
// Flash loan amount.
uint256[] memory amounts = new uint256[](1);
amounts[0] = _flashAmount;

BALANCER_VAULT.flashLoan(this, loanTokens, loanAmounts, strategyData);
VAULT.flashLoan(this, tokens, amounts, data);
}

/**
* @notice Handles the flash loan callback and executes the arbitrage strategy
* @dev Implements the core arbitrage logic: borrow → swap A → swap B → repay
*/
function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external override {
require(msg.sender == address(BALANCER_VAULT), "Unauthorized callback");

ArbStrategy memory strategy = abi.decode(userData, (ArbStrategy));
uint256 loanAmount = amounts[0];

// Execute first leg of arbitrage
executeSwap(
strategy.dexRouters[0],
strategy.tradingPath[0],
loanAmount,
strategy.tradingPath[1],
0, // Accept any output for first swap
strategy.poolFee
require(msg.sender == address(VAULT));

// Decode our swap data so we can use it
Trade memory trade = abi.decode(userData, (Trade));
uint256 flashAmount = amounts[0];

// Since balancer called this function, we should have funds to begin swapping...

// We perform the 1st swap.
// We swap the flashAmount of token0 and expect to get X amount of token1
_swapOnV3(trade.routerPath[0], trade.tokenPath[0], flashAmount, trade.tokenPath[1], 0, trade.fee);

// We perform the 2nd swap.
// We swap the contract balance of token1 and
// expect to at least get the flashAmount of token0
_swapOnV3(
trade.routerPath[1],
trade.tokenPath[1],
IERC20(trade.tokenPath[1]).balanceOf(address(this)),
trade.tokenPath[0],
flashAmount,
trade.fee
);

// Execute second leg of arbitrage
uint256 intermediateBalance = IERC20(strategy.tradingPath[1]).balanceOf(address(this));
executeSwap(
strategy.dexRouters[1],
strategy.tradingPath[1],
intermediateBalance,
strategy.tradingPath[0],
loanAmount, // Minimum output must cover flash loan
strategy.poolFee
);

// Repay flash loan
IERC20(strategy.tradingPath[0]).transfer(address(BALANCER_VAULT), loanAmount);
// Transfer back what we flash loaned
IERC20(trade.tokenPath[0]).transfer(address(VAULT), flashAmount);

// Transfer profits to strategist
uint256 profitAmount = IERC20(strategy.tradingPath[0]).balanceOf(address(this));
if (profitAmount > 0) {
IERC20(strategy.tradingPath[0]).transfer(strategist, profitAmount);
}
// Transfer any excess tokens [i.e. profits] to owner
IERC20(trade.tokenPath[0]).transfer(owner, IERC20(trade.tokenPath[0]).balanceOf(address(this)));
}

/**
* @notice Executes a single swap on a DEX
* @dev Optimized for exact input swaps with minimum output requirements
*/
function executeSwap(
address _dexRouter,
// -- INTERNAL FUNCTIONS -- //

function _swapOnV3(
address _router,
address _tokenIn,
uint256 _swapAmount,
uint256 _amountIn,
address _tokenOut,
uint256 _minReturn,
uint24 _poolFee
uint256 _amountOut,
uint24 _fee
) internal {
IERC20(_tokenIn).approve(_dexRouter, _swapAmount);
// Approve token to swap
IERC20(_tokenIn).approve(_router, _amountIn);

ISwapRouter.ExactInputSingleParams memory swapParams = ISwapRouter.ExactInputSingleParams({
// Setup swap parameters
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: _tokenIn,
tokenOut: _tokenOut,
fee: _poolFee,
fee: _fee,
recipient: address(this),
deadline: block.timestamp,
amountIn: _swapAmount,
amountOutMinimum: _minReturn,
amountIn: _amountIn,
amountOutMinimum: _amountOut,
sqrtPriceLimitX96: 0
});

ISwapRouter(_dexRouter).exactInputSingle(swapParams);

emit ArbitrageExecuted(_tokenIn, _tokenOut, _swapAmount, _minReturn);
// Perform swap
ISwapRouter(_router).exactInputSingle(params);
}
}
16 changes: 0 additions & 16 deletions src/interfaces/ISwapRouter02.sol

This file was deleted.

Loading