はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、World of Warcraftの伝説的なアイテムがキャラクターにバインドされるように、譲渡不可能なNFTがEthereumアカウントにバインドするインターフェースを提案しているERC4973についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
4973は現在(2023年9月9日)では「Review」段階です。
概要
Account-Bound Tokens(ABT)の標準API提案
ABT(アカウント固有トークン)は、スマートコントラクト内で扱う交換不可能なトークンで、1つのアカウントに紐付いています。
ABTはトークンの転送について標準のインターフェースを持っていません。
このEIP(Ethereum Improvement Proposal:Ethereumの改善提案)は、ABTを発行、割り当て、取り消し、および追跡するための基本的な機能を定義します。
動機
人気のMMORPG「ワールド・オブ・ウォークラフト」では、ゲームデザイナーが意図的に一部のアイテムをワールドのオークションハウス・マーケット・システムから外し、価格が公開されないようにして入手しにくくしていました。
バニラ版WoWの「Thunderfury, Blessed Blade of the Windseeker」はその仕組みが適用されている伝説的アイテムの1つで、剣を1回クラフトするのに必要な素材である「Essence of the Firelord」を手に入れるには、40人規模のレイドに参加する必要があった。
この剣を手に入れると、そのキャラクターのsoulと永久に結合するため、プレイヤーのキャラクター間で剣を交換したり、売ったりすることは不可能になる。
言い換えれば、「Thunderfury」の価格は、友達やギルドメンバーと共に難しいクエストを達成するためにかかるすべての社会的なコストの合計でした。
「Thunderfury」を見つけた他のプレイヤーは、その持ち主が烈火の剣士「Ragnaros」を倒したことの証明になります。
「World of Warcraft」のプレイヤーは、「Thunderfury」のような伝説のアイテムやソウルバウンド・アイテムを自分のアカウントから永久に削除するために、それをゴミ箱に捨てることができました。
アイテムを目に見える形で装備するか装備解除するかはプレイヤーの自由であり、それ故に自分の功績を皆に示すことができるのです。
Ethereumのコミュニティは、WoWのソウルバウンド・アイテムに似た、譲渡不可、交換不可、社会的に価値が決まるトークンが必要とされています。
現在、一般的なコントラクトは、アカウント固有の相互作用の権利を暗黙のうちに実装しています。
標準化は相互運用性を助け、オンチェーンデータインデックスを改善します。
この提案の目的は、Ethereum上でABTを実現することです。
仕様
ABTは以下のインターフェースを実装する必要があります。
-
ERC165 (
0x01ffc9a7
) -
ERC721のERC721Metadata (
0x5b5e139f
)
ERC165は以下を参考にしてください。
ABTは以下のインタフェースを実装してはいけないです。
-
ERC721 (
0x80ac58cd
)
ERC721は以下を参考にしてください。
ABTレシーバは、常にfunction unequip(address _tokenId)
を呼び出して、ABTをオフチェーンにすることができます。
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.6;
/// @title Account-bound tokens
/// @dev See https://eips.ethereum.org/EIPS/eip-4973
/// Note: the ERC-165 identifier for this interface is 0xeb72bb7c
interface IERC4973 {
/// @dev This emits when ownership of any ABT changes by any mechanism.
/// This event emits when ABTs are given or equipped and unequipped
/// (`to` == 0).
event Transfer(
address indexed from, address indexed to, uint256 indexed tokenId
);
/// @notice Count all ABTs assigned to an owner
/// @dev ABTs assigned to the zero address are considered invalid, and this
/// function throws for queries about the zero address.
/// @param owner An address for whom to query the balance
/// @return The number of ABTs owned by `address owner`, possibly zero
function balanceOf(address owner) external view returns (uint256);
/// @notice Find the address bound to an ERC4973 account-bound token
/// @dev ABTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param tokenId The identifier for an ABT.
/// @return The address of the owner bound to the ABT.
function ownerOf(uint256 tokenId) external view returns (address);
/// @notice Removes the `uint256 tokenId` from an account. At any time, an
/// ABT receiver must be able to disassociate themselves from an ABT
/// publicly through calling this function. After successfully executing this
/// function, given the parameters for calling `function give` or
/// `function take` a token must be re-equipable.
/// @dev Must emit a `event Transfer` with the `address to` field pointing to
/// the zero address.
/// @param tokenId The identifier for an ABT.
function unequip(uint256 tokenId) external;
/// @notice Creates and transfers the ownership of an ABT from the
/// transaction's `msg.sender` to `address to`.
/// @dev Throws unless `bytes signature` represents a signature of the
// EIP-712 structured data hash
/// `Agreement(address active,address passive,bytes metadata)` expressing
/// `address to`'s explicit agreement to be publicly associated with
/// `msg.sender` and `bytes metadata`. A unique `uint256 tokenId` must be
/// generated by type-casting the `bytes32` EIP-712 structured data hash to a
/// `uint256`. If `bytes signature` is empty or `address to` is a contract,
/// an EIP-1271-compatible call to `function isValidSignatureNow(...)` must
/// be made to `address to`. A successful execution must result in the
/// `event Transfer(msg.sender, to, tokenId)`. Once an ABT exists as an
/// `uint256 tokenId` in the contract, `function give(...)` must throw.
/// @param to The receiver of the ABT.
/// @param metadata The metadata that will be associated to the ABT.
/// @param signature A signature of the EIP-712 structured data hash
/// `Agreement(address active,address passive,bytes metadata)` signed by
/// `address to`.
/// @return A unique `uint256 tokenId` generated by type-casting the `bytes32`
/// EIP-712 structured data hash to a `uint256`.
function give(address to, bytes calldata metadata, bytes calldata signature)
external
returns (uint256);
/// @notice Creates and transfers the ownership of an ABT from an
/// `address from` to the transaction's `msg.sender`.
/// @dev Throws unless `bytes signature` represents a signature of the
/// EIP-712 structured data hash
/// `Agreement(address active,address passive,bytes metadata)` expressing
/// `address from`'s explicit agreement to be publicly associated with
/// `msg.sender` and `bytes metadata`. A unique `uint256 tokenId` must be
/// generated by type-casting the `bytes32` EIP-712 structured data hash to a
/// `uint256`. If `bytes signature` is empty or `address from` is a contract,
/// an EIP-1271-compatible call to `function isValidSignatureNow(...)` must
/// be made to `address from`. A successful execution must result in the
/// emission of an `event Transfer(from, msg.sender, tokenId)`. Once an ABT
/// exists as an `uint256 tokenId` in the contract, `function take(...)` must
/// throw.
/// @param from The origin of the ABT.
/// @param metadata The metadata that will be associated to the ABT.
/// @param signature A signature of the EIP-712 structured data hash
/// `Agreement(address active,address passive,bytes metadata)` signed by
/// `address from`.
/// @return A unique `uint256 tokenId` generated by type-casting the `bytes32`
/// EIP-712 structured data hash to a `uint256`.
function take(address from, bytes calldata metadata, bytes calldata signature)
external
returns (uint256);
/// @notice Decodes the opaque metadata bytestring of an ABT into the token
/// URI that will be associated with it once it is created on chain.
/// @param metadata The metadata that will be associated to an ABT.
/// @return A URI that represents the metadata.
function decodeURI(bytes calldata metadata) external returns (string memory);
}
メタデータのJSONスキーマの定義についてはERC721を参照してください。
EIP-712型付き構造データのハッシュ化とバイト配列署名の作成
関数(give(...)
やtake(...)
)を呼び出すためには、EIP712を使用してバイト配列の署名(signature
)を作成する必要があります。
Node.jsでのテスト済みのリファレンス実装は、index.mjs、index_test.mjs、package.jsonに添付されています。
Solidityでは、このバイト配列シグネチャは次のように作成できます。
bytes32 r = 0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90;
bytes32 s = 0x7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064;
uint8 v = 27;
bytes memory signature = abi.encodePacked(r, s, v);
補足
インターフェース
ABTs(アカウント固有トークン)は、後方互換性を最大限に保ちつつ、依然として最小かつ簡単に実装できるインターフェースを提供します。
ERC721トークンは、ウォレットプロバイダーやマーケットプレイスで広く採用されており、ERC721Metadataインターフェースを使用し、機能検出のためにERC165を組み合わせることで、ABTsをすぐにサポートできるようになります。
ERC721の実装者がERC165のsupportsInterface(bytes4 interfaceID)
関数を正しく実装していれば、ERC721のトラックとtransfer
のインターフェース(識別子: 0x80ac58cd
)が実装されていないことを確認でき、トークンの転送機能をユーザーインターフェースのオプションとして表示しないようにします。
ただ、ABTsはERC721のERC721Metadata拡張をサポートしているため、ウォレットとマーケットプレイスは変更せずにアカウント固有トークンを表示できます。
アカウント固有トークンの他の実装方法も考えられますが、すべてのtransfer
関数をリバート(実行を中止)させる方法など、ABTsはERC165を通じて機能の検出をサポートする点で優れています。
また、function unequip(address _tokenId)
を公開し、ABTの所有者がいつでも呼び出せるようにします。
例外処理
ABTがアカウント間で譲渡できないという特性を持つため、ユーザーの鍵(秘密鍵)が漏洩したり変更されたりすると、そのユーザーはトークンとの関連付けを失う可能性があります。
場合によっては、これが望ましい結果になることもあります。
そのため、ABTの実装者は、再発行と取り消しのプロセスを構築して、問題の解決策を提供できるようにするべきです。
厳密に分散化され、許可なしで、検閲に対抗できる再発行プロセスを実装することが推奨されます。
しかし、この提案では、ユーザーの鍵が漏洩したり変更されたりした場合の例外処理の標準化を提供することを意図的に避けています。
実装者がアカウント固有トークンを異なるアカウント間で共有したい場合(たとえば、鍵が漏洩したときにアクセスを失わないために)、多重署名機能を実装したコントラクトアカウントに向けてアカウント固有トークンを発行することを提案しています。
Provenance Indexing
ABTは、Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
イベントの発生を追跡することでインデックス化できます。
ERC721と同様に、2つのアカウント間のトークン転送は、アドレス「from
」とアドレス「to
」が0
でないアドレス間で実行されます。
トークンの「unequipping
」(取り外し)は、アドレス「to
」を0
アドレスにすることで実行できます。
アドレス「from
」が0
に設定されるmint
(発行)操作は存在しません。
意図的に不正な方法で「Transfer
」イベントを発生させるコントラクトのなりすましを防ぐために、インデクサー(情報を整理して検索可能にするシステム)は、トランザクションの送信者が「Transfer
」イベントの「from
」値と等しいことを確認する必要があります。
「Transfer
」イベントが不正に発行されると、トークンの所有権が誤って移動されたように見せかけることができ、ユーザーを混乱させたり、詐欺行為に利用されたりする可能性があります。
後方互換性
この提案では、ERC165とERC721Metadataの機能を採用しています。
その目的は、ERC721と高い後方互換性を持たせることです。
また、意図的にERC721の用語を使用して、ERC20やERC721の実装に慣れているユーザーや開発者にとって理解しやすくするために、関数ownerOf(...)
やbalanceOf(...)
などを採用しました。
インデクサー(情報を整理して検索可能にするシステム)に対しても、幅広く実装されているTransfer
のイベント署名を再利用しました。
参考実装
全体のコードは以下のGithubに格納しています。
abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 {
using BitMaps for BitMaps.BitMap;
BitMaps.BitMap private _usedHashes;
string private _name;
string private _symbol;
mapping(uint256 => address) private _owners;
mapping(uint256 => string) private _tokenURIs;
mapping(address => uint256) private _balances;
constructor(string memory name_, string memory symbol_, string memory version)
EIP712(name_, version)
{
_name = name_;
_symbol = symbol_;
}
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return interfaceId == type(IERC721Metadata).interfaceId
|| interfaceId == type(IERC4973).interfaceId
|| super.supportsInterface(interfaceId);
}
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
require(_exists(tokenId), "tokenURI: token doesn't exist");
return _tokenURIs[tokenId];
}
function unequip(uint256 tokenId) public virtual override {
require(msg.sender == ownerOf(tokenId), "unequip: sender must be owner");
_usedHashes.unset(tokenId);
_burn(tokenId);
}
function balanceOf(address owner)
public
view
virtual
override
returns (uint256)
{
require(owner != address(0), "balanceOf: address zero is not a valid owner");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view virtual returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ownerOf: token doesn't exist");
return owner;
}
function give(address to, bytes calldata metadata, bytes calldata signature)
external
virtual
returns (uint256)
{
require(msg.sender != to, "give: cannot give from self");
uint256 tokenId = _safeCheckAgreement(msg.sender, to, metadata, signature);
string memory uri = decodeURI(metadata);
_mint(msg.sender, to, tokenId, uri);
_usedHashes.set(tokenId);
return tokenId;
}
function take(address from, bytes calldata metadata, bytes calldata signature)
external
virtual
returns (uint256)
{
require(msg.sender != from, "take: cannot take from self");
uint256 tokenId = _safeCheckAgreement(msg.sender, from, metadata, signature);
string memory uri = decodeURI(metadata);
_mint(from, msg.sender, tokenId, uri);
_usedHashes.set(tokenId);
return tokenId;
}
function decodeURI(bytes calldata metadata)
public
virtual
returns (string memory)
{
return string(metadata);
}
function _safeCheckAgreement(
address active,
address passive,
bytes calldata metadata,
bytes calldata signature
)
internal
virtual
returns (uint256)
{
bytes32 hash = _getHash(active, passive, metadata);
uint256 tokenId = uint256(hash);
require(
SignatureChecker.isValidSignatureNow(passive, hash, signature),
"_safeCheckAgreement: invalid signature"
);
require(!_usedHashes.get(tokenId), "_safeCheckAgreement: already used");
return tokenId;
}
function _getHash(address active, address passive, bytes calldata metadata)
internal
view
returns (bytes32)
{
bytes32 structHash =
keccak256(abi.encode(AGREEMENT_HASH, active, passive, keccak256(metadata)));
return _hashTypedDataV4(structHash);
}
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _owners[tokenId] != address(0);
}
function _mint(address from, address to, uint256 tokenId, string memory uri)
internal
virtual
returns (uint256)
{
require(!_exists(tokenId), "mint: tokenID exists");
_balances[to] += 1;
_owners[tokenId] = to;
_tokenURIs[tokenId] = uri;
emit Transfer(from, to, tokenId);
return tokenId;
}
function _burn(uint256 tokenId) internal virtual {
address owner = ownerOf(tokenId);
_balances[owner] -= 1;
delete _owners[tokenId];
delete _tokenURIs[tokenId];
emit Transfer(owner, address(0), tokenId);
}
}
constructor
constructor(string memory name_, string memory symbol_, string memory version)
EIP712(name_, version)
{
_name = name_;
_symbol = symbol_;
}
概要
- コントラクトのコンストラクタ関数。
- トークンの名前、シンボル、バージョンを設定します。
詳細
- コントラクトがデプロイされるときに呼び出される初期化関数。
引数
-
name_
- トークンの名前を表す文字列。
-
symbol_
- トークンのシンボルを表す文字列。
-
version
- トークンのバージョンを表す文字列。
supportsInterface
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return interfaceId == type(IERC721Metadata).interfaceId
|| interfaceId == type(IERC4973).interfaceId
|| super.supportsInterface(interfaceId);
}
概要
- インターフェースがサポートされているかどうかを確認する関数。
詳細
- 指定されたインターフェースがサポートされているかどうかを確認します。
引数
-
interfaceId
- 確認するインターフェースのIDを表すバイト列。
戻り値
- インターフェースがサポートされていれば
true
、そうでなければfalse
を返します。
name
function name() public view virtual override returns (string memory) {
return _name;
}
概要
- トークンの名前を取得する関数。
詳細
- トークンの名前を文字列として取得します。
戻り値
- トークンの名前を表す文字列を返します。
symbol
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
概要
- トークンのシンボルを取得する関数。
詳細
- トークンのシンボルを文字列として取得します。
戻り値
- トークンのシンボルを表す文字列を返します。
tokenURI
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
require(_exists(tokenId), "tokenURI: token doesn't exist");
return _tokenURIs[tokenId];
}
概要
- トークンのURI(Uniform Resource Identifier)を取得する関数。
詳細
- 指定されたトークンのURIを文字列として取得します。
- トークンが存在しない場合、エラーメッセージが表示されます。
引数
-
tokenId
- 取得したいトークンID。
戻り値
- トークンのURIを表す文字列を返します。
unequip
function unequip(uint256 tokenId) public virtual override {
require(msg.sender == ownerOf(tokenId), "unequip: sender must be owner");
_usedHashes.unset(tokenId);
_burn(tokenId);
}
概要
- トークンをアンイクイップ(装備解除)する関数。
詳細
- トークンの所有者でない場合、トークンをアンイクイップできません。
- また、トークンをアンイクイップすると、トークンのハッシュをリセットし、トークンを削除します。
引数
-
tokenId
- アンイクイップするトークンID。
balanceOf
function balanceOf(address owner)
public
view
virtual
override
returns (uint256)
{
require(owner != address(0), "balanceOf: address zero is not a valid owner");
return _balances[owner];
}
概要
- 指定されたアドレスのトークンの残高を取得する関数。
詳細
- アドレスが
0
ゼロの場合、エラーメッセージが表示されます。
引数
-
owner
- 残高を取得するアドレス。
戻り値
- 指定されたアドレスのトークン残高を表す整数を返します。
ownerOf
function ownerOf(uint256 tokenId) public view virtual returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ownerOf: token doesn't exist");
return owner;
}
概要
- 指定されたトークンの所有者を取得する関数。
詳細
- トークンが存在しない場合、エラーメッセージが表示されます。
引数
-
tokenId
- 所有者を取得するトークンID。
戻り値
- トークンの所有者のアドレスを返します。
give
function give(address to, bytes calldata metadata, bytes calldata signature)
external
virtual
returns (uint256)
{
require(msg.sender != to, "give: cannot give from self");
uint256 tokenId = _safeCheckAgreement(msg.sender, to, metadata, signature);
string memory uri = decodeURI(metadata);
_mint(msg.sender, to, tokenId, uri);
_usedHashes.set(tokenId);
return tokenId;
}
概要
- トークンを他のアドレスに贈る関数。
詳細
- 送信者が自分自身にトークンを贈ることはできません。
- トークンを贈る前に、署名の検証を行い、
URI(Uniform Resource Identifier)
をデコードしてトークンを発行します。
引数
-
to
- トークンを贈る相手のアドレス。
-
metadata
- トークンに関する情報をバイト列で表すデータ。
-
signature
- 署名データ。
戻り値
- 発行されたトークンのIDを表す整数を返します。
take
function take(address from, bytes calldata metadata, bytes calldata signature)
external
virtual
returns (uint256)
{
require(msg.sender != from, "take: cannot take from self");
uint256 tokenId = _safeCheckAgreement(msg.sender, from, metadata, signature);
string memory uri = decodeURI(metadata);
_mint(from, msg.sender, tokenId, uri);
_usedHashes.set(tokenId);
return tokenId;
}
概要
- 他のアドレスからトークンを受け取る関数。
詳細
- 送信者が自分自身からトークンを受け取ることはできません。
- トークンを受け取る前に、署名の検証を行い、URIをデコードしてトークンを発行します。
引数
-
from
- トークンを受け取る相手のアドレス。
-
metadata
- トークンに関する情報をバイト列で表すデータ。
-
signature
- 署名データ。
戻り値
- 発行されたトークンのIDを表す整数を返します。
decodeURI
function decodeURI(bytes calldata metadata)
public
virtual
returns (string memory)
{
return string(metadata);
}
概要
- メタデータをデコードして文字列として取得する関数。
詳細
- 渡されたバイト列形式のメタデータを文字列に変換して返します。
引数
-
metadata
- デコードするメタデータをバイト列で表すデータ。
戻り値
- メタデータを文字列として表す文字列を返します。
おっしゃる通り、途中で省略せずに続きを出力いたします。
_safeCheckAgreement
function _safeCheckAgreement(
address active,
address passive,
bytes calldata metadata,
bytes calldata signature
)
internal
virtual
returns (uint256)
{
bytes32 hash = _getHash(active, passive, metadata);
uint256 tokenId = uint256(hash);
require(
SignatureChecker.isValidSignatureNow(passive, hash, signature),
"_safeCheckAgreement: invalid signature"
);
require(!_usedHashes.get(tokenId), "_safeCheckAgreement: already used");
return tokenId;
}
概要
- 合意を確認し、トークンIDを生成する関数。
詳細
- 送信者(
active
)と受信者(passive
)のアドレス、メタデータ、および署名を基にトークンIDを生成します。 - 署名の検証と、同じトークンIDがすでに使用されていないかの確認を行います。
引数
-
active
- 送信者のアドレス。
-
passive
- 受信者のアドレス。
-
metadata
- トークンに関する情報をバイト列で表すデータ。
-
signature
- 署名データ。
戻り値
- 生成されたトークンIDを表す整数を返します。
_getHash
function _getHash(address active, address passive, bytes calldata metadata)
internal
view
returns (bytes32)
{
bytes32 structHash =
keccak256(abi.encode(AGREEMENT_HASH, active, passive, keccak256(metadata)));
return _hashTypedDataV4(structHash);
}
概要
- ハッシュを生成する関数。
詳細
- 送信者(
active
)と受信者(passive
)のアドレス、メタデータを基にハッシュを生成します。
引数
-
active
- 送信者のアドレス。
-
passive
- 受信者のアドレス。
-
metadata
- トークンに関する情報をバイト列で表すデータ。
戻り値
- 生成されたハッシュを表すバイト列(bytes32)を返します。
_exists
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _owners[tokenId] != address(0);
}
概要
- トークンが存在するかどうかを確認する関数。
詳細
- 指定されたトークンIDが存在するかどうかを確認します。
引数
-
tokenId
- 存在を確認するトークンのIDを表す整数。
戻り値
- トークンが存在する場合は
true
、存在しない場合はfalse
を返します。
_mint
function _mint(address from, address to, uint256 tokenId, string memory uri)
internal
virtual
returns (uint256)
{
require(!_exists(tokenId), "mint: tokenID exists");
_balances[to] += 1;
_owners[tokenId] = to;
_tokenURIs[tokenId] = uri;
emit Transfer(from, to, tokenId);
return tokenId;
}
概要
- トークンを発行する関数。
詳細
- 新しいトークンを発行し、所有者を設定し、URIを設定します。
引数
-
from
- トークンを発行するアドレス。
-
to
- トークンを所有するアドレス。
-
tokenId
- 発行するトークンのIDを表す整数。
-
uri
- トークンに関連付ける
URI(Uniform Resource Identifier)
。
- トークンに関連付ける
戻り値
- 発行されたトークンのIDを表す整数を返します。
_burn
function _burn(uint256 tokenId) internal virtual {
address owner = ownerOf(tokenId);
_balances[owner] -= 1;
delete _owners[tokenId];
delete _tokenURIs[tokenId];
emit Transfer(owner, address(0), tokenId);
}
概要
- トークンを焼却(削除)する関数。
詳細
- 指定されたトークンIDの所有者を特定し、トークンを焼却(削除)します。
引数
-
tokenId
- 焼却するトークンのIDを表す整数。
戻り値
- なし
セキュリティ考慮事項
この規格の実装に直接関係するセキュリティ上の考慮事項はありません。
引用
Tim Daubenschütz (@TimDaub), "ERC-4973: Account-bound Tokens [DRAFT]," Ethereum Improvement Proposals, no. 4973, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4973.
参考
What are Account-bound tokens?
最後に
今回は「World of Warcraftの伝説的なアイテムがキャラクターにバインドされるように、譲渡不可能なNFTがEthereumアカウントにバインドするインターフェースを提案しているERC4973」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!