こんにちは!SuishowでCTOをやっています、立川です。早いもので、Solidityに触れ始めてか6ヶ月経ちました。

現在、SuishowではNFT MarketPlaceの開発を進めています。その際にOpenSeaのソースコードが非常に参考になったので、忘備録も兼ねて整理していこうと思います。

ここにソースコードがあります。

全体図

主要なContractとして、ExchangeCore、ProxyRegistry、Proxy (AuthenticatedProxy)があげられます。

  • ExchangeCore : 出品や購入といった基本機能を担う
  • ProxyRegistry : EOAごとにProxy (AuthenticatedProxy)を作成
  • Proxy (AuthenticatedProxy) : safeTransferFromを実行

では、主要なContractをひとつ一つ見ていきます。

AuthenticatedProxy

対象となるNFT ContractのsafeTransferFromを実行するのが主な役割です。以下のproxy関数で実現します。

EOAはあらかじめsetApprovalForAll()により、AuthenticatedProxyにsafeTransferFromの実行権限を付与しておく必要があります。

function proxy(address dest, HowToCall howToCall, bytes calldata)
        public
        returns (bool result)
    {
        require(msg.sender == user || (!revoked && registry.contracts(msg.sender))); // userはこのAuthenticatedProxyが担当するEOA。msg.senderがuserの時、または、msg.senderがOpenSea MarketPlace Contractの時のみ実行できる

    // abi.encodeWithSignature("safeTransferFrom(...)")
        if (howToCall == HowToCall.Call) {
            result = dest.call(calldata);
        } else if (howToCall == HowToCall.DelegateCall) {
            result = dest.delegatecall(calldata);
        }
        return result;
    }

後で紹介するExchangeCoreからproxy関数が呼び出されます。

ProxyRegistry

mapping(address => OwnableDelegateProxy) public proxies;
mapping(address => bool) public contracts;

各EOAに対応するproxyを保持するproxiesと、proxyの関数を実行する権限を持つContract (特にExchangeCore) のaddressを保持するcontractsという二つのmappingが鍵になります。

各EOAは、初めてOpenSeaで取引する際にregisterProxy()を実行し、そのEOA専用のproxyを作成します。(AuthenticatedProxyの説明でも述べていますが、EOAはNFT ContractのsetApprovalForAllを呼び出して、このproxyがsafeTransferFromを実行できるよう権限を付与します。)

function registerProxy()
        public
        returns (OwnableDelegateProxy proxy)
    {
        require(proxies[msg.sender] == address(0));
        proxy = new OwnableDelegateProxy(msg.sender, delegateProxyImplementation,                                                                                                   abi.encodeWithSignature("initialize(address,address)", msg.sender, address(this)));
        proxies[msg.sender] = proxy;
        return proxy;
    }

ExchangeCore

ExchangeCoreは出品や購入、出品キャンセルなど基本的な機能を提供します。理解する上で欠かせないのが構造体Orderです。Orderは出品やオファーの情報を保持します。

struct Order {
          address exchange; //このExchangeCore Contractのaddress
          address maker; // Orderを作成したEOA
          address taker; // Orderを受け入れたEOA
          uint makerRelayerFee; // feeRecipientに払う手数料 (アフィリエイト等に使われる?)
          uint takerRelayerFee; // feeRecipientに払う手数料 (アフィリエイト等に使われる?)
          uint makerProtocolFee; // OpenSeaに払う手数料?
          uint takerProtocolFee; // OpenSeaに払う手数料?
          address feeRecipient;
          FeeMethod feeMethod; // ProtocolFee or SplitFee
          SaleKindInterface.Side side; // Buy or Sell
          SaleKindInterface.SaleKind saleKind; // FixedPrice or DutchAuction
          address target; // 対象となるNFT Contract Address
          AuthenticatedProxy.HowToCall howToCall; //call or delegateCall
          bytes calldata; // ex) abi.encodeWithSignature("safeTransferFrom(1, 0x...)")
          bytes replacementPattern;
          address staticTarget;
          bytes staticExtradata;
          address paymentToken; // 支払いに使われるERC20
          uint basePrice;
          uint extra;
          uint listingTime;
          uint expirationTime;
          uint salt; // hash値が被るのを防ぐ
      }

このOrderに操作を施すことで、出品やオファー、出品キャンセル等を実現します。具体的な流れを見ていきましょう。

まず、OpenSeaで出品、オファー受け入れ等をしたことがないのであれば、ProxyRegistryのregisterProxy()を実行して、proxyを作成します。

出品

ExchangeCoreのapproveOrder_を実行します。引数の情報から新たなOrderを作成し、approveOrder内でOrderのhash値を求めます。approvedOrders[hash] = trueによりOrderの登録が完了です。最後に、emit OrderApprove*により新たなOrderが作られたことをBroadCastします。

bytes32 hash = hashToSign(order);

/* Assert order has not already been approved. */
require(!approvedOrders[hash]);

/* EFFECTS */
    
/* Mark order as approved. */
approvedOrders[hash] = true
emit OrderApprovedPartOne(...)
emit OrderApprovedPartTwo(...)

上の例からもわかるようにOpenSeaではOrderをhashによって上手に扱っています。Orderの情報そのものはContractのStorageに保存することはなく、emitされたeventからアクセスするようにします。代わりに、Orderが登録されたか、キャンセルされたか、終了したかをContractのStorageに保存します。

mapping(bytes32 => bool) public cancelledOrFinalized;
mapping(bytes32 => bool) public approvedOrders;

Orderそのものの情報をStorageに保存してしまいそうですが、hashをうまく利用することで使用するStorage領域を削減できています。

購入

atomicMatch_(…)を実行します。購入の条件を満たしていれば、ETHのtransferが行われ、続いてproxy(..)が実行されて、safeTransferFromによりNFTの所有が移ります。(実際はERC20トークンでも売買できます。) OrdersMatchedなるeventがemitされてNFTが売られたことがBroadCastされます。

オファー

出品と同様です。作成されるOrderのSaleKindInterface.SideがBuyになります。

Dutch Auction

出品と同様です。作成されるOrderのSaleKindInterface.SaleKindがDutchAuctionになります。

Orderという一つの構造体で出品、購入、オファー、Dutch Auctionを実現できるのは脱帽です。

English Auction

English Auctionはオフチェーンで実現しているようです。

以上、OpenSea Market Contractの主要な機能と流れをざっくりと見てきました。実装上の工夫など気になった点は他の記事で書いていこうと思います。

お付き合いありがとうございました。