|
| 1 | +# Airdrop Claiming Guidelines |
| 2 | + |
| 3 | +In this article we're going to study imaginary claim solution, try to identify it's performance problems and solve them. |
| 4 | +This article focuses on contract interactions and their impact on overall performance. |
| 5 | +Code, security aspects, and other nuances left aside. |
| 6 | + |
| 7 | +## Claim Machine |
| 8 | + |
| 9 | +:::info |
| 10 | +How pretty much any claim solution works? |
| 11 | +Let's think about it. |
| 12 | +::: |
| 13 | + |
| 14 | +User send some kind of proof, that he is eligible for the claim |
| 15 | +solution checks it and sends jettons back. |
| 16 | +In current case `proof` means [merkle proof](https://docs.ton.org/develop/data-formats/exotic-cells#merkle-proof) but it could very well be signed data or whatever else authorization method one could come up with. |
| 17 | +Sending jettons, - so there would be a jetton wallet and minter. |
| 18 | +And we got to make sure that these sneaky users can't claim twice - double spend protection contract. |
| 19 | +Oh, and we probably want to make some money, do we? |
| 20 | +So at least one claim wallet. |
| 21 | +Let's sum it up: |
| 22 | + |
| 23 | +### Distributor |
| 24 | + |
| 25 | +Takes the proof from the user, checks it, releases the jettons. |
| 26 | +State init: `(merkle_root, admin, fee_wallet_address)`. |
| 27 | + |
| 28 | +### Double spend |
| 29 | + |
| 30 | +Receives message, bounces if already used, otherwise passes message further |
| 31 | + |
| 32 | +### Jetton |
| 33 | +Jetton wallet where the tokens will be sent from by the *distributor*. |
| 34 | +Jetton minter is out of the scope of this article. |
| 35 | + |
| 36 | +### Fee wallet |
| 37 | + |
| 38 | +Any kind of wallet contract |
| 39 | + |
| 40 | +## Architecture |
| 41 | + |
| 42 | +### V1 |
| 43 | + |
| 44 | +First desing that comes to mind is something like this: |
| 45 | +- User sends proof to the distributor |
| 46 | +- Distributor checks proof and deploys `double spend` contract |
| 47 | +- Distributor passes message to the double spend. |
| 48 | +- Double spend sends `claim_ok` to the distributor if wasn't deployed previously |
| 49 | +- Distributor sends claim fee to the fee wallet. |
| 50 | +- Distributor releases jettons to the user. |
| 51 | + |
| 52 | +**NAIVE ART AHEAD!** |
| 53 | + |
| 54 | +What's wrong with that? |
| 55 | +Looks like a loop is redundant here. |
| 56 | + |
| 57 | +### V2 |
| 58 | + |
| 59 | +Linear desing is much better: |
| 60 | +- User deploys the `double spend` and it proxies proof to the distributor |
| 61 | +- Distributor checks the sending `double spend` address by state init `(distributor_address, user_address?)` |
| 62 | +- Distributor checks proof, in this case user index should be part of the proof and releases jettons. |
| 63 | +- Distributor sends fee to the fee wallet |
| 64 | +**MOAR NAIVE ART** |
| 65 | + |
| 66 | +## Shard optimizations |
| 67 | + |
| 68 | +Ok, we got something going, but what about shard optimizations? |
| 69 | + |
| 70 | +### What are these? |
| 71 | + |
| 72 | +In order to get some very basic grasp of understanding, please take a look at [Wallet Creation for Different shards](https://docs.ton.org/develop/dapps/asset-processing/#wallet-creation-for-different-shards) |
| 73 | +Long story short - shard is a 4 bit address prefix. Kind of like in networking. |
| 74 | +When contract is in the same network segment, messages get processed without routing and therefore - much faster. |
| 75 | + |
| 76 | + |
| 77 | +### Identifying what addresses we can control |
| 78 | + |
| 79 | +#### Distributor address |
| 80 | + |
| 81 | +We are in full control of the distributor data, so we must be able to to put it in whatever shard. |
| 82 | +How to? |
| 83 | +Remember, the contract address is [defined by it's state](https://docs.ton.org/learn/overviews/addresses#account-id). |
| 84 | +We should use some of the contract's data field as nonce and keep on trying until we get the desired result. |
| 85 | +Example of a good nonce in real contracts can (subwalletId/publicKey) for a wallet contract. |
| 86 | +Any field that can be either modified after deployment or does not impact contract logic(like subwalletId) will fit the bill. |
| 87 | +One might even create unused field explicitly for this purpose, like [vanity-contract](https://github.com/ton-community/vanity-contract) does |
| 88 | + |
| 89 | +#### Distributor jetton wallet |
| 90 | + |
| 91 | +We can't control resulting jetton wallet address directly. |
| 92 | +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. |
| 93 | +But how to do it? There is a [lib](https://github.com/Trinketer22/turbo-wallet) for it! |
| 94 | +It currently supports only wallets, but it's relatively easy to add arbitrary contract support. |
| 95 | +Take a look how it is done for [HighloadV3](https://github.com/Trinketer22/turbo-wallet/blob/44fe7ee4300e37e052871275be8dd41035d45c3a/src/lib/contracts/HighloadWalletV3.ts#L20) does. |
| 96 | + |
| 97 | +### Double spend contract |
| 98 | + |
| 99 | +Double spend contract should be unique per proof, so hardly we can shard tune it? |
| 100 | +Let's think about it for a bit. |
| 101 | +If you think about it, it depends on the proof structure. |
| 102 | +First thing that comes to mind is same structure as [mintless jettons](https://github.com/tonkeeper/TEPs2/blob/mintles/text/0177-mintless-jetton-standard.md#handlers) |
| 103 | + |
| 104 | +``` |
| 105 | +_ amount:Coins start_from:uint48 expired_at:uint48 = AirdropItem; |
| 106 | +
|
| 107 | +_ _(HashMap 267 AirdropItem) = Airdrop; |
| 108 | +
|
| 109 | +``` |
| 110 | +In that case, of course it's not tunable, because address distribution is random and all the data fields are meaningful. |
| 111 | +But nothing stops us from simply doing this: |
| 112 | +``` |
| 113 | +_ amount:Coins start_from:uint48 expired_at:uint48 nonce:uint64 = AirdropItem; |
| 114 | +
|
| 115 | +_ _(HashMap 267 AirdropItem) = Airdrop; |
| 116 | +``` |
| 117 | +or even |
| 118 | +``` |
| 119 | +_ amount:Coins start_from:uint48 expired_at:uint48 addr_hash: uint256 = AirdropItem; |
| 120 | +
|
| 121 | +_ _(HashMap 64 AirdropItem) = Airdrop; |
| 122 | +
|
| 123 | +``` |
| 124 | + |
| 125 | +Where 64 bit index can be used as nonce and address becomes part of data payload for verification. |
| 126 | +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. |
| 127 | + |
| 128 | +#### User address |
| 129 | + |
| 130 | +Obviously we're not in control of user addresses, do we? |
| 131 | +Yes, **BUT** we can group them in such a way that the user address shard matches the distributor shard. |
| 132 | +In that case each distributor would process *merkle root* which consists entirely from users originating from it's shard. |
| 133 | + |
| 134 | +#### Summary |
| 135 | + |
| 136 | +We can put `double_spend->dist->dist_jetton` part of the chain in the same shard. |
| 137 | +What is left for the other shards is `dist_jetton->user_jetton->user_wallet`. |
| 138 | + |
| 139 | +### How do we actually deploy such setup |
| 140 | +Let's see step by step. |
| 141 | +One requirement is that *distributor* contract has to have updatable *merkle root* |
| 142 | +- Deploy distributor in each shard (0-15) within the same shard as their jetton wallets using initial `merkle_root` as nonce |
| 143 | +- Group the users by dist shard |
| 144 | +- For each user find such index, so it's *double spend* contract `(distributor, index)` ends up in same shard as the user address. |
| 145 | +- Generate *merkle roots* with indexes from step above |
| 146 | +- Update *distributors* with according *merkle roots* |
| 147 | + |
| 148 | + |
| 149 | +Should be good to go now! |
| 150 | + |
| 151 | +### V3 |
| 152 | +- User deploys *double spend* contract in the same shard using index tuning |
| 153 | +- Distributor in user shard checks the sending `double spend` address by state init `(distributor_address, index)` |
| 154 | +- Distributor sends fee to the fee wallet |
| 155 | +- Distributor checks proof, in this case user index should be part of the proof and releases jettons via jetton wallet in same shard. |
| 156 | + |
| 157 | +**MOAR NAIVE ART** |
| 158 | +Is there anything wrong with that? Let's take a good look. |
| 159 | +.... |
| 160 | +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?). |
| 161 | + |
| 162 | +### V4 |
| 163 | +- Same as V3 but 16 wallets now, each in same shard as it's *distributor*. |
| 164 | +- Going to have to make *fee wallet* address updatable |
| 165 | + |
| 166 | +**Bit moar art** |
| 167 | + |
| 168 | +How about now? LGTM. |
| 169 | + |
| 170 | +## What's next? |
| 171 | +We always can go even further. |
| 172 | +Take a look at a custom [jetton wallet](https://github.com/ton-community/mintless-jetton/blob/main/contracts/jetton-utils.fc#L142) which has a built-in shard optimization. |
| 173 | +As a result user's jetton wallet ends up in the same shard as a user with 87% probability. |
| 174 | +But that's a fairly uncharted territory yet, so you're on your own. |
| 175 | +Good luck with TGE! |
0 commit comments