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.