forked from libevm/subway
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
334 lines (298 loc) · 8.98 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
import { formatUnits } from "@ethersproject/units";
import { ethers } from "ethers";
import { CONTRACTS, wssProvider, searcherWallet } from "./src/constants.js";
import {
logDebug,
logError,
logFatal,
logInfo,
logSuccess,
logTrace,
} from "./src/logging.js";
import { calcSandwichOptimalIn, calcSandwichState } from "./src/numeric.js";
import { parseUniv2RouterTx } from "./src/parse.js";
import {
callBundleFlashbots,
getRawTransaction,
sanityCheckSimulationResponse,
sendBundleFlashbots,
} from "./src/relayer.js";
import {
getUniv2ExactWethTokenMinRecv,
getUniv2PairAddress,
getUniv2Reserve,
} from "./src/univ2.js";
import { calcNextBlockBaseFee, match, stringifyBN } from "./src/utils.js";
// Note: You'll probably want to break this function up
// handling everything in here so you can follow along easily
const sandwichUniswapV2RouterTx = async (txHash) => {
const strLogPrefix = `txhash=${txHash}`;
// Bot not broken right
logTrace(strLogPrefix, "received");
// Get tx data
const [tx, txRecp] = await Promise.all([
wssProvider.getTransaction(txHash),
wssProvider.getTransactionReceipt(txHash),
]);
// Make sure transaction hasn't been mined
if (txRecp !== null) {
return;
}
// Sometimes tx is null for some reason
if (tx === null) {
return;
}
// We're not a generalized version
// So we're just gonna listen to specific addresses
// and decode the data from there
if (!match(tx.to, CONTRACTS.UNIV2_ROUTER)) {
return;
}
// Decode transaction data
// i.e. is this swapExactETHForToken?
// You'll have to decode all the other possibilities :P
const routerDataDecoded = parseUniv2RouterTx(tx.data);
// Basically means its not swapExactETHForToken and you need to add
// other possibilities
if (routerDataDecoded === null) {
return;
}
const { path, amountOutMin, deadline } = routerDataDecoded;
// If tx deadline has passed, just ignore it
// As we cannot sandwich it
if (new Date().getTime() / 1000 > deadline) {
return;
}
// Get the min recv for token directly after WETH
const userMinRecv = await getUniv2ExactWethTokenMinRecv(amountOutMin, path);
const userAmountIn = tx.value; // User is sending exact ETH (not WETH)
logTrace(
strLogPrefix,
"potentially sandwichable swapExactETHForTokens tx found",
JSON.stringify(
stringifyBN({
userAmountIn,
userMinRecv,
path,
})
)
);
// Note: Since this is swapExactETHForTokens, the path will always be like so
// Get the optimal in amount
const [weth, token] = path;
const pairToSandwich = getUniv2PairAddress(weth, token);
const [reserveWeth, reserveToken] = await getUniv2Reserve(
pairToSandwich,
weth,
token
);
const optimalWethIn = calcSandwichOptimalIn(
userAmountIn,
userMinRecv,
reserveWeth,
reserveToken
);
// Lmeow, nothing to sandwich!
if (optimalWethIn.lte(ethers.constants.Zero)) {
return;
}
// Contains 3 states:
// 1: Frontrun state
// 2: Victim state
// 3: Backrun state
const sandwichStates = calcSandwichState(
optimalWethIn,
userAmountIn,
userMinRecv,
reserveWeth,
reserveToken
);
// Sanity check failed
if (sandwichStates === null) {
logDebug(
strLogPrefix,
"sandwich sanity check failed",
JSON.stringify(
stringifyBN({
optimalWethIn,
reserveToken,
reserveWeth,
userAmountIn,
userMinRecv,
})
)
);
return;
}
// Cool profitable sandwich :)
// But will it be post gas?
logInfo(
strLogPrefix,
"sandwichable target found",
JSON.stringify(stringifyBN(sandwichStates))
);
// Get block data to compute bribes etc
// as bribes calculation has correlation with gasUsed
const block = await wssProvider.getBlock();
const targetBlockNumber = block.number + 1;
const nextBaseFee = calcNextBlockBaseFee(block);
const nonce = await wssProvider.getTransactionCount(searcherWallet.address);
// Craft our payload
const frontslicePayload = ethers.utils.solidityPack(
["address", "address", "uint128", "uint128", "uint8"],
[
token,
pairToSandwich,
optimalWethIn,
sandwichStates.frontrun.amountOut,
ethers.BigNumber.from(token).lt(ethers.BigNumber.from(weth)) ? 0 : 1,
]
);
const frontsliceTx = {
to: CONTRACTS.SANDWICH,
from: searcherWallet.address,
data: frontslicePayload,
chainId: 1,
maxPriorityFeePerGas: 0,
maxFeePerGas: nextBaseFee,
gasLimit: 250000,
nonce,
type: 2,
};
const frontsliceTxSigned = await searcherWallet.signTransaction(frontsliceTx);
const middleTx = getRawTransaction(tx);
const backslicePayload = ethers.utils.solidityPack(
["address", "address", "uint128", "uint128", "uint8"],
[
weth,
pairToSandwich,
sandwichStates.frontrun.amountOut,
sandwichStates.backrun.amountOut,
ethers.BigNumber.from(weth).lt(ethers.BigNumber.from(token)) ? 0 : 1,
]
);
const backsliceTx = {
to: CONTRACTS.SANDWICH,
from: searcherWallet.address,
data: backslicePayload,
chainId: 1,
maxPriorityFeePerGas: 0,
maxFeePerGas: nextBaseFee,
gasLimit: 250000,
nonce: nonce + 1,
type: 2,
};
const backsliceTxSigned = await searcherWallet.signTransaction(backsliceTx);
// Simulate tx to get the gas used
const signedTxs = [frontsliceTxSigned, middleTx, backsliceTxSigned];
const simulatedResp = await callBundleFlashbots(signedTxs, targetBlockNumber);
// Try and check all the errors
try {
sanityCheckSimulationResponse(simulatedResp);
} catch (e) {
logError(
strLogPrefix,
"error while simulating",
JSON.stringify(
stringifyBN({
error: e,
block,
targetBlockNumber,
nextBaseFee,
nonce,
sandwichStates,
frontsliceTx,
backsliceTx,
})
)
);
return;
}
// Extract gas
const frontsliceGas = ethers.BigNumber.from(simulatedResp.results[0].gasUsed);
const backsliceGas = ethers.BigNumber.from(simulatedResp.results[2].gasUsed);
// Bribe 99.99% :P
const bribeAmount = sandwichStates.revenue.sub(
frontsliceGas.mul(nextBaseFee)
);
const maxPriorityFeePerGas = bribeAmount
.mul(9999)
.div(10000)
.div(backsliceGas);
// Note: you probably want some circuit breakers here so you don't lose money
// if you fudged shit up
// If 99.99% bribe isn't enough to cover base fee, its not worth it
if (maxPriorityFeePerGas.lt(nextBaseFee)) {
logTrace(
strLogPrefix,
`maxPriorityFee (${formatUnits(
maxPriorityFeePerGas,
9
)}) gwei < nextBaseFee (${formatUnits(nextBaseFee, 9)}) gwei`
);
return;
}
// Okay, update backslice tx
const backsliceTxSignedWithBribe = await searcherWallet.signTransaction({
...backsliceTx,
maxPriorityFeePerGas,
});
// Fire the bundles
const bundleResp = await sendBundleFlashbots(
[frontsliceTxSigned, middleTx, backsliceTxSignedWithBribe],
targetBlockNumber
);
logSuccess(
strLogPrefix,
"Bundle submitted!",
JSON.stringify(
block,
targetBlockNumber,
nextBaseFee,
nonce,
sandwichStates,
frontsliceTx,
maxPriorityFeePerGas,
bundleResp
)
);
};
const main = async () => {
logInfo(
"============================================================================"
);
logInfo(
" _ _ _ \r\n ____ _| |____ __ ____ _ _ _ | |__ ___| |_ \r\n (_-< || | '_ \\ V V / _` | || | | '_ \\/ _ \\ _|\r\n /__/\\_,_|_.__/\\_/\\_/\\__,_|\\_, | |_.__/\\___/\\__|\r\n | |__ _ _ | (_) |__ ___|__/__ __ \r\n | '_ \\ || | | | | '_ \\/ -_) V / ' \\ \r\n |_.__/\\_, | |_|_|_.__/\\___|\\_/|_|_|_| \r\n |__/ \n"
);
logInfo("github: https://github.com/libevm");
logInfo("twitter: https://twitter.com/libevm");
logInfo(
"============================================================================\n"
);
logInfo(`Searcher Wallet: ${searcherWallet.address}`);
logInfo(`Node URL: ${wssProvider.connection.url}\n`);
logInfo(
"============================================================================\n"
);
// Add timestamp to all subsequent console.logs
// One little two little three little dependency injections....
const origLog = console.log;
console.log = function (obj, ...placeholders) {
if (typeof obj === "string")
placeholders.unshift("[" + new Date().toISOString() + "] " + obj);
else {
// This handles console.log( object )
placeholders.unshift(obj);
placeholders.unshift("[" + new Date().toISOString() + "] %j");
}
origLog.apply(this, placeholders);
};
logInfo("Listening to mempool...\n");
// Listen to the mempool on local node
wssProvider.on("pending", (txHash) =>
sandwichUniswapV2RouterTx(txHash).catch((e) => {
logFatal(`txhash=${txHash} error ${JSON.stringify(e)}`);
})
);
};
main();