はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、ERC1155の仕組みを参考にして、SBTに紐づいているアドレスをリカバリーできる仕組みを提案しているERC5516についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

ERC5516では、アカウントに紐づくトークン(ソウルバウンドトークン、SBT)の新しい仕組みを提案しています。 従来のSBTでは、ユーザーが秘密鍵を失ったり鍵を作り替えると、トークンが失われてしまう問題がありました。 ERC5516では、その問題に対応するためにSBTの再利用を可能にする設計を導入しています。

動機

ERC5516は、ERC1155がベースになっています。 ERC1155は、1つのコントラクト内で複数のトークンを柔軟に扱える仕組みを提供しています。 ERC5516も同様に「複数の種類のトークンをまとめて扱う」ことを目的としています。

また、SBTに関する過去の議論やユースケース(例えば、学位やPOAPなど)も参考にされており、それらの用途に適したトークン仕様となっています。

特徴

この提案で定義されているトークンには、以下のような特徴があります。

  • 一度だけ譲渡可能

初回のtransferが完了した後は、トークンのtransferができなくなります。 つまり、最初にユーザーにトークン付与された後は、そのユーザーのアカウントに永久的に結びつきます。

  • ERC1155との部分的な互換性

完全な互換性ではないものの、ERC1155の考え方や構造を取り入れているため、既存のツールやインフラとの親和性が高くなっています。

  • ダブル署名

トークンのmintやtransferにおいて、二者の署名が必要になります。 これにより、片方のアカウントが秘密鍵を失った場合でも、もう一方の署名を使って安全に操作を行えます。

  • マルチトークン設計

同一のコントラクト内で、複数種類のトークンを一括管理できます。 これにより、複数のトークンをまとめて発行・管理できるようになります。

  • 複数人での所有が可能

トークンの所有者が1人とは限らず、複数人での共同所有もサポートされます。

  • セミ・ファンジブル

トークンの一部は代替可能で、他の一部は代替不可能、という混合的な性質を持っています。 例えば、「同じイベントに参加した人たちには同じトークンを配るが、それぞれに異なる情報を埋め込む」といったことができます。

想定されるユースケース

この新しいトークン標準は以下のような用途に適しています。

  • 学位証明書

大学などの教育機関が発行する卒業証明など。 本人だけが持つことができ、他人に譲渡できないため真正性の証明に適しています。

  • コード監査の証明

コントラクトなどのコードが監査されたことを証明するためのトークン。 開発者や監査会社の署名が必要なため、信頼性が高まります。

  • POAP(Proof of Attendance Protocol)

特定のイベントに参加したことを証明するNFT。 譲渡不可能で、本人のアカウントに結びつくことで真正性を担保します。

仕様

ERC5516に準拠したコントラクトでは以下のルールを適用する必要があります。

  • ERC5516インターフェースの全ての関数を実装することが必須
  • ERC165(インターフェース検出用の標準)を実装し、0x8314f22b に対して true を返す必要がある
  • ERC1155を実装し、0xd9b67a26 に対して true を返す必要がある
  • ERC1155のメタデータ拡張も実装し、0x0e89341c に対して true を返す必要がある

ERC165については以下の記事を参考にしてください。

インターフェース

IERC5516.sol
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.4;

/**
    @title Soulbound, Multi-Token standard.
    @notice Interface of the EIP-5516
    Note: The ERC-165 identifier for this interface is 0x8314f22b.
 */

interface IERC5516 {
    /**
     * @dev Emitted when `account` claims or rejects pending tokens under `ids[]`.
     */
    event TokenClaimed(
        address indexed operator,
        address indexed account,
        bool[] actions,
        uint256[] ids
    );

    /**
     * @dev Emitted when `from` transfers token under `id` to every address at `to[]`.
     */
    event TransferMulti(
        address indexed operator,
        address indexed from,
        address[] to,
        uint256 amount,
        uint256 id
    );

    /**
     * @dev Get tokens owned by a given address.
     */
    function tokensFrom(address from) external view returns (uint256[] memory);

    /**
     * @dev Get tokens awaiting to be claimed by a given address.
     */
    function pendingFrom(address from) external view returns (uint256[] memory);

    /**
     * @dev Claims or Reject pending `id`.
     *
     * Requirements:
     * - `account` must have a pending token under `id` at the moment of call.
     * - `account` must not own a token under `id` at the moment of call.
     *
     * Emits a {TokenClaimed} event.
     *
     */
    function claimOrReject(
        address account,
        uint256 id,
        bool action
    ) external;

    /**
     * @dev Claims or Reject pending tokens under `ids[]`.
     *
     * Requirements for each `id` `action` pair:
     * - `account` must have a pending token under `id` at the moment of call.
     * - `account` must not own a token under `id` at the moment of call.
     *
     * Emits a {TokenClaimed} event.
     *
     */
    function claimOrRejectBatch(
        address account,
        uint256[] memory ids,
        bool[] memory actions
    ) external;

    /**
     * @dev Transfers `id` token from `from` to every address at `to[]`.
     *
     * Requirements:
     *
     * - `from` MUST be the creator(minter) of `id`.
     * - All addresses in `to[]` MUST be non-zero.
     * - All addresses in `to[]` MUST have the token `id` under `_pendings`.
     * - All addresses in `to[]` MUST not own a token type under `id`.
     *
     * Emits a {TransfersMulti} event.
     *
     */
    function batchTransfer(
        address from,
        address[] memory to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) external;
    
}

イベント

TokenClaimed

js
event TokenClaimed(address indexed operator, address indexed account, bool[] actions, uint256[] ids);

ユーザーが保留中のトークンを「受け取る」または「拒否する」アクションを取ったときに発行されるイベント。 actionsはtrue(受け取る)かfalse(拒否)を表します。

TransferMulti

js
event TransferMulti(address indexed operator, address indexed from, address[] to, uint256 amount, uint256 id);

トークンの発行者(from)が、1つのトークンIDに対して、複数のアドレスに同時に送信したときに発行されるイベント。

関数

tokensFrom

js
function tokensFrom(address from) external view returns (uint256[] memory);

指定したアドレスが「所有している」トークンIDの一覧を返す関数。

pendingFrom

js
function pendingFrom(address from) external view returns (uint256[] memory);

指定したアドレスに対して「保留中」のトークンIDの一覧を返す関数。 まだユーザーが受け取りを承認していないトークンを確認できます。

claimOrReject

js
function claimOrReject(address account, uint256 id, bool action) external;

1つのトークンIDに対して「受け取る」か「拒否する」かを決定する関数。 以下が実行条件になります。

  • そのトークンが保留中であること
  • まだそのアカウントがそのトークンを持っていないこと

claimOrRejectBatch

js
function claimOrRejectBatch(address account, uint256[] memory ids, bool[] memory actions) external;

複数のトークンIDに対して一括で「受け取る」、「拒否する」処理ができます。 idsとactionsの配列の長さは一致している必要があります。

batchTransfer

js
function batchTransfer(address from, address[] memory to, uint256 id, uint256 amount, bytes memory data) external;

1つのトークンIDを、複数のアドレスに対して一括で送信する関数。 以下の条件が必須です。

  • from はそのトークンIDの発行者であること
  • to に含まれるアドレスは全てゼロアドレスでないこと
  • 各toはそのトークンIDが「保留中」であること
  • 各toはそのトークンIDをまだ所有していないこと

この仕様でできること

このインターフェースを使うことで、以下ののような動きが可能になります。

  • トークンを受け取る前に「承認するか拒否するか」をユーザーが選べる。
  • 発行者が一括で複数人にトークンを送る。
  • 一度受け取ったトークンは他人に譲渡できない。
  • ただし、初回の送付は発行者が自由に行える。

補足

ERC1155をベースにした理由

ソウルバウンドトークン(SBT)は、ERC1155の拡張として設計されています。 ERC1155はもともと、NFT(非代替トークン)やFT(代替可能トークン)を1つのコントラクトで柔軟に扱えるようにした規格です。

この設計を活用することで、SBTも既存のNFT関連のサービスとスムーズに連携できます。 例えば、マーケットプレイスやウォレット、エクスプローラーなどは、ERC1155に対応していれば特別な改修をしなくてもSBTを認識できます。

さらに、ERC1155には「複数アカウントで同じトークンを所有できる**」という仕組みがあるため、SBTを1人だけでなく複数の人やコントラクトに紐づけることが可能になります。

ダブル署名で不要なトークンをブロック

ダブル署名の仕組みは、送り手と受け手の両方の合意がないとトークンを受け取れないようにするためのものです。

通常のNFTでは、送り手が一方的にトークンを送ることができますが、それだとスパム的にトークンを送りつけられる危険があります。 ERC5516では、受け取り側が「承認」または「拒否」するステップを必ず踏む必要があります。

このやりとりは、トークンのtransferに対して同意が必要なことを意味しています**。

メタデータ対応で既存システムと連携しやすく

ERC1155には、メタデータ(名前・画像URL・説明など)を扱う拡張インターフェースがあります。 今回の仕様でもこれを取り入れており、既存のNFT表示システムやマーケットプレイスがSBTを見た目で判別・表示しやすくなっています。

ログを通じた正確なデータ追跡

Ethereum上の多くのDAppsやエクスプローラーは、コントラクトが出力する「イベントログ」を元に、ユーザーが持っているトークンや取引履歴を追跡しています。

ERC1155では、このログ情報がとても充実しており、誰がどのトークンをいつ取得したかなどを正確に記録できます。 ERC5516ではこの考え方を拡張していて、トークンのやりとりをログとしてしっかり残せるようになっています。

  • claimOrReject 系の関数が TokenClaimed イベントを発行する。
  • batchTransfer 関数が TransferMulti イベントを発行する。

鍵の喪失への対処:複数人所有で安全性を高める

SBTは一度受け取ると譲渡できないため、秘密鍵を失うとトークンへのアクセスも失ってしまうという問題があります。

しかし、ERC1155ベースの設計であれば、1つのSBTを複数のアカウントにひもづけることが可能です。 例えば、マルチシグ(複数の鍵が揃わないと動かせない)コントラクトにトークンを発行することで、鍵を1つ失っても他の鍵で復旧できる設計が可能になります。

このような使い方は、ERC4973(別のSBT関連EIP)でも推奨されています。

ERC4973については以下の記事を参考にしてください。

複数種類のトークンを1つのコントラクトで管理できる

ERC5516では、ERC1155の特徴を活かして、さまざまなトークンを1つのコントラクトでまとめて扱うことができます。

これにより以下のメリットがあります。

  • 同じコントラクト内に複数のトークン種類をまとめられる。
  • 新しいトークンを追加する時に、別のコントラクトをデプロイする必要がなく、無駄なガス消費を減らせる。
  • トークンの発行元(issuer)から見ても管理がラクになる。

一括transferで効率的に配布できる

batchTransfer 関数を使えば、1つのトークンIDを複数のアドレスに対して一括で送信できます。

例えば、あるイベントの参加者100人全員にSBTを配布する場合、1回のトランザクションで完了させることが可能です。 これにより、ガス代の削減と処理効率の向上が期待できます。

互換性

ERC5516は、最初のtransfer後にトークンを譲渡不可にするため、ERC1155と部分的にしか互換性がありません。

参考実装

ERC5516.sol
//SPDX-License-Identifier: CC0-1.0

/**
 * @notice Reference implementation of the eip-5516 interface.
 * Note: this implementation only allows for each user to own only 1 token type for each `id`.
 * @author Matias Arazi <matiasarazi@gmail.com> , Lucas Martín Grasso Ramos <lucasgrassoramos@gmail.com>
 * See https://github.com/ethereum/EIPs/pull/5516
 *
 */

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "./IERC5516.sol";

contract ERC5516 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC5516 {
    using Address for address;

    // Used for making each token unique, Maintains ID registry and quantity of tokens minted.
    uint256 private nonce;

    // Used as the URI for all token types by relying on ID substitution, e.g. https://ipfs.io/ipfs/token.data
    string private _uri;

    // Mapping from token ID to account balances
    mapping(address => mapping(uint256 => bool)) private _balances;

    // Mapping from address to mapping id bool that states if address has tokens(under id) awaiting to be claimed
    mapping(address => mapping(uint256 => bool)) private _pendings;

    // Mapping from account to operator approvals
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // Mapping from ID to minter address.
    mapping(uint256 => address) private _tokenMinters;

    // Mapping from ID to URI.
    mapping(uint256 => string) private _tokenURIs;

    /**
     * @dev Sets base uri for tokens. Preferably "https://ipfs.io/ipfs/"
     */
    constructor(string memory uri_) {
        _uri = uri_;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC165, IERC165)
        returns (bool)
    {
        return
            interfaceId == type(IERC1155).interfaceId ||
            interfaceId == type(IERC1155MetadataURI).interfaceId ||
            interfaceId == type(IERC5516).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC1155MetadataURI-uri}.
     */
    function uri(uint256 _id)
        external
        view
        virtual
        override
        returns (string memory)
    {
        return string(abi.encodePacked(_uri, _tokenURIs[_id]));
    }

    /**
     * @dev See {IERC1155-balanceOf}.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     *
     */
    function balanceOf(address account, uint256 id)
        public
        view
        virtual
        override
        returns (uint256)
    {
        require(account != address(0), "EIP5516: Address zero error");
        if (_balances[account][id]) {
            return 1;
        } else {
            return 0;
        }
    }

    /**
     * @dev See {IERC1155-balanceOfBatch}.
     *
     * Requirements:
     *
     * - `accounts` and `ids` must have the same length.
     *
     */
    function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
        public
        view
        virtual
        override
        returns (uint256[] memory)
    {
        require(
            accounts.length == ids.length,
            "EIP5516: Array lengths 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;
    }

    /**
     * @dev Get tokens owned by a given address
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     *
     */
    function tokensFrom(address account)
        public
        view
        virtual
        override
        returns (uint256[] memory)
    {
        require(account != address(0), "EIP5516: Address zero error");

        uint256 _tokenCount = 0;
        for (uint256 i = 1; i <= nonce; ) {
            if (_balances[account][i]) {
                unchecked {
                    ++_tokenCount;
                }
            }
            unchecked {
                ++i;
            }
        }

        uint256[] memory _ownedTokens = new uint256[](_tokenCount);

        for (uint256 i = 1; i <= nonce; ) {
            if (_balances[account][i]) {
                _ownedTokens[--_tokenCount] = i;
            }
            unchecked {
                ++i;
            }
        }

        return _ownedTokens;
    }

    /**
     * @dev Get tokens marked as _pendings of a given address
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     *
     */
    function pendingFrom(address account)
        public
        view
        virtual
        override
        returns (uint256[] memory)
    {
        require(account != address(0), "EIP5516: Address zero error");

        uint256 _tokenCount = 0;

        for (uint256 i = 1; i <= nonce; ) {
            if (_pendings[account][i]) {
                ++_tokenCount;
            }
            unchecked {
                ++i;
            }
        }

        uint256[] memory _pendingTokens = new uint256[](_tokenCount);

        for (uint256 i = 1; i <= nonce; ) {
            if (_pendings[account][i]) {
                _pendingTokens[--_tokenCount] = i;
            }
            unchecked {
                ++i;
            }
        }

        return _pendingTokens;
    }

    /**
     * @dev See {IERC1155-setApprovalForAll}.
     */
    function setApprovalForAll(address operator, bool approved)
        public
        virtual
        override
    {
        _setApprovalForAll(_msgSender(), operator, approved);
    }

    /**
     * @dev See {IERC1155-isApprovedForAll}.
     */
    function isApprovedForAll(address account, address operator)
        public
        view
        virtual
        override
        returns (bool)
    {
        return _operatorApprovals[account][operator];
    }

    /**
     * @dev mints(creates) a token
     */
    function _mint(address account, string memory data) internal virtual {
        unchecked {
            ++nonce;
        }

        address operator = _msgSender();
        uint256[] memory ids = _asSingletonArray(nonce);
        uint256[] memory amounts = _asSingletonArray(1);
        bytes memory _bData = bytes(data);

        _beforeTokenTransfer(
            operator,
            address(0),
            operator,
            ids,
            amounts,
            _bData
        );
        _tokenURIs[nonce] = data;
        _tokenMinters[nonce] = account;
        emit TransferSingle(operator, address(0), operator, nonce, 1);
        _afterTokenTransfer(
            operator,
            address(0),
            operator,
            ids,
            amounts,
            _bData
        );
    }

    /**
     * @dev See {IERC1155-safeTransferFrom}.
     *
     * Requirements:
     *
     * - `from` must be the creator(minter) of `id` or must have allowed _msgSender() as an operator.
     *
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public virtual override {
        require(amount == 1, "EIP5516: Can only transfer one token");
        require(
            _msgSender() == _tokenMinters[id] ||
                isApprovedForAll(_tokenMinters[id], _msgSender()),
            "EIP5516: Unauthorized"
        );

        _safeTransferFrom(from, to, id, amount, data);
    }

    /**
     * @dev See {eip-5516-batchTransfer}
     *
     * Requirements:
     *
     * - 'from' must be the creator(minter) of `id` or must have allowed _msgSender() as an operator.
     *
     */
    function batchTransfer(
        address from,
        address[] memory to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) external virtual override {
        require(amount == 1, "EIP5516: Can only transfer one token");
        require(
            _msgSender() == _tokenMinters[id] ||
                isApprovedForAll(_tokenMinters[id], _msgSender()),
            "EIP5516: Unauthorized"
        );

        _batchTransfer(from, to, id, amount, data);
    }

    /**
     * @dev Transfers `amount` tokens of token type `id` from `from` to `to`.
     *
     * Emits a {TransferSingle} event.
     *
     * Requirements:
     *
     * - `from` must be the creator(minter) of the token under `id`.
     * - `to` must be non-zero.
     * - `to` must have the token `id` marked as _pendings.
     * - `to` must not own a token type under `id`.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
     *   acceptance magic value.
     *
     */
    function _safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        require(from != address(0), "EIP5516: Address zero error");
        require(
            _pendings[to][id] == false && _balances[to][id] == false,
            "EIP5516: Already Assignee"
        );

        address operator = _msgSender();

        uint256[] memory ids = _asSingletonArray(id);
        uint256[] memory amounts = _asSingletonArray(amount);

        _beforeTokenTransfer(operator, from, to, ids, amounts, data);

        _pendings[to][id] = true;

        emit TransferSingle(operator, from, to, id, amount);
        _afterTokenTransfer(operator, from, to, ids, amounts, data);

        _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
    }

    /**
     * Transfers `id` token from `from` to every address at `to[]`.
     *
     * Requirements:
     * - See {eip-5516-safeMultiTransfer}.
     *
     */
    function _batchTransfer(
        address from,
        address[] memory to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        address operator = _msgSender();

        _beforeBatchedTokenTransfer(operator, from, to, id, data);

        for (uint256 i = 0; i < to.length; ) {
            address _to = to[i];

            require(_to != address(0), "EIP5516: Address zero error");
            require(
                _pendings[_to][id] == false && _balances[_to][id] == false,
                "EIP5516: Already Assignee"
            );

            _pendings[_to][id] = true;

            unchecked {
                ++i;
            }
        }

        emit TransferMulti(operator, from, to, amount, id);

        _beforeBatchedTokenTransfer(operator, from, to, id, data);
    }

    /**
     * @dev See {eip-5516-claimOrReject}
     *
     * If action == true: Claims pending token under `id`.
     * Else, rejects pending token under `id`.
     *
     */
    function claimOrReject(
        address account,
        uint256 id,
        bool action
    ) external virtual override {
        require(_msgSender() == account, "EIP5516: Unauthorized");

        _claimOrReject(account, id, action);
    }

    /**
     * @dev See {eip-5516-claimOrReject}
     *
     * For each `id` - `action` pair:
     *
     * If action == true: Claims pending token under `id`.
     * Else, rejects pending token under `id`.
     *
     */
    function claimOrRejectBatch(
        address account,
        uint256[] memory ids,
        bool[] memory actions
    ) external virtual override {
        require(
            ids.length == actions.length,
            "EIP5516: Array lengths mismatch"
        );

        require(_msgSender() == account, "EIP5516: Unauthorized");

        _claimOrRejectBatch(account, ids, actions);
    }

    /**
     * @dev Claims or Reject pending token under `_id` from address `_account`.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have a _pendings token under `id` at the moment of call.
     * - `account` mUST not own a token under `id` at the moment of call.
     *
     * Emits a {TokenClaimed} event.
     *
     */
    function _claimOrReject(
        address account,
        uint256 id,
        bool action
    ) internal virtual {
        require(
            _pendings[account][id] == true && _balances[account][id] == false,
            "EIP5516: Not claimable"
        );

        address operator = _msgSender();

        bool[] memory actions = new bool[](1);
        actions[0] = action;
        uint256[] memory ids = _asSingletonArray(id);

        _beforeTokenClaim(operator, account, actions, ids);

        _balances[account][id] = action;
        _pendings[account][id] = false;

        delete _pendings[account][id];

        emit TokenClaimed(operator, account, actions, ids);

        _afterTokenClaim(operator, account, actions, ids);
    }

    /**
     * @dev Claims or Reject _pendings `_id` from address `_account`.
     *
     * For each `id`-`action` pair:
     *
     * Requirements:
     * - `account` cannot be the zero address.
     * - `account` must have a pending token under `id` at the moment of call.
     * - `account` must not own a token under `id` at the moment of call.
     *
     *  Emits a {TokenClaimed} event.
     *
     */
    function _claimOrRejectBatch(
        address account,
        uint256[] memory ids,
        bool[] memory actions
    ) internal virtual {
        uint256 totalIds = ids.length;
        address operator = _msgSender();

        _beforeTokenClaim(operator, account, actions, ids);

        for (uint256 i = 0; i < totalIds; ) {
            uint256 id = ids[i];

            require(
                _pendings[account][id] == true &&
                    _balances[account][id] == false,
                "EIP5516: Not claimable"
            );

            _balances[account][id] = actions[i];
            _pendings[account][id] = false;

            delete _pendings[account][id];

            unchecked {
                ++i;
            }
        }

        emit TokenClaimed(operator, account, actions, ids);

        _afterTokenClaim(operator, account, actions, ids);
    }

    /**
     * @dev Destroys `id` token from `account`
     *
     * Emits a {TransferSingle} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` must own a token under `id`.
     *
     */
    function _burn(address account, uint256 id) internal virtual {
        require(_balances[account][id] == true, "EIP5516: Unauthorized");

        address operator = _msgSender();
        uint256[] memory ids = _asSingletonArray(id);
        uint256[] memory amounts = _asSingletonArray(1);

        _beforeTokenTransfer(operator, account, address(0), ids, amounts, "");

        delete _balances[account][id];

        emit TransferSingle(operator, account, address(0), id, 1);
        _beforeTokenTransfer(operator, account, address(0), ids, amounts, "");
    }

    /**
     * @dev Destroys all tokens under `ids` from `account`
     *
     * Emits a {TransferBatch} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` must own all tokens under `ids`.
     *
     */
    function _burnBatch(address account, uint256[] memory ids)
        internal
        virtual
    {
        uint256 totalIds = ids.length;
        address operator = _msgSender();
        uint256[] memory amounts = _asSingletonArray(totalIds);
        uint256[] memory values = _asSingletonArray(0);

        _beforeTokenTransfer(operator, account, address(0), ids, amounts, "");

        for (uint256 i = 0; i < totalIds; ) {
            uint256 id = ids[i];

            require(_balances[account][id] == true, "EIP5516: Unauthorized");

            delete _balances[account][id];

            unchecked {
                ++i;
            }
        }

        emit TransferBatch(operator, account, address(0), ids, values);

        _afterTokenTransfer(operator, account, address(0), ids, amounts, "");
    }

    /**
     * @dev Approve `operator` to operate on all of `owner` tokens
     *
     * Emits a {ApprovalForAll} event.
     *
     */
    function _setApprovalForAll(
        address owner,
        address operator,
        bool approved
    ) internal virtual {
        require(owner != operator, "ERC1155: setting approval status for self");
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning, as well as batched variants.
     *
     * The same hook is called on both single and batched variants. For single
     * transfers, the length of the `ids` and `amounts` arrays will be 1.
     *
     * Calling conditions (for each `id` and `amount` pair):
     *
     * - `amount` will always be and must be equal to 1.
     * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * of token type `id` will be transferred to `to`.
     * - When `from` is zero, `amount` tokens of token type `id` will be minted
     * for `to`.
     * - When `to` is zero, `amount` of ``from``'s tokens of token type `id`
     * will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {}

    /**
     * @dev Hook that is called after any token transfer. This includes minting
     * and burning, as well as batched variants.
     *
     * The same hook is called on both single and batched variants. For single
     * transfers, the length of the `id` and `amount` arrays will be 1.
     *
     * Calling conditions (for each `id` and `amount` pair):
     *
     * - `amount` will always be and must be equal to 1.
     * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * of token type `id` will be transferred to `to`.
     * - When `from` is zero, `amount` tokens of token type `id` will be minted
     * for `to`.
     * - When `to` is zero, `amount` of ``from``'s tokens of token type `id`
     * will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {}

    /**
     * @dev Hook that is called before any batched token transfer. This includes minting
     * and burning, as well as batched variants.
     *
     * The same hook is called on both single and batched variants. For single
     * transfers, the length of the `id` and `amount` arrays will be 1.
     *
     * Calling conditions (for each `id` and `amount` pair):
     *
     * - `amount` will always be and must be equal to 1.
     * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * of token type `id` will be transferred to `to`.
     * - When `from` is zero, `amount` tokens of token type `id` will be minted
     * for `to`.
     * - When `to` is zero, `amount` of ``from``'s tokens of token type `id`
     * will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeBatchedTokenTransfer(
        address operator,
        address from,
        address[] memory to,
        uint256 id,
        bytes memory data
    ) internal virtual {}

    /**
     * @dev Hook that is called after any batched token transfer. This includes minting
     * and burning, as well as batched variants.
     *
     * The same hook is called on both single and batched variants. For single
     * transfers, the length of the `id` and `amount` arrays will be 1.
     *
     * Calling conditions (for each `id` and `amount` pair):
     *
     * - `amount` will always be and must be equal to 1.
     * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * of token type `id` will be transferred to `to`.
     * - When `from` is zero, `amount` tokens of token type `id` will be minted
     * for `to`.
     * - When `to` is zero, `amount` of ``from``'s tokens of token type `id`
     * will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterBatchedTokenTransfer(
        address operator,
        address from,
        address[] memory to,
        uint256 id,
        bytes memory data
    ) internal virtual {}

    /**
     * @dev Hook that is called before any token claim.
     +
     * Calling conditions (for each `action` and `id` pair):
     *
     * - A token under `id` must exist.
     * - When `action` is non-zero, a token under `id` will now be claimed and owned by`operator`.
     * - When `action` is false, a token under `id` will now be rejected.
     * 
     */
    function _beforeTokenClaim(
        address operator,
        address account,
        bool[] memory actions,
        uint256[] memory ids
    ) internal virtual {}

    /**
     * @dev Hook that is called after any token claim.
     +
     * Calling conditions (for each `action` and `id` pair):
     *
     * - A token under `id` must exist.
     * - When `action` is non-zero, a token under `id` is now owned by`operator`.
     * - When `action` is false, a token under `id` was rejected.
     * 
     */
    function _afterTokenClaim(
        address operator,
        address account,
        bool[] memory actions,
        uint256[] memory ids
    ) internal virtual {}

    function _asSingletonArray(uint256 element)
        private
        pure
        returns (uint256[] memory)
    {
        uint256[] memory array = new uint256[](1);
        array[0] = element;

        return array;
    }

    /**
     * @dev see {ERC1155-_doSafeTransferAcceptanceCheck, IERC1155Receivable}
     */
    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try
                IERC1155Receiver(to).onERC1155Received(
                    operator,
                    from,
                    id,
                    amount,
                    data
                )
            returns (bytes4 response) {
                if (response != IERC1155Receiver.onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
            }
        }
    }

    /**
     * @dev Unused/Deprecated function
     * @dev See {IERC1155-safeBatchTransferFrom}.
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override {}
}

最後に

今回は「ERC1155の仕組みを参考にして、SBTに紐づいているアドレスをリカバリーできる仕組みを提案しているERC5516」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!