一 NFT概述
1.1 什么是NFT
ERC-20是同质化代币标准,同质化的意思就是说,代币在单位内表示的价值和功能是一样, 代币之间是可以互换的。比如你现在有100个BNB, 那么每一个BNB价值和功能都是一样的。
但是,有时候我们希望给区块链中的虚拟资产赋予独特的、唯一的等特性的时候,比如对数字资产的所有权、纪念章、数字艺术品、各种唯一的虚拟物品,比如游戏道具等等,这时候同质化代币是做不到这一点,因此有了非同质化代币NFT,即Non-Fungible Tokens
NFT: 就是Non-Fungible Token,即非同质化代币,他也是以太坊区块链上一种虚拟数字资产,但是和同质化代币不同,它是唯一的,独特的,不可互换的代币,每一个NFT都具有独特的属性和价值。
1.2 Non-Fungible Token(NFT) 和 Fungible Token(Token)比较
1.2.1 可互换性
Token是可以互换的,每一个Token的价值和功能是一样的
NFT: 不可互换,每一个NFT都是独一无二的,都可能具有不同价值
1.2.2 不可分割性
Token是可以分割的,通常可以分割为更小的单位进行交易。比如你可以购买0.1个或者0.001个代币
NFT通常不可分割,每个NFT都是一个完整的单位,你不能购买0.5个NFT
1.2.3 使用场景不一样
Token主要用于支付、交易和价值存储,适合需要标准化价值的场景
NFT主要用于表示独特的资产和所有权,适合需要强调独特性和所有权的场景。比如游戏道具、数字艺术品、数字收藏品、所有权、数字票据等等
二 ERC-721标准
2.1 什么是ERC-721
ERC-721是以太坊非同质化代币(NFT)的一个标准,它定义了一套规范或者标准,如果NFT实现了ERC-721标准,外部的钱包、交易所或者其他DApp应用程序就可以很方便的和NFT进行交互。
2.2 ERC-721特点
非同质化:
与同质化代币(如比特币和以太币)不同,ERC-721 代币是独特的,每个代币有其独特的属性和标识符。
这种独特性使得 ERC-721 代币特别适合表示独一无二的资产,如数字艺术品、收藏品、游戏物品等。
不可分割:
大多数 ERC-721 代币是不可分割的,即不能像同质化代币那样分割成更小的单位。一个完整的 ERC-721 代币必须作为一个整体进行交易。
所有权和转移:
ERC-721 标准定义了一组接口,使得NFT的所有权和转移过程可以在区块链上透明和安全地进行。代币的所有权信息被永久记录在区块链上,任何人都可以验证。
2.3 ERC-721标准内容
2.3.1 字段
和ERC-20不同,ERC-721因为要表示代币独一无二。因此,给NFT添加了一个tokenId的字段,表示该NFT独一无二。
另外,每一个NFT都有一个tokenURI,通常指向一个包含该代币详细信息的 JSON 文件,如名称、描述、图像链接等。这使得每个 NFT 可以有丰富的元数据,与其独特性相对应。比如:
{
"title": "Louis Vena",
"description": "Cneter Left",
"image": "ipfs://QmX6pUBZDRt2uzw79iatA9zH9sbbDrrT1wThs8cjamqAkF",
"attributes": [
{
"trait_type": "Singer",
"value": "Belly"
},
{
"trait_type": "Area",
"value": "America"
}
],
"version": "1"
}
2.3.2 函数
2.3.2.1 balanceOf
function balanceOf(address _owner) external view returns (uint256);
返回指定地址的NFT余额,一个人可能会拥有多个
2.3.2.2 ownerOf
function ownerOf(uint256 _tokenId) external view returns (address);
根据指定的tokenId返回该NFT的拥有者地址
2.3.2.3 safeTransferFrom
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
安全的将指定的tokenId的NFT从_from地址转移到_to地址,并且还可以携带一些额外数据。
在复杂的交易场景中,可能需要附加的上下文信息。例如,某个 NFT 可能代表一个游戏中的物品,附加的数据可以包含关于游戏场景的信息。
在目标地址是合约时,附加的数据可以用来传递特定指令给合约。
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
安全的将指定的tokenId的NFT从_from地址转移到_to地址,不能携带一些额外数据
2.3.2.4 transferFrom
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
将指定的tokenId的NFT从_from地址转移到_to地址
2.3.2.5 approve
function approve(address _approved, uint256 _tokenId) external payable;
授权某个地址_approved可以转移指定tokenId的NFT。
2.3.2.6 setApprovalForAll
function setApprovalForAll(address _operator, bool _approved) external;
授权或取消授权_operator地址可以管理调用者的所有NFT
2.3.2.7 getApproved
function getApproved(uint256 _tokenId) external view returns (address);
返回指定tokenId的NFT被授权哪个地址可以进行转移
2.3.2.8 isApprovedForAll
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
是否_operator是否被授权管理_owner的所有NFT
2.3.3 事件
2.3.3.1 Transfer
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
在代币转移时需要触发
2.3.3.2 Approval
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
在代币授权时触发
2.3.3.3 ApprovalForAll
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
在设置或取消所有授权时触发
三 safeTransferFrom和transferFrom比较
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
3.1 相同点
都是将 指定tokenId的NFT 从 `from` 地址转移到` to` 地址
3.2 不同点
不同点主要是安全问题:
我们知道智能合约地址一般分为EOA地址和CA地址,那么钱包一般属于EOA地址,这类地址他们不会有代码的执行,仅用于持有和发送代币等;但是CA地址就是合约地址,合约地址是可以有代码执行逻辑的。
那这个和NFT转移安全有什么关系呢? 难道CA地址就无法转移成功,EOA地址就可以?
其实是这样的,无论是EOA地址还是CA地址,safeTransferFrom和transferFrom都是可以转移成功的。但是转移给CA地址,需要考虑到这个合约是否有接口可以把NFT转出来,如果转不出来,那么这个NFT就一直在这个合约里面了。因此,如果 to 地址是一个不支持 操作ERC721的合约,最后safeTransferFrom就会revert, 这笔交易不会成功。
那safeTransferFrom是怎么实现检查合约是否具备操作NFT的能力的呢?
在OpenZeppelin中,定义了一个ERC721Receiver接口,如果你的合约你觉得需要让调用者知晓你是具备操作ERC721的能力的,比如转账,那么你需要在你的合约中实现这个ERC721Receiver接口的onERC721Received函数;否则调用者就无法知晓你这个合约到底是不是支持ERC721接口的,那么safeTransferFrom就会认为不支持,则判定失败,回滚交易。
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
那么实现了或者重载了,怎么认为他就具备处理ERC721的能力了?只需要这个函数返回值是`bytes4`类型,并且返回值为`this.onERC721Received.selector`,这样可以确保函数符合ERC721接口中定义的规范。比如:
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external override returns (bytes4) {
// 这里可以添加处理逻辑,例如记录接收到的代币信息
// 返回这个值表明成功接收
return this.onERC721Received.selector;
}
四 手动实现ERC-721
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./IERC721.sol";
abstract contract ERC720 is IERC721 {
string private _name;
string private _symbol;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _approvals;
mapping(address owner => mapping(address operator => bool))
private _operatorApprovals;
constructor(string memory name, string memory symbol) {
_name = name;
_symbol = symbol;
}
function name() public view virtual returns (string memory) {
return _name;
}
function symbol() public view virtual returns (string memory) {
return _symbol;
}
function tokenURI(uint256 tokenId) public view virtual returns (string memory) {
require(_owners[tokenId] != address(0), "Not exists owner");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenId.toString()) : "";
}
// Any contract extends the ERC721 contract could ovveride the function and set the baseURI value, default is empty string
function _baseURI() internal view virtual returns (string memory) {
return "";
}
function balanceOf(
address owner
) external view override returns (uint256 balance) {
return _balances[owner];
}
function ownerOf(
uint256 tokenId
) external view override returns (address owner) {
return _owners[tokenId];
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external override {
_checkOnERC721Received(from, to, tokenId, data);
transferFrom(from, to, tokenId);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external override {
safeTransferFrom(from, to, tokenId, "");
}
/**
* Transfer ownership of an NFT
* @param from
* @param to
* @param tokenId
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external override {
// check if msg.sender has approved
require(_checkPermission(from, msg.sender, tokenId), "msg.sender has no previleges to transfer");
// update owner balance
address prev = _update(to, tokenId, msg.sender);
require(prev == from, "Incorrect owner");
}
// Approve permisson to `to` address for tokenId
function approve(address to, uint256 tokenId) external override {
_approve(to, tokenId, msg.sender);
}
function setApprovalForAll(
address operator,
bool approved
) external override {
require(operator != address(0), "Invalid operator");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function getApproved(
uint256 tokenId
) external view override returns (address operator) {
require(_owners[tokenId] != address(0), "Not exist owner");
return _getApproved(tokenId);
}
function isApprovedForAll(
address owner,
address operator
) public view override returns (bool) {
return _operatorApprovals[owner][operator];
}
function _update(address to, uint256 tokenId, address operator) internal virtual returns (address) {
address from = _owners[tokenId];
if (operator != address(0)) {
_checkPermission(from, operator, tokenId);
}
if (from != address(0)) {
unchecked {
_balances[from] -= 1;
}
}
if (to != address(0)) {
unchecked {
_balances[to] += 1;
}
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
return from;
}
function _approve(address to, uint256 tokenId, address owner) internal virtual {
require(_owners[tokenId] == owner, "Incorrect owner address")
require(to != address(0), "Invalid to address");
_approvals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
function _getApproved(uint256 tokenId) internal view virtual returns (address) {
return approvals[tokenId];
}
/*
* Check if owner approved for spender about the tokenId
* @param owner
* @param spender
* @param tokenId
*/
function _checkPermission(address owner, address operator, uint256 tokenId) internal returns (bool) {
require(_owners[tokenId] == owner, "Incorrect owner");
require(operator != address(0), "Illegal operator address");
// If operator is owner, so operator must have the permission
if (owner == operator) {
return true;
}
// If operator is approved, return true
if (_getApproved(tokenId) == operator) {
return true;
}
return isApprovedForAll(owner, operator);
}
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) internal {
// check smart contract code length, EOA address length is zero, CA address length is greater than zero
if (to.code.length > 0) {
// if to address is contract address, see if implements the IERC721Receiver interface
// if IERC721Receiver.onERC721Received return value is IERC721Receiver.onERC721Received.selector
// it means the contract could operate NFT, such as transfer .etc
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
revert ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
revert ERC721InvalidReceiver(to);
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
/**
* Get nft approver address by tokenId
*/
function _getApproved(uint256 tokenId) internal view virtual returns (address) {
return _approvals[tokenId];
}
/**
* Mint nft to `to` address
* @param to
* @param tokenId
*/
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "Invalid mint address");
unchecked {
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
function _safeMint(address to, uint256 tokenId) internal {
_safeMint(to, tokenId, "");
}
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
_mint(to, tokenId);
_checkOnERC721Received(address(0), to, tokenId, data);
}
}