Skip to content

Architecture

TokenTable TVM distributes Jettons on the TON blockchain. It uses Merkle proofs to check that a claimed allocation (recipient address, amount and unlock time) is part of the committed distribution list.

Overview

The full distribution list is a dictionary of entries keyed by index, and only its hash (the Merkle root) is stored on-chain. Proofs are generated off-chain. Each claim carries the proof for one entry, so the contract never stores the whole list, a claim reveals only its own entry, and only one proof is verified on-chain per claim. Proof verification, double-claim protection and the Jetton transfer all happen on-chain.

Once an entry is verified against the root, the contract sends the entry's amount of Jettons to the address recorded in that entry, subject to the claim window, the entry's own unlock time and the pause flag. Because tokens always go to that address, the contract does not check who sends the claim.

Each entry has its own Leaf contract that records whether it has been claimed, which stops the same entry being claimed twice. Claim state lives in these small contracts, so the Main Distributor Contract's storage does not grow with the number of recipients.

Component contracts

Main Distributor Contract

The Main Distributor Contract (contracts/merkle-token-distributor.fc) manages the distribution. It stores the Merkle root, the claim window, the address of its own Jetton wallet, the Leaf contract code, the claim fee settings and the pause flag. The admin sets the Merkle root, the Jetton wallet address and the claim window with the SET_BASE_PARAMS message, and every claim is checked against that root.

When it receives PROCESS_CLAIM from a Leaf contract, the contract checks the proof against the stored root, and checks the claim window, the pause flag, its TON balance and that the sender is the Leaf contract for that index. If every check passes, it sends a standard Jetton transfer to its own Jetton wallet, which delivers the tokens to the address in the entry. If any check fails, it unlocks the Leaf contract instead.

Leaf contracts

Each Leaf contract (contracts/leaf.fc) belongs to one entry index of the distribution list and stores only a used flag, the distributor address and the index. Its address is derived from the Leaf code, the distributor address and the index, so you can compute it off-chain or with the distributor's get_leaf_address getter.

Leaf contracts are not deployed in advance. The first CLAIM message for an index carries the Leaf's StateInit and deploys it.

The claimant sends CLAIM with the Merkle proof and the index to the Leaf contract. The Leaf checks that the proof is a Merkle proof cell that contains the index, and leaves the check against the root to the Main Distributor Contract. It then sets used to 1 and forwards the proof, the index, its previous used value and the claimant's address to the Main Distributor Contract in a PROCESS_CLAIM message. If the Main Distributor Contract rejects the claim, it sends UNLOCK back and the Leaf resets used to 0.

Claim flow

TON claim message flow TON claim message flow
TON claim message flow

A claim goes from the claimant to the Leaf contract, then to the Main Distributor Contract, which sends the tokens to the recipient's wallet:

  1. The claimant sends CLAIM (Merkle proof and index) to the Leaf contract for that index, with 0.17 TON plus the current claim fee attached (the amount Leaf.sendClaim in the wrappers sends). The first claim for an index also deploys the Leaf.
  2. The Leaf checks that the proof contains the index, sets used to 1 and forwards all of its balance to the Main Distributor Contract in a PROCESS_CLAIM message.
  3. The Main Distributor Contract verifies the claim. If it passes, the contract takes the claim fee (only when the amount is above the feeless threshold) and sends a Jetton transfer from its own Jetton wallet to the address in the entry. Remaining TON goes back to that address as excess.
  4. If the Leaf's own checks fail (malformed proof, index not in the proof, entry already claimed, not enough TON attached), the Leaf rejects the message and forwards nothing.
  5. If the Main Distributor Contract's checks fail (paused, not enough TON, proof does not match the root, outside the claim window or the entry's unlock time, sender is not the Leaf for that index), it sends UNLOCK to the Leaf. The Leaf resets used to 0 and returns the remaining TON to the claimant, who can then claim again.
  6. If the Jetton transfer bounces, for example because the distributor's Jetton wallet does not hold enough tokens, the Main Distributor Contract sends UNLOCK to the Leaf without a refund address. The Leaf resets used to 0 so the entry can be claimed again later.