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;
. -
We are now very used to this structure of cell; No explanation needed. This is
info:CommonMsgInfoRelaxed
andinit:(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_tokens
function: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_body
is stored inmsg
as 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_address
toto_wallet_address
- message from
to_wallet_address
to the owner ofto_wallet_address
for transfer notification. - message from
to_wallet_address
toresponse_address
for 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