Skip to content

Smart Contract Schema

The TokenTable TVM contracts are written in FunC. The TL-B (Type Language - Binary) schemas below describe how they serialize their storage and messages, taken from contracts/merkle-token-distributor.fc, contracts/leaf.fc and the TypeScript wrappers in wrappers/.

Main Distributor Contract

The Main Distributor Contract splits its persistent storage across the root cell and two referenced cells:

merkle_token_distributor_storage#_
    version:uint64              ;; Contract version. The wrappers store a short string such as "1.0.0" as its byte value
    admin_address:MsgAddress    ;; Admin address with management privileges
    root:uint256                ;; Merkle root: hash of the entries dictionary cell
    ^[
      deployer:MsgAddress       ;; Deployer address, initial contract creator
      token:MsgAddress          ;; The distributor's own Jetton wallet, which holds the tokens to distribute
      start_time:uint32         ;; Start of the claim window (Unix time)
      end_time:uint32           ;; End of the claim window (Unix time)
      leaf_code:^Cell           ;; Code for the Leaf contracts
    ]
    ^[
      claim_fee:Coins                                  ;; Claim fee in nanotons
      has_claim_address:(## 1)                         ;; 1 if a claim fee address is set
      claim_fee_address:has_claim_address?MsgAddress   ;; Address that receives claim fees
      paused:(## 1)                                    ;; 1 = paused, 0 = active
      feeless_threshold:Coins                          ;; Claims of this amount or less are not charged the claim fee
      fee_accumulated:Coins                            ;; Fees held in the contract while no claim fee address is set
    ]
    = MerkleTokenDistributorStorage;

Leaf contract

Each Leaf contract belongs to one entry index and records whether that entry has been claimed. Its address is derived from the Leaf code and this initial data (with used = 0), on workchain 0.

leaf_storage#_
    used:(## 1)               ;; 1 = claim in progress or completed, 0 = unused
    distributor:MsgAddress    ;; Address of the main distributor contract
    index:uint64              ;; Index of the entry in the merkle tree
    = LeafStorage;

Distribution entries and Merkle proof

The distribution list is a dictionary keyed by a 64-bit index. The Merkle root stored in the Main Distributor Contract is the hash of the dictionary's root cell (stored with storeDictDirect). A claim carries a Merkle proof cell (an exotic cell of type 3) for one index, generated with dict.generateMerkleProof(index).

entry#_
    address:MsgAddress        ;; Recipient of the tokens
    claimable_amount:Coins    ;; Amount of Jettons, in the Jetton's smallest units
    claimable_timestamp:uint32 ;; Earliest time this entry can be claimed (Unix time)
    = Entry;

_ (Hashmap 64 Entry) = Entries;

Claim messages

The claimant sends CLAIM to the Leaf contract, the Leaf sends PROCESS_CLAIM to the Main Distributor Contract, and the Main Distributor Contract answers with UNLOCK if it rejects the claim. Op codes are the CRC32 of the op name ("CLAIM"c in FunC).

claim#503d1c97
    query_id:uint64
    proof:^Cell               ;; Merkle proof cell for the entry
    index:uint64              ;; Index of the entry in the merkle tree
    = InMsgBody;              ;; claimant -> Leaf

process_claim#d0d5f405
    query_id:uint64
    proof:^Cell
    index:uint64
    used:(## 1)               ;; The Leaf's `used` value before this claim (always 0)
    claimant:MsgAddress       ;; Sender of the CLAIM message
    = InMsgBody;              ;; Leaf -> Main Distributor Contract

unlock#9a1c7769
    query_id:uint64
    index:uint64
    has_claimant:(## 1)
    claimant:has_claimant?MsgAddress  ;; If present, the Leaf returns the remaining TON here
    = InMsgBody;              ;; Main Distributor Contract -> Leaf

The wrappers' Leaf.sendClaim attaches 0.17 TON (MIN_CLAIM_FEE) plus the current claim fee. The Leaf requires a balance of at least 0.07 TON before it forwards the claim. The Main Distributor Contract requires a balance of at least 0.11 TON plus the claim fee, otherwise it unlocks the Leaf.

Token transfer

After a claim passes verification, the Main Distributor Contract sends a standard Jetton (TEP-74) transfer to its own Jetton wallet (token):

transfer#0f8a7ea5
    query_id:uint64                       ;; Set to the entry index
    amount:(VarUInteger 16)               ;; claimable_amount from the entry
    destination:MsgAddress                ;; address from the entry
    response_destination:MsgAddress       ;; address from the entry (receives the excess TON)
    custom_payload:(Maybe ^Cell)          ;; none
    forward_ton_amount:(VarUInteger 16)   ;; 0
    forward_payload:(Either Cell ^Cell)   ;; empty
    = InternalMsgBody;

If this message bounces (for example, the Jetton wallet does not hold enough tokens), the Main Distributor Contract reads the index from the bounced query_id and sends UNLOCK to that index's Leaf without a claimant address.

The claim fee is charged only when claimable_amount is greater than feeless_threshold. If a claim fee address is set, the contract sends the fee to it in the same transaction. Otherwise it adds the fee to fee_accumulated and keeps it.

Admin messages

All admin messages go to the Main Distributor Contract and start with op:uint32 query_id:uint64. Unless noted, only admin_address may send them.

Op name Op code Body after query_id Effect
CHANGE_ADMIN 0x4640fc1d new_admin:MsgAddress Replaces the admin.
CHANGE_CODE 0x5800c4cf code_type:uint32 new_code:^Cell code_type 0xf00aed53 (LEAF) replaces the Leaf code; 0xcdb4d337 (TOKEN_DISTRIBUTOR) upgrades the distributor's own code.
CHANGE_VERSION 0x5d456901 version:uint64 Sets the version.
SET_BASE_PARAMS 0xc46600a8 token:MsgAddress start_time:uint32 end_time:uint32 root:uint256 Sets the Jetton wallet, claim window and Merkle root. Fails if start_time >= end_time or root is 0.
SET_CLAIM_FEE 0x0c281f96 claim_fee:Coins Sets the claim fee.
SET_CLAIM_FEE_ADDRESS 0x10354bed claim_fee_address:MsgAddress Sets the claim fee address.
CLEAR_CLAIM_FEE_ADDRESS 0x11faeb88 (none) Removes the claim fee address and sets the claim fee to 0.
SET_PAUSED 0x26d65fa9 paused:(## 1) Pauses or resumes claims.
SET_FEELESS_THRESHOLD 0x79ce8ce5 feeless_threshold:Coins Sets the feeless threshold.
WITHDRAW 0x73edc3d2 amount:Coins Sends amount TON to the admin. Fails if less than 0.05 TON would remain.
WITHDRAW_TOKENS 0xa17f43e1 amount:Coins Sends amount Jettons from the distributor's Jetton wallet to the admin.
WITHDRAW_FEES 0xf9c658d4 (none) Sender may be the admin or the claim fee address. Sends the TON balance above 0.05 TON to the claim fee address and resets fee_accumulated. Fails if no claim fee address is set.

Get methods

Contract Get method Returns
Main Distributor get_version version
Main Distributor get_leaf_address(index) Address of the Leaf for index
Main Distributor get_root Merkle root
Main Distributor get_time (start_time, end_time)
Main Distributor get_token The distributor's Jetton wallet
Main Distributor get_admin_address Admin address
Main Distributor get_claim_fee Claim fee in nanotons
Main Distributor get_claim_fee_address Claim fee address, or null
Main Distributor get_paused 1 if paused
Main Distributor get_feeless_threshold Feeless threshold
Main Distributor get_accumulated_fees fee_accumulated
Leaf get_is_used used
Leaf get_leaf_data Cell with used, distributor and index

Error codes

Code Name Raised when
35 unauthorized Sender is not allowed to send this message.
36 invalid_message_params Invalid parameters, such as an empty proof, empty address or bad claim window.
38 time_inactive Outside the claim window, or before the entry's claimable_timestamp.
40 leaf_used The entry has already been claimed or a claim is in progress.
42 is_proof_exotic The proof is not an exotic cell.
43 tree_exceeded The proof is not a Merkle proof cell (type 3).
44 invalid_tree The proof does not match the stored Merkle root.
45 not_found The index is not in the proof.
46 wrong_sender PROCESS_CLAIM did not come from the Leaf for that index.
47 invalid_index UNLOCK index does not match the Leaf's index.
48 not_enough_tokens Not enough TON attached or held to process the claim.
49 contract_paused Claims are paused.
65535 unknown_op Unknown op code.

The Leaf raises 36, 40, 42, 43, 45 and 48 itself, which fails the CLAIM message. In the Main Distributor Contract, failures of the claim checks (38, 42 to 46, 48 and 49) do not fail the transaction. They make the contract send UNLOCK to the Leaf instead.