Pablo Cibraro

My notes about software development, security and other stuff.

NFTs in Ethereum explained

NFT or Non-Fungible-Token is a relatively new concept that is hard to grasp for many. A token in the scope of a Blockchain represents proof of ownership in a digital world, which means you can use it as evidence to prove you own something. There is a subtle difference between ownership and proof of ownership. You could own a car, but you might need an invoice or title to prove it is yours. A token represents the latter. You demonstrate a token is yours by using public and private key cryptography. The token is associated with an address that can only be computed with a private key you own. A token was initially used to represent value in the native currency of a Blockchain or an equivalent fiat currency such as US Dollars. All the issue tokens were worth the same value. If you have one token, you could change with another token without making any difference or losing value.

NFTs were introduced to represent things with a unique value other than currency. As each NFT is unique, they have different intrinsic values as well. You can not simply change an NFT for another NFT anymore. They could represent ownership on digital assets such as images, subscriptions, game rewards, etc.

NFTs in Ethrereum

NFTs are implemented in Ethrereum with Smart Contracts. It is worth mentioning that a Smart Contract is not an NFT but a source or factory for them.

The contracts in this scenario control how the NFTs are issued and assigned in the Blockchain. The following image shows how NFTs are grouped and stored in the Blockchain under their parent contract.

alt

In that sense, they can not directly be accessed or located by an address in the Blockchain. The access is always controlled by a contract. NFTs are not a built-in construct in the Blockchain either, so anyone could implement the Smart Contract to manage them in any way. To prevent the latter from happening, different community members in Ethereum defined a primary interface with a few methods that any Smart Contract should adhere to be considered an NFT factory. That interface became the facto standard known as ERC-721.

pragma solidity ^0.4.20;

/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev This emits when the approved address for an NFT is changed or
    ///  reaffirmed. The zero address indicates there is no approved address.
    ///  When a Transfer event emits, this also indicates that the approved
    ///  address for that NFT (if any) is reset to none.
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator is enabled or disabled for an owner.
    ///  The operator can manage all NFTs of the owner.
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice Count all NFTs assigned to an owner
    /// @dev NFTs assigned to the zero address are considered invalid, and this
    ///  function throws for queries about the zero address.
    /// @param _owner An address for whom to query the balance
    /// @return The number of NFTs owned by `_owner`, possibly zero
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT. When transfer is complete, this function
    ///  checks if `_to` is a smart contract (code size > 0). If so, it calls
    ///  `onERC721Received` on `_to` and throws if the return value is not
    ///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @param data Additional data with no specified format, sent in call to `_to`
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev This works identically to the other function with an extra data parameter,
    ///  except this function just sets data to "".
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
    ///  THEY MAY BE PERMANENTLY LOST
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Change or reaffirm the approved address for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner.
    /// @param _approved The new approved NFT controller
    /// @param _tokenId The NFT to approve
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  all of `msg.sender`'s assets
    /// @dev Emits the ApprovalForAll event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param _operator Address to add to the set of authorized operators
    /// @param _approved True if the operator is approved, false to revoke approval
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Get the approved address for a single NFT
    /// @dev Throws if `_tokenId` is not a valid NFT.
    /// @param _tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for another address
    /// @param _owner The address that owns the NFTs
    /// @param _operator The address that acts on behalf of the owner
    /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

Interfaces don't exist as a primary construct in the Ethereum Virtual Machine (EVM). They are supported in Solidity, but they don't correlate to anything once they are deployed. You can assume a Smart Contract follows a convention for the methods it provides.

Since a Smart Contract becomes the source for NFTs, it also makes a lot of sense for grouping collections like image collectibles.

NFTs are unique IDs mapped to an address (the owner) and stored in a Smart Contract.

Storage space in the Blockchain is expensive. As long as you store basic data about your NFTs in the storage, you should be good. How about images? That is a different story. Although it would make sense to store the whole blob of bytes for an image in the Blockchain, that would be extremely expensive and hard to manage in the long run. For that reason, the community found a different way to address that scenario. It is called NFT metadata. It is a JSON file that describes additional data about your NFT that you wouldn't store in the Blockchain. That metadata file contains, for example, an URL to the location of the image blob. This metadata file could be stored anywhere, but many prefer it in decentralized storage like IPFS. The Smart Contract ends up holding a reference to the location of this metadata in the Blockchain.

The chicken and egg dilemma. My NFT in the Blockchain assures data integrity. Once it is saved in the Blockchain, it can be changed. However, the same thing can not be ensured for the metadata file is pointing to. There are solutions to this problem. Store the metadata as text in the same storage as the Smart Contract, or store a checksum created from the data in the metadata file.

Implementing an ERC-721 contract

OpenZeppelin already provides a base contract that can be used as a starting point for any implementation that sticks to the ERC-721 standard.

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

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

contract TikenToken is ERC721, Ownable {
    using Counters for Counters.Counter; 

    Counters.Counter private _tokenIds;

    event TokenEmitted(string indexed symbol, address _sender, uint256 _id);

    constructor() ERC721("My NFT", "MYNFT") {
    }

    function createToken(string memory _tokenURI, address to) public onlyOwner returns (uint) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        _mint(to, newItemId);

        emit TokenEmitted(symbol(), to, newItemId);

        return newItemId;
    }
}

The code above shows a straightforward implementation of an ERC721 contract. It generates a new identifier for the NFT and calls the internal _mint method for associating the sender address with this new token (internally stored in a mapping). This implementation does not offer any metadata for the NFTs. If you also want to store the URL for the metadata associated with the generated NFT, you must implement the ERC721URIStorage interface.

function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function createToken(string memory _tokenURI) public onlyOwner returns (uint) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        _mint(msg.sender, newItemId);
        _setTokenURI(newItemId, _tokenURI);

        emit TokenEmitted(symbol(), msg.sender, newItemId);

        return newItemId;
    }

These changes include a new method, tokenURI that returns the metadata URL associated with the token and a new line _setTokenURI in the createToken method for persisting the URL in the contract storage.

Finally, if you also want to support the ability to destroy or burn issued NTFs, you can also implement the interface ERC721Burnable

function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
}

The method assigns the token to an address that is no longer accessible.