https://broxus.com/
BlogThreaded Solidity: How to write smart co...

Threaded Solidity: How to write smart contracts for TVM-compatible blockchains

Sep 18 • 20 min read

In 2020, in order to write smart contracts for the only decentralized network that existed on the TON Virtual Machine, developers had to learn the Fift language, which was designed for developers to create and test smart contracts executed on this virtual machine.

Later, the development team split into two separate groups. One of the new groups of developers began to improve the TON Virtual Machine, and as a result of the improvements, the Threaded Virtual Machine (TVM) appeared. Following this, the Everscale and Venom networks were launched (the latter is currently in the testnet stage), both of which rely upon the Threaded Virtual Machine to execute smart contracts.

Later on, a special Solidity code compiler was written into TVM machine instructions. Naturally, this added convenience to the development of smart contracts for the Everscale and Venom blockchains, as it is capable of transforming high-level code into code that can be processed by a machine.

In this article, we will describe the process of writing smart contracts for TVM blockchains. Smart contract files on the Everscale and Venom networks use the .tsol extension, which stands for Threaded Solidity, which differentiates asynchronous smart contracts from regular synchronous contracts on Solidity. However, the .sol extension is also used on these networks.

Developer tools for TVM solutions

Writing smart contracts for TVM blockchains is made much more comfortable with the help of community plugins for Jetbrains IDEA or Visual Studio Code.

The testing of smart contracts is carried out using the Locklift framework. It supports automated tests on Mocha, and, in addition, allows for testing to be done within the Locklift Network’s own environment, rather than on the test/mainnet.

The Locklift network is more suitable for testing various scenarios for the execution of smart contracts since it does not save states, unlike tests done on a local node.

Locklift has excellent exception tracing capable of highlighting specific lines of .tsol code when testing within its own environment.

In addition, testing smart contracts in the Locklift environment does not involve interaction with the node and is carried out entirely in the machine’s RAM, which significantly speeds up testing and increases stability under high loads.

For further interaction with smart contracts, programmers should use the Everscale Inpage Provider — an SDK with a set of APIs for interacting with the Everscale and Venom blockchains. The toolkit can be built as a library for Typescript or Javascript. The writing experience is similar to using the web3.js framework to write smart contracts in Solidity.

Particularities of Threaded Solidity 

There are some basics that need to be understood when it comes to writing smart contracts for the Venom and Everscale decentralized networks: 

  • smart contract code is executed on the Threaded Virtual Machine, which works with its own data structure or tree of cells;
  • these networks operate in an asynchronous manner;
  • all actions on the network are interactions between smart contracts via internal messages;
  • a storageFee, which is a commission for storing data on the network, is charged from each smart contract. When a smart contract balance reaches -0.1, it is first frozen and then removed from the network state. To enable recovery, a hash of the smart contract state remains on the network.

These aspects are definitive features of Threaded Solidity and differentiate it from standard Solidity.

Cells, slices and builders in TVM

As mentioned above, TVM works with a special data structure, a tree of cells. A cell includes an array of up to 1023 bits of information or up to four references to neighboring cells (256 bits each). Thanks to links to neighboring cells, a tree structure is built. Part of a cell with a certain “segment” of data and part of the links of a single cell are called cell slices or simply slices. This is an independent data structure with its own methods in Threaded Solidity.

An entire cell can be compared with another cell (comparison operators = and != -> bool), and the depth of child cells can be found out (the number of cells that refer to a given cell directly and indirectly) — <TvmCell>.depth().

The empty cell constructor — TvmCell() — can also be used to find out whether a particular cell is empty:

if (cell == TvmCell()) {

The following is a method that allows you to calculate the amount of data, the number of child cells and the number of links for a single cell:

<TvmCell>.dataSize(uint n)

Sending less than the number of cells returned will cause a cell overflow exception to be thrown.

To avoid an exception, you can use the <TvmCell>.dataSizeQ(uint n) method. When using this method, an empty value will be returned instead of an exception:

<TvmCell>.dataSizeQ(uint n) -> (optional(uint /*cells*/, uint /*bits*/, uint /*refs*/))

As you can see, full cells do not have enough methods to fully work with the data they contain. With Threaded Solidity, working with data mainly involves using the slice type.

You can convert a cell into a slice using the following cell method:

<TvmCell>.toSlice()

Slices can be fully compared with each other via the following operators: <=, <, ==, !=, >=, >. In addition, slices can be converted into bytes.

Slices have their own methods for checking the presence of data in them, including a reference counter, a bit weight counter and the dataSize() and dataSizeQ() methods, just like complete cells.

To obtain data from other slices, function arguments and static variables, including those of other contracts, the following methods are used:

<TvmSlice>.load{...}(), <TvmSlice>.preLoad{...}()
<TvmSlice>.load{...}Q(), <TvmSlice>.preLoad{...}Q

Unlike with load(), preLoad(), here, slices are not changed.

Here is an example of loading static variables from the data field of contract A:

contract A {
	uint a = 111;
	uint b = 22;
	uint c = 3;
	uint d = 44;
	address e = address(12);
	address f;
}

contract B {
	function f(TvmCell data) public pure {
		TvmSlice s = data.toSlice();
		(uint256 pubkey, uint64 timestamp, bool flag,
			uint a, uint b, uint c, uint d, address e, address f) = s.loadStateVars(A);
			
		// pubkey - public key of contract A
		// timestamp - replay protection
                // flag - always equals true
		// a == 111
		// b == 22
		// c == 3
		// d == 44
		// e == address(12)
		// f == address(0)
		// s.empty()
	}
}

Another type of cell is the TVM cell builder, a “partial” cell builder that takes data from the top of the stack and quickly serializes it, creating a new cell or slice. The builder has unique methods for calculating the remaining space for loading data and the available slots for adding links:

<TvmBuilder>.remBits() -> (uint16);
<TvmBuilder>.remRefs() -> (uint8);
<TvmBuilder>.remBitsAndRefs() -> (uint16 /*bits*/, uint8 /*refs*/);

The builder type is used to work with previously unknown data. Knowing the message structure with which smart contracts interact, we can write a contract that will send a message to another contract and call its logic.

The TVM namespace

Smart contract codes must interact with the virtual machine. The compiler API has functions for working with TVM, which, in essence, are wrappers for instructions sent to the virtual machine.

tvm.accept() — This instruction sets the gas limit to the maximum value. This is necessary to process external messages, which may not have an EVER amount attached to cover the TVM commission. We will cover message types in more detail in the messages section.

tvm.commit() — creates a snapshot of static variables (copying variables from register c7 to register c4) and register c5, to which the state of the smart contract is rolled back in case of exceptions being thrown during code execution. The TVM operation phase in this case is still considered to be successfully completed. If no exceptions occur during the execution of the smart contract code, the instruction will have no effect on the transaction.

Registers of Threaded Virtual Machine

Registers (control registers) are spots in the memory of the virtual machine allocated for storing certain immutable data structures that require constant access to perform functions. Storing this data on the stack would require multiple operations to move it, which has a negative impact on performance. TVM supports the use of 16 registers, but in practice registers 0-5 and 7 are used.

We touched on operations with registers c4, c5 and c7:

  • c4 stores a cell tied to the root of the smart contract cell tree, which stores contract data written to the network state;
  • c5 stores a cell with data about actions performed after the smart contract code is executed. If the result of executing the contract code is to send a message, the message data is written to the cell stored in register c5;
  • c7 stores temporary data. The data is stored in a tuple, which is initialized by accessing an empty tuple and deleted after the execution of the smart contract code is completed.

tvm.rawCommit() — this is the same as tvm.commit(), however, this does not copy static variables from register c7 to register c4, which may result in data loss when using this function after tvm.accept() while external messages are being processed.

tvm.setData(TvmCell data) — stores the cell data in register c4. This can be used in combination with tvm.rawCommit():

TvmCell data = ...;
tvm.setData(data); // stores the cell in register c4
tvm.rawCommit();   // stores registers c4 and c5
revert(200);       // calls an exception for a completed transaction

tvm.getData() -> (TvmCell) — returns data from register c4. May be useful for upgrading a contract.

Authorization of external messages 

Some smart contract functions can only be called by certain actors. To validate the caller, the smart contract can check the public key used to sign the external message or the address of the smart contract that sent the internal message. The address of a smart contract is a hash of its code and static variables, which is enough to validate the calling actor.

tvm.pubkey() -> (uint256) — returns the public key from the data field of the smart contract. If the key is not provided, it will return 0.

tvm.setPubkey(uint256 newPubkey) — writes the public key into the data field of the smart contract.

tvm.checkSign() — calculates whether the data sent as an argument was signed by the public key and then returns a boolean value.

tvm.insertPubkey() — inserts the public key passed as an argument into the data field of a cell with stateInit.

tvm.initCodeHash() — returns a hash of the smart contract source code that is current at the time of its deployment to the network. Can be used, for example, to calculate the TIP-3 token code when checking which token a user is trying to send.

The problem with calculating commissions in advance with asynchronous blockchains 

Since we cannot know for sure whether a transaction will succeed or fail (and on the Everscale or Venom network, transactions may be divided into many individual transactions), we cannot determine exactly how much gas should be applied to the initial message. The Threaded Virtual Machine has safeguards in place that limit the amount of gas spent to complete a transaction as defense mechanisms against malicious exploits and for the convenience of developers.

tvm.setGasLimit(uint g) — sets the gas limit for processing one transaction. This function allows you to limit the amount of money spent on paying for TVM, which can be useful if we want to protect against spam with external messages from a stolen account. If the function parameter exceeds the maximum gas limit value specified in the network configuration, the function will work in the same way as tvm.accept(), since the gas limit is set according to the following formula: min(g,gmax). 

tvm.buyGas(uint value) — this instruction calculates the amount of gas that can be purchased for the applied amount of nano EVERs. Then it works similarly to tvm.setGasLimit().

tvm.rawReserve(uint value, uint8 flag) — this function can be used to reserve a specified amount of nano EVERs and send it to yourself. It may be useful to limit gas consumption on subsequent outgoing calls.

Before presenting the flag options, it should be noted that the original balance of the contract (original_balance) is the balance of the contract before the execution of the transaction on TVM with the storageFee already taken into account. The remaining balance is the amount left on the contract balance after the TVM work is completed along with part of the outgoing calls (the remaining balance after one outgoing call is greater than the remaining balance after three outgoing calls. In total, up to 255 outgoing calls can be made, that is, execution of the code of a separate smart contract can result in the sending of a maximum of 255 outgoing messages).

The flags are as follows:

  • 0 -> reserve = value nEVER.
  • 1 -> reserve = remaining_balance – value nEVER.
  • 2 -> reserve = min(value, remaining_balance) nEVER.
  • 3 = 2 + 1 -> reserve = remaining_balance – min(value, remaining_balance) nEVER.
  • 4 -> reserve = original_balance + value nEVER.
  • 5 = 4 + 1 -> reserve = remaining_balance – (original_balance + value) nEVER.
  • 6 = 4 + 2 -> reserve = min(original_balance + value, remaining_balance) = remaining_balance nEVER.
  • 7 = 4 + 2 + 1 -> reserve = remaining_balance – min(original_balance + value, remaining_balance) nEVER.
  • 12 = 8 + 4 -> reserve = original_balance – value nEVER.
  • 13 = 8 + 4 + 1 -> reserve = remaining_balance – (original_balance – value) nEVER.
  • 14 = 8 + 4 + 2 -> reserve = min(original_balance – value, remaining_balance) nEVER.
  • 15 = 8 + 4 + 2 + 1 -> reserve = remaining_balance – min(original_balance – value, remaining_balance) nEVER

All other flags are invalid.

Updating smart contract code on Threaded Solidity with two functions 

Updating the code of smart contracts for TVM-compatible blockchains is very similar to updating the code of any other software: you download a new version of the code and deploy it. With Threaded Solidity, you have to send a new version of the code in a message and then save it in the smart contract code.

tvm.setcode(TvmCell newCode) — this updates the smart contract code to the version contained in the sent cell. The changes will take effect after the current session of smart contract code execution ends.

tvm.setCurrentCode(TvmCell newCode) — this changes the smart contract code for the current TVM session. After the smart contract code is executed, the changes will no longer be valid and the next transaction will be executed according to the original version of the code.

The code can be updated within a current transaction and saved by sending the code in a message with an alternate call: tvm.setCurrentCode() and tvm.setcode(). The current transaction will work according to the new version of the code thanks to tvm.setCurrentCode(), and tvm.setcode() will rewrite the smart contract code for subsequent transactions.

Global configurations

Global configurations for the blockchain are stored in the masterchain (which will be described later in the article). Using global configs, some network operation parameters are set, for example, the number of validators, commission settings, etc.

tvm.configParam(uint8 paramNumber) and tvm.rawConfigParam(uint8 paramNumber) — return the parameters from the global configurations.

The return structures of these functions are different: configParam() returns the typed values, configParamRaw() returns the cell and a boolean value. Indexes of global parameters are passed as function arguments (paramNumber): 1, 15, 17 34.

Methods for deploying smart contracts

To deploy a smart contract, you need to calculate its address, which is a hash of the initial state (stateInit), which includes the smart contract code and its static variables.

tvm.buildStateInit() — generates the contract’s initial state (stateInit) from code and cell data.

tvm.buildDataInit() — generates the data field of the initial state of the smart contract.

tvm.stateInitHash() — returns a hash of the initial state of the smart contract.

Additional functions of the TVM namespace

The Threaded Virtual Machine compiler API has methods for calculating random values and performing algebraic operations. You can find them in the compiler API documentation.

Below we will get into several more useful methods that can be used to write TVM smart contracts.

tvm.hash() — returns a 256-bit hash of the data passed as the argument. If the byte data or string type is passed as an argument, it is not the data itself that is hashed, but the tree of cells that contains this data.

tvm.code() -> (TvmCell) — returns the code contract.

tvm.codeSalt(TvmCell code) -> (optional(TvmCell) optSalt) — if the code uses “salt”, then the returned optSalt cell will contain its values, otherwise it will return an empty value.

Salt denotes additional data used to obtain a new hash without making changes to the structure of the source code.

tvm.setCodeSalt(TvmCell code, TvmCell salt) -> (TvmCell newCode) — adds salt to the code, sent as a cell, and returns the new code.

tvm.resetStorage() — returns the values of static variables to their default values.

tvm.functionId() — returns an unsigned 32-bit number that is the number of a public or external function or smart contract constructor. This is used to describe the logic of smart contract behavior when receiving a bounce message, among other things. We will deal with the details of processing bounce messages in the corresponding section.

tvm.log() is similar to the print() function. Lines of code that are longer than 127 symbols are shortened.

tvm.log(string log);
logtvm(string log) // an alternative form of the function that performs in the same way.

tvm.hexdump() and tvm.bindump() — output to the console cell or integer data in binary or hexadecimal formats.

tvm.exit() and tvm.exit1() — are functions that save static variables and terminate the execution of the smart contract with code 0 and 1 respectively.

TVM messaging functionality

tvm.buildIntMsg() — generates an outgoing internal message that should cause another function to be executed on the receiving contract. This also returns a cell that can be used as a function argument with tvm.sendrawmsg().

tvm.sendrawmsg(TvmCell msg, uint8 flag) — sends an external/internal message with a passed flag. An internal message can be generated using the function tvm.buildIntMsg() above. Other potential flag variants are contained within the address.transfer() function. When sending a message, you have to be sure that the argument has the correct format.

tvm.encodeBody(function, arg0, arg1, arg2, ...) -> (TvmCell) — this codes the body of the message, which then can be used as an argument of the address.transfer() function. If the function is responsible, a callback function also has to be sent.

TVM smart contracts communicate via messages

All operations on TVM-compatible networks are initiated by smart contracts receiving incoming messages.

Messages are:

  • External and internal:
    • External messages are sent from outside the Everscale network or intended for recipients outside the network;
    • Internal messages are sent and received by smart contracts within the network;
    • You can also define external-internal messages as messages sent from one workchain to another.
  • Incoming and outgoing.

Messages have their own specific format called message-X. When calling some functions, a failure to comply with this format may cause exceptions.

The message namespace (msg) has the following functions:

msg.sender (address) — returns the sender address of an internal message, or 0 in the case of an external message or tick/tock transaction.

Tick/Tock transactions

Tick/Tock transactions are service transactions sent to synchronize and verify the operation of some smart contracts.

msg.value (uint128) — returns the amount attached to the internal message in nanoEVERs (nEVER). If there is an external message, this function returns 0; if there is a tick/tock transaction, the function returns undefined.

msg.pubkey() -> (uint256) — returns the public key with which the external message was signed. If you are dealing with an internal message, it will return 0; it will also return 0 if the external message was not signed (the external message calls a smart contract function that does not require actor validation). This is an important method, by which an actor requesting the execution of a smart contract code can be validated.

msg.createdAt (uint32) — returns the creation time of an external message.

msg.data (TvmCell) — returns all the data included in a message (the header and body).

msg.body (TvmSlice) — returns the body of a message.

msg.hasStateInit (bool) — returns the boolean value of a message, depending on whether the message contains a stateInit field.

msg.forwardFee (varUint16) — returns the forwardFee value charged for sending an internal message.

forwardFee

Depending on the type of message, this commission can be broken down into separate parts, which go to validators as a reward for processing messages and are included in the total action fee calculations.

Total action fee is the total commission calculated from all sent messages (smart contract actions initiated as a result of a given transaction), and is not charged if the execution of the smart contract code did not result in the sending of a single message.

msg.importFee (varUint16) — returns the value of the importFee charged for processing external messages. This most likely does not reflect the real value of a message because it is set by a user outside the network.

The externalMsg and internalMsg modifiers: these modifiers determine what types of messages can call a defined function. The absence of a modifier allows a function to be called by both external and internal messages.

Addresses on TVM blockchains

There are several address types that TVM networks work with: 

  • addr_none — an address that has not been returned to the network; 
  • addr_extern — an address that is not on the Everscale network;
  • addr_std — a standard address;
  • addr_var — any address.

The address (address_value) constructor creates a standard address in workchain 0. TVM blockchains are heterogeneous, meaning that one masterchain (workchain -1) can unite up to 232 workchains, each of which can be flexibly configured. In simple terms, this means that the network operates as if second-layer solutions existed directly inside it, instead of on top of it as they do with Ethereum.

Creation of a “special” address

The creation of an address in a specific workchain / a necessary workchain:

  • address addrStd = address.makeAddrStd(wid, address);
  • address addrNone = address.makeAddrNone();
  • address addrExtern = address.makeAddrExtern(addrNumber, bitCnt);

There are also some utility functions:

  • <address>.wid — returns the number of the workchain in which the address is deployed. If the address does not belong to the addr_std or addr_var types, a range check error exception is thrown.
  • <address>.value — returns the 256-bit address value for the addr_std and addr_var types (if addr_var has a 256-bit value). Otherwise, a range check error exception is thrown.
  • <address>.balance — returns the balance to its corresponding address.
  • <address>.currencies — returns assets to their corresponding address.

Transfers and flags

In order to send a message to a needed address, the following function is required:

<address>.transfer(
uint128 value, 
bool bounce, 
uint16 flag, 
TvmCell body,
TvmCell stateInit
);

All parameters can be left blank except for value.

Parameters:

  • value — the amount of the native currency attached to the message. This will be used to pay network fees. Since the code execution is asynchronous, users cannot calculate the exact amount of gas required to complete the operation.
  • bounce — if a flag is set to “true” and the transaction ends early with an error, the sent funds must return to the balance of the sender. Setting a flag as “false” will result in funds being delivered to the selected address, even if it does not exist or is frozen. A user may need to use the bounce = false  function to deploy a new contract while sending funds to the address at the same time. 
  • flag — default value 0. This means that the message has as many funds attached as specified in the value parameter. Other flag parameter values:
    • 128 — a message will be accompanied by the entire contract balance, which will become equal to 0 after the message is delivered;
    • 64 — a message has as many EVERs attached as specified in the value parameter and the entire amount received from the incoming message (which initiated the execution of the smart contract code);
    • flag + 1 (for example, 64 + 1) — the sender wants to pay fees separately from the address balance;
    • flag + 2 — all errors that occur during the action phase must be ignored (the action phase is when the network state is updated after calculations are carried out on TVM);
    • flag + 32 — removes an address from the network if its balance is zero. A message with a 128 + 32 flag has sent the entire balance of the corresponding smart contract and will be removed from the network.
  • body — the body of the message. This is the default kind of TVM cell;
  • currencies — additional currencies attached to the message. This is used to send TIP3 tokens.
  • stateInit — the init field of a standard message. If stateInit is sent in an incorrect format, a cell underflow exception is thrown. Normally, stateInit is sent when a contract is deployed or unfrozen. 

Considering that all activity on the Everscale blockchain is communication between smart contracts via messages, the transfer() function is used often. Needless to say, with the transfer function, users can even deploy new smart contracts to the network. This corresponds to the actor model, where an actor can communicate with other actors through messages and create new actors.

Blocks and transactions

On the Everscale and Venom networks, blocks can be either master blocks (masterchain blocks) or workchain blocks. The hashes of all workchain blocks finalized during the allotted time for assembling the master block are recorded in master blocks.

Workchain blocks contain information about messages and transactions from all smart contracts contained in workchain threads.

By definition, a TVM transaction is a record of the execution of smart contract code.

The API compiler has methods that allow users to get the block/transaction time and the logical block/transaction time.

block.timestamp -> (uint32) — returns the time in the UNIX format for the current block. All transactions in the block are assigned this time value.

block.logicaltime -> (uint64) — returns the starting point in logical time for the current block, i.e., the starting point of the block’s logical time.

tx.logicaltime -> (uint64) — returns the logical time of the current transaction.

tx.storageFee -> (uint120) — returns the value of the storageFee paid during the transaction.

If the time in UNIX format is straightforward, then logical time (LT) needs to be considered in more detail. It has nothing to do with time in the usual sense. Logical time entails a strict order in which messages are delivered from one smart contract to another.

The logical time of a new master block is always 1,000,000 more than the time of the previous master block’s logical time. Thus, all entities dependent on the master block will have a lower logical time than the master block itself.

The logical time of the new thread block is equal to the logical time of the last master block + 1,000,000. So, at the starting point, the logical time of a future thread block is equal to the logical time of the future master block (the thread block references the previous master block and adds 1,000,000 to its logical time, while a future master block automatically adds 1,000,000 to the time of the previous master block).

The logical transaction time is as follows: 

tx.LT = max(block.LT, msgInbound.LT, prevTx.LT) + 1

The logical time of a message is as follows:

msg.LT = max(tx.LT, txPrevMsg.LT) + 1

If a smart contract sends two messages to one addressee, they will be delivered and executed strictly in the order they were sent.

Notable specifics:

  • Two messages from different smart contracts to different recipients can have the same logical time.
  • A message can be delivered in the same block when a transaction creates a message whose recipient is in the same thread. The message queue should be empty at this point.
  • A strict order of receiving messages is not guaranteed if, let’s say, contract A sends two messages to contract B and contract C, after executing a code that has both contracts sending a message to contract D. If contracts B and C are in different threads, then the time of sending messages will depend on how busy the thread is. This feature of asynchronous architecture should be kept in mind when writing smart contracts. And, although this may seem like a serious problem, writing smart contracts that are as independent as possible, with logic that does not depend on each other, will help prevent negative consequences. The risk of receiving errors is more than compensated by the fact the asynchronous architecture of TVM networks allows for high throughput, or a high degree of scalability, without compromising the decentralization and security of the blockchain.

Abstract Binary Interface 

The pragma keyword is also used to explain to the compiler which headers will be included in the .abi file of the smart contract. ABI is an acronym for Abstract Binary Interface, which is necessary to build the correct algorithm for converting data from a message into a tree of cells for further TVM operations. Headers are required to adequately process external messages (messages sent from outside the Everscale or Venom network).

The following headers can be included in ABI files: 

  • timestamp — the time it takes to send an external message. This is necessary for replay protection;
  • expire — the lifetime of a message, which by default is equal to the timestamp + 2 minutes;
  • signature — this signs a message with a public key. External messages can be signed with a public key in order to call functions on smart contracts that require user validation (internal messages are not signed since they are sent by smart contracts. Smart contracts validate each other using the address, which is essentially a hash of the smart contract code and static variables). The smart contract code must explicitly indicate that the function can be called by receiving an external message, and if the smart contract ABI file contains this header, then all external messages will be checked for their signatures.

This is what the body of a message looks like inside an ABI file:

{
  "name": "set",
  "inputs": [{"name":"_value","type":"uint256"}],
  "outputs": []
},

Using the ABI namespace, we can serialize data into a cell data type and deserialize bit data from cells. This is convenient for describing the logic of transmitting a received message. 

abi.encode(TypeA a, TypeB b, ...) -> (TvmCell /*cell*/) – this codes the different types and returns a cell.

abi.decode(TvmCell cell, (TypeA, TypeB, ...)) -> (TypeA /*a*/, TypeB /*b*/, ...) — this decodes the cell and returns the different types. Not all types can be passed as arguments. If an incorrect type is passed, the function will throw an exception.

For deploys and upgrades

In the tvm functions section, we already touched on the process of upgrading and deploying smart contracts on Threaded Solidity. Let’s take a closer look at it.

The contract address is calculated by hashing the stateInit. To make sure that the address of the new smart contract is unique, you need to sign the external message used to deploy the contract. If it is an internal message (a new contract deployed through an existing smart contract), it is necessary to pass the address of the calling smart contract (the address of the owner of the new smart contract) to the constructor.
If you need to deploy several smart contracts with the same code, then it is worth including an additional static variable in stateInit in the form of, for example, different numbers so that the resulting hash addresses differ from each other.

The new, stateInit and code keywords

The stateInit and code keywords must be used when deploying a smart contract to the network using a function with the new keyword.

You can write a smart contract with all static variables, then the code and all the data of this contract will make up the stateInit in the form of a tree of cells.

stateInit — determines the initial state of the new contract.

code — defines the code of the new smart contract.

// file SimpleWallet.tsol
...
contract SimpleWallet {
    address static m_owner;
    uint static m_value;
    ...
}

// contract file, of the SimpleWallet deploying contract on the
TvmCell network code = ...;
address newWallet = new SimpleWallet{
    value: 1 ever,
    code: code,
    pubkey: 0xe8b1d839abe27b2abb9d4a2943a9143a9c7e2ae06799bd24dec1d7a8891ae5dd,
    splitDepth: 15,
    varInit: {m_owner: address(this), m_value: 15}
}(arg0, arg1, ...);

Note the variables that are passed to new:

  • value — the number of EVER that will be attached to the deploy message;
  • currencies — additional currencies (tokens) that will be attached to the deploy message;
  • bounce — by default this is set to true and funds sent with the deploy message, if an exception occurs during the TVM operation phase, will be returned to the balance of the contract that sent the deploy message. In order for the funds to remain on the new smart contract, you must pass the bounce = false flag;
  • wid — the number of the workchain in which the new address should be deployed; the default is 0. Today, only one workchain, number 0, is deployed on the Everscale network;
  • flag — by default 0. You can find more info on initial message flags in the <address>.transfer() section.

A contract can be deployed on the network with the help of the <address>.transfer() function. To do this, you simply need to send the stateInit of the new contract to the function parameters.

Updating the smart contract code

Unlike EVM networks, where smart contracts are updated through proxy contracts and address reassignment, the owner of a contract in a TVM-compatible network can send a new version of the code in a message with a small description of the logic for applying the update.

Updating the smart contract of a TIP-3 token
 // If the owner knows that a new version of the code is in the root contract, 
  // They can request an upgrade.
  function upgrade(address remainingGasTo) override external onlyOwner {
    ITokenRootUpgradeable(root_).requestUpgradeWallet{ value: 0, flag: TokenMsgFlag.REMAINING_GAS, bounce: false }(
      version_,
      owner_
    );
  }

  // They will then receive the new version of the code 
  function acceptUpgrade(TvmCell newCode, uint32 newVersion) override external onlyRoot {
    if (version_ != newVersion) {
      // The entire state of the contract is encoded in TvmCell + the new version
      TvmCell state = abi.encode(root_, owner_, balance_, version_, newVersion);
      // A new code for the contract is set for the next transaction
      tvm.setcode(newCode);
      // The new code is then included in the transaction
      tvm.setCurrentCode(newCode);
      // the onCodeUpgrade function of the new code is called.
      onCodeUpgrade(state);
    }
  }
}

// The onCodeUpgrade function will be in the new version of the contract, only after the setCurrentCode function
// calls it.

function onCodeUpgrade(TvmCell data) private {
  // The contract storage is set to 0, because if we 
  // change anything, the storage structure itself would
  // change. This call does not affect the variations of
  // the _pubkey, _replayTs, _constructorFlag, 
  // instead it sets the remaining variables c7
  // to 0.
  tvm.resetStorage();
  
  // the state is decoded 
  (address root, address owner, uint128 balance, uint32 fromVersion, uint32 newVersion) =
        abi.decode(data, (address, address, uint128, uint32, uint32));

  // the state is initiated
  root_ = root;
  owner_ = owner;
  balance_ = balance;
  version_ = newVersion;
}

tvm.setcode(TvmCell newCode) — updates the smart contract code to the version contained in the sent cell. The changes will take effect after the current smart contract code completes execution.

Working with fallback() and onBounce()

Since interactions between smart contracts written in Threaded Solidity occur asynchronously and some of the data can be determined during the execution of a particular operation, it is impossible to determine in advance whether a particular transaction will be completed successfully. For an asynchronous blockchain to operate as it should, mechanisms similar to exception handling are required, but at the level of smart contracts active in the network.

The fallback() method defines the behavior of the contract when receiving an invalid message:

  • In the message the id function, which does not exist in the smart contract code, is called;
  • Message length in bits ranges from 1 to 31;
  • Message length is 0 bits, but links are included in the message.

The onBounce() method defines the behavior of the contract when receiving a bounce message, generated by the network if the bounce = true flag was set in the outgoing message, which was described in the <address>.transfer() section. Bounce messages are also generated by the network if:

  • the receiving address of a smart contract message is not deployed on the network;
  • the smart contract called cannot execute its code.

A Bounce message can only be sent if the amount of EVER attached to the original message is enough to cover the network fee.

tvm.functionId() — returns an unsigned 32-bit number that is the number of a public or external function or smart contract constructor. This is used, for example, to describe the logic of smart contract behavior when receiving a bounce message:

onBounce(TvmSlice slice) external {
		// Increasing the counter of received bounce messages.
		bounceCounter++;

		// Decoding the message. The first 32 bits store the
               function id.

		uint32 functionId = slice.load(uint32);

		// tvm.functionId() calculates the id from function name.
		if (functionId == tvm.functionId(
                   AnotherContract.receiveMoney
                   )
              ) {
		// loadFunctionParams() loads function parameters
               from a slice.
		// After decoding, this saves the function parameters
               to static variables.
			invalidMoneyAmount = slice.loadFunctionParams(AnotherContract.receiveMoney);
		} else if (functionId == tvm.functionId(AnotherContract.receiveValues)) {
			(invalidValue1, invalidValue2, invalidValue3) = slice.loadFunctionParams(AnotherContract.receiveValues);
		}
	}

Other Threaded Solidity capabilities

Try-catch

Exception handling using try-catch in smart contract code, unlike Solidity, works both with calls by external functions and when new contracts are created.

Synchronized (as opposed to asynchronous) calls

Part of the function code should be executed after receiving a response from the called smart contract using the .await suffix:

contract Caller {
    function call(address addr) public pure {
        ... // the code will be executed up to the following line:
        uint res = IContract(addr).getNum(123).await; 
            // will start executing after receiving a response
                from the called contract
        require(res == 124, 101);
        ...
    }

There is a special responsible modifier for this action. Executing the responsible function always results in sending an internal message. Throughout the article, we have noted that some methods behave differently when applied to different functions with and without responsible modifiers.

selfdestruct(address dest_addr) — this sends all funds from the balance of the smart contract to the transferred address and deletes this account.

New protocol — old language 

Despite the fact that detailed the many distinguishing features of the compiler API and the Everscale protocol itself, we still believe that writing smart contract code in Threaded Solidity today is convenient and understandable for developers that are used to standard Solidity.

If you are inspired by the capabilities of modern TVM-compatible blockchains or would like to take a closer look at the code of smart contracts written to be executed in an asynchronous paradigm in accordance with the actor model, you should check out our open repositories with examples of smart contracts to familiarize yourself with various use cases. You should also head to our GitHub, where you will find real solutions (FlatQube, Octus Bridge, Gravix) that have been operating on the mainnet for several years.

Related

Web3 era challenges for blockchain technology and possible solutions
Popular blockchains fail to meet the requirements set by the new web standard. We know how to tackle this issue.
Sep 22
Locklift upgrade: Improved smart contract testing and performance
A new version supports a new transport and makes debugging more convenient.
Sep 14