-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.js
More file actions
269 lines (209 loc) · 9.13 KB
/
Copy pathbot.js
File metadata and controls
269 lines (209 loc) · 9.13 KB
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
// -- HANDLE INITIAL SETUP -- //
require('./helpers/server')
require("dotenv").config();
const ethers = require("ethers")
const config = require('./config.json')
const { getTokenAndContract, getPairContract, buildPoolKey, getPoolId, calculateV4Price, calculateSushiPrice, simulate } = require('./helpers/helpers')
const { provider, poolManager, stateView, quoter, sRouter, sFactory, arbitrage } = require('./helpers/initialization')
const { EVENT_TYPES, emitDefiEvent, isTradingHalted, SOAR_ENABLED } = require('./helpers/soar')
// -- .ENV VALUES HERE -- //
const arbFor = process.env.ARB_FOR // This is the address of token we are attempting to arbitrage (WETH)
const arbAgainst = process.env.ARB_AGAINST // USDC
const units = process.env.UNITS // Used for price display/reporting
const difference = process.env.PRICE_DIFFERENCE
const arbAmount = process.env.ARB_AMOUNT || "0.5" // WETH traded into the first leg
const gasLimit = process.env.GAS_LIMIT
const gasPrice = process.env.GAS_PRICE
let poolKey, poolId, baseIsZero, sPair, weth, token, amount
let isExecuting = false
const main = async () => {
const { token0Contract: wethContract, token0: wethToken, token1: quoteToken } = await getTokenAndContract(arbFor, arbAgainst, provider)
weth = wethToken
token = quoteToken
// Uniswap v4 pool (identified by its PoolKey/PoolId, not a pair contract)
const pool = buildPoolKey(weth.address, token.address, config.UNISWAP_V4.POOL)
poolKey = pool.key
baseIsZero = pool.baseIsZero
poolId = getPoolId(poolKey)
const [sqrtPriceX96] = await stateView.getSlot0(poolId)
if (sqrtPriceX96 === 0n) {
throw new Error(`Uniswap v4 pool ${poolId} is not initialized. Check UNISWAP_V4.POOL settings in config.json`)
}
// Sushiswap still uses V2 pair contracts
sPair = await getPairContract(sFactory, weth.address, token.address, provider)
console.log(`Uniswap v4 PoolId: ${poolId}`)
console.log(`sPair Address: ${await sPair.getAddress()}\n`)
// v4 emits all Swap events from the PoolManager singleton, filtered by PoolId
poolManager.on(poolManager.filters.Swap(poolId), async () => {
await handleSwapEvent('Uniswap v4')
})
sPair.on('Swap', async () => {
await handleSwapEvent('Sushiswap')
})
console.log("Waiting for swap event...")
}
const handleSwapEvent = async (_exchange) => {
if (!isExecuting) {
isExecuting = true
const priceDifference = await checkPrice(_exchange)
const startOnUniswap = await determineDirection(priceDifference)
if (startOnUniswap === null) {
console.log(`No Arbitrage Currently Available\n`)
console.log(`-----------------------------------------\n`)
isExecuting = false
return
}
const isProfitable = await determineProfitability(startOnUniswap)
if (!isProfitable) {
console.log(`No Arbitrage Currently Available\n`)
console.log(`-----------------------------------------\n`)
isExecuting = false
return
}
// The SOAR stack sets this flag when it raises a CRITICAL finding for
// this bot. Clearing it is a human action.
if (await isTradingHalted()) {
console.log(`Trading halted by SOAR - skipping execution\n`)
console.log(`-----------------------------------------\n`)
isExecuting = false
return
}
await executeTrade(startOnUniswap)
isExecuting = false
}
}
const checkPrice = async (_exchange) => {
isExecuting = true
console.log(`Swap Initiated on ${_exchange}, Checking Price...\n`)
const currentBlock = await provider.getBlockNumber()
const uPrice = await calculateV4Price(stateView, poolId, baseIsZero, weth.decimals, token.decimals)
const sPrice = await calculateSushiPrice(sPair, weth, token)
const uFPrice = Number(uPrice).toFixed(units)
const sFPrice = Number(sPrice).toFixed(units)
const priceDifference = (((uFPrice - sFPrice) / sFPrice) * 100).toFixed(2)
console.log(`Current Block: ${currentBlock}`)
console.log(`-----------------------------------------`)
console.log(`UNISWAP V4 | ${token.symbol}/${weth.symbol}\t | ${uFPrice}`)
console.log(`SUSHISWAP | ${token.symbol}/${weth.symbol}\t | ${sFPrice}\n`)
console.log(`Percentage Difference: ${priceDifference}%\n`)
return priceDifference
}
const determineDirection = async (_priceDifference) => {
console.log(`Determining Direction...\n`)
if (_priceDifference >= difference) {
// WETH is more expensive on Uniswap v4: sell it there first
console.log(`Potential Arbitrage Direction:\n`)
console.log(`Sell\t -->\t Uniswap V4`)
console.log(`Buy\t -->\t Sushiswap\n`)
return true
} else if (_priceDifference <= -(difference)) {
// WETH is more expensive on Sushiswap: sell it there first
console.log(`Potential Arbitrage Direction:\n`)
console.log(`Sell\t -->\t Sushiswap`)
console.log(`Buy\t -->\t Uniswap V4\n`)
return false
} else {
return null
}
}
const determineProfitability = async (_startOnUniswap) => {
console.log(`Determining Profitability...\n`)
// This is where you can customize your conditions on whether a profitable trade is possible...
try {
const amountIn = ethers.parseUnits(arbAmount, 'ether')
const { amountIn: wethIn, amountOut: wethOut } = await simulate(amountIn, _startOnUniswap, {
quoter,
key: poolKey,
baseIsZero,
sRouter,
wethAddress: weth.address,
tokenAddress: token.address
})
const amountDifference = wethOut - wethIn
const estimatedGasCost = gasLimit * gasPrice
// Fetch account
const account = new ethers.Wallet(process.env.PRIVATE_KEY, provider)
const ethBalanceBefore = ethers.formatUnits(await provider.getBalance(account.address), 'ether')
const data = {
'WETH In': wethIn,
'WETH Out': wethOut,
'WETH Gained/Lost': amountDifference,
'-': {},
'ETH Balance': ethBalanceBefore,
'ETH Spent (gas)': estimatedGasCost,
'Total Gained/Lost': amountDifference - estimatedGasCost
}
console.table(data)
console.log()
if (Number(wethOut) - Number(wethIn) <= estimatedGasCost) {
return false
}
amount = amountIn
return true
} catch (error) {
console.log(error)
console.log(`\nError occurred while trying to determine profitability...\n`)
console.log(`This can typically happen because of liquidity issues, see README for more information.\n`)
await emitDefiEvent({
type: EVENT_TYPES.BOT_ERROR,
pair: `${weth.symbol}/${token.symbol}`,
detail: `Profitability simulation failed: ${error.shortMessage || error.message}`,
})
return false
}
}
const executeTrade = async (_startOnUniswap) => {
console.log(`Attempting Arbitrage...\n`)
// Create Signer
const account = new ethers.Wallet(process.env.PRIVATE_KEY, provider)
// Profit accrues inside the arbitrage contract (as WETH or native ETH)
const arbitrageAddress = await arbitrage.getAddress()
const wethContract = new ethers.Contract(weth.address, ["function balanceOf(address) view returns (uint256)"], provider)
const contractBalanceBefore = await wethContract.balanceOf(arbitrageAddress) + await provider.getBalance(arbitrageAddress)
const ethBalanceBefore = await provider.getBalance(account.address)
if (config.PROJECT_SETTINGS.isDeployed) {
try {
const transaction = await arbitrage.connect(account).executeTrade(
{
key: poolKey,
startOnUniswap: _startOnUniswap,
amountIn: amount,
minProfit: 0
},
{ gasLimit: process.env.GAS_LIMIT }
)
await transaction.wait()
} catch (error) {
// A revert here is the routine outcome of losing the race, so this is
// not an error path for the bot - but the SOAR correlation agent needs
// the signal to spot a run of them.
console.log(`Trade failed: ${error.shortMessage || error.message}\n`)
await emitDefiEvent({
type: EVENT_TYPES.TRADE_REVERTED,
pair: `${weth.symbol}/${token.symbol}`,
txHash: error.transaction?.hash || error.receipt?.hash,
valueUsd: undefined,
detail: `${_startOnUniswap ? 'v4->Sushi' : 'Sushi->v4'}: ${error.shortMessage || error.message}`,
})
return
}
}
console.log(`Trade Complete:\n`)
const contractBalanceAfter = await wethContract.balanceOf(arbitrageAddress) + await provider.getBalance(arbitrageAddress)
const ethBalanceAfter = await provider.getBalance(account.address)
const contractBalanceDifference = contractBalanceAfter - contractBalanceBefore
const ethBalanceDifference = ethBalanceBefore - ethBalanceAfter
const data = {
'ETH Balance Before': ethers.formatUnits(ethBalanceBefore, 'ether'),
'ETH Balance After': ethers.formatUnits(ethBalanceAfter, 'ether'),
'ETH Spent (gas)': ethers.formatUnits(ethBalanceDifference.toString(), 'ether'),
'-': {},
'Contract WETH+ETH Before': ethers.formatUnits(contractBalanceBefore, 'ether'),
'Contract WETH+ETH After': ethers.formatUnits(contractBalanceAfter, 'ether'),
'WETH Gained/Lost': ethers.formatUnits(contractBalanceDifference.toString(), 'ether'),
'-': {},
'Total Gained/Lost': `${ethers.formatUnits((contractBalanceDifference - ethBalanceDifference).toString(), 'ether')} ETH`
}
console.table(data)
}
main()