0xProject / ZEIPs

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

Property Based Orders #43

Closed dekz closed 4 years ago

dekz commented 5 years ago

Summary

Allow the trading of assets based on their properties, rather than their exact identifier.

Motivation

Creating liquidity for NFT's is a challenge as the AssetProxy requires the identifier in its encoding. Traders currently create orders for specific NFTs, 1 DAI for Axie #123. This ZEIP makes it possible to create general standing orders, such as 1 DAI for an Axie and 100 Dai for 100 Axies. It is also possible to create OR orders such as 1 DAI for an Axie or Etheremon.

Property Based Orders allow the trader to specify the exact properties of the asset they require, such assets may not even exist yet. Having a number of these orders in an order allows a base line price discovery mechanism.

Specification

  1. The maker specifies the properties of the asset they wish to receive (predicate). This is encoded in the order.
  2. The taker specifies the exact asset which must satisfy the makers properties (id).
  3. 0x Exchange verifies the given (maker) predicate is satisfied with the supplied (taker) id.

Implementation

A simple way to achieve the challenge of taker supplied data is by using the matchOrders function in Exchange. This receives 2 orders as input, allowing the taker to supply parameters in the second order.

function matchOrders(
    LibOrder.Order memory leftOrder,
    LibOrder.Order memory rightOrder,
    bytes memory leftSignature,
    bytes memory rightSignature
)
    public
    nonReentrant
    returns (LibFillResults.MatchedFillResults memory matchedFillResults)
{
    // Check if leftOrder is property based order

    // leftOrder.makerAssetData  == WETH
    // leftOrder.takerAssetData  == maker supplied predicate
    // rightOrder.makerAssetData == abi encoded asset data
    // rightOrder.takerAssetData == WETH

    // Decode the maker predicate and args
    (makerPredicateMethodId, 
     makerPredicateContractAddress, 
     makerPredicateMakerArgs) = abi.decode(leftOrder.takerAssetData, (bytes4, bytes32, bytes));

    // Encode the predicate call with the taker supplied asset data
    callData = abi.encodeWithSelector(
        makerPredicateMethodId,
        makerPredicateMakerArgs,
        rightOrder.makerAssetData
    );
    // Static call the provided address
    result = address(makerPredicateContractAddress).staticcall(callData);
    require(result, "INVALID_PREDICATE_RESULT");
    leftOrder.takerAssetData = rightOrder.makerAssetData;
    // Continue on to normal matchOrders settlement    
    // ...
}

// Maker specified contract
function checkPredicate(
    bytes makerPropertyArgs,
    bytes takerSuppliedAssetData,
    uint256 amount
)
    public
    returns bool
{
    // check taker supplied asset data meets the given property arguments
    // Decode asset data
    // Check address
    // Decode maker property args, e.g (address, uint256[])
    // call specific functions on the address querying properties
    // compare queried properties to maker property args
    return true;
}

Challenges

It is not currently possible for a taker to supply additional data using fillOrder and the batch variants. The taker does supply the signature, though adding additional encoding here is not ideal.

The order may be for a number of assets, i.e 100 DAI for 100 Axies, the design should allow the taker to supply an arbitrary number of assets (via the MultiAssetProxy). This will need to be enforced by the maker predicate. As such it will need to handle MultiAssetProxy encoded data.

charlesmarino commented 5 years ago

This ZEIP is needed by NFT open finance. By allowing the maker to supply a predicate, we are making subsets of NFTs fungible(i.e. buy any axie with plant attribute or any 10x10 plot on Maldives) which allows for traditional order books on these specific subsets creating price support due to volume in limit orders. This is a needed step to create NFT markets that can be used in defi protocols such as Maker CDPs, Dharma Loans, etc.

Match Orders makes sense for this use case.

In addition to the following use cases: 1 DAI for an Axie 100 DAI for 100 Axies

We would also be able to sign orders such as: 1000 DAI for any decentraland parcel located inside x(-20,20) and y(10,30) 100 dai for 10 Axies with the gen 0 attribute

dorothy-zbornak commented 5 years ago

Been thinking about this recently from the perspective of V3. I think we could pull this off with a small change to matchOrders() and by using the new Validator signature type (#33).

The idea is to exploit the fact that matchOrders() already copies the mirrored asset data of the leftOrder into the rightOrder before signature validation (we expect it to fail if the orders are not complementary). Thus, if rightOrder uses a Validator signature type, its callback will receive the full order with the asset data supplied by leftOrder. rightOrder can then validate that takerAssetData is an NFT that fits its criterion.

This actually sort of works right now, but because the order hash will be different for every fill, rightOrder's filled state never updates, so a taker could keep filling this order until it expires or is cancelled. So we need to update matchOrders() to use the original order hash when updating the filled state, and the updated order hash (with the mirrored asset data) for everything else.

There is some extra data you would need to encode in the order signature for safety and maybe flexibility, but I don't think it's too bad.

recmo commented 4 years ago

I've been thinking about this in light of #74. What is being proposed here are ways to make it work with minimal changes to the existing exchange, mostly using matchOrders. But the proposals feel like hacks to me. What we are dealing with is not a new order type or a new settlement function, but a generalization of the asset string mechanism.

Also consider that this not only applies to order.takerAssetData (i.e.: I'm willing to buy any yellow cryptokitty) but also to order.makerAssetData (i.e. I'm willing to sell any beginner deck card), or even maker/takerFeeAssetData (10 ZRXs for the first to come up with a good usecase).

We also need to think carefuly about how it changes the meaning of *AssetAmount. Rounding, prices and limits are harder to define. It would be fun to have a predicate for 'USDC or DAI', but if you want to include non-USD pegged stablecoins in the mix you need different prices. But if are taking the generality that far that we might as well replace the entire maker order by a big maker-signed predicate that answers 'is this trade acceptable to you?'. Probably don't want to go there. (Or do we?)

Anyway,

Currently the maker fully specifies the allowed assets for the trade on signing. What we want here is that instead the maker signs a predicate that defines what allowable assets are. The maker part of this is easy, we can add a new asset id that specifies the predicate and use that as the asset string. Where we currently have an asset strings like ERC721(<contract address>, <token id>) we would create a new asset string that encodes Predicate(<contract address>, <extra data>). The contract at specified address would then implement some interface like satisfiesPredicate(<extra data>, <taker asset id>) which the exchange or asset proxy would have to call. The <extra data> is just there so you don't need to deploy a new contract every time, but can instead program in some reusable generality.

So far so good, the problems start with the <taker asset id>. Currently we simply take the asset strings from maker's order and apply those to taker. To implement this correctly we need to allow taker to supply it's own asset strings. We may even want taker to supply additional data to convince the predicate (like 'here's a merkle proof that this cryptokitty is indeed yellow like you want'). The current contracts are not designed for this. The only thing taker provides is evidence that it wants this trade to happen (the signature) not how it should happen. That said, without major changes to the protocol, it is the only place where taker can say anything at all, and none of this information except amounts and taker address is passed into the asset proxy.

So it seems like this can not be solved without compromise, workaround or major change. :/

One useful compromise might be to only support cases where the the asset proxy knows by itself what the correct asset id is. Either because taker told the proxy in advance, or because it can be computed during the fill. I'd need to think further about what this allows.

moodlezoup commented 4 years ago

Superseded by #75