ERC721 定义了一种以太坊生态中不可分割的、具有唯一性的Token交互、流通的接囗规范。官网简要称为 Non-Fungible Token Standard(简称NFT标准规范),即非同质化Token(或不可替代的Token)。
ERC721合约标准提供了在实现ERC721 Token 时必须要遵守的协议,要求每个ERC721标准合约需要实现ERC721接囗及ERC165接囗。
ERC721特性:
- 在该合约内,tokenId唯一
- tokenId只能被一个owner所拥有
- 一个owner可以拥有多个NFT,balance函数只能查询owner拥有多少个token
- NFT可通过approve、transfer等接囗方法进行流通,即NFT所有权转移
ERC721接口实现及详解:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.7;
interface IERC721 /* is ERC165 */ {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
//转移事件:token发生转移时进行event通知;
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
//授权事件:发生授权事件时进行通知;
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
//全部授权事件,发生全部授权时进行通知;
function balanceOf(address _owner) external view returns (uint256);
//账户余额:账户有多少个tokenID
function ownerOf(uint256 _tokenId) external view returns (address);
//拥有权查询:按照tokenID查询其归属账户
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external payable;
//tokenID转移,参数不同;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
//tokenID安全转移:转移三要素要存在;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
//tokenID转移
function approve(address _approved, uint256 _tokenId) external payable;
//授权:这里的授权指to有权利将委托方的tokenID进行转移;
function setApprovalForAll(address _operator, bool _approved) external;
//全部授权,指将用户名下的所有tokenID进行转移;
function getApproved(uint256 _tokenId) external view returns (address);
//授权查询:查询tokenID对应的被授权者;
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
//查看全部授权:查看owner是否对operator赋予了全部授权;
}
ERC721具体示例代码如下:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.7;
import "./IERC165.sol";
import "./IERC721.sol";
import "./Address.sol"; //导入库
/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
interface ERC721TokenReceiver {
/// @notice Handle the receipt of an NFT
/// @dev The ERC721 smart contract calls this function on the recipient
/// after a `transfer`. This function MAY throw to revert and reject the
/// transfer. Return of other than the magic value MUST result in the
/// transaction being reverted.
/// Note: the contract address is always the message sender.
/// @param _operator The address which called `safeTransferFrom` function
/// @param _from The address which previously owned the token
/// @param _tokenId The NFT identifier which is being transferred
/// @param _data Additional data with no specified format
/// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
/// unless throwing
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes memory _data) external returns(bytes4);
}
contract ERC721 is IERC721,IERC165 {
mapping(bytes4 => bool)supportsInterfaces;
bytes4 invalidID = 0xffffffff;
bytes4 constant ERC165_InterfaceID=0x01ffc9a7; //erc165
bytes4 constant ERC721_InterfaceID=0x80ac58cd; //erc721
mapping(address=>uint256)ercTokenCount; //记录用户token count;
mapping(uint256=>address)ercTokenOwner; //记录token的拥有者;
mapping(uint256=>address)ercTokenApproved;
mapping(address=>mapping(address=>bool))ercOperatorForAll;
using Address for address;
constructor(){
_registerInterface(ERC165_InterfaceID);
_registerInterface(ERC721_InterfaceID);
}
//授权
modifier canOperator(uint256 _tokenId){
address owner = ercTokenOwner[_tokenId];
require(msg.sender == owner || ercOperatorForAll[owner][msg.sender]);
_;
}
//转账
modifier canTransfer(uint256 _tokenId,address _from){
address owner = ercTokenOwner[_tokenId];
require(owner==_from);
require(msg.sender == owner || msg.sender == ercTokenApproved[_tokenId] || ercOperatorForAll[owner][msg.sender]);
_;
}
function _registerInterface(bytes4 interfaceID)internal {
supportsInterfaces[interfaceID]=true;
}
function supportsInterface(bytes4 interfaceID) override external view returns (bool) {
require(invalidID != interfaceID);
return supportsInterfaces[interfaceID];
}
//721
function balanceOf(address _owner) override external view returns (uint256){
return ercTokenCount[_owner];
}
function ownerOf(uint256 _tokenId) override external view returns (address){
return ercTokenOwner[_tokenId];
}
function getApproved(uint256 _tokenId) override external view returns (address){
return ercTokenApproved[_tokenId];
}
function isApprovedForAll(address _owner, address _operator) external view returns (bool){
return ercOperatorForAll[_owner][_operator];
}
function approve(address _approved, uint256 _tokenId) override external payable{
ercTokenApproved[_tokenId]=_approved;
emit Approval(msg.sender,_approved,_tokenId);
}
function setApprovalForAll(address _operator, bool _approved) override external{
ercOperatorForAll[msg.sender][_operator]=_approved;
emit ApprovalForAll(msg.sender,_operator,_approved);
}
function transferFrom(address _from, address _to, uint256 _tokenId) override external payable{
_transferFrom(_from,_to,_tokenId);
}
function _transferFrom(address _from, address _to, uint256 _tokenId)internal canTransfer(_tokenId,_from){
ercTokenOwner[_tokenId] = _to; //更改属主;
ercTokenCount[_from]-=1;
ercTokenCount[_to]+=1;
ercTokenApproved[_tokenId] = address(0);
emit Transfer(_from, _to, _tokenId);
}
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) override external payable{
_safeTransferFrom(_from,_to,_tokenId,data);
}
function safeTransferFrom(address _from, address _to, uint256 _tokenId)override external payable{
_safeTransferFrom(_from,_to,_tokenId,"");
}
function _safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) internal { //安全转账即判断对方是否有资格持有ERCtoken,避免转错
_transferFrom(_from, _to, _tokenId);
//add safe code
if(_to.isContract()){ //首先判断对方是否是合约
bytes4 retval = ERC721TokenReceiver(_to).onERC721Received(msg.sender,_from,_tokenId,data); //是否实现了ERC721Received
require(retval == ERC721TokenReceiver.onERC721Received.selector);
}
}
function mint(address _to,uint256 _tokenId,bytes memory data)external{
require(_to != address(0));
require(ercTokenOwner[_tokenId]==address(0));
ercTokenOwner[_tokenId]= _to;
ercTokenCount[_to]+=1;
if(_to.isContract()){ //首先判断对方是否是合约
bytes4 retval = ERC721TokenReceiver(_to).onERC721Received(msg.sender,address(0),_tokenId,data); //是否实现了ERC721Received
require(retval == ERC721TokenReceiver.onERC721Received.selector);
}
emit Transfer(address(0),_to,_tokenId);
}
}
注意事项:
- 引用Address识别地址,在Solidity合约中需要了解一个地址是普通钱包地址还是合约地址。 OpenZeppelin的
Address
库提供了一个方法isContract()
可以帮我们解决这个问题。 - safeTransferFrom()要转移NFT所有权,如果_to是一个合约,那么它必须实现ERC721TokenReceiver接口。
- 这里使用了modifier关键字,对转账和授权进行了判断处理。