├── .env sample ├── .gitattributes ├── .gitignore ├── README.md ├── contracts └── Arbitrage │ ├── SuperArbit.sol │ └── interfaces │ ├── IERC20.sol │ ├── IPancakeCallee.sol │ ├── IPancakeERC20.sol │ ├── IPancakeFactory.sol │ ├── IPancakeMigrator.sol │ ├── IPancakePair.sol │ ├── IPancakeRouter01.sol │ ├── IPancakeRouter02.sol │ └── IWETH.sol ├── delete.json ├── hardhat.config.js ├── matchedPairs.json ├── package-lock.json ├── package.json ├── pairsList.json └── scripts ├── arbUtils.js ├── config.js ├── fetchPairs.js ├── findMatchedPairs.js ├── helpers.js ├── main.js └── superArbitDeploy.js /.env sample: -------------------------------------------------------------------------------- 1 | WEB3_INFURA_PROJECT_ID = 2 | ETHERSCAN_TOKEN = 3 | ALCHEMY_ETH_MAIN_ARCHIVE = 4 | MORALIS_SPEEDY_API_KEY = 5 | QUICKNODE_KEY = 6 | PRIV_KEY = 7 | 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | .env 3 | node_modules/ 4 | cache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Bot for Triangular DEX Arbitrage 2 | 3 | ## 1. Description 4 | 5 | This bot is designed to make automated crypto token cyclic-arbitrage transactions on DEX platforms(Decentralized Exchange) for profit. The implemented form of the arbitrage is cyclic-arbitrage with three legs, so the name "Triangular". In triangular arbitrage, the aim is to start with an asset(here it is crypto tokens) and do 3 swap transactions to end with the start token. An example would be: WBNB->BUSD->Cake->WBNB The bot constantly searches for arbitrage opportunites on different DEX platforms and if the trade is profitable(end amount > start amount + transaction fees), then the trade is executed. It can be used on DEX platforms where Uniswap V2 AMM(Automated Market Maker) is utilized for swap calculations. 6 | 7 | I used/tested this bot on Binance Smart Chain(BSC) where significant number of DEX platforms can be found, which are all Uniswap V2 clones (Such as PancakeSwap, biSwap, MDEX etc.) yet it can be used on other EVM-compatible blockchains like Ethereum, Avalanche etc. with slight modification of parameters. 8 | The algorithm(profitibility calculations, calculation of optimum input token amount etc.) used in this project is taken from [this paper](https://arxiv.org/pdf/2105.02784.pdf) 9 | 10 | A smart contract is also written for batched data fetching from blockchain(to speed up the searching as well as for minimizing the number of request from RPC Node API) and batched static checking of swap transactions to see if they go through without executing actual transaction. In order to run the bot, the contract must be deployed on mainnet of the blockchain(for example on BSC) 11 | This project is built on Hardhat/ethers.js 12 | 13 | ## 2. Run on local 14 | 15 | ### 2.1 Requirements 16 | 17 | After cloning this repo: (node.js must be already installed) 18 | 19 | ```bash 20 | $ npm install 21 | ``` 22 | 23 | ### 2.2. Usage 24 | 25 | Usage is based on BSC. 26 | Before starting, change the name of the file ".env sample" to ".env" and update the information, which is then needed in hardhat.config.js 27 | 28 | 1. After installing the dependencies, first compile and deploy the contract(for BSC) 29 | 30 | ```bash 31 | $ npx hardhat compile 32 | $ npx hardhat run ./scripts/superArbitDeploy.js --network bscmain_bscrpc 33 | ``` 34 | 35 | 2. After contract deployment, update the contract address in config.js(SUPER_ARBIT_ADDRESS) 36 | First we fetch all the swap pairs from available DEX platform pools.Only pairs from pools, that are active in last 7 days(can be changed), are fetched. 37 | This scripts outputs all the available pairs in a json file.(see pairsList.json) 38 | 39 | ```bash 40 | $ npx hardhat run ./scripts/fetchPairs.js --network bscmain_bscrpc 41 | ``` 42 | 43 | 3. Next step is to find all possible routes which starts with pivot token(WBNB) and ends also with the same token with two other tokens in between. 44 | After succesful run, this script outputs the result also in a json file.(see matchedPairs.json) 45 | 46 | ```bash 47 | $ npx hardhat run ./scripts/findMatchedPairs.js --network bscmain_bscrpc 48 | ``` 49 | 50 | 4. In the last step, run the main.js to check arbitrage opportunities as well as execute transactions if they are profitable. 51 | 52 | ```bash 53 | $ npx hardhat run ./scripts/main.js --network bscmain_bscrpc 54 | ``` 55 | 56 | ## Disclaimer 57 | 58 | Use this bot at your own risk! 59 | This bot occasionally finds arbitrage opportunities and execute them. Sometimes it is possible that the transactions are reverted, which can result from many reasons.(For example, a swap transaction is executed before ours, which changes the balances one of the swap pools, so the calculation is not valid anymore). So the bot must be improved in order to catch such situations. 60 | -------------------------------------------------------------------------------- /contracts/Arbitrage/SuperArbit.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IPancakeFactory.sol"; 5 | import "./interfaces/IPancakePair.sol"; 6 | import "./interfaces/IPancakeRouter02.sol"; 7 | import "./interfaces/IERC20.sol"; 8 | 9 | contract SuperArbit { 10 | struct PairInfo { 11 | address pairAddr; 12 | address token0Addr; 13 | address token1Addr; 14 | string token0Symbol; 15 | string token1Symbol; 16 | uint32 lastBlockTimestamp; 17 | uint32 poolId; 18 | } 19 | 20 | struct PairReserve { 21 | uint112 reserve0; 22 | uint112 reserve1; 23 | } 24 | 25 | address owner; 26 | 27 | modifier onlyOwner() { 28 | require(owner == msg.sender, "Ownable: caller is not the owner"); 29 | _; 30 | } 31 | 32 | constructor() { 33 | owner = msg.sender; 34 | } 35 | 36 | function getBatchReserves(address[] calldata pairs) public view onlyOwner returns (PairReserve[] memory) { 37 | PairReserve[] memory pairReserveList = new PairReserve[](pairs.length); 38 | for (uint256 i = 0; i < pairs.length; i++) { 39 | (uint112 reserve0, uint112 reserve1, ) = IPancakePair(pairs[i]).getReserves(); 40 | pairReserveList[i] = PairReserve(reserve0, reserve1); 41 | } 42 | return pairReserveList; 43 | } 44 | 45 | function getContractSize(address _addr) public view returns (uint32) { 46 | uint32 size; 47 | assembly { 48 | size := extcodesize(_addr) 49 | } 50 | return size; 51 | } 52 | 53 | function retrievePairInfo( 54 | address factoryAddr, 55 | uint256 factoryStartIdx, 56 | uint256 numOfPairs 57 | ) public view onlyOwner returns (PairInfo[] memory) { 58 | IPancakeFactory factory = IPancakeFactory(factoryAddr); 59 | uint256 totalNumOfPairs = factory.allPairsLength(); 60 | uint256 availNumOfPairs = ( 61 | factoryStartIdx + numOfPairs > totalNumOfPairs ? totalNumOfPairs - factoryStartIdx : numOfPairs 62 | ); 63 | PairInfo[] memory pairInfoList = new PairInfo[](availNumOfPairs); 64 | for (uint256 i = 0; i < availNumOfPairs; i++) { 65 | uint256 currIdx = factoryStartIdx + i; 66 | address pairAddr = factory.allPairs(currIdx); 67 | IPancakePair pair = IPancakePair(pairAddr); 68 | (, , uint32 blockTs) = pair.getReserves(); 69 | address token0Addr = pair.token0(); 70 | address token1Addr = pair.token1(); 71 | bool success0; 72 | bool success1; 73 | bytes memory result0; 74 | bytes memory result1; 75 | if (getContractSize(token0Addr) != 148 && getContractSize(token1Addr) != 148) { 76 | (success0, result0) = token0Addr.staticcall(abi.encodeWithSignature("symbol()")); 77 | (success1, result1) = token1Addr.staticcall(abi.encodeWithSignature("symbol()")); 78 | } 79 | PairInfo memory pairInfo; 80 | if ((success0 && success1) && (result0.length == 96) && (result1.length == 96)) { 81 | pairInfo = PairInfo( 82 | pairAddr, 83 | token0Addr, 84 | token1Addr, 85 | abi.decode(result0, (string)), 86 | abi.decode(result1, (string)), 87 | blockTs, 88 | uint32(currIdx) 89 | ); 90 | } else { 91 | pairInfo = PairInfo(address(0), address(0), address(0), "", "", 0, uint32(currIdx)); 92 | } 93 | pairInfoList[i] = pairInfo; 94 | } 95 | return pairInfoList; 96 | } 97 | 98 | function SafeTransferFrom( 99 | address token, 100 | address from, 101 | address to, 102 | uint256 value 103 | ) internal { 104 | // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); 105 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); 106 | require(success && (data.length == 0 || abi.decode(data, (bool))), "Safe: transferFrom failed"); 107 | } 108 | 109 | // amounts ->[amountIn, [amount1Out,0] or [0,amount1Out], [amount2Out,0] or [0,amount2Out]...] 110 | function superSwap( 111 | uint256[] memory amounts, 112 | address[] memory pools, 113 | address startToken 114 | ) public onlyOwner { 115 | SafeTransferFrom(startToken, msg.sender, pools[0], amounts[0]); 116 | for (uint256 i; i < pools.length; i++) { 117 | uint256 amount0Out = amounts[i * 2 + 1]; 118 | uint256 amount1Out = amounts[i * 2 + 2]; 119 | address _to = i == pools.length - 1 ? msg.sender : pools[i + 1]; 120 | IPancakePair(pools[i]).swap(amount0Out, amount1Out, _to, new bytes(0)); 121 | } 122 | } 123 | 124 | function superSwapBatch( 125 | uint256[][] memory amountsArr, 126 | address[][] memory poolsArr, 127 | address startToken 128 | ) external onlyOwner returns (bool[] memory) { 129 | require(amountsArr.length == poolsArr.length, "unbalanced"); 130 | uint256 size = amountsArr.length; 131 | bool[] memory results = new bool[](size); 132 | for (uint256 i = 0; i < size; i++) { 133 | (results[i], ) = address(this).delegatecall( 134 | abi.encodeWithSignature("superSwap(uint256[],address[],address)", amountsArr[i], poolsArr[i], startToken) 135 | ); 136 | } 137 | return results; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IERC20 { 5 | event Approval( 6 | address indexed owner, 7 | address indexed spender, 8 | uint256 value 9 | ); 10 | event Transfer(address indexed from, address indexed to, uint256 value); 11 | 12 | function deposit() external payable; 13 | 14 | function name() external view returns (string memory); 15 | 16 | function symbol() external view returns (string memory); 17 | 18 | function decimals() external view returns (uint8); 19 | 20 | function totalSupply() external view returns (uint256); 21 | 22 | function balanceOf(address owner) external view returns (uint256); 23 | 24 | function allowance(address owner, address spender) 25 | external 26 | view 27 | returns (uint256); 28 | 29 | function approve(address spender, uint256 value) external returns (bool); 30 | 31 | function transfer(address to, uint256 value) external returns (bool); 32 | 33 | function transferFrom( 34 | address from, 35 | address to, 36 | uint256 value 37 | ) external returns (bool); 38 | } 39 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakeCallee.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IPancakeCallee { 5 | function pancakeCall( 6 | address sender, 7 | uint256 amount0, 8 | uint256 amount1, 9 | bytes calldata data 10 | ) external; 11 | } 12 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IPancakeERC20 { 5 | event Approval(address indexed owner, address indexed spender, uint256 value); 6 | event Transfer(address indexed from, address indexed to, uint256 value); 7 | 8 | function name() external pure returns (string memory); 9 | 10 | function symbol() external pure returns (string memory); 11 | 12 | function decimals() external pure returns (uint8); 13 | 14 | function totalSupply() external view returns (uint256); 15 | 16 | function balanceOf(address owner) external view returns (uint256); 17 | 18 | function allowance(address owner, address spender) external view returns (uint256); 19 | 20 | function approve(address spender, uint256 value) external returns (bool); 21 | 22 | function transfer(address to, uint256 value) external returns (bool); 23 | 24 | function transferFrom( 25 | address from, 26 | address to, 27 | uint256 value 28 | ) external returns (bool); 29 | 30 | function DOMAIN_SEPARATOR() external view returns (bytes32); 31 | 32 | function PERMIT_TYPEHASH() external pure returns (bytes32); 33 | 34 | function nonces(address owner) external view returns (uint256); 35 | 36 | function permit( 37 | address owner, 38 | address spender, 39 | uint256 value, 40 | uint256 deadline, 41 | uint8 v, 42 | bytes32 r, 43 | bytes32 s 44 | ) external; 45 | } 46 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakeFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IPancakeFactory { 5 | event PairCreated(address indexed token0, address indexed token1, address pair, uint256); 6 | 7 | function feeTo() external view returns (address); 8 | 9 | function feeToSetter() external view returns (address); 10 | 11 | function getPair(address tokenA, address tokenB) external view returns (address pair); 12 | 13 | function allPairs(uint256) external view returns (address pair); 14 | 15 | function allPairsLength() external view returns (uint256); 16 | 17 | function createPair(address tokenA, address tokenB) external returns (address pair); 18 | 19 | function setFeeTo(address) external; 20 | 21 | function setFeeToSetter(address) external; 22 | 23 | function INIT_CODE_PAIR_HASH() external view returns (bytes32); 24 | } 25 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakeMigrator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IPancakeMigrator { 5 | function migrate( 6 | address token, 7 | uint256 amountTokenMin, 8 | uint256 amountETHMin, 9 | address to, 10 | uint256 deadline 11 | ) external; 12 | } 13 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakePair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IPancakePair { 5 | event Approval(address indexed owner, address indexed spender, uint256 value); 6 | event Transfer(address indexed from, address indexed to, uint256 value); 7 | 8 | function name() external pure returns (string memory); 9 | 10 | function symbol() external pure returns (string memory); 11 | 12 | function decimals() external pure returns (uint8); 13 | 14 | function totalSupply() external view returns (uint256); 15 | 16 | function balanceOf(address owner) external view returns (uint256); 17 | 18 | function allowance(address owner, address spender) external view returns (uint256); 19 | 20 | function approve(address spender, uint256 value) external returns (bool); 21 | 22 | function transfer(address to, uint256 value) external returns (bool); 23 | 24 | function transferFrom( 25 | address from, 26 | address to, 27 | uint256 value 28 | ) external returns (bool); 29 | 30 | function DOMAIN_SEPARATOR() external view returns (bytes32); 31 | 32 | function PERMIT_TYPEHASH() external pure returns (bytes32); 33 | 34 | function nonces(address owner) external view returns (uint256); 35 | 36 | function permit( 37 | address owner, 38 | address spender, 39 | uint256 value, 40 | uint256 deadline, 41 | uint8 v, 42 | bytes32 r, 43 | bytes32 s 44 | ) external; 45 | 46 | event Mint(address indexed sender, uint256 amount0, uint256 amount1); 47 | event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); 48 | event Swap( 49 | address indexed sender, 50 | uint256 amount0In, 51 | uint256 amount1In, 52 | uint256 amount0Out, 53 | uint256 amount1Out, 54 | address indexed to 55 | ); 56 | event Sync(uint112 reserve0, uint112 reserve1); 57 | 58 | function MINIMUM_LIQUIDITY() external pure returns (uint256); 59 | 60 | function factory() external view returns (address); 61 | 62 | function token0() external view returns (address); 63 | 64 | function token1() external view returns (address); 65 | 66 | function getReserves() 67 | external 68 | view 69 | returns ( 70 | uint112 reserve0, 71 | uint112 reserve1, 72 | uint32 blockTimestampLast 73 | ); 74 | 75 | function price0CumulativeLast() external view returns (uint256); 76 | 77 | function price1CumulativeLast() external view returns (uint256); 78 | 79 | function kLast() external view returns (uint256); 80 | 81 | function mint(address to) external returns (uint256 liquidity); 82 | 83 | function burn(address to) external returns (uint256 amount0, uint256 amount1); 84 | 85 | function swap( 86 | uint256 amount0Out, 87 | uint256 amount1Out, 88 | address to, 89 | bytes calldata data 90 | ) external; 91 | 92 | function skim(address to) external; 93 | 94 | function sync() external; 95 | 96 | function initialize(address, address) external; 97 | } 98 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakeRouter01.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.6.2; 3 | 4 | interface IPancakeRouter01 { 5 | function factory() external pure returns (address); 6 | 7 | function WETH() external pure returns (address); 8 | 9 | function addLiquidity( 10 | address tokenA, 11 | address tokenB, 12 | uint256 amountADesired, 13 | uint256 amountBDesired, 14 | uint256 amountAMin, 15 | uint256 amountBMin, 16 | address to, 17 | uint256 deadline 18 | ) 19 | external 20 | returns ( 21 | uint256 amountA, 22 | uint256 amountB, 23 | uint256 liquidity 24 | ); 25 | 26 | function addLiquidityETH( 27 | address token, 28 | uint256 amountTokenDesired, 29 | uint256 amountTokenMin, 30 | uint256 amountETHMin, 31 | address to, 32 | uint256 deadline 33 | ) 34 | external 35 | payable 36 | returns ( 37 | uint256 amountToken, 38 | uint256 amountETH, 39 | uint256 liquidity 40 | ); 41 | 42 | function removeLiquidity( 43 | address tokenA, 44 | address tokenB, 45 | uint256 liquidity, 46 | uint256 amountAMin, 47 | uint256 amountBMin, 48 | address to, 49 | uint256 deadline 50 | ) external returns (uint256 amountA, uint256 amountB); 51 | 52 | function removeLiquidityETH( 53 | address token, 54 | uint256 liquidity, 55 | uint256 amountTokenMin, 56 | uint256 amountETHMin, 57 | address to, 58 | uint256 deadline 59 | ) external returns (uint256 amountToken, uint256 amountETH); 60 | 61 | function removeLiquidityWithPermit( 62 | address tokenA, 63 | address tokenB, 64 | uint256 liquidity, 65 | uint256 amountAMin, 66 | uint256 amountBMin, 67 | address to, 68 | uint256 deadline, 69 | bool approveMax, 70 | uint8 v, 71 | bytes32 r, 72 | bytes32 s 73 | ) external returns (uint256 amountA, uint256 amountB); 74 | 75 | function removeLiquidityETHWithPermit( 76 | address token, 77 | uint256 liquidity, 78 | uint256 amountTokenMin, 79 | uint256 amountETHMin, 80 | address to, 81 | uint256 deadline, 82 | bool approveMax, 83 | uint8 v, 84 | bytes32 r, 85 | bytes32 s 86 | ) external returns (uint256 amountToken, uint256 amountETH); 87 | 88 | function swapExactTokensForTokens( 89 | uint256 amountIn, 90 | uint256 amountOutMin, 91 | address[] calldata path, 92 | address to, 93 | uint256 deadline 94 | ) external returns (uint256[] memory amounts); 95 | 96 | function swapTokensForExactTokens( 97 | uint256 amountOut, 98 | uint256 amountInMax, 99 | address[] calldata path, 100 | address to, 101 | uint256 deadline 102 | ) external returns (uint256[] memory amounts); 103 | 104 | function swapExactETHForTokens( 105 | uint256 amountOutMin, 106 | address[] calldata path, 107 | address to, 108 | uint256 deadline 109 | ) external payable returns (uint256[] memory amounts); 110 | 111 | function swapTokensForExactETH( 112 | uint256 amountOut, 113 | uint256 amountInMax, 114 | address[] calldata path, 115 | address to, 116 | uint256 deadline 117 | ) external returns (uint256[] memory amounts); 118 | 119 | function swapExactTokensForETH( 120 | uint256 amountIn, 121 | uint256 amountOutMin, 122 | address[] calldata path, 123 | address to, 124 | uint256 deadline 125 | ) external returns (uint256[] memory amounts); 126 | 127 | function swapETHForExactTokens( 128 | uint256 amountOut, 129 | address[] calldata path, 130 | address to, 131 | uint256 deadline 132 | ) external payable returns (uint256[] memory amounts); 133 | 134 | function quote( 135 | uint256 amountA, 136 | uint256 reserveA, 137 | uint256 reserveB 138 | ) external pure returns (uint256 amountB); 139 | 140 | function getAmountOut( 141 | uint256 amountIn, 142 | uint256 reserveIn, 143 | uint256 reserveOut 144 | ) external pure returns (uint256 amountOut); 145 | 146 | function getAmountIn( 147 | uint256 amountOut, 148 | uint256 reserveIn, 149 | uint256 reserveOut 150 | ) external pure returns (uint256 amountIn); 151 | 152 | function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); 153 | 154 | function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); 155 | } 156 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IPancakeRouter02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.6.2; 3 | 4 | import "./IPancakeRouter01.sol"; 5 | 6 | interface IPancakeRouter02 is IPancakeRouter01 { 7 | function removeLiquidityETHSupportingFeeOnTransferTokens( 8 | address token, 9 | uint256 liquidity, 10 | uint256 amountTokenMin, 11 | uint256 amountETHMin, 12 | address to, 13 | uint256 deadline 14 | ) external returns (uint256 amountETH); 15 | 16 | function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( 17 | address token, 18 | uint256 liquidity, 19 | uint256 amountTokenMin, 20 | uint256 amountETHMin, 21 | address to, 22 | uint256 deadline, 23 | bool approveMax, 24 | uint8 v, 25 | bytes32 r, 26 | bytes32 s 27 | ) external returns (uint256 amountETH); 28 | 29 | function swapExactTokensForTokensSupportingFeeOnTransferTokens( 30 | uint256 amountIn, 31 | uint256 amountOutMin, 32 | address[] calldata path, 33 | address to, 34 | uint256 deadline 35 | ) external; 36 | 37 | function swapExactETHForTokensSupportingFeeOnTransferTokens( 38 | uint256 amountOutMin, 39 | address[] calldata path, 40 | address to, 41 | uint256 deadline 42 | ) external payable; 43 | 44 | function swapExactTokensForETHSupportingFeeOnTransferTokens( 45 | uint256 amountIn, 46 | uint256 amountOutMin, 47 | address[] calldata path, 48 | address to, 49 | uint256 deadline 50 | ) external; 51 | } 52 | -------------------------------------------------------------------------------- /contracts/Arbitrage/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.5.0; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function transfer(address to, uint256 value) external returns (bool); 8 | 9 | function withdraw(uint256) external; 10 | 11 | function approve(address spender, uint256 value) external returns (bool); 12 | 13 | function balanceOf(address owner) external view returns (uint256); 14 | } 15 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-waffle"); 2 | require("@nomiclabs/hardhat-etherscan"); 3 | require("dotenv").config(); 4 | 5 | /** 6 | * @type import('hardhat/config').HardhatUserConfig 7 | */ 8 | 9 | module.exports = { 10 | defaultNetwork: "hardhat", 11 | networks: { 12 | hardhat: { 13 | forking: { 14 | url: `https://speedy-nodes-nyc.moralis.io/${process.env.MORALIS_SPEEDY_API_KEY}/bsc/mainnet/archive`, 15 | blockNumber: 17360588, 16 | }, 17 | }, 18 | rinkeby: { 19 | url: `https://rinkeby.infura.io/v3/${process.env.WEB3_INFURA_PROJECT_ID}`, 20 | accounts: [process.env.PRIV_KEY], 21 | }, 22 | bscmain_bscrpc: { 23 | url: `https://bscrpc.com`, 24 | accounts: [process.env.PRIV_KEY], 25 | }, 26 | bscmain_quicknode: { 27 | url: `https://weathered-damp-forest.bsc.quiknode.pro/${process.env.QUICKNODE_KEY}/`, 28 | accounts: [process.env.PRIV_KEY], 29 | }, 30 | }, 31 | etherscan: { 32 | apiKey: process.env.ETHERSCAN_TOKEN, 33 | }, 34 | solidity: "0.8.4", 35 | }; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial", 3 | "version": "1.0.0", 4 | "description": "tbd", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@nomiclabs/hardhat-ethers": "^2.0.5", 13 | "@nomiclabs/hardhat-etherscan": "^3.0.3", 14 | "@nomiclabs/hardhat-solhint": "^2.0.0", 15 | "@nomiclabs/hardhat-waffle": "^2.0.3", 16 | "chai": "^4.3.6", 17 | "dotenv": "^16.0.0", 18 | "ethereum-waffle": "^3.4.4", 19 | "ethers": "^5.6.2", 20 | "hardhat": "^2.9.2", 21 | "objects-to-csv": "^1.3.6", 22 | "prettier-plugin-solidity": "^1.0.0-beta.18" 23 | }, 24 | "dependencies": { 25 | "package.json": "^2.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scripts/arbUtils.js: -------------------------------------------------------------------------------- 1 | const { bnSqrt, saveJSONToFile, saveObjToCsv, readFromJSONFile } = require("./helpers"); 2 | const { ethers, network } = require("hardhat"); 3 | const hre = require("hardhat"); 4 | const { recoverAddress } = require("ethers/lib/utils"); 5 | const { PIVOT_TOKEN } = require("./config"); 6 | 7 | const generateTriads = function (matchPairFile) { 8 | const matchPairList = readFromJSONFile(matchPairFile); 9 | const tokenTriads = []; 10 | for (let match of matchPairList) { 11 | let firstToken = Object.keys(match)[0]; 12 | for (let i in match[firstToken]) { 13 | let triadObj = {}; 14 | triadObj.path = [PIVOT_TOKEN, firstToken, match[firstToken][i], PIVOT_TOKEN]; 15 | triadObj.pools = [match.startPool, match.middlePools[i], match.endPools[i]]; 16 | tokenTriads.push(triadObj); 17 | } 18 | } 19 | return tokenTriads; 20 | }; 21 | 22 | const addPairReserves = async function (triads, superArbit, batchSize) { 23 | const pairAddrList = triads.map((e) => e.pools).flat(); 24 | let pairReserveList = []; 25 | const numOfPairs = pairAddrList.length; 26 | const loopLim = Math.floor(numOfPairs / batchSize); 27 | let i = 0; 28 | let pairAddrBatch; 29 | while (i <= loopLim) { 30 | if (i != loopLim) { 31 | pairAddrBatch = pairAddrList.slice(i * batchSize, (i + 1) * batchSize); 32 | } else { 33 | pairAddrBatch = pairAddrList.slice(i * batchSize, i * batchSize + (numOfPairs % batchSize)); 34 | } 35 | try { 36 | pairReserveList = pairReserveList.concat(await superArbit.getBatchReserves(pairAddrBatch)); 37 | i++; 38 | } catch (error) { 39 | console.log("Trying again step ", i); 40 | } 41 | } 42 | const triadsWithRes = []; 43 | for (const [index, triad] of triads.entries()) { 44 | let numLegs = triad.pools.length; 45 | triad.reserves = pairReserveList.slice(index * numLegs, (index + 1) * numLegs); 46 | triadsWithRes.push(triad); 47 | } 48 | return triadsWithRes; 49 | }; 50 | 51 | // params 52 | const RATIO_SCALE_FACT = 100000; 53 | const RATIO_SCALE_FACT_BN = ethers.BigNumber.from(RATIO_SCALE_FACT); 54 | const MAX_RATIO_LIM = ethers.BigNumber.from(1.5 * RATIO_SCALE_FACT); 55 | const R1 = ethers.BigNumber.from(0.9969 * RATIO_SCALE_FACT); // %0,3 input fee 56 | const R2 = ethers.BigNumber.from(1 * RATIO_SCALE_FACT); // %0 output fee 57 | const APPROX_GAS_FEE = ethers.BigNumber.from("1250000000000000"); //("1250000000000000"); //250000 gas * 5 gwei per gas 58 | 59 | const getAmountsOut = function (amountIn, reserves, r1, ratioScaleFact) { 60 | const amounts = []; 61 | let amountInTemp = amountIn; 62 | for (const reserve of reserves) { 63 | amountInTemp = getAmountOut(amountInTemp, reserve[0], reserve[1], r1, ratioScaleFact); 64 | amounts.push(amountInTemp); 65 | } 66 | return amounts; 67 | }; 68 | 69 | const getAmountOut = function (amountIn, res0, res1, r1, ratioScaleFact) { 70 | return amountIn 71 | .mul(r1) 72 | .mul(res1) 73 | .div(res0.mul(ratioScaleFact).add(amountIn.mul(r1))); 74 | }; 75 | 76 | const calculateProfit = function (triadsWithRes) { 77 | lucrPaths = []; 78 | for (const triad of triadsWithRes) { 79 | let reserves = []; 80 | for (const [i, pool] of triad.pools.entries()) { 81 | const resPool = triad.reserves[i]; 82 | let resFirst; 83 | let resSecond; 84 | if (triad.path[i].toLowerCase() < triad.path[i + 1].toLowerCase()) reserves.push([resPool.reserve0, resPool.reserve1]); 85 | else reserves.push([resPool.reserve1, resPool.reserve0]); 86 | } 87 | let res = calcRatio(reserves, R1, R2); 88 | if (res.ratio != undefined) { 89 | if (res.reverse) { 90 | triad.path.reverse(); 91 | triad.pools.reverse(); 92 | // reverse the reserves array for backward trades 93 | reserves.reverse(); 94 | reserves.map((r) => r.reverse()); 95 | } 96 | const { optAmountIn, amountOut } = calcOptiAmountIn(reserves, R1, R2); 97 | const expectedProfit = amountOut.sub(optAmountIn).sub(APPROX_GAS_FEE); 98 | // populate only profitable triads 99 | if (expectedProfit.gt(0)) { 100 | triad.reserves = reserves.map((rs) => rs.map((r) => r.toString())); 101 | swapAmounts = getAmountsOut(optAmountIn, reserves, R1, RATIO_SCALE_FACT_BN); 102 | triad.swapAmounts = swapAmounts.map((s) => s.toString()); 103 | triad.ratio = res.ratio.toNumber() / RATIO_SCALE_FACT; 104 | triad.optimumAmountInBN = optAmountIn.toString(); 105 | triad.AmountOutBN = amountOut.toString(); 106 | triad.expectedProfitBN = expectedProfit.toString(); 107 | if (optAmountIn.eq(0)) triad.realRatio = NaN; 108 | else triad.realRatio = amountOut.sub(optAmountIn).mul(10000).div(optAmountIn).toNumber() / 10000; 109 | triad.optimumAmountIn = parseFloat(ethers.utils.formatEther(optAmountIn)); 110 | triad.AmountOut = parseFloat(ethers.utils.formatEther(amountOut)); 111 | triad.expectedProfit = parseFloat(ethers.utils.formatEther(expectedProfit)); 112 | lucrPaths.push(triad); 113 | } 114 | } 115 | } 116 | return lucrPaths; 117 | }; 118 | 119 | function calcOptiAmountIn(reserves, r1, r2) { 120 | // straight implementation for triangular case 121 | // use loop version for cyclic arb. with more legs 122 | // "Cyclic Arbitrage in Decentralized Exchanges - Wang,Chen, Zhou" 123 | const a12 = reserves[0][0]; 124 | const a21 = reserves[0][1]; 125 | const a23 = reserves[1][0]; 126 | const a32 = reserves[1][1]; 127 | const a31 = reserves[2][0]; 128 | const a13 = reserves[2][1]; 129 | 130 | const a_13 = a12.mul(a23).div(a23.add(r1.mul(r2).mul(a21).div(RATIO_SCALE_FACT_BN.pow(2)))); 131 | const a_31 = a21 132 | .mul(a32) 133 | .mul(r1) 134 | .mul(r2) 135 | .div(RATIO_SCALE_FACT_BN.pow(2).mul(a23.add(r1.mul(r2).mul(a21).div(RATIO_SCALE_FACT_BN.pow(2))))); 136 | const a = a_13.mul(a31).div(a31.add(r1.mul(r2).mul(a_31).div(RATIO_SCALE_FACT_BN.pow(2)))); 137 | const a_ = r1 138 | .mul(r2) 139 | .mul(a13) 140 | .mul(a_31) 141 | .div(RATIO_SCALE_FACT_BN.pow(2).mul(a31.add(r1.mul(r2).mul(a_31).div(RATIO_SCALE_FACT_BN.pow(2))))); 142 | const optAmountIn = bnSqrt(r1.mul(r2).mul(a_).mul(a).div(RATIO_SCALE_FACT_BN.pow(2))) 143 | .sub(a) 144 | .mul(RATIO_SCALE_FACT_BN) 145 | .div(r1); 146 | // calculate achievable amountOut 147 | let amountOut; 148 | let amountIn = optAmountIn; 149 | reserves.forEach((r) => { 150 | amountOut = r1 151 | .mul(r2) 152 | .mul(r[1]) 153 | .mul(amountIn) 154 | .div(RATIO_SCALE_FACT_BN.pow(2)) 155 | .div(r[0].add(r1.mul(amountIn).div(RATIO_SCALE_FACT_BN))); 156 | amountIn = amountOut; 157 | }); 158 | return { optAmountIn, amountOut }; 159 | } 160 | 161 | function calcRatio(reserves, r1, r2) { 162 | let result = { ratio: undefined, reverse: undefined }; 163 | try { 164 | const feeRatio = RATIO_SCALE_FACT_BN.pow(7).div(r1.pow(3)).div(r2.pow(3)); 165 | const num = reserves[0][1].mul(reserves[1][1]).mul(reserves[2][1]); 166 | const den = reserves[0][0].mul(reserves[1][0]).mul(reserves[2][0]); 167 | const forwardRatio = num.mul(RATIO_SCALE_FACT).div(den); 168 | const reverseRatio = den.mul(RATIO_SCALE_FACT).div(num); 169 | if (forwardRatio.gt(RATIO_SCALE_FACT_BN) && forwardRatio.lt(MAX_RATIO_LIM) && forwardRatio.gt(feeRatio)) { 170 | result = { ratio: forwardRatio.sub(feeRatio), reverse: false }; 171 | } else if (reverseRatio.gt(RATIO_SCALE_FACT_BN) && reverseRatio.lt(MAX_RATIO_LIM) && reverseRatio.gt(feeRatio)) { 172 | result = { ratio: reverseRatio.sub(feeRatio), reverse: true }; 173 | } 174 | } catch (error) {} 175 | 176 | return result; 177 | } 178 | 179 | exports.generateTriads = generateTriads; 180 | exports.addPairReserves = addPairReserves; 181 | exports.calculateProfit = calculateProfit; 182 | exports.APPROX_GAS_FEE = APPROX_GAS_FEE; 183 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SUPER_ARBIT_ADDRESS: "", // BSC Mainnet address of arbitrage contract(SuperArbit.sol) 3 | FACTORY_ADDRESSES: { 4 | // Factory contract addresses of chosen DEX'es 5 | pancake: "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", 6 | biswap: "0x858E3312ed3A876947EA49d572A7C42DE08af7EE", 7 | nomiswap: "0xd6715A8be3944ec72738F0BFDC739d48C3c29349", 8 | mdex: "0x3CD1C46068dAEa5Ebb0d3f55F6915B10648062B8", 9 | babyswap: "0x86407bEa2078ea5f5EB5A52B2caA963bC1F889Da", 10 | safeswap: "0x4d05D0045df5562D6D52937e93De6Ec1FECDAd21", 11 | moonlift: "0xe9cABbC746C03010020Fd093cD666e40823E0D87", 12 | tendieswap: "0xb5b4aE9413dFD4d1489350dCA09B1aE6B76BD3a8", 13 | apeswap: "0x0841BD0B734E4F5853f0dD8d7Ea041c241fb0Da6", 14 | pandaswap: "0x9Ad32bf5DaFe152Cbe027398219611DB4E8753B3", 15 | teddyswap: "0x8A01D7F2e171c222372F0962BEA84b8EB5a3368E", 16 | kingkongswap: "0x3F0525D90bEBC8c1B7C5bE7C8FCb22E7B5e656c7", 17 | gibxswap: "0x97bCD9BB482144291D77ee53bFa99317A82066E8", 18 | }, 19 | PIVOT_TOKEN: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // WBNB -> Wrapped BNB on BSC 20 | PAIRLIST_OUTPUT_FILE: "./pairsList.json", 21 | MATCHED_PAIRS_OUTPUT_FILE: "./matchedPairs.json", 22 | MAX_GAS: 2000000, 23 | BSC_GAS_PRICE: 5000000000, 24 | MAX_TRADE_INPUT: 10, // WBNB 25 | }; 26 | -------------------------------------------------------------------------------- /scripts/fetchPairs.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | const { saveJSONToFile, saveObjToCsv, readFromJSONFile } = require("./helpers"); 3 | const { SUPER_ARBIT_ADDRESS, FACTORY_ADDRESSES, PAIRLIST_OUTPUT_FILE } = require("./config"); 4 | 5 | /* SuperArbit contract must be already deployed, because this script fetches batch data 6 | from blockchain using the smart contract */ 7 | 8 | const QUERY_STEP = 500; 9 | const MAX_DAYS_OLD = 7; // The pool must be active latest 7 days ago in order to be added into results 10 | 11 | const fetchData = async () => { 12 | const superArbit = await hre.ethers.getContractAt("SuperArbit", SUPER_ARBIT_ADDRESS); 13 | console.log(`Contract deployed at ${superArbit.address}`); 14 | const pairsArr = []; 15 | const timeLimit = Math.floor(Date.now() / 1000) - MAX_DAYS_OLD * 24 * 60 * 60; 16 | try { 17 | for (key of Object.keys(FACTORY_ADDRESSES)) { 18 | const factoryAddr = FACTORY_ADDRESSES[key]; 19 | const swapFactory = await hre.ethers.getContractAt("IPancakeFactory", factoryAddr); 20 | const totalNumOfPairs = await swapFactory.allPairsLength(); 21 | const loopLim = Math.floor(totalNumOfPairs.toNumber() / QUERY_STEP) + 1; 22 | console.log(`Factory: ${key}`); 23 | console.log(`Total Number of Pairs: ${totalNumOfPairs}`); 24 | console.log(`Loop limit: ${loopLim}\n`); 25 | for (let i = 0; i < loopLim; i++) { 26 | try { 27 | console.log(`Querying pairs from index ${i * QUERY_STEP} to ${(i + 1) * QUERY_STEP}...`); 28 | let data = await superArbit.retrievePairInfo(factoryAddr, i * QUERY_STEP, QUERY_STEP); 29 | data.forEach((e) => { 30 | if (e.lastBlockTimestamp >= timeLimit) { 31 | pairsArr.push({ 32 | fromFactory: key, 33 | pairAddress: e.pairAddr, 34 | token0Address: e.token0Addr, 35 | token1Address: e.token1Addr, 36 | token0Symbol: e.token0Symbol, 37 | token1Symbol: e.token1Symbol, 38 | lastActivity: e.lastBlockTimestamp, 39 | poolId: e.poolId, 40 | }); 41 | } 42 | }); 43 | } catch (error) { 44 | console.log("Timeout.Trying again..."); 45 | i--; 46 | } 47 | } 48 | } 49 | } catch (error) { 50 | console.log("Call Exception Error, aborting..."); 51 | console.log(error); 52 | } finally { 53 | // write JSON string to a file 54 | saveJSONToFile(PAIRLIST_OUTPUT_FILE, pairsArr); 55 | console.log("JSON file is created."); 56 | } 57 | }; 58 | 59 | fetchData() 60 | .then(() => process.exit(0)) 61 | .catch((error) => { 62 | console.error(error); 63 | process.exit(1); 64 | }); 65 | -------------------------------------------------------------------------------- /scripts/findMatchedPairs.js: -------------------------------------------------------------------------------- 1 | const { bnSqrt, saveJSONToFile, saveObjToCsv, readFromJSONFile } = require("./helpers"); 2 | const hre = require("hardhat"); 3 | const { PIVOT_TOKEN, PAIRLIST_OUTPUT_FILE, MATCHED_PAIRS_OUTPUT_FILE } = require("./config"); 4 | 5 | // helpers 6 | function getPairsOtherToken(pair, firstToken) { 7 | if (pair["token0Address"] == firstToken) return pair["token1Address"]; 8 | else if (pair["token1Address"] == firstToken) return pair["token0Address"]; 9 | else return ""; 10 | } 11 | 12 | function isTokenInPair(pair, token) { 13 | return pair["token0Address"] == token || pair["token1Address"] == token; 14 | } 15 | 16 | function printProgress(curr, total) { 17 | process.stdout.clearLine(); 18 | process.stdout.cursorTo(0); 19 | process.stdout.write(`${curr.toString()} of total ${total.toString()}`); 20 | } 21 | 22 | pairsArr = readFromJSONFile(PAIRLIST_OUTPUT_FILE); 23 | const pivotPairs = pairsArr.filter((pair) => isTokenInPair(pair, PIVOT_TOKEN)); 24 | const pivotPairTokens = new Set(pivotPairs.map((p) => getPairsOtherToken(p, PIVOT_TOKEN))); 25 | 26 | const otherPairs = pairsArr.filter((pair) => !isTokenInPair(pair, PIVOT_TOKEN)); 27 | console.log(`Total number of pivot pairs: ${pivotPairs.length}`); 28 | console.log(`Total number of other pairs: ${otherPairs.length}`); 29 | 30 | const matchPairs = []; 31 | const includedPairs = new Set(); 32 | const lenPivotPairs = pivotPairs.length; 33 | 34 | for (let [index, pivotPair] of pivotPairs.entries()) { 35 | printProgress(index + 1, lenPivotPairs); 36 | let firstToken = getPairsOtherToken(pivotPair, PIVOT_TOKEN); 37 | let pathPairs = ""; 38 | let matchObj = {}; 39 | matchObj[firstToken] = []; 40 | matchObj.startPool = pivotPair["pairAddress"]; 41 | matchObj.middlePools = []; 42 | matchObj.endPools = []; 43 | for (let otherPair of otherPairs) { 44 | let secondToken = getPairsOtherToken(otherPair, firstToken); 45 | if (secondToken != "" && pivotPairTokens.has(secondToken)) { 46 | for (endPair of pivotPairs) { 47 | let otherToken = getPairsOtherToken(endPair, PIVOT_TOKEN); 48 | if (otherToken == secondToken) { 49 | const pathPairs = [ 50 | pivotPair["pairAddress"].toLowerCase(), 51 | otherPair["pairAddress"].toLowerCase(), 52 | endPair["pairAddress"].toLowerCase(), 53 | ]; 54 | const pathPairsJoined = pathPairs.sort().join(); 55 | if (!includedPairs.has(pathPairsJoined)) { 56 | matchObj[firstToken].push(secondToken); 57 | matchObj.middlePools.push(otherPair["pairAddress"]); 58 | matchObj.endPools.push(endPair["pairAddress"]); 59 | includedPairs.add(pathPairsJoined); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | if (matchObj[firstToken].length > 0) { 66 | matchPairs.push(matchObj); 67 | } 68 | } 69 | saveJSONToFile(MATCHED_PAIRS_OUTPUT_FILE, matchPairs); 70 | -------------------------------------------------------------------------------- /scripts/helpers.js: -------------------------------------------------------------------------------- 1 | const ethers = require("ethers"); 2 | const fs = require("fs"); 3 | const ObjectsToCsv = require("objects-to-csv"); 4 | 5 | const bnSqrt = (value) => { 6 | const ONE = ethers.BigNumber.from(1); 7 | const TWO = ethers.BigNumber.from(2); 8 | let x = ethers.BigNumber.from(value); 9 | let z = x.add(ONE).div(TWO); 10 | let y = x; 11 | while (z.sub(y).isNegative()) { 12 | y = z; 13 | z = x.div(z).add(z).div(TWO); 14 | } 15 | return y; 16 | }; 17 | 18 | const saveJSONToFile = (filepath, data) => { 19 | fs.writeFileSync(filepath, JSON.stringify(data, null, 2), (err) => { 20 | if (err) { 21 | throw err; 22 | } 23 | }); 24 | }; 25 | 26 | const saveObjToCsv = async (obj, filepath) => { 27 | const csv = new ObjectsToCsv(obj); 28 | await csv.toDisk(filepath); 29 | console.log("CSV file is created. Path: ", filepath); 30 | }; 31 | 32 | const readFromJSONFile = (filepath) => 33 | JSON.parse(fs.readFileSync(filepath, "utf8")); 34 | 35 | exports.bnSqrt = bnSqrt; 36 | exports.saveJSONToFile = saveJSONToFile; 37 | exports.saveObjToCsv = saveObjToCsv; 38 | exports.readFromJSONFile = readFromJSONFile; 39 | -------------------------------------------------------------------------------- /scripts/main.js: -------------------------------------------------------------------------------- 1 | const { generateTriads, addPairReserves, calculateProfit, APPROX_GAS_FEE } = require("./arbUtils"); 2 | const hre = require("hardhat"); 3 | const { ethers, network } = require("hardhat"); 4 | const { 5 | PIVOT_TOKEN, 6 | SUPER_ARBIT_ADDRESS, 7 | MATCHED_PAIRS_OUTPUT_FILE, 8 | MAX_GAS, 9 | BSC_GAS_PRICE, 10 | MAX_TRADE_INPUT, 11 | } = require("./config"); 12 | 13 | let execCount = 0; // Delete later... 14 | 15 | const checkProfitAndExecute = async function (lucrPaths, router, signer) { 16 | console.log("Static batch check starts..."); 17 | const startToken = PIVOT_TOKEN; 18 | for (const lucrPath of lucrPaths) { 19 | const pools = lucrPath.pools; 20 | amounts = [lucrPath.optimumAmountInBN]; 21 | for (let i = 0; i < lucrPath.path.length - 1; i++) { 22 | if (lucrPath.path[i].toLowerCase() < lucrPath.path[i + 1].toLowerCase()) { 23 | amounts.push("0"); 24 | amounts.push(lucrPath.swapAmounts[i]); 25 | } else { 26 | amounts.push(lucrPath.swapAmounts[i]); 27 | amounts.push("0"); 28 | } 29 | lucrPath.execAmounts = amounts; 30 | lucrPath.execPools = pools; 31 | } 32 | } 33 | const amountsArr = lucrPaths.map((l) => l.execAmounts); 34 | const poolsArr = lucrPaths.map((l) => l.execPools); 35 | let result = []; 36 | try { 37 | result = await router.callStatic.superSwapBatch(amountsArr, poolsArr, startToken, { gasLimit: MAX_GAS * 10 }); 38 | } catch (error) { 39 | console.log(`reason:${error.reason}`); 40 | } 41 | const lucrPathsPassed = lucrPaths.filter((l, index) => result[index]); 42 | // execute! 43 | console.log("Number of triads, which passed static check: ", lucrPathsPassed.length); 44 | for (const path of lucrPathsPassed) { 45 | path.gas = "0"; 46 | if (parseFloat(path.optimumAmountIn) < MAX_TRADE_INPUT) { 47 | console.log("Amount In= ", path.optimumAmountIn); 48 | try { 49 | let gas = await router.estimateGas.superSwap(path.execAmounts, path.execPools, startToken); 50 | console.log("Gas(static) used: ", gas); 51 | path.gas = gas.toString(); 52 | const newProfit = ethers.BigNumber.from(path.expectedProfitBN) 53 | .sub(gas.mul(BSC_GAS_PRICE.toString())) 54 | .add(APPROX_GAS_FEE); 55 | console.log("New Profit", parseFloat(ethers.utils.formatEther(newProfit))); 56 | if (newProfit.gt(0)) { 57 | await router.callStatic.superSwap(path.execAmounts, path.execPools, startToken, { gasLimit: MAX_GAS }); 58 | router.superSwap(path.execAmounts, path.execPools, startToken, { gasLimit: MAX_GAS }); 59 | console.log("!!!!EXECUTED!!!"); 60 | execCount++; 61 | } 62 | } catch (error) { 63 | console.log(error.reason); 64 | } 65 | } 66 | } 67 | return lucrPathsPassed; 68 | }; 69 | 70 | const main = async () => { 71 | // ---connect to router and other stuff, reorg later--- 72 | const router = await ethers.getContractAt("SuperArbit", SUPER_ARBIT_ADDRESS); 73 | const signer = await ethers.getSigner(); 74 | let triads = generateTriads(MATCHED_PAIRS_OUTPUT_FILE); // generate triads with pivot token -> WBNB 75 | let allLucrPathsPassed = []; 76 | while (true) { 77 | const stepSize = 333; 78 | const numOfTriads = triads.length; 79 | const loopLim = Math.floor(numOfTriads / stepSize); 80 | console.log(`\nNumber of Triads from JSON:${numOfTriads}, Total number of batches:${loopLim}\n`); 81 | let i = 0; 82 | let triadsSliced; 83 | 84 | while (i <= loopLim) { 85 | console.log(`Processing batch ${i + 1} of total ${loopLim}`); 86 | if (i != loopLim) { 87 | triadsSliced = triads.slice(i * stepSize, (i + 1) * stepSize); 88 | } else { 89 | triadsSliced = triads.slice(i * stepSize, i * stepSize + (numOfTriads % stepSize)); 90 | } 91 | const triadsWithRes = await addPairReserves(triadsSliced, router, (batchSize = stepSize * 3)); 92 | const lucrPaths = calculateProfit(triadsWithRes); 93 | console.log("Length of lucrative triads in current batch:", lucrPaths.length); 94 | //------------------------------------- 95 | //--Here comes the check/execute stuff 96 | const lucrPathsPassed = await checkProfitAndExecute(lucrPaths, router, signer); 97 | if (lucrPathsPassed.length > 0) allLucrPathsPassed = allLucrPathsPassed.concat(lucrPathsPassed); 98 | console.log("Length all lucrative paths passed: ", allLucrPathsPassed.length); 99 | console.log(`-------Total number of executions: ${execCount}\n`); 100 | i++; 101 | } 102 | } 103 | }; 104 | 105 | main().then(); 106 | -------------------------------------------------------------------------------- /scripts/superArbitDeploy.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | const hre = require("hardhat"); 3 | const { PIVOT_TOKEN } = require("./config"); 4 | 5 | async function deploy() { 6 | // Get the contract to deploy 7 | const SuperArbit = await hre.ethers.getContractFactory("SuperArbit"); 8 | const superArbit = await SuperArbit.deploy(); 9 | 10 | await superArbit.deployed(); 11 | console.log("SuperArbit deployed to:", superArbit.address); 12 | 13 | // Approve super arbit contract to swap WBNB 14 | const wbnbToken = await ethers.getContractAt("IERC20", PIVOT_TOKEN); 15 | await wbnbToken.approve(superArbit.address, "0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); 16 | } 17 | 18 | deploy() 19 | .then(() => process.exit(0)) 20 | .catch((error) => { 21 | console.error(error); 22 | process.exit(1); 23 | }); 24 | --------------------------------------------------------------------------------