--- 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) :::