diff --git a/script/DeployArbitrage.s.sol b/script/DeployArbitrage.s.sol index ce63534..458d7dd 100644 --- a/script/DeployArbitrage.s.sol +++ b/script/DeployArbitrage.s.sol @@ -17,7 +17,7 @@ contract DeployArbitrage is Script { function run() public returns (Arbitrage) { vm.startBroadcast(); - arbitrage = new Arbitrage(currentConfig.uniswapQuoter); + arbitrage = new Arbitrage(); vm.stopBroadcast(); return arbitrage; } diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol index 3d32037..295bdb6 100644 --- a/script/HelperConfig.s.sol +++ b/script/HelperConfig.s.sol @@ -13,12 +13,14 @@ contract HelperConfig is Script { TYPES //////////////////////////////////////////////////////////////*/ struct NetworkConfig { + address weth; address usdc; address uniswapFactory; //Uniswap V3 address uniswapRouter; //Uniswap V3 + address uniswapQuoter; address sushiswapFactory; address sushiswapRouter; - address uniswapQuoter; + address sushiswapQuoter; } /*////////////////////////////////////////////////////////////// @@ -26,48 +28,56 @@ contract HelperConfig is Script { //////////////////////////////////////////////////////////////*/ function getBaseSepoliaConfig() public pure returns (NetworkConfig memory) { NetworkConfig memory SepoliaConfig = NetworkConfig({ + weth: 0x4200000000000000000000000000000000000006, usdc: 0x036CbD53842c5426634e7929541eC2318f3dCF7e, uniswapFactory: 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24, uniswapRouter: 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4, + uniswapQuoter: 0xC5290058841028F1614F3A6F0F5816cAd0df5E27, sushiswapFactory: address(0), sushiswapRouter: address(0), - uniswapQuoter: 0xC5290058841028F1614F3A6F0F5816cAd0df5E27 + sushiswapQuoter: address(0) }); return SepoliaConfig; } function getETHSepoliaConfig() public pure returns (NetworkConfig memory) { NetworkConfig memory SepoliaConfig = NetworkConfig({ + weth: 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14, usdc: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, uniswapFactory: 0x0227628f3F023bb0B980b67D528571c95c6DaC1c, uniswapRouter: 0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E, + uniswapQuoter: 0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3, sushiswapFactory: address(0), sushiswapRouter: 0x93c31c9C729A249b2877F7699e178F4720407733, - uniswapQuoter: address(0) + sushiswapQuoter: 0x039e87AB90205F9d87c5b40d4B28e2Be45dA4a20 }); return SepoliaConfig; } function getModeSepoliaConfig() public pure returns (NetworkConfig memory) { NetworkConfig memory SepoliaConfig = NetworkConfig({ + weth: address(0), usdc: address(0), uniswapFactory: 0x879A0F1E8402E37ECC56C53C55B6E02EB704eDD4, uniswapRouter: 0x9eE1289c21321E212994B23Bf0b4Cdc453C17EEE, + uniswapQuoter: address(0), sushiswapFactory: address(0), sushiswapRouter: address(0), - uniswapQuoter: address(0) + sushiswapQuoter: address(0) }); return SepoliaConfig; } function getModeMainnetConfig() public pure returns (NetworkConfig memory) { NetworkConfig memory SepoliaConfig = NetworkConfig({ + weth: address(0), usdc: 0xd988097fb8612cc24eeC14542bC03424c656005f, uniswapFactory: address(0), uniswapRouter: address(0), + uniswapQuoter: address(0), sushiswapFactory: address(0), sushiswapRouter: 0xf2614A233c7C3e7f08b1F887Ba133a13f1eb2c55, - uniswapQuoter: address(0) + sushiswapQuoter: address(0) }); return SepoliaConfig; } @@ -78,12 +88,14 @@ contract HelperConfig is Script { function getAnvilConfig() public pure returns (NetworkConfig memory) { console2.log("Testing On Anvil Network"); NetworkConfig memory AnvilConfig = NetworkConfig({ + weth: address(0), usdc: address(1), uniswapFactory: address(2), uniswapRouter: address(3), + uniswapQuoter: address(6), sushiswapFactory: address(4), sushiswapRouter: address(5), - uniswapQuoter: address(6) + sushiswapQuoter: address(6) }); return AnvilConfig; } diff --git a/script/Swap.s.sol b/script/Swap.s.sol index 6c6f85c..3177c04 100644 --- a/script/Swap.s.sol +++ b/script/Swap.s.sol @@ -22,12 +22,16 @@ contract Swap is Script { function run() public { vm.startBroadcast(); - bool success = IERC20(currentConfig.usdc).transfer(address(arbitrage), 2000000); + bool success = IERC20(currentConfig.usdc).transfer( + address(arbitrage), + 2000000 + ); console.log("transfer usdc to arbitrage contract", success); uint256 amountOut = arbitrage._swapOnV3( currentConfig.uniswapRouter, + currentConfig.uniswapQuoter, currentConfig.usdc, 2000000, 0x4200000000000000000000000000000000000006, diff --git a/src/Arbitrage.sol b/src/Arbitrage.sol index 5b7cfb3..ed6eaf5 100644 --- a/src/Arbitrage.sol +++ b/src/Arbitrage.sol @@ -7,27 +7,56 @@ import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRoute import {ISwapRouter02} from "./interfaces/ISwapRouter02.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 { - IVault private constant vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + /// @notice Balancer Vault instance. + IVault private constant vault = + IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); address public owner; - IQuoterV2 public uniswapQuoter; + /// @notice Struct representing a swap / i.e. a buy or sell in this case. struct Trade { address[] routerPath; + address[] quoterPath; address[] tokenPath; uint24 fee; } - constructor(address _quoter) { + event TokensSwapped( + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + uint256 quotedFee, + uint256 amountOut + ); + + constructor() { owner = msg.sender; - uniswapQuoter = IQuoterV2(_quoter); } - 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})); + 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); @@ -56,13 +85,22 @@ contract Arbitrage is IFlashLoanRecipient { // 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); + _swapOnV3( + trade.routerPath[0], + trade.quoterPath[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.quoterPath[1], trade.tokenPath[1], IERC20(trade.tokenPath[1]).balanceOf(address(this)), trade.tokenPath[0], @@ -74,13 +112,17 @@ contract Arbitrage is IFlashLoanRecipient { 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))); + IERC20(trade.tokenPath[0]).transfer( + owner, + IERC20(trade.tokenPath[0]).balanceOf(address(this)) + ); } // -- INTERNAL FUNCTIONS -- // function _swapOnV3( address _router, + address _quoter, address _tokenIn, uint256 _amountIn, address _tokenOut, @@ -91,35 +133,55 @@ contract Arbitrage is IFlashLoanRecipient { IERC20(_tokenIn).approve(_router, _amountIn); // Setup swap parameters - ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02.ExactInputSingleParams({ - tokenIn: _tokenIn, - tokenOut: _tokenOut, - fee: _fee, - recipient: address(this), - amountIn: _amountIn, - amountOutMinimum: _amountOut, - sqrtPriceLimitX96: 0 - }); + ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02 + .ExactInputSingleParams({ + tokenIn: _tokenIn, + tokenOut: _tokenOut, + fee: _fee, + recipient: address(this), + amountIn: _amountIn, + amountOutMinimum: _amountOut, + sqrtPriceLimitX96: 0 + }); // Get fee amount - uint256 fee = getUniswapFeeQuote(_tokenIn, _tokenOut, _amountIn, _fee); + uint256 fee = getFeeQuote( + _quoter, + _tokenIn, + _tokenOut, + _amountIn, + _fee + ); // Perform swap amountOut = ISwapRouter02(_router).exactInputSingle{value: fee}(params); + + emit TokensSwapped( + _tokenIn, + _tokenOut, + _amountIn, + _amountOut, + fee, + amountOut + ); } - function getUniswapFeeQuote(address _tokenIn, address _tokenOut, uint256 amountIn, uint24 _fee) - public - returns (uint256 fee) - { - IQuoterV2.QuoteExactInputSingleParams memory params = IQuoterV2.QuoteExactInputSingleParams({ - tokenIn: _tokenIn, - tokenOut: _tokenOut, - amountIn: amountIn, - fee: _fee, - sqrtPriceLimitX96: 0 - }); - (,,, fee) = uniswapQuoter.quoteExactInputSingle(params); + function getFeeQuote( + address _quoter, + address _tokenIn, + address _tokenOut, + uint256 amountIn, + uint24 _fee + ) public returns (uint256 fee) { + IQuoterV2.QuoteExactInputSingleParams memory params = IQuoterV2 + .QuoteExactInputSingleParams({ + tokenIn: _tokenIn, + tokenOut: _tokenOut, + amountIn: amountIn, + fee: _fee, + sqrtPriceLimitX96: 0 + }); + (, , , fee) = IQuoterV2(_quoter).quoteExactInputSingle(params); } receive() external payable {} diff --git a/test/unit/ArbitrageTest.t.sol b/test/unit/ArbitrageTest.t.sol index 75ca049..710b969 100644 --- a/test/unit/ArbitrageTest.t.sol +++ b/test/unit/ArbitrageTest.t.sol @@ -14,7 +14,6 @@ contract ArbitrageTest is Test { HelperConfig.NetworkConfig currentConfig; address owner = address(1); - address weth = 0x4200000000000000000000000000000000000006; uint256 sepoliaFork; uint256 baseSepoliaFork; @@ -24,13 +23,12 @@ contract ArbitrageTest is Test { function setUp() public { helperConfig = new HelperConfig(); - currentConfig = helperConfig.getBaseSepoliaConfig(); + currentConfig = helperConfig.getETHSepoliaConfig(); - baseSepoliaFork = vm.createFork(BASE_SEPOLIA_RPC_URL); - vm.selectFork(baseSepoliaFork); + baseSepoliaFork = vm.createSelectFork(ETH_SEPOLIA_RPC_URL); vm.startPrank(owner); - arbitrage = new Arbitrage(currentConfig.uniswapQuoter); + arbitrage = new Arbitrage(); vm.stopPrank(); deal(currentConfig.usdc, owner, 69); @@ -40,22 +38,64 @@ contract ArbitrageTest is Test { } function test_swapOnV3_uniswap() public { - vm.selectFork(baseSepoliaFork); vm.startPrank(owner); - uint256 amountOut = arbitrage._swapOnV3(currentConfig.uniswapRouter, currentConfig.usdc, 10, weth, 0, 3000); + uint256 amountOut = arbitrage._swapOnV3( + currentConfig.uniswapRouter, + currentConfig.uniswapQuoter, + currentConfig.usdc, + 10, + currentConfig.weth, + 0, + 3000 + ); vm.stopPrank(); assertGt(amountOut, 0); + assertGt(IERC20(currentConfig.weth).balanceOf(address(arbitrage)), 0); + assertLt(IERC20(currentConfig.usdc).balanceOf(address(arbitrage)), 69); } - function test_swapOnV3_sushiswap() public {} + // function test_swapOnV3_sushiswap() public { + // vm.startPrank(owner); + // uint256 amountOut = arbitrage._swapOnV3( + // currentConfig.sushiswapRouter, + // currentConfig.sushiswapQuoter, + // currentConfig.usdc, + // 10, + // currentConfig.weth, + // 0, + // 3000 + // ); + // vm.stopPrank(); + // assertGt(amountOut, 0); + // assertGt(IERC20(currentConfig.weth).balanceOf(address(arbitrage)), 0); + // assertLt(IERC20(currentConfig.usdc).balanceOf(address(arbitrage)), 69); + // } function test_getUniswapFeeQuote() public { - vm.selectFork(baseSepoliaFork); vm.startPrank(owner); - uint256 fee = arbitrage.getUniswapFeeQuote(currentConfig.usdc, weth, 10, 3000); + uint256 fee = arbitrage.getFeeQuote( + currentConfig.uniswapQuoter, + currentConfig.usdc, + currentConfig.weth, + 10, + 3000 + ); vm.stopPrank(); assertGt(fee, 0); } + // function test_getSushiswapFeeQuote() public { + // vm.startPrank(owner); + // uint256 fee = arbitrage.getFeeQuote( + // currentConfig.sushiswapQuoter, + // currentConfig.usdc, + // currentConfig.weth, + // 10000000, + // 3000 + // ); + // vm.stopPrank(); + // assertGt(fee, 0); + // } + function test_receiveFlashLoan() public {} }