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 bit0xffffffff
followed by 256 bit from original message.- we load the data with
load_data()
. - the original
op
is 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_tokens
declaration:transfer
constructor is the message coming from the user's master wallet into this jetton wallet.internal_transfer
constructor 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_body
is 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.fc
file: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_id
of 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_addr
returns 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_address
is 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_depth
andspecial
. So we go past them.store_dict(jetton_wallet_code)
. You might be wondering whystore_dict
instead of something likestore_ref
, because it's a cell? Well,store_dict
actually looks likebuilder store_dict(builder b, cell c) asm(c b) "STDICT";
and stores dictionaryD
represented by cellc
ornull
into builderb
. In other words, stores 1-bit and a reference toc
ifc
is notnull
and 0-bit otherwise. 3 So it's a perfect use case for short-circuitingMaybe ^Cell
at once, becauseMaybe
requires 1 to be prefixed if there is something in it. For that reason,store_maybe_ref
is 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_address
takes the result ofcalculate_jetton_wallet_state_init
and returns a wallet address.to_wallet_address
will bedest:MsgAddressInt
inint_msg_info$0
, so we will need to refer toMsgAddressInt
when looking into this.store_uint(4, 3)
. It stores0b100
. The first0b10
is foraddr_std$10
prefix. The next0
is 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_id
needs to beint8
in this specific type.cell_hash
returns 256-bit uint hash of a cell.state_init
is 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:MsgAddress
oftransfer
constructor. -
cell custom_payload = in_msg_body~load_dict();
iscustom_payload:(Maybe ^Cell)
oftransfer
constructor. We useload_dict()
here to loadMaybe ^Cell
, because it can be used for values of arbitraryMaybe ^Y
types or a dictionary. -
int forward_ton_amount = in_msg_body~load_coins();
is loadingforward_ton_amount:(VarUInteger 16)
. Note thatVarUInteger 16
means $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) >= 1
is not true. This can happen in case of a malformed forward payload. For example, ifforward_payload:(Either Cell ^Cell)
is not included inin_msg_body
at 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;
.
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