Flow でランダムなパラメータを持つ NFT を作る
こんにちは。荒川です。久しぶりの投稿です。
Flow は最近、JavaScript SDK や Flow CLI に破壊的な変更があったりして、なかなか追従が大変です。
さて今回は、最近私が取り組んでいる、Flow 上のプロジェクトで作った NFT について紹介したいと思います。
目次
- Ethereum の NFT のレプリカを Flow に作成する
- Flow の NFT の基本的な構成
- Replica NFT のフィールド
- ランダムな値を手に入れるには
- コントラクトで生成する疑似乱数がなぜ危険か
Ethereum の NFT のレプリカを Flow に作成する
・・・ということができないかと思い、実験的にコントラクトを作り始めました。
Ethereum 上で NFT を持っている人が、その人の意志で、自分の NFT のコピーを Flow 上に作れるというものを想定しています。
これをやるためには、以下のような機能が必要になります。
- Ethereum の NFT の所有者チェック
- NFT の所有者による合意チェック
- これらを確認できたら、Flow にて、同じ情報を持つ NFT をmint する
また、加えて、NFT ごとに何かランダムなパラメータ(攻撃力とか防御力とか)を生成できたら、バーコ◯ドバトラー的で面白いんじゃないかということで、その検討も行いました。
お試しでコントラクトを作ってみたので、紹介していきます。(コードは GitHub に公開しています。)
Flow の NFT の基本的な構成
Flow の NFT は、 NonFungibleToken
というインターフェースのコントラクトを継承して作成します。
このコントラクトは、テストネット・メインネットにすでにデプロイされています(詳しくは公式ドキュメントを参照)。
以下のように、コントラクトを継承します。今回作成した NFT は、 Replica
という名前にしています。
import NonFungibleToken from 0x631e88ae7f1d7c20pub contract Replica: NonFungibleToken {
:
}
実装しなければならない主要なインターフェースは以下のとおりです。
id
フィールドを持つNFT
リソースwithdraw
deposit
関数を持つCollection
リソース
NonFungibleToken
コントラクトのコードでは、インターフェースは次のように定義されています(一部抜粋)。
pub resource interface INFT {
pub let id: UInt64
}pub resource interface Provider {
pub fun withdraw(withdrawID: UInt64): @NFT {
post {
result.id == withdrawID: "(略)"
}
}
}pub resource interface Receiver {
pub fun deposit(token: @NFT)
}
Flow のコントラクトの特長のひとつは、インターフェースに事前・事後条件を記述できることで、この場合だと withdraw の処理結果の id フィールドの値が、引数の値と一致する必要があることが明記されています。
この機能は、ある規格のコントラクトが、悪意をもって、あるいはバグによって変な挙動になることを防ぐので、コントラクトの安全性を高めます。
Replica NFT のフィールド
今回の NFT では、id に加えて、追加で以下のフィールドを用意しました。
pub resource NFT: NonFungibleToken.INFT {
pub let id: UInt64
pub let contractAddress: String
pub let tokenId: UInt64
pub let ownerAtTime: String
pub let message: String
pub let signature: String
pub let rarity: UInt8
pub let hp: UInt32
pub let def: UInt32
pub let atk: UInt32
:
}
contractAddress
tokenId
は元の Ethereum の NFT のコントラクトアドレスとトークンID、ownerAtTime
は Flow の NFT を mint した時点の所有者のEth アドレス、message
と signature
は Ethereum の NFT の所有者による合意のためのメッセージと署名、 rarity
hp
def
atk
はそれぞれレア度、体力、防御力、攻撃力で、NFT 生成時にランダムに決まる値を入れるフィールドです。
今回、署名の検証をコントラクト内でやりたかったのですが、いくつかの問題があることがわかり、断念しました。大きなところで言うと、Ethereum の署名で使われているハッシュ関数(Keccak-256)と、Flow の署名で利用できるハッシュ関数(SHA3-256)が違っていました。Flow の Cadence はどんどん機能が追加されているので、今後、検証できるようになる可能性はあります。期待してます。
ランダムな値を手に入れるには
結論から言うと、Flow のコントラクト内で安全なランダム値を生成する方法はありません。現在のところ、もっとも現実的な方法は、オラクルを使う、つまり、外部から値を与えてあげることです。
有名なオラクルのプロジェクトとして Chanlink が Flow でも使えるようになる予定です(関連記事)。ただし、まだ具体的な時期は発表されていません。なので現状は、NFT を発行する側がオフチェーンでランダムな値を用意して、コントラクトの関数を実行することで設定するのが現実的な落とし所かと思います。用途次第では、この作りで問題ないと思います。
今回は実験的な実装であり、また処理はサーバー側から実行していることもあり、コントラクト内で疑似乱数を利用するために、以下のような方法を試してみました。
※これは安全ではありません。
// このコードは安全ではありません。let block = getCurrentBlock()let rarityBase = Int(Int(block.id[0]) * Int(block.id[1])) % 1000let rarity = UInt8(rarityBase > 999 ? 5 : rarityBase > 990 ? 4 :
rarityBase > 850 ? 3 : rarityBase > 500 ? 2 : 1)let hp = UInt32(Int(Int(block.id[2]) % 100 + 1) *
Int(Int(block.id[3]) % 100 + 1))let def = UInt32(Int(Int(block.id[4]) % 100 + 1) *
Int(Int(block.id[5]) % 100 + 1))let atk = UInt32(Int(Int(block.id[6]) % 100 + 1) *
Int(Int(block.id[7]) % 100 + 1))
Cadence のコードでは、getCurrentBlock()
で、トランザクションやスクリプトを実行している現在のブロック情報を取得できます(詳細)。この中には、ブロック番号、ブロックタイムスタンプ、ブロック ID(ブロックのハッシュ値、UInt8 型の配列)が含まれており、上記のコードではブロック ID を使用してパラメータを決定しています。
各パラメータは、確率的に偏りが出るようにしています。例えば rarity
は、0.1 % の確率で 5
、0.9 % で 4
、14 % で 3
、35 % で 2
、50 % で 1
となります。 hp
def
atk
は疑似乱数を掛け合わせることで高い値が出にくいようにしてあります。
なお、Cadence 言語には、他に unsafeRandom()
という組み込み関数も用意されています(詳細)。これを使うと、UInt64 型の疑似乱数を取得できます。このほか Flow チームからは、乱数に関連するベストプラクティスも紹介されています。
コントラクトで生成する疑似乱数がなぜ危険か
ブロック ID を乱数として使う方法がなぜ安全ではないのかというと、先日、Meebits という NFT で起きた攻撃のように、希望のパラメータを持つ NFT が mint されるまで、トランザクションを送信 → 意図的に失敗ということを繰り返せてしまうからです。
私は、Flow ではこれが特に問題になると感じています。なぜなら、Flow ではトランザクションのコードをユーザーが自由に書けるため、希望のパラメータでなければトランザクションを失敗させるという処理を、追加のツールなど使うことなく、Flow の持つネイティブな機能だけで実現できてしまうからです。
次回以降の記事で、このプロジェクトについてさらに紹介したいと思います。お楽しみに!