├── .gitignore
├── images
├── Execution.PNG
├── TXResult.PNG
├── summary.jpg
└── Deployment.PNG
├── LICENSE
├── FlashArbitragerV2.sol
├── README.md
└── FlashArbitrageBot.sol
/.gitignore:
--------------------------------------------------------------------------------
1 | .gitattributes
--------------------------------------------------------------------------------
/images/Execution.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelinfosec/flash-arb-bot/HEAD/images/Execution.PNG
--------------------------------------------------------------------------------
/images/TXResult.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelinfosec/flash-arb-bot/HEAD/images/TXResult.PNG
--------------------------------------------------------------------------------
/images/summary.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelinfosec/flash-arb-bot/HEAD/images/summary.jpg
--------------------------------------------------------------------------------
/images/Deployment.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelinfosec/flash-arb-bot/HEAD/images/Deployment.PNG
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Fiona Kobayashi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/FlashArbitragerV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity 0.8.4;
3 |
4 | /**
5 | Ropsten instances:
6 | - Uniswap V2 Router: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
7 | - Sushiswap V1 Router: No official sushi routers on testnet
8 | - DAI: 0xf80A32A835F79D7787E8a8ee5721D0fEaFd78108
9 | - ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
10 | - Aave LendingPoolAddressesProvider: 0x1c8756FD2B28e9426CDBDcC7E3c4d64fa9A54728
11 |
12 | Mainnet instances:
13 | - Uniswap V2 Router: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
14 | - Sushiswap V1 Router: 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F
15 | - DAI: 0x6B175474E89094C44Da98b954EedeAC495271d0F
16 | - ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
17 | - Aave LendingPoolAddressesProvider: 0x24a42fD28C976A61Df5D00D0599C34c4f90748c8
18 | */
19 |
20 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
21 | import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';
22 |
23 | import './interfaces/IERC3156FlashBorrower.sol';
24 | import './interfaces/IERC3156FlashLender.sol';
25 |
26 | contract ArbitragerV2 is IERC3156FlashBorrower {
27 |
28 | enum Direction {
29 | UNISWAP_SUSHISWAP,
30 | SUSHISWAP_UNISWAP
31 | }
32 |
33 | struct ExtraData {
34 | address swapToken;
35 | Direction direction;
36 | uint256 deadline;
37 | uint256 amountRequired;
38 | address profitReceiver;
39 | uint256 minAmountSwapToken;
40 | uint256 minAmountBorrowedToken;
41 | }
42 |
43 | IERC3156FlashLender public immutable lender;
44 | IUniswapV2Router02 public immutable uniswapRouter;
45 | IUniswapV2Router02 public immutable sushiswapRouter;
46 |
47 | constructor(
48 | IERC3156FlashLender _lender,
49 | IUniswapV2Router02 _uniswapRouter,
50 | IUniswapV2Router02 _sushiswapRouter
51 | ) {
52 | lender = _lender;
53 | uniswapRouter = _uniswapRouter;
54 | sushiswapRouter = _sushiswapRouter;
55 | }
56 |
57 | function onFlashLoan(
58 | address initiator,
59 | address borrowedToken,
60 | uint256 amount,
61 | uint256 fee,
62 | bytes calldata data
63 | ) external override returns (bytes32) {
64 | require(msg.sender == address(lender), 'FLASH_BORROWER_UNTRUSTED_LENDER');
65 | require(initiator == address(this), 'FLASH_BORROWER_LOAN_INITIATOR');
66 |
67 | IERC20 borrowedTokenContract = IERC20(borrowedToken);
68 | ExtraData memory extraData = abi.decode(data, (ExtraData));
69 | (IUniswapV2Router02 router1, IUniswapV2Router02 router2) = _getRouters(extraData.direction);
70 |
71 | // Call protocol 1
72 | uint256 amountReceivedSwapToken = _protocolCall(
73 | router1,
74 | borrowedToken,
75 | extraData.swapToken,
76 | amount,
77 | extraData.minAmountSwapToken,
78 | extraData.deadline
79 | );
80 |
81 | // Call protocol 2
82 | uint256 amountReceivedBorrowedToken = _protocolCall(
83 | router2,
84 | extraData.swapToken,
85 | borrowedToken,
86 | amountReceivedSwapToken,
87 | extraData.minAmountBorrowedToken,
88 | extraData.deadline
89 | );
90 |
91 | uint256 repay = amount + fee;
92 |
93 | // Transfer profits
94 | borrowedTokenContract.transfer(
95 | extraData.profitReceiver,
96 | amountReceivedBorrowedToken - repay - extraData.amountRequired
97 | );
98 |
99 | // Approve lender
100 | borrowedTokenContract.approve(address(lender), repay);
101 |
102 | return keccak256('ERC3156FlashBorrower.onFlashLoan');
103 | }
104 |
105 | function arbitrage(
106 | address borrowedToken,
107 | uint256 amount,
108 | ExtraData memory extraData
109 | ) public {
110 | _checkAmountOut(borrowedToken, amount, extraData);
111 | bytes memory _data = abi.encode(extraData);
112 | lender.flashLoan(this, borrowedToken, amount, _data);
113 | }
114 |
115 | function _checkAmountOut(
116 | address borrowedToken,
117 | uint256 amount,
118 | ExtraData memory extraData
119 | ) internal view {
120 | (IUniswapV2Router02 router1, IUniswapV2Router02 router2) = _getRouters(extraData.direction);
121 |
122 | uint256 amountOutSwapToken = _getAmountOut(router1, borrowedToken, extraData.swapToken, amount);
123 | uint256 amountOutBorrowedToken = _getAmountOut(router2, extraData.swapToken, borrowedToken, amountOutSwapToken);
124 | require(amountOutBorrowedToken >= extraData.minAmountBorrowedToken, 'ARBITRAGER_AMOUNT_OUT_TOO_LOW');
125 | }
126 |
127 | function _getRouters(Direction direction) internal view returns (IUniswapV2Router02, IUniswapV2Router02) {
128 | if (direction == Direction.SUSHISWAP_UNISWAP) {
129 | return (sushiswapRouter, uniswapRouter);
130 | }
131 |
132 | return (uniswapRouter, sushiswapRouter);
133 | }
134 |
135 | function _getAmountOut(
136 | IUniswapV2Router02 router,
137 | address token1,
138 | address token2,
139 | uint256 amount
140 | ) internal view returns (uint256) {
141 | address[] memory path = new address[](2);
142 | path[0] = token1;
143 | path[1] = token2;
144 |
145 | return router.getAmountsOut(amount, path)[1];
146 | }
147 |
148 | function _protocolCall(
149 | IUniswapV2Router02 router,
150 | address token1,
151 | address token2,
152 | uint256 amount,
153 | uint256 minAmount,
154 | uint256 deadline
155 | ) internal returns (uint256) {
156 | address[] memory path = new address[](2);
157 | path[0] = token1;
158 | path[1] = token2;
159 | IERC20(token1).approve(address(router), amount);
160 |
161 | return router.swapExactTokensForTokens(amount, minAmount, path, address(this), deadline)[1];
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flash Loan Arbitrage Bot
2 | This repo holds a functional implementation of flash loan and arbitrage swap on Ethereum developed and executed last year.
3 |
4 | ## Why are you releasing it?
5 | It created lower-than-expected returns, so I moved on. I think it might be useful for others as learning material.
6 |
7 | Even though it made some [profits live](https://ethtx.info/mainnet/0x0f3a3dbc5d6887c08b4f1bf039b3676aa4ba0256e7e4f6d6d94ceb0e50fff9ea/), it wasn't up to my expectations. Although there were some little miscalculations, here is a summary of one of my profitable transactions:
8 |
9 | 
10 |
11 | I made two other iterations of this arbitrage which will be released shortly, with similar commentary.
12 |
13 | ## Overview
14 |
15 | This is a simple working example of a flash arbitrage smart contract. Within a single transaction it:
16 | 1. Instantly flash borrows a certain asset (ETH in this example) from Aave lending pools with zero collateral
17 | 2. Calls UniswapV2 Router02 to wrap the flash liquidity of ETH into WETH and exchange it for DAI tokens
18 | 3. Checks the exchange rate of DAI back into ETH on Sushiswap V1
19 | 4. Calls SushiswapV1 Router02 to swap the DAI back into WETH and then ETH
20 | 5. There's also an independent function to withdraw all ETH and ERC20 tokens at the contract owner's discretion
21 |
22 | Before you start playing with this I highly recommend to have a read of the [Aave Flash Loan mechanism](https://aave.com/flash-loans) and get an indepth conceptual understanding, as it's equally important as understanding the code.
23 |
24 | Since Sushiswap is a fork of UniswapV2, I also suggest familiarising yourself with the Uniswap V2 guide on [trading via smart contracts](https://uniswap.org/docs/v2/smart-contract-integration/trading-from-a-smart-contract/), particularly if you plan on adding more swaps to your arbitrage strategy.
25 |
26 |
27 | ## Deployment
28 |
29 | The contract can be deployed unto Remix, using solidity compiler 0.6.12, and Metamask using Injected Web3.
30 |
31 | On deployment, set the following parameters:
32 |
33 | 
34 |
35 | - ***_AaveLendingPool:*** the LendingPoolAddressesProvider address corresponding to the deployment environment. see [Deployed Contract Instances](https://docs.aave.com/developers/deployed-contracts/deployed-contract-instances).
36 | - ***_UniswapV2Router:*** the Router02 address for UniswapV2 see [here](https://uniswap.org/docs/v2/smart-contracts/router02/).
37 | - ***_SushiswapV1Router:*** the Router02 address for SushiswapV1. There isn't an official testnet router02 so for demo purposes you can just use the uniswapV2 address when playing on the testnet since their codebase is identical (for now - which may not be the case in the future). Alternatively see [Sushiswap repo](https://github.com/sushiswap/sushiswap) for the mainnet router02 address to test in prod or deploy your own version of Router02 onto testnet.
38 | - Click 'transact' and approve the Metamask pop up.
39 | - Once the flash arb contract is deployed, send some ETH or ERC20 token to this contract depending on what asset you're planning to flash borrow from Aave in case you need extra funds to cover the flash fee.
40 |
41 |
42 | ## Execution
43 |
44 | On execution, set the following parameters:
45 |
46 | 
47 |
48 | - ***_flashAsset:*** address of the asset you want to flash loan. e.g. ETH is 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. If you want to flash anything else see [Reserved Assets](https://docs.aave.com/developers/deployed-contracts/deployed-contract-instances#reserves-assets) but you will need to adjust the executeArbitrage() function accordingly.
49 | - ***_flashAmount:*** how much of _flashAsset you want to borrow, demoniated in wei (e.g. 1000000000000000000 for 1 ether).
50 | - ***_daiTokenAddress:*** for this demo we're swapping with the DAI token, so lookup the reserved address of the DAI token. See [Reserved Assets](https://docs.aave.com/developers/deployed-contracts/deployed-contract-instances#reserves-assets).
51 | - ***_amountToTrade:*** how much of the newly acquired _flashAsset you'd like to use as part of this arbitrage.
52 | - ***_tokensOut:*** how much of the ERC20 tokens from the first swap would you like to swap back to complete the arb. Denominated in actual tokens, i.e. 1 = 1 DAI token.
53 | - Click 'transact' and approve in Metamask.
54 |
55 |
56 |
57 | ## Result
58 | 
59 |
60 | If all goes well, a successful execution of this contract looks like [this (Ropsten testnet)](https://ropsten.etherscan.io/tx/0xc1da19c7a5e189b372ec3b310453d7ee267da5df661ee61833230470e5b97fd8).
61 |
62 | ## Updates
63 | - Added V2 contract for a more recent deployment.
64 | - Expected to provide more promising results for arbitraging with flash loans.
65 |
66 | ## Tips for further customization
67 | - This contract would typically be executed by a Web3py bot (beyond this scope) via a web3.eth.Contract() call, referencing the deployed address of this contract and its corresponding ABI. You would usually get the bot to interact with price aggregators such as [1inch](https://1inch.exchange) to assess arb opportunities and execute this contract if the right opportunity is found.
68 | - To have any chance of getting in front of other arbitrage bots on significant arb opportunities the Web3py bot needs to be hosted on your own fast Ethereum node. You will most likely come off second best going through the Infura API to interact with the Ethereum blockchain.
69 | - Some people like to get an unfair advantage by building Transaction-Ordering Dependence (front running) capabilities into the Web3py. However this smart contract would then need to be significantly more complex and flexible enough to cater for a wide range of arbitrage permutations across multiple protocols.
70 | - User specified parameters (as opposed to hardcoded variables) should be passed via the flashloan() function in the first instance. You can subsequently set these parameters to contract variables with higher visibility across the contract.
71 | - There are no direct ETH pairs in UniswapV2 therefore the need for a WETH wrapper. Since Sushiswap is forked from UniswapV2 you'll need to wrap in WETH as well.
72 |
73 | ## Connect with me
74 | If you appreciate this, leave the repo a star and feel free to follow me on:
75 |
76 | [](https://twitter.com/manuelinfoec)
77 | [](https://www.linkedin.com/in/manuelinfosec/)
78 | [](https://manuelinfosec.medium.com/)
79 |
80 | ## Appreciation
81 | You could also donate:
82 |
83 | Ethereum/Binance Smart Chain/Polygon/Avalanche/etc address: 0xE882D838eF07e796bf6b19636931F143e3eC4Dc3
84 |
--------------------------------------------------------------------------------
/FlashArbitrageBot.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity 0.6.12;
3 |
4 | /**
5 | Ropsten instances:
6 | - Uniswap V2 Router: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
7 | - Sushiswap V1 Router: No official sushi routers on testnet
8 | - DAI: 0xf80A32A835F79D7787E8a8ee5721D0fEaFd78108
9 | - ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
10 | - Aave LendingPoolAddressesProvider: 0x1c8756FD2B28e9426CDBDcC7E3c4d64fa9A54728
11 |
12 | Mainnet instances:
13 | - Uniswap V2 Router: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
14 | - Sushiswap V1 Router: 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F
15 | - DAI: 0x6B175474E89094C44Da98b954EedeAC495271d0F
16 | - ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
17 | - Aave LendingPoolAddressesProvider: 0x24a42fD28C976A61Df5D00D0599C34c4f90748c8
18 | */
19 |
20 | // importing flash loan dependencies as per https://docs.aave.com/developers/tutorials/performing-a-flash-loan/...-with-remix
21 | import "https://github.com/aave/flashloan-box/blob/Remix/contracts/aave/FlashLoanReceiverBase.sol";
22 | import "https://github.com/aave/flashloan-box/blob/Remix/contracts/aave/ILendingPoolAddressesProvider.sol";
23 | import "https://github.com/aave/flashloan-box/blob/Remix/contracts/aave/ILendingPool.sol";
24 |
25 | // importing both Sushiswap V1 and Uniswap V2 Router02 dependencies
26 | import "https://github.com/sushiswap/sushiswap/blob/master/contracts/uniswapv2/interfaces/IUniswapV2Router02.sol";
27 | import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol";
28 |
29 | contract FlashArbBot is FlashLoanReceiverBase {
30 |
31 | using SafeMath for uint256;
32 | IUniswapV2Router02 uniswapV2Router;
33 | IUniswapV2Router02 sushiswapV1Router;
34 | uint deadline;
35 | IERC20 dai;
36 | address daiTokenAddress;
37 | uint256 amountToTrade;
38 | uint256 tokensOut;
39 |
40 | /**
41 | Initialize deployment parameters
42 | */
43 | constructor(
44 | address _aaveLendingPool,
45 | IUniswapV2Router02 _uniswapV2Router,
46 | IUniswapV2Router02 _sushiswapV1Router
47 | ) FlashLoanReceiverBase(_aaveLendingPool) public {
48 |
49 | // instantiate SushiswapV1 and UniswapV2 Router02
50 | sushiswapV1Router = IUniswapV2Router02(address(_sushiswapV1Router));
51 | uniswapV2Router = IUniswapV2Router02(address(_uniswapV2Router));
52 |
53 | // setting deadline to avoid mining wait
54 | deadline = block.timestamp + 300; // 5 minutes
55 | }
56 |
57 | /**
58 | Mid-flashloan logic i.e. what you do with the temporarily acquired flash liquidity
59 | */
60 | function executeOperation(
61 | address _reserve,
62 | uint256 _amount,
63 | uint256 _fee,
64 | bytes calldata _params
65 | )
66 | external
67 | override
68 | {
69 | require(_amount <= getBalanceInternal(address(this), _reserve), "Invalid balance");
70 |
71 | // execute arbitrage strategy
72 | try this.executeArbitrage() {
73 | } catch Error(string memory) {
74 | // Reverted with a reason string provided
75 | } catch (bytes memory) {
76 | // failing assertion
77 | }
78 |
79 | // return the flash loan plus Aave's flash loan fee back to the lending pool
80 | uint totalDebt = _amount.add(_fee);
81 | transferFundsBackToPoolInternal(_reserve, totalDebt);
82 | }
83 |
84 | /**
85 | The specific cross protocol swaps that makes up your arb strategy
86 | UniswapV2 -> SushiswapV1 example below
87 | */
88 | function executeArbitrage() public {
89 |
90 | // Trade 1: Execute swap of Ether into designated ERC20 token on UniswapV2
91 | try uniswapV2Router.swapETHForExactTokens{
92 | value: amountToTrade
93 | }(
94 | amountToTrade,
95 | getPathForETHToToken(daiTokenAddress),
96 | address(this),
97 | deadline
98 | ){
99 | } catch {
100 | // error handling when arb failed due to trade 1
101 | }
102 |
103 | // Re-checking prior to execution since the NodeJS bot that instantiated this contract would have checked already
104 | uint256 tokenAmountInWEI = tokensOut.mul(1000000000000000000); //convert into Wei
105 | uint256 estimatedETH = getEstimatedETHForToken(tokensOut, daiTokenAddress)[0]; // check how much ETH you'll get for x number of ERC20 token
106 |
107 | // grant uniswap / sushiswap access to your token, DAI used since we're swapping DAI back into ETH
108 | dai.approve(address(uniswapV2Router), tokenAmountInWEI);
109 | dai.approve(address(sushiswapV1Router), tokenAmountInWEI);
110 |
111 | // Trade 2: Execute swap of the ERC20 token back into ETH on Sushiswap to complete the arb
112 | try sushiswapV1Router.swapExactTokensForETH (
113 | tokenAmountInWEI,
114 | estimatedETH,
115 | getPathForTokenToETH(daiTokenAddress),
116 | address(this),
117 | deadline
118 | ){
119 | } catch {
120 | // error handling when arb failed due to trade 2
121 | }
122 | }
123 |
124 | /**
125 | sweep entire balance on the arb contract back to contract owner
126 | */
127 | function WithdrawBalance() private payable onlyOwner {
128 |
129 | // withdraw all ETH
130 | msg.sender.call{ value: address(this).balance }("");
131 |
132 | // withdraw all x ERC20 tokens
133 | dai.transfer(msg.sender, dai.balanceOf(address(this)));
134 | }
135 |
136 | /**
137 | Flash loan x amount of wei's worth of `_flashAsset`
138 | e.g. 1 ether = 1000000000000000000 wei
139 | */
140 | function flashloan (
141 | address _flashAsset,
142 | uint _flashAmount,
143 | address _daiTokenAddress,
144 | uint _amountToTrade,
145 | uint256 _tokensOut
146 | ) public onlyOwner {
147 |
148 | bytes memory data = "";
149 |
150 | daiTokenAddress = address(_daiTokenAddress);
151 | dai = IERC20(daiTokenAddress);
152 |
153 | amountToTrade = _amountToTrade; // how much wei you want to trade
154 | tokensOut = _tokensOut; // how many tokens you want converted on the return trade
155 |
156 | // call lending pool to commence flash loan
157 | ILendingPool lendingPool = ILendingPool(addressesProvider.getLendingPool());
158 | lendingPool.flashLoan(address(this), _flashAsset, uint(_flashAmount), data);
159 | }
160 |
161 | /**
162 | Using a WETH wrapper here since there are no direct ETH pairs in Uniswap v2
163 | and sushiswap v1 is based on uniswap v2
164 | */
165 | function getPathForETHToToken(address ERC20Token) private view returns (address[] memory) {
166 | address[] memory path = new address[](2);
167 | path[0] = uniswapV2Router.WETH();
168 | path[1] = ERC20Token;
169 |
170 | return path;
171 | }
172 |
173 | /**
174 | Using a WETH wrapper to convert ERC20 token back into ETH
175 | */
176 | function getPathForTokenToETH(address ERC20Token) private view returns (address[] memory) {
177 | address[] memory path = new address[](2);
178 | path[0] = ERC20Token;
179 | path[1] = sushiswapV1Router.WETH();
180 |
181 | return path;
182 | }
183 |
184 | /**
185 | helper function to check ERC20 to ETH conversion rate
186 | */
187 | function getEstimatedETHForToken(uint _tokenAmount, address ERC20Token) public view returns (uint[] memory) {
188 | return uniswapV2Router.getAmountsOut(_tokenAmount, getPathForTokenToETH(ERC20Token));
189 | }
190 | }
191 |
--------------------------------------------------------------------------------