---
title: 'Solidity WTF 103 34 單元 ERC721'
lang: zh-tw
---
Solidity WTF 103 34 單元 ERC721
===
:::info
:date: 2024/10/12
:::
[TOC]
# ERC721
`ERC721`是`非同質化代幣`的`標準協議`,非同質化代幣就是俗稱的`NFT`(`Non-Fungible Token`),為何會有這種東西產生?
像是`BTC`和`ETH`這類是屬於同質化代幣,當礦工挖出第`1`枚與第`10000`枚並沒有不同,都是等於一顆的價錢,他只會隨著市場波動而改變。但是世界有很多東西是不同質的,包括房地產、古董、藝術品等類型的,無法有同質化代幣的抽象去表達。因此`EIP721`提出了`ERC721`標準,用來抽象這類非同質化的物品。
:::success
`ERC721`代幣可以簡稱為`NFT`。
:::
## IERC165
首先先介紹`ERC165`,因為`ERC165`標準可以供其他合約檢查,簡單來說,`ERC165`就是檢查一個智能合約是否支持`ERC721`和`ERC155`的接口。
在`IERC165`中,只聲明了一個`supportsInterface`函數且返回`bool`,輸入要查詢的`interfaceId`接口`id`,若合約實現了該接口`id`,則返回`true`:
```javascript=
interface IERC165 {
/**
* @dev 如果合約實現了查詢的`interfaceId`,則返回true
* 規則:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
*
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
```
> ERC721中實現supportInterface()函數:
```javascript=
function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}
```
:::success
`ERC165`檢查一個智能合約是否支持`ERC721`和`ERC155`的`接口`。
:::
## IERC721
是`ERC721`的接口合約,規定了`ERC721`的一些基本函數。利用`tokenId`來表示非同質化代幣,授權或轉帳都要給予`tokenId`,用來標註是要用哪一個`NFT`。而`ERC20`只需要轉帳的代幣數量即可。
```javascript=
/**
* @dev ERC721標準介面,用於不可替代代幣(NFT)的操作。
*/
interface IERC721 is IERC165 {
/**
* @dev 在轉帳時釋放,紀錄代幣的發出地址from,接受地址to和tokenid。
* @param from 代幣的轉出者地址
* @param to 代幣的接收者地址
* @param tokenId 被轉移的代幣ID
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev 授權時釋放,紀錄授權地址owner,被授權地址approved和tokenid。
* @param owner 擁有代幣的地址
* @param approved 被授權代幣的地址
* @param tokenId 授權操作的代幣ID
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev 在批量授權時釋放,紀錄批量授權的發出地址owner,被授權地址operator和授權與否的approved。
* @param owner 擁有代幣的地址
* @param operator 被授權或取消授權的操作地址
* @param approved 是否授權(`true`為授權,`false`為取消授權)
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev 返回指定地址`owner`持有的NFT數量。
* @param owner 擁有代幣的地址
* @return balance 該地址持有的代幣數量
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev 返回指定代幣`tokenId`的擁有者地址。
* @param tokenId 代幣的唯一ID
* @return owner 該代幣的擁有者地址
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev 安全地將`tokenId`代幣從`from`轉移到`to`,並附加額外的資料`data`。
* 如果接收者是合約地址,則需要實現`IERC721Receiver.onERC721Received`來確認接收。
* 安全轉帳的重載函數。
* @param from 代幣的轉出者地址
* @param to 代幣的接收者地址
* @param tokenId 被轉移的代幣ID
* @param data 附加的轉移資料(可選)
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
/**
* @dev 普通轉帳,將`tokenId`代幣從`from`轉移到`to`。
* @param from 代幣的轉出者地址
* @param to 代幣的接收者地址
* @param tokenId 被轉移的代幣ID
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev 授權`to`可以轉移`tokenId`代幣。只有代幣的擁有者或已被授權的操作員可以調用此函數。
* @param to 被授權轉移代幣的地址
* @param tokenId 被授權操作的代幣ID
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev 將自己持有的該系列NFT批量授權給某個地址operator。
* @param operator 被授權操作所有代幣的地址
* @param _approved 授權狀態(`true`為授權,`false`為取消授權)
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev 返回`tokenId`被批准給那些地址。
* @param tokenId 被授權的代幣ID
* @return operator 被授權操作該代幣的地址
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev 查詢某地址的NFT是否批量授權給了另一個operatro地址。
* @param owner 擁有代幣的地址
* @param operator 被授權操作代幣的地址
* @return true 如果`operator`被授權,否則為false
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
```
## IERC721Receiver
如果合約沒有實現`ERC721`相關函數,轉入NFT就等同於轉入黑洞。為防止誤轉帳,`ERC721`實現`safeTransferFrom()`安全轉帳函數,目標合約必須實現`IERC721Receiver`接口才能接收`ERC721`代幣,否則會`revert`。
>IERC721Receiver接口只包含一个onERC721Received()函數。
```javascript=
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}
```
:::warning
一定要實現此函數,否則無法轉入
:::
## IERC721Metadata
`IERC721Metadata`是`ERC721`擴展接口,規定三個查詢metadata數組的函數:
- `name()`: 返回代幣名稱。
- `symbol()`: 返回代幣代號。
- `tokenURI()`: 通過`tokenId`查詢`metadata`的連接`url`,`ERC721`特有的函數。
```javascript!
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實現了IERC721、IERC165和IERC721Metadata定義的函數,其中包含四個狀態變量與17個函數。
```javascript!
// SPDX-License-Identifier: MIT
// 作者: 0xAA
pragma solidity ^0.8.21;
// 引入相關的接口
import "./IERC165.sol"; // 引入IERC165接口
import "./IERC721.sol"; // 引入IERC721接口
import "./IERC721Receiver.sol"; // 引入IERC721Receiver接口
import "./IERC721Metadata.sol"; // 引入IERC721Metadata接口
import "./String.sol"; // 引入String庫
contract ERC721 is IERC721, IERC721Metadata {
using Strings for uint256; // 使用String庫處理uint256類型的轉換
// 代幣名稱
string public override name;
// 代幣代號
string public override symbol;
// tokenId 到持有者地址的映射
mapping(uint => address) private _owners;
// 地址到持有代幣數量的映射
mapping(address => uint) private _balances;
// tokenId 到授權地址的映射
mapping(uint => address) private _tokenApprovals;
// 擁有者地址到operator地址的批量授權映射
mapping(address => mapping(address => bool)) private _operatorApprovals;
// 錯誤:無效的接收者
error ERC721InvalidReceiver(address receiver);
/**
* 构造函数,初始化代幣的名稱和代號
*/
constructor(string memory name_, string memory symbol_) {
name = name_; // 設置代幣名稱
symbol = symbol_; // 設置代幣代號
}
// 實現IERC165接口的supportsInterface
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IERC721).interfaceId || // 支援IERC721接口
interfaceId == type(IERC165).interfaceId || // 支援IERC165接口
interfaceId == type(IERC721Metadata).interfaceId; // 支援IERC721Metadata接口
}
// 實現IERC721的balanceOf,利用_balances變量查詢owner地址的持有數量
function balanceOf(address owner) external view override returns (uint) {
require(owner != address(0), "owner = zero address"); // 確保地址不是零地址
return _balances[owner]; // 返回該地址持有的代幣數量
}
// 實現IERC721的ownerOf,利用_owners變量查詢tokenId的擁有者
function ownerOf(uint tokenId) public view override returns (address owner) {
owner = _owners[tokenId]; // 獲取tokenId的擁有者
require(owner != address(0), "token doesn't exist"); // 確保代幣存在
}
// 實現IERC721的isApprovedForAll,查詢owner是否將所有代幣批量授權給operator
function isApprovedForAll(address owner, address operator)
external
view
override
returns (bool)
{
return _operatorApprovals[owner][operator]; // 返回批量授權的狀態
}
// 實現IERC721的setApprovalForAll,將所有代幣授權給operator地址
function setApprovalForAll(address operator, bool approved) external override {
_operatorApprovals[msg.sender][operator] = approved; // 設置批量授權
emit ApprovalForAll(msg.sender, operator, approved); // 發出ApprovalForAll事件
}
// 實現IERC721的getApproved,查詢tokenId的授權地址
function getApproved(uint tokenId) external view override returns (address) {
require(_owners[tokenId] != address(0), "token doesn't exist"); // 確保代幣存在
return _tokenApprovals[tokenId]; // 返回授權地址
}
// 授權函數,設置tokenId的授權地址
function _approve(
address owner,
address to,
uint tokenId
) private {
_tokenApprovals[tokenId] = to; // 設置tokenId的授權地址
emit Approval(owner, to, tokenId); // 發出Approval事件
}
// 實現IERC721的approve,將tokenId授權給to地址
function approve(address to, uint tokenId) external override {
address owner = _owners[tokenId]; // 獲取tokenId的擁有者
require(
msg.sender == owner || _operatorApprovals[owner][msg.sender],
"not owner nor approved for all" // 確保授權者是擁有者或已獲批的operator
);
_approve(owner, to, tokenId); // 呼叫授權函數
}
// 查詢spender地址是否可以使用tokenId(必須是擁有者或已授權地址)
function _isApprovedOrOwner(
address owner,
address spender,
uint tokenId
) private view returns (bool) {
return (spender == owner || // spender是擁有者
_tokenApprovals[tokenId] == spender || // spender是授權地址
_operatorApprovals[owner][spender]); // spender是批量授權的operator
}
/*
* 轉帳函數,將tokenId從from轉帳給to
* 條件:
* 1. tokenId被from擁有
* 2. to地址不是零地址
*/
function _transfer(
address owner,
address from,
address to,
uint tokenId
) private {
require(from == owner, "not owner"); // 確保from是擁有者
require(to != address(0), "transfer to the zero address"); // 確保to不是零地址
_approve(owner, address(0), tokenId); // 清除tokenId的授權
_balances[from] -= 1; // 減少from的持有數量
_balances[to] += 1; // 增加to的持有數量
_owners[tokenId] = to; // 更新擁有者
emit Transfer(from, to, tokenId); // 發出Transfer事件
}
// 實現IERC721的transferFrom,非安全轉帳,不建議使用
function transferFrom(
address from,
address to,
uint tokenId
) external override {
address owner = ownerOf(tokenId); // 獲取tokenId的擁有者
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved" // 確保msg.sender是擁有者或授權者
);
_transfer(owner, from, to, tokenId); // 執行轉帳
}
/**
* 安全轉帳,確保tokenId從from轉移到to時,檢查合約接收者是否了解ERC721協議
* 會檢查接收者的合約是否實現IERC721Receiver
*/
function _safeTransfer(
address owner,
address from,
address to,
uint tokenId,
bytes memory _data
) private {
_transfer(owner, from, to, tokenId); // 執行轉帳
_checkOnERC721Received(from, to, tokenId, _data); // 檢查接收者
}
/**
* 實現IERC721的safeTransferFrom,安全轉帳,呼叫了_safeTransfer函數
*/
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes memory _data
) public override {
address owner = ownerOf(tokenId); // 獲取tokenId的擁有者
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved" // 確保msg.sender是擁有者或授權者
);
_safeTransfer(owner, from, to, tokenId, _data); // 執行安全轉帳
}
// safeTransferFrom的重載函數
function safeTransferFrom(
address from,
address to,
uint tokenId
) external override {
safeTransferFrom(from, to, tokenId, ""); // 調用帶有空數據的重載函數
}
/**
* 鑄造函數,將tokenId的代幣鑄造並轉帳給to
* 此函數應該由開發者重寫,加上相應的條件
* 條件:
* 1. tokenId尚不存在
* 2. to地址不是零地址
*/
function _mint(address to, uint tokenId) internal virtual {
require(to != address(0), "mint to the zero address"); // 確保to不是零地址
require(_owners[tokenId] == address(0), "token already minted"); // 確保tokenId尚未存在
_balances[to] += 1; // 增加to的持有數量
_owners[tokenId] = to; // 設置tokenId的擁有者
emit Transfer(address(0), to, tokenId); // 發出Transfer事件
}
// 銷毀函數,銷毀tokenId的代幣
function _burn(uint tokenId) internal {
address owner = ownerOf(tokenId); // 獲取tokenId的擁有者
_approve(owner, address(0), tokenId); // 清除tokenId的授權
_balances[owner] -= 1; // 減少擁有者的持有數量
delete _owners[tokenId]; // 刪除tokenId的擁有者
emit Transfer(owner, address(0), tokenId); // 發出Transfer事件
}
// 檢查合約接收者是否實現IERC721Receiver接口
function _checkOnERC721Received(
address from,
address to,
uint tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) { // 如果to是一個合約地址
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data); // 呼叫接收者的onERC721Received函數
return retval == IERC721Receiver.onERC721Received.selector; // 返回結果
}
return true; // 非合約地址返回true
}
/**
* 實現 IERC721Metadata 的 tokenURI 函數,查詢 metadata。
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_owners[tokenId] != address(0), "代幣不存在"); // 確保 tokenId 存在
string memory baseURI = _baseURI(); // 獲取基本 URI
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; // 拼接 baseURI 和 tokenId
}
/**
* 計算 {tokenURI} 的 BaseURI,tokenURI 就是把 baseURI 和 tokenId 拼接在一起,需要開發者重寫。
* BAYC 的 baseURI 為 ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
*/
function _baseURI() internal view virtual returns (string memory) {
return ""; // 返回空的基本 URI,需由開發者實現
}
}
```
# 寫一個免費鑄造的APE
利用`ERC721`寫一個免費鑄造代幣,總量設置為`10000`,只需重寫一下`mint()`和`baseURI()`即可。由於`baseURI()`設置的何`BAYC`一樣,元數據會直接獲取無聊猿的。
```javascript!
// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.21;
import "./ERC721.sol";
contract AlengApe is ERC721{
uint public MAX_APES = 10000; // 總量
// 建構函數
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
}
//BAYC的baseURI為ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
}
// 鑄造函數
function mint(address to, uint tokenId) external {
require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range");
_mint(to, tokenId);
}
}
```
# ERC165 與 ERC721
在ERC721裡面實現了IERC165的接口,而ERC165前面有提到是用來驗證是否是ERC721。驗證方式是把IERC函數做計算。
```javascript=
ERC721 : bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ··· ^keccak256(ERC721.isApprovedForAll.selector))
最終結果是**0x80ac58cd**
```
> 把所有函數用keccak256 + bytes4後與其他函數一起運算
> 同時也能驗證ERC721的擴展接口還有ERC165本身。
## 比對方式
可以看成是下列這種方式
```javascript!
// 原本是這樣的方式
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId;
}
// 可以看成這樣
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata
interfaceId == 0x780e9d63; // ERC165 Interface ID for ERC721Enumerable
}
```
:::warning
這章重點我認為在IERC165比對方式,還有ERC721擴充接口需要記
:::
:::success
基本上只要看到有合約實現了ERC721TokenReceiver接口(具體而言是實現了onERC721Received這個函數),該合約就是擁有管理NFT的能力。
:::
:::info
參閱,[EIP-165](https://eips.ethereum.org/EIPS/eip-165#simple-summary) [EIP-721](https://eips.ethereum.org/EIPS/eip-721)
:::