Skip to content

StaticCallAssetProxy #39

@dorothy-zbornak

Description

@dorothy-zbornak

Summary

We introduce a new asset proxy for validating an order bundle during settlement through an arbitrary, read-only callback.

Motivation

One issue when trading non-fungible items (kitties, composables, CDPs) is that many of these assets are stateful. Their value is derived from the state at a given point in time. If this state were to be modified, the value of the asset is also potentially modified.

We wish to implement a method to guarantee for the buyer of an asset that the state remains desirable throughout the entire settlement process.

StaticCallProxyAssetData Fields

struct StaticCallProxyAssetData {
    // Asset proxy ID.
    bytes4 assetProxyId, // bytes4(keccak256("StaticCallAssetProxy(...)")),
    // Address of the call target.
    address callTarget,
    // ABI-encoded call data to pass to the call target,
    // generated by `abi.encodeWithSelector()`. 
    bytes callData,
    // Keccak256 hash of the expected, ABI-encoded result of the call.
    // Will revert if the hash of the actual result does not match.
    bytes32 callResultHash,
}`

Usage

The StaticCallAssetProxy “asset” is intended to be mixed with real assets within a MultiAssetProxy bundle, such as:

// An asset bundle that transfers a crypto kitty and verifies that it is
// ready to breed.
MultiAssetProxyAssetData(
    // Amounts of each "asset". Because StaticCallAssetProxy assets 
    // cannot modify state, we should only call them once.
    [1, 1],
    // The "assets" in this bundle.
    [
        // The crypto ktty ERC721 asset to transfer.
        ERC721AssetProxyAssetData(CRYPTO_KITTIES_CONTRACT, kittyId),
        // Validation callback to verify that `kittyId` is ready to breed.
        StaticCallAssetProxy(
            // `callTarget`
            CRYPTO_KITTIES_CONTRACT,
            // `callData`
            abi.encodeWithSelector(
                // Call the `isReadyToBreed()` function.
                bytes4(keccak256("isReadyToBreed(bytes32)")),
                // Pass `kittyId`.
                kittyId
            ),
            // `callResult`: `isReadyToBreed(kittyId)` should return `true`.
            keccak256(abi.encode(true))
        )
    ]
)

When the MultiAssetProxy executes the transfer of this asset, the StaticCallAssetProxy will perform the following read-only check:

keccak256(assetData.callTarget.staticcall(assetData.callData)) == assetData.callResultHash 

If the above condition is not satisfied, the asset proxy will revert, aborting the entire trade. A maker can even chain multiple StaticCallAssetProxy assets inside the MultiAssetProxy bundle to perform multiple state checks, or they can use a bespoke contract to aggregate all checks (or perform more complex checks) in a single call.

Altogether, this allows the maker to establish some sort of state guarantees on the asset(s) being traded. And, since callbacks can access any on-chain state, we imagine some pretty creative uses could arise.

StaticCallAssetProxy Implementation

A pseudo-code implementation of the asset proxy would look like:

contract StaticCallAssetProxy is
    MixinAuthorizable
{
    /// @dev The main entry point for the asset proxy.
    function transferFrom(
        bytes StaticCallAssetProxyData assetData,
        // These parameters are ignored/have no relevance.
        address from,
        address to,
        uint256 amount
    )
        external
        view
        onlyAuthorized
    {
        (bool success, bytes returnData) = assetData.callTarget.staticcall(
            assetData.callData
        );

        // Abort if the call reverted or tried to modify state.
        require(success, returnData);

        // Abort if the hash of the return data does not match what is expected.
        require(keccak256(returnData) == callData.callResultHash);
    }
}

Example Use Cases

  • Protect a CDP from being liquidated and incurring a 13% liquidation fee by creating a 0x order to sell your CDP at a slight discount IF AND ONLY IF (IFF) the collateral-to-DAI ratio for your CDP is below 155%. Liquidation occurs when collateral-to-DAI falls below 150%. Note that this would rely on MakerDAO's on-chain ETH/DAI price feed or some other oracle solution. Similar to CDP Saver but native to 0x.
  • Purchase a Decentraland Estate IFF it contains specific parcels of LAND.
  • Offer to purchase a parcel of Decentraland LAND IFF neighboring parcel located at [x,y] is NOT owned by a specific address (“bad neighbor” clause).

Challenges

  • It is not very intuitive to treat this feature as an “asset,” as it can have no value. Without proper safeguarding, it's possible to trade a real asset for a worthless StaticCall “asset.”
  • Complex, non-aggregate state checks require deploying a contract.
  • The validity of an order can be much more difficult to anticipate in a general sense, as any state outside the assets being traded can render an order invalid. Issuing an eth_call fill is the only way to generally validate these asset bundles.
  • The maker dictates the state guarantees on the asset, which might not exactly align with the taker's interests. Some kind of Exchange wrapper/extension might be helpful in these cases.
  • The from, to, and amount fields are currently ignored, but could be useful to the callback— for example, to check the KYC status of the taker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions