What is an ERC-721 Token?
An ERC-721 token is a smart contract on Ethereum that implements the methods and events specified in the EIP-721: Non-Fungible Token Standard. It is designed to be used as a non-fungible token (NFT), meaning the token’s instances (or units) are distinct from one another.
The ERC-721 standard is based on the fungible token standard (ERC-20), so it would make sense to explore the functionality by comparing it with ERC-20.
If youβre not familiar with the ERC-20 standard, you might find it helpful to check my previous article below.
Like ERC-20 tokens, ERC-721 tokens provide the following functionality:
- Transfer tokens from one account to another
- Get the current token counts of an account
- Approve whether a third-party account can transfer tokens
In addition, ERC-721 tokens provide the following functionality, which is unique to NFT:
- Get the owner of the specific token
Weβll look at them one by one in this article.
Overview of a Basic ERC-721 Token
This article will look at the ERC-721 token implementation by OpenZeppelin. We can find the Solidity code in their Github repository. In this article, we use the latest version (v4.4.1) at the time of writing.
_owners state variable
The core of an ERC-721 token is the _owners
private variable, which is a map of the token ID and its owner address.
// Mapping from token ID to owner address mapping(uint256 => address) private _owners;
(Line 29 – 30)
The ERC-721’s non-fungibility is achieved by assigning a unique identifier to each token and keeping the ID and owner information in this variable.
For example, when Alice has the NFT (ID = 1) and Bob has the NFT (ID = 2), the _owners
variable would contain the following entries.
_owners
Token ID | Address |
1 | Alice |
2 | Bob |
So, simply put, when we say we βownβ a certain NFT, it just means that its smart contract keeps the token ID and our address in this variable. The token ID and owner address are added to this variable whenever a new NFT is created (or minted).
The ownerOf()
function looks up this variable and returns a corresponding entry if it finds one.
/** * @dev See {IERC721-ownerOf}. */ function ownerOf(uint256 tokenId) public view virtual override returns (address) { address owner = _owners[tokenId]; require(owner != address(0), "ERC721: owner query for nonexistent token"); return owner; }
(Line 67 – 74)
_balances variable
Similar to ERC-20, ERC-721 has the _balances
private variable that keeps track of the holdings of each account.
// Mapping owner address to token count mapping(address => uint256) private _balances;
(Line 32 – 33)
For example, when Alice and Bob have an NFT each, the variable would contain the entries like below.
_balances
Address | Token count |
Alice | 1 |
Bob | 1 |
The balanceOf()
function looks up this variable and returns a corresponding entry if it finds one.
/** * @dev See {IERC721-balanceOf}. */ function balanceOf(address owner) public view virtual override returns (uint256) { require(owner != address(0), "ERC721: balance query for the zero address"); return _balances[owner]; }
(Line 59 – 65)
Create a new token (minting)
The _mint()
internal function creates (or mints) a new token. It updates the owner’s count in the _balances
variable and adds the owner of the token ID in the _owners
variable.
/** * @dev Mints `tokenId` and transfers it to `to`. * * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible * * Requirements: * * - `tokenId` must not exist. * - `to` cannot be the zero address. * * Emits a {Transfer} event. */ function _mint(address to, uint256 tokenId) internal virtual { require(to != address(0), "ERC721: mint to the zero address"); require(!_exists(tokenId), "ERC721: token already minted"); _beforeTokenTransfer(address(0), to, tokenId); _balances[to] += 1; _owners[tokenId] = to; emit Transfer(address(0), to, tokenId); _afterTokenTransfer(address(0), to, tokenId); }
(Line 268 – 292)
For example, if Alice mints a new NFT, the _owners
and _balances
variables are updated, as shown below.
_owners
Token ID | Address |
1 | Alice |
2 | Bob |
3 | Alice |
_balances
Address | Token count |
Alice | 2 |
Bob | 1 |
There are two internal functions called _safeMint()
(with and without _data
parameter), which additionally verify that the new owner (the to address) can receive ERC-721 tokens.
/** * @dev Safely mints `tokenId` and transfers it to `to`. * * Requirements: * * - `tokenId` must not exist. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function _safeMint(address to, uint256 tokenId) internal virtual { _safeMint(to, tokenId, ""); } /** * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. */ function _safeMint( address to, uint256 tokenId, bytes memory _data ) internal virtual { _mint(to, tokenId); require( _checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer" ); }
Transfer workflow
Like ERC-20, ERC-721 has two workflows to transfer tokens from one address to another. The first workflow is used when the owner transfers tokens, and it is implemented in the _transfer() internal function, as shown below.
/** * @dev Transfers `tokenId` from `from` to `to`. * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. * * Requirements: * * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * * Emits a {Transfer} event. */ function _transfer( address from, address to, uint256 tokenId ) internal virtual { require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); require(to != address(0), "ERC721: transfer to the zero address"); _beforeTokenTransfer(from, to, tokenId); // Clear approvals from the previous owner _approve(address(0), tokenId); _balances[from] -= 1; _balances[to] += 1; _owners[tokenId] = to; emit Transfer(from, to, tokenId); _afterTokenTransfer(from, to, tokenId); }
(Line 320 – 351)
The core part of the function is to update the _owners
and _balances
variables.
For example, when Alice has the NFTs (ID = 1 and 3) and Bob has the NFT (ID = 2), the _owners
and _balances
variables contain the following entries.
_owners
Token ID | Address |
1 | Alice |
2 | Bob |
3 | Alice |
_balances
Address | Token count |
Alice | 2 |
Bob | 1 |
Then, when Alice transfers the NFT (ID 3) to Charlie, the address value for Token ID 3 is updated from Alice to Charlie in the _owners
variable. Their counts are also updated in the _balances
variable accordingly.
_owners
Token ID | Address |
1 | Alice |
2 | Bob |
3 | Charlie |
_balances
Address | Token count |
Alice | 1 |
Bob | 1 |
Charlie | 1 |
Although this function performs the actual transfer operations, it is not exposed to the users because it is internal. The transferFrom()
function (and its variants) is used for the transfer workflow instead, which we will explore in the next section.
TransferFrom workflow
The second workflow to transfer tokens is used when a third party transfers tokens using the transferFrom()
function on behalf of the owner.
/** * @dev See {IERC721-transferFrom}. */ function transferFrom( address from, address to, uint256 tokenId ) public virtual override { //solhint-disable-next-line max-line-length require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); _transfer(from, to, tokenId); }
(Line 147 – 159)
The difference is that the transferFrom()
function also calls the _isApprovedOrOwner()
internal function, which is implemented as shown below.
/** * @dev Returns whether `spender` is allowed to manage `tokenId`. * * Requirements: * * - `tokenId` must exist. */ function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { require(_exists(tokenId), "ERC721: operator query for nonexistent token"); address owner = ERC721.ownerOf(tokenId); return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); }
(Line 225 – 236)
In other words, the transferFrom()
function is checking if any of the following conditions is true or not:
- The
msg.sender
(who is calling this function) is the owner. - The
msg.sender
has been approved to transfer the token on behalf of the owner. - The owner has approved the
msg.sender
to transfer all their tokens.
The first condition is true when the owner calls the transferFrom()
function. It means that we can use the transferFrom()
function in the transfer workflow we looked at in the previous section.
The getApproved()
function is used in the second condition, which simply looks up the _tokenApprovals
private variable.
/** * @dev See {IERC721-getApproved}. */ function getApproved(uint256 tokenId) public view virtual override returns (address) { require(_exists(tokenId), "ERC721: approved query for nonexistent token"); return _tokenApprovals[tokenId]; }
(Line 124 – 131)
The _tokenApprovals
variable is a map of a token ID and an address.
// Mapping from token ID to approved address mapping(uint256 => address) private _tokenApprovals;
(Line 35 – 36)
It is updated by the approve()
function via the _approve()
internal function.
/** * @dev See {IERC721-approve}. */ function approve(address to, uint256 tokenId) public virtual override { address owner = ERC721.ownerOf(tokenId); require(to != owner, "ERC721: approval to current owner"); require( _msgSender() == owner || isApprovedForAll(owner, _msgSender()), "ERC721: approve caller is not owner nor approved for all" ); _approve(to, tokenId); }
(Line 109 – 122)
/** * @dev Approve `to` to operate on `tokenId` * * Emits a {Approval} event. */ function _approve(address to, uint256 tokenId) internal virtual { _tokenApprovals[tokenId] = to; emit Approval(ERC721.ownerOf(tokenId), to, tokenId); }
(Line 353 – 361)
So, the owner needs to call the approve()
function first for the second condition to be true. It will add the spender address and the token ID to the the _tokenApprovals
variable, so the _isApprovedOrOwner()
internal function will return true.
In the third condition, the isApprovedForAll()
function checks if the owner has given msg.sender
a blanket approval to send their tokens.
/** * @dev See {IERC721-isApprovedForAll}. */ function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { return _operatorApprovals[owner][operator]; }
(Line 140 – 145)
It looks up the _operatorApprovals
private variable, a map of maps.
// Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals;
It is updated by the setApprovalForAll()
function via the _setApprovalForAll()
internal function.
/** * @dev See {IERC721-setApprovalForAll}. */ function setApprovalForAll(address operator, bool approved) public virtual override { _setApprovalForAll(_msgSender(), operator, approved); }
(Line 133 – 138)
/** * @dev Approve `operator` to operate on all of `owner` tokens * * Emits a {ApprovalForAll} event. */ function _setApprovalForAll( address owner, address operator, bool approved ) internal virtual { require(owner != operator, "ERC721: approve to caller"); _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); }
(Line 363 – 376)
So, for the third condition to be true, the owner would need to call the setApprovalForAll()
function to give the msg.sender
approval in advance.
If any of these three conditions is true, the transferFrom()
function calls the _transfer()
internal function, and it performs the transfer, as explained in the previous section.
Safe Transfer
In addition to the transferFrom()
function, the safeTransferFrom()
functions (there are two variations, with and without the _data
parameter) are available. They perform extra checks to see if the recipient can receive ERC-721 tokens or not, via the _safeTransfer()
internal function.
/** * @dev See {IERC721-safeTransferFrom}. */ function safeTransferFrom( address from, address to, uint256 tokenId ) public virtual override { safeTransferFrom(from, to, tokenId, ""); } /** * @dev See {IERC721-safeTransferFrom}. */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) public virtual override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); _safeTransfer(from, to, tokenId, _data); }
(Line 161 – 183)
ERC-721 Metadata Extension
So far, weβve looked at the basic functionality of ERC-721, but the token’s non-fungibility is only manifested in the token IDs. Although we can see that each token is unique by looking at the ID, its use case is very limited in the real world if unique data is only the IDs.
The ERC-721 specification defines the optional metadata extension (ERC721Metadata), where we can implement the tokenURI()
function to give each token a unique URI (Uniform Resource Identifier). A typical form of URI is URL (such as an HTTP address).
Another popular form of URI for NFT is the content identifier (CID) in the IPFS network because we cannot modify the data without changing the CID. We will not go into details about IPFS, but you can read more in the IPFS documentation.Β
The OpenZeppelin library includes the ERC721URIStorage extension, which provides a basic implementation of the tokenURI()
function. In real-world use cases, each project will need to override this function depending on the requirements for the NFT.
ERC-721 Token with Brownie
This section will create an ERC-721 token using an example in the OpenZeppelin documentation and try some functions.
Set up the Brownie environment
If you havenβt installed Brownie, you can install it by following the tutorial below:
After installing Brownie, go to a new directory and run the brownie init
command.
[~/erc721_test]$ brownie init Brownie v1.17.1 - Python development framework for Ethereum SUCCESS: A new Brownie project has been initialized at /Users/mikio/erc721_test
Then install the OpenZeppelin library.Β
[~/erc721_test]$ brownie pm install OpenZeppelin/openzeppelin-contracts@4.4.1 Brownie v1.17.1 - Python development framework for Ethereum 1.71MiB [00:00, 11.2MiB/s] New compatible solc version available: 0.8.10 Compiling contracts... Solc version: 0.8.10 Optimizer: Enabled Runs: 200 EVM Version: Istanbul Generating build data... - AccessControl - AccessControlEnumerable ... ... ... SUCCESS: Package 'OpenZeppelin/openzeppelin-contracts@4.4.1' has been installed
Create the configuration file brownie-config.yaml
in the current directory and add the following content.
brownie-config.yaml
dependencies: - OpenZeppelin/openzeppelin-contracts@4.4.1 compiler: solc: remappings: - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.4.1"
Set up accounts
In this example, we will use the accounts alice, bob, charlie, which we created in my previous article. If you donβt have these accounts, you can create them in Brownie, as shown below. When prompted, type a password. Remember it, as we need it in the later steps.
[~/erc721_test]$ brownie accounts generate alice Brownie v1.17.1 - Python development framework for Ethereum Generating a new private key... mnemonic: 'xxxx xxxx ...' Enter the password to encrypt this account with: SUCCESS: A new account '0x32d506BccD1367e779c53A2887070C611cC40Aa8' has been generated with the id 'alice' [~/erc721_test]$ brownie accounts generate bob Brownie v1.17.1 - Python development framework for Ethereum Generating a new private key... mnemonic: 'yyyy yyyy ....' Enter the password to encrypt this account with: SUCCESS: A new account '0x80b8EFE085A0671931606eB1cF1314cf4dcc9062' has been generated with the id 'bob' [~/erc721_test]$ brownie accounts generate charlie Brownie v1.17.1 - Python development framework for Ethereum Generating a new private key... mnemonic: 'zzzz zzzz ....' Enter the password to encrypt this account with: SUCCESS: A new account '0xCa31275A8eF4a16cfaC4f7317aBeeDe31F73558D' has been generated with the id 'charlie'
Deploy smart contract
We use the following example for demonstration purposes, which I took from the OpenZeppelin documentation and slightly modified it. Save this file as GameItem.sol
in the contracts directory.
contracts/GameItem.sol
// contracts/GameItem.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract GameItem is ERC721URIStorage { using Counters for Counters.Counter; Counters.Counter private _tokenIds; constructor() ERC721("GameItem", "ITM") {} function awardItem(address player, string memory tokenURI) public returns (uint256) { _tokenIds.increment(); uint256 newItemId = _tokenIds.current(); _mint(player, newItemId); _setTokenURI(newItemId, tokenURI); return newItemId; } }
The smart contract inherits from ERC721URIStorage
, which again inherits from ERC721
. Both of them are provided in the OpenZeppelin library.Β
The awardItem()
function creates a new NFT. It generates a token ID, creates a new token using the _mint()
function with the token ID, and sets the specified URI to the token metadata using the _setTokenURI()
function. In this example, anyone can call this function and mint a new NFT.
Letβs deploy this smart contract in the local development environment to interact with it using the Brownie console.Β
[~/erc721_test]$ brownie console Brownie v1.17.1 - Python development framework for Ethereum New compatible solc version available: 0.8.10 Compiling contracts... Solc version: 0.8.10 Optimizer: Enabled Runs: 200 EVM Version: Istanbul Generating build data... - OpenZeppelin/openzeppelin-contracts@4.4.1/ERC721 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC721 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC721Receiver - OpenZeppelin/openzeppelin-contracts@4.4.1/ERC721URIStorage - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC721Metadata - OpenZeppelin/openzeppelin-contracts@4.4.1/Address - OpenZeppelin/openzeppelin-contracts@4.4.1/Context - OpenZeppelin/openzeppelin-contracts@4.4.1/Counters - OpenZeppelin/openzeppelin-contracts@4.4.1/Strings - OpenZeppelin/openzeppelin-contracts@4.4.1/ERC165 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC165 - GameItem Erc721TestProject is the active project. Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'... Brownie environment is ready. >>>
Then, load the accounts.
>>> alice = accounts.load('alice') Enter password for "alice": >>> bob = accounts.load('bob') Enter password for "bob": >>> charlie = accounts.load('charlie') Enter password for "charlie":
Now, letβs deploy the smart contract using the alice account.
>>> gameItem = GameItem.deploy({'from': alice}) Transaction sent: 0xc973d95cd3ee93fccbab0536778a450252c82932653e595a32af4b2d532829df Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0 GameItem.constructor confirmed Block: 1 Gas used: 1367483 (11.40%) GameItem deployed at: 0xa0433E13f093739643D7d8bb632f5fa87942ae24
Create NFTs
Letβs create a new NFT and award it to Alice. The URI is just a random URL in this example.
>>> tx1 = gameItem.awardItem(alice, 'http://example.com/item1.json') Transaction sent: 0x44db1ab578dfc8cea532a06a1d0408c028e5a34788337622aa68c06ca3188da6 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1 GameItem.awardItem confirmed Block: 2 Gas used: 111734 (0.93%)
The token ID is 1 in the transaction details.Β
>>> tx1.info() Transaction was Mined --------------------- Tx Hash: 0x44db1ab578dfc8cea532a06a1d0408c028e5a34788337622aa68c06ca3188da6 From: 0x32d506BccD1367e779c53A2887070C611cC40Aa8 To: 0xa0433E13f093739643D7d8bb632f5fa87942ae24 Value: 0 Function: GameItem.awardItem Block: 2 Gas Used: 111734 / 12000000 (0.9%) Events In This Transaction -------------------------- βββ GameItem (0xa0433E13f093739643D7d8bb632f5fa87942ae24) βββ Transfer βββ from: 0x0000000000000000000000000000000000000000 βββ to: 0x32d506BccD1367e779c53A2887070C611cC40Aa8 βββ tokenId: 1
Now, we can run the ownerOf()
function to verify that Alice is indeed the owner of the token 1. We can also run the balanceOf()
function to check that Alice owns one token.
>>> gameItem.ownerOf(1) == alice True >>> gameItem.balanceOf(alice) 1
We can also run the tokenURI()
function to find the token URI.Β
>>> gameItem.tokenURI(1) 'http://example.com/item1.json'
We can repeat the same process and award a new item to Bob.
>>> tx2 = gameItem.awardItem(bob, 'http://example.com/item2.json') Transaction sent: 0xafb89196d99b50f265490f1ae077959488da8de369c6dec303a901e76b7e84f0 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 2 GameItem.awardItem confirmed Block: 3 Gas used: 96734 (0.81%) >>> tx2.info() Transaction was Mined --------------------- Tx Hash: 0xafb89196d99b50f265490f1ae077959488da8de369c6dec303a901e76b7e84f0 From: 0x32d506BccD1367e779c53A2887070C611cC40Aa8 To: 0xa0433E13f093739643D7d8bb632f5fa87942ae24 Value: 0 Function: GameItem.awardItem Block: 3 Gas Used: 96734 / 12000000 (0.8%) Events In This Transaction -------------------------- βββ GameItem (0xa0433E13f093739643D7d8bb632f5fa87942ae24) βββ Transfer βββ from: 0x0000000000000000000000000000000000000000 βββ to: 0x80b8EFE085A0671931606eB1cF1314cf4dcc9062 βββ tokenId: 2 >>> gameItem.ownerOf(2) == bob True >>> gameItem.balanceOf(bob) 1 >>> gameItem.tokenURI(2) 'http://example.com/item2.json'
Transfer an NFT
We can use the transferFrom()
function to transfer an NFT.
Firstly, letβs check that Alice can transfer the NFT 1 to Charlie.
>>> tx3 = gameItem.transferFrom(alice, charlie, 1, {'from': alice}) Transaction sent: 0x4b2977d149f81867351457ff0ed7afcf701134e5eab7dbf2e9a4246fde2dff1b Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 3 GameItem.transferFrom confirmed Block: 4 Gas used: 49948 (0.42%) >>> tx3.info() Transaction was Mined --------------------- Tx Hash: 0x4b2977d149f81867351457ff0ed7afcf701134e5eab7dbf2e9a4246fde2dff1b From: 0x32d506BccD1367e779c53A2887070C611cC40Aa8 To: 0xa0433E13f093739643D7d8bb632f5fa87942ae24 Value: 0 Function: GameItem.transferFrom Block: 4 Gas Used: 49948 / 12000000 (0.4%) Events In This Transaction -------------------------- βββ GameItem (0xa0433E13f093739643D7d8bb632f5fa87942ae24) βββ Approval β βββ owner: 0x32d506BccD1367e779c53A2887070C611cC40Aa8 β βββ approved: 0x0000000000000000000000000000000000000000 β βββ tokenId: 1 βββ Transfer βββ from: 0x32d506BccD1367e779c53A2887070C611cC40Aa8 βββ to: 0xCa31275A8eF4a16cfaC4f7317aBeeDe31F73558D βββ tokenId: 1
We can see two events, Approval
and Transfer
. The first event comes from the _approve()
internal function and the second comes from the _transfer(
) internal function.
Now, we can confirm that the token 1 owner is Charlie, not Alice.
>>> gameItem.ownerOf(1) == alice False >>> gameItem.ownerOf(1) == charlie True
Next, letβs see if Alice can transfer Bobβs token to Charlie.
>>> gameItem.transferFrom(bob, charlie, 2, {'from': alice}) Transaction sent: 0x26b0f19314c9962970580cf20e0e79059b2e70654676212ea3e2057ebef66f32 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 4 GameItem.transferFrom confirmed (ERC721: transfer caller is not owner nor approved) Block: 5 Gas used: 27476 (0.23%) <Transaction '0x26b0f19314c9962970580cf20e0e79059b2e70654676212ea3e2057ebef66f32'>
This call fails ("transfer caller is not the owner nor approved"
). It is correct because Alice is not the owner of the token 2, and Bob hasnβt approved Alice.
Assuming that Bob is happy for Alice to transfer his token, he runs the approve()
function.
>>> gameItem.approve(alice, 2, {'from': bob}) Transaction sent: 0xd1dbaf920fa26b0af79055e5ed4f68ba5a8a55dac1cdece7f61c86c627238089 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0 GameItem.approve confirmed Block: 6 Gas used: 46822 (0.39%) <Transaction '0xd1dbaf920fa26b0af79055e5ed4f68ba5a8a55dac1cdece7f61c86c627238089'>
Alternatively, Bob can run the setApprovalForAll()
function if he wants to give Alice his approval to transfer all his tokens.
Either way, Alice can now transfer Bobβs token 2 to Charlie.
>>> gameItem.transferFrom(bob, charlie, 2, {'from': alice}) Transaction sent: 0xcace7e5b7f6317861046e6b2c2e84c2376fac9f36b10e552545f60337f76ab1c Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 5 GameItem.transferFrom confirmed Block: 7 Gas used: 28026 (0.23%) <Transaction '0xcace7e5b7f6317861046e6b2c2e84c2376fac9f36b10e552545f60337f76ab1c'>
Now, we can see that Charlie is the token 2 owner.
>>> gameItem.ownerOf(2) == charlie True
Summary
In this article, we looked at the basic functionality of the ERC-721 token.
An ERC-721 token is a smart contract on Ethereum that implements the methods and events specified in the EIP-721: Non-Fungible Token Standard. It is designed to be used as a non-fungible token (NFT), meaning the token’s instances (or units) are distinct from one another.Β
The non-fungibility is achieved by assigning a unique identifier to each token, and then, the ID and owner are kept in a map variable.
The ERC-721 standard defines the functionality of the ERC-721 token, such as creating and transferring tokens. It also specifies the optional functionality, such as the metadata extension, where we can implement a function to give each token a unique URI (Uniform Resource Identifier). This part will vary significantly depending on the requirements for the NFT project.
I hope this overview has been helpful to understand how ERC-721 tokens work.
You can find more in the following resources.
- EIP-721: Non-Fungible Token Standard
- ERC-721 NON-FUNGIBLE TOKEN STANDARD (ethereum.org)
- ERC 721 (OpenZeppelin docs)
- Mint an NFT with IPFS (IPFS docs)
Learn Solidity Course
Solidity is the programming language of the future.
It gives you the rare and sought-after superpower to program against the “Internet Computer”, i.e., against decentralized Blockchains such as Ethereum, Binance Smart Chain, Ethereum Classic, Tron, and Avalanche – to mention just a few Blockchain infrastructures that support Solidity.
In particular, Solidity allows you to create smart contracts, i.e., pieces of code that automatically execute on specific conditions in a completely decentralized environment. For example, smart contracts empower you to create your own decentralized autonomous organizations (DAOs) that run on Blockchains without being subject to centralized control.
NFTs, DeFi, DAOs, and Blockchain-based games are all based on smart contracts.
This course is a simple, low-friction introduction to creating your first smart contract using the Remix IDE on the Ethereum testnet – without fluff, significant upfront costs to purchase ETH, or unnecessary complexity.