Skip to main content

Airdrop Claiming Guidelines

In this article we're going to study imaginary claim solution, try to identify it's performance problems and solve them. This article focuses on contract interactions and their impact on overall performance. Code, security aspects, and other nuances left aside.

Claim Machine

info

How pretty much any claim solution works? Let's think about it.

User send some kind of proof, that he is eligible for the claim solution checks it and sends jettons back. In current case proof means merkle proof but it could very well be signed data or whatever else authorization method one could come up with. Sending jettons, - so there would be a jetton wallet and minter. And we got to make sure that these sneaky users can't claim twice - double spend protection contract. Oh, and we probably want to make some money, do we? So at least one claim wallet. Let's sum it up:

Distributor

Takes the proof from the user, checks it, releases the jettons. State init: (merkle_root, admin, fee_wallet_address).

Double spend

Receives message, bounces if already used, otherwise passes message further

Jetton

Jetton wallet where the tokens will be sent from by the distributor. Jetton minter is out of the scope of this article.

Fee wallet

Any kind of wallet contract

Architecture

V1

First design that comes to mind is something like this:

  • User sends proof to the distributor
  • Distributor checks proof and deploys double spend contract
  • Distributor passes message to the double spend.
  • Double spend sends claim_ok to the distributor if wasn't deployed previously
  • Distributor sends claim fee to the fee wallet.
  • Distributor releases jettons to the user.

NAIVE ART AHEAD!

What's wrong with that? Looks like a loop is redundant here.

V2

Linear design is much better:

  • User deploys the double spend and it proxies proof to the distributor
  • Distributor checks the sending double spend address by state init (distributor_address, user_address?)
  • Distributor checks proof, in this case user index should be part of the proof and releases jettons.
  • Distributor sends fee to the fee wallet MORE NAIVE ART

Shard optimizations

Ok, we got something going, but what about shard optimizations?

What are these?

In order to get some very basic grasp of understanding, please take a look at Wallet Creation for Different shards Long story short - shard is a 4 bit address prefix. Kind of like in networking. When contract is in the same network segment, messages get processed without routing and therefore - much faster.

Identifying what addresses we can control

Distributor address

We are in full control of the distributor data, so we must be able to to put it in whatever shard. How to? Remember, the contract address is defined by it's state. We should use some of the contract's data field as nonce and keep on trying until we get the desired result. Example of a good nonce in real contracts can (subwalletId/publicKey) for a wallet contract. Any field that can be either modified after deployment or does not impact contract logic(like subwalletId) will fit the bill. One might even create unused field explicitly for this purpose, like vanity-contract does

Distributor jetton wallet

We can't control resulting jetton wallet address directly. However, if we control the distributor address, we can pick it so, that the resulting jetton wallet for it would end up in the same shard. But how to do it? There is a lib for it! It currently supports only wallets, but it's relatively easy to add arbitrary contract support. Take a look how it is done for HighloadV3 does.

Double spend contract

Double spend contract should be unique per proof, so hardly we can shard tune it? Let's think about it for a bit. If you think about it, it depends on the proof structure. First thing that comes to mind is same structure as mintless jettons

_ amount:Coins start_from:uint48 expired_at:uint48 = AirdropItem;

_ _(HashMap 267 AirdropItem) = Airdrop;

In that case, of course it's not tunable, because address distribution is random and all the data fields are meaningful. But nothing stops us from simply doing this:

_ amount:Coins start_from:uint48 expired_at:uint48 nonce:uint64 = AirdropItem;

_ _(HashMap 267 AirdropItem) = Airdrop;

or even

_ amount:Coins start_from:uint48 expired_at:uint48 addr_hash: uint256 = AirdropItem;

_ _(HashMap 64 AirdropItem) = Airdrop;

Where 64 bit index can be used as nonce and address becomes part of data payload for verification. So, if double spend data is constructed from (distributor_address, index) where index is part of the data, we can still have the initial reliability, but now the address shard is tunable via index parameter.

User address

Obviously we're not in control of user addresses, do we? Yes, BUT we can group them in such a way that the user address shard matches the distributor shard. In that case each distributor would process merkle root which consists entirely from users originating from it's shard.

Summary

We can put double_spend->dist->dist_jetton part of the chain in the same shard. What is left for the other shards is dist_jetton->user_jetton->user_wallet.

How do we actually deploy such setup

Let's see step by step. One requirement is that distributor contract has to have updatable merkle root

  • Deploy distributor in each shard (0-15) within the same shard as their jetton wallets using initial merkle_root as nonce
  • Group the users by dist shard
  • For each user find such index, so it's double spend contract (distributor, index) ends up in same shard as the user address.
  • Generate merkle roots with indexes from step above
  • Update distributors with according merkle roots

Should be good to go now!

V3

  • User deploys double spend contract in the same shard using index tuning
  • Distributor in user shard checks the sending double spend address by state init (distributor_address, index)
  • Distributor sends fee to the fee wallet
  • Distributor checks proof, in this case user index should be part of the proof and releases jettons via jetton wallet in same shard.

MORE NAIVE ART Is there anything wrong with that? Let's take a good look. .... Damn right! There is only one fee wallet, and fees are queueing up to a single shard. That could have been a disaster! (Wondering if it ever happened for real?).

V4

  • Same as V3 but 16 wallets now, each in same shard as it's distributor.
  • Going to have to make fee wallet address updatable

Bit more art

How about now? LGTM.

What's next?

We always can go even further. Take a look at a custom jetton wallet which has a built-in shard optimization. As a result user's jetton wallet ends up in the same shard as a user with 87% probability. But that's a fairly uncharted territory yet, so you're on your own. Good luck with TGE!