Getting Started with Writing NFT Sale Smart Contracts from Scratch - Using FunC Language on TON Blockchain

Preface

In the rapidly evolving world of blockchain technology, the NFT (Non-Fungible Token) market has seen a surge in popularity. The TON (The Open Network) blockchain provides a platform where developers can leverage the FunC language to create sophisticated smart contracts for managing NFT sales. This guide is tailored for beginners, aiming to demystify the process of writing an NFT sale smart contract by breaking down a sample code and providing step-by-step instructions on how to craft your own.

I. Smart Contract Code Interpretation

Let’s delve into the anatomy of an NFT sale smart contract by examining the provided code snippet:

#include "imports/stdlib.fc";
#include "imports/op-codes.fc";
#include "imports/utils.fc";
#include "imports/error.fc";
#include "imports/constants.fc";

;; NFT sale smart contract v3r3
;; It's a v3r2 but with returning query_id, handling code 32, allow change price

global int data::is_complete;
global int data::created_at;
global slice data::collection_address;
global slice data::nft_address;
global slice data::nft_owner_address;
global int data::full_price;
global cell data::fees_cell;
global int data::sold_at;
global int data::query_id;

() load_data() impure inline {
  // Omitted code, same below
}
// Omitted other functions and code

1.1 Contract Structure

The smart contract is modularized into several components:

  • Import Statements: These include essential library files that provide the necessary functions and constants for the contract.
  • Global Variables: These store the state of the contract, including details about the NFT, its owner, and the sale.
  • Function Definitions: These are the building blocks of the contract, encapsulating various operations such as data loading, sending money, and handling purchases.

1.2 Detailed Explanation of Functions

Here’s a more in-depth look at some of the key functions within the contract:

  • load_data(): This function retrieves the contract’s state from its data cell, including timestamps, addresses, and prices.
  • send_money(): A crucial function for transferring funds between addresses on the blockchain.
  • purchase(): This function handles the complexities of an NFT purchase, including fee distribution and ownership transfer.
  • delisting_nft(): Used to remove an NFT from sale, reverting ownership to the original owner.
  • change_price(): Allows the owner to update the price of the NFT for sale.

II. Writing an NFT Sale Smart Contract

Embarking on the journey of writing your own NFT sale smart contract involves several steps. Let’s walk through the process.

2.1 Environment Setup

Before you start coding, you need to set up a development environment that supports the TON blockchain and the FunC language. This typically involves installing the TON SDK, the FunC compiler, and any other necessary tools.

2.2 Creating a New Contract

Begin by creating a new file for your smart contract. Let’s call it nft_sale.fc. Here’s how you start:

#include "imports/stdlib.fc";
#include "imports/op-codes.fc";
#include "imports/utils.fc";
#include "imports/error.fc";
#include "imports/constants.fc";

global int data::is_complete;
global int data::created_at;
global slice data::collection_address;
global slice data::nft_address;
global slice data::nft_owner_address;
global int data::full_price;
global cell data::fees_cell;
global int data::sold_at;
global int data::query_id;

2.3 Implementing Key Features

Now, let’s expand on the key features of the smart contract.

2.3.1 Data Loading and Saving

The load_data() and save_data() functions are vital for managing the contract’s state.

() load_data() impure inline {
  var ds = get_data().begin_parse();
  data::is_complete = ds~load_uint(1);
  data::created_at = ds~load_uint(32);
  data::collection_address = ds~load_msg_addr();
  data::nft_address = ds~load_msg_addr();
  data::nft_owner_address = ds~load_msg_addr();
  data::full_price = ds~load_coins();
  data::fees_cell = ds~load_ref();
  data::sold_at = ds~load_uint(32);
  data::query_id = ds~load_uint(64);
}

() save_data() impure inline {
  set_data(begin_cell()
    .store_uint(data::is_complete, 1)
    .store_uint(data::created_at, 32)
    .store_slice(data::collection_address)
    .store_slice(data::nft_address)
    .store_slice(data::nft_owner_address)
    .store_coins(data::full_price)
    .store_ref(data::fees_cell)
    .store_uint(data::sold_at, 32)
    .store_uint(data::query_id, 64)
    .end_cell()
  );
}

2.3.2 Sending Money

The send_money() function facilitates transactions within the contract.

() send_money(slice address, int amount) impure inline {
  check_std_addr(address);
  var msg = begin_cell()    
	  .store_uint(0x10, 6) ;; nobounce    
	  .store_slice(address)    
	  .store_coins(amount)    
	  .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)    
	  .end_cell();  
	send_raw_message(msg, 1);
}

2.3.3 Purchasing an NFT

The purchase() function is where the magic happens—transferring ownership and handling payments.

() purchase(int my_balance, int msg_value, slice sender_address, int query_id) impure {
	throw_unless(error::not_enough_balance, msg_value >= data::full_price + const::min_gas_amount);

  var (
    marketplace_fee_address,
    marketplace_fee,
    royalty_address,
    royalty_amount
  ) = load_fees(data::fees_cell);

  ;; Owner message
  send_money(
    data::nft_owner_address,
    data::full_price - marketplace_fee - royalty_amount + (my_balance - msg_value)
  );

  ;; Royalty message
  if ((royalty_amount > 0) & (royalty_address.slice_bits() > 2)) {
    send_money(royalty_address, royalty_amount);
  }

  ;; Marketplace fee message
  send_money(marketplace_fee_address, marketplace_fee);

  builder nft_transfer = begin_cell()
    .store_uint(op::transfer(), 32)
    .store_uint(query_id, 64)
    .store_slice(sender_address) ;; new_owner_address
    .store_slice(sender_address) ;; response_address
    .store_int(0, 1) ;; empty custom_payload
    .store_coins(0) ;; forward amount to new_owner_address
    .store_int(0, 1); ;; empty forward_payload
  
  var nft_msg = begin_cell()
    .store_uint(0x18, 6)
    .store_slice(data::nft_address)
    .store_coins(0)
    .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    .store_ref(nft_transfer.end_cell());

  check_std_addr(data::nft_address);
  send_raw_message(nft_msg.end_cell(), 128);

  ;; Set sale as complete
  data::is_complete = 1;
  data::sold_at = now();
  data::query_id = query_id;
  save_data();
}

2.4 Additional Functions

You’ll also need to implement functions for delisting and changing the price of the NFT.

() delisting_nft(int msg_value, slice sender_address, int query_id) impure {
	throw_unless(error::not_enough_gas, msg_value >= const::min_gas_amount);
  throw_unless(error::not_owner, 
    equal_slices_bits(sender_address, data::nft_owner_address) | equal_slices_bits(sender_address, data::collection_address));

  var msg = begin_cell()
    .store_uint(0x10, 6) ;; nobounce
    .store_slice(data::nft_address)
    .store_coins(0)
    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    .store_uint(op::transfer(), 32)
    .store_uint(query_id, 64)
    .store_slice(data::nft_owner_address) ;; new_owner_address
    .store_slice(data::nft_owner_address) ;; response_address;
    .store_int(0, 1) ;; empty custom_payload
    .store_coins(0) ;; forward amount to new_owner_address
    .store_int(0, 1); ;; empty forward_payload

  send_raw_message(msg.end_cell(), 128);

  data::is_complete = 1;

  save_data();
}

() change_price(slice sender_address, slice in_msg_body) impure {
  throw_unless(error::not_owner, equal_slices_bits(sender_address, data::nft_owner_address));

  var new_full_price = in_msg_body~load_coins();
  throw_unless(error::invalid_price, new_full_price > 0);

  data::full_price = new_full_price;
  save_data();
}

2.5 Handling Internal Messages

The recv_internal function is responsible for processing incoming messages and triggering the appropriate actions based on the message content.

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
  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();

  load_data();

  int op = op::accept_coins();
  int query_id = 0;

  if (in_msg_body.slice_empty?() == false) {
    op = in_msg_body~load_uint(32);
  }

  if (op != 0) {
    query_id = in_msg_body~load_uint(64);
  } else {
    if (equal_slices_bits(msg::cancel_msg(), in_msg_body)) {
      op = op::delisting_nft();
    }
  }

  if (op == op::accept_coins()) {
    ;; just accept coins
    return ();
  }

  var is_initialized = data::nft_owner_address.slice_bits() > 2; ;; not initialized if null address

  if ((op == op::emergency_transfer()) & 
    ((data::is_complete != 1) | (~ is_initialized)) & equal_slices_bits(sender_address, data::collection_address)) {
    ;; way to fix unexpected troubles with sale contract
    ;; for example if some one transfer nft to this contract
    var msg = in_msg_body~load_ref().begin_parse();
    var mode = msg~load_uint(8);

    throw_if(error::invalid_mode, mode & 32);

    send_raw_message(msg~load_ref(), mode);
    return ();
  }

  ;; Throw if sale is complete
  throw_if(error::sold_completed, data::is_complete == 1);

  if ((op == op::change_price())) {
    change_price(sender_address, in_msg_body);
    return ();
  }

  if (~ is_initialized) {
    if (equal_slices_bits(sender_address, data::collection_address)) {
      return (); ;; just accept coins on deploy
    }

    throw_unless(error::not_owner, equal_slices_bits(sender_address, data::nft_address));
    throw_unless(error::invalid_operation, op == op::ownership_assigned());
    data::nft_owner_address = in_msg_body~load_msg_addr();

    save_data();
    return ();
  }

  if (op == op::delisting_nft()) {
    delisting_nft(msg_value, sender_address, query_id);
    return ();
  }

  if (op == op::purchase()) {
    purchase(my_balance, msg_value, sender_address, query_id);
    return ();
  }

  throw(0xffff);
}

2.6 Deploying and Testing

After writing the contract, you’ll need to compile it using the FunC compiler. Once compiled, you can deploy it to the TON blockchain. It’s crucial to thoroughly test the contract to ensure it behaves as expected.

// Compilation command
// Deployment command

2.7 Debugging and Optimization

During the testing phase, you may encounter bugs or performance issues. Debugging tools and techniques are essential for identifying and fixing these problems. Optimization may also be necessary to ensure that your contract runs efficiently and within the gas limits of the network.

III. Advanced Features and Considerations

As you become more comfortable with writing smart contracts, you can explore more advanced features and considerations to enhance your NFT sale contract.

3.1 Royalties and Fees

Implementing a system for royalties and fees can be crucial for creators and marketplaces. Your contract should be able to distribute funds to the appropriate parties after a sale.

3.2 Security

Smart contract security is of utmost importance. You must protect against common vulnerabilities such as reentrancy attacks, integer overflow, and unauthorized access. Consider using formal verification tools to mathematically prove the correctness of your contract.

3.3 User Experience

The user experience of interacting with your NFT sale contract can also impact its success. Ensure that the contract is user-friendly and provides clear instructions and feedback for buyers and sellers.

3.4 Compliance and Regulation

Stay informed about the legal and regulatory aspects of NFTs. Your contract should comply with relevant laws and regulations, including those related to copyright, privacy, and money laundering.

IV. Conclusion

Writing an NFT sale smart contract using the FunC language on the TON blockchain is a complex but rewarding task. By following the steps outlined in this guide, you’ve taken the first steps towards mastering the art of smart contract development. Remember that continuous learning and practice are key to improving your skills and creating secure, efficient, and user-friendly contracts.

As you continue your journey, keep in mind the following tips:

  • Always test your contracts thoroughly.
  • Stay updated with the latest developments in blockchain technology and smart contract best practices.
  • Engage with the community for support and to share your knowledge.
  • Consider the ethical implications of your contracts and strive to create solutions that benefit all parties involved.
    With dedication and hard work, you can become a proficient smart contract developer and make a significant impact in the NFT space. Happy coding!
1 Like