Building an NFT Contract from Scratch: An Introduction to FunC on the TON Blockchain

With the rise of digital art, Non-Fungible Tokens (NFTs) have become a hot topic in the blockchain space. This article will guide you through creating a simple NFT contract from scratch using the FunC language on the TON blockchain. By the end of this tutorial, you will have a grasp of the basic structure and key functionalities of an NFT contract, laying the foundation for your entry into the NFT realm.

Understanding TON Blockchain and FunC Language

TON (The Open Network) is a decentralized blockchain platform designed to provide high-speed, scalable, and secure transactions. FunC is a high-level programming language for the TON blockchain that simplifies the development of smart contracts, making it more intuitive to write contracts.

Overview of NFT Contracts

An NFT contract on the TON blockchain is a smart contract that manages non-fungible tokens. Typically, an NFT contract includes functions for creating NFTs, transferring ownership, updating content, and setting royalty parameters. We will walk you through how to write such a contract step by step.

Setting Up the Contract Storage Structure

To start, we need to define the storage structure of the contract. Here is the basic storage structure for an NFT contract:

  • owner_address: The address of the contract owner.
  • next_item_index: The index of the next NFT item.
  • collection_content: The common content of the collection.
  • nft_item_code: The code for NFT items.
  • royalty_params: The royalty parameters, including the royalty factor, royalty base, and royalty address.
    In FunC, we can define the storage structure using the following code:
default#_ royalty_factor:uint16 royalty_base:uint16 royalty_address:MsgAddress = RoyaltyParams;
storage#_ owner_address:MsgAddress next_item_index:uint64
          ^[collection_content:^Cell common_content:^Cell]
          nft_item_code:^Cell
          royalty_params:^RoyaltyParams
          = Storage;

Writing Contract Functions and Operations

Next, we will write the core functions and operations of the contract.

1. Loading and Saving Data

(slice, int, cell, cell, cell) load_data() inline {
  var ds = get_data().begin_parse();
  return
    (ds~load_msg_addr(), ;; owner_address
     ds~load_uint(64), ;; next_item_index
     ds~load_ref(), ;; content
     ds~load_ref(), ;; nft_item_code
     ds~load_ref()  ;; royalty_params
     );
}
() save_data(slice owner_address, int next_item_index, cell content, cell nft_item_code, cell royalty_params) impure inline {
  set_data(begin_cell()
    .store_slice(owner_address)
    .store_uint(next_item_index, 64)
    .store_ref(content)
    .store_ref(nft_item_code)
    .store_ref(royalty_params)
    .end_cell());
}

These two functions are used to load and save the contract’s data, respectively.

2. Calculating the State Init Cell and Address for NFT Items

cell calculate_nft_item_state_init(int item_index, cell nft_item_code) {
  cell data = begin_cell().store_uint(item_index, 64).store_slice(my_address()).end_cell();
  return begin_cell().store_uint(0, 2).store_dict(nft_item_code).store_dict(data).store_uint(0, 1).end_cell();
}

slice calculate_nft_item_address(int wc, cell state_init) {
  return begin_cell().store_uint(4, 3)
                     .store_int(wc, 8)
                     .store_uint(cell_hash(state_init), 256)
                     .end_cell()
                     .begin_parse();
}

These functions calculate the state init cell and address for NFT items.

3. Deploying NFT Items

() deploy_nft_item(int item_index, cell nft_item_code, int amount, cell nft_content) impure {
  cell state_init = calculate_nft_item_state_init(item_index, nft_item_code);
  slice nft_address = calculate_nft_item_address(workchain(), state_init);
  var msg = begin_cell()
            .store_uint(0x18, 6)
            .store_slice(nft_address)
            .store_coins(amount)
            .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
            .store_ref(state_init)
            .store_ref(nft_content);
  send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors
}

This function is used to deploy a new NFT item.

4. Sending Royalty Parameters

() send_royalty_params(slice to_address, int query_id, slice data) impure inline {
  var msg = begin_cell()
    .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool packages:MsgAddress -> 011000
    .store_slice(to_address)
    .store_coins(0)
    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    .store_uint(op::report_royalty_params(), 32)
    .store_uint(query_id, 64)
    .store_slice(data);
  send_raw_message(msg.end_cell(), 64); ;; carry all the remaining value of the inbound message
}

This function sends royalty parameters to the requester.

5. Handling Internal Messages

() recv_internal(cell in_msg_full, slice in_msg_body) impure {
  if (in_msg_body.slice_empty?()) { ;; ignore empty messages
        return ();
    }
    slice cs = in_msg_full.begin_parse();
    int flags = cs~load_uint(4);

    if (flags & 1) { ;; ignore all bounced messages
        return ();
    }
    slice sender_address = cs~load_msg_addr();

    int op = in_msg_body~load_uint(32);
    int query_id = in_msg_body~load_uint(64);

    var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_data();

    if (op == op::get_royalty_params()) {
        send_royalty_params(sender_address, query_id, royalty_params.begin_parse());
        return ();
    }

    throw_unless(401, equal_slices(sender_address, owner_address));

    if (op == 1) { ;; deploy new nft
      int item_index = in_msg_body~load_uint(64);
      throw_unless(402, item_index <= next_item_index);
      var is_last = item_index == next_item_index;
      deploy_nft_item(item_index, nft_item_code, in_msg_body~load_coins(), in_msg_body~load_ref());
      if (is_last) {
        next_item_index += 1;
        save_data(owner_address, next_item_index, content, nft_item_code, royalty_params);
      }
      return ();
    }
    if (op == 2) { ;; batch deploy of new nfts
      int counter = 0;
      cell deploy_list = in_msg_body~load_ref();
      do {
        var (item_index, item, f?) = deploy_list~udict::delete_get_min(64);
        if (f?) {
          counter += 1;
          if (counter >= 250) { ;; Limit due to limits of action list size
            throw(399);
          }

          throw_unless(403 + counter, item_index <= next_item_index);
          deploy_nft_item(item_index, nft_item_code, item~load_coins(), item~load_ref());
          if (item_index == next_item_index) {
            next_item_index += 1;
          }
        }
      } until ( ~ f?);
      save_data(owner_address, next_item_index, content, nft_item_code, royalty_params);
      return ();
    }
    if (op == 3) { ;; change owner
      slice new_owner = in_msg_body~load_msg_addr();
      save_data(new_owner, next_item_index, content, nft_item_code, royalty_params);
      return ();
    }
    if (op == 4) { ;; change content
      save_data(owner_address, next_item_index, in_msg_body~load_ref(), nft_item_code, in_msg_body~load_ref());
      return ();
    }
    throw(0xffff);
}

This function processes incoming internal messages and executes different logic based on the operation code.

Implementing Contract Operation Code Handling

In the contract, we need to handle the following operation codes:

  • op::get_royalty_params(): Retrieve royalty parameters.
  • op == 1: Deploy a new NFT.
  • op == 2: Deploy new NFTs in bulk.
  • op == 3: Change the contract owner.
  • op == 4: Change the collection content.
    We can execute the corresponding logic based on the operation code within the recv_internal function.

Providing Public Methods

The contract also needs to provide the following public methods for external calls:

  • get_collection_data(): Get collection data.
  • get_nft_address_by_index(): Get the address of an NFT item by index.
  • royalty_params(): Get royalty parameters.
  • get_nft_content(): Get the content of an NFT.
    These methods make the contract more functional and flexible.

Error Handling and Considerations

When writing the contract, we need to perform error checks to ensure the security and stability of the contract. For example, we should check if the message is empty, if it is a bounced message, if the sender is the owner, and if the item index is valid.
Additionally, here are some considerations to keep in mind:

  • Use the TON blockchain’s operation codes and built-in functions.
  • Consider security and permission controls in the logic processing.
  • Be mindful of the quantity limit when deploying NFTs in bulk to prevent overload.

Compiling and Deploying

After writing the contract, we need to use a specific compiler to compile the FunC code into the lower-level code that can run on the TON blockchain. The compilation process may involve some optimization and debugging to ensure the contract works as expected.

Compiling the Contract

The steps to compile the contract typically include:

  1. Install the TON compiler.
  2. Use the compiler to convert the FunC code into TVM bytecode.
  3. Check the compiler output logs to ensure there are no errors.

Deploying the Contract

Once compiled successfully, you can deploy the contract using the following steps:

  1. Use a TON wallet or command-line tool to create a new contract account.
  2. Upload the compiled contract code and initialization parameters to the newly created contract account.
  3. Confirm that the contract has been successfully deployed and activated.

Testing and Verification

After deployment, thorough testing is required to ensure the contract functions correctly. Here are some testing suggestions:

  1. Test each function of the contract in a simulated environment.
  2. Verify the creation, ownership transfer, content update, and royalty setting of NFTs.
  3. Check that the contract’s error handling mechanisms are effective.

Conclusion

Through this tutorial, we have learned how to build an NFT contract from scratch using the FunC language on the TON blockchain. We started with the contract’s storage structure, step by step introduced how to write core functions, handle operation codes, provide public methods, and finally covered the compilation, deployment, and testing of the contract. With these basic skills, you can further explore advanced features of NFT contracts, such as sophisticated royalty logic, permission management, and cross-contract interactions.

Remember, blockchain development is a continuous process of learning and practice. As you delve deeper into FunC language and the TON blockchain, you will be able to create more complex and innovative NFT contracts, bringing new possibilities to the digital asset space. Keep practicing and exploring, and you will carve out your own niche in the NFT world.

2 Likes