ERC-721是一个流行的技术标准,基于ERC-721标准开发的代币合约被统称为"非同质化代币(Non-Fungible Tokens,缩写为NFT)",ERC-721代币相较于ERC-20代币最大的区别就是不可分割性和唯一性,其Token的最小单位为1,且每个Token都是唯一的。ERC-721标准提供了一套规则和准则,允许开发者在以太坊生态系统内以一致和可互操作的方式创建和管理NFT。
ERC721规范与扩充功能
- ERC165:用来判断该合约实现了哪些接口,要实现ERC721标准必须实现ERC165
- ERC721:用来实现ERC721的核心功能
- ERC721Receiver:防止NFT转入黑洞
- ERC721TokenReceiver:提供钱包/代理人/拍卖的合约应实现此标准以提供接口来实现安全转移(safe transfer)
- ERC721Metadata:提供NFT资产的元数据,不实现的话就看不到NFT的图片了
- ERC721A:变种合约,节省gas
- ERC721R:变种合约,可退款NFT
- ERC721Enumerable:方便查询合约中的NFT信息,缺点是gas消耗多
ERC165
当我们需要和一个合约进行交互时,可能并不清楚他是否实现了某些合约,例如"ERC20/ERC721"等。所以可以先通过Erc165进行查询(该合约必须实现ERC165),就可以避免很多交互的问题。
IERC165接口合约只声明了一个supportsInterface函数,输入要查询的interfaceId接口id,若合约实现了该接口id,则返回true:
interface IERC165 {
// 如果合约实现了查询的`interfaceId`,则返回true,否则返回false
// interfaceId:接口ID
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
ERC721 主合约对 supportsInterface() 的实现如下:
function supportsInterface(bytes4 interfaceId) public view returns (bool){
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
interfaceId == type(ERC165).interfaceId;
}
ERC721Receiver
IERC721Receiver是一个接口(interface),一个合约必须实现这个接口才可以收到ERC721标准(NFT)的Token,用于安全转账中对合约地址的检查。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
下面代码是ERC721合约中对收币地址的一个判断,如果地址是账户,则直接转账,是合约的话则会判断该合约是否实现IERC721Receiver这个接口,并返回onERC721Received.selector,没有的话,会进行一个回滚。
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal returns (bool) {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
ERC721Metadata
定义合约的元数据信息,如:合约名字、简称、以及每个代币的 tokenURI。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC721.sol";
interface IERC721Metadata is IERC721 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
}
ERC721
ERC721定义了非同质化代币(NFT)的标准接口,包括代币发行、转移、查询等功能。它能够实现数字资产的唯一性和独特性,例如游戏中的角色、道具、艺术品等。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC165.sol";
interface IERC721 is IERC165 {
// 事件
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);
// 查询
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
// 转移
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
// 授权
function approve(address to, uint256 tokenId) external;
function setApprovalForAll(address operator, bool approved) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
event Transfer
:NFT转移时触发event Approval
:授权NFT时触发event ApprovalForAll
:授权或撤销第三方管理权时触发balanceOf
:查看拥有者持有NFT的数量ownerOf
:查询NFT的持有者safeTransferFrom
:安全转移NFT,为了防止NFT永远被锁定在没有能力接收NFT的合约地址中,ERC721的safeTransferFrom要求接收者若为[合约],需要实现IERC721Receiver接口。msg.sender可以是 NFT的持有者 或 被授权的第三方transferFrom
:转移NFT。msg.sender将编号为[tokenId]的NFT从持有者[from]转移给接收者[to],msg.sender可以是 NFT的持有者 或 被授权的第三方approve
:授权NFT给第三方,msg.sender授权所持有编号为[tokenId]的NFT给第三方[to]setApprovalForAll
:授权或撤销第三方管理权。msg.sender授权或撤销全部持有的NFT给第三方getApproved
:查询NFT的被授权人isApprovedForAll
:查询第三方管理权限。给定持有者地址[owner]与要查询的第三方地址[operator],返回是否授权或撤销管理权
ERC721完整代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC721Receiver.sol";
import "./ERC165.sol";
import "./IERC721.sol";
import "./IERC721Metadata.sol";
contract ERC721 is ERC165,IERC721,IERC721Metadata {
// 账户=>NFT数量 映射
mapping(address => uint) _balances;
// tokenId => 持有者 映射
mapping(uint => address) _owners;
// 授权映射:持有者=>tokenId=>被授权人
mapping(uint=>address) _tokenApprovels;
mapping(address=>mapping(address=>bool)) _operatorApprovels;
string _name;
string _symbol;
mapping(uint => string) _tokenURIs;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
interfaceId == type(ERC165).interfaceId;
}
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function tokenURI(uint256 tokenId) external view returns (string memory) {
require(_owners[tokenId] != address(0), "tokenId is invalid");
return _tokenURIs[tokenId];
}
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "address 0 cannot be owner");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "Invalid tokenId");
return owner;
}
// 检查1:to不能是持有者
// 检查2:msg.sender是否是持有者,或者是否被授权管理全部NFT(通过isApprovedForAll判断)
function approve(address to, uint256 tokenId) public {
address owner = _owners[tokenId];
require(owner == msg.sender || isApprovedForAll(owner, msg.sender), "not permission");
require(owner != to, "cannot approve to yourself");
_tokenApprovels[tokenId] = to;
emit Approval(owner, to, tokenId);
}
// 检查:持有者是否为address(0), 地址为address(0)表示未铸造或已被销毁
function getApproved(uint256 tokenId) public view returns (address operator) {
address owner = _owners[tokenId];
require(owner != address(0), "Token not minted or destroyed");
return _tokenApprovels[tokenId];
}
// 检查:operator不能和owner相同,避免发生授权给持有者自己的情况
function setApprovalForAll(address operator, bool approved) public {
require(msg.sender != operator, "operator==owner");
_operatorApprovels[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovels[owner][operator];
}
// 检查1:from是否为owner
// 检查2:msg.sender是否为持有者,或是否被授权管理该NFT,或是否被授权管理所有NFT
// 转移时做以下事情:
// 1、移除该tokenId的被授权者,转移后原来的授权关系会取消:delete _tokenApprovels[tokenId]
// 2、持有者的balance-1
// 3、接收者的balance+1
// 4、设置接收者为新的owner
// 5、触发转移事件
function transferFrom(address from, address to, uint256 tokenId) public {
_transfer(from, to, tokenId);
}
// 转移后,若接收者为合约,则调用IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (byte4 retval),并检查返回值
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) public {
_safeTransfer(from, to, tokenId, data);
}
function safeTransferFrom(address from, address to, uint256 tokenId) public {
_safeTransfer(from, to, tokenId, "");
}
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
function _transfer(address from, address to, uint256 tokenId) internal {
address owner = _owners[tokenId];
require(owner == from, "from address is not owner");
require(
owner == msg.sender ||
getApproved(tokenId) == msg.sender ||
isApprovedForAll(owner, msg.sender),
"caller no permission to transfer"
);
delete _tokenApprovels[tokenId];
_balances[from] -=1 ;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal returns (bool) {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
// 铸造
// 合约持有者或特殊权限的人才能调用
// 有address(0) 到目标账户,触发Transfer事件
// 检查1:不能铸造到address(0)
// 检查2: tokenId不能重复
function mint(address to, uint tokenId) public {
_mint(to, tokenId);
}
// 安全铸造,类似safeTransferFrom
function safeMint(address to, uint256 tokenId, bytes calldata data) public {
_safeMint(to, tokenId, data);
}
function safeMint(address to, uint tokenId) public {
_safeMint(to, tokenId, "");
}
// 销毁NFT
function burn(uint tokenId) public {
address owner = _owners[tokenId];
require(msg.sender == owner, "Only owner can burn");
_balances[owner] -= 1; // 持有者余额减1
delete _owners[tokenId]; // 删除映射
delete _tokenApprovels[tokenId]; // 删除授权映射
emit Transfer(msg.sender, address(0), tokenId);
}
function _safeMint(address to, uint256 tokenId, bytes memory data) public {
_mint(to, tokenId);
require(_checkOnERC721Received(address(0), to, tokenId, data), "ERC721: mint to non ERC721Receiver implementer");
}
function _mint(address to, uint tokenId) internal {
require(to != address(0), "cannot mint to address(0)");
require(_owners[tokenId] == address(0), "tokenId existed");
_balances[msg.sender] += 1;
_owners[tokenId] = msg.sender;
emit Transfer(address(0), to, tokenId);
}
}