diff --git a/script/DeployArbitrage.s.sol b/script/DeployArbitrage.s.sol index 3ecc9ae..ca85cea 100644 --- a/script/DeployArbitrage.s.sol +++ b/script/DeployArbitrage.s.sol @@ -2,26 +2,26 @@ pragma solidity ^0.8.13; import {Script, console2} from "forge-std/Script.sol"; -import {Arbitrage} from "../src/Arbitrage.sol"; +import {FlashArbitrage} from "../src/FlashArbitrage.sol"; import {HelperConfig} from "../script/HelperConfig.s.sol"; contract DeployArbitrage is Script { HelperConfig public helperConfig; HelperConfig.NetworkConfig modeConfig; HelperConfig.ForkNetworkConfig SepoliaConfig; - Arbitrage public arbitrage; + FlashArbitrage public flashArbitrage; function setUp() public { helperConfig = new HelperConfig(); modeConfig = helperConfig.getModeSepoliaConfig(); } - function run() public returns (Arbitrage) { + function run() public returns (FlashArbitrage) { vm.startBroadcast(); - arbitrage = new Arbitrage(); + flashArbitrage = new FlashArbitrage(); vm.stopBroadcast(); - console2.log("Arbitrage contract deployed to:", address(arbitrage)); - return arbitrage; + console2.log("Arbitrage contract deployed to:", address(flashArbitrage)); + return flashArbitrage; } } diff --git a/script/Swap.s.sol b/script/Swap.s.sol index d02a84e..e4daeda 100644 --- a/script/Swap.s.sol +++ b/script/Swap.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; -import {Arbitrage} from "../src/Arbitrage.sol"; +import {FlashArbitrage} from "../src/FlashArbitrage.sol"; import {DeployArbitrage} from "../script/DeployArbitrage.s.sol"; import {HelperConfig} from "../script/HelperConfig.s.sol"; import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; @@ -10,7 +10,7 @@ import {IQuoterV2} from "@uniswap/v3-periphery/contracts/interfaces/IQuoterV2.so import {IERC20} from "@openzeppelin/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; contract Swap is Script { - Arbitrage public arbitrage; + FlashArbitrage public arbitrage; HelperConfig public helperConfig; HelperConfig.NetworkConfig currentConfig; diff --git a/src/Arbitrage.sol b/src/Arbitrage.sol deleted file mode 100644 index 5381ec2..0000000 --- a/src/Arbitrage.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: MIT -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 {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import {IQuoterV2} from "@uniswap/v3-periphery/contracts/interfaces/IQuoterV2.sol"; - -/** - * @title Arbitrage. - * @author FlashArb-AI. - * @notice Earn arbitrage between two DEX's using balancer Flash Loans. - * @dev DEX's supported currently are the ones that share the same interface as uniswap(i.e. a fork). - */ -contract Arbitrage is IFlashLoanRecipient { - /// @notice Balancer Vault instance. - IVault private constant vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - - address public owner; - - /// @notice Struct representing a swap / i.e. a buy or sell in this case. - struct Trade { - address[] routerPath; - address[] quoterPath; - address[] tokenPath; - uint24 fee; - } - - event TokensSwapped(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut); - - constructor() { - owner = msg.sender; - } - - function executeTrade( - address[] memory _routerPath, - address[] memory _quoterPath, - address[] memory _tokenPath, - uint24 _fee, - uint256 _flashAmount - ) external { - bytes memory data = - abi.encode(Trade({routerPath: _routerPath, quoterPath: _quoterPath, tokenPath: _tokenPath, fee: _fee})); - - // Token to flash loan, by default we are flash loaning 1 token. - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = IERC20(_tokenPath[0]); - - // Flash loan amount. - uint256[] memory amounts = new uint256[](1); - amounts[0] = _flashAmount; - - vault.flashLoan(this, tokens, amounts, data); - } - - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory feeAmounts, - bytes memory userData - ) external override { - 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 - ); - - // Transfer back what we flash loaned - IERC20(trade.tokenPath[0]).transfer(address(vault), flashAmount); - - // Transfer any excess tokens [i.e. profits] to owner - IERC20(trade.tokenPath[0]).transfer(owner, IERC20(trade.tokenPath[0]).balanceOf(address(this))); - } - - // -- INTERNAL FUNCTIONS -- // - - function _swapOnV3( - address _router, - address _tokenIn, - uint256 _amountIn, - address _tokenOut, - uint256 _amountOut, - uint24 _fee - ) internal { - // Approve token to swap - IERC20(_tokenIn).approve(_router, _amountIn); - - // Setup swap parameters - ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ - tokenIn: _tokenIn, - tokenOut: _tokenOut, - fee: _fee, - recipient: address(this), - deadline: block.timestamp, - amountIn: _amountIn, - amountOutMinimum: _amountOut, - sqrtPriceLimitX96: 0 - }); - - // Perform swap - ISwapRouter(_router).exactInputSingle(params); - - emit TokensSwapped(_tokenIn, _tokenOut, _amountIn, _amountOut); - } -} diff --git a/src/FlashArbitrage.sol b/src/FlashArbitrage.sol new file mode 100644 index 0000000..7918696 --- /dev/null +++ b/src/FlashArbitrage.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +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 {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%) + } + + event ArbitrageExecuted(address sourceToken, address targetToken, uint256 inputAmount, uint256 minimumReturn); + + constructor() { + strategist = 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 + }) + ); + + // Configure flash loan parameters + IERC20[] memory loanTokens = new IERC20[](1); + loanTokens[0] = IERC20(_tradingPath[0]); + + uint256[] memory loanAmounts = new uint256[](1); + loanAmounts[0] = _loanSize; + + BALANCER_VAULT.flashLoan(this, loanTokens, loanAmounts, strategyData); + } + + /** + * @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 + ); + + // 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 profits to strategist + uint256 profitAmount = IERC20(strategy.tradingPath[0]).balanceOf(address(this)); + if (profitAmount > 0) { + IERC20(strategy.tradingPath[0]).transfer(strategist, profitAmount); + } + } + + /** + * @notice Executes a single swap on a DEX + * @dev Optimized for exact input swaps with minimum output requirements + */ + function executeSwap( + address _dexRouter, + address _tokenIn, + uint256 _swapAmount, + address _tokenOut, + uint256 _minReturn, + uint24 _poolFee + ) internal { + IERC20(_tokenIn).approve(_dexRouter, _swapAmount); + + ISwapRouter.ExactInputSingleParams memory swapParams = ISwapRouter.ExactInputSingleParams({ + tokenIn: _tokenIn, + tokenOut: _tokenOut, + fee: _poolFee, + recipient: address(this), + deadline: block.timestamp, + amountIn: _swapAmount, + amountOutMinimum: _minReturn, + sqrtPriceLimitX96: 0 + }); + + ISwapRouter(_dexRouter).exactInputSingle(swapParams); + + emit ArbitrageExecuted(_tokenIn, _tokenOut, _swapAmount, _minReturn); + } +} diff --git a/test/unit/ArbitrageTest.t.sol b/test/unit/ArbitrageTest.t.sol index 79de255..d85ba48 100644 --- a/test/unit/ArbitrageTest.t.sol +++ b/test/unit/ArbitrageTest.t.sol @@ -3,13 +3,13 @@ pragma solidity 0.8.18; import {Test, console} from "forge-std/Test.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; -import {Arbitrage} from "../../src/Arbitrage.sol"; +import {FlashArbitrage} from "../../src/FlashArbitrage.sol"; import {HelperConfig} from "../../script/HelperConfig.s.sol"; import {IERC20} from "@openzeppelin/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; contract ArbitrageTest is Test { - Arbitrage public arbitrage; + FlashArbitrage public arbitrage; HelperConfig public helperConfig; HelperConfig.ForkNetworkConfig public networkConfig; @@ -28,7 +28,7 @@ contract ArbitrageTest is Test { baseSepoliaFork = vm.createSelectFork(ETH_SEPOLIA_RPC_URL); vm.startPrank(owner); - arbitrage = new Arbitrage(); + arbitrage = new FlashArbitrage(); vm.stopPrank(); deal(networkConfig.usdc, owner, 69);