# What is ERC721:Section 1

By [KevinWang](https://paragraph.com/@kevin-wang) · 2025-09-06

---

What is ERC-721.
================

Actually you can simply understand the ERC-721 is NFT.

This is the link of EIP-721.[https://eips.ethereum.org/EIPS/eip-721](https://eips.ethereum.org/EIPS/eip-721?utm_source=chatgpt.com)

Official explanation of ERC-721 is “Non-Fungible Token”, which means every token is specially. That means you can use NFT to represent unique image, stock, or even real estate.

Practical Application
=====================

You can find plenty of tutorial of ERC-721. And this blog will show you the practical application.

Fist of all, let’s see the solidity code of a contract which implemented the ERC-721 protocol.

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    
    import {ERC721URIStorage, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    
    contract Merkamigos is ERC721URIStorage {
        uint256 private _nextTokenId;
    
        constructor() ERC721("Merkamigos", "MKG") {}
    }
    

It’s very easy to implement the ERC-721 by inheriting the ERC721URIStorage, but we want to find out what is happening in it.

Let’s see the constructor first.

constructor()
=============

When the contract was deployed, the constructor() would be executed.

And you may confuse of the grammar.

In solidity, if a contract inherit from the parent contract, and it’s parent contract has a constructor function with parameters, it is necessary to call the parent contract constructor within the child contract constructor and provide parameters.

    constructor() ERC721("Merkamigos", "MKG") {}
    

Now let’s see the constructor() of it’s father contract.

    abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
        using Strings for uint256;
    
        // Token name
        string private _name;
    
        // Token symbol
        string private _symbol;
    
        /**
         * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
         */
        constructor(string memory name_, string memory symbol_) {
            _name = name_;
            _symbol = symbol_;
        }
    
        /// @inheritdoc IERC721Metadata
        function name() public view virtual returns (string memory) {
            return _name;
        }
    
        /// @inheritdoc IERC721Metadata
        function symbol() public view virtual returns (string memory) {
            return _symbol;
        }
        
         /**
         * ......
         */
    }
    

We can find the private parameters: \_name and \_symbol.

\_name is the name of NTF and \_symbol is the symbol of NTF.

In OpenSea, you can find all NTF have the name and symbol, it is achieved by these two parameters’s getter function, name() and symbol().

And our example contract is named as ‘Merkamigos’ and symboled as ‘MKG’.

Public functions
================

We find that there is no other function of parameter in our example contract. It is caused by

father contract-ERC721URIStorage and ERC-721.

We can explicitly find the public function on REMIX.

!image.png

approve
=======

    abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {		
            /// @inheritdoc IERC721
        function approve(address to, uint256 tokenId) public virtual {
            _approve(to, tokenId, _msgSender());
        }
            /**
         * @dev Approve `to` to operate on `tokenId`
         *
         * The `auth` argument is optional. If the value passed is non 0, then this function will check that `auth` is
         * either the owner of the token, or approved to operate on all tokens held by this owner.
         *
         * Emits an {Approval} event.
         *
         * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
         */
        function _approve(address to, uint256 tokenId, address auth) internal {
            _approve(to, tokenId, auth, true);
        }
    
        /**
         * @dev Variant of `_approve` with an optional flag to enable or disable the {Approval} event. The event is not
         * emitted in the context of transfers.
         */
        function _approve(address to, uint256 tokenId, address auth, bool emitEvent) internal virtual {
            // Avoid reading the owner unless necessary
            if (emitEvent || auth != address(0)) {
                address owner = _requireOwned(tokenId);
    
                // We do not use _isAuthorized because single-token approvals should not be able to call approve
                if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
                    revert ERC721InvalidApprover(auth);
                }
    
                if (emitEvent) {
                    emit Approval(owner, to, tokenId);
                }
            }
    
            _tokenApprovals[tokenId] = to;
        }
    }
    

The caller can use `approve()` to specify another address to own the auth of controlling the token. `_tokenApprovals` records the mapping of tokenId to approvals. Notice that there is only an address can be approved to a token.

SafeTransferFrom
================

    
    abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {		
            /// @inheritdoc IERC721
        function safeTransferFrom(address from, address to, uint256 tokenId) public {
            safeTransferFrom(from, to, tokenId, "");
        }
    
        /// @inheritdoc IERC721
        function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
            transferFrom(from, to, tokenId);
            ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
        }
    }
    

The safeTransferFrom apply four parameters.

In the safeTransferFrom function, it calls transferFrom first.

    /// @inheritdoc IERC721
        function transferFrom(address from, address to, uint256 tokenId) public virtual {
            if (to == address(0)) {
                revert ERC721InvalidReceiver(address(0));
            }
            // Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists
            // (from != 0). Therefore, it is not needed to verify that the return value is not 0 here.
            address previousOwner = _update(to, tokenId, _msgSender());
            if (previousOwner != from) {
                revert ERC721IncorrectOwner(from, tokenId, previousOwner);
            }
        }
        
        function _update(address to, uint256 tokenId, address auth) internal virtual returns (address) {
            address from = _ownerOf(tokenId);
    
            // Perform (optional) operator check
            if (auth != address(0)) {
                _checkAuthorized(from, auth, tokenId);
            }
    
            // Execute the update
            if (from != address(0)) {
                // Clear approval. No need to re-authorize or emit the Approval event
                _approve(address(0), tokenId, address(0), false);
    
                unchecked {
                    _balances[from] -= 1;
                }
            }
    
            if (to != address(0)) {
                unchecked {
                    _balances[to] += 1;
                }
            }
    
            _owners[tokenId] = to;
    
            emit Transfer(from, to, tokenId);
    
            return from;
        }
        
        function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual {
            if (!_isAuthorized(owner, spender, tokenId)) {
                if (owner == address(0)) {
                    revert ERC721NonexistentToken(tokenId);
                } else {
                    revert ERC721InsufficientApproval(spender, tokenId);
                }
            }
        }
        
        /**
         * @dev Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in
         * particular (ignoring whether it is owned by `owner`).
         *
         * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
         * assumption.
         */
        function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
            return
                spender != address(0) &&
                (owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender);
        }
    

In transferFrom, it first checks if the to address is 0.

Then it calls `update()` to check if the caller has auth to control the NTF.

In `_checkAuthorized()` , the function check if the caller is the owner of NTF, or if the caller has been approved by the owner of NFT.

Then it calls `_approve(address(0), tokenId, address(0), false);` to clear the approve of the NFT. Because when the NTF has been transferred, the old approvers should be cleared to avoid old approvers can still control the NFT without the permission from new owner.

Then, it add 1 on the new owner’s balance, and reduce 1 on the old owner’s balance. Then it declares the NTF’s owner has been changed.

Finally, it calls `ERC721Utils.checkOnERC721Received` to check if the to address can accept the NFT. Notion that only the contract address should be checked.

    if (to.code.length > 0) {
                try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
                    if (retval != IERC721Receiver.onERC721Received.selector) {
                        // Token rejected
                        revert IERC721Errors.ERC721InvalidReceiver(to);
                    }
                } catch (bytes memory reason) {
                    if (reason.length == 0) {
                        // non-IERC721Receiver implementer
                        revert IERC721Errors.ERC721InvalidReceiver(to);
                    } else {
                        assembly ("memory-safe") {
                            revert(add(reason, 0x20), mload(reason))
                        }
                    }
                }
            }
    

Every contract which can receive NFT should achieve `onERC721Received` function and return the specific data- `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));`

If the contract does not achieve the function, it will throw an error.

setApprovalForAll
=================

    /// @inheritdoc IERC721
        function setApprovalForAll(address operator, bool approved) public virtual {
            _setApprovalForAll(_msgSender(), operator, approved);
        }
        
        /**
         * @dev Approve `operator` to operate on all of `owner` tokens
         *
         * Requirements:
         * - operator can't be the address zero.
         *
         * Emits an {ApprovalForAll} event.
         */
        function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
            if (operator == address(0)) {
                revert ERC721InvalidOperator(operator);
            }
            _operatorApprovals[owner][operator] = approved;
            emit ApprovalForAll(owner, operator, approved);
        }
    

When some address x calls the setApprovalForAll, it can specify an address to control all NFTs of the owner address x.

This function do not need to check the ownership cause the owner is the caller, the caller do not have the motivation to attack itself.

Summary
=======

In this article, we introduce all state-changing \*\*\*\*function in ERC-721.

We will introduce the view/pure function in ERC-721 in next article.

---

*Originally published on [KevinWang](https://paragraph.com/@kevin-wang/what-is-erc721-section-1)*
