Usage¶
TokenTable TVM has two contracts. The Main Distributor Contract stores the Merkle root and handles token distribution. Each entry index has its own Leaf contract, which records whether its entry has been claimed and forwards claims to the Main Distributor Contract. Because claim state lives in the Leaf contracts, one distributor can serve a list of any size, such as an airdrop or a reward program, without its own storage growing.
Deployment and setup¶
You deploy the Main Distributor Contract yourself. Leaf contracts do not need to be deployed in advance: the first CLAIM message for an index carries the Leaf's StateInit (the Leaf code plus the distributor address and the index) and deploys it.
-
Build the distribution list off-chain. Create a dictionary of entries keyed by a
uint64index, where each entry holds the recipient address, the claimable amount and the earliest claim timestamp. ThegenerateEntriesDictionaryhelper inwrappers/MerkleTokenDistributor.tsdoes this.The Merkle root is the hash of the dictionary cell,
beginCell().storeDictDirect(dict).endCell().hash(). Keep the dictionary cell on your backend, because you need it to generate proofs. -
Deploy the Main Distributor Contract. Its initial data holds the version, admin address, deployer address, Leaf code, claim window, claim fee, optional claim fee address, optional pause flag and feeless threshold, plus the Merkle root and Jetton wallet address. See
scripts/deployMerkleTokenDistributor.ts.The distributor's Jetton wallet address depends on the distributor's own address, so you usually set it after deployment. Send
SET_BASE_PARAMSwith the Jetton wallet address (from the Jetton master'sget_wallet_addressfor the distributor), the start time, the end time and the Merkle root. Seescripts/sendSetBaseParams.ts. The admin can sendSET_BASE_PARAMSagain later to replace the root or the claim window. -
Transfer the Jettons to be distributed to the Main Distributor Contract's address, so that its Jetton wallet holds them. If the Jetton wallet runs out, claims bounce and the Leaf is unlocked so the entry can be claimed later.
Claiming tokens¶
- Off-chain, generate a Merkle proof for the user's entry index with
dict.generateMerkleProof(index)on the stored dictionary. The proof must correspond to the Merkle root stored in the Main Distributor Contract. - The user sends a
CLAIMmessage with the Merkle proof and index to the Leaf contract for that index. Its address comes from the distributor'sget_leaf_addressgetter or fromLeaf.createFromConfig.Leaf.sendClaimattaches 0.17 TON plus the current claim fee (get_claim_fee). - The Leaf checks that the proof contains the index. The Main Distributor Contract then checks the proof against the root, the claim window, the entry's unlock time and the pause flag.
- If every check passes, the contract transfers the entry's amount of Jettons to the address recorded in the entry. Remaining TON comes back to that address as Jetton transfer excess.
If the Main Distributor Contract rejects the claim, it unlocks the Leaf, which returns the remaining TON to the claimant, and the entry can be claimed again. If the Leaf itself rejects the claim (for example, because the entry has already been claimed), the message fails at the Leaf.
Roles and permissions¶
The admin is the admin_address stored in the Main Distributor Contract. Anyone can deploy the contract; the admin is whoever is set in its initial data. Only the admin can send CHANGE_ADMIN, CHANGE_CODE (replace the Leaf code or upgrade the distributor code), CHANGE_VERSION, SET_BASE_PARAMS (Jetton wallet, claim window, Merkle root), SET_CLAIM_FEE, SET_CLAIM_FEE_ADDRESS, CLEAR_CLAIM_FEE_ADDRESS, SET_PAUSED, SET_FEELESS_THRESHOLD, WITHDRAW (TON) and WITHDRAW_TOKENS (Jettons, sent to the admin).
Anyone can send CLAIM to a Leaf contract. The tokens always go to the address recorded in the Merkle entry, whoever sends the claim, and users cannot change the contract configuration.
The claim fee address is the optional claim_fee_address. When it is set, each charged claim fee goes to it straight away. When it is not set, fees accumulate in the contract (see get_accumulated_fees). Either the admin or the claim fee address can send WITHDRAW_FEES, which sends the contract's TON balance above 0.05 TON to the claim fee address and resets the accumulated fee counter. WITHDRAW_FEES fails while no claim fee address is set, so set one before withdrawing accumulated fees.