はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、さまざまなNFTを特定のNFTに装備できるような、コンポーザブルなNFTを実現するERC6220についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
概要
「Composable NFTs utilizing equippable parts」は、ERC721を拡張し、NFT(非代替トークン)が自身にパーツを選択して装備することを可能にする標準です。
各NFTインスタンスごとに、カタログからパーツのリストを選び、そのNFTはカタログ内で定義されたスロットに他のNFTを装備できます。
カタログには、NFTが構成されるパーツが含まれています。
この提案では、2つの種類のパーツが導入されます。
スロットタイプのパーツと固定タイプのパーツです。
スロットタイプのパーツは、他のNFTコレクションを装備できる一方、固定パーツは独自のメタデータを持つ完全なコンポーネントです。
NFTにパーツを装備することは新しいトークンを生成するのではなく、トークンを取得する際にレンダリングされる別のコンポーネントを追加します。
動機
NFTはEthereumエコシステムで広く使用され、さまざまなユースケースに利用されています。
トークンが他のトークンを装備し、利用可能なパーツのセットから構成される能力を持つことは、より高いユーティリティ、使いやすさ、将来の互換性を提供します。
ERC721が公開されてから4年間、追加の機能が必要とされ、無数の拡張が行われてきました。
このEIPは、以下の領域でERC721を改良します。
- 構成
- トークンの進化
- メリットの追跡
- 証明可能なデジタル希少性
構成(Composing)
NFT同士は協力してより大きな構造物を作成することができます。
この提案以前では、複数のNFTを単一の構造物に組み合わせる際、特定のアカウントに関連付けられたすべての互換性のあるNFTをチェックし、無差別に使用する方法がありました(これは、同じスロットに使用することを意図したNFTが複数ある場合、予期せぬ結果になる可能性があります)。
また、組み合わせる部品のカスタム台帳を(スマートコントラクト内またはオフチェーンのデータベースで)管理する方法もありました。
この提案は、単一のNFTがどの部品を一体となって使用するかを選択できる、オンチェーン上の情報で構成可能なNFTの標準的なフレームワークを確立します。
この方法により、ベースNFTのほぼ無制限のカスタマイズが可能となります。
映画のNFTの例を挙げると、クレジットなどの一部は固定であるべきですが、シーンなどは交換可能であり、ベースバージョン、拡張版、記念版などのリリースを置き換えることができます。
トークンの進化(Token progression)
NFTはその存在のさまざまな段階を通じて進化し、さまざまな部品を取得または受賞することができます。
これはゲーミングの観点から説明できます。
この提案を活用したNFTを使用してキャラクターを表現することができ、ゲームプレイの活動を通じて入手したギアを装備できます。
ゲーム内で進行するにつれて、より良いアイテムが利用可能になります。
進行度に応じて収集したアイテムを表現するために多数のNFTを持つ代わりに、装備可能な部品をアンロックし、NFTの所有者がどのアイテムを装備するか、どのアイテムをインベントリに保持するか(装備されていない状態)を中央集権的な介在者なしで決定できます。
メリットの追跡(Merit tracking)
装備可能なNFTは、メリットを追跡するためにも使用できます。
これを学術的な功績の例で説明します。
この場合、装備可能なNFTは、学術的な成果のデジタルポートフォリオを表現し、所有者は自分の学位証明書、公開された論文、受賞歴などを装備して公開できます。
証明可能なデジタル希少性(Provable Digital Scarcity)
現在の多くのNFTプロジェクトは、モックの希少性しか提供していません。
一部のトークンしかないとしても、これらのトークンのユーティリティ(もしあれば)は制限されていません。
例えば、同じウォレットと同じNFTを使用して同じゲームの500の異なるインスタンスにログインし、同じ帽子を500の異なるゲーム内アバターに同時に装備することができます。
これは、その視覚的表現がクライアント側の仕組みだからです。
この提案では、帽子が1つのアバターに装備された場合(送信されてから装備される)、それを別のアバターに装備できないように強制する機能が追加されます。
これにより、本物のデジタル希少性が提供されます。
仕様
IERC6220
pragma solidity ^0.8.16;
import "./IERC5773.sol";
interface IERC6220 is IERC5773 /*, ERC165 */ {
struct Equipment {
uint64 assetId;
uint64 childAssetId;
uint256 childId;
address childEquippableAddress;
}
struct IntakeEquip {
uint256 tokenId;
uint256 childIndex;
uint64 assetId;
uint64 slotPartId;
uint64 childAssetId;
}
event ChildAssetEquipped(
uint256 indexed tokenId,
uint64 indexed assetId,
uint64 indexed slotPartId,
uint256 childId,
address childAddress,
uint64 childAssetId
);
event ChildAssetUnequipped(
uint256 indexed tokenId,
uint64 indexed assetId,
uint64 indexed slotPartId,
uint256 childId,
address childAddress,
uint64 childAssetId
);
event ValidParentEquippableGroupIdSet(
uint64 indexed equippableGroupId,
uint64 indexed slotPartId,
address parentAddress
);
function equip(
IntakeEquip memory data
) external;
function unequip(
uint256 tokenId,
uint64 assetId,
uint64 slotPartId
) external;
function isChildEquipped(
uint256 tokenId,
address childAddress,
uint256 childId
) external view returns (bool);
function canTokenBeEquippedWithAssetIntoSlot(
address parent,
uint256 tokenId,
uint64 assetId,
uint64 slotId
) external view returns (bool);
function getEquipment(
uint256 tokenId,
address targetCatalogAddress,
uint64 slotPartId
) external view returns (Equipment memory);
function getAssetAndEquippableData(uint256 tokenId, uint64 assetId)
external
view
returns (
string memory metadataURI,
uint64 equippableGroupId,
address catalogAddress,
uint64[] calldata partIds
);
}
Equipment
- Struct
Equippableコンポーネントの中核的な構造を格納するため構造体。
-
assetId
- 子を装備する親のアセットID。
-
childAssetId
- 装備として使用される子のアセットID。
-
childId
- 装備される子トークンのID。
-
childEquippableAddress
- 子アセットが属するコレクションのアドレス。
IntakeEquip
- Struct
装備データの入力用の構造体。
データを格納するのではなく、入力のために使用されます。
-
tokenId
- 管理対象の親トークンのID。
-
childIndex
- 親トークンのアクティブな子のリスト内での子のインデックス。
-
assetId
- 装備先のアセットID。
-
slotPartId
- 装備するスロットのパートID。
-
childAssetId
- 装備する子のアセットID。
ChildAssetEquipped
- Event
子アセットが親アセットに装備された時に発行されるイベント。
-
tokenId
- 装備を行った親トークンのID。
-
assetId
- 装備先の親アセットのID。
-
slotPartId
- 装備するスロットのパートID。
-
childId
- 装備された子トークンのID。
-
childAddress
- 子トークンのコレクションのアドレス。
-
childAssetId
- 装備された子のアセットID。
ChildAssetUnequipped
- Event
子アセットが親アセットから取り外された時に発行されるイベント。
-
tokenId
- 取り外しを行った親トークンのID。
-
assetId
- 取り外し元の親アセットのID。
-
slotPartId
- 取り外すスロットのパートID。
-
childId
- 取り外された子トークンのID。
-
childAddress
- 取り外された子のコレクションのアドレス。
-
childAssetId
- 取り外された子のアセットID。
ValidParentEquippableGroupIdSet
- Event
特定の親とスロットに対して、特定のEquippableグループのアセットが装備可能であることを通知するイベント。
-
equippableGroupId
- 装備可能グループのID。
-
slotPartId
- 装備可能グループのスロットのパートID
-
parentAddress
- 親のコレクションのアドレス
equip
- Function
子トークンを親トークンに装備する関数。
IntakeEquip
構造体が引数として与えられます。
unequip
- Function
子トークンを親トークンから取り外すための関数。
呼び出すためにはトークンの所有者または現在の所有者による権限が必要です。
引数
-
tokenId
- トークンを取り外す親トークンのID。
-
assetId
- 子トークンが装備されている親のアセットID。
-
slotPartId
- 取り外すスロットのパートID。
isChildEquipped
- Function
指定された親トークンが特定の子トークンを装備しているかどうかを確認する関数。
引数
-
tokenId
- 確認対象の親トークンのID。
-
childAddress
- 子トークンのスマートコントラクトアドレス。
-
childId
- 確認対象の子トークンのID。
戻り値
子トークンが装備されているかどうかを表すbool
値を返します。
canTokenBeEquippedWithAssetIntoSlot
特定の親トークンが指定されたスロットに、特定のトークンを装備できるかどうかを確認する関数。
引数
-
parent
- 親トークンのスマートコントラクトアドレス。
-
tokenId
- 装備を検討しているトークンのID。
-
assetId
- 装備するアセットのID。
-
slotId
- 装備するスロットのパートID。
戻り値
指定されたトークンが指定されたスロットに装備可能かどうかを表すbool
値を返します。
getEquipment
特定のトークンの指定されたスロットに装備されている、オブジェクトのデータを取得する関数。
引数
-
tokenId
- データを取得するトークンのID。
-
targetCatalogAddress
- トークンの Slot パートに関連付けられた Catalog のアドレス。
-
slotPartId
- 装備を確認するスロットのパートID。
戻り値
Equipment
構造体が返されます。
この構造体には装備されたオブジェクトに関するデータが含まれます。
getAssetAndEquippableData
指定されたトークンとアセットに関連するデータを取得する関数。
引数
-
tokenId
- データを取得するトークンのID。
-
assetId
- データを取得するアセットのID。
戻り値
アセットに関するメタデータURI、装備可能グループID、カタログのアドレス、アセット内のパートIDの配列が返されます。
複数のIDやアドレスが出てきたのでここで一旦整理します。
assetId
アセットの識別子で、特定のアセットを一意に識別するためのIDです。
例えば、特定の装備アイテムや部品のIDがここで使用されます。
childAssetId
子アセットの識別子で、親アセットに装備される子アセットを識別するためのIDです。
親アセットが装備品を持つ場合、その装備されるアイテムのIDがここに入ります。
childId
子の識別子で、親アセットに装備される子アセットを一意に識別するためのIDです。
子アセットが親アセットに装備される場合、その子アセットのIDがここに入ります。
childEquippableAddress
子アセットの装備可能なスマートコントラクトのアドレスです。
親アセットに装備される子アセットのスマートコントラクトがどこにあるかを示すアドレスです。
slotPartId
スロットパーツの識別子です。
スロットパーツは他のNFTを装備するためのスペースを提供するもので、そのスロットパーツ自体も一意なIDを持ちます。
スロットパーツがどの部分かを識別するために使用されます。
childAddress
子アセットのスマートコントラクトのアドレスです。
子アセットのスマートコントラクトがどこにあるかを示すアドレスです。子アセットが親アセットに装備される際に使用されます。
assetIdとchildAssetIdの関係性
仮想世界のゲームを考えてみます。
剣(Sword): assetId
が12345
。
盾(Shield): assetId
が67890
。
プレイヤーAが所有しているNFT(トークンID: 9876
)に剣を装備するケースを考えてみましょう。
プレイヤーAのNFT(トークンID: 9876
)に剣(assetId: 12345
)を装備したい。
この場合、assetId
は剣の一意の識別子です。
childAssetId
は、NFTが装備する子アセットの識別子です。
assetId: 9876
(NFTのアセットID)
childAssetId: 12345
(剣のアセットID)
このように、assetId
は親アセット(NFT)を、childAssetId
は装備された子アセット(NFT)を示すものです。
childAssetId と childId の関連性
childAssetId
: 子アセット(装備品など)の一意の識別子です。
childId
: NFT内での子アセットの識別子です。
1つのNFT内に複数の子アセットがある場合、それぞれにユニークなchildId
が割り当てられます。
子アセットがない場合、childId
は存在しません。
例で考えてみます。
プレイヤーAの所有するNFT(トークンID: 123
)に剣(childAssetId: 456
)と盾(childAssetId: 789
)を装備する場合:
- 剣の
childAssetId
:456
。 - 盾の
childAssetId
:789
。 - NFTの子アセットである剣の
childId
は1
。 - NFTの子アセットである盾の
childId
は2
。
つまり、childId
はNFT内での子アセットの個別の識別子です。
まとめ
「A」というNFTコレクション内のトークンIDが10
のNFTがあります。
「B」というNFTコレクション内のNFTに装備したいとします。
この時、childId
は「A」というNFTコレクション内のトークンIDである10
となり、childAssetId
は「B」というNFTコレクション内でのユニークな識別子が割り当てられます。
childEquippableAddress と childAddress の違い
-
childEquippableAddress
: 装備可能な子アセットのNFTがどのアドレスで装備されることができるかを示すものです。- 特定のNFTがどのスマートコントラクトで装備されるかを指定するために使用されます。
- これは、親のNFTに子供のNFTを装備する場合に重要な情報です。
- どのスマートコントラクトがその武器やアクセサリーを発行し、それを装備可能かどうかを示すために、
childEquippableAddress
が使用されます。
-
childAddress
:- 子アセット自体のスマートコントラクトのアドレスです。
具体例でみていきます。
ファンタジーゲームのNFTプラットフォームを考えてみます。
プレイヤーはキャラクターNFTを所有し、それに武器やアクセサリーなどの子供NFTを装備することができます。
プレイヤーが所有するキャラクターNFTに、武器を装備したいとします。
武器は別のスマートコントラクトで発行されており、そのスマートコントラクトのアドレスが childEquippableAddress
です。
このアドレスによって、キャラクターNFTに装備可能な武器NFTが制限されます。
つまり、childEquippableAddress
は装備可能な子アセットNFTが発行されるスマートコントラクトのアドレスを指します。
プレイヤーが所有するキャラクターNFTに、特定の武器NFTを装備した場合、その武器NFTのコントラクトアドレスが childAddress
です。
これにより、特定の武器NFTがどのスマートコントラクトが所有しているかを示します。
つまり、childAddress
は子アセットNFTを保持するスマートコントラクトのアドレスを指します。
ICatalog
pragma solidity ^0.8.16;
interface ICatalog /* is IERC165 */ {
event AddedPart(
uint64 indexed partId,
ItemType indexed itemType,
uint8 zIndex,
address[] equippableAddresses,
string metadataURI
);
event AddedEquippables(
uint64 indexed partId,
address[] equippableAddresses
);
event SetEquippables(uint64 indexed partId, address[] equippableAddresses);
event SetEquippableToAll(uint64 indexed partId);
enum ItemType {
None,
Slot,
Fixed
}
struct Part {
ItemType itemType; //1 byte
uint8 z; //1 byte
address[] equippable; //n Collections that can be equipped into this slot
string metadataURI; //n bytes 32+
}
struct IntakeStruct {
uint64 partId;
Part part;
}
function getMetadataURI() external view returns (string memory);
function getType() external view returns (string memory);
function checkIsEquippable(uint64 partId, address targetAddress)
external
view
returns (bool);
function checkIsEquippableToAll(uint64 partId) external view returns (bool);
function getPart(uint64 partId) external view returns (Part memory);
function getParts(uint64[] calldata partIds)
external
view
returns (Part[] memory);
}
Part
- Struct
アイテムの基本情報を保持する構造体。
-
itemType
- アイテムの種類を示す列挙型 (
None
、Slot
、Fixed
)。
- アイテムの種類を示す列挙型 (
-
z
- アイテムの表示の深さを指定する整数値。
-
equippable
- このアイテムを装備できるアドレスの配列。
-
metadataURI
- アイテムのメタデータURI。
IntakeStruct
- Struct
新しいPart
を追加するための構造体。
-
partId
-
Part
に割り当てるID。
-
-
part
- 追加される
Part
の情報を保持するPart
構造体。
- 追加される
AddedPart
- Event
新しいパートが追加された時に発行されるイベント。
-
partId
- 追加されたパートのID。
-
itemType
- パートの種類 (
None
、Slot
、Fixed
)。
- パートの種類 (
-
zIndex
- パートの深さを示す整数値。
-
equippableAddresses
- このパートを装備できるアドレスの配列。
-
metadataURI
- パートのメタデータURI。
AddedEquippables
- Event
特定のパートに新しい装備可能なアドレスが追加されたことを通知するイベント。
-
partId
- 新しい装備可能なアドレスが追加されたパートのID。
-
equippableAddresses
- 新しい装備可能なアドレスの配列。
SetEquippables
- Event
特定のパートの装備可能なアドレスのリストが新しいリストで上書きされた時に発行されるイベント。
-
partId
- 装備可能なアドレスのリストが上書きされたパートのID。
-
equippableAddresses
- 新しい装備可能なアドレスの配列。
SetEquippableToAll
- Event
特定のパートがすべてのアドレスによって装備可能であることを通知するイベント。
-
partId
- すべてのアドレスによって装備可能とされたパートのID。
getMetadataURI
- Function
関連するカタログのメタデータURIを取得する関数。
戻り値
関連するカタログのベースメタデータURIの文字列が返されます。
getType
- Function
関連するカタログのitemType
を取得する関数。
戻り値
関連するカタログの itemType の文字列が返されます。
checkIsEquippable
- Function
指定されたアドレスが指定されたパートを装備できるかどうかを確認する関数。
引数
-
partId
- 確認するパートのID。
-
targetAddress
- パートを装備できるかどうかを確認するアドレス。
戻り値
指定されたアドレスが指定されたパートを装備可能かどうかを示すbool
値が返されます。
checkIsEquippableToAll
- Function
特定のPart
がすべてのアドレスによって装備可能であるかを確認する関数。
引数
-
partId
- 確認する
Part
のID。
- 確認する
戻り値
特定のパートがすべてのアドレスによって装備可能かどうかを示すbool
値が返されます。
getPart
- Function
指定されたIDのPart
情報を取得する関数。
引数
-
partId
- 取得する
Part
のID。
- 取得する
戻り値
指定されたIDのパート情報を含むPart
構造体が返されます。
getParts
- Function
複数のPart
情報をまとめて取得する関数。
引数
-
partIds
- 取得したい複数の
Part
のIDが含まれる配列。
- 取得したい複数の
戻り値
指定されたIDの複数のPart
情報を含むPart
配列が返されます。
補足
なぜカタログを使用して直接的なNFTの装備をサポートしないのですか?
NFTを直接的に他のNFTに装備できる場合、その組み合わせは予測不可能になる可能性があります。
カタログを使用することで、パーツが事前に検証され、予想どおりの組み合わせを実現できます。
カタログのもう一つの利点は、再利用可能な固定パーツを定義できることです。
なぜ2種類のパーツを提案しているのですか?
一部のパーツは、すべてのトークンに共通であり、個々のNFTで表現する必要がない場合、固定パーツで表現できます。
これにより、オーナーのウォレットの混乱が軽減されるだけでなく、NFTに関連する繰り返しアセットを効率的に伝達する方法が導入されます。
一方、スロットパーツは、NFTを装備できるようにします。
これにより、異なるNFTコレクションをベースNFTに装備できる能力が提供され、検証された後に適切に構成されることが保証された無関係なコレクションを装備できます。
2つの異なるパーツの提案は、多くのユースケースをサポートするためであり、両方の使用を強制することはないため、必要に応じて構成が適用できます。
なぜ装備されたすべてのパーツを取得する方法が含まれていないのですか?
すべてのパーツを取得する操作は、すべての実装者にとって必要な操作ではない可能性があります。
さらに、拡張として追加することができ、フックを使用するか、インデクサを使用してエミュレートすることができます。
拡張として追加することができる
カタログの機能は基本的なものであり、特定の要件に合わせてカスタマイズする必要がある場合、追加の機能や操作をカタログに組み込むことができます。
これは、将来的なニーズに対応するための柔軟性を提供します。
フックを使用する
フック(Hook
)は、ソフトウェアコンポーネント間で特定のポイントでカスタムコードを挿入する仕組みです。
提案文の文脈では、カタログが特定の操作を実行する前や後に、外部のカスタムコードを実行できる仕組みを指しています。
これにより、既存の機能をカスタマイズしたり、追加の操作を組み込んだりすることが可能です。
インデクサを使用してエミュレートすることができる
インデクサ(Indexer
)は、データベースのような機能を持ち、特定の情報を迅速に検索したりクエリしたりするためのツールです。
提案文では、カタログに含まれる情報を効率的に検索・取得するために、インデクサを活用することができると述べています。
これにより、複雑な情報構造を持つカタログのデータを効率的に操作できます。
まとめると、「新しい機能や操作をカタログに追加でき、カタログの操作をカスタマイズできる仕組みの実現とデータの効率的な取得ができるようになる」ということです。
カタログは、1つのNFTコレクションをサポートするか、任意の数のコレクションをサポートするか、どちらが良いですか?
カタログは、使用ケースに対して疎通性のある方法で設計されているため、できる限り幅広い再利用性をサポートすることが合理的です。
1つのカタログが複数のコレクションをサポートすることで、最適な操作が可能になり、固定およびスロットパーツの設定時にガス料金が削減されます。
固定パーツ
固定パーツは、カタログに定義されています。
固有のメタデータを持ち、NFTのライフサイクルを通じて変更されることはありません。
固定パーツは置き換えることはできません。
固定パーツの利点は、一度定義するだけで任意の数のトークンとコレクションで装備可能なパーツを表現できることです。
一度だけ定義すれば、複数のトークンで再利用できる利点があります。
具体例として、仮想ペットのNFTを考えてみましょう。
この仮想ペットは目や耳などの部分を持っています。
これらの部分はすべて同じデザインで、どの仮想ペットにも適用できます。
この場合、目や耳は固定パーツです。
一度定義されれば、複数の仮想ペットで再利用できます。
スロットパーツ
スロットパーツはカタログで定義され、カタログに含まれています。
これらには独自のメタデータはありませんが、選択されたNFTコレクションを装備するサポートを提供します
スロットに装備されるトークンには独自のメタデータが含まれます。
これにより、ベースNFTの所有者が制御する装備可能な変更可能なコンテンツが可能となります。
これらは任意の数のトークンに任意の数のコレクションを装備できるため、特定のスロットにどのNFTを装備できるかを一度検証してから何度も再利用できる信頼性のある最終トークンの構築が可能です。
具体例として、先ほどと同じく仮想ペットのNFTを考えます。
この仮想ペットには背中にスペースがあり、そこに異なる色や模様のフリルを装備できるスロットパーツがあります。
ユーザーは異なるフリルを装備して、自分の仮想ペットの外見をカスタマイズできます。
この場合、フリルはスロットパーツであり、異なるNFTを装備するスペースを提供します。
固定パーツとスロットパーツの違いとしては、固定パーツは同じデザインを再利用するためのものであり、スロットパーツは他のNFTを組み合わせてカスタマイズするためのスペースを提供するものです。
後方互換性
Equippableトークンの標準はERC721と互換性があり、ERC721の実装に対する堅牢なツールを活用するために作成され、既存のERC721インフラストラクチャとの互換性を確保しています。
テスト
Tests are included in equippableFixedParts.ts and equippableSlotParts.ts.
equippableFixedParts.ts
equippableSlotParts.ts
参考実装
セキュリティ考慮事項
ERC721と同じセキュリティ上の考慮事項が適用されます。
burn
、add
、accept
など、どの関数にも隠されたロジックが存在する可能性があります。
引用
Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer), "ERC-6220: Composable NFTs utilizing Equippable Parts," Ethereum Improvement Proposals, no. 6220, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6220.
考察
NFTにNFTを装備できるというのは面白いなと思いました。
さまざまなNFTプロジェクトとコラボしていけるので、できることも幅広そうです。
例えばゲームにおいて、キャラクターに装備できるアイテムだったり、ペットとして他のNFTコレクションのNFTを装備させたりなどが思い浮かびます。
パーティーなどもここでいう「装備」扱いにすれば、所有しているキャラクターを、自身のNFTに装備することで4人パーティーによるRPGゲームとかもできそうです。
何か作ってみたいと思うような面白い規格です。
最後に
今回は「さまざまなNFTを特定のNFTに装備できるような、コンポーザブルなNFTを実現するERC6220」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
採用強化中!
CryptoGamesでは一緒に働く仲間を大募集中です。
この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!