# ERC-721 Tokens ## Theory ### What are ERC-721 tokens? - ERC-721 tokens are Non-Fungible Tokens (NFTs). - Non-Fungible Tokens -> Each token is unique and not interchangeable. No two tokens have the same identity or value. The smart contract that defines the functionality of these tokens follow the ERC-721 standard, and that's why they’re called ERC-721 tokens. - The contract of ERC-721 tokens follows the ERC-721 token standard. - This is not like regular currency. Each token is like a unique collector’s item. For example, a digital artwork, or a ticket, each one has different attributes and value. - Check out the official ERC-721 standard -> https://eips.ethereum.org/EIPS/eip-721 - Check out the ERC-721 minimal implementation -> https://github.com/nikillxh/token-frenzy/blob/master/ERC721min.sol - Check out Openzeppelin's implementation of ERC-721 -> https://docs.openzeppelin.com/contracts/5.x/api/token/erc721 -> https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721 ### Properties of ERC-721 contract - Each token has a unique ID (`uint256`) → No two tokens in the same contract share a same token ID. - There’s a mapping of token ID to owner address, which keeps track of who owns which NFT. - Contract can have a name or symbol to describe the collection but the individual tokens don't share a common name or symbol as each token is unique. - Each token is unique: metadata, appearance or even utility can differ even within the same contract. - Unlike ERC-20, decimals don't apply here since ERC-721 tokens are not divisible. ### Functionalities of ERC-721 (Minimal) - Excluding the Safe Transfer Functionality. - **Mint a Token** -> An address can mint any token for any address, given that the NFT's token ID is not owned (i.e. belongs to `address(0)`). This is how NFTs are brought into existence in a contract. - **Owner of a Token** -> A function to get the owner address of an NFT token using its ID. Returns `address` of the owner. - **Balance of an address** -> Total token count number of an address can be checked using this function. Returns the `uint256` value. - **Transfer tokens** -> This function can be used to transfer the token from a source address to a destination address. It can be executed by source address's owner, operator or only by the account which got approved for that token ID. - **Approval for all tokens of an owner** -> There are two types of Approvals in ERC-721 tokens, unlike one in ERC-20. This function allows the owner of an address to give approval over all its tokens to an operator address. Now, the operator address has some delegrated authority over the tokens of the address it got approval for. Hence, the operator can transfer the tokens of the address it got approval for. This approval is connected to the owner instead of the token itself. An address can have multiple operators. - **Approval of a single token** -> This function allows the owner of an address or operators of that address to give access of a single token to another address. This approval is connected to the NFT token's ID instead of owner's address. A token can have only one approved address, directly connected to it. - **Check Approval connected to address** -> If an address is allowed approval to all the tokens of another address, can be checked using the two addresses as input. It will return a `bool` value. - **Check the approved address of an NFT** -> This function gets the approved `address` of that token using its `uint256` token ID as the input. ### Variables of ERC-721 - **OwnerOf** -> Stores token ID and its owner address. It maps from the `uint56` token ID to the `address` which owns it. Updates on every transfer transaction. - **balanceOf** -> Stores address & its token balance. It maps from an `address` to its `uint256` token balance. Updates on every transfer transaction. - **isApprovedForAll** -> Stores the approval status for an address by an address. It maps from `address` of an owner to another operator `address` & stores its approval status in `bool`. Updates when approval is modified. - **getApproved** -> Stores approved address for a token. Default approved address is `address(0)`. It maps from `uint256` token ID to an `address` which is approved. A token can have only one approved address. It gets updated on a transfer transaction. ### Events of ERC-721 - **Transfer** -> Every transaction must be logged. This event logs the `from` address, `to` address and the `uint256` id of token which was transferred. - **ApproveForAll** -> Every change in approval of an address to another address must be logged. This event logs the `owner` address, `operator` address & the `bool` approval status. - **Approved** -> Every change in approval of a token to an address must be logged. This event logs the owner `address` of the token, spender (approved) `address` & the `uint256` token id. Removing approval means spender address becomes `address(0)`. ### Example - The contract is deployed. - Addresses: `Addr1`, `Addr2`, `Addr3`, `Addr4` - Addr1 mints Token1 for its address. - Addr1 mints Token2 for its address. - Addr1 mints Token4 for its address. - Addr2 mints Token3 for its address. - Owner of Token1 is Addr1 & owner of Token3 is Addr2. - Balance of Addr1 is 3, Addr2 is 1 & Addr3 is 0. - Addr1 transfers Token1 to Addr3. - Balance of Addr1 is 2, Addr2 is 1 & Addr3 is 1. - Addr1 gives Approval for all tokens to Addr4. - Addr4 gives Approval of Token2 to Addr2. - Addr4 transfers Token2 to Addr3. - Addr2 loses Approval of Token2. - Owner of Token2 is Addr3 - Balance of Addr1 is 1, Addr2 is 1, Addr3 is 2, Addr4 is 0. ## Practical ### Let's code! Open [Remix](https://remix.ethereum.org) in a parallel tab & get started! ### Structure ![image](https://hackmd.io/_uploads/HkwKq__Xxl.png) ### The Interface - ERC-20 is often used directly as a contract because it’s simple and fungible. - ERC-721 is usually used via interfaces because each token is non-fungible and contracts tend to customize it heavily. - Hence, the functions are external. Interface functions are not public. - This is a minimal form of ERC-721, excluding the safe transfer functionality. ```solidity interface IERC721 { // Read-only functions function ownerOf(uint256 id) external view returns (address); function balanceOf(address owner) external view returns (uint256); function isApprovedForAll(address owner, address operator) external view returns (bool); function getApproved(uint256 id) external view returns (address); // State-changing functions function mint(address owner, uint256 id) external; function setApprovalForAll(address operator, bool approved) external payable; function approve(address approved, uint256 id) external payable; function transferFrom(address from, address to, uint256 id) external payable; // Events // event Transfer(address indexed from, address indexed to, uint256 indexed id); // event ApproveForAll(address indexed owner, address indexed operator, bool approved); // event Approved(address indexed owner, address indexed spender, uint256 id); } ``` ### ERC-721 Contract: `contract ERC721 is IERC721{}` #### Variables - **ownerOf** -> Maps `uint256` token ID to it owner `address`. It's public as internal functions need to access it. Also, we don't need to create a getter function as Remix creates it automatically for all variables. - **balanceOf** -> Maps owner `address` to its `uint256` balance. - **isApprovedForAll** -> Maps owner `address` to operator `address` which maps to approval denoted by `bool`. - **getApproved** -> Maps `uint256` token id to the approved `address`. ```solidity // Token -> Owner Mapping mapping(uint256 token_ID => address owner) public ownerOf; // NFT balances of an address mapping(address owner => uint256 balance) public balanceOf; // Approved for all mapping(address owner => mapping (address operator => bool)) public isApprovedForAll; // Approved address for an NFT mapping(uint256 token_ID => address approved) public getApproved; ``` #### Events - **Transfer** -> Whenever a token transfer happens, it logs the source address `from`, destination address `to` & the `uint256 id` of token. - **ApproveForAll** -> Whenever an owner address makes changes in approval of all tokens to another address, it logs the `owner address`, `operator address` & the approval status `bool`. - **Approved** -> Whenever a change in approval of a token happens for an address, it logs the `owner address`, `spender address` & the `uint256 id` of token. ```solidity // Logs Transfer events event Transfer(address indexed from, address indexed to, uint256 indexed id); // Logs changes in operator's approval for an owner's all NFT's event ApproveForAll(address indexed owner, address indexed operator, bool approved); // Logs changes in single NFT operator's approval event Approved(address indexed owner, address indexed spender, uint256 id); ``` #### Functions - **mint(address receiver, uint256 id)** -> Requires the current owner of the NFT be `address(0)` which means it's not owned by any address. -> Assign the first owner. -> Update balance of the owner. -> Emit the transfer to log the transaction. ```solidity // Mint NFTs to an address function mint(address receiver, uint256 id) external{ require(ownerOf[id] == address(0), "Already Minted"); ownerOf[id] = receiver; balanceOf[receiver] += 1; emit Transfer(address(0), receiver, id); } ``` - **setApprovalForAll(address operator, bool approved)** -> This function is payable as it has the ability to take ETH or some other currency for execution. -> Set the approval for an address to make it operator or remove operator access. -> Requires the operator address to be other than `address(0)`. -> `isApprovedForAll` variable updated -> Emitted and logged using `ApproveForAll` event. ```solidity // Set Approval For an owner's all NFT's function setApprovalForAll(address operator, bool approved) external payable{ require(operator != address(0), "Invalid operator"); isApprovedForAll[msg.sender][operator] = approved; emit ApproveForAll(msg.sender, operator, approved); } ``` - **approve(address approved, uint256 id)** -> This function is payable. -> Requires the `msg.sender` to be owner of the NFT or an operator who is already approved by the owner. -> Update the `getApproved` variable. -> Emit the owner of the NFT, the address approved & id of the NFT to be logged. ```solidity // Set Approval for a single NFT function approve(address approved, uint256 id) external payable{ require(msg.sender == ownerOf[id] || isApprovedForAll[ownerOf[id]][msg.sender], "Not authorized"); getApproved[id] = approved; emit Approved(ownerOf[id], approved, id); } ``` - **transferFrom(address from, address to, uint256 id)** -> This function is payable. -> Requires the code execution to be done by owner of NFT or operator of the owner's NFTs or operator of that single NFT. -> Update owner, balances of previous and new owner. -> Remove approval of operator of that single NFT if exists (approval can be provided by the new owner if required) & emit the event. -> Emit the addresses of previous owner, new owner and the ID of NFT. ```solidity // NFT Transfer Function function transferFrom(address from, address to, uint256 id) external payable{ require(ownerOf[id] == msg.sender || isApprovedForAll[from][msg.sender] || getApproved[id] == msg.sender, "Not authorized"); ownerOf[id] = to; balanceOf[from] -= 1; balanceOf[to] += 1; if (getApproved[id] != address(0)) { delete getApproved[id]; emit Approved(ownerOf[id], address(0), id); } emit Transfer(from, to, id); } ``` ### The complete code ```solidity // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.30; interface IERC721 { // Read-only functions function ownerOf(uint256 id) external view returns (address); function balanceOf(address owner) external view returns (uint256); function isApprovedForAll(address owner, address operator) external view returns (bool); function getApproved(uint256 id) external view returns (address); // State-changing functions function mint(address receiver, uint256 id) external; function setApprovalForAll(address operator, bool approved) external payable; function approve(address approved, uint256 id) external payable; function transferFrom(address from, address to, uint256 id) external payable; // Events // event Transfer(address indexed from, address indexed to, uint256 indexed id); // event ApproveForAll(address indexed owner, address indexed operator, bool approved); // event Approved(address indexed owner, address indexed spender, uint256 id); } contract ERC721 is IERC721 { // Variables // Token -> Owner Mapping mapping(uint256 token_ID => address owner) public ownerOf; // NFT balances of an address mapping(address owner => uint256 balance) public balanceOf; // Approved for all mapping(address owner => mapping (address operator => bool)) public isApprovedForAll; // Approved address for an NFT mapping(uint256 token_ID => address approved) public getApproved; // Events // Logs Transfer events event Transfer(address indexed from, address indexed to, uint256 indexed id); // Logs changes in operator's approval for an owner's all NFT's event ApproveForAll(address indexed owner, address indexed operator, bool approved); // Logs changes in single NFT operator's approval event Approved(address indexed owner, address indexed spender, uint256 id); // Functions // Mint NFTs to an address function mint(address receiver, uint256 id) external{ require(ownerOf[id] == address(0), "Already Minted"); ownerOf[id] = receiver; balanceOf[receiver] += 1; emit Transfer(address(0), receiver, id); } // Set Approval For an owner's all NFT's function setApprovalForAll(address operator, bool approved) external payable{ require(operator != address(0), "Invalid operator"); isApprovedForAll[msg.sender][operator] = approved; emit ApproveForAll(msg.sender, operator, approved); } // Set Approval for a single NFT function approve(address approved, uint256 id) external payable{ require(msg.sender == ownerOf[id] || isApprovedForAll[ownerOf[id]][msg.sender], "Not authorized"); getApproved[id] = approved; emit Approved(ownerOf[id], approved, id); } // NFT Transfer Function function transferFrom(address from, address to, uint256 id) external payable{ require(ownerOf[id] == msg.sender || isApprovedForAll[from][msg.sender] || getApproved[id] == msg.sender, "Not authorized"); ownerOf[id] = to; balanceOf[from] -= 1; balanceOf[to] += 1; if (getApproved[id] != address(0)) { delete getApproved[id]; emit Approved(ownerOf[id], address(0), id); } emit Transfer(from, to, id); } } ``` ### Test the functionalities - After writing the contract, compile the code. Deploy the contract. - Try switching accounts, make transactions, check balances. - Ideally, for testing, you have to test all the lines and all the branches of logic. But, using Remix, we can perform simple testing. - As an exercise, simulate the example which was discussed above, for ERC-721 tokens. - If everything works as expected, ERC-721 contract is coded successfully. **Congratulations!** - Now, take reference from this repository & code it yourself from scratch! -> https://github.com/nikillxh/token-frenzy ### QnA Section 1. **Why do we not store all the tokens of an owner in a list?** -> Let's say we map the address to a list of NFTs owned by the address. -> Everytime, when minting, transfer or burning of tokens take place, you'd need to add or remove from that list. -> Loop through arrays to find or delete a specific token for changes. -> This costs a lot of gas. Hence, it's unfeasible. -> Instead we take the path of mapping an NFT id to its owner address. -> This functionality is missing in ERC721. But, there exists **ERC721Enumerable**, which is an extension of ERC721 contract that stores the all the tokens as per the owner address. 2. **Why can’t multiple addresses be approved for a single token?** -> It's because of the simplicity and gas efficiency. -> Single approval avoids confusion or accidental misuse. -> It's similar to the reason of why don't we store all the tokens of an owner in a list. -> Additionally, it becomes difficult to manage approvals for different addresses by the new owner after a token transfer.