はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、SFT(セミファンジブルトークン)のロール管理の仕組みを提案しているERC7589についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
この規格では、SFT(セミファンジブルトークン) のロール管理について提案されています。
SFT(セミファンジブルトークン)とは、ERC1155などをはじめとするERC20とERC721の2つの特徴を併せ持つトークンのことです。
ERC1155はERC721の特徴であるtokenId
を持ちつつ、そのtokenId
はERC20のように複数発行することができます。
各ロールの割り当ては単一のアドレスに付与され、期限が切れると自動でロールが失効されます。
ロールはbytes32
型として定義され、カスタマイズ可能な任意のデータサイズを持つフィールドである_data
が定義されています。
ERC20については以下の記事を参考にしてください。
ERC721については以下の記事を参考にしてください。
ERC1155については以下の記事を参考にしてください。
動機
ERC1155は開発者が単一のコントラクトでファンジブル(代替性)とノンファンジブル(非代替性)の特徴を併せ持つトークンを作成できるようにする規格で、Ethereumのトークン化機能に大きく貢献しました。
ERC1155はトークンの所有権のトラッキングには優れていますが、これは主にトークンの残高に焦点を当てていて、トークンの利用方法にはあまり焦点が当てられていません。
トークンの使用において不可欠なのはアクセスコントロールです。
これにより、誰がトークンを使用できる権限を持っているのか管理できます。
また、場合によっては所有者の残高を完全にコントロールすることもできます。
例えば、「トークンをいくつ以上持っている場合はこの機能を実行できる」などです。
多くの場合、権限を他のユーザーに委任することで、より複雑なユースケースを実現することができます。
例えば、ゲームの中では、1つのERC1155コントラクトでゲーム内アイテムを発行し、それらをセキュアなロール管理インターフェースを介して他のプレイヤーに貸し出すことができます。
仕様
この規格に準拠するコントラクトは以下のインターフェースを実装する必要があります。
/// @title ERC-7589 Semi-Fungible Token Roles
/// @dev See https://eips.ethereum.org/EIPS/eip-7589
/// Note: the ERC-165 identifier for this interface is 0xc4c8a71d.
interface IERC7589 /* is IERC165 */ {
/** Events **/
/// @notice Emitted when tokens are committed (deposited or frozen).
/// @param _grantor The owner of the SFTs.
/// @param _commitmentId The identifier of the commitment created.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _tokenAmount The token amount.
event TokensCommitted(
address indexed _grantor,
uint256 indexed _commitmentId,
address indexed _tokenAddress,
uint256 _tokenId,
uint256 _tokenAmount
);
/// @notice Emitted when a role is granted.
/// @param _commitmentId The commitment identifier.
/// @param _role The role identifier.
/// @param _grantee The recipient the role.
/// @param _expirationDate The expiration date of the role.
/// @param _revocable Whether the role is revocable or not.
/// @param _data Any additional data about the role.
event RoleGranted(
uint256 indexed _commitmentId,
bytes32 indexed _role,
address indexed _grantee,
uint64 _expirationDate,
bool _revocable,
bytes _data
);
/// @notice Emitted when a role is revoked.
/// @param _commitmentId The commitment identifier.
/// @param _role The role identifier.
/// @param _grantee The recipient of the role revocation.
event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee);
/// @notice Emitted when a user releases tokens from a commitment.
/// @param _commitmentId The commitment identifier.
event TokensReleased(uint256 indexed _commitmentId);
/// @notice Emitted when a user is approved to manage roles on behalf of another user.
/// @param _tokenAddress The token address.
/// @param _operator The user approved to grant and revoke roles.
/// @param _isApproved The approval status.
event RoleApprovalForAll(address indexed _tokenAddress, address indexed _operator, bool _isApproved);
/** External Functions **/
/// @notice Commits tokens (deposits on a contract or freezes balance).
/// @param _grantor The owner of the SFTs.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _tokenAmount The token amount.
/// @return commitmentId_ The unique identifier of the commitment created.
function commitTokens(
address _grantor,
address _tokenAddress,
uint256 _tokenId,
uint256 _tokenAmount
) external returns (uint256 commitmentId_);
/// @notice Grants a role to `_grantee`.
/// @param _commitmentId The identifier of the commitment.
/// @param _role The role identifier.
/// @param _grantee The recipient the role.
/// @param _expirationDate The expiration date of the role.
/// @param _revocable Whether the role is revocable or not.
/// @param _data Any additional data about the role.
function grantRole(
uint256 _commitmentId,
bytes32 _role,
address _grantee,
uint64 _expirationDate,
bool _revocable,
bytes calldata _data
) external;
/// @notice Revokes a role.
/// @param _commitmentId The commitment identifier.
/// @param _role The role identifier.
/// @param _grantee The recipient of the role revocation.
function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external;
/// @notice Releases tokens back to grantor.
/// @param _commitmentId The commitment identifier.
function releaseTokens(uint256 _commitmentId) external;
/// @notice Approves operator to grant and revoke roles on behalf of another user.
/// @param _tokenAddress The token address.
/// @param _operator The user approved to grant and revoke roles.
/// @param _approved The approval status.
function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external;
/** View Functions **/
/// @notice Returns the owner of the commitment (grantor).
/// @param _commitmentId The commitment identifier.
/// @return grantor_ The commitment owner.
function grantorOf(uint256 _commitmentId) external view returns (address grantor_);
/// @notice Returns the address of the token committed.
/// @param _commitmentId The commitment identifier.
/// @return tokenAddress_ The token address.
function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_);
/// @notice Returns the identifier of the token committed.
/// @param _commitmentId The commitment identifier.
/// @return tokenId_ The token identifier.
function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_);
/// @notice Returns the amount of tokens committed.
/// @param _commitmentId The commitment identifier.
/// @return tokenAmount_ The token amount.
function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_);
/// @notice Returns the custom data of a role assignment.
/// @param _commitmentId The commitment identifier.
/// @param _role The role identifier.
/// @param _grantee The recipient the role.
/// @return data_ The custom data.
function roleData(
uint256 _commitmentId,
bytes32 _role,
address _grantee
) external view returns (bytes memory data_);
/// @notice Returns the expiration date of a role assignment.
/// @param _commitmentId The commitment identifier.
/// @param _role The role identifier.
/// @param _grantee The recipient the role.
/// @return expirationDate_ The expiration date.
function roleExpirationDate(
uint256 _commitmentId,
bytes32 _role,
address _grantee
) external view returns (uint64 expirationDate_);
/// @notice Returns the expiration date of a role assignment.
/// @param _commitmentId The commitment identifier.
/// @param _role The role identifier.
/// @param _grantee The recipient the role.
/// @return revocable_ Whether the role is revocable or not.
function isRoleRevocable(
uint256 _commitmentId,
bytes32 _role,
address _grantee
) external view returns (bool revocable_);
/// @notice Checks if the grantor approved the operator for all SFTs.
/// @param _tokenAddress The token address.
/// @param _grantor The user that approved the operator.
/// @param _operator The user that can grant and revoke roles.
/// @return isApproved_ Whether the operator is approved or not.
function isRoleApprovedForAll(
address _tokenAddress,
address _grantor,
address _operator
) external view returns (bool isApproved_);
}
この規格では、コミットという言葉が出てきますが、これは以下のことを表しています。
-
デポジット(預け入れ)
- トークンを特定のスマートコントラクトに預け入れることです。
- この場合、トークンは所有者からスマートコントラクトに移され、特定の条件が満たされるまで引き出せない状態になります。
-
フリーズ
- トークンを一時的に使用できないようにすることです。
- この場合、トークンは所有者のアドレスに残ったままですが、特定の条件が満たされるまで使用や
transfer
が制限されます。
Event
TokensCommitted
トークンがコミット(デポジットまたはフリーズ)されたときに発行されるイベント。
発行者、コミットメントID、トークンアドレス、トークンID、トークン量を記録します。
RoleGranted
ロールが付与されたときに発行されるイベント。
コミットメントID、ロールID、ロールを付与されたアドレス、期限、取り消しできるか、追加データを記録します。
RoleRevoked
ロールが取り消されたときに発行されるイベント。
コミットメントID、ロールID、ロールを外されたアドレスを記録します。
TokensReleased
トークンがコミットメントから解放されたときに発行されるイベント。
コミットメントIDを記録します。
RoleApprovalForAll
他のユーザーがロールを管理する権限を付与または取り消したときに発行されるイベント。
トークンアドレス、承認されたユーザー、承認ステータスを記録します。
書き込み関数
commitTokens
トークンをコミット(デポジットまたはフリーズ)する関数。
発行者、トークンアドレス、トークンID、トークン量を指定し、コミットメントIDを返します。
grantRole
ロールをアドレスに付与する関数。
コミットメントID、ロールID、付与されるアドレス、期限、取り消しできるか、追加データを指定します。
revokeRole
ロールを取り消す関数。
コミットメントID、ロールID、取り消されるアドレスを指定します。
releaseTokens
コミットメントからトークンを発行者に戻す関数。
コミットメントIDを指定します。
setRoleApprovalForAll
他のユーザーがロールを管理する権限を付与または取り消す関数。
トークンアドレス、承認するユーザー、承認ステータスを指定します。
view関数
grantorOf
コミットメントの所有者(発行者)を返す関数。
コミットメントIDを指定します。
tokenAddressOf
コミットされたトークンのアドレスを返す関数。
コミットメントIDを指定します。
tokenIdOf
コミットされたトークンのIDを返す関数。
コミットメントIDを指定します。
tokenAmountOf
コミットされたトークンの量を返す関数。
コミットメントIDを指定します。
roleData
ロール付与のカスタムデータを返す関数。
コミットメントID、ロールID、ロール付与されたアドレスを指定します。
roleExpirationDate
ロール付与の期限を返す関数。
コミットメントID、ロールID、ロール付与されたアドレスを指定します。
isRoleRevocable
ロールが取り消し可能かどうかを返す関数。
コミットメントID、ロールID、ロール付与されたアドレスを指定します。
isRoleApprovedForAll
特定のユーザーが全てのSFTに対してロールを管理する権限があるかどうかを返す関数。
トークンアドレス、発行者、承認されたユーザーを指定します。
単一のトランザクションの拡張
ロールの付与は2つのトランザクションを必要とします。
1つ目のトランザクションではトークンをコミットし、2つの目のトランザクションでロールを付与します。
以下の拡張機能を実装することで、ユーザーは1つのトランザクションでコミットからロール付与まで実行できます。
/// @title ERC-7589 Semi-Fungible Token Roles, optional single transaction extension
/// @dev See https://eips.ethereum.org/EIPS/eip-7589
/// Note: the ERC-165 identifier for this interface is 0x5c3d7d74.
interface ICommitTokensAndGrantRoleExtension /* is IERC7589 */ {
/// @notice Commits tokens and grant role in a single transaction.
/// @param _grantor The owner of the SFTs.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _tokenAmount The token amount.
/// @param _role The role identifier.
/// @param _grantee The recipient the role.
/// @param _expirationDate The expiration date of the role.
/// @param _revocable Whether the role is revocable or not.
/// @param _data Any additional data about the role.
/// @return commitmentId_ The identifier of the commitment created.
function commitTokensAndGrantRole(
address _grantor,
address _tokenAddress,
uint256 _tokenId,
uint256 _tokenAmount,
bytes32 _role,
address _grantee,
uint64 _expirationDate,
bool _revocable,
bytes calldata _data
) external returns (uint256 commitmentId_);
}
commitTokensAndGrantRole
トークンをコミットし、ロールを1つのトランザクションで付与する関数。
-
パラメータ
-
_grantor
- SFTの所有者。
-
_tokenAddress
- トークンのアドレス。
-
_tokenId
- トークンの識別子。
-
_tokenAmount
- トークンの量。
-
_role
- ロールの識別子。
-
_grantee
- ロール付与されるアドレス。
-
_expirationDate
- ロールの有効期限。
-
_revocable
- ロールが取り消し可能かどうか。
-
_data
- ロールに関する追加データ。
-
-
戻り値:
-
commitmentId_
- 作成されたコミットメントの識別子。
-
ロールバランスの拡張
/// @title ERC-7589 Semi-Fungible Token Roles, optional role balance extension
/// @dev See https://eips.ethereum.org/EIPS/eip-7589
/// Note: the ERC-165 identifier for this interface is 0x2f35b73f.
interface IRoleBalanceOfExtension /* is IERC7589 */ {
/// @notice Returns the sum of all tokenAmounts granted to the grantee for the given role.
/// @param _role The role identifier.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _grantee The user for which the balance is returned.
/// @return balance_ The balance of the grantee for the given role.
function roleBalanceOf(
bytes32 _role,
address _tokenAddress,
uint256 _tokenId,
address _grantee
) external returns (uint256 balance_);
}
コアインターフェースでは、コミットされたトークンの残高を取得できますが、特定のユーザーのコミットされた残高を取得することができません。
この拡張機能を実装することで、特定のユーザーがコミットしている特定のトークンの残高を取得できます。
ただし、この拡張機能は、特定のユースケースにおいて役立ちますが、実装の複雑さを増す可能性があります。
roleBalanceOf
指定されたロールに対して特定のユーザーが持つトークンの合計残高を返す関数。
-
パラメータ
-
_role
- ロールの識別子。
-
_tokenAddress
- トークンのアドレス。
-
_tokenId
- トークンの識別子。
-
_grantee
- 残高を返す対象ユーザー。
-
-
戻り値:
-
balance_
- 指定されたロールに対する対象ユーザーのトークン合計残高。
-
メタデータ拡張
SFTのこれまでのJSONベースのメタデータスキーマを拡張します。
メタデータにロールに関する情報を追加できる拡張機能であり、ERC1155を使用しているDappsはこの機能を実装する必要があります。
{
/** Existing ERC-1155 Metadata **/
"title": "Token Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this token represents"
},
"decimals": {
"type": "integer",
"description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
},
"description": {
"type": "string",
"description": "Describes the asset to which this token represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
},
/** Additional fields for ERC-7589 **/
"roles": [{
"id": {
"type": "bytes32",
"description": "Identifies the role"
},
"name": {
"type": "string",
"description": "Human-readable name of the role"
},
"description": {
"type": "string",
"description": "Describes the role"
},
"inputs": [{
"name": {
"type": "string",
"description": "Human-readable name of the argument"
},
"type": {
"type": "string",
"description": "Solidity type, e.g., uint256 or address"
}
}]
}]
}
例としては以下のようになります。
{
/** Existing ERC-1155 Metadata **/
"name": "Asset Name",
"description": "Lorem ipsum...",
"image": "https:\/\/s3.amazonaws.com\/your-bucket\/images\/{id}.png",
"properties": {
"simple_property": "example value",
"rich_property": {
"name": "Name",
"value": "123",
"display_value": "123 Example Value",
"class": "emphasis",
"css": {
"color": "#ffffff",
"font-weight": "bold",
"text-decoration": "underline"
}
},
"array_property": {
"name": "Name",
"value": [1,2,3,4],
"class": "emphasis"
}
},
/** Additional fields for ERC-7589 **/
"roles": [
{
// keccak256("Player(uint256)")
"id": "0x70d2dab8c6ff873dc0b941220825d9271fdad6fdb936f6567ffde77d05491cef",
"name": "Player",
"description": "The user allowed to use this item in-game.",
"inputs": [
{
"name": "ProfitShare",
"type": "uint256"
}
]
}
]
}
ロール配列のプロパティは推奨機能であり、開発者ユースケースに関連するその他の情報(ロールごとの画像など)を追加する必要があります。
inputs
プロパティはエンコードされてgrantRole関数に渡されるべきパラメータを記述し、データの形式を表すtype
プロパティとcomponents
を含めることができます。
また、Solidity ABIの仕様で定義されているプロパティタイプとコンポーネントを使用することが推奨されます。
Solidity ABIの仕様については以下の公式ドキュメントを参考にしてください。
注意
この規格に準拠するコントラクトを実装する時の注意点がまとめられています。
基本要件
-
準拠インターフェイス
- コントラクトは必ず
IERC7589
インターフェイスを実装する必要があります。
- コントラクトは必ず
-
ロールの識別子
- すべてのロールは
bytes32
識別子で表されます。 - 識別子としては、ロール名と引数の
keccak256
ハッシュを使用することが推奨されます。 - 例
keccak256("Player(uint256)")
- すべてのロールは
commitTokens
関数
commitTokens
関数は、トークンをコミット(デポジットまたはフリーズ)する関数です。
この関数は、以下の条件が満たされない場合にrevert
(実行を中止)します。
_tokenAmount
がゼロ以外の必要があり、msg.sender
が_grantor
によって承認されている必要があります。
grantRole
関数
grantRole
関数は、特定のロールを特定のアドレスに付与する関数です。
この関数は、_expirationDate
が過去の日付である場合や、msg.sender
がgrantor
を代表してロールを付与する権限を持っていない場合にrevert
します。
永久ロールにはtype(uint64).max
を使用することが推奨されます。
revokeRole
関数
revokeRole
関数は、付与されたロールを取り消す関数です。
この関数は常にgrantee
が自分のロールを取り消すことを許可すべきです。
revert
条件として、ロールの割り当てが見つからない場合、msg.sender
がgrantor
またはgrantee
から承認されていない場合、msg.sender
がgrantor
であるか承認されているが、ロールが取り消し可能でないか期限切れでない場合があります。
releaseTokens
関数
releaseTokens
関数は、コミットされたトークンを発行者に戻す関数です。
この関数は、コミットメントが見つからない場合、msg.sender
がgrantor
から承認されていない場合、少なくとも1つの非取り消しロールがあり、それが期限切れでない場合にrevert
します。
Please note that “approval” refers to allowing users to commit tokens and grant/revoke roles on one’s behalf. An approved user either received the role approval or is the target user. Role approvals are not to be confused with ERC-1155 approvals. More information can be found in the Role Approvals section.
“approve”は、ユーザーが自分に変わってトークンをコミットしてロールを付与/取り消すことを許可することを指すことに注意してください。“approve”されたユーザーはロールの承認を受けるかターゲットユーザーとなります。ロールの承認はERC1155の
approve
と混同しないでください。...
補足
理論的背景
「トークンコミットメント」の概念は、SFT(セミファンジブルトークン)の制御を委任するための機能です。
トークンコミットメントは、凍結された残高またはコントラクトにデポジットされたトークンを表し、SFTの所有者がその資産の使用を他のアドレスに委任します。
ERC7589の独自性
ERC7589は、ERC1155の拡張ではありません。
これはこの標準を特定の実装から独立させることにあります。
このアプローチにより、標準を外部またはSFTと同じコントラクトで実装することが可能になり拡張性が向上します。
ロール承認
ERC7589は、ユーザーがオペレーターに対してロールの付与および取り消しを許可する機能を提供します。
この機能は相互運用性において重要であり、サードパーティのアプリケーションがユーザーロールを管理する時にカストディレベルの承認を必要としません。
ロール承認はコアインターフェイスの一部であり、準拠するコントラクトはsetRoleApprovalForAll
およびisRoleApprovedForAll
関数を実装する必要があります。
自動有効期限
自動有効期限はユーザーのガス代を節約するために実装されました。
ロールの割り当てを終了するには、ユーザーが常にrevokeRole
を呼び出す必要がないように、アプリケーションはroleExpirationDate
を呼び出して現在のタイムスタンプと比較し、ロールがまだ有効かどうかを確認する必要があります。
ERC7589の文脈では、日付はuint64
で表されます。
uint64
で表される最大のUNIXタイムスタンプは約5840億年であり、これを「永久」と見なすのに十分です。
このため、type(uint64).max
を使用することで、ロールが決して期限切れにならないことを表します。
取り消し可能なロール
特定のシナリオでは、grantor
がロールの有効期限前にロールを取り消す必要があります。
一方、grantee
はロールが早期に取り消されないことを保証する必要があります(例えば、grantee
がトークンを利用するために支払う場合)。このため、grantRole
関数には_revocable
パラメータが含まれており、grantor
が有効期限前にロールを取り消すことができるかどうかを指定します。
_revocable
の値に関係なく、grantee
は常にロールを取り消すことができるため、ロールを付与されたアドレスは不要な割り当てを排除することができます。
カスタムデータ
grantRole
関数の_data
パラメータは重要な機能です。
SFTにはさまざまなユースケースがあり、それらをすべてSolidityレベルのインターフェイスでカバーすることは現実的ではありません。
そのため、bytes
型の汎用パラメータが組み込まれ、ロールを付与する時に任意のカスタム情報を渡すことができます。
例えば、Web3ゲームでは、NFTをプレイヤーに委任して利益共有機能を導入することがあります。
これはuint256
で表されます。
ERC7589を使用すると、uint256
をbytes
としてエンコードし、それをgrantRole
関数に渡すことができます。
データの検証はオンチェーンまたはオフチェーンで行うことができ、他のコントラクトはroleData
関数を使用してこの情報をクエリできます。
互換性
多くのSFTはアップグレードできない仕組みでコントラクトをデプロイします。
この時、「SFTのロール管理をどのように実現するか」という課題があります。
この提案では、トークンのコミットめんど時に、tokenAddress
パラメータを渡すことで、SFT内部にERC7589を実装するか、外部の信頼できるコントラクトにロール管理を託すか選択することができます。
実装
以下に参考実装のコードが格納されています。
セキュリティ
SFTとこの規格を統合する時以下の点に注意する必要があります。
- 不正なロールの割り当てや取り消しを防止するために、適切なアクセス制御が行われているかを確認する。
- 特にトークンのコミットとリリースにおいて重要です。
- リエントランシー攻撃などの潜在的な攻撃を考慮し、適切なセーフガード機能(
ReentrancyGuard
など)が実装されているか確認する。 - ユーザーにロール割り当ての利用を許可する前に、常に有効期限を確認するようにする。
引用
Ernani São Thiago (@ernanirst), Daniel Lima (@karacurt), "ERC-7589: Semi-Fungible Token Roles [DRAFT]," Ethereum Improvement Proposals, no. 7589, December 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7589.
最後に
今回は「SFT(セミファンジブルトークン)のロール管理の仕組みを提案しているERC7589」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!