forked from flashbots/simple-arbitrage
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
11 changed files
with
2,606 additions
and
309 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
module.exports = { | ||
"env": { | ||
"browser": true, | ||
"es2021": true | ||
}, | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:@typescript-eslint/recommended" | ||
], | ||
"parser": "@typescript-eslint/parser", | ||
"parserOptions": { | ||
"ecmaVersion": 12, | ||
"sourceType": "module" | ||
}, | ||
"plugins": [ | ||
"@typescript-eslint" | ||
], | ||
"rules": { | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/.idea | ||
/node_modules | ||
/build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
//SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.6.12; | ||
|
||
pragma experimental ABIEncoderV2; | ||
|
||
interface IERC20 { | ||
event Approval(address indexed owner, address indexed spender, uint value); | ||
event Transfer(address indexed from, address indexed to, uint value); | ||
|
||
function name() external view returns (string memory); | ||
function symbol() external view returns (string memory); | ||
function decimals() external view returns (uint8); | ||
function totalSupply() external view returns (uint); | ||
function balanceOf(address owner) external view returns (uint); | ||
function allowance(address owner, address spender) external view returns (uint); | ||
|
||
function approve(address spender, uint value) external returns (bool); | ||
function transfer(address to, uint value) external returns (bool); | ||
function transferFrom(address from, address to, uint value) external returns (bool); | ||
} | ||
|
||
interface IWETH is IERC20 { | ||
function deposit() external payable; | ||
function withdraw(uint) external; | ||
} | ||
|
||
contract FlashBotsMultiCall { | ||
address private immutable owner; | ||
address private immutable executor; | ||
IWETH private constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); | ||
|
||
modifier onlyExecutor() { | ||
require(msg.sender == executor); | ||
_; | ||
} | ||
|
||
modifier onlyOwner() { | ||
require(msg.sender == owner); | ||
_; | ||
} | ||
|
||
constructor(address _executor) public payable { | ||
owner = msg.sender; | ||
executor = _executor; | ||
if (msg.value > 0) { | ||
WETH.deposit{value: msg.value}(); | ||
} | ||
} | ||
|
||
receive() external payable { | ||
} | ||
|
||
function uniswapWeth(uint256 _wethAmountToFirstMarket, uint256 _ethAmountToCoinbase, address[] memory _targets, bytes[] memory _payloads) external onlyExecutor payable { | ||
require (_targets.length == _payloads.length); | ||
uint256 _wethBalanceBefore = WETH.balanceOf(address(this)); | ||
WETH.transfer(_targets[0], _wethAmountToFirstMarket); | ||
for (uint256 i = 0; i < _targets.length; i++) { | ||
(bool _success, bytes memory _response) = _targets[i].call(_payloads[i]); | ||
require(_success); _response; | ||
} | ||
|
||
uint256 _wethBalanceAfter = WETH.balanceOf(address(this)); | ||
require(_wethBalanceAfter > _wethBalanceBefore + _ethAmountToCoinbase); | ||
if (_ethAmountToCoinbase == 0) return; | ||
|
||
uint256 _ethBalance = address(this).balance; | ||
if (_ethBalance < _ethAmountToCoinbase) { | ||
WETH.withdraw(_ethAmountToCoinbase - _ethBalance); | ||
} | ||
block.coinbase.transfer(_ethAmountToCoinbase); | ||
} | ||
|
||
function call(address payable _to, uint256 _value, bytes calldata _data) external onlyOwner payable returns (bytes memory) { | ||
require(_to != address(0)); | ||
(bool _success, bytes memory _result) = _to.call{value: _value}(_data); | ||
require(_success); | ||
return _result; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import * as _ from "lodash"; | ||
import { ETHER, WETH_ADDRESS } from "./addresses"; | ||
import { BigNumber, Contract, Wallet } from "ethers"; | ||
import { EthMarket } from "./EthMarket"; | ||
import { FlashbotsBundleProvider } from "@flashbots/ethers-provider-bundle"; | ||
import { bigNumberToDecimal } from "./index"; | ||
|
||
export interface CrossedMarketDetails { | ||
profit: BigNumber, | ||
volume: BigNumber, | ||
tokenAddress: string, | ||
buyFromMarket: EthMarket, | ||
sellToMarket: EthMarket, | ||
} | ||
|
||
type MarketsByToken = { [tokenAddress: string]: Array<EthMarket> } | ||
|
||
// TODO: implement binary search (assuming linear/exponential global maximum profitability) | ||
const TEST_VOLUMES = [ | ||
ETHER.div(100), | ||
ETHER.div(10), | ||
ETHER.div(6), | ||
ETHER.div(4), | ||
ETHER.div(2), | ||
ETHER.div(1), | ||
ETHER.mul(2), | ||
ETHER.mul(5), | ||
ETHER.mul(10), | ||
] | ||
|
||
function getBestCrossedMarket(crossedMarkets: Array<EthMarket>[], tokenAddress: string) { | ||
let bestCrossedMarket: CrossedMarketDetails | undefined = undefined; | ||
for (const crossedMarket of crossedMarkets) { | ||
const sellToMarket = crossedMarket[0] | ||
const buyFromMarket = crossedMarket[1] | ||
for (const size of TEST_VOLUMES) { | ||
const tokensOutFromBuyingSize = buyFromMarket.getTokensOut(WETH_ADDRESS, tokenAddress, size); | ||
const proceedsFromSellingTokens = sellToMarket.getTokensOut(tokenAddress, WETH_ADDRESS, tokensOutFromBuyingSize) | ||
const profit = proceedsFromSellingTokens.sub(size); | ||
if (bestCrossedMarket !== undefined && profit.lt(bestCrossedMarket.profit)) { | ||
// If the next size up lost value, meet halfway. TODO: replace with real binary search | ||
const trySize = size.add(bestCrossedMarket.volume).div(2) | ||
const tryTokensOutFromBuyingSize = buyFromMarket.getTokensOut(WETH_ADDRESS, tokenAddress, trySize); | ||
const tryProceedsFromSellingTokens = sellToMarket.getTokensOut(tokenAddress, WETH_ADDRESS, tryTokensOutFromBuyingSize) | ||
const tryProfit = tryProceedsFromSellingTokens.sub(trySize); | ||
if (tryProfit.gt(bestCrossedMarket.profit)) { | ||
bestCrossedMarket = { | ||
volume: trySize, | ||
profit: tryProfit, | ||
tokenAddress, | ||
sellToMarket, | ||
buyFromMarket | ||
} | ||
} | ||
break; | ||
} | ||
bestCrossedMarket = { | ||
volume: size, | ||
profit: profit, | ||
tokenAddress, | ||
sellToMarket, | ||
buyFromMarket | ||
} | ||
} | ||
} | ||
return bestCrossedMarket; | ||
} | ||
|
||
export class Arbitrage { | ||
private flashbotsProvider: FlashbotsBundleProvider; | ||
private bundleExecutorContract: Contract; | ||
private executorWallet: Wallet; | ||
|
||
constructor(executorWallet: Wallet, flashbotsProvider: FlashbotsBundleProvider, bundleExecutorContract: Contract) { | ||
this.executorWallet = executorWallet; | ||
this.flashbotsProvider = flashbotsProvider; | ||
this.bundleExecutorContract = bundleExecutorContract; | ||
} | ||
|
||
static printCrossedMarket(crossedMarket: CrossedMarketDetails): void { | ||
const buyTokens = crossedMarket.buyFromMarket.tokens | ||
const sellTokens = crossedMarket.sellToMarket.tokens | ||
console.log( | ||
`Profit: ${bigNumberToDecimal(crossedMarket.profit)} Volume: ${bigNumberToDecimal(crossedMarket.volume)}\n` + | ||
`${crossedMarket.buyFromMarket.protocol()} (${crossedMarket.buyFromMarket.marketAddress})\n` + | ||
` ${buyTokens[0]} => ${buyTokens[1]}\n` + | ||
`${crossedMarket.sellToMarket.protocol()} (${crossedMarket.sellToMarket.marketAddress})\n` + | ||
` ${sellTokens[0]} => ${sellTokens[1]}\n` + | ||
`\n` | ||
) | ||
} | ||
|
||
|
||
async evaluateMarkets(marketsByToken: MarketsByToken): Promise<Array<CrossedMarketDetails>> { | ||
const bestCrossedMarkets = new Array<CrossedMarketDetails>() | ||
|
||
for (const tokenAddress in marketsByToken) { | ||
const markets = marketsByToken[tokenAddress] | ||
const pricedMarkets = _.map(markets, (ethMarket: EthMarket) => { | ||
return { | ||
uniswapPair: ethMarket, | ||
buyTokenPrice: ethMarket.getTokensIn(tokenAddress, WETH_ADDRESS, ETHER.div(100)), | ||
sellTokenPrice: ethMarket.getTokensOut(WETH_ADDRESS, tokenAddress, ETHER.div(100)), | ||
} | ||
}); | ||
|
||
const crossedMarkets = new Array<Array<EthMarket>>() | ||
for (const pricedMarket of pricedMarkets) { | ||
_.forEach(pricedMarkets, pm => { | ||
if (pm.sellTokenPrice.gt(pricedMarket.buyTokenPrice)) { | ||
crossedMarkets.push([pricedMarket.uniswapPair, pm.uniswapPair]) | ||
} | ||
}) | ||
} | ||
|
||
const bestCrossedMarket = getBestCrossedMarket(crossedMarkets, tokenAddress); | ||
if (bestCrossedMarket !== undefined && bestCrossedMarket.profit.gt(ETHER.div(500))) { | ||
bestCrossedMarkets.push(bestCrossedMarket) | ||
} | ||
} | ||
bestCrossedMarkets.sort((a, b) => a.profit.lt(b.profit) ? 1 : a.profit.gt(b.profit) ? -1 : 0) | ||
return bestCrossedMarkets | ||
} | ||
|
||
// TODO: take more than 1 | ||
async takeCrossedMarkets(bestCrossedMarkets: CrossedMarketDetails[], blockNumber: number): Promise<void> { | ||
const globalBestCrossedMarket = bestCrossedMarkets[0] | ||
console.log("Send this much WETH", globalBestCrossedMarket.volume.toString(), "get this much profit", globalBestCrossedMarket.profit.toString()) | ||
const buyCalls = await globalBestCrossedMarket.buyFromMarket.sellTokensToNextMarket(WETH_ADDRESS, globalBestCrossedMarket.volume, globalBestCrossedMarket.sellToMarket); | ||
const inter = globalBestCrossedMarket.buyFromMarket.getTokensOut(WETH_ADDRESS, globalBestCrossedMarket.tokenAddress, globalBestCrossedMarket.volume) | ||
const sellCallData = await globalBestCrossedMarket.sellToMarket.sellTokens(globalBestCrossedMarket.tokenAddress, inter, this.bundleExecutorContract.address); | ||
|
||
const targets: Array<string> = [...buyCalls.targets, globalBestCrossedMarket.buyFromMarket.marketAddress, globalBestCrossedMarket.sellToMarket.marketAddress] | ||
const payloads: Array<string> = [...buyCalls.data, sellCallData] | ||
console.log({targets, payloads}) | ||
const transaction = await this.bundleExecutorContract.populateTransaction.uniswapWeth(globalBestCrossedMarket.volume, globalBestCrossedMarket.profit.sub(1), targets, payloads, { | ||
gasPrice: BigNumber.from(0), | ||
gasLimit: BigNumber.from(1000000), | ||
}); | ||
console.log(transaction) | ||
const bundlePromises = _.map([blockNumber + 1, blockNumber + 2], targetBlockNumber => | ||
this.flashbotsProvider.sendBundle( | ||
[ | ||
{ | ||
signer: this.executorWallet, | ||
transaction: transaction | ||
} | ||
], | ||
targetBlockNumber | ||
) | ||
) | ||
await Promise.all(bundlePromises).catch(e => console.error(e)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { BigNumber } from "ethers"; | ||
|
||
export interface TokenBalances { | ||
[tokenAddress: string]: BigNumber | ||
} | ||
|
||
export interface Hi { | ||
targets: Array<string> | ||
data: Array<string> | ||
} | ||
|
||
export interface CallDetails { | ||
target: string; | ||
data: string; | ||
value?: BigNumber; | ||
} | ||
|
||
export abstract class EthMarket { | ||
get tokens(): Array<string> { | ||
return this._tokens; | ||
} | ||
|
||
get marketAddress(): string { | ||
return this._marketAddress; | ||
} | ||
|
||
protected readonly _tokens: Array<string>; | ||
protected readonly _marketAddress: string; | ||
|
||
constructor(marketAddress: string, tokens: Array<string>) { | ||
this._marketAddress = marketAddress; | ||
this._tokens = tokens | ||
} | ||
|
||
abstract protocol(): string | ||
|
||
abstract getTokensOut(tokenIn: string, tokenOut: string, amountIn: BigNumber): BigNumber; | ||
|
||
abstract getTokensIn(tokenIn: string, tokenOut: string, amountOut: BigNumber): BigNumber; | ||
|
||
abstract sellTokensToNextMarket(tokenIn: string, amountIn: BigNumber, ethMarket: EthMarket): Promise<Hi> | ||
|
||
abstract sellTokens(tokenIn: string, amountIn: BigNumber, recipient: string): Promise<string> | ||
|
||
abstract receiveDirectly(tokenAddress: string): boolean; | ||
|
||
abstract prepareReceive(tokenAddress: string, amountIn: BigNumber): Promise<Array<CallDetails>> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
//SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.6.12; | ||
|
||
pragma experimental ABIEncoderV2; | ||
|
||
interface IUniswapV2Pair { | ||
function token0() external view returns (address); | ||
function token1() external view returns (address); | ||
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); | ||
} | ||
|
||
abstract contract UniswapV2Factory { | ||
mapping(address => mapping(address => address)) public getPair; | ||
address[] public allPairs; | ||
function allPairsLength() external view virtual returns (uint); | ||
} | ||
|
||
contract FlashBotsUniswapQuery { | ||
function getReservesByPairs(IUniswapV2Pair[] calldata _pairs) external view returns (uint256[3][] memory) { | ||
uint256[3][] memory result = new uint256[3][](_pairs.length); | ||
for (uint i = 0; i < _pairs.length; i++) { | ||
(result[i][0], result[i][1], result[i][2]) = _pairs[i].getReserves(); | ||
} | ||
return result; | ||
} | ||
|
||
function getPairsByIndexRange(UniswapV2Factory _uniswapFactory, uint256 _start, uint256 _stop) external view returns (address[3][] memory) { | ||
uint256 _allPairsLength = _uniswapFactory.allPairsLength(); | ||
if (_stop > _allPairsLength) { | ||
_stop = _allPairsLength; | ||
} | ||
require(_stop >= _start, "start cannot be higher than stop"); | ||
uint256 _qty = _stop - _start; | ||
address[3][] memory result = new address[3][](_qty); | ||
for (uint i = 0; i < _qty; i++) { | ||
IUniswapV2Pair _uniswapPair = IUniswapV2Pair(_uniswapFactory.allPairs(_start + i)); | ||
result[i][0] = _uniswapPair.token0(); | ||
result[i][1] = _uniswapPair.token1(); | ||
result[i][2] = address(_uniswapPair); | ||
} | ||
return result; | ||
} | ||
} |
Oops, something went wrong.