TIP-31 Non Fungible Tokens and their Collections

Basic terms

NFT Collection Root (or Root) - initial smart contract for any NFT. Deploys other NFT smart contracts.

NFT smart contract (or NFT contract) - user smart contract which stores NFT.

NFT item - unique element of collection.

NFT Collection Resolver - ****simple smart contract with public image (tvc) which stores code hash of NFT contract.

NFT item is a binary data (text, picture or some data) with some metadata (example: name, symbol, description and some other data related to collection). It has a unique ID calculated as hash of its binary data (see diagram bellow).

NFT Collection Diagram

Step 1: Deploy NFT Root

Root

To create a new collection NFT Root must be deployed. NFT Root stores metadata about collection:

  • collection name;
  • total amount of NFT items;
  • amount of granted NFT items;
  • NFT contract code.

Root is a ledger of un-issued NFT items. Only root can grant NFT item to NFT contracts. When it happens Root removes item from its ledger and transfer it to destination NFT address. If address doesn’t exist it will be instantiated with NFT contract code and data.

Important Note: NFT contract can be deployed only via constructor and only by its NFT Collection Root. Otherwise NFT contract must be self-destructed.

Root сan have additional special features:

  • minting - creating new NFT items;
  • burning (?)

NFT Contract

NFT contract stores NFT item.

Root address must be a part of NFT contract code. NFT’s constructor function must compare sender address with Root address hardcoded into its code. If they are not equal the contract must call selfdestruct function sending all its balance to the Root.

No one except the Root can deploy an NFT contract.

If someone will try to deploy it by external function with empty body or with encoded constructor call in body then it must fail because the contract must not accept external message if constructor was not called.

Also constructor itself must not accept external messages.

Fallback function must be defined to throw exceptions on any internal messages.

If someone will send internal message with constructor call then security check in constructor should prevent from successfull deployment.

Important Security Note: currently there is a bug in C++ node and in Transaction Executor particularly. If “constructor” (with code and data) internal message does not contain enough gas to complete constructor (or any other function) then transaction is aborted but contract is deployed anyway. It will be fixed in the node code and no special checks are needed on contract side.

NFT contract example

pragma ton-solidity >=0.40.0;
pragma AbiHeader expire;
pragma AbiHeader time;

struct NftItem {
    bytes data;
    string name;
    // TODO: add other related metadata
}

contract NftBase {
    mapping (uint256 => NftItem) public nftItems;

    modifier onlyRoot {
        optional(TvmCell) optSalt = tvm.codeSalt();
        require(optSalt.hasValue(), 101);
        TvmCell salt = optSalt.get();
        require(salt.toSlice().decode(address) == msg.sender, 102);
        _;
    }

    modifier acceptOwner {
        require(msg.pubkey() == tvm.pubkey(), 100);
        tvm.accept();
        _;
    }

    constructor() public onlyRoot {
        // TODO: init some vars if needed.
    }

    // Default fallback function is used.
}

contract NftAcceptable is NftBase {

    function accept(NftItem item) public onlyRoot {
        TvmBuilder b;
        b.store(item);
        nftItems[tvm.hash(b.toCell())] = item;
        msg.sender.transfer({value: 0, flag: 64, bounce: false});
    }

}

contract NftTranferable is NftBase {

    function transfer(uint256 nftHash, address dest) public acceptOwner {
        // TODO: 
    }
}

// Build your NFT from features. NftBase is mandatory.
contract NFT is NftAcceptable, NftTranferable, NftBase {
	
}

Step 2: Enable NFT Collection in global search

All NFT contracts from one collection must have a unique code hash. This is archived by mixing Root address into NFT’s initial code.

In order for your collection to be included in global search you should take the following steps:

  • Deploy NFT Collection Resolver.

    Its address should be calculated from its initial image and NFT contract’s code hash used as part of initial data.

  • Store Root address in NFT Collection Resolver.

NFT Collection Resolver Source Code

pragma ton-solidity >=0.40.0;
pragma AbiHeader expire;
pragma AbiHeader time;

contract NftResolver {
		// Code hash of NFT contract
    uint256 static codeHash;
		// TON address of NFT Root
    address m_root;

    modifier onlyOwner() {
        require(msg.pubkey() == tvm.pubkey(), 100);
        tvm.accept();
        _;
    }

    constructor(address root) public onlyOwner {
        m_root = root;
    }

    function getInfo() public view returns (uint256 nftCodeHash, address nftRoot) {
        nftCodeHash = codeHash;
        nftRoot = m_root;
    }

    function destruct(address addr) public onlyOwner {
        selfdestruct(addr);
    }
}

All NFT collections can be easily found by querying all NFT Collection Resolvers (by one request using its code hash).

Using DENS

Register a Certificate in DeNS under DeNS://NFT/{Collection} where {Collection} is an name of your collection. Store address of your NFT Collection Resolver in the Certificate.

11 Likes

Спасибо вам большое ,за ваши труды

Hi! Does this standart extend TIP-3 and should we use it, for example, in NFT marketplace contest? Or is it just an early in-progress proposal?

It is an extension to TIP-3. You can use it right away. We are working on some node and TVM modifications to support some checks but the token itself should be fine even without them. The checks are not for security but rather usability concerns. We are going to promote it as a new standard for NFT and hope to receive community feedback on it.