Try   HackMD

Recently the CoW Grants program recieved a noteable grant proposal for working on a new version for the milkman project.

milkman received traction with several prominent DAOs, which is indicating demand for such products.
It also uncovered some challenges with the usability of milkman.

  • Requiring a user to specify a price checker is cumbersome. Requiring a user
    to manually encode bytes to be passed to the price checker is even more cumbersome.
  • Price checker development for custom pairs is hindering trading of such pairs.
  • Brownie, the original framework used to write Milkman’s tests, makes it hard
    for developers to write their own price checkers. I picked Brownie over Foundry
    because at the time there was no Solidity HTTP client. But now, there is.
  • Sometimes, as was the case for Aave, users of Milkman will prefer a
    dedicated support engineer. This is especially true when the user requires
    a custom price checker.

Thinking through this problem, below we describe an alternative design for milkman2.0 which could use some existing building blocks and could offer a completely generic default deployment that works for any arbitrary ERC20 token pair with minimal parameters required.

  • The user will be able to deploy a ComposablCoW (CC) compatilble safe and add a price guard on top of a sell order. The default offered price guard will use a falling price dutch auction with with just a few parameters settings.
  • The reference price could be set as a constant or as a price input.
  • The starting price is defined as the reference price + auction premium %
  • The closing price is defined as the reference price - auction discount %
  • Start time could be set as constant or to start at deployment
  • The user also defines auction duration and absolute expiration time.

With different configurations, this can allow for the following use cases:

  • generic dutch auction
  • fixed price limit order
  • milkman1.0 style price checker
  • TWAP (gas intensive) by deploying several orders with time delays

Technical Assessment

Code assessment: https://github.com/kayibal/composable-cow/pull/1

Overview

A gap analysis was made between the above repository and the use cases required for DAOs as described above in the alternate Milkman 2.0 (ie. no price checkers, dutch-auction only).

Using the code in the repository as a basis, and taking into account the below assessment, the time requirements would equate to:

  1. Contract implementation: 1 day
  2. Contract testing: 1 day
  3. Watch tower (Web3 Actions) updating: 1 day
  4. SDK support in cow-sdk: 0.5 day

Data requirements

Currently the staticInput (ie. Data struct):

    struct Data {
        IERC20 sellToken;
        IERC20 buyToken;
        uint256 sellAmount;
        bytes32 appData;
        address receiver;
        bool isPartiallyFillable;
        // the time the auction starts
        uint32 startTs;
        // how long the auction will run for
        uint32 duration;
        // time step, after each step a new order with adjusted limit price is emitted
        uint32 timeStep;
        // == Curve parameters ==
        // both oracles need to use the same numeraire
        IAggregatorV3Interface sellTokenPriceOracle;
        IAggregatorV3Interface buyTokenPriceOracle;
        // start and end price need to be in the oracle numeraire
        // expected as sell token / buy token price
        uint256 startPrice;
        uint256 endPrice;
    }

Instead, given the requirements, at order mining time, we can determine:

  1. Starting Price (+ auction premium)
  2. Ending Price (- auction discount)
  3. Starting time (with offset reference mining time)
  4. Starting balance of sellToken.

To achieve this, use a custom IValueFactory, that takes the parameters:

  1. Auction Premium (in BPS or start fixed).
  2. Auction Discount (in BPS or end price fixed).
  3. Auction time (offset if less than 1yr, otherwise actual start time).
  4. Fixed or Oracle (bool).
  5. Optional oracle config bytes, consisting of abi.encode(OracleConfig) where the OracleConfig struct consists of:
    a. sellTokenOracle
    b. buyTokenOracle
    c. sellTokenERC20
    d. buyTokenERC20

Using IValueFactory, fill out the struct:

struct Cabinet {
    uint256 startPrice;
    uint256 endPrice;
    uint32 t0;
    // by including the `sellAmountBalance` in the struct, we
    // can within some realm of certainty restrict the order
    // from potentially firing multiple times before `duration`
    // expires. This is only a valid consideration when:
    // B(safe) >= 2 x sellAmount
    uint256 sellAmountBalance;
}

In the cabinet, store H(Cabinet). This struct would be required to be passed into offChainInput, and subsequently be validated against the H(Cabinet) stored in the cabinet.

The staticInput (ie. Data) struct could now be abbreviated to:

    struct Data {
        IERC20 sellToken;
        IERC20 buyToken;
        uint256 sellAmount;
        bytes32 appData;
        address receiver;
        bool isPartiallyFillable;
        // how long the auction will run for
        uint32 duration;
        // time step, after each step a new order with adjusted limit price is emitted
        uint32 timeStep;
    }

Watch Tower Requirements

Given the use of the context this way, there would be the following requirements:

  1. This is likely only to be usable with createWithContext (ie. not with Merkle Roots, due to the specific nature of the ctx generated and stored in the cabinet).

  2. It would require some modification of the watch tower, notably:

    a. Through normal events emitted, the watch tower can detect that it is a dutch auction order type. No problems here.
    b. There is no way to extract the parameters that were passed to IValueFactory as the functions is view only, and therefore unable to emit events.
    c. (b) can be overcome by use of an RPC with trace filters in order to extract the original calldata. From this, the watch tower would then be able to process the logic required to determine the contents of the Cabinet struct, so that this can be passed in as offChainInput, and verified against H(Cabinet) that is stored in the cabinet mapping.