Skip to content

Commit

Permalink
Make it work
Browse files Browse the repository at this point in the history
epheph committed Dec 24, 2020
1 parent 1b9a877 commit 3b704db
Showing 11 changed files with 2,606 additions and 309 deletions.
20 changes: 20 additions & 0 deletions .eslintrc.js
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": {
}
};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.idea
/node_modules
/build
79 changes: 79 additions & 0 deletions src/ArbThing.sol
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;
}
}
154 changes: 154 additions & 0 deletions src/Arbitrage.ts
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));
}
}
48 changes: 48 additions & 0 deletions src/EthMarket.ts
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>>
}
43 changes: 43 additions & 0 deletions src/UniswapFlashQuery.sol
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;
}
}
Loading
Oops, something went wrong.

0 comments on commit 3b704db

Please sign in to comment.