Open Source Trading #1: Airdrop Claim & Sell
Intro
The crypto market is pretty boring right now, so I figured there’s no better time to do something that I’ve always planned on doing: open source any useful code I’ve written when I “retire” from active trading.
Well, I’m not actually retiring, and realistically, I don’t plan on doing so any time soon. But rather than open sourcing multi-year-old code sometime in the future, I decided that I could begin by periodically open sourcing code that I’m not actively using anymore - and likely won’t use ever again - through a series of articles.
Why would I open source (currently) useless code? A couple of reasons:
Educational purposes. Although the exact code I post has no alpha in it anymore, all of it can be repurposed for other use cases, and the thought process behind each idea’s development should offer valuable insights. At least, that’s my hope.
Seeding new ideas. Especially with the market having cooled off a bit, it’s a good time to work on tooling that you’ve always wanted to have but could never find a good time to build. Maybe something in this article or a future one sparks an idea, and you can use this lull in the market to build it. (If you come up with something promising and want to discuss, please reach out.)
I will note that these articles are meant for a generally technical, crypto-native audience. However, I’ll begin each article with a non-technical outline of the idea before moving on to the actual implementation.
For the first article (this one), I’m open sourcing a bot I used to claim and sell an airdropped token that snipers briefly pushed to an unsustainable $4b+ FDV. I’ll go over the idea, initial thought process, implementation, and result - as well as link to the full code at the end. Let’s get into it.
Initial Idea
In early December 2023, Tree of Alpha (ToA) announced the TREE token, which would serve as a utility token for Tree News. The litepaper explained the initial allocations of the 200m total tokens: 50% airdropped to Tree News users, 45% to the treasury, and 5% to ToA / initial liquidity.
As you can see, Tree News NFT holders would receive the majority of the airdrop - but I was happy to see that previous subscribers would get some tokens too. Nothing sizable at where I figured the token would trade, but free money is free money.
As I looked closer though, there were some notable details about this specific launch that got me thinking.
First, given that ToA said only part of his 5% would go into initial liquidity, there would potentially be a very small % of the total supply in the TREE-WETH LP at launch.
Second, there would be a large (50%), immediately unlocked airdrop, and the remaining 45% of coins would not be in circulation at launch.
Together, these meant there could be a scenario where initial buyers (snipers) were competing over a small % of token supply - as long as the majority of the liquid supply (airdropped tokens) had not hit the market yet. If that occurred, there could be a short opportunity to sell TREE at some massively inflated valuation.
However, this was not guaranteed, as there were roughly three possible cases:
Airdrop claims open well before trading goes live, giving recipients ample time to claim and prepare to sell. In this case, price would be suppressed from the very start by these claimers.
Airdrop claims open at the same time as trading goes live. In this case, price would be suppressed by the claimers who were fastest to claim and sell their tokens.
Airdrop claims open after trading goes live. Similar to case #2, price would only become suppressed once tokens were claimed and sold - but in this case there may be more time for trading to settle at some more logical market cap as early snipers exited.
Since non-NFT holders only would receive a small number of tokens, I figured that the only way I could make significant profit would be by selling at some inflated valuation, which would be possible in case #2 or potentially case #3, and by selling faster than other claimers. And the more I thought about it, the more I thought it was possible.
Sniping new coins on Ethereum was popular at the time. Many snipers sniped “blindly”, meaning that they’d spend ETH to buy regardless of what # of tokens they’d receive, or what % of supply was in the LP. Effectively, this meant no limit on what price they’d snipe at, other than a “max buy limit” that some coins imposed.
TREE was already deployed, so I could confirm that there were no taxes or max buy limits in place. So, if only a small % of supply was put into the initial LP, and airdrop claimers couldn’t sell immediately, snipers could fire large chunks of ETH at this small % of supply with no cap on their buy price. TREE was a well-known enough launch that I felt this was likely, and if these snipers didn’t have automated limit sells in place, price could sustain inflated levels for at least a few blocks.
Additionally, the airdrop would be going to a relatively constrained recipient base (something like 300-400 users), with only a few of them, as far as I knew, being technical onchain. With only one week from the litepaper release, and four days from the token / airdrop claim contract deploys, until trading went live, I felt good about my odds of beating other claimers to sell if I automated the process.
So, the general idea became to build out some infra to quickly (a) claim the airdrop, and (b) sell it if the price was inflated enough. The only real downside was potential time wasted on building this - if price didn’t go high enough, or if the bot was late to sell, it just wouldn’t execute. It was an airdrop anyways, so no real money lost.
Now, I’m going to walk through the process of how I actually built this out.
Planning
The idea felt sound, but there were some questions I needed to think through before implementing.
How do you claim TREE?
This one was easy, since the claims contract was live with verified code. ToA would manually add all airdrop recipients, as well as their claimable token amounts, to the “claims” array (see image below). I could simply call “claim”, with no additional data needed, to claim TREE. The Tree News UI already showed the amount of tokens each user would receive.
Here’s the TREE airdrop verified contract code:
What is the best way to account for the cases mentioned above: claims opening before, at the same time as, or after the LP was trading live?
Here, I opted to simply add an option to the bot to either bundle the claim and sell transactions (do them at the same time), or to only sell. That way, if claims opened well before trading, I could claim manually and only use the bot to sell. Otherwise, I’d leave the “bundle” option on, and the bot would bundle the claim and sell.
Worst case, if claims opened significantly after LP trading opened, my transaction bundles simply wouldn’t execute because the claim transaction couldn’t succeed.
How will ToA execute the transactions to open airdrop claims, and to add liquidity to the TREE-WETH pool?
There was a good chance that both of these transactions would come through the public mempool. If so, I could monitor the mempool in order to claim and/or sell even faster by putting my transactions behind ToA’s, but in the same block (yes, I could spam transactions too, but let’s ignore that for now).
However, I was slightly time constrained and mempool support would require whole new components in the bot for monitoring the mempool, bundling / backrunning transactions, perhaps blindly attempting sells in hopes that private transactions from bots push the price up during a pending block, etc. So I chose to start by only focusing on monitoring transactions that land in new blocks, and decided that I’d add mempool support at the end if I had time (spoiler: I did not).
What minimum price per TREE should I set?
I went with a minimum price of $0.83 per TREE. This would imply a $166m FDV, and I was fine selling anywhere above this. No math involved, just intuition - it felt like snipers could push the price up to over that level, and I’d be happy with my profit selling anywhere above there.
What should I use to implement this?
I was learning Rust at the time, so to get some extra practice I chose to use Artemis. Artemis is a framework by Paradigm for writing MEV bots in Rust that is “designed to be simple, modular, and fast.” It features all the components needed: “collectors” (monitor for events, like new blocks and new Uniswap V2/V3 pools), “strategies” (contain strategy logic and build custom transactions), and “executors” (execute actions, like bundled transactions). There are some pre-requisite installs found in the README for Artemis: Rust and Anvil (from Foundry).
(Side note: It seems that Artemis is no longer maintained, as it’s been 2 years since the last commit. But it remains a nice starter framework that is extremely extensible, and I still occasionally use it for implementing new strategies today.)
I used Bloxroute for submitting private bundles of transactions. I chose this for various reasons, but mainly because I was short on time and already familiar with their API.
Implementation
This section is going to be heavy on Rust code, but I’ll try to keep explanations high level. The full code is linked at the bottom of this article.
Keep in mind that this was written in late 2023 - some libraries or APIs used may have changed slightly since then. For instance, ethers-rs has become Alloy.
I’ve divided implementation into six steps: implementation overview, configuration, adding contract support, executing transactions, strategy creation, and finally putting it all together.
Step 1: Implementation Overview
We’re going to work with the Artemis framework - thankfully, there’s already a nice example included that implements an arbitrage strategy. We don’t actually care about its implementation, but we can use it to outline the key files we’ll be working with.
Generally, there will be four key things to focus on:
Step 2: Configuration
Glancing at main.rs (the “Main” arrow in the image above), which is by default set up for the arbitrage strategy, we see a couple things.
First, the general arguments that will be used to run the program/strategy.
We’ll be editing this to only include arguments we need, like the TREE token address, some Uniswap addresses, whether or not to claim AND sell or just sell, etc.
Second, we see the actual setup implementation.
Much of this will remain the same, but we’ll want to change:
The “collector”. We won’t be monitoring transactions coming through MEV share; instead, we’ll be monitoring for new Uniswap V2/V3 pools, as well as for new blocks.
Why both V2 and V3? ToA will most likely use a Uniswap V2 TREE-WETH pool, but a V3 pool is possible too, so it’s worth supporting both.
Why also monitor for new blocks? Because even after we see the new pool, we may not execute right away, so we want to be able to fetch the latest TREE price after every new block to see if we should execute at that point.
The “strategy”. We’ll be creating a new strategy, so we’ll end up using that instead.
The “executor”. We won’t be sending transactions through MEV share, we’ll be sending them through Bloxroute.
Also, since Artemis already supports Flashbots by default, we may as well send the transactions to Flashbots as well - no harm in doing so. We’ll use both the existing Flashbots executor, as well as the Bloxroute executor that we’ll create soon.
Now that we have an idea of what needs to be changed, we can start by deciding what arguments we’ll require for our strategy. Similar to the example arbitrage strategy, we’ll need Ethereum node websocket http endpoints, as well as our TREE airdrop wallet’s private key, so we’ll keep those.
Some other things we’ll need are, for example, a Bloxroute authentication key so we can send bundles, the TREE contract address, the TREE claims contract address, some relevant Uniswap contract addresses, the option to claim and sell or only sell, etc. Here’s the full new configuration:
In practice, should_claim will be true, do_execute_bundles will be true (false is only used for testing, where we’d build but not actually send transactions), and use_uni_v2 + use_uni_v3 will both be true. The rest of the main.rs changes, like switching the collector and executor, we’ll do in a later step.
Step 3: Adding Contract Support
Next, to easily interact with any Ethereum contract in our Rust code, we need to have “bindings” for it. This basically allows Rust to understand the contract, enabling us to build transactions for it and parse events coming from it.
To start, we have to grab the contract code from Etherscan for the TREE token, TREE airdrop claim, Uniswap V2 factory, Uniswap V2 router, Uniswap V2 pair, Uniswap V3 Factory, Uniswap V3 router (“swap router”), Uniswap V3 quoter, and Uniswap V3 pool.
(Note: Often the contracts will show in Etherscan as multiple files, and it helps to “flatten” them into one file - I’ve done that in the code. There’s also a command to automate fetching contract code, called “cast etherscan-source”, mentioned in Artemis’s justfile but not used here).
Once the contract code is all put into the “contracts” src directory, we need to generate bindings for them. We open artemis/strategies/mev-share-uni-arb, and run:
forge bind --bindings-path ./bindings --root ./contracts --crate-name mev-share-bindings --force --overwrite
If you check the bindings src directory in the mev-share-uni-arb folder, you should see auto-generated bindings for a bunch of contracts. They’re ready to use now.
Step 4: Executing Transactions
The final preparation step before writing the strategy itself is to add Bloxroute bundled transaction execution. Inside of artemis/crates/artemis-core/src/executors, we see already-created executors for Flashbots and MEV share (private transaction services), as well as mempool (regular transaction execution).
Each executor simply requires (a) some setup parameters, like a “client” (Ethereum node connection), and (b) an implementation of the “execute” function that takes some type of transaction and attempts to execute it.
We’ll create a bloxroute_executor.rs file, and implement it to allow us to send bundled transactions through Bloxroute’s API. For the setup parameters, there’s no “client” needed here, just our Bloxroute authentication header:
For actually sending the API request, if we look at the parameters in the Bloxroute docs for submitting Ethereum transaction bundles, we can come up with the structure for sending them:
Finally, we just need to implement the “execute” function to send the bundle. To cut down on time, we’re not going to monitor the bundle’s status (though this would be something well worth adding later), we’re just going to send the bundle to Bloxroute’s API and hope it lands onchain:
Step 5: Strategy Creation
On to the heavy lifting. In artemis/crates/strategies/mev-share-uni-arb/src, we see the example arbitrage strategy (strategy.rs) and types (types.rs) files.
Starting with the types file, we see that we have to define the “events” our strategy will receive and handle, and the “actions” the strategy can take - as well as some other types to help out. For instance, the arbitrage strategy receives “MEV Share” events and can take “Submit Bundle” actions:
Remember, we’re going to be monitoring for new Uniswap V2/V3 pools and new blocks. The former will come in as “log events”, since we’ll be monitoring the Uniswap V2/V3 factory contracts for emitted events that indicate new pools were created, and the latter will come in as “block events”.
The actions we can take are submitting bundled transactions, and we want to support both Bloxroute and Flashbots. We’ll create an additional type to help hold our bundle in two different formats, one for Bloxroute and one for Flashbots.
Putting that together, we get:
Now, to write the strategy itself. At a high level, the example arbitrage strategy shows us we need to handle four things: creating the strategy from input parameters, syncing state (fetching initial data), processing all possible events, and creating actions for the executors when needed.
1: Create Strategy
We’re going to take all the configuration parameters from step 3 and store them in our strategy. We’re also going to create contract instances for all relevant contracts, and we’re going to add some new variables like storing the TREE pool address, “next_nonce” (will help us decide the nonces to use in our transactions), “submitted_block_numbers” (used to double check if we’ve submitted a bundle for a block, avoiding race conditions), and some values to help us do math like “wad” and fee factors.
2: Sync State
Notice how there are some “default” values assigned in the strategy creation above. The v2/v3 pool addresses default to None, “next_nonce” defaults to zero, “fee_factor_v3” defaults to a 0.3% fee, etc.
When we start up our strategy and sync state, we have the opportunity to fill in any of those values we can. So, we’ll want to fetch the next nonce to use for our address and check for any existing TREE-WETH pools on Uniswap V2 and V3 (in case we somehow started this strategy late), and save them in our strategy to use later. That’s really all we need at the start.
Notice how we can directly call functions like “get_pair” on the Uniswap V2 factory contract. This is enabled by the bindings we generated earlier, which handle converting this into an actual call to the contract.
3: Process All Possible Events
There are only two possible events our strategy can receive: a LogEvent, which would either be a Uniswap V2 or V3 pool creation log, or a BlockEvent, which would indicate a new block.
In general, the logic tree for handling those events looks like:
LogEvent received
If the log is a Uniswap V2 pool creation, check if we’re using Uniswap V2 and if it’s a TREE-WETH pool. Same for Uniswap V3 - if the log is a V3 pool creation, check if we’re using V3 and if it’s a TREE-WETH pool.
If those checks pass, check if the current TREE-WETH pool price is above our minimum USD per TREE (accounting for liquidity and slippage), and try to execute if so.
BlockEvent received
If we’re using Uniswap V2 and we already found a TREE-WETH V2 pool, check if the current pool price is above our minimum USD per TREE (accounting for slippage and liquidity), and try to execute if so.
Same idea as above for Uniswap V3.
There’s a lot of repetitive code here (forgive me, I wrote this quickly, not optimally), so I’ll just walk through the most likely branch to execute since the others follow very similar patterns.
Typically, we’d be running this and waiting for a Uniswap V2 TREE-WETH pool to be created. If we pick up a LogEvent, and it came from the Uniswap V2 pool factory, we can check to see if it is a TREE-WETH pool:
Let’s say we did find the TREE-WETH pool here. Immediately, we want to see if we should be executing.
Whether we can execute depends on if, accounting for slippage and liquidity, we can sell our TREE at our minimum USD price per TREE ($0.83).
Uniswap V2 utilizes an x * y = k price curve, so we start by fetching the reserves and calculating the price of TREE in WETH terms. Ethereum’s uint256 and Rust’s corresponding U256 are integers rather than decimals, so we use fixed-point arithmetic with a scaling factor (“wad”, which is 10^18).
Next, we calculate how much minimum WETH we could actually get in exchange for our TREE - after applying the protocol fee (0.3% for V2 pools) and slippage (we’re using 2% here), and accounting for liquidity.
Then, we use the WETH we’d receive from selling TREE to calculate the effective sell price per TREE in USD.
Finally, before actually building the transaction, we run some final safety checks to ensure that we’re (a) able to sell TREE at a USD price we’re happy with, (b) targeting the next block, and (c) not targeting a block we’ve already submitted a bundle for.
4: Creating Actions
The generate_bundles function called in the image above will handle creating our actions, which are transaction bundles.
It does a few things, including checking that our gas fee wouldn’t exceed some value (I hardcoded 0.3 ETH), setting our gas (I used 18% over the market rate), setting nonces, creating the EIP-1559 claim (if we opted to bundle claim AND sell) and sell transactions, and formatting them as Bloxroute and Flashbots bundles. It’s fairly long, but here’s the portion where we build the sell transaction:
As you can see, we figure out what nonce to use, fill the sell transaction with our parameters (like minimum ETH to receive), set the gas to use, and format the transaction for our bundle.
This function returns the bundled transactions, which will automatically be sent to the Bloxroute and Flashbots executors for execution.
Step 6: Putting It All Together
All that’s left is to set up the wallet, finish up main.rs, and then run the program.
The only wallet that could be used was one that would be receiving the TREE airdrop. Outside of grabbing the private key for this wallet, we’d also need to (using the Etherscan UI) send “approve” transactions to the TREE token contract in order to approve the Uniswap V2 and V3 router contracts for selling TREE.
There’s now a unified “universal router” contract that supports them V2 and V3, among other protocols, but I was already familiar with the individual routers so I used those.
Back to wrapping up the code. In main.rs, we need to replace the collectors with ones that monitor for new Uniswap V2/V3 pools and new blocks. We’ll also replace the strategy with the newly built TreeStrategy, and the executor with the Bloxroute and Flashbots executors.
We’d also create a .env file in the same directory, mirroring the format of “env.template”, to store things like the private key and RPC URLs.
To run, we’d navigate to artemis/examples/mev-share-arb, and run:
cargo run --release
But realistically, don’t run this - mainly because there’s no point to run it now, as the opportunity is gone, but also because some libraries and APIs may have changed since I wrote this, so output may vary.
Result
This strategy was run during the TREE launch, and it worked. Snipers pushed the price to over $4b FDV (albeit with low liquidity), and it took a few blocks for anyone who had claimed the airdrop to sell. Just by looking at the chart over the first hour of trading, you can see what the initial snipers did to the price, and what airdrop recipients did to it after:
Rather than going over everything that happened, I’ll refer you to this thread I wrote about the whole thing.
Conclusion
The full code is linked here. Disclaimer: Again, this code should not be run as-is, since the opportunity it was targeting is no longer present. However, it can be repurposed to fit other trading strategies, and hopefully the implementation outline above provides some guidelines for how to think about doing so.
Lastly, I’ll note some features that I left out because I was short on time, but that would have been nice to have:
Monitoring the status of bundles to see if they landed onchain, and automatically stopping the bot if so (rather than having to manually stop it).
Mempool monitoring (decoding mempool transactions that this strategy would care about, bundling / backrunning them if helpful, potentially blindly submitting sell bundles in case price got pushed into the sell transaction’s price during a block, etc.).
Accounting for varying token decimals in my math (I knew WETH and TREE both used 18 decimals, but in other cases this assumption may not hold).
Allowing for slippage to be set by a configuration parameter (rather than hardcoded to 2%). Other hardcoded values like the max gas fee and what % over market rate to set the gas fee to could also be moved to configuration parameters.
Checking that approvals are done while syncing state (rather than just assuming I had done them already).
Checking which token will be token0 vs token1 in the Uniswap pair (this is deterministically decided by Uniswap’s contracts depending on the pair’s two token contract addresses, so I knew token0 would be TREE when paired with WETH, but this may not hold for all tokens).
Monitoring for claims opening rather than blindly assuming they’d be open when the bot tries to claim + sell.
Simulating transactions on a forked Ethereum mainnet (with a TREE-WETH pool live and TREE claims open) to be certain that the built transactions worked.
Hopefully this article provides some useful insights or sparks some ideas of your own. I’ll continue gradually open sourcing code as time permits, so subscribe if you’re interested, and feel free to DM me on Twitter @ bh359 if you have any ideas that may be worth building.