ERC1155: 批发小能手,妈妈再也不用担心网络拥堵造成的gas费飙升啦
Hello ~ 大家好,首先感谢大家对本系列前两篇文章 👇👇👇 的喜爱,不知读者们都学废(不是,是学会)了吗?
ERC20:从入门到飞起,妈妈再也不用担心我不会写Token合约了
ERC721:全生命周期精析,妈妈再也不用担心我不会玩NFT合约啦
今天主要想跟大家聊的是 ERC1155 。了解过前两个标准的读者都知道,ERC20 和 ERC721 对应的令牌标准是不同的,1⃣️ 当业务场景同时涉及这两种令牌标准时,我们需要分别部署满足这两种标准的合约; 2⃣️ 当存在批量发售(mint)/ 转账(transfer)等需求时,我们需要在合约中额外实现相关接口以减少冗余操作并节约 gas 费用。
然鹅,神奇的 ERC1155 在满足 ERC20 和 ERC721 双标准的同时,还一并解决了批发需求。接下来让我们一起看看 Openzeppelin 对 ERC1155 标准的实现吧~~~
———————————————————————— 分割线 ————————————————————————
由于篇幅有限,本博客将围绕 ERC1155 区别于ERC20 和 ERC721 的核心特点展开介绍,文章内容尽量做到通俗易懂,但其中不可避免地可能涉及一些新手不友好的概念,您可以查阅相关博客做进一步了解,本系列博客也会不断扩充、提升及优化,尽量做到不留死角,人人都能上手Solidity标准开发。
0. ERC 是什么鬼?
ERC 全称 Ethereum Request For Comment (以太坊意见征求稿), 是以太坊上应用级的开发标准和协议(application-level standards and conventions),为以太坊开发人员提供了实施标准,开发人员可以使用这些标准来构建智能合约。
ERC 的雏形是开发人员提交的EIP(Ethereum Improvement Proposals),即新的ERC标准提案,一旦其得到以太坊委员会的批准并最终确定,新的ERC便由此诞生。
1. 初识 ERC 1155
ERC1155 是多资产( FT、NFT) 的 API 标准,在 ERC20 和 ERC721 的基础上引入了批量的概念,在 ERC1155 中,每个资产 id 既可以是同质资产 FT ,也可以是非同质资产 NFT,项目方可在仅部署一个 ERC1155 合约的基础上同时实现 ERC20 和 ERC721 资产标准。本文将基于Openzeppelin 中实现的 ERC1155 标准,针对其新增的批量概念进行代码精析,由于作者经验有限,欢迎广大读者批评指正。
2. 批发大佬 ERC1155 的四大金刚
金刚1: 全新的余额管理办法
在 ERC20 中,余额存在账户地址到金额的映射中;
在 ERC721 中,余额不仅是账户拥有的 NFT 数量,存在账户地址到数量的映射中,还是账户地址对特定 NFT 的 id 的所有权,存在 NFT id 到账户地址的映射中;
对ERC1155而言,余额是资产 id 到账户地址,再到资产余额 / 数量的映射。当该资产是ERC20代币时,通过 balanceOf 函数返回的是传入地址持有的传入资产的余额;当该资产是ERC721代币时,通过 balanceOf 函数返回的是传入地址持有的传入资产的数量。
// ERC20:
mapping(address => uint256) private _balances;
// ERC721:
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _owners;
// ERC1155:
mapping(uint256 => mapping(address => uint256)) private _balances;
// ERC1155中的 balanceOf 函数:
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: balance query for the zero address");
return _balances[id][account];
}
ERC1155 也实现了批量的余额查询接口:balanceOfBatch
。该接口允许 caller 批量传入要查询的账户地址和资产 id 数组,查询前会检查以确认两个传入数组长度相等,接着新建一个等长的数组 batchBalance
用于保存查到的资产余额。然后通过循环便利的方式调用 balanceOf
函数逐一进行指定地址特定资产id 的余额 / 数量查询,并按顺序存入数组 batchBalance
,最后返回该数组作为此接口调用的返回值。
function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
public
view
virtual
override
returns (uint256[] memory)
{
require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}
金刚2: 全新的资产转移办法
caller 可以通过 safeBatchTransferFrom
接口以数组方式批量传入想要批量转移的资产 id 和数量。该函数首先会进行资产转移的权限检查:即要求 caller 是资产转出地址 或 caller 有权从资产转出地址转移资产。接着调用内部函数 _safeBatchTransferFrom
完成资产的批量转移。
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()),
"ERC1155: transfer caller is not owner nor approved"
);
_safeBatchTransferFrom(from, to, ids, amounts, data);
}
内部函数 _safeBatchTransferFrom
首先还是跟 balanceOfBatch
函数一样,对传入的两个数组(转移资产 id 及转移数量)进行长度相等的检查,以确保资产转移的正确性;接着要求资产转入地址不为 0 地址,这是为了避免因资产流通受限引入的各种意料外的麻烦;注意到该函数还调用了 _beforeTokenTransfer
接口,开发者可以在该接口内添加转移前的业务逻辑。接着,函数 _safeBatchTransferFrom
通过 for 循环基于传入的资产 id 和数量执行资产转移:首先要求资产转出地址的余额 / 数量足够,然后通过分别修改资产转出、转入地址的资产持有情况完成资产转移;最后,函数 _safeBatchTransferFrom
会触发 TransferBatch
事件标记资产批量转移完成,并通过调用函数 _doSafeBatchTransferAcceptanceCheck
对资产转入地址进行安全检查,避免将资产转入一个无法再次转出的合约中,出现将资产转入 0 地址的相同效果。
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = _msgSender();
_beforeTokenTransfer(operator, from, to, ids, amounts, data);
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 amount = amounts[i];
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
}
emit TransferBatch(operator, from, to, ids, amounts);
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}
金刚3: 资产的批量发售
与函数 safeBatchTransferFrom
类似,内部函数 _mintBatch
接收存储资产 id 和数量的两个数组,在函数执行前检查以确保这两个数组长度相等,且资产接收地址不为 0 地址;接着,函数 _mintBatch
通过调用 _beforeTokenTransfer
接口执行资产转移前逻辑;随后通过 for 循环逐一修改资产id到账户地址在到资产余额 / 数量的 _balances
映射,实现资产的批量发售逻辑;最后,触发 TransferBatch
事件标记资产批量发售完成,并通过调用函数 _doSafeBatchTransferAcceptanceCheck
对资产转入地址进行安全检查,避免将资产转入一个无法再次转出的合约中,出现将资产转入 0 地址的相同效果。
function _mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
address operator = _msgSender();
_beforeTokenTransfer(operator, address(0), to, ids, amounts, data);
for (uint256 i = 0; i < ids.length; i++) {
_balances[ids[i]][to] += amounts[i];
}
emit TransferBatch(operator, address(0), to, ids, amounts);
_doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
}
金刚4: 资产的批量销毁
资产批量销毁函数 _burnBatch
逻辑几乎同批量发售函数 _mintBatch
,区别在于 1⃣️ 函数执行前检查以确保资产(转出)销毁地址不为 0 地址;2⃣️ 执行资产销毁前检查资产(转出)销毁地址的资产余额 / 数量足够;3⃣️ 最后不调用函数 _doSafeBatchTransferAcceptanceCheck
对资产转入地址进行安全检查,因为销毁的资产转入的是 0 地址,该地址不是合约地址。
⚠️ 注意,函数内无需调用 transfer 函数将销毁资产显示转入 0 地址,只需将资产(转出)销毁地址的资产余额 / 数量减少即可。
function _burnBatch(
address from,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
address operator = _msgSender();
_beforeTokenTransfer(operator, from, address(0), ids, amounts, "");
for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
unchecked {
_balances[id][from] = fromBalance - amount;
}
}
emit TransferBatch(operator, from, address(0), ids, amounts);
}
至此,ERC1155 的新特性都已介绍完毕,你学会了嘛~