0xProject / ZEIPs

0x Improvement Proposals
Apache License 2.0
91 stars 24 forks source link

Property-based Orders Redux #75

Closed moodlezoup closed 4 years ago

moodlezoup commented 4 years ago

Summary

NFTs have become an important asset for the 0x ecosystem, accounting for a large number of trades in the last several months. Property-based orders can be defined as orders which can be filled by any one of a subset of assets, where the subset is specified by the maker.

Property-based orders cannot be filled directly via the Exchange contract because the taker asset is not fully determined until the fill, so the taker needs some mechanism to pass in data specifying the asset they’d like to fill the order with. The solution, then, requires a Forwarder-like contract serving as the entry point to the Exchange, and some mechanism to check that the taker’s asset(s) satisfy the properties specified by the maker. In the following section we explore an alternative implementation to those proposed in ZEIPs #74 and #43 for property-based orders.

Motivation/Rationale

For the motivation for property-based orders in general, refer to #74 and #43. The motivation for this particular implementation is to achieve the flexibility of #43, but to do so permissionlessly (i.e. without needing to change the core Exchange contracts) and supporting Gods Unchained cards as the first use case. The proposed architecture is also easily extensible, and could be adapted to support #51 at some point in the future.

Specification

We introduce the Broker contract, which serves as the entry-point to the 0x Exchange. The taker will need to allow the Broker to transfer assets on their behalf. In order to create a property-based order, the maker will set the takerAssetData to be ERC1155 asset data encoding the following (we will use Gods Unchained cards in the examples hereafter):

// The Broker serves as the token
address erc1155TokenAddress = address(Broker);
// Can leave ids empty, could potentially use it for other use cases
uint256[] memory ids = [];
// If the maker wishes to buy multiple NFTs satisfying the same property, 
// they can scale the makerAssetAmount, while keeping this as [1]. 
uint256[] memory values = [1];
// We leverage the extra data field in 1155 asset data to specify the validator
// contract and the desired card properties.
bytes memory data = abi.encode(validatorAddress, propertyData);

All other fields of the property-based order remain the same as their vanilla counterpart.

To fill the order, the taker does the following (depicted in the schematic below):

  1. The taker enters the Broker contract by specifying the order(s) they’d like to fill and the assetData corresponding to the GU card(s) they’d like to fill it with.
  2. The Broker caches the assetData specified by the taker in storage, and calls the Exchange.
  3. For the taker → maker transfer, the Exchange calls the ERC1155 asset proxy.
  4. The asset proxy calls safeBatchTransferFrom on the Broker contract.
  5. In safeBatchTransferFrom, the Broker staticcalls the property validator contract encoded in the extra data field, passing in the cached takerAssetData and the maker-specified properties.
  6. The property validator reverts if the taker’s card doesn’t have the required properties. Otherwise, the Broker will transfer the card from taker to maker by calling the GU contract. Finally, the Broker clears its own storage before exiting.

image

The Broker contract will have the following interface:

/// @dev Fills a single property-based order by the given amount using the given assets.
///      Pays protocol fees using either the ETH supplied by the taker to the transaction or
///      WETH acquired from the maker during settlement. The final WETH balance is sent to the taker.
/// @param brokeredTokenIds Token IDs specified by the taker to be used to fill the orders.
/// @param order The property-based order to fill. The format of a property-based order is the
///        same as that of a normal order, except the takerAssetData. Instaed of specifying a
///        specific ERC721 asset, the takerAssetData should be ERC1155 assetData where the
///        underlying tokenAddress is this contract's address and the desired properties are
///        encoded in the extra data field. Also note that takerFees must be denominated in
///        WETH (or zero).
/// @param takerAssetFillAmount The amount to fill the order by.
/// @param signature The maker's signature of the given order.
/// @param fillFunctionSelector The selector for either `fillOrder` or `fillOrKillOrder`.
/// @param ethFeeAmounts Amounts of ETH, denominated in Wei, that are paid to corresponding feeRecipients.
/// @param feeRecipients Addresses that will receive ETH when orders are filled.
/// @return fillResults Amounts filled and fees paid by the maker and taker.
function brokerTrade(
    uint256[] calldata brokeredTokenIds,
    LibOrder.Order calldata order,
    uint256 takerAssetFillAmount,
    bytes calldata signature,
    bytes4 fillFunctionSelector,
    uint256[] calldata ethFeeAmounts,
    address payable[] calldata feeRecipients
)
    external
    payable
    returns (LibFillResults.FillResults memory fillResults);

/// @dev Fills multiple property-based orders by the given amounts using the given assets.
///      Pays protocol fees using either the ETH supplied by the taker to the transaction or
///      WETH acquired from the maker during settlement. The final WETH balance is sent to the taker.
/// @param brokeredTokenIds Token IDs specified by the taker to be used to fill the orders.
/// @param orders The property-based orders to fill. The format of a property-based order is the
///        same as that of a normal order, except the takerAssetData. Instaed of specifying a
///        specific ERC721 asset, the takerAssetData should be ERC1155 assetData where the
///        underlying tokenAddress is this contract's address and the desired properties are
///        encoded in the extra data field. Also note that takerFees must be denominated in
///        WETH (or zero).
/// @param takerAssetFillAmounts The amounts to fill the orders by.
/// @param signatures The makers' signatures for the given orders.
/// @param batchFillFunctionSelector The selector for either `batchFillOrders`,
///        `batchFillOrKillOrders`, or `batchFillOrdersNoThrow`.
/// @param ethFeeAmounts Amounts of ETH, denominated in Wei, that are paid to corresponding feeRecipients.
/// @param feeRecipients Addresses that will receive ETH when orders are filled.
/// @return fillResults Amounts filled and fees paid by the makers and taker.
function batchBrokerTrade(
    uint256[] calldata brokeredTokenIds,
    LibOrder.Order[] calldata orders,
    uint256[] calldata takerAssetFillAmounts,
    bytes[] calldata signatures,
    bytes4 batchFillFunctionSelector,
    uint256[] calldata ethFeeAmounts,
    address payable[] calldata feeRecipients
)
    external
    payable
    returns (LibFillResults.FillResults[] memory fillResults);

/// @dev The Broker implements the ERC1155 transfer function to be compatible with the ERC1155 asset proxy
/// @param from Since the Broker serves as the taker of the order, this should equal `address(this)`
/// @param to This should be the maker of the order.
/// @param amounts Should be an array of just one `uint256`, specifying the amount of the brokered assets to transfer.
/// @param data Encodes the validator contract address and any auxiliary data it needs for property validation.
function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] calldata /* ids */,
    uint256[] calldata amounts,
    bytes calldata data
)
    external;

As a minimum viable product, the Gods Unchained property validator contract can be as simple as the following:

contract GodsUnchainedValidator is
    IPropertyValidator
{
    IGodsUnchained internal GODS_UNCHAINED; // solhint-disable-line var-name-mixedcase

    using LibBytes for bytes;

    constructor(address _godsUnchained)
        public
    {
        GODS_UNCHAINED = IGodsUnchained(_godsUnchained);
    }

    /// @dev Checks that the given card (encoded as assetData) has the proto and quality encoded in `propertyData`.
    ///      Reverts if the card doesn't match the specified proto and quality.
    /// @param tokenId The ERC721 tokenId of the card to check.
    /// @param propertyData Encoded proto and quality that the card is expected to have.
    function checkBrokerAsset(
        uint256 tokenId,
        bytes calldata propertyData
    )
        external
        view
    {
        (uint16 expectedProto, uint8 expectedQuality) = abi.decode(
            propertyData,
            (uint16, uint8)
        );

        // Validate card properties.
        (uint16 proto, uint8 quality) = GODS_UNCHAINED.getDetails(tokenId);
        require(proto == expectedProto, "GodsUnchainedValidator/PROTO_MISMATCH");
        require(quality == expectedQuality, "GodsUnchainedValidator/QUALITY_MISMATCH");
    }
}
moodlezoup commented 4 years ago

Specification added at https://github.com/0xProject/0x-protocol-specification/blob/master/order-types/property-based.md