The non-fungible tokens, or in short NFTs, have one the most influential roles in the crypto and Web3 scene. These are digital assets that can represent digital art, virtual collectibles, assets in games, and more.
Ethereum Foundation introduced the ERC-721 standard in 2017, which helped all the crypto wallets, brokers, and protocols use this feature in a translatable way between any crypto solution.
In this post, we will look more deeply into the ERC-721 standard and explore how it is used in the Solidity programming language for the Ethereum blockchain.
Non-fungible tokens represent unique assets that can't be exchanged, copied, substituted, or divided. Each token has a unique identifier. Therefore, we can see who owns what.
An NFT can represent but is not limited to:
NFTs are like humans - there are no two people the same.
To represent an NFT, the Ethereum Foundation has implemented it with an ERC-721 token standard. This standard describes a table of who owns what. The standardized approach helps crypto wallets, brokers, and auctions to work with NFTs on Ethereum and other EVM blockchains.
The ERC-721 standard was launched in 2017 and authored by William Entriken, Dieter Shirley, Jacob Evans, and Nastassia Sachs. I met William Entriken at one of the NFT conferences.
There are several functions defined in the ERC-721 standard. Let's look at the most important ones separately.
These functions describe ownership rights.
balanceOf(address _owner)
returns a number of NFTS owned by _owner
address;
ownerOf(uint256 _tokenId)
returns an owner who owns a token with the _tokenId
identifier;These functions help to transfer a token.
safeTransferFrom(address _from, address _to, uint256 _tokenId)
transfers the token with the tokenId
identifier from the _from
address to the _to
address checking that recipient is aware of the ERC-721 standard;
safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data)
similar to the function above with the option to send data
to the to _to
in a call, can be useful to send additional data;
approve(address _approved, uint256 _tokenId)
gives rights to the _approved
to transfer the token with the _tokenId
identifier;
getApproved(uint256 _tokenId)
returns the wallet address approved to transfer the token with _tokenId
identifier;
setApprovalForAll(address _operator, bool _approved)
approve or remove approval for all the msg.sender
assets to _operator
;
isApprovedForAll(address _owner, address _operator)
checks if the _operator
address has rights for the _owner
assets.Events help to notify about changes with an NFT. For instance, the web frontend can be updated.
Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)
emits when the token with the _tokenId
identifier is transferred from the _from
to the _to
.
Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId)
emits when approval rights are given to the _approved
to the token with the _tokenId' identifier by the
_owner` who has ownership rights;
ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved)
emits when _owner
gives or removes rights to the _operator
for their assets.
A commonly used ERC-721 extension describes metadata in a JSON format. It represents information about a token, like a name, symbol, and token URI. Using this extension, crypto wallets can ask for the information using three functions:
function name() external view returns (string _name)
returns a descriptive name;
function symbol() external view returns (string _symbol)
returns token symbol;
function tokenURI(uint256 _tokenId) external view returns (string)
returns URL to JSON file that describes the NFT. Usually, this is stored on IPFS.
The token URI points to a JSON file that conforms to the ERC721 Metadata JSON Schema:
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image representing the asset to which this NFT represents. "
}
}
}
If you're unfamiliar with IPFS file storage, we will discuss that in one of the following posts.
The most straightforward way to implement NFTs is to use the OpenZepplin contracts suite that includes the ERC721
and ERC721URIStorage
contracts. It fully conforms to the ERC-721 and metadata extension standards.
Let's build an NFT ticketing smart contract.
First, we must import OpenZepplin ERC-721 contract implementations and conform our smart contract to it.
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract Ticket is ERC721URIStorage {
/// ...
}
At first, we need to define the NFT name and symbol. To do that, we pass the event name and description to smart contract the constructor.
constructor(
string memory eventName,
string memory shortName
) ERC721(eventName, shortName) {
// initializes the NFT storage
}
We can now start minting NFTs because the OpenZepplin hides away all the nitty gritty implementation details.
function createTicket(address visitor, string memory tokenURI) external {
tokenId++;
// mint the NFT and assign to the visitor address
_mint(visitor, tokenId);
// set the token JSON file link that was uploaded to the IPFS
_setTokenURI(tokenId, tokenURI);
console.log(tokenId);
}
All the ERC-721 functions discussed above come for free with the OpenZepplin contracts.
The non-fungible tokens, or NFTs, differ much from regular ERC-20 tokens. They describe uniqueness. Remember what we discussed at the beginning of this post - NFTs are like humans. Or let me call it differently this time - NFTs are like kitties - there are no two kitties alike.
The most commonly used standard that defines NFTs is ERC-721, with metadata extension. It helps to attach metadata to the token, like images which is the most used feature in the crypto world.