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

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 的新特性都已介绍完毕,你学会了嘛~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一个比较省gasERC721合约示例代码: ``` pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/utils/Address.sol"; contract MyNFT is IERC721 { using Address for address; mapping (uint256 => address) private _owners; mapping (address => uint256) private _balances; mapping (uint256 => address) private _tokenApprovals; mapping (address => mapping (address => bool)) private _operatorApprovals; function balanceOf(address owner) public view override returns (uint256) { require(owner != address(0), "ERC721: balance query for the zero address"); return _balances[owner]; } function ownerOf(uint256 tokenId) public view override returns (address) { address owner = _owners[tokenId]; require(owner != address(0), "ERC721: owner query for nonexistent token"); return owner; } function safeTransferFrom(address from, address to, uint256 tokenId) public override { safeTransferFrom(from, to, tokenId, ""); } function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public override { require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved"); _safeTransfer(from, to, tokenId, _data); } function transferFrom(address from, address to, uint256 tokenId) public override { require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved"); _transfer(from, to, tokenId); } function approve(address to, uint256 tokenId) public override { address owner = ownerOf(tokenId); require(to != owner, "ERC721: approval to current owner"); require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "ERC721: approve caller is not owner nor approved for all" ); _tokenApprovals[tokenId] = to; emit Approval(owner, to, tokenId); } function setApprovalForAll(address operator, bool approved) public override { require(operator != msg.sender, "ERC721: approve to caller"); _operatorApprovals[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function isApprovedForAll(address owner, address operator) public view override returns (bool) { return _operatorApprovals[owner][operator]; } function getApproved(uint256 tokenId) public view override returns (address) { require(_owners[tokenId] != address(0), "ERC721: approved query for nonexistent token"); return _tokenApprovals[tokenId]; } function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual { _transfer(from, to, tokenId); require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); } function _transfer(address from, address to, uint256 tokenId) internal virtual { require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own"); require(to != address(0), "ERC721: transfer to the zero address"); _owners[tokenId] = to; _balances[from] -= 1; _balances[to] += 1; emit Transfer(from, to, tokenId); } function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { require(_owners[tokenId] != address(0), "ERC721: operator query for nonexistent token"); address owner = ownerOf(tokenId); return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); } function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) { if (!to.isContract()) { return true; } bytes memory returndata = to.functionCall(abi.encodeWithSelector( IERC721Receiver(to).onERC721Received.selector, msg.sender, from, tokenId, _data ), "ERC721: transfer to non ERC721Receiver implementer"); bytes4 retval = abi.decode(returndata, (bytes4)); return (retval == _ERC721_RECEIVED); } bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; } ``` 这个合约代码只包含了ERC721标准规定的基本函数,没有额外的复杂逻辑,因此省去了一些不必要的计算和存储操作,从而降低了gas用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个可爱的小朋友

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值