2021.07.05
Openseaのコントラクトソースコードを調査してみる
こんにちは。次世代システム研究室のL.W.です。
NFT(Non Fungible Token)は、非均質化されたトークンであります(非代替性トークンも呼ばれます)。均質化(Fungible Token)された形の暗号資産(仮想通貨)と同じく、ブロックチェーン上で発行および取引されます。
ブロックチェーン技術を活用することで、唯一無二な資産的価値を付与し、新たな売買市場を生み出す技術として注目を浴びています。中にはとてつもない価格が付くデジタルアートの作品も次々と登場していました。
- 1. 全体図
- 2. WyvernExchange
- 3. Registry
- 4. tokenTransferProxy
- 5. exchangeToken
- 6. protocolFeeRecipient
- 7. OPENSTORE
- 8. まとめ
- 9. 最後に
1. 全体図
OpenseaはMetamaskと連携していますので、MetamaskでのEOAで各コントラクトとやり取りができます。
EOAからエクスチェンジ(EX)の「ordersCanMatch_」方法の呼び出しでNFTの購入(Buy Now)、Bundleの購入(Buy Bundle)、オファーの受け入れ(Accept)、オークションのビッドの受け入れを実現できます。
EOAからエクスチェンジ(EX)の「cancelOrder_」方法の呼び出しで、出品の取下げ(Cancle Listing)を実現できます。
WETHを始めのERC20も取り扱われているので、OpenseaにERC20の利用を許可するために、各ERC20コントラクトの「approve」は呼び出されます。
ERC721とERC1155のNFTをOpenseaに預けることなく、販売するためには、各NFTコントラクトの「setApprovalForAll」は呼び出されます。
各コントラクトのコンポーネントについて、以下で紹介します。
2. EX:WyvernExchange
OpenseaのNFTのエクスチェンジのコアなコントラクトです。(Rinkebyテストネットでのソースコードはメインネットでのと一緒です。)
ETH以外にどのようなトークンは取り扱われているか、NFTの取引のルール、アクセスコントロールなどのロジックはここで定義されています。
出品者は出品する場合、sell orderを作成します。購入者はMake OfferまたはPlace Bidする場合、buy orderを作成します。
理解しないといけないデータ構造はOrderだと認識しています。
/* An order on the exchange. */ struct Order { /* Exchange address, intended as a versioning mechanism. */ address exchange; /* Order maker address. */ address maker; /* Order taker address, if specified. */ address taker; /* Maker relayer fee of the order, unused for taker order. */ uint makerRelayerFee; /* Taker relayer fee of the order, or maximum taker fee for a taker order. */ uint takerRelayerFee; /* Maker protocol fee of the order, unused for taker order. */ uint makerProtocolFee; /* Taker protocol fee of the order, or maximum taker fee for a taker order. */ uint takerProtocolFee; /* Order fee recipient or zero address for taker order. */ address feeRecipient; /* Fee method (protocol token or split fee). */ FeeMethod feeMethod; /* Side (buy/sell). */ SaleKindInterface.Side side; /* Kind of sale. */ SaleKindInterface.SaleKind saleKind; /* Target. */ address target; /* HowToCall. */ AuthenticatedProxy.HowToCall howToCall; /* Calldata. */ bytes calldata; /* Calldata replacement pattern, or an empty byte array for no replacement. */ bytes replacementPattern; /* Static call target, zero-address for no static call. */ address staticTarget; /* Static call extra data. */ bytes staticExtradata; /* Token used to pay for the order, or the zero-address as a sentinel value for Ether. */ address paymentToken; /* Base price of the order (in paymentTokens). */ uint basePrice; /* Auction extra parameter - minimum bid increment for English auctions, starting/ending price difference. */ uint extra; /* Listing timestamp. */ uint listingTime; /* Expiration timestamp - 0 for no expiry. */ uint expirationTime; /* Order salt, used to prevent duplicate hashes. */ uint salt; }
2.1. Buy Now
出品する場合は、sell orderが作成されて、出品者のEOAでsell orderの内容に署名を行います。この署名で、出品者はsell orderでのNFT(コントラクト、トークンID)、販売価格、販売方式、販売期間などの情報を同意することと等しいです。
出品者の署名後、Sell Order情報と署名情報の一つ例は、以下の通りです。
. basePrice: "1000000000000000" //販売価格(ETH) . calldata: "0x23b872dd000000000000000000000000e6b48d76bc4805abf61f38a55f1d7c362c8bfda800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" . exchange: "0x5206e78b21ce315ce284fb24cf05e0585a93b1d9" . expirationTime: "0" . extra: "0" . feeMethod: 1 . feeRecipient: "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" . hash: "0xb4fc308f42aa95eb42e5f54fd133c73554517908938708bfa10b5b190849e900" . howToCall: 0 . listingTime: "1625391765" . maker: "0xe6b48d76bc4805abf61f38a55f1d7c362c8bfda8" . makerProtocolFee: "0" . makerReferrerFee: "0" . makerRelayerFee: "250" . metadata: {asset: {id: "1", address: "0x6a55df8080c8cef5ae2d941356792fa925fdefea"}, schema: "ERC721"} . paymentToken: "0x0000000000000000000000000000000000000000" . quantity: "1" . r: "0x3533205369ab9c22035830bd94ee2fc62474bb6fa343d458a8adfb45fc27ee21" . replacementPattern: "0x000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000" . s: "0x5e4923851c46b5d720ca4098ce7ed62d977db6458f66c4c91a8f65fe4db7e2d0" . saleKind: 0 . salt: "39105810472022714477231934228712424453123455914673974930876616049220543647209" . side: 1 . staticExtradata: "0x" . staticTarget: "0x0000000000000000000000000000000000000000" . taker: "0x0000000000000000000000000000000000000000" . takerProtocolFee: "0" . takerRelayerFee: "0" . target: "0x6a55df8080c8cef5ae2d941356792fa925fdefea" . v: 27
購入者はOpenseaの画面で「Buy Now」クリックすると、Metamaskがポップアップして、決済の手続きに入ります。成功するトランザクションの一例はこれです。このトランザクションからすると、「atomicMatch_」方法が呼び出されました。購入者はガス負担となります。
atomicMatch_方法を複雑に見えるが、模擬コードで簡単にまとめて記述すると、以下の通りです。
function atomicMatch(Order memory buy, Sig memory buySig, Order memory sell, Sig memory sellSig, bytes32 metadata) { check buy side parameter; check sell order is valid;(expirationTime, signature) check sell order is not cancelled or finalized; transfer sell price(ETH or ERC20) to seller; transfer fee to Opensea; transfer NFT from seller to buyer; emit event; }
ここで強調したいのは、NFTの移転です。
proxy.proxy(sell.target, sell.howToCall, sell.calldata); // sell.targetはNFTコントラクトとなる。 // sell.howToCallはcallとなる // sell.calldataはsafeTransferFrom(address,address,uint256)のdataとなる。
2.2. Make Offer
購入者は直接に気にいる作品をオファーを出せます。購入者のEOAでbuy orderの内容に署名を行います。この署名で、購入者はbuy orderでのNFT(コントラクト、トークンID)、オファー価格、オファー有効期間などの情報を同意することと等しいです。
オファーを出すには、ERC20(WETHなど)が必要で、OpenseaではEOAのERC20の残高をチェックを行います。先にERC20コントラクトのapproveを呼び出して、許可先はtokenTransferProxyコントラクトのアドレスとなります。これでいたずらを防げるかと思っています。
購入者はオファーでbuy orderを作成し、署名を行います。Buy Order情報と署名情報の一つ例は、以下の通りです。
. basePrice: "1230000000000000" // オファーの価格(WETH) . calldata: "0x23b872dd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046352bf252f8e150f87a54cc09372b89e538c2bd0000000000000000000000000000000000000000000000000000000000000001" . exchange: "0x5206e78b21ce315ce284fb24cf05e0585a93b1d9" . expirationTime: "1626000859" . extra: "0" . feeMethod: 1 . feeRecipient: "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" . hash: "0x257ad0ff4bb057c54e2300b4cb6058f93c7511ec36f3a9fe3a0fa33897a44ec9" . howToCall: 0 . listingTime: "1625395990" . maker: "0x46352bf252f8e150f87a54cc09372b89e538c2bd" . makerProtocolFee: "0" . makerReferrerFee: "0" . makerRelayerFee: "0" . metadata: {asset: {id: "1", address: "0x6a55df8080c8cef5ae2d941356792fa925fdefea"}, schema: "ERC721"} . paymentToken: "0xc778417e063141139fce010982780140aa0cd5ab" . quantity: "1" . r: "0x5bce1c2c1a7ed2257de19ec74196695902bd56d97c8b7570afef55dfe7c6e83b" . replacementPattern: "0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" . s: "0x78a271bb0056c5fa504dce456d2ee113ce0a7549fe9b636c4975513b517fc4e9" . saleKind: 0 . salt: "56589070341976203264620168480772649861226097129076943384856741423280029670357" . side: 0 . staticExtradata: "0x" . staticTarget: "0x0000000000000000000000000000000000000000" . taker: "0x0000000000000000000000000000000000000000" . takerProtocolFee: "0" . takerRelayerFee: "250" . target: "0x6a55df8080c8cef5ae2d941356792fa925fdefea" . v: 28
出品者はこのオファーを受け入れる(accept)と、Metamaskがポップアップして、決済の手続きに入ります。成功するトランザクションの一例はこれです。このトランザクションからすると、「atomicMatch_」方法が呼び出されました。そして、今回はガスの負担は出品者となります。
2.3. Auction + Place Bid
出品者はオークション形式でNFT作品を販売できます。Minimum Bid、Reserve price、Expiration Dateなどの情報で、sell orderを作れます。このsell orderに署名することで、指定の内容でオークション販売することを許可します。
Sell Order情報と署名情報の一つ例は、以下の通りです。
. basePrice: "3000000000000000" . calldata: "0x23b872dd000000000000000000000000e6b48d76bc4805abf61f38a55f1d7c362c8bfda800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003" . englishAuctionReservePrice: "1000000000000000000" . exchange: "0x5206e78b21ce315ce284fb24cf05e0585a93b1d9" . expirationTime: "1626433752" . extra: "0" . feeMethod: 1 . feeRecipient: "0x0000000000000000000000000000000000000000" . hash: "0x50081676ee3ab2fc28aca11124ab67505d041b3d34358cafd19a10147ebba876" . howToCall: 0 . listingTime: "1625828952" . maker: "0xe6b48d76bc4805abf61f38a55f1d7c362c8bfda8" . makerProtocolFee: "0" . makerReferrerFee: "0" . makerRelayerFee: "0" . metadata: {asset: {id: "3", address: "0x6a55df8080c8cef5ae2d941356792fa925fdefea"}, schema: "ERC721"} . paymentToken: "0xc778417e063141139fce010982780140aa0cd5ab" . quantity: "1" . r: "0xb4f73a86448d60bb2a0f353d572da396518675755dbee26e59780455fa2b56f5" . replacementPattern: "0x000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000" . s: "0x27d1b5f3bf75a1dcb5d077a5c5b1526e1c98457f1852d9e51edae2baa3fd926e" . saleKind: 0 . salt: "25810885847732897704358855487251854621543428487682073494449509339899062516907" . side: 1 . staticExtradata: "0x589ad31c" . staticTarget: "0xe291abab95677bc652a44f973a8e06d48464e11c" . taker: "0x0000000000000000000000000000000000000000" . takerProtocolFee: "0" . takerRelayerFee: "250" . target: "0x6a55df8080c8cef5ae2d941356792fa925fdefea" . v: 27
購入者はこのオークションにビッドすることができます。「Place Bid」でbuy orderを作成できます。オークションにの参加はMaker Offerと同様、ERC20(WETHなど)が必要です。buy orderを署名することで、オークションに参加することを同意します。
Buy Order情報と署名情報の一つ例は、以下の通りです。
. basePrice: "3000000000000000" . calldata: "0x23b872dd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046352bf252f8e150f87a54cc09372b89e538c2bd0000000000000000000000000000000000000000000000000000000000000003" . exchange: "0x5206e78b21ce315ce284fb24cf05e0585a93b1d9" . expirationTime: "1626401352" . extra: "0" . feeMethod: 1 . feeRecipient: "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" . hash: "0xcf8adf860703c1f9836f004893d35b3e6e6124bad1914067077e3bd95e986c88" . howToCall: 0 . listingTime: "1625397882" . maker: "0x46352bf252f8e150f87a54cc09372b89e538c2bd" . makerProtocolFee: "0" . makerReferrerFee: "0" . makerRelayerFee: "0" . metadata: {asset: {id: "3", address: "0x6a55df8080c8cef5ae2d941356792fa925fdefea"}, schema: "ERC721"} . paymentToken: "0xc778417e063141139fce010982780140aa0cd5ab" . quantity: "1" . r: "0x9faaec5a0ccb28c78653b77e0cf137d153b398fe76be204c910be175c57c77d7" . replacementPattern: "0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" . s: "0x6a1751468173e1a57eb619ee9a328f1d23b00a39a10305ab1d0744b4f6f74c48" . saleKind: 0 . salt: "43226028328122570551964935108260340564536238109284034242939650470844365668270" . side: 0 . staticExtradata: "0x" . staticTarget: "0x0000000000000000000000000000000000000000" . taker: "0xe6b48d76bc4805abf61f38a55f1d7c362c8bfda8" . takerProtocolFee: "0" . takerRelayerFee: "250" . target: "0x6a55df8080c8cef5ae2d941356792fa925fdefea" . v: 28
ビッドがありましたので、出品者はビッドリストの中、どちらか選択して、「accept」クリックすると、Metamaskがポップアップして、決済の手続きに入ります。成功するトランザクションの一例はこれです。このトランザクションからすると、「atomicMatch_」方法が呼び出されました。今回もガスの負担は出品者となります。
実際には、Openseaは二つの販売形式がサポートしています。FixedPrice, DutchAuctionとなります。上の例ではEnglish Auctionに見えますが、FixedPriceの処理のロジックと一緒です。buyPrice >= sellPrice成立できれば、処理を続けることです。
function calculateMatchPrice(Order memory buy, Order memory sell) view internal returns (uint) { /* Calculate sell price. */ uint sellPrice = SaleKindInterface.calculateFinalPrice(sell.side, sell.saleKind, sell.basePrice, sell.extra, sell.listingTime, sell.expirationTime); /* Calculate buy price. */ uint buyPrice = SaleKindInterface.calculateFinalPrice(buy.side, buy.saleKind, buy.basePrice, buy.extra, buy.listingTime, buy.expirationTime); /* Require price cross. */ require(buyPrice >= sellPrice); /* Maker/taker priority. */ return sell.feeRecipient != address(0) ? sellPrice : buyPrice; }
2.4. Buy Bundle
ロジックは殆どBuy Nowのと同じですが、異なる部分だけを説明します。
NFTの移転は異なりますね。
proxy.proxy(sell.target, sell.howToCall, sell.calldata); // sell.targetはWyvernAtomicizerコントラクトとなる。 // sell.howToCallはWyvernAtomicizerとなる // sell.calldataはatomicize (address[] addrs, uint[] values, uint[] calldataLengths, bytes calldatas)のdataとなる。
WyvernAtomicizerについて、ここで参照できます。
2.5. cancelOrder_
buy orderとsell orderはatomicMatch_処理される前には、キャンセルすることが可能です。
このキャンセルのステータスはスマートコントラクトで保持されます。
function cancelOrder(Order memory order, Sig memory sig) internal { /* CHECKS */ /* Calculate order hash. */ bytes32 hash = requireValidOrder(order, sig); /* Assert sender is authorized to cancel order. */ require(msg.sender == order.maker); /* EFFECTS */ /* Mark order as cancelled, preventing it from being matched. */ cancelledOrFinalized[hash] = true; /* Log cancel event. */ emit OrderCancelled(hash); }
2.6. approveOrder_
コントラクトアドレスは署名することできないでしょう。コントラクトアドレス配下のNFTトークンを販売するためには、approveOrder_を呼び出せます。
このapproveのステータスもスマートコントラクトで保持されます。
function approveOrder(Order memory order, bool orderbookInclusionDesired) internal { /* CHECKS */ /* Assert sender is authorized to approve order. */ require(msg.sender == order.maker); /* Calculate order hash. */ bytes32 hash = hashToSign(order); /* Assert order has not already been approved. */ require(!approvedOrders[hash]); /* EFFECTS */ /* Mark order as approved. */ approvedOrders[hash] = true; ... ... }
3. Registry
EOA毎にプロキシが生成されます。各プロキシはmapping(address => OwnableDelegateProxy) public proxiesに保存されます。
ERC721とERC1155のNFTをOpenseaに預けることなく、販売するためには、各NFTコントラクトの「setApprovalForAll」は呼び出されます。許可先は各EOAのプロキシとなります。
proxiesでのOwnableDelegateProxyの呼び出せる権限の持つコントラクトはmapping(address => bool) public contractsに保持されます。
このトランザクションからすると、EXのアドレスは既に保持されました。つまり、EXコントラクトからproxiesでのプロキシを呼び出せる権限が持ちます。
function atomicMatch(Order memory buy, Sig memory buySig, Order memory sell, Sig memory sellSig, bytes32 metadata) internal reentrancyGuard { ... ... /* Retrieve delegateProxy contract. */ OwnableDelegateProxy delegateProxy = registry.proxies(sell.maker); /* Proxy must exist. */ require(delegateProxy != address(0)); /* Assert implementation. */ require(delegateProxy.implementation() == registry.delegateProxyImplementation()); /* Access the passthrough AuthenticatedProxy. */ AuthenticatedProxy proxy = AuthenticatedProxy(delegateProxy); ... ... /* Execute specified call through proxy. */ require(proxy.proxy(sell.target, sell.howToCall, sell.calldata)); ... ... }
4. tokenTransferProxy
ERC20トークンでの決済する場合、Openseaプラットフォームへ手数料の精算、販売者への販売料金の精算を行なってから、EXで保持している「tokenTransferProxy」というコントラクトよりトークンのトランスファーを行います。
function transferTokens(address token, address from, address to, uint amount) internal { if (amount > 0) { require(tokenTransferProxy.transferFrom(token, from, to, amount)); } }
function transferFrom(address token, address from, address to, uint amount) public returns (bool) { require(registry.contracts(msg.sender)); return ERC20(token).transferFrom(from, to, amount); }
将来にはプロトコル手数料の使途はDAOに任せるか不明です。
まだ利用されていないです。
7. OPENSTORE
function safeTransferFrom( address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data ) public { uint256 mintedBalance = super.balanceOf(_from, _id); if (mintedBalance < _amount) { // Only mint what _from doesn't already have mint(_to, _id, _amount.sub(mintedBalance), _data); if (mintedBalance > 0) { super.safeTransferFrom(_from, _to, _id, mintedBalance, _data); } } else { super.safeTransferFrom(_from, _to, _id, _amount, _data); } }
function _isOwner(address _address) internal view returns (bool) { return owner() == _address || _isProxyForUser(owner(), _address); } function _isProxyForUser(address _user, address _address) internal view returns (bool) { return _proxy(_user) == _address; } function _proxy(address _address) internal view returns (address) { ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); return address(proxyRegistry.proxies(_address)); }
8. まとめ
ただの2000行ぐらいのSolidityソースコードで、分散型のNFTの取引所の機能を殆ど揃えるなんて、素晴らしいでしょう。
今回はOpenseaのコントラクトを軽く調査していたが、まだ不明なところがあります。Openseaのコントラクトの理解に一助になれば幸いです。これから時間が取れば、もっと深掘りしたいです。また共有いたします。
Openseaは完全な分散型のNFTマーケットプレイスなので、偽物の作品は混在されていることは避けられないかと思います。
この課題を改善するためには、非分散型、ハイブリッド型のNFTマーケットプレイスもどんどん出ています。
我が社の熊谷社長は「NFTはデジタルコンテンツの流通革命」と認識されていて、我が社もマーケットプレイス「アダム by GMO」を近いうちに世に出せます。楽しみにしてくださいね。
9. 最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD