今年の秋に書いてほったらかしてた記事があったので年末のお掃除も兼ねて公開します。
自分の理解を深めるためにブログの体裁で書いたメモですので、期待はしないでください。少しでも役に立てれば幸いです。
対象読者:
OpenSeaを触ったことがあるが、仕組みはイマイチ分かっていない技術者
———
OpenSeaの売買はどのような仕組みで動いているかを解説していきます。といっても、実際には数日調べた程度ですので間違い等あればご指摘いただければと思います。
この記事ではNFTの売買のみを取り上げます。オークションやその他のケースについては今回は取り上げません。
OpenSeaの売買を振り返る
詳細な説明に入る前に、OpenSeaでNFTを売買する際にユーザーはどういったアクションを取るのかを確認しましょう。
今回は、売り手と買い手に見立てた2つのアドレスを用意しました。それぞれ以下に示すWalletを持っており、Rinkeby Testnetで取引を行います。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F650592ce-7fc8-4aee-86a5-c620828e94e0%2Falice_bob02.png?table=block&id=ec18a1b7-74a3-42fd-93a6-affb25448152&cache=v2)
大まかな流れとしては以下の4工程になります。
0. AliceがNFTを発行する 1. AliceがそのNFTをOpenSeaで売りに出す 2. それを見たBobが購入処理をする 3. NFTの所有権がAliceからBobに移動する
それでは、順を追って詳しく見ていきましょう。
0. AliceがNFTを発行する
タイトルの通りです。NFT発行の仕組みについては割愛します。
今回は、Mettani というContractを作成しました。(名前は某NFTプロジェクトのパクリ)
OpenSea:
ContractはOpenZeppelinのpresetであるERC721PresetMinterPauserAutoId に少し変更を加えたもので、市場で流通しているNFTと同様のものです。ソースコードはどこかに行ったので探しておきます。
また、このMettani Token Contractを使用してトークンをMintしました。
Transactionは Contract Creation と Mint を見てください。
1. AliceがNFTをOpenSeaで売りに出す
NFTが発行できたことを確認したら、MettaniのContractAddressで検索します。すると、既にNFTの情報(metadata)が表示されていることが分かります。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F06bf86d4-a369-4964-bd7e-e2e0951eeb59%2FScreen_Shot_2021-09-20_at_21.55_2.png?table=block&id=2c2983c9-dfee-4d24-b2fb-9ec41c79af81&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7bd9f7e6-c785-454e-8c32-511e8c550bc1%2F01_opensea_minted_alice.png?table=block&id=c0e2e2d2-c52e-495d-afd9-c281e0c2bf97&cache=v2)
また、右上の方にSellボタンが出ていますが、これはAliceがこのNFTを所有しているためです。では、このSellボタンを押してNFTを売ってみましょう。
ボタンを押すと、モーダルが出てきて、同時にMetamaskのウインドウが出てきます。3つ工程がありますが、詳細は後述します。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F3de53268-0c88-4920-905a-0e4145f44c5d%2FScreen_Shot_2021-09-20_at_22.06_3.png?table=block&id=d55dd980-407d-493e-897d-3c97bcd49e02&cache=v2)
無事、NFTを売りに出すことが出来ました。右上のボタンがCancel OrderとLower Priceというボタンに変わっていることが分かります。これはNFTの売却を取り消したり、価格を下げるという意味です。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fb1db3e8d-959e-44c6-8d7b-db6555296034%2FScreen_Shot_2021-09-20_at_22.18_4.png?table=block&id=882edb4c-1af2-46ab-8cc8-a093c82a7183&cache=v2)
売りに出すと言うことは、買い手がいないと取引は成立しないため、Aliceは買い手が現れるまで待ちます。
2. Bobが購入する
次はBob側の視点です。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F602e1845-5bbe-4cf8-8b36-937d1fab648c%2FScreen_Shot_2021-09-20_at_22.18_5.png?table=block&id=6f187367-edc2-4fd5-9b5a-c3ea552dcef6&cache=v2)
Aliceが見ていた画面とは少しだけ違う点があります。右上にあったボタンが消え、"Buy Now"と"Make Offer"という画面が中央付近に現れたことです。これはそれぞれ、以下のような意味になります。
- Buy Now
- Aliceが出した金額で買う
- Make Offer
- Aliceに別の金額を要求する(値下げ交渉)
今回Bobは"Buy Now"だけ行うものとします。
BobがBuy Nowをクリックすると、モーダルが現れました。モーダルの情報を確認して、ボタンを押します。すると、Metamaskのウインドウが現れ、Transactionを送ることを承認するか、拒否するかという選択が出てきました。承認します。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fb131481a-0c5c-4760-a0aa-b184174fd027%2FScreen_Shot_2021-09-20_at_22.20_2.png?table=block&id=b6e4b079-bf3f-4f52-ad3b-9982c5cccbd6&cache=v2)
3. NFTの所有権がAliceからBobに移動する
Bobの送ったTransactionが承認され、無事にNFTがBobのものになりました。このときAlice側からページを見てみると、Owned by B0B400と表示され、Bobのものになっていることが分かります。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F0fc11211-59a9-42ce-b124-6d25680892e8%2Fa.png?table=block&id=7abcf37e-8f54-4d41-a7b7-87e80d03cf79&cache=v2)
なにが起きていたのか
まずは、全体の流れを表した図です。矢印が色分けされていますが、以下のように対応しています。
- 赤: 1. AliceがNFTをOpenSeaで売りに出す
- 緑: 2. Bobが購入する
- 青: 3. NFTの所有権がAliceからBobに移動する
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fd3d20592-8188-488f-9053-beb179cbb64a%2Fopensea_research01.png?table=block&id=e0998893-b840-424e-a0b7-d15e6c3db275&cache=v2)
これだけだとよくわからないと思うので順を追って説明します。
それぞれのContractに関する説明や、全体像については触れませんので、もし予備知識がない方は以下の記事を目を通しておくといいかと思います。
1. AliceがNFTをOpenSeaで売りに出す
それでは、実際のTransactionを元に何が起きていたのかを確認していきましょう。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fb43a8581-ade1-42c0-8244-9fdc1fcb026c%2FScreen_Shot_2021-09-20_at_22.06_3.png?table=block&id=63acb58e-521e-4ec9-91ef-38c83706b682&cache=v2)
1.1. Delegated Proxyを作成
まず最初に、ProxyRegistoryコントラクトのregisterProxy()という関数を実行して、OwnableDelegatedProxy(以下DelegatedProxy)というContractを作成します。
以下のリンクでTransactionを確認できます。https://rinkeby.etherscan.io/tx/0xd4f50fdd6d047adb969b1a4f26ed80077a40617498559ee5290f0351076ff5ea
このDelegated Proxyは、ユーザーの代わりに処理を実行してくれる代理人(Proxy)だと思ってください。
なお、このDelegateProxyは各ウォレットごとに1つでいいので、この登録作業は最初の1回のみとなります。
1.2. Delegeted ProxyがAliceのMettaniトークンを動かすことを承諾(Approve)する
次に、Mettaniトークンのコントラクトに対して、setApprovalForAll()という関数を実行します。これは、任意のアカウントに対して、所有する全てのトークンのTransferを承認するというERC721の関数です。今回のケースでは、Aliceが所有するすべてのMettaniトークンのTransfer権限を、DelegetedProxyに付与しています。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F899cd290-5fbd-407b-ac82-77158ec3b31f%2FScreen_Shot_2021-09-20_at_22.08_3.png?table=block&id=9039f6f8-af1a-44f6-8364-c306160a42bc&cache=v2)
以下のリンクからTransactionを確認できます。
なお、Approveは各トークン(コントラクト)ごとに1回ずつ行う必要があります。例えば、自分が所持しているMettaniとCrypto Punkを売りたい場合、2回Approveが必要になります。しかし、Mettaniトークンを100個所持していてそれを全部売りたい場合は1回Approveで済みます(同一コントラクトのため)。
1.3. OpenSeaにSell Orderを出す
これで準備は整いました。最後にSell Orderを出しますが、上記2つとは少し異なる点があります。それは、AliceからEthereumネットワーク上のいかなるコントラクトに対してもTransactionを送っていないということです。以下の画像を見てください。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fef82de7a-f919-4b93-9fee-e035827af3a0%2FScreen_Shot_2021-09-20_at_22.09_4.png?table=block&id=8c435bca-9827-4ce4-9b1b-04cddf0772e2&cache=v2)
右のMetamaskのWindowを見ると、Signature Requestと書いてあります。これは、特定の処理(Transaction)に対して署名をするという意味です。では、Aliceはどこにこの署名付きのTransactionを送信しているのでしょうか。
最初の全体図を見てもらうと分かるように、OpenSeaに送っています。つまり、オフチェーンの処理が発生しているということです。(ブラウザの開発者ツールからも確認できます)
今回のケースでは、以下のデータを送っています。
いろんな情報が載っていますが、その中にAliceの情報や署名情報らしきものが含まれていることがわかりますね。(
r
, s
, v
, maker
, etc){ "exchange": "0x5206e78b21ce315ce284fb24cf05e0585a93b1d9", "maker": "0xa11ce12df3f71319b3f7430a1a9c1205a5f12fbd", "taker": "0x0000000000000000000000000000000000000000", "makerRelayerFee": "250", "takerRelayerFee": "0", "makerProtocolFee": "0", "takerProtocolFee": "0", "makerReferrerFee": "0", "feeMethod": 1, "feeRecipient": "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073", "side": 1, "saleKind": 0, "target": "0xb5cb37b39ee423586eccdce4fd28d306486a5d31", "howToCall": 0, "calldata": "0x23b872dd000000000000000000000000a11ce12df3f71319b3f7430a1a9c1205a5f12fbd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "replacementPattern": "0x000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000", "staticTarget": "0x0000000000000000000000000000000000000000", "staticExtradata": "0x", "paymentToken": "0x0000000000000000000000000000000000000000", "quantity": "1", "basePrice": "100000000000000000", "extra": "0", "listingTime": "1632143398", "expirationTime": "0", "salt": "12095206509338352627461631422413009994542071659625893648240554600530228703540", "metadata": { "asset": { "id": "0", "address": "0xb5cb37b39ee423586eccdce4fd28d306486a5d31" }, "schema": "ERC721" }, "v": 27, "r": "0x08f3e59b63b10ba1b10f8d06075d60a57a6a1673902ab9eced319fde76b5c12a", "s": "0x0ac69b19ef821e71f9f8d5f4c717a9fb3e906f307072c1ad1e8139cdcaf1b733", "hash": "0x9c3c773426d4b119b37b0d542890463ea711c013b153a67b026e5b1c2ec5ba6b" }
2. Bobが購入する
2.1. AliceのSell OrderをOpenSeaから取得
では、BobはAliceのSell OrderをOpenSeaからどのように取得するのでしょうか。
これには、2つ方法があります。
- GraphQLから取得(web browser)
- OpenSea APIから取得(opensea-js, etc)
1番と2番で取得できるデータ構造が異なっているのですが、内容はほぼ同じですので差異については割愛します。
以下の画像を見ると、OpenSeaのWebサイトではOpenSeaのGraphQLにクエリを投げて、Order情報等を取得していることが分かります。どのタイミングでどんなクエリを投げているかは、Browserの開発者ツール内、Networkタブで見ることができますので、気になる方は見に行ってください。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F33faa44f-93bc-4c9a-ac67-3af23fb43879%2Fbuy_order.png?table=block&id=0c755a98-63ec-436b-88a3-57a9ff2221f1&cache=v2)
また、この手順で最も大事な点は、取得したデータの中にあるOrder情報(
oldOrder
)にAliceの署名が入っているということです。2.2. atomicMatch関数を呼び出す
BobがWyvern ExchangeコントラクトのatomicMatch関数を呼び出します。
すると、そのatomicMatch関数内で、Aliceの署名が正しいか、orderの形式は合っているか、AliceのDelegatedProxyは存在しているか、Orderは既にCancelされていないかを確認した後、指定額のETHをBobのwalletからAliceのwalletに送り、AliceのDelegatedProxyがTokenをTransferします。
このWyvern Exchangeコントラクトが、Ethereumネットワーク上でOpenSeaと呼ばれているものの正体です。
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F51288e34-c38b-43b3-abe7-eb9be89a05a9%2FScreen_Shot_2021-12-30_at_15.20.25.png?table=block&id=e21f5fd7-49cf-48fe-a035-71c0565d55d1&cache=v2)
(出典: Gas Guzzlers - etherscan, Dec 30 2021)
3. NFTの所有権がAliceからBobに移動する
3.1. ProxyがAliceのMettaniトークンをBobに渡す
順番は前後しましたが、2.2の
atomicMatch()
の処理によって、AliceのMettani TokenはBobに転送されました。これで取引は終了です。(補足) 厳密には、AliceのWalletからBobのWalletに移動したのではなく、トークンのコントラクト内で所有権を移動しています。気になる方はERC721.solを参照してください。
さいごに
OpenSeaでは、NFTを売買する際にスマートコントラクト(Web3.0)だけでなく、自社のデータベース等(Web2.0)を使用して取引を仲介していることが分かりました。スマートコントラクトのみで完結していると思っていた人も多いのではないでしょうか。僕のその内の一人です。
実際に、OpenSeaの公式ドキュメントには以下の記述があります。
On OpenSea, most actions are off-chain, meaning they generate orders that are stored in the our system and can be fulfilled by a matching order from another user.
OpenSeaでは、ほとんどのアクションがオフチェーンとなります。つまり、システムに保存されているオーダーを生成し、他のユーザーからのマッチしたオーダーによって満たすことができるのです。- DeepLで翻訳
実際に、OpenSeaはTokenの転送に関わる処理だけEthereumネットワーク上で行い、OrderMatchingは自社のサーバー上で行っています。これについては以下のように記述されています。
OpenSea is heavily optimized to prevent users from having to spend gas.
OpenSeaは、ユーザーがガソリンを使わなくても済むように、かなり最適化されています。- DeepLで翻訳
もし体力がある方は引用元を見に行ってみてください。