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.
Above shows the brief architecture of a NFT marketplace and here are steps to process a transaction:
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.
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).
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.
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.
There are four steps inside Wyvern to match and execute orders.
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:
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.