diff --git a/docs/flashbots-auction/searchers/libraries/mev-share-clients.md b/docs/flashbots-auction/searchers/libraries/mev-share-clients.md index bfc3f7830..8d1fece10 100644 --- a/docs/flashbots-auction/searchers/libraries/mev-share-clients.md +++ b/docs/flashbots-auction/searchers/libraries/mev-share-clients.md @@ -4,6 +4,7 @@ title: MEV-Share Clients ### Typescript -* [mev-share-client-ts](https://github.com/flashbots/mev-share-client-ts) - reference implementation +* [mev-share-client-ts](https://github.com/flashbots/mev-share-client-ts) - reference implementation in Typescript +* [mev-share-rs](https://github.com/paradigmxyz/mev-share-rs) - Rust implementation -> :eyes: If you are writing (or want to write) a client library for MEV-Share, please [reach out](https://twitter.com/zeroXbrock). We love client diversity! +> :eyes: If you are writing (or want to write) a client library for MEV-Share, please [reach out](/flashbots-mev-share/searchers/tutorials/limit-order/more-resources). We love client diversity! diff --git a/docs/flashbots-mev-share/searchers/debugging.mdx b/docs/flashbots-mev-share/searchers/debugging.mdx new file mode 100644 index 000000000..87f306e42 --- /dev/null +++ b/docs/flashbots-mev-share/searchers/debugging.mdx @@ -0,0 +1,230 @@ +--- +title: Debugging +--- + +If you find that your bundles aren't landing when you think they should, we have some tips to help you figure out why. + +> Note: Some examples you see in the code here are based on the [limit order bot tutorial](/flashbots-mev-share/searchers/tutorials/limit-order/introduction). The same tactics can be used to debug any MEV-Share bundle. + +## Simulate your bundle + +The simplest way to find out what happened with your bundle is to simulate it. The [mev-share-client](https://www.npmjs.com/package/@flashbots/mev-share-client) library has a function `simulateBundle` which executes your bundle in a virtual environment based on the block which the bundle was targeting. + +In our project, we can simply call the `simulateBundle` function with our original bundle. The block we choose is the block before the first transaction (the one we tried to backrun) lands on-chain, because we want the state as close as possible to when the transaction actually landed; this is the most accurate representation of the state of the blockchain (i.e. prices) when our bundle was supposed to have landed. + +```tsx +// after you call sendBundle(bundleParams) +let simResult: SimBundleLogs = await mevshare.simulateBundle(bundleParams) +console.log("simResult", simResult) +``` + +Here's a breakdown of the simulation result: + +```tsx +type SimBundleLogs = { + /* True if the simulation executes without error. */ + success: boolean, + /* The error message, if there is an error; otherwise undefined. */ + error?: string, + /* The block that the simulation derived its state from. */ + stateBlock: number, + /* (profit) / (gasUsed) */ + mevGasPrice: bigint, + /* Coinbase profit; the amount paid to the builder after user receives kickback. */ + profit: bigint, + /* Total ETH paid by searcher to coinbase (gas fees + coinbase transfers). */ + refundableValue: bigint, + /* Total gas used by transactions in the bundle. */ + gasUsed: bigint, + logs?: Array<{ + /* ETH transaction logs. */ + txLogs?: Array, + /* Logs for nested bundles. */ + bundleLogs?: Array, + }>, +} +``` + +`LogParams` are defined in the [Ethers documentation](https://docs.ethers.org/v6/api/providers/formatting/#LogParams). + +:::info Simulating private Transactions + +The backend endpoint for simulating bundles (`mev_simBundle`) only accepts signed transactions, not private transactions (specified by `{hash}`). The mev-share-client library automatically waits for the transactions in a bundle specified by `{hash}` to land on-chain by querying for the full signed transactions with `provider.getTransaction` before it calls `mev_simBundle`. If the private transaction(s) in your bundle don't land on-chain, you won't be able to simulate your bundle with them. + +::: + +There are several things you want to look for when debugging your bundles: + +### 'invalid inclusion' + +When simulating your bundles, you might see a result like this: + +```tsx +{ + success: false, + error: 'invalid inclusion', + stateBlock: 17674041, + mevGasPrice: 0n, + profit: 0n, + refundableValue: 0n, + gasUsed: 0n, + logs: undefined +} +``` + +This happens when `bundleParams.inclusion.block` (and `bundleParams.inclusion.maxBlock` if you set it) target a block (range) that doesn't overlap with the simulation state block. + +To remedy this, you can adjust your `bundleParams` when you call `simulateBundle` to target more blocks — Protect transactions target 25 blocks starting from when they were submitted, so you may want to set your bundleParams like this: + +```tsx +const bundleParams = { + inclusion: { block: currentBlockNumber + 1 }, + body: [ + { hash: pendingTxHash }, + { tx: backrunSignedTx, canRevert: false } + ] +} +// ... +sendBundle(bundleParams) +// ... +simulateBundle({ + ...bundleParams, + inclusion: { + ...bundleParams.inclusion, + maxBlock: bundleParams.inclusion.block + 24, + } +}) +``` + +We would just set the `maxBlock` parameter to `inclusion.block + 25`, but we have to subtract 1 since `inclusion.block` was `currentBlockNumber + 1` when we initially defined it. + +You can also simply modify your original `bundleParams` to target more blocks, but note that this opens up the possibility of bundles using an outdated price landing on-chain. + +### 'tx failed' + +```tsx +{ + success: false, + error: 'tx failed', + stateBlock: 17674503, + mevGasPrice: 0n, + profit: 472614000000000n, + refundableValue: 472614000000000n, + gasUsed: 157538n, + logs: undefined +} +``` + +This happens when one of your transactions reverts. Since our `simulateBundle` function waits for private transactions to land on chain before simulating the bundle, we know that in our example, this error means that our backrun transaction failed. + +If you see this error when running the bot, it most likely means that the transaction you tried to backrun didn't affect the price enough to meet our target. This is expected — it's a feature, not a bug! + +However, if you want to be sure, a good way to further verify what's happening is to simulate transactions in a local development environment. Toolkits such as [foundry](https://github.com/foundry-rs/foundry) or [hardhat](https://hardhat.org/docs) give you tools to compile contracts, create local nodes that fork their state from public nodes (e.g. mainnet, goerli), and simulate transactions locally, with stack traces to show you where things went wrong. + +## Did it pay enough gas? + +One of the most common reasons a bundle doesn't land is that it didn't pay enough gas. Of course, you need to pay at least the base fee (the current minimum gas price on Ethereum), but you may also be facing competition. If there are other searchers trying to include the same transaction as you in a bundle, then only one of these bundles can land, because the transaction specified in multiple bundles can only be included on-chain once. The one that lands is determined by who pays the most. + +Increasing the priority fee (`maxPriorityFeePerGas`) on your backrun transaction is a reliable way to improve your chances of inclusion. Note that we also increase `maxFeePerGas` by the same amount — this ensures that the builder/validator gets the tip, and it isn't consumed by the base fee. See this [blog post from Blocknative](https://www.blocknative.com/blog/eip-1559-fees) for a detailed explanation of gas fees on Ethereum. + +Here's one example of how we can set higher gas fees in our code: + +```tsx +async function getSignedBackrunTx( outputAmount: bigint, nonce: number, gasTip: bigint ) { + const backrunTx = await uniswapRouterContract.swapExactTokensForTokens.populateTransaction(SELL_TOKEN_AMOUNT, outputAmount, [SELL_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS], executorWallet.address, 9999999999n) + const backrunTxFull = { + ...backrunTx, + chainId: 1, + maxFeePerGas: MAX_GAS_PRICE * GWEI + gasTip, + maxPriorityFeePerGas: MAX_PRIORITY_FEE * GWEI + gasTip, + gasLimit: TX_GAS_LIMIT, + nonce: nonce + } + return executorWallet.signTransaction(backrunTxFull) +} +``` + +Then we'd call it like this: + +```tsx +// tip: +5 gwei per gas +const tip = 5n * GWEI; +const backrunTx = await getSignedBackrunTx(outputAmount, nonce, tip) +``` + +You'll notice that we set our gas prices (`maxFeePerGas` and `maxPriorityFeePerGas`) to constant values. If you don't mind waiting for the gas price on Ethereum to go down, then you can safely ignore these errors. However, if you want to ensure that your trade goes through regardless of the gas price, then you'll have to track the base fee on Ethereum and adjust your transactions' gas prices accordingly. + +Here's how we can do that in our project: + +```tsx +async function getSignedBackrunTx( outputAmount: bigint, nonce: number, tip: bigint ) { + const gasFees = await provider.getFeeData() + const backrunTx = await uniswapRouterContract.swapExactTokensForTokens.populateTransaction(SELL_TOKEN_AMOUNT, outputAmount, [SELL_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS], executorWallet.address, 9999999999n) + const backrunTxFull = { + ...backrunTx, + chainId: 1, + maxFeePerGas: gasFees.maxFeePerGas! + tip, + maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas! + tip, + gasLimit: TX_GAS_LIMIT, + nonce: nonce + } + return executorWallet.signTransaction(backrunTxFull) +} +``` + +We start off by fetching the gas fees using `provider.getFeeData` — this queries the RPC provider. You may want to do this in a background thread (once per block) to avoid making redundant calls. But it's OK for demonstration's sake. +In `backrunTxFull`, we set our gas parameters with `gasFees`. We use the `!` suffix operator to coerce the value, which is possibly null, into a bigint. This is safe to do when you're sure that your network has EIP-1559 integrated, which Ethereum mainnet does. The values will only be null on old, deprecated networks. + +## Did any transactions fail? If so, which one(s)? + +It's possible that one of the transactions in your bundle encountered an error and was not able to execute. If this is the case, you'll see `false` in the `success` parameter of the simulation response, along with an error message, and some data about gas usage and payment. + +For example, a simulation result for a bundle including a transaction with its gas price set too low might look like this: + +```tsx +{ + success: false, + error: 'max fee per gas less than block base fee: address 0x2326Bd2F29a6004D31344a1FE2329F2C13284f0d, maxFeePerGas: 2000000000 baseFee: 13077974866', + stateBlock: 17674455, + mevGasPrice: 0n, + profit: 288942000000000n, + refundableValue: 288942000000000n, + gasUsed: 192628n, + logs: undefined +} +``` + +If `mevGasPrice` is 0, it just means that the transaction didn't use any gas (in this case, because it reverted). + +When a simulation succeeds, it looks something like this: + +```tsx +{ + success: true, + error: undefined, + stateBlock: 17674443, + mevGasPrice: 9485937386n, + profit: 2731874079688143n, + refundableValue: 2731874079688143n, + gasUsed: 287992n, + logs: [ { txLogs: [Array] }, { txLogs: [Array] } ] +} +``` + +This means that your bundle *could have* landed in a block, but it isn't a guarantee. If your `mevGasPrice` is not high enough, your bundle may not be included in a block. Refer back to [Did it pay enough gas?](#did-it-pay-enough-gas) for info on setting your gas price. + +There is also one more factor that often comes into play… + +## Did the Flashbots builder have a chance to include my bundle? + +Another common reason a bundle doesn't land is simply because the builders on MEV-Share didn't have an opportunity to build a block for the desired slot. For more details about how builders work, and why this happens, see [Searching Post-Merge](https://writings.flashbots.net/searching-post-merge). You can check to see if your target block was built by any of the builders connected to MEV-Share by checking the block's `miner` parameter. The Flashbots builder address is `0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5` + +```tsx +async function isBlockBuiltByFlashbots(blockNum: number) { + const flashbotsCoinbase = "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5" + const block = await provider.getBlock(blockNum) + return block?.miner === flashbotsCoinbase +} +``` + +With this function, look at each block from `bundleParams.inclusion.block` through `bundleParams.inclusion.maxBlock` (if you set it) for a bundle to determine whether it was possible for the bundle to land in that block. \ No newline at end of file diff --git a/docs/flashbots-mev-share/searchers/tutorials/limit-order/debugging.mdx b/docs/flashbots-mev-share/searchers/tutorials/limit-order/debugging.mdx new file mode 100644 index 000000000..68fea6cc6 --- /dev/null +++ b/docs/flashbots-mev-share/searchers/tutorials/limit-order/debugging.mdx @@ -0,0 +1,5 @@ +--- +title: Debugging +--- + +If your bundles aren't landing, there could be a few reasons why. Take a look at our [debugging guide](/flashbots-mev-share/searchers/debugging) for tips on how to simulate and interpret bundle failures. \ No newline at end of file diff --git a/docs/flashbots-mev-share/searchers/tutorials/limit-order/introduction.mdx b/docs/flashbots-mev-share/searchers/tutorials/limit-order/introduction.mdx new file mode 100644 index 000000000..c9a0a6688 --- /dev/null +++ b/docs/flashbots-mev-share/searchers/tutorials/limit-order/introduction.mdx @@ -0,0 +1,49 @@ +--- +title: Introduction +--- + +MEV-Share is a new protocol for sending and searching on Ethereum transactions. In this guide, we'll write our own bot to see first-hand how MEV-Share works, and how to find profitable opportunities. + +This guide assumes you have some programming experience, but we've tried to make it as beginner-friendly as possible! No prior searching experience will be required. + +We'll provide full examples with code so you can follow along and run the bot yourself! + +## Limit Orders + +In this guide, we'll be making a version of a [limit order](https://www.investor.gov/introduction-investing/investing-basics/how-stock-markets-work/types-orders#:~:text=A%20limit%20order%20is%20an,for%20no%20more%20than%20%2410.) bot. Limit orders are a common feature on exchanges which let you fill an order when the price of a trading pair (e.g. ETH/DAI) reaches a target that you specify. For example, if I wanted to buy DAI when the price reaches 1800 DAI/ETH, I could place a limit order for to buy 1800 DAI for 1 ETH, and the trade would automatically execute when the price reached 1800. If the price was over 1800, then we'd want to fill our order at the higher price — since we're buying DAI, we want more DAI out for a fixed amount of ETH in. + +Limit orders are useful if you are sensitive to price but not time. For example, I might have a lot of ETH and I want to make sure I get a good price for it. But it's okay if it takes week to complete the trades. + +The bot we're building works like a traditional limit order — we'll buy when the price reaches our target. But in this case, we'll have an additional edge: private orderflow (and some clever code). + +## MEV-Share Bot + +We'll use the MEV-Share event stream to watch for pending transactions (private orderflow) that change the price of our trading pair. Then we'll backrun each of those transactions with our ideal trade. When a transaction sufficiently shifts the price in our favor, our backrun will be first in line to buy the tokens at a discounted rate. + +Our backrun transaction will specify an exact price at which the order can be filled, otherwise the transaction reverts. Because we're sending to Flashbots, reverted transactions won't land on chain and we won't pay any fees for failed attempts. + +In short, we'll attempt to backrun all trades for the assets we care about — but only land the backruns that execute at a desirable price. + +> This guide is based on the [simple-limit-order-bot repo](https://github.com/flashbots/simple-limit-order-bot). If you want to get straight to the code, the repo contains a fully-operational bot that only requires a `.env` file for you to run. + +## Glossary + +*Remember the following terminology (because we use it a lot!):* + +- **MEV**: [Maximal Extractable Value](https://ethereum.org/en/developers/docs/mev/). +- **bundle**: an array of transactions that execute in order and [atomically](https://en.wikipedia.org/wiki/Atomicity_(database_systems)). +- **backrun**: a transaction sent immediately after another transaction. + - example: a “backrun bundle” is an array with two or more transactions; the first transaction (presumably chosen from a public pool of pending transactions) creates an MEV opportunity, which the following transactions try to capture. +- **orderflow**: umbrella term for transactions or bundles. +- **searcher**: a bot operator (or bot) that attempts to extract MEV. +- **on-chain**: a transaction that lands “on chain” (or on-chain, or onchain) is permanently included on the blockchain. +- **WETH**: “Wrapped ETH” — ERC-20 version of ETH, used by Uniswap in trading pairs +- **MEV-Share**: “a protocol that lets users selectively share orderflow and information with searchers”. + +:::info Running this bot requires that you have an Ethereum account with some ETH and WETH. + +Our example uses 1/10000000000 of an ETH — you may want more. If you don't have any WETH, you can wrap ETH into WETH on Uniswap: + +![Example weth swap](/img/weth-swap.png) + +::: info \ No newline at end of file diff --git a/docs/flashbots-mev-share/searchers/tutorials/limit-order/more-resources.mdx b/docs/flashbots-mev-share/searchers/tutorials/limit-order/more-resources.mdx new file mode 100644 index 000000000..010ae268d --- /dev/null +++ b/docs/flashbots-mev-share/searchers/tutorials/limit-order/more-resources.mdx @@ -0,0 +1,31 @@ +--- +title: More Resources +--- + +We hope you found this guide useful! This is only the first of several, in which we'll show you more exciting ways to use MEV-Share, and all its unique features. In future guides, we'll talk about how to write more advanced bots that extract MEV opportunities, how to maximize your profits by using hints, and cover advanced features and techniques that are made possible by MEV-Share. + +While we work on more guides, we encourage you to check out our other resources on MEV-Share, and get in touch with us on Discord, on our Forum, or @ one of us on Twitter! + +- [Discord](https://discord.gg/flashbots) +- [Forum](https://collective.flashbots.net/) +- Twitter handles for MEV-Share: [@SheaKetsdever](https://twitter.com/SheaKetsdever) [@epheph](https://twitter.com/epheph) [@drog_v](https://twitter.com/drog_v) [@zeroXbrock](https://twitter.com/zeroxbrock) + +**Documentation** + +- [MEV-Share Documentation](https://docs.flashbots.net/flashbots-mev-share/overview) +- [MEV-Share Spec](https://github.com/flashbots/mev-share) + +**Code Examples** + +- https://github.com/flashbots/simple-limit-order-bot (Typescript) +- https://github.com/flashbots/simple-blind-arbitrage (Javascript) +- https://github.com/paradigmxyz/artemis (Rust) + +**MEV-Share Client Libraries** + +- https://github.com/flashbots/matchmaker-ts (Typescript) +- https://github.com/paradigmxyz/mev-share-rs (Rust) + +**MEV-Share Backend Node** + +- https://github.com/flashbots/mev-share-node \ No newline at end of file diff --git a/docs/flashbots-mev-share/searchers/tutorials/limit-order/sending-bundles.mdx b/docs/flashbots-mev-share/searchers/tutorials/limit-order/sending-bundles.mdx new file mode 100644 index 000000000..da05d33bc --- /dev/null +++ b/docs/flashbots-mev-share/searchers/tutorials/limit-order/sending-bundles.mdx @@ -0,0 +1,184 @@ +--- +title: Sending Bundles +--- + +## Getting our trade ready + +After we find a transaction that touches the trading pair we're targeting, we need to calculate how many tokens we should expect to receive. In this project, we are specifically *buying* DAI with ETH (technically WETH, because Uniswap only trades ERC-20 tokens), so we want the price of DAI/WETH to be as low as possible. In code, we define our price requirements in terms of the *amount of tokens we receive* from the trade — so we want the *highest* amount of tokens possible, but we have a definite minimum (1800 DAI). + +In terms of a limit order, we're saying that we want our order to be filled when the price is *at most* 1800 DAI, but if the price is lower, then we want to fill at the lower price, which yields more DAI per WETH. + +To find our token's market price, we simulate our trade by calling the `swapExactTokensForTokens` function with a static call. A static call simply simulates the transaction, so we can see what it would do if we were to actually send it. We set our buy/sell amounts that we defined earlier to see how much we'd get from the swap. Add this function to your code — we'll add it to our main function later. + +`src/index.ts` + +```tsx +async function getBuyTokenAmountWithExtra() { + const resultCallResult = await uniswapRouterContract + .swapExactTokensForTokens + .staticCallResult( + SELL_TOKEN_AMOUNT, + 1n, + [SELL_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS], + executorWallet.address, + 9999999999n + ) + const normalOutputAmount = resultCallResult[0][1] + const extraOutputAmount = normalOutputAmount * (10000n + DISCOUNT_IN_BPS) / 10000n + return extraOutputAmount +} +``` + +The minimum amount we can expect to receive from a swap is defined by `normalOutputAmount`. Then, we calculate how much we'd get if with a 40 basis-points discount, which we should expect if we successfully backrun a transaction that shifts the price in our favor, and assign this value to `extraOutputAmount`. + +When we detect a new transaction, we'll need to check the going price and set up our trade accordingly. If the price is lower than our target, and because we're trying to *buy* tokens, we want to make sure our trade expects more tokens out; as many as we can get at the lower price with our fixed sell amount (the ETH we'll spend to buy the tokens). If the price is higher than our target, then we'll just set the expected output to the minimum amount we'd expect to if the price were at our target, in hopes that the transaction we backrun will move the price enough for us to make a trade. + +Let's add a couple more functions to implement this logic: + +`src/index.ts` + +```tsx +async function getSignedBackrunTx( outputAmount: bigint, nonce: number ) { + const backrunTx = await uniswapRouterContract.swapExactTokensForTokens.populateTransaction(SELL_TOKEN_AMOUNT, outputAmount, [SELL_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS], executorWallet.address, 9999999999n) + const backrunTxFull = { + ...backrunTx, + chainId: 1, + maxFeePerGas: MAX_GAS_PRICE * GWEI, + maxPriorityFeePerGas: MAX_PRIORITY_FEE * GWEI, + gasLimit: TX_GAS_LIMIT, + nonce: nonce + } + return executorWallet.signTransaction(backrunTxFull) +} + +async function backrunAttempt( currentBlockNumber: number, nonce: number, pendingTxHash: string ) { + let outputAmount = await getBuyTokenAmountWithExtra() + if (outputAmount < BUY_TOKEN_AMOUNT_CUTOFF) { + console.log(`Even with extra amount, not enough BUY token: ${ outputAmount.toString() }. Setting to amount cut-off`) + outputAmount = BUY_TOKEN_AMOUNT_CUTOFF + } + const backrunSignedTx = await getSignedBackrunTx(outputAmount, nonce) + try { + const sendBundleResult = await mevshare.sendBundle({ + inclusion: { block: currentBlockNumber + 1 }, + body: [ + { hash: pendingTxHash }, + { tx: backrunSignedTx, canRevert: false } + ] + },) + console.log('Bundle Hash: ' + sendBundleResult.bundleHash) + } catch (e) { + console.log('err', e) + } +} +``` + +The `getSignedBackrunTx` function creates the transaction we'll send to execute our trade on Uniswap. We set a fixed gas price here for simplicity. If you prefer, you could replace the with dynamic fees that track the base fee of the chain. But constant gas prices may work better if you don't want to spend a lot on gas, and don't mind having to wait if the network's base fee exceeds your settings. + +The `backrunAttempt` function defines our price requirement logic: we make sure that `outputAmount` is at least our previously-defined cutoff amount. However, if the simulated output amount is higher, then we set `outputAmount` to expect that much, which protects us from [slippage](https://en.wikipedia.org/wiki/Slippage_(finance)) in case other transactions in the block happen to trade on the same pair. This function then sends our bundle to MEV-Share. If the bundle was received successfully, we should see a bundle hash logged to our console. + +Using `getBuyTokenAmountWithExtra`, we define `outputAmount`, the amount we expect to receive from the trade. We create our backrun transaction `backrunSignedTx` and send it to MEV-Share in a bundle by calling `mevshare.sendBundle`. + +*Real quick, let's break down the bundle we passed to `sendBundle`:* + +The `block` parameter in `inclusion` specifies which block we want the bundle to land in. We indicate here that we want our bundle to land in the next block. + +The `body` parameter is where we set our bundle's transactions. The order in which they're specified is the order in which they'll execute on chain. Each transaction is specified as an object, with either a `hash` parameter, or a `tx` parameter (paired with `canRevert` to specify whether this transaction is allowed to revert and land on chain). The transaction we specify with `hash` is the pending transaction from the event stream that we want to backrun. We have to use its hash because the MEV-Share event stream does not reveal the entire signed transaction. Naturally, the following transaction, specified by `tx`, is our trade. + +Once we stitch all these new functions into our main loop, our bot will be done! + +## Sending a backrun bundle + +When we detect a new pending transaction in the `mevshare.on("transaction")` callback that affects the price of our target pair, we need to send a bundle using the `backrunAttempt` function. This bundle checks our target price and sets up our trade to get us the best price possible. + +`src/index.ts` + +```tsx +mevshare.on("transaction", (pendingTx) => { + // ... + // TODO: backrun the user tx + if (!transactionIsRelatedToPair(pendingTx, PAIR_ADDRESS)) { + console.log('skipping tx: ' + pendingTx.hash) + return + } + console.log(`It's a match: ${ pendingTx.hash }`) + const currentBlockNumber = await provider.getBlockNumber() + backrunAttempt(currentBlockNumber, nonce, pendingTx.hash) +}) +``` + +We'll also want to set up a callback that watches for new blocks and retries previous backrun attempts. Our bundles only target one block, but Protect transactions (which make up the transactions in the event stream) are valid for 25 blocks from when they're received. This means that if our backrun wasn't successful before, we can try again up to 24 more times. + +Add this code to your `main` function: + +```tsx +let recentPendingTxHashes: Array<{ txHash: string, blockNumber: number }> = [] +provider.on('block', ( blockNumber ) => { + for (const recentPendingTxHash of recentPendingTxHashes) { + console.log(recentPendingTxHash) + backrunAttempt(blockNumber, nonce, recentPendingTxHash.txHash) + } + // Cleanup old pendingTxHashes + recentPendingTxHashes = recentPendingTxHashes.filter(( recentPendingTxHash ) => + blockNumber > recentPendingTxHash.blockNumber + BLOCKS_TO_TRY) +}) +``` + +And in your `mevshare.on` callback, add this piece at the end: + +```tsx +recentPendingTxHashes.push({ txHash: pendingTx.hash, blockNumber: currentBlockNumber }) +``` + +When you're done, your main function should look like this: + +`src/index.ts` + +```tsx +async function main() { + console.log('mev-share auth address: ' + authSigner.address) + console.log('executor address: ' + executorWallet.address) + const PAIR_ADDRESS = (await uniswapFactoryContract.getPair(SELL_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS)).toLowerCase() + await approveTokenToRouter(SELL_TOKEN_ADDRESS, UNISWAP_V2_ADDRESS) + const nonce = await executorWallet.getNonce('latest') + let recentPendingTxHashes: Array<{ txHash: string, blockNumber: number }> = [] + + mevshare.on('transaction', async ( pendingTx: IPendingTransaction ) => { + if (!transactionIsRelatedToPair(pendingTx, PAIR_ADDRESS)) { + console.log('skipping tx: ' + pendingTx.hash) + return + } + console.log(`It's a match: ${ pendingTx.hash }`) + const currentBlockNumber = await provider.getBlockNumber() + backrunAttempt(currentBlockNumber, nonce, pendingTx.hash) + recentPendingTxHashes.push({ txHash: pendingTx.hash, blockNumber: currentBlockNumber }) + }) + provider.on('block', ( blockNumber ) => { + for (const recentPendingTxHash of recentPendingTxHashes) { + console.log(recentPendingTxHash) + backrunAttempt(blockNumber, nonce, recentPendingTxHash.txHash) + } + // Cleanup old pendingTxHashes + recentPendingTxHashes = recentPendingTxHashes.filter(( recentPendingTxHash ) => + blockNumber > recentPendingTxHash.blockNumber + BLOCKS_TO_TRY) + }) +} +``` + +> For a full, working code example, check out https://github.com/flashbots/simple-limit-order-bot + +![Mario finish](/img/mario-finish.png) + +*That's all you need!* This code will listen for new transactions and blocks, and trigger our code to send bundles when we find a transaction that changes the price. + +Run the code and you should see something like this: + +![First logs](/img/limit-order-logs-1.png) + +And after some time… + +![Second logs](/img/limit-order-logs-2.png) + +It may take a while to find a match — remember, the code scans for a trade on the ETH/DAI pair on Uniswap V2. There are lots of other events to consider for future improvements to this bot! + +You may also consider removing the code that checks the event to see if it matches our pair address. It's possible that the price could move to our target level without us seeing an event to backrun. If we simply backrun every transaction we see, then we can potentially benefit from opportunities that we can't yet see. However, it is essential to understand how to use logs on MEV-Share, as they provide critical data that can be used for a multitude of other purposes, so we've introduced it here as a practical example. \ No newline at end of file diff --git a/docs/flashbots-mev-share/searchers/tutorials/limit-order/setup.mdx b/docs/flashbots-mev-share/searchers/tutorials/limit-order/setup.mdx new file mode 100644 index 000000000..a16056b50 --- /dev/null +++ b/docs/flashbots-mev-share/searchers/tutorials/limit-order/setup.mdx @@ -0,0 +1,159 @@ +--- +title: Set Up +--- + +We'll be writing this bot in Typescript. Client libraries and examples for other languages will be available soon. + +## Starting a new bot project + +First, some boilerplate project setup. Run these commands to set up a new typescript project. + +```bash +mkdir simple-limit-order-bot && cd simple-limit-order-bot +yarn init +# install typescript & eslint dev dependencies +yarn add -D @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser dotenv eslint eslint-plugin-tsdoc ts-node typescript +# install ethers & mev-share client +yarn add ethers @flashbots/mev-share-client +``` + +Now in your editor, make a `src` directory and add a new file called `index.ts` + +Then, import the required dependencies. + +`src/index.ts` + +```tsx +import MevShareClient, {IPendingTransaction} from '@flashbots/mev-share-client' +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +``` + +We'll use the mev-share-client library to listen for new pending transactions, and we'll use ethers to create and sign our own transactions, and query the blockchain. + +Lastly, we'll create a file called `.env` in the project root directory to store our private variables, such as private keys and RPC endpoints. + +`.env` + +``` +RPC_URL= +EXECUTOR_KEY= +FB_REPUTATION_KEY= +``` + +Fill this in with your own values. + +- `RPC_URL` is the Ethereum RPC endpoint we'll use to query smart contract values and account balances. [Alchemy](https://www.alchemy.com/), [Quicknode](https://www.quicknode.com/), and [Infura](https://www.infura.io/) are popular options for free RPC endpoints. +- `EXECUTOR_KEY` is the private key that will send transactions; it should have at least 0.05 ETH in it to pay for our trade (or more, if you want to make a larger trade than our example). +- `FB_REPUTATION_KEY` is the private key used to sign the payload sent to Flashbots, and is used for tracking searcher reputation. If you earn a high reputation, you may be placed in a high-priority queue, which is prioritized during periods of high traffic. This account *should not* have any ETH in it. + +Your project should now look like this: + +![Project setup](/img/limit-order-project-setup.png) + +:::info *Use [cast](https://github.com/foundry-rs/foundry#readme) to generate private keys for cool addresses like this:* + +```bash +cast wallet vanity --starts-with babe +``` + +``` +Starting to generate vanity address... +Successfully found vanity address in 0 seconds. +Address: 0xbabe32A9112Dc37a0A9274c86CAD0D1676fEA55a +Private Key: 😉 +``` + +::: info + +Next we'll read in the variables from our .env file with `dotenv`. + +Add the following code to your project: + +`src/index.ts` + +```tsx +import dotenv from "dotenv" +dotenv.config() + +const RPC_URL = process.env.RPC_URL || 'http://127.0.0.1:8545' +const EXECUTOR_KEY = process.env.EXECUTOR_KEY || Wallet.createRandom().privateKey +const FB_REPUTATION_PRIVATE_KEY = process.env.FB_REPUTATION_KEY || Wallet.createRandom().privateKey +``` + +> Notice we set default values with the **||** operator. You can omit these if you prefer the variables to remain undefined when they're not set in the .env file. +> + +## Connecting to smart contracts to get prices and make trades + +To get the price of our trading pair and make trades, we'll need to interact with a few smart contracts: the Uniswap V2 Router, the ERC20 token contracts, and the factory contract. + +- **Uniswap V2 Router contract**: smart contract to trade tokens on Uniswap V2. We also use it to get the market price of the trading pair, by simulating a small trade. +- **factory contract**: this is where Uniswap trading pairs are created. We use it to find the pair address for the tokens we want to trade (e.g. WETH/DAI). +- **ERC20 token contract**: the tokens themselves; in our example, we use the WETH contract to call `approve`, so that the router can transfer our WETH tokens for us. + +To do this in our code, we'll create contract instances using ethers. We instantiate contracts with the ABI and contract address of each contract we want to use. The ABI specifies the functions that can be called on the contract. + +For convenience, we've gathered the ABIs required to create ethers contracts for Uniswap V2. Copy these into a new file `src/abi.ts`. + +`src/abi.ts` + +```tsx +export const UNISWAP_V2_ABI = [{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"amountADesired","type":"uint256"},{"internalType":"uint256","name":"amountBDesired","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountTokenDesired","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsIn","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"reserveA","type":"uint256"},{"internalType":"uint256","name":"reserveB","type":"uint256"}],"name":"quote","outputs":[{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETHSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermit","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermitSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityWithPermit","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapETHForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] +export const UNISWAP_FACTORY_ABI = [{"inputs":[{"internalType":"address","name":"_feeToSetter","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":false,"internalType":"address","name":"pair","type":"address"},{"indexed":false,"internalType":"uint256","name":"","type":"uint256"}],"name":"PairCreated","type":"event"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"allPairs","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"allPairsLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"}],"name":"createPair","outputs":[{"internalType":"address","name":"pair","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"feeTo","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"feeToSetter","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"getPair","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_feeTo","type":"address"}],"name":"setFeeTo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_feeToSetter","type":"address"}],"name":"setFeeToSetter","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] +export const ERC20_ABI = [{ "constant": true, "inputs": [], "name": "name", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }], "name": "transfer", "outputs": [{ "name": "", "type": "bool" }], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "payable": true, "stateMutability": "payable", "type": "fallback" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" }], "name": "Transfer", "type": "event" }] +``` + +We need to add this import in `src/index.ts`, and then write just a little more boilerplate code. It should look like this all together: + +```tsx +import MevShareClient, {IPendingTransaction} from '@flashbots/mev-share-client' +import { Contract, JsonRpcProvider, Wallet } from 'ethers' +import { UNISWAP_V2_ABI, UNISWAP_FACTORY_ABI, ERC20_ABI } from './abi' // <-- new import +import dotenv from "dotenv" +dotenv.config() + +const RPC_URL = process.env.RPC_URL || 'http://127.0.0.1:8545' +const EXECUTOR_KEY = process.env.EXECUTOR_KEY || Wallet.createRandom().privateKey +const FB_REPUTATION_PRIVATE_KEY = process.env.FB_REPUTATION_KEY || Wallet.createRandom().privateKey + +// create web3 provider & wallets, connect to mev-share +const provider = new JsonRpcProvider(RPC_URL) +const executorWallet = new Wallet(EXECUTOR_KEY, provider) +const authSigner = new Wallet(FB_REPUTATION_PRIVATE_KEY, provider) +const mevshare = MevShareClient.useEthereumGoerli(authSigner) +// if you want to connect to mainnet instead: +// const mevshare = MevShareClient.useEthereumMainnet(authSigner) + +// create contract instances +const UNISWAP_V2_ADDRESS = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d' +const UNISWAP_FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' +const uniswapRouterContract = new Contract(UNISWAP_V2_ADDRESS, UNISWAP_V2_ABI, executorWallet) +const uniswapFactoryContract = new Contract(UNISWAP_FACTORY_ADDRESS, UNISWAP_FACTORY_ABI, provider) + +/* While we're here, let's also set some useful constants we'll use later */ +// discount we expect from the backrun trade (basis points): +const DISCOUNT_IN_BPS = 40n +// try sending a backrun bundle for this many blocks: +const BLOCKS_TO_TRY = 24 +// WETH: +const SELL_TOKEN_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' +const SELL_TOKEN_AMOUNT = 100000000n +// DAI: +const BUY_TOKEN_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f' +const BUY_TOKEN_AMOUNT_CUTOFF = SELL_TOKEN_AMOUNT * 1800n + +const TX_GAS_LIMIT = 400000 +const MAX_GAS_PRICE = 20n +const MAX_PRIORITY_FEE = 5n +const GWEI = 10n ** 9n +``` + +`uniswapRouterContract`is the contract we use to execute trades. + +`uniswapFactoryContract` is used to find the contract address of the token pair we trade on (e.g. [WETH/DAI](https://etherscan.io/address/0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11)). + +`SELL_TOKEN_ADDRESS` is the token we want to sell (in this case, WETH). The token we're buying in this example is [DAI](https://etherscan.io/address/0x6b175474e89094c44da98b954eedeac495271d0f). Choose whichever tokens you want to trade if you're following along — you can check to see if your tokens have a pair by calling the `getPair` function with your token addresses on the Uniswap [factory contract](https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f#readContract). + +`SELL_TOKEN_AMOUNT` specifies 0.1 gwei of WETH to spend, and in `BUY_TOKEN_AMOUNT_CUTOFF` we specify that we want to buy when the token price is **1800** DAI/WETH. + +*We set our gas fees (see `MAX_GAS_PRICE` and `MAX_PRIORITY_FEE`) to constant values for the example. If you don't mind possibly paying more gas, we recommend setting your gas parameters so that they follow the base fee. Ethers has a function for this called [getFeeData](https://docs.ethers.org/v5/single-page/#/v5/api/providers/provider/-%23-Provider-getFeeData).* \ No newline at end of file diff --git a/docs/flashbots-mev-share/searchers/tutorials/limit-order/using-events.mdx b/docs/flashbots-mev-share/searchers/tutorials/limit-order/using-events.mdx new file mode 100644 index 000000000..5c40b1162 --- /dev/null +++ b/docs/flashbots-mev-share/searchers/tutorials/limit-order/using-events.mdx @@ -0,0 +1,140 @@ +--- +title: Using Events +--- + +## Finding pending transactions with MEV-Share Event Stream + +Now that we have all the setup done, we need to find some transactions to backrun. Remember, we're looking for transactions that affect the price of our trading pair (WETH/DAI), so that we can take advantage of the price impact created by the transaction to get a better price for our trade. By placing the two transactions in a bundle (the transaction we found on the event stream, and our backrun trade), we ensure that our trade only executes if it gets placed immediately behind the transaction that causes the price impact that benefits us. + +We'll start by listening to the MEV-Share event stream. The event stream shares data about pending transactions (and bundles). We can use this information to deduce whether a transaction affects the price of our trading pair. + +In our project, we add a main function at the bottom, and in it, use the mev-share client's `.on` function to execute our own code when we receive new pending transaction events. To start, let's just look at the event stream by printing each event to the console. + +`src/index.ts` + +```tsx +// ... previous code still up here ^ + +async function main() { + console.log("mev-share auth address: " + authSigner.address) + console.log("executor address: " + executorWallet.address) + + // bot only executes one trade, so get the nonce now + const nonce = await executorWallet.getNonce("latest") + + mevshare.on('transaction', async ( pendingTx: IPendingTransaction ) => { + // callback to handle pending transaction + console.log(pendingTx) + }) +} +main() +``` + +Try running this: + +```bash +npx ts-node src/index.ts +``` + +You should see events popping up in the console. Something like this: + +![MEV-Share events](/img/mev-share-events.png) + +> Traffic is often low on goerli, so you may want to try connecting to mainnet with `MevShareClient.useEthereumMainnet(authSigner)` to see more events. + +The logs filling up your console are all transactions which we can backrun. You'll notice that some share more data than others. We'll cover how to include these transactions in our bundles soon, but first we need to go a little deeper to understand these events, and how we might use them to our advantage. + +## Finding backrun opportunities using event data + +Transactions on MEV-Share can share a wide variety of data with searchers. They can choose to only share their transaction hash to maintain the most privacy, or they can share logs, calldata, the function selector, and/or the `to` address of their transaction. Sharing more data gives searchers more options for running MEV strategies with those transactions, and so improves the chances of those transactions landing on chain quickly. + +:::info For the Adventurous + +💡 Bundles can also be shared on MEV-Share. If you query the raw stream (you can view it in your web browser here: https://mev-share.flashbots.net), you'll see that each event actually has a `txs` property, which itself may contains transaction events. In the client library, we convert the events into transaction or bundle types for you. To listen for bundles with the client lib, call the function `.on("bundle", ...)`. We'll talk more about this in a later guide. + +:::info + +In our project, we want to know whether a transaction interacts with the token pair that we want to trade on (in our case, WETH/DAI). We want to know this because these transactions might move the price towards our target price. + +To find out which trading pair a transaction is interacting with, we need to look at one of two fields: `to` and `logs`, depending on which is shared by the sender of the transaction. + +The `to` address is the actual recipient of the transaction. Typically, this would be a router contract like the [Uniswap Universal Router](https://etherscan.io/address/0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD), which allows users of the Uniswap web app to split trades between multiple Uniswap liquidity pools. However, we're not interested in any Uniswap routers. We're looking for the token pair contract (e.g. [WETH/DAI](https://etherscan.io/address/0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11)). If the `to` field is the pair address, the transaction sender is trading directly on the pair, which may mean that the transaction is from another searcher, as web UI users would most likely be interacting with the router contract. Nevertheless, if someone is trading on the token pair, we want to try to backrun it. + +If the `logs` field is specified, we look for the pair address in the `address` field of one of the logs. MEV-Share, by default, shares the pair address in logs for swaps on Uniswap V2/V3 (V4 coming soon), Curve and Balancer. We'll look more in depth at decoding logs in a later guide, but for now it's sufficient to simply look for the pair address — if a log contains this address, then we can be confident that the transaction in the event is a good candidate to backrun. + +Add the following code in your project: + +`src/index.ts` + +```tsx +// preceding code omitted for brevity + +function transactionIsRelatedToPair(pendingTx: IPendingTransaction, PAIR_ADDRESS: string) { + return pendingTx.to === PAIR_ADDRESS || + ((pendingTx.logs || []).some(log => log.address === PAIR_ADDRESS)) +} +``` + +Additionally, we'll need to approve the router to spend our WETH. Add this function: + +```tsx +async function approveTokenToRouter( tokenAddress: string, routerAddress: string ) { + const tokenContract = new Contract(tokenAddress, ERC20_ABI, executorWallet) + const allowance = await tokenContract.allowance(executorWallet.address, routerAddress) + const balance = await tokenContract.balanceOf(executorWallet.address) + if (balance == 0n) { + console.error("No token balance for " + tokenAddress) + process.exit(1) + } + if (allowance >= balance) { + console.log("Token already approved") + return + } + await tokenContract.approve(routerAddress, 2n**256n - 1n) +} +``` + +Then at the start of your `main` function, find the smart contract address for the token pair we want to trade, and call our function to approve the router to trade our WETH: + +```tsx +const PAIR_ADDRESS = (await uniswapFactoryContract.getPair( + SELL_TOKEN_ADDRESS, + BUY_TOKEN_ADDRESS + )).toLowerCase() +await approveTokenToRouter(SELL_TOKEN_ADDRESS, UNISWAP_V2_ADDRESS) +``` + +Then, where we handle new pending transactions, call `transactionIsRelatedToPair` to see if we should backrun the transaction. + +Your main function should similar to this when you're done: + +```tsx +async function main() { + console.log('mev-share auth address: ' + authSigner.address) + console.log('executor address: ' + executorWallet.address) + const PAIR_ADDRESS = (await uniswapFactoryContract.getPair(SELL_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS)).toLowerCase() + await approveTokenToRouter(SELL_TOKEN_ADDRESS, UNISWAP_V2_ADDRESS) + const nonce = await executorWallet.getNonce('latest') + + mevshare.on('transaction', async ( pendingTx: IPendingTransaction ) => { + if (!transactionIsRelatedToPair(pendingTx, PAIR_ADDRESS)) { + console.log('skipping tx: ' + pendingTx.hash) + return + } + console.log(`It's a match: ${ pendingTx.hash }`) + }) +} +``` + +If you run the code now, you'll probably see a lot of skipped transactions, but eventually you'll find a match! If you're not seeing any activity, try switching to mainnet: + +```tsx +// const mevshare = MevShareClient.useEthereumGoerli(authSigner) +// if you want to connect to mainnet instead: +const mevshare = MevShareClient.useEthereumMainnet(authSigner) +``` + +:::info For the Adventurous +Try logging `pendingTx` in its entirety; look at all the fields. Or in code, check out the [interface](https://github.com/flashbots/matchmaker-ts/blob/main/src/api/interfaces.ts#L258) directly. See if you can find any patterns in the `logs` parameter. + +:::info \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index 833016b85..2f0fde953 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -135,6 +135,21 @@ module.exports = { 'flashbots-mev-share/searchers/event-stream', 'flashbots-mev-share/searchers/understanding-bundles', 'flashbots-mev-share/searchers/sending-bundles', + 'flashbots-mev-share/searchers/debugging', + { + 'Tutorials': [ + { + 'Limit Order Bot': [ + 'flashbots-mev-share/searchers/tutorials/limit-order/introduction', + 'flashbots-mev-share/searchers/tutorials/limit-order/setup', + 'flashbots-mev-share/searchers/tutorials/limit-order/using-events', + 'flashbots-mev-share/searchers/tutorials/limit-order/sending-bundles', + 'flashbots-mev-share/searchers/tutorials/limit-order/debugging', + 'flashbots-mev-share/searchers/tutorials/limit-order/more-resources' + ] + } + ] + } ], 'For Wallet/Dapp Developers': [ { diff --git a/static/img/limit-order-logs-1.png b/static/img/limit-order-logs-1.png new file mode 100644 index 000000000..e056f48b5 Binary files /dev/null and b/static/img/limit-order-logs-1.png differ diff --git a/static/img/limit-order-logs-2.png b/static/img/limit-order-logs-2.png new file mode 100644 index 000000000..97354b460 Binary files /dev/null and b/static/img/limit-order-logs-2.png differ diff --git a/static/img/limit-order-project-setup.png b/static/img/limit-order-project-setup.png new file mode 100644 index 000000000..a6972645f Binary files /dev/null and b/static/img/limit-order-project-setup.png differ diff --git a/static/img/mario-finish.png b/static/img/mario-finish.png new file mode 100644 index 000000000..e48885349 Binary files /dev/null and b/static/img/mario-finish.png differ diff --git a/static/img/mev-share-events.png b/static/img/mev-share-events.png new file mode 100644 index 000000000..f05ffd2c9 Binary files /dev/null and b/static/img/mev-share-events.png differ diff --git a/static/img/weth-swap.png b/static/img/weth-swap.png new file mode 100644 index 000000000..04887150e Binary files /dev/null and b/static/img/weth-swap.png differ