ERC1155: 批发小能手,妈妈再也不用担心网络拥堵造成的gas费飙升啦|猿创征文

一个可爱的小朋友 2022-06-19 17:03:31

ERC1155: 批发小能手,妈妈再也不用担心网络拥堵造成的gas费飙升啦

Hello ~ 大家好,首先感谢大家对本系列前两篇文章 👇👇👇 的喜爱,不知读者们都学废(不是,是学会)了吗?

ERC20:从入门到飞起,妈妈再也不用担心我不会写Token合约了
ERC721:全生命周期精析,妈妈再也不用担心我不会玩NFT合约啦

今天主要想跟大家聊的是 ERC1155 。了解过前两个标准的读者都知道,ERC20ERC721 对应的令牌标准是不同的,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 的新特性都已介绍完毕,你学会了嘛~

...全文
139 回复 打赏 收藏 举报
写回复
回复
切换为时间正序
请发表友善的回复…
发表回复
发帖
Ethereum中文社区
加入

171

社区成员

这里是由区块链、以太坊、Web3.0、元宇宙等相关技术组成的国内最大的开发者聚集地,帮助社区成员快速获得更全面、更深度的技术信息,链接更多资源,让开发者更好地入门学习、成长与应用实践。
社区管理员
  • ETHPlanet
  • 活动助手
帖子事件
创建了帖子
2022-06-19 17:03
社区公告

Hi,欢迎加入我们!

 

在这里你可以:

  • 获取最官方最新最全的区块链、以太坊、Web3.0学习内容与资源
  • 获取行业内更有价值的研讨会、公开课资源,或参与社区发起的主题活动课程
  • 获得专业的以太坊、区块链、Web3.0相关技术资深专家/讲师的回复或指导,突破学习瓶颈
  • 进行学习打卡、提问或回答问题,提高个人或在社区的影响力,将有机会与各大技术官方深度合作
  • 参与丰富的社区活动,获得更多学习资源、行业资源等
  • 结识更多行业伙伴,参与线上/线下课程、交流会,拓展行业交流圈

 

【最新活动】:

1、6月1日21:00-21:30,DappLearning Infura AMA(线上)点击查看详情