最近看了一些智能合约,发现用的最经常的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);
}