详情参见:EIP-721: Non-Fungible Token Standard
ERC721 定义了一种以太坊生态中不可分割的、具有唯一性的Token交互、流通的接囗规范。官网简要称为 Non-Fungible Token Standard(简称NFT标准规范),即非同质化Token(或不可替代的Token)。
ERC721合约标准提供了在实现ERC721 Token 时必须要遵守的协议,要求每个ERC721标准合约需要实现ERC721接囗及ERC165接囗,ERC165可参见:Solidity - 智能合约标准ERC-165_ling1998的博客-CSDN博客
NFT标准接囗规范具有如下特性:
1、在该合约内,tokenId唯一
2、tokenId只能被一个owner所拥有
3、一个owner可以拥有多个NFT,balance函数只能查询owner拥有多少个token
4、NFT可通过approve、transfer等接囗方法进行流通,即NFT所有权转移
以下实现ERC721,共包含4个.sol文件:
(1)IERC721.sol - 接囗
pragma solidity ^0.6.0;
/// @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 IERC721 /* 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 calldata 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);
}
(2)ERC165.sol - 接囗及实现
pragma solidity ^0.6.0;
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
contract ERC165 is IERC165{
//常量 - ERC165接囗ID
bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;
//状态变量 - 记录接囗ID是否已被实现
mapping(bytes4=>bool) private _supportInterface;
//构造函数 - 初始化ERC165已被实现
constructor() public {
registerInterface(_INTERFACE_ID_ERC165);
}
//实现ERC65接囗方法
function supportsInterface(bytes4 interfaceID) external override view returns (bool){
return _supportInterface[interfaceID];
}
//注册接囗,即标记接囗为已实现
function registerInterface(bytes4 interfaceID) public {
require(interfaceID != 0xffffffff, "ERC165:无效的接囗ID");
_supportInterface[interfaceID] = true;
}
}
(3)SafeMath.sol - 安全运算库
pragma solidity ^0.6.0;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0); // Solidity only automatically asserts when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
(4)ERC721.sol - ERC721实现
pragma solidity ^0.6.0;
import "./IERC721.sol";
import "./ERC165.sol";
import "./SafeMath.sol";
interface IERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns(bytes4);
}
contract ERC721 is ERC165, IERC721 {
//状态变量 - 记录owner拥有多少个token
mapping(address => uint256) private _ownerTokensCount;
//状态变量 - 记录tokenId的所有者
mapping(uint256 => address) private _tokenOwner;
//状态变量 - 记录tokenId授权给外部账户
mapping(uint256 => address) private _tokenApproval;
//状态变量 - 记录用户全部授权
mapping(address => mapping(address => bool)) private _operatorApprovals;
//使用库作用于uint256类型
using SafeMath for uint256;
//常量 - ERC721接囗ID
bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
//常量 - IERC721TokenReceiver接囗ID
bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
//构造函数 - 初始化ERC721接囗已实现
constructor() public {
registerInterface(_INTERFACE_ID_ERC721);
}
modifier ownerIsNotZeroAddress(address _owner) {
require(address(0) != _owner, "ERC721:地址不能为零地址");
_;
}
/// 获取所有者拥有多少个Token
function balanceOf(address _owner) ownerIsNotZeroAddress(_owner) external override view returns (uint256) {
return _ownerTokensCount[_owner];
}
/// 获取tokenId的所有者
function ownerOf(uint256 _tokenId) external override view returns (address) {
return _ownerOf(_tokenId);
}
function _ownerOf(uint256 _tokenId) internal view returns (address) {
//查询出tokenId的所有者
address _owner = _tokenOwner[_tokenId];
//判断所有者是否为零地址
require(address(0) != _owner, "ERC721:拥有者地址不能为零地址,即所有者不存在");
return _owner;
}
//公用内部函数 - NFT转移,将tokenId由所有者_from转给_to
function _transferFrom(address _from, address _to, uint256 _tokenId) internal virtual {
//判断tokenId的当前所有者是否为_from
require(_ownerOf(_tokenId) == _from, "当前所有者不是转账所有者");
//判断接收者地址为不零地址
require(address(0) != _to, "接收者地址不能为零地址");
//判断用户是否有权转移token
require(_isApprovedOrOwner(msg.sender, _tokenId), "用户无权转移token");
//清除tokenId的授权者
_approve(address(0), _tokenId);
//原所有者拥有的token数量减一
_ownerTokensCount[_from] = _ownerTokensCount[_from].sub(1);
//新接收者拥有的token数量加一
_ownerTokensCount[_to] = _ownerTokensCount[_to].add(1);
//记录token所有者为新接收者
_tokenOwner[_tokenId] = _to;
//调用事件
emit Transfer(_from, _to, _tokenId);
}
//判断tokenId是否存在,若获取不到用户的所有者,则视为不存在
function _isExistTokenId(uint256 _tokenId) internal view returns (bool) {
//查询出tokenId的所有者
address owner = _tokenOwner[_tokenId];
if (address(0) != owner) {
return true;
}
return false;
}
//判断转账方是否有权转出token
function _isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) {
//判断tokenId的所有者是否存在
require(_isExistTokenId(_tokenId), "tokenId不存在");
//查询出tokenId的所有者
address _owner = _ownerOf(_tokenId);
//所有者可能是调用合约的地址spender,可能是tokenId被授权的地址spender,还有可能是所有者的token都被授权给地址spender
return (_owner == _spender || _getApproved(_tokenId) == _spender || _isApprovedForAll(_owner, _spender));
}
//判断是否为合约地址
function _isContract(address addr) internal view returns (bool) {
uint256 _size;
//若为外部账户_size = 0,若为合约账户 _size > 0
assembly { _size := extcodesize(addr) }
return _size > 0;
}
//校验接收地址是否有效
function _checkOnERC721Received(address _from, address _to, uint256 _tokenId, bytes memory _data) private returns (bool) {
//判断是否为合约地址,若为外部账户直接返回true,若为合约账户则校验是否实现了ERC721Receiver接囗方法
if (!_isContract(_to)) { //外部账户
return true;
}
//合约账户:校验是否实现了IERC721Receiver接囗方法,只有实现了IERC721Receiver接囗,才能接收ERC-721标准的token
(bool success, bytes memory returndata) = _to.call(abi.encodeWithSelector(
IERC721TokenReceiver(_to).onERC721Received.selector,
msg.sender,
_from,
_tokenId,
_data
));
//判断返回结果
if (!success) {
revert("合约地址未实现IERC721TokenReceiver接囗");
} else {
bytes4 retval = abi.decode(returndata, (bytes4));
return (retval == _ERC721_RECEIVED);
}
}
// 安全转移token - 公用内部函数
function _safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory _data) internal virtual {
_transferFrom(_from, _to, _tokenId);
require(_checkOnERC721Received(_from, _to, _tokenId, _data), "合约地址未实现IERC721TokenReceiver接囗!");
}
/// 安装转移token,包含data
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external override payable {
_safeTransferFrom(_from, _to, _tokenId, data);
}
/// 安装转移token
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external override payable {
_safeTransferFrom(_from, _to, _tokenId, "");
}
/// 转移token
function transferFrom(address _from, address _to, uint256 _tokenId) external override payable {
//转移token
_transferFrom(_from, _to, _tokenId);
}
/// 授权token
function _approve(address _approved, uint256 _tokenId) internal {
require(_approved != msg.sender, "不能授权给自己");
_tokenApproval[_tokenId] = _approved;
}
/// 授权token
function approve(address _approved, uint256 _tokenId) ownerIsNotZeroAddress(_approved) external override payable {
_approve(_approved, _tokenId);
}
/// 全部授权
function setApprovalForAll(address _operator, bool _approved) external override {
require(_operator != msg.sender, "不能授权给自己");
//修改状态变量 - 全部授权
_operatorApprovals[msg.sender][_operator] = _approved;
//事件
emit ApprovalForAll(msg.sender, _operator, _approved);
}
/// 获取tokenId的被授权者
function _getApproved(uint256 _tokenId) internal view returns (address) {
//判断tokenId的所有者是否存在
require(_isExistTokenId(_tokenId), "tokenId不存在");
return _tokenApproval[_tokenId];
}
/// 获取tokenId的被授权者
function getApproved(uint256 _tokenId) external override view returns (address) {
return _getApproved(_tokenId);
}
/// 是否全部授权,即_owner将自己所有的tokenId全部授权给_operator
function _isApprovedForAll(address _owner, address _operator) internal view returns (bool) {
return _operatorApprovals[_owner][_operator];
}
/// 是否全部授权,即_owner将自己所有的tokenId全部授权给_operator
function isApprovedForAll(address _owner, address _operator) external override view returns (bool) {
return _isApprovedForAll(_owner, _operator);
}
//生成tokenId - 公用函数
function _mint(address _to, uint256 _tokenId) ownerIsNotZeroAddress(_to) internal virtual {
require(!_isExistTokenId(_tokenId), "token已存在");
//设置token的所有者
_tokenOwner[_tokenId] = _to;
//所有者拥有的token数量累加
_ownerTokensCount[_to] = _ownerTokensCount[_to].add(1);
//事件
emit Transfer(address(0), _to, _tokenId);
}
// 生成tokenId
function mint(address _to, uint256 _tokenId) ownerIsNotZeroAddress(_to) external {
_mint(_to, _tokenId);
}
// 生成tokenId - (安全)
function safeMint(address _to, uint256 _tokenId, bytes calldata _data) ownerIsNotZeroAddress(_to) external {
_mint(_to, _tokenId);
require(_checkOnERC721Received(address(0), _to, _tokenId, _data), "合约地址没有实现ERC721Received接囗");
}
}
部署并测试