• YouTube Channel
  • System Status
  • VS Code Extension
  • ERC-721 Interface

    Overview

    What is the ERC-721 Interface vulnerability?

    In the context of Ethereum, the ERC-721 standard represents an interface for non-fungible tokens. Similar to ERC-20, this standardization allows smart contract developers to create generalized functionality that utilizes the standard interface; however, implementations that do not conform to the specification may cause errors or logical failures in interfacing smart contracts.

    Further reading: ERC-721 Standard

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract SimplifiedERC721 {
          // Basic ERC-721 mappings for tracking ownership and approved transfers
          mapping(uint256 => address) private _tokenOwner;
          mapping(uint256 => address) private _tokenApprovals;
          mapping(address => uint256) private _ownedTokensCount;
          mapping(address => mapping(address => bool)) private _operatorApprovals;
    
          // Required ERC-721 events
          event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
          event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
          event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
          // ERC-721 name and symbol for this token collection
          string private _name;
          string private _symbol;
    
          constructor(string memory name_, string memory symbol_) {
              _name = name_;
              _symbol = symbol_;
          }
    
          // Returns the number of NFTs owned by `owner`.
          function balanceOf(address owner) public view returns (uint256) {
              require(owner != address(0), "ERC721: balance query for the zero address");
              return _ownedTokensCount[owner];
          }
    
          // Returns the owner of the NFT specified by `tokenId`.
          function ownerOf(uint256 tokenId) public view returns (address) {
              address owner = _tokenOwner[tokenId];
              require(owner != address(0), "ERC721: owner query for nonexistent token");
              return owner;
          }
    
          // Enables or disables approval for a third party ("operator") to manage all of `owner`'s assets.
          function setApprovalForAll(address operator, bool approved) public {
              require(operator != msg.sender, "ERC721: approve to caller");
              _operatorApprovals[msg.sender][operator] = approved;
              emit ApprovalForAll(msg.sender, operator, approved);
          }
    
          // Transfers the ownership of an NFT from one address to another address.
          // N.B. this function uses a non-standard order of arguments
          function transferFrom(uint256 tokenId, address from, address to) public {
              require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
              _transfer(from, to, tokenId);
          }
    
          // Internal function to transfer ownership of a given token ID to another address.
          function _transfer(address from, address to, uint256 tokenId) internal {
              _ownedTokensCount[to] += 1;
              _ownedTokensCount[from] -= 1;
              _tokenOwner[tokenId] = to;
    
              emit Transfer(from, to, tokenId);
          }
    
          // Checks if `spender` is allowed to manage `tokenId`.
          function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
              require(_tokenOwner[tokenId] != address(0), "ERC721: operator query for nonexistent token");
              address owner = _tokenOwner[tokenId];
              return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
          }
    
          // Returns the approved address for a token ID, or zero if no address set
          function getApproved(uint256 tokenId) public view returns (address) {
              require(_tokenOwner[tokenId] != address(0), "ERC721: approved query for nonexistent token");
              return _tokenApprovals[tokenId];
          }
    
          // Returns if the `operator` is allowed to manage all of the assets of `owner`.
          function isApprovedForAll(address owner, address operator) public view returns (bool) {
              return _operatorApprovals[owner][operator];
          }
    
          // Other standard function bodies omitted for brevity
          function approve(address _approved, uint256 _tokenId) external payable;
          function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
          function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
      }
    
    

    In the example above, contract SimplifiedERC721 implements most functionality of the ERC-721 standard according to the specification, but in the transferFrom() function, it uses a non-standard order of arguments. This could cause smart contracts attempting to interface with it to fail, or could cause users expecting the standard interface to perform erroneous transactions.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract SimplifiedERC721Updated {
          // Basic ERC-721 mappings for tracking ownership and approved transfers
          mapping(uint256 => address) private _tokenOwner;
          mapping(uint256 => address) private _tokenApprovals;
          mapping(address => uint256) private _ownedTokensCount;
          mapping(address => mapping(address => bool)) private _operatorApprovals;
    
          // Required ERC-721 events
          event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
          event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
          event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
          // ERC-721 name and symbol for this token collection
          string private _name;
          string private _symbol;
    
          constructor(string memory name_, string memory symbol_) {
              _name = name_;
              _symbol = symbol_;
          }
    
          // Transfers the ownership of an NFT from one address to another address.
          function transferFrom(address from, address to, uint256 tokenId) public {
              require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
              _transfer(from, to, tokenId);
          }
    
          // Internal function to transfer ownership of a given token ID to another address.
          function _transfer(address from, address to, uint256 tokenId) internal {
              _ownedTokensCount[to] += 1;
              _ownedTokensCount[from] -= 1;
              _tokenOwner[tokenId] = to;
    
              emit Transfer(from, to, tokenId);
          }
    
          // Other ERC-721 functions omitted for brevity
    
      }
    
    

    In the revised example above, contract SimplifiedERC721Updated now uses the specified argument order for transferFrom() per the ERC-721 standard.