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
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!