Select Page

3 NFT Smart Contract Examples (Solidity, Vyper and Rust) + What Each Function Means

Explore 3 NFT smart contract examples (in Solidity, Vyper, and Rust) and learn what each function in the code does.

by | Mar 3, 2023

Looking for an NFT smart contract example? You’ve come to the right place! As you may know, a smart contract is the backbone of an NFT. Thus, with the increasing popularity of NFTs in art, gaming, music, and more, it’s no wonder so many people are interested in developing NFT smart contracts.

A great way to better understand NFTs and how to create a smart contract is by reviewing NFT smart contract examples. So, we’ve compiled some detailed NFT smart contract code samples and explanations of how the code works. Below, you’ll find NFT smart contract examples written in Solidity, Vyper, and Rust.

Ready to check out an NFT smart contract example? Let’s dive in.

Here’s what we’ll cover.
9
What are NFTs?
9
The Role of Smart Contracts in NFTs
9
Smart Contract Languages
9
Smart Contract Examples
9
Solidity vs Vyper vs Rust for NFT Smart Contracts
NFT smart contract examples icon

What are NFTs and how do they work?

NFTs are unique digital tokens that represent ownership of digital content, such as a piece of artwork, music, or even a tweet. NFTs are non-fungible. This means that they are one-of-a-kind and cannot be replicated.

In the next section, we’ll explore the role of smart contracts in NFTs and why they are so important in digital ownership.

The Role of Smart Contracts in NFTs

When an NFT is created, it is accompanied by a smart contract. This NFT smart contract contains all the information about the asset, including its owner, unique identifier, and relevant metadata. This smart contract is stored on the blockchain and can be accessed and verified by anyone with an internet connection.

-> Learn more about NFT smart contracts, their benefits, disadvantages, and real-world use cases here – NFT Smart Contracts

Do all NFTs have a smart contract?

Yes, all NFTs have a smart contract associated with them. The smart contract serves as a set of rules that govern the creation, ownership, and transfer of the NFT. 

An NFT’s smart contract is deployed on a blockchain network, and all transactions related to the NFT are recorded on the blockchain. The smart contract also ensures that the NFT is unique and cannot be replicated or duplicated. Thus, without a smart contract, an NFT could not exist as a unique and valuable digital asset.

Smart Contract Languages

NFT smart contracts can be written in various programming languages, with some being more popular than others. 

Here are some of the most commonly used languages for creating NFT smart contracts:

1. Solidity

Solidity is the most popular programming language for creating smart contracts on the Ethereum blockchain. It is a high-level language similar to JavaScript and designed explicitly for creating smart contracts. 

Want to see what an NFT smart contract looks like in Solidity? Check out our Solidity smart contract example below.

2. Vyper

Vyper is a smart contract programming language similar to Solidity but designed to be more secure and less prone to bugs. Vyper also has a simpler syntax than Solidity and is easier to audit.

Vyper smart contracts can be used on the Ethereum blockchain. The Vyper language was designed for Ethereum and optimized for use with the Ethereum Virtual Machine (EVM). However, Vyper code can also be compiled to run on other Ethereum-compatible blockchains, such as Binance Smart Chain or Polygon.

Want to see what an NFT smart contract looks like in Vyper? Check out our Vyper smart contract example below.

3. Rust

Rust is a systems programming language designed for performance and safety. It is becoming increasingly popular for creating smart contracts on the Ethereum blockchain because of its safety features and ability to write efficient code.

Want to see what an NFT smart contract looks like in Rust? Check out our Rust smart contract example below.

4. JavaScript

JavaScript is a popular programming language that is used for web development. It can also be used to create smart contracts. However, it is not as commonly used for creating NFT smart contracts as Solidity, Vyper, or Rust.

5. Go

Go is a programming language known for its simplicity and efficiency. It is becoming increasingly popular for creating smart contracts on the Ethereum blockchain because of its ease of use and ability to write efficient code.

Now, let’s look at an NFT smart contract example for the top three smart contract programming languages above.

What does a smart contract look like? 3 NFT Smart Contract Examples

The appearance of a smart contract varies depending on the programming language it’s written in. That said, smart contract code typically includes various elements, including functions, variables, events, and data structures. It may also incorporate other smart contracts, libraries, or external services.

Below are three NFT smart contract examples – written in Solidity, Vyper, and Rust. We’ll take an in-depth look at each code and the various elements it uses.

Please note: the examples below are for educational purposes. They do not represent actual contracts for any particular NFT.

Solidity NFT Smart Contract Example

Below is an example of an NFT smart contract written in the Solidity programming language.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;
import “@openzeppelin/contracts/access/Ownable.sol”;
import “@openzeppelin/contracts/utils/Counters.sol”;

contract MyNFT is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;

constructor() ERC721(“MyNFT”, “MNFT”) {}

function mintNFT(address recipient, string memory tokenURI) public returns (uint256) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}

To help you better understand the Solidity smart contract example above, let’s look at the code’s various components.

 

  • pragma solidity ^0.8.0; – this line of code is called a compiler version pragma. It specifies the minimum version of the Solidity compiler that can be used to compile the smart contract. In this case, the pragma specifies that the minimum version of the Solidity compiler that can be used is version 0.8.0 or higher. Including this pragma at the beginning of a Solidity contract is critical to ensure that the correct compiler version is used to compile the contract.

 

  • import “@openzeppelin/contracts/token/ERC721/ERC721.sol”; – this line of code imports the ERC721 contract from the OpenZeppelin library. “ERC721” is a standard interface for creating non-fungible tokens on the Ethereum blockchain. It defines a set of functions and events that allow developers to create, own, and transfer unique digital assets. Thus, by importing ERC721 into the MyNFT contract, we gain access to its functions and events.

 

  • import “@openzeppelin/contracts/access/Ownable.sol”; – this line of code imports Ownable. Ownable is also a contract from the OpenZeppelin library. It provides a basic access control mechanism. In particular, it allows for the specification of a contract owner and grants this owner the ability to perform certain actions, such as minting new tokens or transferring ownership, that are restricted only to them.

 

  • import “@openzeppelin/contracts/utils/Counters.sol”; – this line of code imports a library called Counters from the OpenZeppelin library. The Counters library creates and manages numerical counters, commonly used in smart contract development. By importing this library, the smart contract can use the Counter data type and associated functions without having to rewrite the code from scratch. This saves time and reduces the likelihood of errors.

 

  • contract MyNFT is ERC721, Ownable { – this line of code creates a new contract called “MyNFT” and indicates that it inherits functionality from two other contracts: “ERC721” and “Ownable.” By using the “is” keyword, the MyNFT contract can inherit all of the functions and variables from both contracts and add any additional functionality required.

 

  • using Counters for Counters.Counter; – this is a Solidity feature called “library usage.” It allows the contract to inherit and use functions from the Counters library, specifically the Counter data type. In this case, the Counters library keeps track of the unique IDs assigned to each NFT minted by the contract. By using the Counters library, the contract does not have to increment NFT IDs manually. Instead, it can use the Counters library’s functions to handle this process automatically.

 

  • Counters.Counter private _tokenIds; – this line of code creates a private variable _tokenIds of type Counters.Counter from the Counters library. The Counters.Counter is a simple counter that starts at zero and increments by one every time the increment() function is called. This variable keeps track of the next token ID to be minted. It is marked as private, meaning it can only be accessed within the smart contract.

 

  • constructor() ERC721(“MyNFT”, “MNFT”) {} – this is a constructor function that is used to initialize the state variables of the smart contract when it is deployed on the Ethereum blockchain. The constructor function calls the constructor function of the ERC721 contract and passes two parameters:
      • The name of the NFT contract (i.e., “MyNFT”)
      • The symbol of the NFT contract (i.e., “MNFT”)In other words, this line of code ensures that the MyNFT contract inherits the implementation of the ERC721 standard and sets the name and symbol of the NFT.

 

  • function mintNFT(address recipient, string memory tokenURI) public returns (uint256) { – this line of code defines a function called “mintNFT.” The mintNFT function is the most important part of the contract. This function takes two arguments: recipient (i.e., the address of the user who will receive the NFT) and tokenURI (i.e., a unique identifier that points to the metadata of the NFT). The function is declared public, meaning anyone can call it, and returns a uint256 value, representing the unique ID of the newly created NFT.

 

      • _tokenIds.increment(); – this is the first step in the mintNFT function. It increases the value of the _tokenIds counter by 1. It does this by calling the increment function on the Counter. The _tokenIds counter is used to keep track of the unique ID of each NFT that is minted. Each new NFT that is minted will have a unique ID that is one greater than the ID of the previously minted NFT.

 

  •  
      • uint256 newItemId = _tokenIds.current(); – this is the second step in the mintNFT function. This line of code creates a new variable called newItemId and assigns the current value of the _tokenIds counter to it. This ensures that each newly minted NFT has a unique identifier, starting from 1 and incrementing by 1 for each subsequent NFT.

 

      • _mint(recipient, newItemId); – this is the third step in the mintNFT function. This line of code mints a new NFT and assigns ownership to the recipient address provided in the function parameter. The _mint function is a built-in function of the ERC721 contract that handles the creation and ownership assignment of a new NFT. It takes two parameters – the recipient address and the ID of the newly created NFT. In this case, the ID is the value of the “newItemId” variable, which is incremented for each new NFT created.

 

      • _setTokenURI(newItemId, tokenURI) – this is the fourth step in the mintNFT function. It calls the function _setTokenURI provided by the OpenZeppelin ERC721 implementation. This function takes two arguments: the first argument is the token ID, and the second argument is the URI (where the metadata for the token is stored). The _setTokenURI function is used to set the token URI for a given token ID, allowing external applications to access the metadata for that token. This metadata typically includes information such as the name, description, and image of the NFT. The tokenURI is usually a URL pointing to a JSON file containing the metadata. Put more simply, this line of code is used to associate the metadata for the newly minted NFT with its token ID.

 

      • return newItemId; – this is the fifth and final step in the mintNFT function. This line of code returns the unique token ID of the newly minted NFT. This token ID can be used to identify the NFT within the contract, on the blockchain, and in any marketplace or platform where the NFT is traded or displayed. The token ID is also used in functions such as ownerOf() and getApproved() to query the ownership and approval status of the NFT.

 

Again, this example is just one possible implementation of an NFT smart contract. There are many other ways to write these contracts depending on the specific use case and requirements.

 

Vyper NFT Smart Contract Example

Here’s an example of an NFT smart contract written in Vyper. This programming language is another popular smart contract language for Ethereum.

# Vyper version: 0.2.0-beta.16

event Transfer(_from: address, _to: address, _tokenId: int128)
event Approval(_owner: address, _approved: address, _tokenId: int128)

struct NFT:
name: String[64]
symbol: String[16]
totalSupply: int128
balances: HashMap[address, int128]
ownerOf: HashMap[int128, address]
approved: HashMap[int128, address]

nft: NFT

@public
def __init__(name: String[64], symbol: String[16]):
self.nft.name = name
self.nft.symbol = symbol
self.nft.totalSupply = 0

@public
def name() -> String[64]:
return self.nft.name

@public
def symbol() -> String[16]:
return self.nft.symbol

@public
def totalSupply() -> int128:
return self.nft.totalSupply

@public
def _mint(to: address, tokenId: int128):
self.nft.balances[to] += 1
self.nft.ownerOf[tokenId] = to
self.nft.totalSupply += 1

@public
def mint(to: address):
tokenId: int128 = self.nft.totalSupply + 1
self._mint(to, tokenId)
Transfer(ZERO_ADDRESS, to, tokenId)

@public
def balanceOf(owner: address) -> int128:
return self.nft.balances[owner]

@public
def ownerOf(tokenId: int128) -> address:
return self.nft.ownerOf[tokenId]

@public
def approve(approved: address, tokenId: int128):
owner: address = self.nft.ownerOf[tokenId]
require(msg.sender == owner, “Not the owner of this token”)
self.nft.approved[tokenId] = approved
Approval(owner, approved, tokenId)

@public
def getApproved(tokenId: int128) -> address:
return self.nft.approved[tokenId]

@public
def transferFrom(from: address, to: address, tokenId: int128):
require(from == self.nft.ownerOf[tokenId], “Not the owner of this token”)
require(to != ZERO_ADDRESS, “Cannot transfer to zero address”)
require(self.nft.approved[tokenId] == msg.sender or msg.sender == from, “Not approved to transfer this token”)
self.nft.balances[from] -= 1
self.nft.balances[to] += 1
self.nft.ownerOf[tokenId] = to
self.nft.approved[tokenId] = ZERO_ADDRESS
Transfer(from, to, tokenId)

Let’s examine the functions in this NFT smart contract example.

 

  • # Vyper version: 0.2.0-beta.16 – the first line of this NFT smart contract example is a comment. It specifies the version of the Vyper compiler that the contract was written for. This information is helpful because different versions of the Vyper compiler may have different features or syntax, so it’s essential to know which version was used when compiling the contract. In this case, the contract was written for Vyper version 0.2.0-beta.16.

 

  • event Transfer(_from: address, _to: address, _tokenId: int128) – this line of code defines an event called Transfer. An event is a way for the contract to communicate that something has happened on the blockchain. In this case, the Transfer event is called when an NFT is transferred from one address (_from) to another address (_to), with the ID of the NFT being _tokenId. By calling this event, the contract can notify other contracts or external applications about the NFT transfer, which can be helpful in tracking ownership or triggering other actions.

 

  • event Approval(_owner: address, _approved: address, _tokenId: int128) – this line of code defines an event (Approval) that is called when a user approves another address to manage their NFT. When this event is called, it allows external parties, such as other smart contracts or user interfaces, to be notified of the approval and to take any necessary actions. For example, a user interface might display a message to the user indicating that the approval was successful, or a smart contract might use the event to trigger a function that performs some action related to the approval. The event has three parameters:
    • _owner: this is the address of the NFT owner who is granting the approval.
    • _approved: the address of the account that is approved to manage the NFT.
    • _tokenId: the ID of the NFT being approved for management.
  • struct NFT: – this defines a new data structure called NFT. It is similar to a class in other programming languages and can be thought of as a template for creating instances of a custom data type. By defining a custom struct, the contract can store multiple instances of NFTs with different properties in a more organized way. For example, the contract could create an array or mapping of NFT instances to keep track of all the NFTs that have been minted, and each NFT instance would contain its own unique properties. In this case, the NFT struct defines the properties of an NFT, including:
    • name: String[64] – this variable declaration creates a string data type variable called name with a maximum length of 64 characters. The name variable is used to store the name of the NFT that is created using this contract. This is similar to the name variable in other NFT contract examples, such as the Solidity example we previously discussed.
    • symbol: String[16] – this line of code creates a state variable that describes the symbol of the NFT. The symbol is a short code for the NFT. It is commonly used to identify and display the NFT in different platforms and applications. For example, the symbol of the Ethereum network’s native NFT is “ETH.” In this code, the symbol variable is defined as a fixed-length string of 16 characters.
    • totalSupply: int128 – this creates a state variable that stores the total number of tokens that have been minted. It is of the data type int128, which means it is a signed integer with a maximum value of 2^127 – 1. This variable is used to keep track of the total number of tokens that have been minted so far, which is important for enforcing the maximum supply of the NFT contract.
    • balances: HashMap[address, int128] – this line of code creates a mapping of address to int128, which represents the number of tokens that a given address owns. This mapping keeps track of the number of tokens each address has in their possession. For example, if the address 0x123456 owns three NFTs, the mapping would look like this: balances[0x123456] = 3. This allows for efficient checking of ownership and balance transfers.
    • ownerOf: HashMap[int128, address] – this line of code is used to maintain a mapping between a token ID and its current owner’s address. This mapping allows the contract to quickly identify the current owner of a specific NFT by looking up its corresponding token ID. Here, int128 is the data type for the token ID, and address is the data type for the owner’s address. When a new NFT is created, the ownerOf mapping is updated to associate the newly minted token ID with the address of the account that created it. When a token is transferred to a new owner, the ownerOf mapping is updated to reflect the new ownership.
    • approved: HashMap[int128, address] – this variable is a HashMap that maps an NFT ID (int128) to an approved address (address). This allows the owner of an NFT to approve another address to transfer the NFT on their behalf. For example, if Alice owns an NFT with ID 1 and she approves Bob’s address, Bob can transfer the NFT to another address without needing Alice’s permission again. The approved variable keeps track of this approved address for each NFT ID.

 

  • nft: NFT – this line of code is used to create an instance of the NFT struct. The NFT struct contains the metadata for the NFT, such as its name, symbol, and total supply. By creating an instance of the NFT struct, we can use its properties and methods in the contract. In this case, the nft instance is used to set the initial metadata for the NFT in the contract constructor.

 

  • def __init__(name: String[64], symbol: String[16]): – this function is the constructor of the NFT contract, which is executed only once when the contract is deployed on the blockchain. The constructor takes two arguments, name and symbol, which represent the name and symbol of the NFT. The first line, @public, is a decorator that specifies that the function can be called from outside the contract. Inside the function, we set the name, symbol, and totalSupply of the NFT by assigning them to the corresponding attributes of the self.nft struct. self refers to the instance of the contract and self.nft refers to the NFT struct we defined earlier. Finally, we set the totalSupply to 0, as there are no NFTs initially.

 

  • def name() -> String[64]: – this line of code defines a function called name(). The name() function is a public function that returns the name of the NFT as a String with a maximum length of 64 characters. In the function definition, @public indicates that this function can be called externally by anyone. The return type of the function is String[64], indicating that it returns a string with a maximum length of 64 characters. Within the function, the name of the NFT is accessed through self.nft.name. Here, self refers to the instance of the contract, and nft is a struct that holds information about the NFT. Finally, the name attribute holds the name of the NFT, which is returned by the function. In other words, the line return self.nft.name simply returns the name of the NFT. This function is a getter function that allows anyone to retrieve the name of the NFT.

 

  • def symbol() -> String[16]: – this code in the Vyper NFT smart contract example above defines a function called symbol(). The symbol() function is a public function that returns the symbol of the NFT token. The function has a @public decorator, which means that it can be called from outside the contract by anyone. The function simply retrieves the symbol of the NFT token from the nft struct and returns it. The String[16] type represents a string of up to 16 characters in length, meaning that the symbol can be a string that is up to 16 characters long.

 

  • def totalSupply() -> int128: – this function (i.e., totalSupply()) is a public function that returns the total number of NFT tokens that have been minted in the contract. The function retrieves the totalSupply value from the nft struct and returns it to the caller. Since the totalSupply variable is declared as public, it can be accessed from outside the contract by anyone who wants to know how many NFT tokens have been minted so far.

 

  • def _mint(to: address, tokenId: int128): – this section of the code defines a function called _mint. The _mint function is used to create and add a new NFT to the contract by assigning it to a specific address. The function takes two arguments – the address of the person who will own the NFT and the unique identifier of the NFT. The function first increases the balance of the receiver’s address in the balances mapping by 1, indicating that the receiver now owns one more NFT. Then, it sets the ownerOf mapping for the newly minted NFT to the receiver’s address. Finally, it increments the totalSupply of the contract to reflect the creation of a new NFT. Essentially, the _mint function is responsible for adding a new NFT to the contract and updating the relevant mappings to reflect this change.

 

  • def mint(to: address): – this code defines a function called mint. The mint function is used to create and assign a new NFT to a specific address. It first calculates the tokenId by adding 1 to the current totalSupply of the NFT. Then it calls the internal _mint function, passing the to address and the tokenId as arguments. This adds the new NFT to the specified address, updates the balances, and increments the total supply of the NFT. Finally, it emits a Transfer event indicating the new NFT was minted and assigned to the specified to address.

 

  • def balanceOf(owner: address) -> int128: – this code defines a function called balanceOf. The balanceOf function is a public function that takes an owner address as input and returns the number of NFT tokens that are owned by that address. The function retrieves the balance of the owner by accessing the balances mapping with the owner’s address as the key and returns that value. The balances mapping is initialized as an empty mapping and is updated whenever a new NFT token is minted or transferred.

 

  • def ownerOf(tokenId: int128) -> address: – this code defines a function called ownerOf(). The ownerOf() function is a getter function that returns the address of the owner of a specific token. It takes in one parameter, tokenId, which represents the unique identifier of the token. Inside the function, it retrieves the owner’s address from the ownerOf mapping by passing in the tokenId as the key. It then returns this address as the output of the function. If the tokenId passed in as a parameter does not exist in the ownerOf mapping, the function will revert with an error.

 

  • def approve(approved: address, tokenId: int128): – this section of code defines a function called approve. The approve function is used to give permission to another address (approved) to transfer the token with the given tokenId. The function first checks if the caller is the owner of the token with the given tokenId. If the caller is not the owner, the function will fail with an error message. Otherwise, the function sets the approved address for the specified tokenId. It does this by checking the self.nft.approved mapping, which keeps track of approved addresses for each token. Finally, the function calls an Approval event. This event uses the owner, approved address, and tokenId as parameters to indicate that the owner has approved the given address to transfer the token.

 

  • def getApproved(tokenId: int128) -> address: – this code defines a function called getApproved. The getApproved function returns the address of the approved recipient for a given token ID. In other words, it returns the address that was approved by the current token owner to transfer ownership of the specified token. The function retrieves the approved address using the provided tokenId and approved mapping from the NFT struct. If no approved address exists for the token, the function will return the zero address.

 

  • def transferFrom(from: address, to: address, tokenId: int128): – this function transfers the ownership of a token from one address to another. It first checks if the sender is approved to transfer the token and if the sender is the owner of the token. Then it reduces the balance of the sender and increases the balance of the receiver by one. Finally, it sets the new owner of the token and sets the approved address to zero. It also emits a Transfer event with the sender, receiver, and token ID as parameters. To help you better understand how this is all done, let’s take a look at each line of code.
    • require(from == self.nft.ownerOf[tokenId], “Not the owner of this token”) – this line ensures that the from address is the current owner of the tokenId. If from is not the owner, then it throws an error.
    • require(to != ZERO_ADDRESS, “Cannot transfer to zero address”) – this line ensures that the to address is not the zero address, which is an invalid address in Ethereum. If to is the zero address, then it throws an error.
    • require(self.nft.approved[tokenId] == msg.sender or msg.sender == from, “Not approved to transfer this token”) – this line checks if the msg.sender is authorized to transfer the tokenId. If the msg.sender is either the current owner (from) or the approved address for the tokenId, then the transfer can proceed. Otherwise, it throws an error.
    • self.nft.balances[from] -= 1 – this line decrements the balance of from (i.e., the address of the sender who is transferring the token) by 1.
    • self.nft.balances[to] += 1 – this line increments the balance of to (i.e., the address of the account to which the NFT is being transferred) by 1. The balances dictionary keeps track of the number of NFTs owned by each address. Thus, the balance of to is incremented by 1 to reflect the fact that the account now owns one more NFT.
    • self.nft.ownerOf[tokenId] = to – this line changes the ownership of the tokenId from from to to.
    • self.nft.approved[tokenId] = ZERO_ADDRESS – this line removes any previously approved address for the tokenId.
    • Transfer(from, to, tokenId) – this line calls the Transfer event. Transfer takes the arguments from, to, and tokenId, which signals that the transfer has taken place.

 

Now, let’s take a look at a Rust NFT smart contract example.

 

NFT Smart Contract Example in Rust

Below is a simple NFT smart contract example written in Rust. Beneath the code, you will find an explanation of how the smart contract works.

# Vyper version: 0.2.0-beta.16

event Transfer(_from: address, _to: address, _tokenId: int128)
event Approval(_owner: address, _approved: address, _tokenId: int128)

struct NFT:
name: String[64]
symbol: String[16]
totalSupply: int128
balances: HashMap[address, int128]
ownerOf: HashMap[int128, address]
approved: HashMap[int128, address]

nft: NFT

@public
def __init__(name: String[64], symbol: String[16]):
self.nft.name = name
self.nft.symbol = symbol
self.nft.totalSupply = 0

@public
def name() -> String[64]:
return self.nft.name

@public
def symbol() -> String[16]:
return self.nft.symbol

@public
def totalSupply() -> int128:
return self.nft.totalSupply

@public
def _mint(to: address, tokenId: int128):
self.nft.balances[to] += 1
self.nft.ownerOf[tokenId] = to
self.nft.totalSupply += 1

@public
def mint(to: address):
tokenId: int128 = self.nft.totalSupply + 1
self._mint(to, tokenId)
Transfer(ZERO_ADDRESS, to, tokenId)

@public
def balanceOf(owner: address) -> int128:
return self.nft.balances[owner]

@public
def ownerOf(tokenId: int128) -> address:
return self.nft.ownerOf[tokenId]

@public
def approve(approved: address, tokenId: int128):
owner: address = self.nft.ownerOf[tokenId]
require(msg.sender == owner, “Not the owner of this token”)
self.nft.approved[tokenId] = approved
Approval(owner, approved, tokenId)

@public
def getApproved(tokenId: int128) -> address:
return self.nft.approved[tokenId]

@public
def transferFrom(from: address, to: address, tokenId: int128):
require(from == self.nft.ownerOf[tokenId], “Not the owner of this token”)
require(to != ZERO_ADDRESS, “Cannot transfer to zero address”)
require(self.nft.approved[tokenId] == msg.sender or msg.sender == from, “Not approved to transfer this token”)
self.nft.balances[from] -= 1
self.nft.balances[to] += 1
self.nft.ownerOf[tokenId] = to
self.nft.approved[tokenId] = ZERO_ADDRESS
Transfer(from, to, tokenId)

This NFT smart contract example may look a little complex. So, let’s break it down.

 

  • use ink_lang::contract; – The use statement in Rust is used to bring symbols (types, functions, constants, etc.) into scope so that they can be used in the current module without having to fully qualify the symbol name each time. In this case, ink_lang::contract is a Rust macro that is defined in the ink_lang module. It is used to define a smart contract in the Ink! framework. By using the contract macro, the Rust code following it will be transformed into a smart contract that can be deployed on a blockchain network that supports the Ink! Framework. Side note: Ink! is a Rust-based eDSL (Embedded Domain-Specific Language) and a framework for writing smart contracts in the Rust programming language for the Ethereum-compatible Web3 blockchain Parity Substrate. It provides a higher level of abstraction than writing smart contracts in Solidity or Vyper and allows developers to write smart contracts in Rust without having to deal with the low-level details of the blockchain. The framework provides features such as a gas-efficient storage model, event management, and contract upgrading capabilities. It also includes a set of built-in testing tools, such as a simulator and an in-memory testing environment, which make it easier to test and deploy smart contracts.

 

  • mod nft { – this section of code is a Rust module named nft that defines the NFT contract using the Ink! framework. Here’s a brief explanation of what each line does:
    • #[contract] is a macro attribute that is used to annotate the Rust module nft as an Ink! contract module. This indicates to the Ink! compiler that the module is a smart contract that should be compiled and deployed to a blockchain network.
    • use ink_prelude::vec::Vec; imports the Vec data structure from the Ink! prelude, which is a set of common Rust and Ink! utilities and data types.
    • use ink_storage::{ … }; imports the HashMap data structure and the PackedLayout and SpreadLayout traits from the ink_storage crate. These are used to define and manage the storage of the NFT contract’s data on the blockchain.
    • HashMap is a data structure that allows for efficient key-value mapping and retrieval. In this case, it will be used to store the owner of each NFT token.
    • PackedLayout and SpreadLayout are traits that are used to define how data structures should be serialized and deserialized in Ink! smart contracts. These traits ensure that the contract’s storage is properly laid out and managed on the blockchain.

 

  • pub struct Token { – this code defines a struct named Token that represents a unique NFT token. It has one field named owner (of type AccountId), which represents the account that owns the token. The struct also includes some Rust procedural macros, such as scale::Encode, scale::Decode, SpreadLayout, and PackedLayout. These are used to automatically generate serialization and deserialization code, as well as implement some layout optimization strategies for the storage of the struct’s data. Additionally, the #[cfg_attr(feature = “ink-generate-abi”, derive(type_metadata::Metadata))] attribute is used to generate metadata for the struct, which is used to automatically generate an ABI for the contract. In the context of smart contracts, an ABI (or Application Binary Interface) defines how to interact with a contract at the low-level binary interface level. It provides a standardized way for different applications or components to communicate with the smart contract. An ABI defines the methods or functions that can be called on a contract, their signatures, and the input/output parameters for each method. It is used by clients or other smart contracts to interact with the functions of the contract, such as transferring tokens or querying the state of the contract.

 

  • pub struct NFT { – this code defines an NFT contract using the Ink! framework. By defining the NFT struct with the #[ink(storage)] attribute, Ink! generates the necessary code to store the fields of the struct in the smart contract’s storage. This allows the smart contract to keep track of the state of the NFT contract and persist it across transactions. The NFT contract is defined as a struct NFT and has three fields:
    • tokens: A hash map that maps each token ID to its corresponding Token struct. This is used to keep track of all the NFT tokens in existence.
    • balances: A hash map that maps each account ID to the number of NFT tokens that it owns. This is used to keep track of the NFT tokens owned by each account.
    • total_supply: The total number of NFT tokens that have been minted.

 

  • impl NFT { – This code defines an implementation block for the NFT contract which contains the new function. The new function is marked with the #[ink(constructor)] attribute, which means that this function is the constructor for the contract. The new function initializes a new instance of the NFT contract by creating an empty HashMap for tokens and balances and setting total_supply to zero. The Self keyword is used to refer to the current struct instance being created, and the code within the curly braces is used to define the struct fields and their initial values. Overall, this constructor sets up the basic data structures needed for the NFT contract to function with empty balances and token maps.

 

  • pub fn mint(&mut self, to: AccountId) { – this code defines a new message mint which creates a new NFT token and assigns it to a specified owner. Inside the function, a new token_id is generated by assigning the current value of self.total_supply to it. self.total_supply is the total number of NFT tokens created so far. By assigning it to token_id, a unique identifier is created for the new NFT token. After the new token_id is generated, a new instance of Token struct is created, with the owner set to the address specified in the to parameter. This Token instance is then added to the tokens map with the token_id as the key. Finally, the owner’s balance is incremented by 1 in the balances map, and the total_supply variable is incremented by 1 to reflect the creation of the new NFT token.

 

  • let token = Token { owner: to }; – in the Rust NFT smart contract example above, this code creates a new Token struct and assigns the specified to account ID as the owner of the token. The Token struct was defined earlier in the code and has only one field – owner (of type AccountId). The owner field represents the account ID of the user who owns the NFT token.

 

  • self.tokens.insert(token_id, token); – The line of code adds a new Token to the tokens map. The tokens map stores all the NFT tokens created in the contract. The key is the TokenId, and the value is the Token struct. In other words, this adds a new NFT token to the contract, with the specified to account as the owner, and assigns a new unique token ID to the token.

 

  • *self.balances.entry(to).or_insert(0) += 1; – this line of code increases the balance of the to account by 1. Here’s how it works:
    • self.balances is a HashMap that maps each account ID to its NFT token balance.
    • self.balances.entry(to) returns a mutable reference to the to account’s balance in the HashMap.
    • If to doesn’t have a balance in the HashMap yet, self.balances.entry(to) inserts a new key-value pair with to as the key and a default value of 0 as the value.
    • The or_insert(0) method returns a mutable reference to the value for the key if it exists or inserts the key-value pair with the given default value if it doesn’t exist (in this case, 0).
    • The * dereferences the mutable reference returned by self.balances.entry(to).or_insert(0). Side note: Dereferencing is the process of accessing the value referred to by a reference (or a pointer) in a programming language. A reference is a type that refers to another value, whereas a pointer is a variable that holds the memory address of another value. Dereferencing allows a program to obtain the actual value of the referenced memory address rather than the memory address itself. This is often necessary when working with complex data structures like arrays, linked lists, and objects, where data is stored in memory in a non-contiguous manner. In many programming languages, the dereference operator is the asterisk (*) symbol.
    • The += 1 operator increments the value pointed to by the mutable reference by 1.

 

  • self.total_supply += 1; – this line of code increments the total_supply variable of the NFT struct by 1 after a new NFT token is minted. The total_supply variable keeps track of the total number of NFT tokens that have been minted in the contract.

 

  • pub fn owner_of(&self, token_id: TokenId) -> Option<AccountId> { – this code defines a public function called owner_of in the NFT contract. The function takes a token_id parameter (of type TokenId) and returns an optional AccountId. The function looks up the token_id in the tokens map to find the corresponding Token struct. It then returns the owner field of the Token struct. If the token_id is not found in the tokens map, the function returns None. This function allows users to look up the owner of a specific NFT token, which is a fundamental feature of any NFT system.

 

  • pub fn balance_of(&self, owner: AccountId) -> u64 { – the balance_of function is an Ink! message that allows a client to query the number of NFT tokens owned by a specific account. It takes an owner argument (of type AccountId) which represents the account whose balance is to be queried. The implementation of this function uses the balances hash map to retrieve the balance of the owner. The get method of the balances map is called with the owner as the key. If the owner has any balance, the get method returns Some(&u64), where the &u64 is a reference to the balance value. The unwrap_or method is then called on the Option<&u64> returned by get. If the Option is None, the unwrap_or method returns a default value of 0, which is then dereferenced using the * operator and returned as the function output. In summary, the balance_of function returns the balance of a given account, which is stored in the balances hash map, and returns 0 if the account has no balance.

 

Now that you’ve seen NFT contract examples in Solidity, Vyper, and Rust, let’s take a look at how these programming languages compare.

Solidity vs Vyper vs Rust for NFT Smart Contracts

So, what’s the difference between Solidity vs Vyper vs Rust when it comes to writing NFT smart contracts? Here’s a comparison table that breaks down the similarities and differences.

 

Feature Solidity Vyper Rust
Language paradigm Object-oriented Contract-oriented Object-oriented
Syntax C-like Pythonic C-like
Type system Dynamic typing with explicit type casting Static typing with type inference Static typing with explicit type annotations
Built-in NFT support Yes Yes No
Development Status Mature with a large community and extensive documentation Still in development, with a small community and less material Still in development, with a small community and less material
Contract Testing Tools Truffle, Embark, Hardhat, Remix, Brownie Brownie, Vyper Cargo-contract, Ink!
Execution Environment Ethereum Virtual Machine (EVM) Ethereum Virtual Machine (EVM) Ink! Virtual Machine (IVM)

 

It’s worth noting that while Solidity and Vyper are designed specifically for writing smart contracts on the Ethereum platform, Rust is a general-purpose programming language that can be used to write smart contracts for various blockchain platforms, including Substrate-based chains, Polkadot, and more.

Conclusion

As the use cases for NFTs and smart contracts continue to expand, it’s clear that this technology has the potential to transform many different industries. Whether you’re an artist, musician, athlete, or just someone interested in exploring new forms of digital ownership, there’s never been a better time to get involved in the world of NFTs and smart contracts.

So go ahead and create your own NFT, explore the different NFT marketplaces, and see what unique digital assets are out there!

More Helpful Crypto Content