TON Virtual Machine / Func Learning
https://docs.ton.org/learn/tvm-instructions/tvm-overview
What are messages and what are transactions?
✉️ Message - the things that happens in between two contracts. It carries a little bit of coins and arbitrary data to a contract address.
💎 Transaction - activity on the contract including running contract code, updating contract state, and emitting new messages.
A transaction in TON consists of the following:
the incoming message that initially triggers the contract (special ways to trigger exist)
contract actions caused by the incoming message, such as an update to the contract's storage (optional)
outgoing generated messages that are sent to other actors (optional)
Contracts interact with each other using messages
every TON smart contract having its own small blockchain
Contracts in TON are not allowed to see the global state, they can only see their own state and the only way that they can communicate with other contracts would be asynchronous message passing.
TON's idea is to figure out how to provide the developers infinite flexibility and scalability.
Contracts in TON are not allowed to see the global state, they can only see their own state.The only way that contracts can communicate with other contracts would be asynchronous message passing.
Every time you make a transaction in a contract, this transaction is 100% independent from another transaction on a separate contract, and those could be processed in any order or independently.
In TON all contracts could be sharded, and also they could communicate with each other, and those messages are routed by the system.
In TON you don't have the single contract that keeps a track of all the accounts that own portions of the token or the units of the token. Instead, there is a multitude of independent contracts with the same code that are called token balances or token wallets. And every user's wallet communicates directly with their own token wallet.
Storage
Everything is built out of 🔸 cells.
On top of the cells you have types provided by 💻 TVM.
TON virtual machine (TVM) do
Verifies all the network rules regarding gas costs and correctness of the operations.
Goes through the code and executes it.
Messages
External message is really just a string of data that comes from nowhere from the perspective of a blockchain. This data is not authenticated by itself. It doesn't have any money attached to it in form of Toncoins. And it can really contain anything that the author of a contract wants.
The internal messages are those that are sent by contracts to other contracts. And these messages have a little bit more rich structure. First of all, the internal messages can carry the balances. And when the contract sends the message to another contract, they can attach any amount of coins to it. Second, those messages are securely authenticated by the address of a sending contract. And the entire architecture of TON guarantees the contracts that this address of a sender is correct.
There are five phases that the transaction goes through: storage phase, credit phase, computation phase, action phase, bounce phase.
Storage phase is the phase when the blockchain charges the contract for all the rent that it owes for its existence. And the rent is computed as a price per bit per second.
Credit phase is the phase when the coins attached to incoming message get credited to the contract.
In computation phase the TVM executes the code and verifies each operation and also keeps track of gas usage.
In action phase, the smart contract transitions to a new state and outgoing messages are processed.
Bounce phase happens if the contract failed and the incoming message had a flag saying I'm a bounceable message. It means that at this phase if there is any failure and there's any money left from the incoming message, then the contract would create the outgoing message back to the sender to bounce the money back.
In TON there are generally three categories of fees. This is the gas cost, the rent, and the message fees.
The tokens in TON are designed in a way that there is a single minter contract, but this contract does not store a list of all its users. Instead, it's authorized to mint and create tokens for other users, and the balances of individual users are spread in so-called Jettons wallets.
Designing Scalable contracts
To help you, give you a guideline on how to properly design multi-user and large scale applications and TON, there are some rules. 📰
❗ Avoid having variable length data.❗ If you have to have a list, then at least make it short and bounded, like statically defined.❗ If you need to have a list and it should be dynamically growing, then at least make sure that this contract is owned by a single user and they have the exclusive right to control its storage. A simple example are the records in the TON DNS items.
Rules of working with lists in TON :
Make data lists short and bounded
Make sure that a contract with big dynamic data is owned by a single user
Avoid having variable length data
In TON the tokens are implemented as distinct contracts. One contract per user plus a separate minter contract that provides the interface for creating new units of the token.
Jetton wallets in TON are a type of subcontract that allows users to create a separate wallet for each user, which can be used for various purposes such as storing and managing small amounts of TON, creating custom tokens, and more.
Smart contract dev life cycle
Our code -> FunC -> Fift -> BOC (Bytecode)
recv_internal is the most important function of all smart contracts
// To add a comment use ";;"
// function handling incoming messages
() recv_internal(int msg_value,cell in_msg,slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
in flags = cs~load_uint(4); // load flags of the cell
slice sender_address = cs~load_msg_addr(); // address is right after flags
// write sender address into persistent storage
set_data(begin_cell().store_slice(sender_address).end_cell());
}
// getter to access persistent storage
slice get_the_latest_sender() method_id {
slice ds = get_data().begin_parse();
// load memory of the size of a msg address
return ds~load_msg_addr();
}
msg_value - this parameter is telling us how many TON coin (or grams) are received
in_msg - this is a complete message that we've received, with all the information about who sent it etc. We can see that it has type Cell. The message body is stored as a Cell on the TVM, so there is one whole Cell dedicated to our message with all of its data.
in_msg_body - this is an actual "readable" part of the message that we received. It has a type of slice, because it is part of the Cell, it indicates the "address" from which part of the cell we should start reading if we want to read this slice parameter.
⇒ both msg_value and in_msg_body are derivable from the in_msg, but for usability we receive them as parameters into the receive_internal function.
** A slice is an address, a pointer.
Always import stdlib.fc in an imports/ folder ⇒ allows to get a lot of “precompiled function” and in order to manipulate data and write other logic in our contract
4 Flags
Modifers :
impure : function is going to have side effect(affecting the state) ⇒ need to validate the data
inline/inline_ref : function code is actually substituted in every place where the function is called. It is forbidden to use recursive calls in inlined functions.
method_id : Every function in TVM program has an internal integer id by which it can be called (like function selector in solidity)
** ~ means we’re reading outside function
OP Codes : Op codes are usually stored in the very beginning of the in_msg_body slice that is passed into the recv_internal function. Op code is simply an integer, that we use to indicate a certain ****logic block.
This is a standard, but op codes don't appear there by themseves - you have to pass them once you are composing a message before sending it.
// To add a comment use ";;"
// function handling incoming messages
() recv_internal(int msg_value,cell in_msg,slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
in flags = cs~load_uint(4); // load flags of the cell
slice sender_address = cs~load_msg_addr(); // address is right after flags
// load op code
int op = in_msg_body~load_uint(32);
if (op == 1) {
slice ds = get_data().begin_parse();
int counter_value = ds~load_uint(32);
// counter logic
set_data(
begin_cell()
.store_uint(counter_value+1,32)
.store_slice(sender_address)
.end_cell());
}
if (op == 2) {
int increment_by = in_msg_body~load_uint(32);
slice ds = get_data().begin_parse();
int counter_value = ds~load_uint(32);
// counter logic
set_data(
begin_cell()
.store_uint(counter_value + increment_by,32)
.store_slice(sender_address)
.end_cell());
}
}
// getter to access persistent storage
slice get_contract_storage_data() method_id {
slice ds = get_data().begin_parse();
// load memory of the size of a msg address
return (
ds~load_uint(32), // counter
ds~load_msg_addr(), // msg.sender
);
}
Withdraw/deposit
// To add a comment use ";;"
const const::in_tons_for_storage = 10000000; // 0.1 TON
(int,slice,slice) load_data() inline {
var ds = get_data().begin_parse();
return (
ds~load_uint(32), //counter_value
ds~load_msg_addr(), // most recent sender
ds~load_msg_addr(), // owner address
}
() save_data(int counter_value,slice recent_sender,slice owner_address) impure inline {
set_data(begin_cell()
.store_uint(counter_value,32) // counter_value
.store_slice(recent_sender) // most recent sender
.store_slice(owner_address) // owner_address
.end_cell());
}
// function handling incoming messages
() recv_internal(int msg_value,cell in_msg,slice in_msg_body) impure {
var (counter_value,recent_sender,owner_address) = load_data();
slice cs = in_msg.begin_parse();
in flags = cs~load_uint(4); // load flags of the cell
slice sender_address = cs~load_msg_addr(); // address is right after flags
// load op code
int op = in_msg_body~load_uint(32);
if (op == 1) {
int increment_by = in_msg_body~load_uint(32);
save_data(counter_value + increment_by,sender_address,owner_address);
return();
}
// deposit
if (op == 2) {
return(); // we just accept funds
}
// withdrawal
if (op == 3) {
throw_unless(103,equal_slice_bits(sender_address,owner_address));
int withdraw_amount = in_msg_body~load_coins();
int balance = balance();
// set minimum that is going to stay to pay rent
throw_unless(104,balance >= withdraw_amount);
int return_value = min(withdraw_amount, balance- const::min_tons_for_storage);
// sending internal message logic => open documentation for this
var msg = begin_cell()
.store_uint(0x18,6) // flag and empty source address
.store_slice(sender_address) // dest address
.store_coins(return_value) // amount to be send
.store_uint(0,1+4+4+64+32+1+1); // to send to an address (not a contract)
// (ordinary message) + 1 (pay transfer fees separately from the message)
int msg_mode = 1;
send_raw_message(msg.end_cell(),msg_mode);
return();
}
// any error code > 1 is an error code
throw(777);
}
// getter to access persistent storage
slice get_contract_storage_data() method_id {
var (counter_value,recent_sender,owner_address) = load_data();
// load memory of the size of a msg address
return (
counter_value,recent_sender,owner_address
);
}
int balance() method_id {
var [balance,_] = get_balance();
return balance;
}
send_raw_message is a standard function that accepts a cell with message and an integer that is containing sum of mode and flag. There are currently 3 Modes and 3 Flags for messages. You can combine a single mode with several (maybe none) flags to get a required mode. Combination simply means getting sum of their values. A table with descriptions of Modes and Flags is given in this documentation part.
Jeton Standard
-
2 contracts :
wallet smart contract handling transfer,burn,getter methods
master contract : suppose to implement functionalities like getters jeton_data, total_supply, admin_address etc
A wallet smart contract is deployed by the minter smart contract once we mint the Jettons. Or the wallet smart contract is deployed by another wallet smart contract when we do transfer. So again, we have certain preparations:
**my_address() = address(this)
// if I have a wallet and I want to get some jetons
// => the jetons wouldn't come to my wallet, they would come
// to the jeton wallet contract, here is the contract to know what will be the addr
slice get_wallet_address(slice owner_address) method_id {
(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code);
}
Security
External messages
Entry points :
Every contract must have a function recv_internal function ⇒ The concept is familiar: the user makes a transaction off-chain, usually, signs it with their private key and sends it to the contract.
Optional recv_external is for inbound external messages.
run_ticktock is called in ticktock transactions of special smart contracts. Not used by regular contracts.
Wallet owner needs to supply a timestamp until which message is valid. It still can be replayed during that time though.
recv_external should be used carefully and accept_message() only after verification. Otherwise, gas could deplete the contract funds with gas-free malicious executions.
Also notice that the same signed message can be replayed on another wallet of the same owner. Or message in testnet can be replayed at mainnet.
https://docs.ton.org/develop/smart-contracts/guidelines/external-messages
In EVM replay attack goes out-of-the-box :
private key has only one associated address
nonce is automatically increased after each processed tx
wallet owner pays for gas and failed transaction can’t be replay
TON is more flexible :
one private key can control many wallets
wallet can have any logic coded
external tx is credited with 10k gas. If accepted it will spend contract balance ⇒ DOS
Recommendation :
let the wallet contract do this job, have only recv_internal in your contract
even if you are writing a multisig or special wallet you can still process only internal messages from regular wallets
you will get EVM-style txs in this case with all the flexibility under the hood
Carry-Value Pattern
Partial execution of transactions
⇒ How to work with message flow :
Determine entry points of the contracts group
check that there are no payload,gas supply, message origin to minimaze the risk of failure
in other message handler (”consequences”) check message origin. Other checks are “asserts”
Can’t afford failing in “consequences”. if can fail —> review message flow
In TON, it is strongly recommended to avoid “unbounded data structures” and split a single logical contract into small pieces, each of which manages a small amount of data. The basic example is the implementation of TON Jettons (this is TON's version of Ethereum's ERC-20 token standard).
Note that if the destination_wallet was unable to process the op::internal_transfer message (an exception occurred or the gas ran out), then this part and subsequent steps will not be executed.
But the first step (reducing the balance in sender_wallet) will be completed. The result is a partial execution of the transaction, an inconsistent state of the Jetton, and, in this case, the loss of money.
In the worst case scenario, all the tokens can be stolen in this way. Imagine that you first accrue bonuses to the user, and then send an op::burn message to their Jetton wallet, but you cannot guarantee that the op::burn will be processed successfully.
Use the DOT language to describe and update such diagrams during the course of the audit. That's what we recommend the developers as well.
Unlike in Ethereum smart contracts where the external call must execute before the execution continues, in TON the external call is a message that will be delivered and processed after some time in some new conditions. That requires much more attention from the developer.
Avoid Fails and Catch Bounced Messages
Determine the "entry points" of the "contracts group".
Check there the payload, gas supply, message origin to minimaze the risk of failure.
In other message handlers ("consequences") check message origin. Other checks are "asserts".
Can't afford failing in "consequences". If can fail - review message flow.
Using the message flow, first define the entry point. This is the message that starts the cascade of messages in your group of contracts (“consequences”). It is here that everything needs to be checked (payload, gas supply, etc.) in order to minimize the possibility of failure in subsequent stages.
If you are not sure whether it will be possible to fulfill all your plans (for example, whether the user has enough tokens to complete the deal), it means that the message flow is probably built incorrectly.
In subsequent messages (consequences), all throw_if()/throw_unless() will play the role of asserts rather than actually checking something.
Expect a Man-in-the-Middle of the Message Flow
A message cascade can be processed over many blocks.
Assume that while one message flow is running, an attacker can initiate a second one in parallel.
That is, if a property was checked at the beginning (e.g. whether the user has enough tokens), do not assume that at the third stage in the same contract they will still satisfy this property.
Use a Carry-Value Pattern
We came up to the most important advice: use Carry-Value Pattern.
In TON Jetton, this is demonstrated:
- sender_wallet subtracts the balance and sends it with an op::internal_transfer message to destination_wallet,
and it, in turn,
receives the balance with the message and adds it to its own balance (or bounces it back).
The value is transferred, not the message.
Return Value Instead of Rejecting
From Carry-Value Pattern, it follows that your group of contracts will often receive not just a request, but a request along with a value. So you can't just refuse to execute the request (via throw_unless()), you have to send the Jettons back to the sender.
For example, a typical flow start:
sender sends an op::transfer message via sender_wallet to your_contract_wallet, specifying forward_ton_amount and forward_payload for your contract.
sender_wallet sends an op::internal_transfer message to your_contract_wallet.
your_contract_wallet sends an op::transfer_notification message to your_contract, delivering forward_ton_amount, forward_payload, and the sender_address and jetton_amount.
And here in your contract in handle_transfer_notification() the flow begins.
There you need to figure out what kind of request it was, whether there is enough gas to complete it, and whether everything is correct in the payload.
At this stage, you should not use throw(), because then the Jettons will simply be lost, and the request will not be executed. It's worth using the try-catch statements. If something does not meet the expectations of your contract, the Jettons must be returned.
Return jettons to sender_address
But be careful,
Send returning op::transfer to sender_address, not to any real Jetton wallets.
You don't know if you got the op::transfer_notification from a real wallet or someone is joking.
If your contract receives unexpected or unknown Jettons, they must also be returned.
Resume
TON requires much more design efforts from the developer to avoid "unbounded data structures" and to allow "infinite sharding paradigm".
Even with the correct design and using of "carry-value pattern" the TON developer has to take into account the async nature of TON messages.
Accepting the Jettons requires manual careful payload analysis and checking the conditions.
Return the value via sender_address since messages can be forged.
At the entry point, verify the payload, assess gas availability, and confirm message origin, then validate the message origin at every subsequent step.
Gas Management
Ethereum
- everything revert if not enough gas
TON
if not enough gas tx will be partially executed
if there is too much gas the excess must be returned ⇒ Developper responsibility
TON can’t do everything itself because of asynchronous nature
⇒ Gas Calculation
Find entry points on message flow
Estimate the cost of each handler
Check in “entry points” that the msg_value is enough
You can’t demand enough with a margin everywhere (say 1 TON). the gas is divided among the “consequences”
Let's say your contract sends three messages, then you can only send 0.33 TON to each. This means that they should “demand” less. It’s important to calculate the gas requirements of your whole contract carefully.
Things get more complicated if, during development, your code starts sending more messages. Gas requirements need to be rechecked and updated.
In TON, the situation is like this :
If there is not enough gas, the transaction will be partially executed;
If there is too much gas, the excess must be returned. This is the developer’s responsibility;
If a “group of contracts” exchanges messages, then control and calculation must be carried out in each message.
TON cannot automatically calculate the gas. The complete execution of the transaction with all its consequences can take a long time, and by the end, the user may not have enough toncoins in their wallet. The carry-value principle is used again here.
⇒ Return Gas Excess Carefully
If excess gas is not returned to the sender, funds will accumulate in your contracts over time
Nothing terrible but suboptimal. you can add a function for raking out excesses
Popular contracts like TON Jetton retturn to the sender with the message op::excesses
\=> send_raw_message(msg, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE) pass the rest of gas.
⇒ Useful if message flow is linear: each handler sends only one message.
When not recommended to carry all the remaining gas :
storage_fee is deducted from the balance of the contract, not from incoming gas
Emitting events eats contract balance,not gas
Attaching value to the message or using SEND_MODE_PAY_FEES_SEPARATELY eats contract balance : If your contract attaches value when sending messages or uses SEND_MODE_PAY_FEES_SEPARETELY = 1. These actions deduct from the balance of the contract, which means that returning unused is "working at a loss.”
Common way to calculate gas cost :
int ton_balance_before_msg = my_ton_balance - msg_value;
int storage_fee = const::min_tons_for_storage - min(ton_balance_before_msg, const::min_tons_for_storage);
msg_value -= storage_fee + const::gas_consumption;
if(forward_ton_amount) {
msg_value -= (forward_ton_amount + fwd_fee);
...
}
if (msg_value > 0) { ;; there is still something to return
var msg = begin_cell()
.store_uint(0x10, 6)
.store_slice(response_address)
.store_coins(msg_value)
...
Resume
TON requires deliberate efforts from developers to manage the gas: calculate the spending and checking if enough provided.
Gas cost can rise over time if "unbounded data structure" used.
TON recommends returning the excesses to the sender - it is also on developer.
If run out of gas, the transaction will be partially executed. That can be a critical issue.
Also here : https://github.com/ton-blockchain/token-contract/blob/main/misc/forward-fee-calc.fc
Storage Management
In TON the storage is accessed via get_data()/set_data() (c4 register holds the ref to the bag of cells).
- Overwrite parameters like min_amount ⇒ FunC allows redeclaring the variables.
⇒ Solution 1 : Use global Variables
⇒ Solution 2 : use nested storage
⇒ Solution 3 : Use end_parse() wherever possible when reading data from storage and from the message payload. Since TON uses bit streams with variable data format, it’s helpful to ensure that you read as much as you write. This can save you an hour of debugging.
Certik Audits to check (LOL)
More Resources :
https://slowmist.medium.com/slowmist-best-practices-for-toncoin-smart-contract-security-df209eb19d08
Checklist for auditing TON Smart Contracts