Final part of a four part series on NFTs. Consider starting here.
You may have read that I had some fun botting the recent NFT craze on Avalanche. Along the way, I developed what I thought was a pretty solid NFT minting bot; to the degree where I had a good chance at bulk minting out anything I was motivated to mint.
So I was pretty hyped to read that the Joepegs team was going to host a mint bot competition. Not only did this sound like free money, but the bragging rights, my god.
Of course, my friends at @CynicalHateDao were hard at work hyping me up.
But of course, we all know what happens if I don’t win.
With the pressure that high, losing is not an option. So I decided to cheat.
The rules
Official rules here.
Basically, the exact timing isn’t known in advance; the mint will open at a random time in a 30 minute window. You need to listen for the ‘initialize’ event, and then send your TX accordingly.
Additionally, everyone will use the same gas settings, making that a moot factor. And you need to mint exactly 1 NFT from each of 3 accounts; no more, no less. This means spamming mint transactions is off the table.
Why it’s a crapshoot
It’s honestly not that difficult to write a bot as good as mine. I’ve also publicized all my strategies, and obviously someone else could have thought up other new strategies that are better than or additive to mine.
Good developers will probably try to figure out how early they can send a TX and still land in the earliest possible mint block. If we’re all spamming TX around the same time, who lands highest in the block is basically up to random chance, based on my experimentation with transaction ordering.
There’s a claim in the documentation that ‘oldest transaction’ is ordered first, but it doesn’t seem to work out that way in practice, probably because of some kind of interaction between block proposers and how TX gossiping works.
But my experiments did show that if your TX is sent approximately .4 seconds ahead of another one with equal gas settings, it’s nearly guaranteed to land earlier in the block.
So how can we guarantee this?
The fatal flaw
Two factors that go into every blockchain are the block size, and the block speed. They trade off against each other; the larger the block, the longer it takes to reach consensus.
Avalanche has a block size cap of 8M, and approximately 1-3 seconds per block. If you emit 8M gas worth of TX at a higher Priority Fee value than other TX, that block is full and no other TX will be mined for the next 2-3 seconds. If you keep emitting 8M gas worth of TX every few seconds, you can basically halt the blockchain.
Now of course, this costs money, but it’s surprisingly little. And Avalanche does scale up the gas cost over time, but it does so slowly. Gas prices will barely budge even if you lock the chain for 8-10 blocks. It starts to scale up more quickly after 20+ blocks though.
So if you want to ensure a TX is the oldest one in the mempool, just lock the blockchain using 8M gas burned by transactions at [PriorityFee+1], send the mint TX at [PriorityFee], and then stop locking it when you want your TX to be processed.
I don’t know if there’s an official name for this, but I’ve been calling it a flood attack.
The optimal way to cheat
My original, and most impressive idea, was to buy ChainSight mempool access and spy for the initializePhases() call that the Joepegs team would send. Then I would start the flood attack, blocking the Initialized event and keeping any other minters from learning about the start of the mint.
I would decode the arguments passed to initializePhases() and use that to determine how long to flood before unlocking the chain. My mint requests would be sent at the initialize TX PriorityFee - 1, ensuring the minted in literally the same block that the mint opened.
Unfortunately, during testing I discovered that the Joe team put some validation inside the initialize call that would prevent it from executing at all if I did this.
if (_preMintStartTime < block.timestamp) {
revert Launchpeg__InvalidStartTime();
}
if (_allowlistStartTime < _preMintStartTime) {
revert Launchpeg__AllowlistBeforePreMint();
}
Sad times.
But in the end, it wouldn’t have worked out anyway; I had expected that the init would happen < 20s from the allowlist phase opening, but it turns out they plan to do it minutes in advance; it would be prohibitively expensive to block for that long.
Fallback plan
OK, so the coolest idea was unfortunately out. But the same basic idea still holds, the oldest TX in the pool will get minted first.
So instead, I work backwards from the allowlist opening time. Start spamming gas-consuming TX N seconds before the mint opens, and send my mint transactions at N-1 seconds before the mint opens.
This would ensure that my TX is extremely old and more likely to be mined ahead of any other mint transactions.
Proxy contract minting
One thing I noticed during testing was that the allowlistMint method was not protected by an ‘isEOA’ clause; this meant that I could actually use a proxy smart contract to send my mint request.
It’s easy to add a limiter in that contract that prevents multiple mints from occurring, which means that I could actually spam my mint TX; this has the advantage of not having issues if my mint TX slips out ahead of the spam transactions.
Additionally, I could use an arbitrary number of bot instances to spam the TX.
Unfortunately, they clarified a few days before the competition that this strategy is not allowed. I asked and was allowed to run that strategy in a non-eligible-to-win way; just curious to see how it compares.
How much does it cost?
Well, when I originally spec’d this out I estimated it would cost about 8 avax to do this attack. But since everyone is hyping the fuck out of this, I increased the parameters quite a bit. Losing by a small amount would literally destroy me.
I ran a full heavyweight test earlier today.
Yikes. Going to tune that down a bit.
I also spent kind about a hundred dollars using Cloud Build to run tests.
Monitoring / testing
Part of the contest is subjective, based on the quality of the code, documentation, and testing. And I want to win that too. So I went kind of hardcore on this.
I have full integration tests that include:
Deploying a new launchpeg, setting up the allowlist
Setting up mint proxies
Running background ‘noise’ jobs to simulate traffic (because Fuji is extremely quiet and obviously local Avalanche has no actual traffic)
Monitoring the blocks as they are mined, including gas usage, price, interactions with the contracts we’re interested in, and the start block detection
Launching all the mint / proxy mint / spammer jobs, either locally, or on Cloud Build
Verifying account balances, tracking gas usage, and TX usage
I print things to the console obviously, but I also use Discord webhooks. This makes it easier to watch in real time, and review older test runs. Here are a couple of screenshots.
Set up:
Job launching:
Block monitoring during spamming:
Block monitoring when the allowlist opens and mint tx slip in:
Job completion, including recovering the minted NFTs from the minters / proxy minters to the deployer:
I had big plans to create a Cloud Build integration test that would run the whole suite on demand, but I spent hours on it and couldn’t get it working. I later found out that this was because IPv6 is not supported =(
I wanted to rewrite that test to run on Compute Engine, but kind of ran out of time. Spent my last hours working on incremental improvements to the bot, and testing.
How did I do?
Fucked if I know, I’m writing this two weeks in advance. I haven’t even finished the code yet. Manifestoooor.
I wrote most of this two weeks in advance but more recently updated it with the final info and screenshots. I had planned to include some helpful diagrams but I’m running super low on energy, maybe I’ll come back to it.
Tomorrow morning I’ll actually be dropping my kid off at school during the contest, so I’m going to kick off my jobs before I head out and watch status from Discord on my phone.
I’m going to schedule a tweet for tomorrow at 8:30 linking to this blog post.
MANIFESTOOOOR
UPDATE - I WON:
Tokens 0, 1, 2 were taken by someone not following the rules.
Tokens 3, 6, 7 were taken by my contest minter
Tokens 4, 5, 8 were taken by my disqualified proxy minter.
chad!