最近看了一些智能合約,發現用的最經常的ERC標准為ERC-20、ERC-165、ERC-721,以此學習一下做個筆記
ERC-20
ERC-20標准接口主要是在智能合約中實現了代幣的標准api,也就是通過函數來為代幣提供基本的功能:例如代幣轉賬、授權等等。
可選函數
ERC-20標准接口中有三個可選函數,主要是返回代幣的基本信息,所以也可以稱為元數據。我們來看某個智能合約中的代碼:
// IERC20Metadata.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
interface IERC20Metadata is IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
其中,函數name()
:返回的是代幣的名稱,例如Binance Coin
函數symbol()
:返回的是代幣的代號,例如BNB
函數decimals()
:返回的是代幣的使用的小數位數,例如8(意味着將代幣總量除以100000000來獲取表現的形式)
必要函數
繼續看上一個智能合約中標准定義的ERC20的必要函數
// IERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
其中,函數totalSupply
:返回總令牌供應量
函數balanceOf
:返回賬戶的余額
函數transfer
:向to
地址轉移value
數量的代幣,函數必須觸發事件Transfer
函數transferFrom
:從from
向to
地址轉移value
數量的代幣,函數必須觸發事件Transfer
transferFrom函數,可以允許第三方代表我們轉移代幣。
如果 _from 賬號沒有授權調用帳戶轉移代幣,則該函數需要拋出異常。
函數approve
:授權spender
可以從我們在賬戶最多轉移代幣的數量value
,可以多次轉移,但總量不超過value
這個函數可以再次調用,以覆蓋授權額度
_value
,調用者可以在調整授權額度時,先設置為0,然后在設置為一個其他額度
函數allowance
:查詢owner
授權給spender
的額度
事件Transfer
:當有代幣轉移時,必須觸發Transfer
事件
如果是新產生代幣,觸發 Transfer 事件的
from
應該設置為0x0
。
事件Approval
:approve
函數成功執行時,必須觸發Approval
事件
ERC-165
ERC-165只是一個標准,要求使用一種標准的方法去發布或者檢測(supportsInterface)一個智能合約所實現的接口。
//IERC165.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
我們可以看到接口的supportsInterface
函數需要一個參數,為接口id
函數選擇器:可以使用 bytes4(keccak256('function_name(bytes4)'));
計算得到,或者使用合約函數的selector
方法
接口ID:為接口中所有函數選擇器的異或(XOR)
例如:
pragma solidity ^0.4.20;
interface Solidity101 {
function hello() external pure;
function world(int) external pure;
}
contract Selector {
function calculateSelector() public pure returns (bytes4) {
Solidity101 i;
return i.hello.selector ^ i.world.selector;
}
}
實現ERC-165標准的合約:
//ERC165.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*
* Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
兼容 ERC-165的合約應該實現接口 ERC165.sol(上面的IERC165.sol)
這個接口的接口ID 為 0x01ffc9a7
因此,若合約實現supportInterface函數將返回:
- true:當接口ID
interfaceID
是0x01ffc9a7
(EIP165標准接口),返回true - false:當
interfaceID
是0xffffffff
,返回false - true:任何合約實現了接口的
interfaceID
都返回true - false:其他的返回false
以上是檢測合約是否采用ERC-165接口的方法,那么如何檢測合約是否實現了某個接口呢?
- 若不確定合約是否實現ERC-165,則按上面的檢測步驟檢測
- 如果沒有實現ERC-165,則不得不查看它采用哪種老式方法
- 如果實現了ERC-165,則直接調用supportInterface(InterfaceID)來確定它是否實現了對應的接口
ERC-721
ERC-721:非同質化(Non-Fungible Token,以下簡稱 NFT 或 NFTs)代幣標准
-
是智能合約中實現 NFT 的標准API。 標准提供了跟蹤和轉移NFTs的基本功能。
-
NFT可以代表對數字或物理資產的所有權。非同質代表獨一無二,NFT是可區分的
-
ERC20代幣是可置換的,且可細分為N份(1 = 10 * 0.1), 而ERC721的Token最小的單位為1,無法再分割。
每個符合ERC-721的合同都必須實現 ERC721
和 ERC165
接口
pragma solidity ^0.4.20;
interface IERC721 is IERC165 {
// 變更NFT所有權、NFT的創建(from == 0)和銷毀時(to == 0)觸發。合約創建時除外
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// 在NFT的授權地址approved address變更或者重新確認時被觸發
// 發起transfer時,approved address會被重置為none
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
// operator被授權或撤權時觸發。operator可以管理owner的所有NFT
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// 返回由owner持有的NFTs的數量。
function balanceOf(address owner) external view returns (uint256 balance);
// 返回tokenId代幣持有者的地址
function ownerOf(uint256 tokenId) external view returns (address owner);
// 將NFT的所有權從一個地址轉移到另一個地址
// 1. 調用者msg.sender應該是當前tokenId的所有者或被授權的地址
// 2. from不是tokenId的所有者 、to是零地址、tokenId不是有效id均拋出異常。
// 3. 當轉移完成時,函數檢查 to是否是合約,如果是,調用onERC721Receiver方法,並檢查其返回值是否是0x150b7a02,(即bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")),如果不是則拋出異常
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
// 轉移所有權 調用者負責確認to是否有能力接收NFTs,否則可能永久丟失
// 1. 調用者msg.sender應該是當前tokenId的所有者或被授權的地址
// 2. from不是tokenId的所有者 、to是零地址、tokenId不是有效id 均拋出異常。
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
// 授予地址to具有tokenId的控制權,方法成功后需觸發Approval 事件。
function approve(address to, uint256 tokenId) external;
// 獲取單個NFT的授權地址
function getApproved(uint256 tokenId) external view returns (address operator);
// 啟用或禁用第三方(操作員)管理msg.sender所有資產
// 觸發 ApprovalForAll 事件,合約必須允許每個所有者可以有多個操作員
// approved True 表示授權, false 表示撤銷
function setApprovalForAll(address operator, bool _approved) external;
// 查詢一個地址是否是另一個地址的授權操作員
function isApprovedForAll(address owner, address operator) external view returns (bool);
// 將NFT的所有權從一個地址轉移到另一個地址,功能同上,附帶data參數,傳遞給接收者
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
}
如果接收NFT的是合約,則需要實現以下接口:
// IERC721Receiver.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721Receiver {
// 處理接收NFT,ERC721智能合約在transfer完成后,在接收這地址上調用這個函數
// 調用這個函數的 msg.sender是ERC721的合約地址
/// @param _from :之前的NFT擁有者
/// @param _tokenId : NFT token id
/// @param _data : 附加信息
/// @return 正確處理時返回bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
可選擴展函數-元信息:
//IERC721Metadata.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../IERC721.sol";
interface IERC721Metadata is IERC721 {
// NFTs集合的名字
function name() external view returns (string memory);
// NFTs縮寫代號
function symbol() external view returns (string memory);
// 一個給定資產的唯一的統一資源標識符(URI)
// URI 也許指向一個 符合 "ERC721 元數據 JSON Schema" 的 JSON 文件
function tokenURI(uint256 tokenId) external view returns (string memory);
}
ERC721 元數據 JSON Schema描述:
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "指示NFT代表什么"
},
"description": {
"type": "string",
"description": "描述NFT 代表的資產"
},
"image": {
"type": "string",
"description": "指向NFT表示資產的資源的URI(MIME 類型為 image/*) , 可以考慮寬度在320到1080像素之間,寬高比在1.91:1到4:5之間的圖像。
}
}
}
ERC-721標准的枚舉擴展信息(可選):
// IERC721Enumerable.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../IERC721.sol";
interface IERC721Enumerable is IERC721 {
//NFTs 總數
function totalSupply() external view returns (uint256);
// 枚舉索引某個所有者的 NFTs
// 如果index >= balanceOf(owner)或owner為零地址則拋出異常
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId);
// 枚舉索引NFT,index應小於totalSupply()
function tokenByIndex(uint256 index) external view returns (uint256);
}