简介
非同质化token (NFT,Non-Fungible Token)
通常使用在艺术品、房子、虚拟收藏品等不可替代的物品中。
最小单元
9个函数,3个事件
// 获取分配给_owner的所有NFTs的数量
function balanceOf(address _owner) external view returns (uint256);
// 查询拥有token ID号为_tokenId的NFT所属者的地址
function ownerOf(uint256 _tokenId) external view returns (address);
// 将token ID号为_tokenId 的NFT 从_form 转给 _to,调用者需是NFT拥有者或者是被授权能操纵此NFT的人
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
// 同transferFrom,但转账双方地址不能为合约地址。 带data
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
// 同transferFrom,但转账双方地址不能为合约地址。不带data
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
// 授权,调用者是_tokenId的拥有者,授权给_approved。授权后_approved可以自行调用transferFrom函数将此tokenId的NFT转到自己账户
function approve(address _approved, uint256 _tokenId) external payable;
// 查看token ID号为 _tokenId的NFT被授权给了哪个地址
function getApproved(uint256 _tokenId) external view returns (address);
// 调用者 授权给第三方_operator进行管理个人的erc721代币token资产
function setApprovalForAll(address _operator, bool _approved) external;
// 查看_owner 是否授权给了_operator 管理所有的erc721代币token资产
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
授权的场景可以理解为:我授权给第三方去中心化交易所。然后挂上价钱。当有人买时,交易所调用我的 transferFrom函数然后卖给别人,我收到钱。
// NFTs资产转移事件
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
// NFTs资产授权地址发生变更事件
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
// 某owner所有NFTs资产被第三方授权的事件
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
需要在对应的函数中状态变更时,对相关事件进行调用。
可选项
// 描述NFTs收藏品的名称
function name() external view returns (string _name);
// 合约中NFTs缩称
function symbol() external view returns (string _symbol);
// 由tokenId获取NFT详细信息
function tokenURI(uint256 _tokenId) external view returns (string);
官方ERC721.sol代码
pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
using Address for address;
using Strings for uint256;
// 代币名称
string private _name;
// 代币缩写
string private _symbol;
// token ID --> owner address
mapping(uint256 => address) private _owners;
// owner address --> token count
mapping(address => uint256) private _balances;
// token ID --> approved address
mapping(uint256 => address) private _tokenApprovals;
// owner address --> operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
// 初始化token,设置name和symbol
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
// 判断合约是否实现了接口id为interfaceId的接口
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
super.supportsInterface(interfaceId);
}
// 返回owner拥有的代币总数量
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
// 传入token id, 判断属于谁,返回一个地址
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
// 查看token名称
function name() public view virtual override returns (string memory) {
return _name;
}
// 查看token缩称
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
// 传入token id,查看此id的详细信息,比如一个图片的URL
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
// 是用于计算tokenURI的基础URI,默认为空
function _baseURI() internal view virtual returns (string memory) {
return "";
}
// 给出tokenId,调用者将其 授权给 to
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner nor approved for all"
);
_approve(to, tokenId);
}
// 给出tokenId,判断其授权给谁了
function getApproved(uint256 tokenId) public view virtual override returns (address) {
_requireMinted(tokenId);
return _tokenApprovals[tokenId];
}
// 调用者将自己的所有token 授权给 第三方operator (approved为true)false为取消授权
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
// 判断 owner 是否把自己的所有token授权给operator了
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
// from 给 to 转移 token
function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
// 判断调用者是否被授权tokenId
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
_transfer(from, to, tokenId);
}
// 安全转移tokenId,两方地址不能为合约
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
// 安全转移tokenId,两方地址不能为合约,且带data数据
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
_safeTransfer(from, to, tokenId, data);
}
//
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
// 判断某个tokenId 是否存在
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _owners[tokenId] != address(0);
}
// 判断spender是否有权管理tokenId
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
address owner = ERC721.ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}
// 安全的铸造tokenId,并将其转移到 to
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
// 还是铸造tokenId,转移给 to,多了个data
function _safeMint(
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
// 铸币的具体实现,一般不直接调用此方法,而是调用_safeMint
function _mint(address to, uint256 tokenId) internal virtual {
// 转移的地址不能是0地址、新生成的tokenId不能已存在
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
// 维护全局状态
_balances[to] += 1;
_owners[tokenId] = to;
// 触发转账事件
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
// 销毁代币的具体实现
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId);
// 清理所有此tokenId的授权
_approve(address(0), tokenId);
// 维护全局状态
_balances[owner] -= 1;
delete _owners[tokenId];
// 触发转账事件,销毁就是转向0地址
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId);
}
// 转账的具体实现
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
// tokenId需要属于from
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
// 转向的地址不能是0地址
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId);
// 此tokenId转走时,清理掉之前的所有授权
_approve(address(0), tokenId);
// 维护全局状态
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
// 触发转账事件
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId);
}
// 授权的具体实现
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
// 触发授权事件
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
// 授权owner所有token的具体实现
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual {
// 授权的第三方不能是自己
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
// 触发全部授权事件
emit ApprovalForAll(owner, operator, approved);
}
// 如果此tokenId还不存在,重新去挖
function _requireMinted(uint256 tokenId) internal view virtual {
require(_exists(tokenId), "ERC721: invalid token ID");
}
// 检查是否是合约地址等..
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(_msgSender(), 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;
}
}
//
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
//
function _afterTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
}
发行
只需要编写一个编写一个 contract 继承自 ERC721URIStorage
ERC721URIStorage 是一个继承自 ERC721.sol 的抽象contract。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/extensions/ERC721URIStorage.sol)
pragma solidity ^0.8.0;
import "../ERC721.sol";
/**
* @dev ERC721 token with storage based token URI management.
*/
abstract contract ERC721URIStorage is ERC721 {
using Strings for uint256;
// Optional mapping for token URIs
mapping(uint256 => string) private _tokenURIs;
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory _tokenURI = _tokenURIs[tokenId];
string memory base = _baseURI();
// If there is no base URI, return the token URI.
if (bytes(base).length == 0) {
return _tokenURI;
}
// If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
if (bytes(_tokenURI).length > 0) {
return string(abi.encodePacked(base, _tokenURI));
}
return super.tokenURI(tokenId);
}
/**
* @dev Sets `_tokenURI` as the tokenURI of `tokenId`.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token");
_tokenURIs[tokenId] = _tokenURI;
}
/**
* @dev See {ERC721-_burn}. This override additionally checks to see if a
* token-specific URI was set for the token, and if so, it deletes the token URI from
* the storage mapping.
*/
function _burn(uint256 tokenId) internal virtual override {
super._burn(tokenId);
if (bytes(_tokenURIs[tokenId]).length != 0) {
delete _tokenURIs[tokenId];
}
}
}
我们编写的合约:(包含一个自己写的 awardItem 方法,用于调用父类的 mint 币方法)
// contracts/GameItem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract GameItem is ERC721URIStorage {
using Counters for Counters.Counter;
// 统计已经mint的币数量
Counters.Counter private _tokenIds;
// 调用父合约 ERC721 的构造方法
constructor() ERC721("LmhNFT", "mhNFT") {}
// 生成NFT
function awardItem(address player, string memory tokenURI) public returns (uint256) {
// nft编号从0开始,获取当前nft数量作为新的nft的编号
uint256 newItemId = _tokenIds.current();
// 造币
_mint(player, newItemId);
// 设置nft对应的URL
_setTokenURI(newItemId, tokenURI);
// 已经造的币数++
_tokenIds.increment();
return newItemId;
}
}
通过 remix 直接编译并部署:(我是连接到了 goerli 测试网)
小狐狸弹出信息,pay 过gas费后。部署成功,在测试网查看:
调用方法 awardItem 用来给指定的地址生成 nft
调用后,发送transaction,在测试网查看信息:
然后在小狐狸中导入nft,输入部署的nft合约地址,就会自动识别出nft名称。导入即可。如下所示,刚才铸币时在此地址造了一个nft,显示 1 mhNFT。
再发送一笔transaction,再铸造一个代币
调用转移代币方法 safeTransferFrom :
小狐狸弹出信息框确认一下:
然后在测试网查看transaction:
转移过后:
部署的合约中还有很多方法都可以调用,这里不再一 一测试。