Better Airdrops

Relic Protocol
ChainLight Blog & Research
8 min readNov 14, 2022

--

Airdrops are pretty simple. At their core the goal is just to send tokens to users based on some criteria. But how is this actually implemented in practice? well, it can get a bit complicated. Let’s take a look at how this is done now, and some better possibilities for airdrops moving forward.

Airdrops can be broken down into three separate stages:

  1. Determining eligible users
  2. Publishing eligible users on-chain
  3. Token transfer

Normally airdrops are totally new tokens, so there is also an implicit step 0 to create the token contract. That’s a whole separate discussion though, and doesn’t change based on the particular airdrop strategy, so we’ll not worry too much about that piece.

Now let’s get a bit deeper into the details for how to implement the rest.

Eligibility

There are a number of criteria one may want to use for determining who is eligible for an airdrop. Some airdrops are simply sent to a handful of known individuals (influencers, investors, etc.), but when it comes to dApps, normally the goal is to send tokens to users. To determine who was a user, the most common criteria are based on possessing tokens in a time window, or interacting with a contract/app in a time window.

Actually turning that idea into a list of users can get tricky. Let’s imagine we want to determine who held a common token (e.g. USDT), at a specific block (e.g. 15,000,000). If we search online for solutions to this problem, there are two general solutions:

  • Write a program to use an archive node to go through all blocks up to 15,000,000 and check who has interacted with USDT. Either track the balances, or simulate a balanceOfcall for each of the potential holders.
  • Find a blockchain analytics company online that has an API you can use for this query (probably for a small fee). Depending on the data you get from the API, you may still need to write a bit of code.

Checking interactions within a time window is likely much simpler. Assuming the smart contract emits events when users interact (such as Transfer events for our USDT example), we can just scan the eligibility range for those events. Again, this will require using an API or archive node and some code. However, this will be a bit simpler to implement compared to finding all holders — perhaps why interactions in a time window are a common criteria for airdrops today.

The choice between these two eligibility criteria mostly depends on the project. For something with an ERC-20 or ERC-721 token, it probably makes sense to use token holders in a range — a long time hodler may not have any transfers after their early purchases, but may still be an active user. For something like a DEX, it may make more sense to base eligibility on interactions, and depending on the circumstances, there may in fact be no other reasonable criteria.

Either way, when determining whether to write the code to handle the eligibility list yourself or using external APIs to assist, the primary concern is correctness: is your code correct, are the APIs returning the correct values, and are your queries fetching the data you expect them to. Hopefully there are some known addresses which should be eligible and some which should not be eligible that can be used to test the results to help gain confidence.

A secondary concern is cost: whether using an archive node or another API service, there may be small fees associated. Luckily these fees are likely to be minor, probably between $10-$100USD.

Bulk Transfer

To handle both publishing eligible users and transferring tokens to them, there are a number of ways to perform “bulk transfers”. The basic idea here is to simply take the list generated in step 1 and send tokens directly.

So simple, even Patrick Star could do it

There are some benefits here: this is conceptually very simple, and users don’t need to do anything to receive their tokens. In some ways this is the epitome of airdrops, but it is not without drawbacks.

The main issue is cost. Using our running example of USDT tokens at block 15,000,000 we’d have over four million users to reimburse. In the most optimistic scenario, this would still require setting at least four million storage slots in Ethereum. Each store would cost at least 21,000 gas, which at 20 Gwei gas prices leads to at least 1680 ETH required for the airdrop — a little over $2.5 million USD right now.

There are actually some additional costs here too. This can’t fit in a single transaction, so will need to be split across several. There are also other operations aside from the storage write that will increase the gas usage.

Some commercial services offer bulk transfers as a service: you can enter a list of addresses and they will deal with the on-chain transfer. However, this adds even greater costs. These services vary in price but tend to be around 15,000 transfers for 1 ETH which would give an additional 266 ETH or about $400,000 USD.

It is hopefully obvious that there are some pretty fundamental limitations with this approach. While it may seem simple, it is cost prohibitive in most circumstances.

Merkle Tree

Fundamentally, the bulk transfer doesn’t work because storing too much data on-chain is expensive. On the one hand, this is unavoidable. For four million users to receive tokens, there need to be four million storage slots used, which means a lot of gas spent.

However, we can amortize this cost through our users. This has a big advantage too: it’s entirely possible not all of those four million users want to claim their airdrops. For example maybe the value for which they are eligible is too low to be worth it, maybe they lost control to their wallet, or any number of possibilities. In that sense, it’s possible we can shortcut the total amount of gas that needs to be spent.

But how can we do this securely? If we need to only write a small number of values on-chain, how can we still include all eligible user accounts and no one else?

The answer is Merkle Trees! These lovely cryptographic constructions allow a single storage slot containing a Keccak-256 hash to describe all of the accounts we want. For a user to claim their airdrop, they simply have to provide a Merkle Proof that their address was included in the published Merkle Tree. This proof will be relatively short: logarithmic in the number of accounts in the tree.

An example Merkle Tree. Check out the Wikipedia article to learn more about them

So the procedure for this looks like:

  • build a Merkle Tree of the eligible users
  • submit the Merkle Root to the minting contract
  • provide front-end for users to build Merkle Proofs of their eligibility
  • verify Merkle on-chain and mint tokens

While there are some Solidity libraries to help build the smart contract side of this, it definitely requires more development effort compared to the bulk transfer approach. Additionally, building the tree must be done off-chain, which limits transparency and means a centralized party is responsible for the construction.

It is also possible to extend the Merkle Tree approach to do more than just give an equal amount of tokens to everyone who is eligible. If the Merkle Tree is created as a list of account, eligible_amount tuples, then one can set differing amounts per user easily as well.

Issues

There are a couple shortcomings with the two approaches so far.

The first is the reliance on off-chain calculations to make both of them work. How do we know the airdrop creator did not mess up their list? Or worse, what if they intentionally manipulated the airdrop amounts to benefit themselves or harm others? Once a Merkle Root is committed, or a bulk transfer is complete, it is not possible to rescind it and start over. Users are not involved in the creation of the lists used to generate either solution: they must trust and rely on the protocol creator.

While today there may not be any airdrops victim to this issue, it is difficult to verify. It also goes against the idea of decentralization. In an ideal world, there would be no need to trust the airdrop creator to be honest. Why can’t we simply build a fully decentralized and trustless airdrop?

The next issue is simply that these approaches require a lot of resources: for bulk transfers that is primarily money. For Merkle Tree systems, it requires development effort: one must create a smart contract to verify the Merkle Tree, a user interface to assist in proof generation, and the actual tree must be generated that is compatible with both.

Another Way

The first realization is that creating a Merkle Tree of something like previous token holders is actually redundant — Ethereum already has one! The entire blockchain history is based on storing data in cryptographically secure data structures like Merkle Patricia Tries, and then linking them together with more Keccak hashes in blocks.

Account state storageRoot? that’s just a Merkle Root!

Luckily there is also an easy way to use this data on-chain using Relic Protocol. The basic idea is then pretty simple:

  • use Relic to prove historic ownership of a token in a block range
  • mint tokens to anyone who has such a proof

Luckily this is quite easy to implement thanks to the provided SDKs.

If you want to read more about how to actually implement this, check out our follow-on article with a full walk-through here.

How does this method compare to the Merkle Tree approach? For one, it saves a lot of effort. Developers don’t need to attempt to query full Ethereum nodes to determine who is eligible for how many tokens. The actual Merkle Tree is already built and just waiting to be used!

Using historic state also means there is no centralized or trusted authority. If the airdrop developers wanted to drop more tokens to themselves or prevent some deserving users from claiming their tokens, it would not be possible without publishing the malfeasance in the token contract that anyone could view.

There is one downside though, which is gas usage. Because the proof issued on-chain requires linking it to a historical block, there is a bit of extra overhead compared to a pure Merkle Proof. Luckily, that is something the developers of Relic are working on. By caching data from popular blocks (such as the relevant account storage root in the block for an airdrop), the gas costs would be competitive with plain Merkle Proofs.

Wrap Up

There are a number of reasons why an application may want to airdrop tokens to its users. As a developer, there are constant compromises between code complexity, decentralization/transparency, cost, and ease of use. Relic takes the substantial benefits of Merkle Tree based airdrops and extends them, benefiting from the substantial cost reduction over bulk transfers while also increasing decentralization and transparency and making it simpler to code.

If you want to use Relic for your next airdrop (or other dApp), be sure to check out our next post in the series to learn more about the detailed implementation of airdrops with Relic. If you have questions, comments, or just want to stay up to date, join our Discord or follow us on Twitter. For more info for developers, check out our docs, and look over our source and examples on Github!

--

--