Project Wyvern: Everything You Should Know Behind OpenSea
May 9th, 2022

Introduction

Wyvern is the name of smart contracts that allows OpenSea and perhaps other Non-fungible-Token marketplaces to trade NFTs in Ethers or ERC20 tokens. The lifecycle of NFT trading is closely associated with orders: they are created, stored and paired within the web server while got verified and executed on the blockchain.

Figure 1. NFT-Marketplace Brief Structure
Figure 1. NFT-Marketplace Brief Structure

Above shows the brief architecture of a NFT marketplace and here are steps to process a transaction:

  1. Create and sign orders in website.
  2. Pair orders and transact to contract
  3. Verify and execute orders

Order Structure

Order is defined as struct in Solidity, it contains everything that smart contracts need to know to execute the trading. For example, maker is address that made the order, to execute an order, this address is required to be recovered using ecrecover in smart contracts (so the off-chain created order becomes tamper-resistant). Fees including relayerFee and protocolFee describe how royalty and fees are calculated. Sale side marks whether this order from a buyer or seller, saleKind shows whether the order is a fixed-price type or auction type (their execution logic might be different). Target, howToCall, calldata and replacementPattern record how to make an external call when the payment is done, while paymentToken and basePrice refer to the token and the price that a buyer or a seller is going to offer or accept. Other values provide additional checks, details can be viewed at github.

Figure 2. Order struct
Figure 2. Order struct

Create and Sign Orders in Web Application

Order is created in front-end as a JavaScript object, to sign it, we need to encode its parameters into a byte array under certain rules defined in smart contract and use wallet to sign its keccak256 hash value by secp256k1.

EIP712 proposes a way to hash and sign typed data to offer a clear view on what data to sign in wallets rather than an unrecognizable hash value. The overall encoding pattern is \x19\x01<DOMAIN_SEPARATOR><ORDER_HASH>

where DOMAIN_SEPARATOR is the keccak256 hash value of abi.encoding of contractName, contractVersion, chainId and the contract address with a prefix TYPE_HASH (indicating above value type and name).

Figure 3. Domain separator
Figure 3. Domain separator

ORDER_HASH is the keccak256 hash value of the abi.encoding of all its parameters with a prefix TYPE_HASH. (in Solidity abi.encoding adds ZEROs to 32 bytes for value type and hashes to 32 bytes for reference type).

After orders and their signatures are generated, they are stored in database instead of writing to smart contracts. The 65 bytes signature can 1) make sure the order is generated by its encoded order maker 2) guarantee the order not to be overwritten before it is executed in the blockchain. It helps cut significant gas costs for users.

In addition, cancelling of an order requires users to transact to the blockchain to disable the order status. All orders are by default enabled, simply taking them down from the website cannot guarantee these orders won’t be occasionally executed since all orders are technically public, they can be retrieved, stored and sent to the smart contract without any permissions.

Verify and Execute Orders in Smart Contract

Wyvern contact takes five parameters to match and execute orders. Orders and signatures are necessary to process the trading, metadata will be emitted in the event to notify the web application.

Figure 4. Transact to contract
Figure 4. Transact to contract

There are four steps inside Wyvern to match and execute orders.

  1. Check signatures (by ecrecover)
  2. Check whether orders can match
  3. Transfer funds from buyer to seller (with royalties and fees)
  4. Transfer NFTs from seller to buyer

You might notice in OpenSea and perhaps other platforms, it seems a bit weird that orders can be settled to execute a transfer of an existing token of any given contract address even they apply different standards (either ERC721 or ERC1155), or even a transfer of an un-minted token in their public sale. Indeed, Wyvern exchange supports arbitrary transaction: when the buyer pays the seller some payment tokens, Wyvern contracts will make an external call to target address through seller’s registered proxy contract to settle the transaction. The behavior of this external call is defined in order parameters and encoded in web application outside the contact.

For example, if a seller want to sell his NFT, part of his order that describes the external call should like this:

Figure 5. How to describe an external call
Figure 5. How to describe an external call
  • target indicates nft token contract address that Wyvern should calls
  • howToCall is the type of the external call (there are three types of calls in Solidity: call, static call and delegate call)
  • calldata is the byte array that Wyvern contract uses while calling the target (in the example. this calldata should be the abi.encoding of transferFrom function. The first four bytes is the function signature, with three parameters each occupies 32 bytes)
  • replacePattern describes which part of byte array (above calldata) could be replaced by another byte array (opposite order’s calldata) and which part should be protected. When seller places a sell order, the buyer address is unpredictable, the byte array in sell order’s calldata that describes buyer address is meaningless and should be replaced by the one in the buy order.
Figure 6. Guarded array replacement
Figure 6. Guarded array replacement

Above shows how sell_order_calldata is replaced by buy_order_calldata with a bitmask (sell_order_replacePattern). The replacement process is calculated conceptually: array[i] = (!mask[i] && array[i]) || (mask[i] && desired[i]). Basically, in each byte if the replacement pattern is 0xff, we replace the original byte with the desired one, if the replacement pattern is 0x00 we keep original byte unchanged. By doing this, we can obtain the integral calldata from paired orders which is exactly the abi.encoding of transferFrom(seller_address, buyer_address, tokenId)

This strategy that uses calldata and replacement pattern derived outside the blockchain enables Wyvern to execute arbitrary transactions (transfer or mint of ERC721 and ERC1155 e.g.), however, it brings a critical issue: Wyvern could not justify the legitimacy of this transaction, in other word, orders could be manipulated to ask Wyvern to transfer a token that does not belong to the seller, that is why we do not approve Wyvern to use our tokens directly but approve an independent proxy contract instead.

Users are required to deploy a proxy contract from registry contract during their first sale in OpenSea (each address owns one proxy) and approve the proxy contract to use their NFTs. When Wyvern is about to make an external call in a transaction, it will forward the external call to the order seller’s proxy, and ask the proxy contract to execute the final transaction (e.g. transfer of a ERC721 token). Even Wyvern could not resolve and verify the legitimacy of the calldata, an invalid transfer request will be reverted as the proxy contract cannot transfer NFTs that the seller does not own.

Figure 7. Proxy and Registry
Figure 7. Proxy and Registry
Subscribe to aolin
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from aolin

Skeleton

Skeleton

Skeleton