ERC-721 Interface
Overview
- Severity: High
- Confidence: Medium
- Affected Versions: All
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.