changed 2 years ago
Linked with GitHub

ComposableCoW Architecture

The following principles have been employed in the architectural design:

  1. O(1) efficiency for n order creation / replacement / deletion.
  2. Conditional orders SHOULD behave the same as a discrete order for EOAs (self-custody of assets, no need to create "container" contracts).
  3. Orders are stateless, with any required data passed via calldata.
  4. Make CoW a first class citizen of Safe and vice-versa 🐮🔒.

Assumptions

  • CoW Protocol enforces single-use orders, ie. no GPv2Order can be filled more than once.

Definitions

Conditional Order: A logical construct representing 0..n discrete orders.

Discrete Order: An order submitted to the CoW Protocol API (ie. GPv2Order.Data), ie. a single orderUid as defined by CoW Protocol.

For the purposes of this documentation, if the type of order is not specified, it shall be assumed to be a conditional order.

Use cases

Currently a smart contract interacting with CoW is required to either:

  1. Call GPv2Settlement.setPreSignature(orderUid) (API signing type "pre-sign"); or
  2. Implement isValidSignature(bytes32,bytes) (API signing type "eip1271"), where the bytes32 parameter passed is the EIP-712 digest of the GPv2Order.Data.

Presently orders have been spawning new contracts on chain, necessitating the handling of basic retrieve / cancel functionality to recover assets. Safe already provides a best-in-class for this purpose, so let's not reinvent the wheel! 🛞

Use cases that this revised architecture seeks to enable include:

  • Automatically swap token ABC for XYZ above a defined threshold balance of ABC.
  • Good after time (GAT) orders (discrete orders, conditional on a starting time).
  • TWAP by breaking orders into n x GAT orders.
  • Private conditional orders (trailing stop loss)
  • Wait4CoW orders (only matching an order with other CoW traders)
  • Doing all the above simultaneously (n x conditional orders)

Current Architecture

The architectural analysis is from the perspective of using Safe with CoW Protocol.

All CoW Protocol discrete orders are "signed". The signing methods are:

  • EIP-712 (gasless)
  • Eth-Sign (gasless)
  • EIP-1271 (gasless)
  • Pre-Sign (tx)
flowchart TD
    A[GPv2Settlement] -->|EOA Only| B[EIP-712]
    A -->|EOA Only| C[Eth-Sign]
    A -->|Contract Only| D[EIP-1271]
    A -->|Contract & EOA| E[Pre-Sign]

Signing via Safe is done via approvedHash or threshold signatures, as follows:

flowchart TD
    A[Safe] -->|delegatecall SigningLib| B[approvedHash - tx]
    A -->|manual assembly of signatures| C[threshold - gasless]

Therefore, when using Safe:

  1. EACH discrete order (ie. orderUid) must be approved/signed individually by the Safe.
  2. ONLY threshold type EIP-1271 signatures are gasless from a CoW Protocol and Safe perspective.

The result is a significantly constrained user experience.

ComposableCoW

This contract implements ISafeSignatureVerifier, designed to be used with ExtensibleFallbackHandler - a new FallbackHandler for Safe. ExtensibleFallbackHandler provides a third method of EIP-1271 validation for safe - it allows delegating an EIP-712 domain to a custom contract.

ComposableCoW therefore will process ALL EIP-1271 signatures for the GPv2Settlement.domainSeparator() EIP-712 domain.

Therefore, ComposableCoW is responsible for:

  1. Discrete order verification routing of n conditional orders per owner.
  2. Lookup discrete orders from a conditional order (watch-towers).

Order Data

Each conditional order has the following properties/data available at settlement depending on implementation:

  1. handler - the contract that will verify the conditional order's parameters.
  2. salt - allows for multiple conditional orders of the same type and data.
  3. staticData - data available to ALL discrete orders created by the conditional order.
  4. offchainData - data optionally provided from off-chain to a discrete order.

As all of these, excluding offchainData are known at creation time, they are grouped together in the struct ConditionalOrderParams:

struct ConditionalOrderParams {
    IConditionalOrder handler;
    bytes32 salt;
    bytes staticData;
}

ConditionalOrderParams has the properties:

  1. H(ConditionalOrderParams) MUST be unique.
  2. salt SHOULD be set to a cryptographically-secure random value to ensure (1) WHEN REQUIRED.
  3. Provides order secrecy (until a discrete order cut from this conditional order is broadcast to the CoW Protocol API).
  4. All values are verified by ComposableCoW prior to calling an order type's verify.

offchainInput has the properties:

  1. Allows input (such as from an off-chain oracle) that is NOT known at order creation time.
  2. NOT verified by ComposableCoW. Validation is the responsibility of the handler.

WARNING: Order implementations MUST validate / verify offchainInput!

Storage

mapping (address => bytes32) roots; // For Merkle roots (n conditional orders)
mapping (address => mapping (bytes32 => bool)) singleOrders; // Per conditional order (if not in Merkle root)

Settlement Execution Path

CoW Protocol order settlement execution path (assuming safe):

flowchart TD
    A[GPv2Settlement] -->|call: isValidSignature| B[SafeProxy]
    B -->|delegatecall: isValidSignature| C[SafeSingleton : FallbackManager]
    C -->|call: isValidSignature| D[ExtensibleFallbackHandler : SignatureVerifierMuxer]
    D -->|call: isValidSafeSignature| E[ComposableCoW]
    E -->|call: verify| F[IConditionalOrder]

Implementing the ISafeSignatureVerifier means that ComposableCoW will implement isValidSafeSignature:

function isValidSafeSignature(
    Safe safe,
    address sender,
    bytes32 _hash,
    bytes32 domainSeparator,
    bytes32, // typeHash
    bytes calldata encodeData,
    bytes calldata payload
) external view override returns (bytes4 magic);
  1. encodeData: The ABI-encoded GPv2Order.Data for the order being validated during a settlement.
  2. payload: abi.encode(bytes32[] proof, ConditionalOrderParams params, bytes offchainInput)
  3. typeHash is ignored as CoW Protocol only has one typeHash (GPv2Order.Data).

Settlement Validity

The following security constraints are enforced by ComposableCoW:

  1. ConditionalOrderParams MUST be authorised for use by the owner.
flowchart TD
    A[Extensible Fallback Handler: SignatureVerifierMuxer] -->|isValidSafeSignature| B[Check Authorisation: MerkleRoot \n Proof & ConditionalOrderParams]
    B -->|valid| V[IConditionalOrder:verify]
    B -->|invalid| C(Check Authorisation: Single Order \n ConditionalOrderParams)
    C -->|valid| V
    C -->|invalid| I[Revert]
    V -->|valid| T[Return ERC1271 Magic]
    V -->|invalid| I

Merkle Root

Leaf: H(ConditionalOrderParams), ie. H(handler || salt || data)
Properties:

  • Achieves O(1) gas efficiency for add / remove n conditional orders.

Methodology:

The caller passes abi.encode(bytes32[] proof, ConditionalOrderParams params) as the payload parameter where proof contains the Merkle Tree proof.

Authorisation:

Order O is valid if proof asserts that leaf H(params) is a member of merkle tree roots[owner].

Single Order

Properties:

  • Simpler method for enabling an order (less tooling overhead).
  • Gas expensive (n x SSTORE per order).

Methodology:

The caller passes abi.encode(bytes32[] proof, ConditionalOrderParams params, bytes offchainInput) as the payload parameter where proof is a zero-length bytes32[].

Authorisation:

Order O is valid if singleOrders(owner, H(params)) == true.

Add / Remove Orders

The owner uses the applicable setter method depending on how they wish to specify the order.

Merkle Root

The owner calls the setRoot(bytes32 root, Proof calldata proof) setter method.

struct Proof {
    uint256 location;
    bytes data;
}
  • root: Merkle Tree of conditional orders (leaves = H(params)).
  • proof: Where watch towers may locate the proofs from.
    Private: Proof({location: 0, data: bytes("")}) - no proofs available to watch-towers.
    Log: Proof.location = 1 and Proof.data = abi.encode(bytes[] order) where order = abi.encode(bytes32[] proof, ConditionalOrderParams params)

When a new merkle root is set, emits MerkleRootSet(address indexed owner, bytes32 root, Proof proof).

NOTE: ComposableCoW will NOT verify the proof data passed in via the proof parameter for setRoot. It is the responsibility of the client and watch-tower to verify / validate this.

NOTE: The Proof.location is intentionally not made an enum to allow for future extensibility as other proof locations may be integrated, including but not limited to Swarm, Waku, IPFS etc.

Single Order

The owner calls the respective setter method:

  • Create orders: create(ConditionalOrderParams params, bool dispatch)
    If the owner wants to make the order public, dispatch is set true, and the order creation will result in an emission of ConditionalOrderCreated(address indexed owner, ConditionalOrderParams params).
  • Removing order: remove(bytes32 orderHash)
    To remove an order, set orderhash = H(params) from the initial ConditionalOrderCreated. No event is emitted, as the order is invalidated (any watch tower trying to cut discrete orders will see the getTradeableOrder() will revert).

Discrete Order Generation

The default is to use a Factory base pattern for conditional orders, where based on an order's properties, it is able to generate a tradeable order (GPv2Order.Data).

To do this, an order implements the IConditionalOrderFactory interface. Following this pattern, a developer MUST ensure:

H(IConditionalOrderFactory.getTradeableOrder(owner,sender,params,offchainInput)) == _hash

Where _hash is the _hash passed into the isValidSignature() call.

Advanced conditional orders

These implement the top-level IConditionalOrder interface, which only provides a raw verify method, and doens't allow for order generation.

Watch Towers

As these orders are not automatically indexed by the CoW Protocol API, there needs to be some method of relaying them to CoW for inclusion in a batch. This is done by reference to the events emitted by ComposableCoW:

  • ConditionalOrderCreated(address indexed owner, ConditionalOrderParams params)
  • MerkleRootSet(address index owner, bytes32 root, Proof proof)

The contract that emits the above methods shall provide a method:

function getTradeableOrderWithSignature(
    address owner,
    bytes32[] proof,
    ConditionalOrder params,
    bytes offchainInput
) external view (GPv2Order.Data memory, bytes memory signature);

In the context of ComposableCoW this will:

  1. Determine if owner is a safe, and provide the SignatureVerifierMuxer appropriate formatting for the EIP-1271 signature submission to CoW Protocol.
  2. If not a safe, format the EIP-1271 signature according to abi.encode(domainSeparator, staticData, offchainData).

ComposableCoW will:

  1. Check that the order is authorised.
  2. Check that the order type supports discrete order generation (ie. IConditionalOrderFactory) by using IERC165 (and revert if not, allowing the watch-tower to prune invalid monitored conditional orders).
  3. Call getTradeableOrder on the handler to get the GPv2Order.Data
  4. Generate the signing data as above.

NOTE: There is no need to emit these events when orders are cancelled / removed as calling the getTradeableOrderWithSignature will yield a revert with a custom error indicating the order will never be valid with the current state.

Interfaces

IConditionalOrder

This is the root-level interface for conditional orders, implementing:

function verify(
    address owner,
    address sender,
    bytes32 hash,
    bytes32 domainSeparator,
    bytes calldata staticInput,
    bytes calldata offchainInput
    GPv2Order.Data calldata order,
) external view;

CAUTION: The verify method MUST revert if the parameters specified do not correspond to a valid order.

IConditionalOrderFactory

Allows for generation of discrete orders for submission to CoW Protocol by implementing ERC165 and:

function getTradeableOrder(
    address owner,
    address sender,
    bytes calldata staticInput,
    bytes calldata offchainInput
) external view returns (GPv2Order.Data memory);
Select a repo