How Jettons work on TON with sharding in mind (Part 2)
19 July, 2024
|
Written by 9oelm
Table of Contents
jetton-wallet
Now, we have a look at jetton-wallet.fc, which is the 'child' contract of jetton.
Let us start with the TL-B scheme of storage:
The storage contains balance, owner_address, jetton_master_address, and jetton_wallet_code. Should be self-explanatory. Recall that jetton_wallet_code is the compiled code of jetton-wallet.
load_data() loads all information stored:
save_data does the opposite:
pack_jetton_wallet_data does nothing but to store the data into a cell, which was the same for jetton-minter. Recall that this is because we can only store a cell:
Now, let us start from recv_internal, which is invoked when the contract receives an internal message:
On bounce
From the previous section explaining jetton-minter, we know that the 4th bit is bounced:Bool:
That is why we are checking the 4th bit of the message in the code:
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) {
on_bounce(in_msg_body);
return ();
}
on_bounce function is defined here:
in_msg_body~skip_bits(32); ;; 0xFFFFFFFF: we do this because the body of the bounced message will contain 32 bit0xfffffffffollowed by 256 bit from original message.- we load the data with
load_data(). - the original
opis retrieved byint op = in_msg_body~load_uint(32);. Recall that the structure of the message body is 32 bits of operation followed by 64 bits of query id. throw_unless(709, (op == op::internal_transfer()) | (op == op::burn_notification()));throws if the bounced operation isn't internal transfer or burn notification. The error code isstatic invalid_op = 709.int query_id = in_msg_body~load_uint(64);is loaded to get past the 64 bits.int jetton_amount = in_msg_body~load_coins();loads the amount that was sent in the message body.- The amount is credited again back to balance and written to the storage. This prevents failed messages from falsely deducting the balance.
balance += jetton_amount; save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
We will understand this function better once we look at send_tokens and burn_tokens functions.
op::transfer
Next, we prepare arguments to use. Recall the int_msg_info$0 constructor and its scheme. We are fast forwarding up to the end of fwd_fee:Grams in src:MsgAddressInt dest:MsgAddressInt value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams:
muldiv(cs~load_coins(), 3, 2) means floor(cs~load_coins() * 3 / 2), meaning it wants to get 1.5 times the original message's fwd_fee. muldiv is a multiple-then-divide operation. The intermediate result is stored in 513-bit integer, so it won't overflow if the actual result fits into a 257-bit integer. 1
The forward fee is for outgoing messages (any messages that go out of a contract).
Now when the opcode is op::transfer, we call send_tokens:
Let's break it down.
-
Note the TL-B schemes above
send_tokensdeclaration:transferconstructor is the message coming from the user's master wallet into this jetton wallet.internal_transferconstructor is the message going out of this jetton wallet. -
int query_id = in_msg_body~load_uint(64);. Remember we already loaded the opcode, so the next up is 64-bits longquery_id. -
The rest of
in_msg_bodyis customizable. We can find the rest of the body atwrappers/JettonWallet.ts:After
query_id,jetton_amount, specifying the amount to send, is stored byint jetton_amount = in_msg_body~load_coins();. Same forto_owner_address. The rest of the cell structure is pretty self-explanatory. Just refer to this while readingsend_tokens. -
force_chain(to_owner_address);The reason for calling this function can be traced back to the comment at the top of
jetton-wallet.fcfile:The TON Blockchain consists of one masterchain and up to $2^32$ workchains. Each workchain is a separate chain with its rules. Each workchain can further split into 260 shardchains, or sub-shards, containing a fraction of the workchain's state. Currently, only one workchain is operating on TON - Basechain 2. The Basechain has an
workchain_idof 0.3 So the default mode is always transferring within the same workchain; and without changing this line, the jetton will always be forced to be sent to the same workchain.parse_std_addrreturns the workchain id and account id.int workchain() asm "0 PUSHINT";just means declaring a zero integer variable. Why zero? because we know that there is only one chain right now, which is the Basechain. And its workchain id is 0. So ifto_owner_addressis from another workchain whose workchain id isn't 0,throw_unless(333, wc == workchain());will throw. -
(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();. Load the data. -
balance -= jetton_amount;. Deduct the amount being transferred. -
throw_unless(705, equal_slices(owner_address, sender_address));. Throw if the message didn't come from the owner's wallet. -
throw_unless(706, balance >= 0);. Throw if the balance is insufficient. -
cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);.Don't know why the cell structure looks like this? Back to the TL-B scheme.
store_uint(0, 2). Two bits of zeroes mean there's nothing insplit_depthandspecial. So we go past them.store_dict(jetton_wallet_code). You might be wondering whystore_dictinstead of something likestore_ref, because it's a cell? Well,store_dictactually looks likebuilder store_dict(builder b, cell c) asm(c b) "STDICT";and stores dictionaryDrepresented by cellcornullinto builderb. In other words, stores 1-bit and a reference tocifcis notnulland 0-bit otherwise. 3 So it's a perfect use case for short-circuitingMaybe ^Cellat once, becauseMayberequires 1 to be prefixed if there is something in it. For that reason,store_maybe_refis equivalent to store_dict`..store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)).store_uint(0, 1). This islibrary:(Maybe ^Cell).
-
slice to_wallet_address = calculate_jetton_wallet_address(state_init);.calculate_jetton_wallet_addresstakes the result ofcalculate_jetton_wallet_state_initand returns a wallet address.to_wallet_addresswill bedest:MsgAddressIntinint_msg_info$0, so we will need to refer toMsgAddressIntwhen looking into this.store_uint(4, 3). It stores0b100. The first0b10is foraddr_std$10prefix. The next0is forMaybe, to say that there is none..store_int(workchain(), 8). Store a 8-bit long workchain id, which is0b00000000. The workchain id is a signed 32-bit integer, but addr_std dictates thatworkchain_idneeds to beint8in this specific type.cell_hashreturns 256-bit uint hash of a cell.state_initis composed ofjetton_wallet_code,owner_address, andjetton_master_address, so every jetton wallet address is practically unique, because if it were a different jetton, owner, or a jetton creator, the hash would be different. This isaddress:bits256.
-
slice response_address = in_msg_body~load_msg_addr();This isresponse_destination:MsgAddressoftransferconstructor. -
cell custom_payload = in_msg_body~load_dict();iscustom_payload:(Maybe ^Cell)oftransferconstructor. We useload_dict()here to loadMaybe ^Cell, because it can be used for values of arbitraryMaybe ^Ytypes or a dictionary. -
int forward_ton_amount = in_msg_body~load_coins();is loadingforward_ton_amount:(VarUInteger 16). Note thatVarUInteger 16means $128 - 8 = 120$ bits of uint. -
throw_unless(708, slice_bits(in_msg_body) >= 1);.slice_bits(in_msg_body)will check if there is any remaining data ifslice_bits(in_msg_body) >= 1is not true. This can happen in case of a malformed forward payload. For example, ifforward_payload:(Either Cell ^Cell)is not included inin_msg_bodyat all, this would throw becauseslice_bits(in_msg_body) == 0. -
Now that we checked that there's a remaining part of the message, we safely call
slice either_forward_payload = in_msg_body;. -
We are now very used to this structure of cell; No explanation needed. This is
info:CommonMsgInfoRelaxedandinit:(Maybe (Either StateInit ^StateInit)).var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_wallet_address) .store_coins(0) .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) .store_ref(state_init); -
Then, we construct the message body. We specify the opcode and query id. Then we store some variables based on the TL-B scheme provieded at the top of
send_tokensfunction:query_id:uint64 amount:(VarUInteger 16) from:MsgAddress response_address:MsgAddress forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) = InternalMsgBody; -
msg = msg.store_ref(msg_body);.msg_bodyis stored inmsgas a reference. -
forward fee. There will be three messages that will be created at maximum:
int fwd_count = forward_ton_amount ? 2 : 1; throw_unless(709, msg_value > forward_ton_amount + ;; 3 messages: wal1->wal2, wal2->owner, wal2->response ;; but last one is optional (it is ok if it fails) fwd_count * fwd_fee + (2 * gas_consumption() + min_tons_for_storage())); ;; universal message send fee calculation may be activated here ;; by using this instead of fwd_fee ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15)- message from
owner_addresstoto_wallet_address - message from
to_wallet_addressto the owner ofto_wallet_addressfor transfer notification. - message from
to_wallet_addresstoresponse_addressfor excess mesasge. Only sent if any ton coins are left after paying the fees.
We will get back to the forwarding fee later. For now, just note that we are checking if we have enough TON sent for forwarding, gas and storage.
- message from
-
send_raw_message(msg.send_cell(), 64). 64 means "carry all the remaining value of the inbound message in addition to the value initially indicated in the new message". -
save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);. Finally, the updated balance is saved.
op::internal_transfer
References
[TON Blog] How to shard your TON smart contract and why - studying the anatomy of TON's Jettons
[TON Blog] Six unique aspects of TON Blockchain that will surprise Solidity developers
[Excalidraw] Contracts design diagram
[Github] awesome-ton-smart-contracts
[Youtube] Technical Demo: Sharded Smart Contract Architecture for Smart Contract Developers