写在前面
智能合约可以实现多种多样的业务逻辑,实际应用场景最多的逃不过同质化、非同质化代币的发行与转移。本文就是选取了在ERC一系列标准中,被广泛应用的ERC-20,ERC-721和ERC-1155作为学习案例,结合协议标准和OpenZepplin合约库中对这三个标准的具体实现学习这三种合约。
ERC-20
设计初衷
允许以太坊上的任何代币被其他应用程序重复使用:从钱包到去中心化交易所。 接口定义如下:
pragma solidity ^0.6.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
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);
}
ERC-20合约示例
继承OZ的ERC-20合约,然后实现自己的一个ERC-20代币的发行,例子如下:
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20, Ownable {
constructor() ERC20("Token demo", "TESTER") {}
function mint(address account, uint256 amount) public onlyOwner {
_mint(account, amount);
}
// add other functions
}
由于在之前的一篇文章中(实战练习Solidity(5)——库合约的使用) 已经对ERC-20的实现合约进行了学习和测试,这里就不再重复叙述了。
ERC-721
设计初衷
追踪和转移非同质化代币。
关于NFT的转移设计 NFT可以在以下情况发起转移:
- The owner of an NFT
- The approved address of an NFT
- An authorized operator of the current owner of an NFT
ERC-721与ERC-20的比较
在ERC-721定义的这些方法里,有一些是和ERC-20中功能相似的,比如:balanceOf, transferFrom 和approve,但也有一个是ERC-721标准独有的方法:
function ownerOf(uint256 _tokenId) external view returns (address);
另外,ERC-721标准的补充定义里有对metadata的定义:
function name() external view returns (string _name);
function symbol() external view returns (string _symbol);
function tokenURI(uint256 _tokenId) external view returns (string);
值得关注的事tokenURI方法,这个方法返回的是一个string类型的URI,这个URI指向一个包含tokenId对应token的名字、描述和图片的JSON数据。规范中没有具体说明存储此元数据的位置和方式,这个可以由开发者自行定义,既可以在中心化存储上,也可以在去中心化网络上存储。
关于接收ERC721的token
如果一个合约要接收ERC721,它应该做如下实现:
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) public returns (bytes4) {
return this.onERC721Received.selector;
}
这个方法会返回一个神奇的4 bytes的数据,在OZ里提供了库合约ERC721Holder来实现这个功能,我们在使用时可以通过引入并继承这个合约就可以了,不必再自定义实现onERC721Received 方法。
import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol";
contract ReceiveNFTTest is ERC721Holder{
...
}
关于mint功能的定义
疑问:为什么在ERC-20 和ERC-721协议标准里都没有关于mint功能的定义?
可能的答案: ERC20和ERC721没有包含铸造(mint)功能的原因是出于设计考虑。这些标准协议旨在提供一种通用的方式来定义代币和非同质化代币(NFT),并且不应该限制其实现方式。 如果这些协议包含了铸造功能,那么它们将强制要求所有实现都必须支持此功能,从而限制了开发者的自由度。相反,这些协议允许开发者根据自己的需求选择是否需要添加铸造功能,并使用其他方法来创建新代币或NFT。
ERC-721合约示例##
创建一个nft合约,继承自OZ的721合约,并测试一下它。
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract Anft is ERC721 {
constructor() ERC721("Anft", "ANFT") {}
function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}
}
测试用例
部署完Anft后给A mint tokenId 为1,2,3的token,给B mint tokenId为8的token
- 通过balanceOf查询A的余额,预期A的余额为3,查询C的余额,预期C的余额为0
- 通过ownerOf查询tokenId为8的owner,预期为B的地址
- B通过approve方法将tokenId为8的token授权给C,再通过getApproved查询tokenId为8的token的授权地址,预期为C。A通过transferFrom方法,将tokenId为8的token从B转移给C,转移失败发生revert
- B通过approve方法将tokenId为8的token授权给C,再通过getApproved查询tokenId为8的token的授权地址,预期为C。C通过transferFrom方法,将tokenId为8的token从B转移给C,此时再用ownerOf查询tokenId为8的owner,预期为C
- B通过approve方法将tokenId为8的token授权给C,再通过getApproved查询tokenId为8的token的授权地址,预期为C。A通过safeTransferFrom方法,将tokenId为8的token从B转移给C,转移失败发生revert
- B通过approve方法将tokenId为8的token授权给C,再通过getApproved查询tokenId为8的token的授权地址,预期为C。C通过safeTransferFrom方法,将tokenId为8的token从B转移给C,此时再用ownerOf查询tokenId为8的owner,预期为C
- A通过setApprovalForAll方法授权给B,isApprovedForAll查询A是否授权给B,预期结果为true。B通过transferFrom方法,将tokenId为1的token从A转移给B,预期转移成功
- A通过setApprovalForAll方法授权给B,isApprovedForAll查询A是否授权给B,预期结果为true。B通过safeTransferFrom方法,将tokenId为1的token从A转移给B,预期转移成功
- A通过setApprovalForAll方法授权给B,isApprovedForAll查询A是否授权给B,预期结果为true。C通过transferFrom方法,将tokenId为1的token从A转移给B,预期转移失败发生revert
- A通过setApprovalForAll方法授权给B,isApprovedForAll查询A是否授权给B,预期结果为true。C通过transferFrom方法,将tokenId为1的token从A转移给C,预期转移失败发生revert
- A先通过setApprovalForAll方法授权给B,再通过setApprovalForAll方法设置取消授权B的操作,此时B通过transferFrom方法,将tokenId为1的token从A转移给B,预期转移失败发生revert
Overload function in Solidity
在用Hardhat写测试用例的时候,遇到了一个新的问题,即在用safeTransferFrom的时候出现:TypeError: instance.connect(...).safeTransferFrom is not a function
经过google,找到了问题答案
If you have two methods with the same name, you must specify the fully qualified signature to access it. You can use either of the following, depending on which you want:
contract["mint(uint256)"](amount) // or contract["mint(address,uint256)"](account, amount)
If you only use one of those methods, you can simply drop the unused one from the ABI you pass into the Contract constructor, then you can use the name
mint
to access it.
调用重载函数时候的写法参考下图:
ERC-1155
设计初衷
ERC1155结合了ERC20和ERC721的能力,支持开发同质化的、半同质化的、非同质化的代币和其他配置的通用智能合约。
ERC-1155合约示例
一个继承OZ 1155合约的例子:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract Bnft is ERC1155{
uint256 public constant Dog=1;
uint256 public constant Cat=2;
uint256 public constant Mouse=3;
constructor() ERC1155("https://ipfs.io/ipfs/bafybeihjjkwdrxxjnuwevlqtqmh3iegcadc32sio4wmo7bv2gbf34qs34a/{id}.json") {
_mint(msg.sender, Dog, 1, "");
_mint(msg.sender, Cat, 1, "");
_mint(msg.sender, Mouse, 1, "");
}
function uri(uint256 _tokenid) override public pure returns (string memory) {
return string(
abi.encodePacked(
"https://ipfs.io/ipfs/bafybeihjjkwdrxxjnuwevlqtqmh3iegcadc32sio4wmo7bv2gbf34qs34a/",
Strings.toString(_tokenid),".json"
)
);
}
function mint(address to,
uint256 id,
uint256 amount
) public {
_mint(to, id,amount,"");
}
function mintBatch (
address to,
uint256[] memory ids,
uint256[] memory amounts
) public{
_mintBatch(to,ids,amounts,"");
}
}
ERC-1155与ERC-721的关键区别
- ERC-1155允许创建半可替代代币和不可替代代币,而ERC-721只允许后者。
- 在ERC-1155中,智能合约链接到多个URI,并且不存储额外的元数据(如文件名)。相比之下,ERC-721仅支持为每个代币ID直接存储在智能合约上的静态元数据,这增加了部署成本并限制了灵活性。
- ERC-1155的智能合约支持无限数量的代币,而ERC-721需要为每种类型的代币提供一个新的智能合约。
- ERC-1155还允许代币的批量转移,这可以减少交易成本和时间。对于ERC-721,如果用户想要发送多个代币,它们会分别发生。
学习资料
ERC协议标准:eips.ethereum.org/ OZ的实现:github.com/OpenZeppeli…