
TON Project Development Tutorial (1): How to Create an NFT on TON Chain from the Source Code Perspective?
TechFlow Selected TechFlow Selected

TON Project Development Tutorial (1): How to Create an NFT on TON Chain from the Source Code Perspective?
Developing DApps in the TON ecosystem is truly an interesting endeavor.
Author: @Web3Mario
Abstract: Following up on the previous article introducing TON technology, I've spent some time diving deep into the official TON developer documentation. I found that there's still a learning curve—the current documentation feels more like an internal development guide and isn't very beginner-friendly for new developers. Therefore, I'm trying to organize a series of articles based on my own learning journey to help others quickly get started with TON DApp development. If you spot any errors in this article, please feel free to point them out. Let's learn together.
Differences Between Developing NFTs on EVM vs. TON Chain
Issuing an FT or NFT is typically one of the most basic requirements for DApp developers. So naturally, I used this as my entry point into learning. First, let’s explore the differences between developing an NFT on an EVM-based stack versus on TON Chain. On EVM platforms, NFTs usually follow the ERC-721 standard. An NFT refers to a non-fungible type of cryptographic asset, where each asset is unique—possessing certain distinct characteristics. The ERC-721 standard defines a common development paradigm for such assets. Let’s examine what functions and data a typical ERC-721 contract must implement. Below is an interface for ERC-721. Unlike fungible tokens (FTs), the transfer function requires specifying a tokenId instead of an amount. This tokenId is the fundamental representation of an NFT’s uniqueness. To support richer attributes, metadata is often associated with each tokenId—a link pointing to external data containing additional information about the NFT, such as a PFP image URL or specific trait names.

For developers familiar with Solidity or object-oriented programming, implementing such a smart contract is straightforward—simply define required data types, including key mappings, and write logic to modify these data structures according to desired functionality.
However, things work differently on TON Chain, primarily due to two core reasons:
l Data storage on TON is implemented using Cells, and Cells within the same account are organized via a directed acyclic graph (DAG). As a result, persistently stored data cannot grow indefinitely because query costs increase with DAG depth. Excessive depth may lead to prohibitively high lookup costs, potentially causing contract deadlocks.
l To achieve high concurrency performance, TON abandoned serial execution architecture in favor of an Actor model—a development paradigm specifically designed for parallelism—to reconstruct its execution environment. This leads to a significant implication: smart contracts can only interact asynchronously by sending internal messages. Note that both state-modifying and read-only calls must adhere to this rule. Additionally, careful consideration must be given to how to handle data rollback when asynchronous calls fail.
Other technical differences were discussed in detail in the previous article, so we won’t go into them here. Instead, this article focuses specifically on smart contract development. These two design principles create major distinctions between TON and EVM smart contract development. Earlier, we noted that an NFT contract needs mappings to store relevant data—for example, a mapping called "owners" that links tokenIDs to owner addresses, thereby determining ownership. Transfers involve modifying this ownership. However, since such mappings could theoretically grow without bounds, they should be avoided. Thus, the official recommendation is to use bounded vs. unbounded data structures as a criterion for sharding. When facing similar storage needs, adopt a master-subordinate contract pattern, creating sub-contracts to manage data for individual keys, while the master contract manages global parameters or facilitates internal message exchanges between sub-contracts.
This means that NFTs on TON should also follow a similar architectural approach: each NFT becomes an independent sub-contract storing private data such as owner address and metadata, while a master contract maintains global data like NFT name, symbol, and total supply.
With the architecture clarified, the next step is addressing functional requirements. Given the master-subordinate structure,
we need to determine which functions belong to the master contract and which to the sub-contracts, how they communicate internally, and how to roll back data if execution fails. Typically, before developing complex large-scale projects, it's essential to draw class diagrams, clarify information flows, and carefully consider rollback logic upon internal call failures. Although this NFT example is simple, similar verification practices still apply.

Learning TON Smart Contract Development from Source Code
TON chose to design Func—a C-like, statically-typed language—as the smart contract programming language. Now, let’s dive into actual source code to learn how to develop TON smart contracts. I’ll walk through an official TON NFT example, available for interested readers to explore. This case implements a simple TON NFT. Let’s look at the contract structure, which consists of two functional contracts and three essential libraries.

These two main functional contracts are designed following the principles outlined above. Let’s first examine the master contract, nft-collection:

Here comes our first key concept: how to persistently store data in TON smart contracts. In Solidity, EVM automatically handles persistent storage based on variable types—state variables are automatically saved after execution with their latest values, requiring no explicit handling from developers. But in Func, developers must manually implement persistence logic—similar to managing memory manually in C/C++, whereas modern languages often automate garbage collection. Looking at the code, we first import necessary libraries. Then we see the function load_data, responsible for reading persisted data. It starts by calling get_data to retrieve the persisted contract storage cell—an implementation provided by the standard library stdlib.fc, whose functions can generally be treated as system-level functions.
The return type is “cell,” a TVM-native type. As previously introduced, all persistent data on TON Blockchain is stored in cell trees. Each cell holds up to 1023 bits of arbitrary data and up to four references to other cells. In the stack-based TVM, cells serve as memory units. Data inside cells is tightly encoded; to extract readable data, cells must be converted into a type called “slice.” A cell can be transformed into a slice using begin_parse. From the slice, we can then extract bit data and references to other cells. Note the syntax sugar in line 15: it directly calls the second function on the return value of the first function. Finally, data is loaded sequentially according to the predefined persistence order. Unlike Solidity, this process does not rely on hashmaps, so the loading sequence must strictly match the defined order.
In save_data, the logic is reversed, introducing another key concept: a new type called “builder,” which acts as a cell constructor. Data bits and cell references can be stored in a builder, which can later be finalized into a new cell. We start by calling the standard function begin_cell to create a builder, then use store-related functions to add data—ensuring the storage order matches the loading order mentioned earlier. After populating the builder, end_cell finalizes it into a new cell residing in memory. Finally, set_data persists this cell to storage.

Next, let’s move to business logic functions. Before that, we need to understand how to create a new contract from within a contract—an operation frequently used in the master-subordinate architecture described earlier. On TON, inter-contract communication occurs via internal messages, achieved through a function named send_raw_message. The first parameter is the message-encoded cell, and the second is a flag indicating execution mode. TON supports different modes and flags for sending internal messages: currently 3 Modes and 3 Flags. A single Mode can be combined with multiple (or no) flags by summing their numeric values.The table below describes available Modes and Flags:

Now let’s look at the first major function: deploy_nft_item. As the name suggests, this function creates—or mints—a new NFT instance. After encoding a message, it sends the internal contract via send_raw_message, using flag 1, meaning only the fee specified in the message will be charged as gas for this transaction. Based on prior explanations, we can infer this encoding scheme corresponds to creating a new smart contract.
Let’s dive into how it works.
Go directly to line 51. The two preceding functions are helpers for generating message data, which we'll revisit later. This section shows the encoding process for an internal message that creates a smart contract. The numbers in the middle are actually flags defining the message’s purpose. Here comes the next concept: TON uses a binary language called TL-B to describe message execution formats, setting different flags to enable specific features. Two obvious use cases are contract creation and function calls on deployed contracts. Line 51 represents the former—creating a new nft-item contract—mainly defined in lines 55–57. The long number string in line 55 consists of several flags. Recall that store_uint takes two arguments: value and bit length. The last three bits determine whether this message creates a contract, with binary value 111 (decimal 4+2+1). The first two indicate that the message carries StateInit data—the new contract’s code and initialization data. The third indicates payload inclusion: i.e., logic execution and input parameters. You’ll notice line 66 doesn’t set these three bits, indicating a function call to an already-deployed contract.See full encoding rules here.
The encoding of StateInit data corresponds to line 49, calculated via calculate_nft_item_state_init. Note that StateInit encoding follows a predefined TL-B format. Besides flags, it mainly includes two parts: the new contract’s code and initial data. The data encoding order must match the storage order defined in the target contract’s persistent cell. In line 36, we see the initialization data includes item_index (analogous to tokenId in ERC-721) and collection_address—the current contract’s address returned by the standard function my_address. This order aligns with declarations in the nft-item contract.
Another important concept: in TON, you can pre-calculate the future address of a contract before it’s created—similar to Solidity’s create2 function. In TON, a new contract address is formed by concatenating two components: the workchain ID and the hash of the StateInit data. The workchain ID, as mentioned earlier, relates to TON’s infinite sharding architecture and currently has a fixed value obtained via the standard function workchain. The latter component comes from cell_hash. Returning to our example, calculate_nft_item_address calculates the anticipated contract address, whose result is encoded in line 53 as the recipient address of the internal message. nft_content refers to the initialization call sent to the newly created contract; details will be covered in the next article.
As for send_royalty_params, this responds to a read-only request via internal message. Earlier, we emphasized that on TON, even read-only operations require internal messaging. This function handles such requests. Notably, line 67 sets a callback flag indicating the response function to invoke on the requester. Following that are the returned data: requested item index and corresponding royalty data.
Next comes another key concept: TON smart contracts have only two unified entry points—recv_internal
and recv_external. The former serves as the universal entry for all internal messages, the latter for external ones. Developers must internally route incoming messages using a switch-like mechanism based on operation flags—such as the callback flag seen in line 67. Back to our example: first, perform null-check on the message. If valid, parse sender_address in line 83—used later for permission checks. Note the ~ operator here, another piece of syntax sugar we won’t elaborate on now. Next, parse the op code (operation flag), then handle requests accordingly. Depending on logic, it invokes previously discussed functions—e.g., responding to royalty queries or minting a new NFT and incrementing the global index.
Line 108 introduces another concept. By the function name, you might guess its purpose. Similar to Solidity’s require(), Func uses throw_unless—a standard function that throws an exception if the second boolean argument is false, returning the first argument as error code. Here, equal_slices checks whether the parsed sender_address matches the owner_address stored in the contract’s persistent data, enforcing access control.

Finally, to improve code clarity, a series of helper functions are defined to retrieve persistent data. We won’t expand on them here, but developers can refer to this structure when building their own contracts.

Developing DApps in the TON ecosystem is truly fascinating, with significant differences from EVM paradigms. I’ll continue writing a series of articles to guide readers through TON Chain DApp development. Let’s learn together and seize this opportunity. Feel free to reach out to me on Twitter to exchange ideas or collaborate on exciting new DApp concepts.
Join TechFlow official community to stay tuned
Telegram:https://t.me/TechFlowDaily
X (Twitter):https://x.com/TechFlowPost
X (Twitter) EN:https://x.com/BlockFlow_News














