Comment on page
Programmatic Order Framework
Programmatic Order Framework is a framework for smoothing the developer experience when building conditional orders on CoW Protocol. Conditional orders are a subset of
ERC-1271
smart contract orders. It allows one to create conditional orders that:- 1.Can be used to generate multiple discrete order (self-expressing)
- 2.Assess a proposed order against a set of conditions (self-validating)
The framework makes boilerplate code for conditional orders a thing of the past, and allows developers to focus on the business logic of their order. Programmatic Order Framework handles:
- 1.Authorization (multiple owners, with multiple orders per owner)
- 2.Order relaying (watch-towers)
The following principles have been employed in the architectural design:
- 1.
O(1)
gas-efficiency forn
conditional order creation / replacement / deletion - 2.Conditional orders SHOULD behave the same as a discrete order for EOAs (self-custody of assets, ie. "wrapper" contracts not required)
- 3.Conditional orders SHOULD be optimized towards statelessness - pass required data via
calldata
- 4.
By using Merkle Trees, the gas efficiency of
O(1)
is achieved for n
conditional orders. This is achieved by storing the Merkle Tree root on-chain, and passing the Merkle Tree proof to the ComposableCoW
contract. This allows for O(1)
gas efficiency for adding / removing conditional orders.For simplicity, single orders are also supported, however, this is NOT recommended for large
n
as the gas efficiency is O(n)
.As there are many nested contracts, it's important for a callee to know some context from the caller. To achieve this, ComposableCoW passes a
bytes32
variable ctx
to the callee, such that:ctx = merkle root of orders: bytes32(0)
single order: H(ConditionalOrderParams)
Having this context also allows for conditional orders / merkle roots to use this as a key in a mapping, to store conditional order-specific data.
The following flowchart illustrates the conditional order verification flow (assuming
safe
):isValidSafeSignaturevalidinvalidvalidinvalidvalidinvalidvalidinvalidExtensible Fallback Handler: SignatureVerifierMuxerCheck Authorization: MerkleRoot
Proof & ConditionalOrderParamsSwapGuard:verifyRevertIConditionalOrder:verifyCheck Authorization: Single Order
ConditionalOrderParamsReturn ERC1271 Magic
CoW Protocol order settlement execution path (assuming
safe
):call: isValidSignaturedelegatecall: isValidSignaturecall: isValidSignaturecall: isValidSafeSignaturecall: verifyGPv2SettlementSafeProxySafeSingleton : FallbackManagerExtensibleFallbackHandler : SignatureVerifierMuxerComposableCoWIConditionalOrder
Programmatic Order Framework implements
ISafeSignatureVerifier
, which allows for delegated ERC-1271
signature validation with an enhanced context:function isValidSafeSignature(
Safe safe,
address sender,
bytes32 _hash,
bytes32 domainSeparator,
bytes32, // typeHash
bytes calldata encodeData,
bytes calldata payload
) external view override returns (bytes4 magic);
Parameter | Description |
safe | Contract that is delegating signing |
sender | msg.sender that called isValidSignature on safe |
_hash | Order digest |
domainSeparator | |
typeHash | Not used |
encodeData | |
payload |
In order to delegate signature verification to
ComposableCoW
, the delegating contract may either:- 1.Be a
Safe
and useExtensibleFallbackHandler
that allows forEIP-712
domain delegation to a custom contract (ie.ComposableCoW
); or - 2.Implement
ERC-1271
and within theisValidSignature
method, callComposableCoW.isValidSafeSignature()
.
TIP
Programmatic Order Framework can also be used with contracts other than
Safe
. The ERC1271Forwarder
abstract contract has been provided to allow for new contracts to easily integrate with Programmatic Order Framework.NOTE
If using
ExtensibleFallbackHandler
, and the CoW Protocol settlement domain is delegated to ComposableCoW
, ALL ERC-1271
signatures will be processed by ComposableCoW
.A conditional order that verifies a proposed discrete order against a set of conditions shall implement the
IConditionalOrder
interface.function verify(
address owner,
address sender,
bytes32 _hash,
bytes32 domainSeparator,
bytes32 ctx,
bytes calldata staticInput,
bytes calldata offchainInput
GPv2Order.Data calldata order,
) external view;
Parameter | Description |
owner | The owner of the conditional order |
sender | msg.sender context calling isValidSignature |
_hash | EIP-712 order digest |
domainSeparator | EIP-712 domain separator |
ctx | |
staticInput | Conditional order type-specific data known at time of creation for all discrete orders |
offchainInput | Conditional order type-specific data NOT known at time of creation for a specific discrete order (or zero-length bytes if not applicable) |
order |
DANGER
Order implementations MUST validate / verify
offchainInput
!CAUTION
The
verify
method MUST revert
with OrderNotValid(string)
if the parameters in staticInput
do not correspond to a valid order.NOTE
All values EXCLUDING
offchainInput
are verified by Programmatic Order Framework prior to calling an order type's verify
.A conditional order that generates discrete orders shall implement the
IConditionalOrderGenerator
interface.function getTradeableOrder(
address owner,
address sender,
bytes32 ctx,
bytes calldata staticInput,
bytes calldata offchainInput
) external view returns (GPv2Order.Data memory);
To simplify the developer experience, a
BaseConditionalOrder
contract has been provided that implements the IConditionalOrderGenerator
interface, and necessary boilerplate.A swap guard is a contract that implements the
ISwapGuard
interface, and if set by an owner
, will be called by ComposableCoW
prior to calling verify
on the conditional order.This allows for
owner
-wide restrictions on the conditional order, such as:- Token whitelist
The
ISwapGuard
interface is as follows:function verify(
GPv2Order.Data calldata order,
bytes32 ctx,
IConditionalOrder.ConditionalOrderParams calldata params,
bytes calldata offchainInput
) external view returns (bool);
Parameter | Description |
order | Proposed discrete order |
ctx | |
params | |
offchainInput | Conditional order type-specific data NOT known at time of creation for a specific discrete order (or zero-length bytes if not applicable) |
- CoW Protocol's settlement contract enforces single-use orders, ie. NO
GPv2Order
can be filled more than once - For merkle trees,
H(ConditionalOrderParams)
MUST be a member of the merkle treeroots[owner]
- For single orders,
singleOrders[owner][H(ConditionalOrderParams)] == true
A conditional order is defined by the following data:
struct ConditionalOrderParams {
IConditionalOrder handler;
bytes32 salt;
bytes staticData;
}
Field | Description |
handler | The contract implementing the conditional order logic |
salt | Allows for multiple conditional orders of the same type and data |
staticData | Data available to ALL discrete orders created by the conditional order |
NOTE
All of the above fields are verified by
ComposableCoW
to be valid, prior to calling the verify
method on the handler (IConditionalOrder
).TIP
When used with Merkle Trees and a cryptographically-secure random
salt
, the conditional order is effectively private (until a discrete order cut from this conditional order is broadcast to the CoW Protocol API).CAUTION
H(ConditionalOrderParams)
MUST be unique- Not setting
salt
to a cryptographically-secure random value MAY result in leaking information or hash collisions
This is the data passed to
ComposableCoW
via the payload
parameter of isValidSafeSignature
:struct PayloadStruct {
bytes32[] proof;
IConditionalOrder.ConditionalOrderParams params;
bytes offchainInput;
}
Field | Description |
proof | Merkle Tree proof (if applicable, zero length otherwise) |
params | |
offchainInput | Off-chain input (if applicable, zero length otherwise) |
By setting
proof
to zero-length, this indicates to ComposableCoW
that the order is a single order, and not part of a Merkle Tree.struct Proof {
uint256 location;
bytes data;
}
NOTE
The
Proof.location
is intentionally not made an enum
to allow for future extensibility as other proof locations may be integrated.Field | Description |
location | An integer representing the location where to find the proofs |
data | location implementation specific data for retrieving the proofs |
Name | location | data |
PRIVATE | 0 | bytes("") |
LOG | 1 | abi.encode(bytes[] order) where order = abi.encode(bytes32[] proof, ConditionalOrderParams params) |
WAKU | 2 | abi.encode(string protobufUri, string[] enrTreeOrMultiaddr, string contentTopic, bytes payload) |
SWARM | 3 | abi.encode(bytes32 swarmCac) |
IPFS | 5 | abi.encode(bytes32 ipfsCid) |
Using an
owner
as a key, the roots
mapping stores the Merkle Tree root for the conditional orders of that owner
.mapping(address => bytes32) public roots;
Using
owner, ctx
as a key, the singleOrders
mapping stores the single orders for the conditional orders of that owner
.mapping(address => mapping(bytes32 => bool)) public singleOrders;
Using
owner, ctx
as a key, the cabinet
mapping stores the conditional order-specific data for the conditional orders of that owner
.mapping(address => mapping(bytes32 => bytes32)) public cabinet;
Using
owner
as a key, the swapGuards
mapping stores the swap guards for the conditional orders of that owner
.mapping(address => ISwapGuard) public swapGuards;
A
safe
or owner
calls the respective setter method to set the Merkle Tree root for their conditional orders:function setRoot(bytes32 root, Proof calldata proof) public;
function setRootWithContext(
bytes32 root,
Proof calldata proof,
IValueFactory factory,
bytes calldata data
) external;
Parameter | Description |
root | Merkle Tree root of conditional orders |
proof | |
factory | An IValueFactory that will be used to populate the ctx storage slot (if applicable) |
data | Data to be passed to the factory to populate the ctx storage slot (if applicable) |
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.The
owner
calls the respective setter method to create a conditional order:function create(
IConditionalOrder.ConditionalOrderParams calldata params,
bool dispatch
) public;
function createWithContext(
IConditionalOrder.ConditionalOrderParams calldata params,
IValueFactory factory,
bytes calldata data,
bool dispatch
) external;
Parameter | Description |
params | |
factory | An IValueFactory that will be used to populate the ctx storage slot (if applicable) |
data | Data to be passed to the factory to populate the ctx storage slot (if applicable) |
dispatch | If true , broadcast the ConditionalOrderCreated event |
The
owner
calls the remove(bytes32 singleOrderHash)
method to remove a conditional order:function remove(bytes32 singleOrderHash) external;
Parameter | Description |
singleOrderHash | H(ConditionalOrderParams) |
The
owner
calls the setSwapGuard(ISwapGuard guard)
method to set a swap guard for a conditional order:function setSwapGuard(ISwapGuard swapGuard) external;
Parameter | Description |
swapGuard | The swap guard contract |
A watch-tower calls the
getTradeableOrderWithSignature
method to get a discrete order that is tradeable on CoW Protocol:function getTradeableOrderWithSignature(
address owner,
IConditionalOrder.ConditionalOrderParams calldata params,
bytes calldata offchainInput,
bytes32[] calldata proof
) external view returns (GPv2Order.Data memory order, bytes memory signature);
This function will:
- 1.Determine if
owner
is asafe
, and provide theSignatureVerifierMuxer
appropriate formatting for theERC-1271
signature submission to CoW Protocol. - 2.If not a
safe
, format theERC-1271
signature according toabi.encode(domainSeparator, staticData, offchainData)
.
Subsequently,
ComposableCoW
will:- 1.Check that the order is authorized.
- 2.Check that the order type supports discrete order generation (ie.
IConditionalOrderGenerator
) by usingIERC165
(andrevert
if not, allowing the watch-tower to prune invalid monitored conditional orders). - 3.
- 4.Generate the signing data as above.
ConditionalOrderCreated(address indexed owner, ConditionalOrderParams params)
MerkleRootSet(address index owner, bytes32 root, Proof proof)
ProofNotAuthed()
- the proof is not authorized (merkle root incorrect)SingleOrderNotAuthed()
- the single order is not authorizedSwapGuardRestricted()
- the swap guard did not pass verificationInvalidHandler()
- the handler is not a valid conditional orderInvalidFallbackHandler()
- the fallback handler is not a valid conditional orderInterfaceNotSupported()
- the handler does not support theIConditionalOrder
interface
KEEP YOUR ORDERS WATCHED
A conditional order developer SHOULD use these error codes to ensure that the conditional order is well-formed and not garbage collected / rate limited by a watch-tower.
OrderNotValid(string)
- thestaticInput
parameters are not valid for the conditional orderPollTryNextBlock(string)
- signal to a watch-tower that polling should be attempted againPollTryAtBlock(uint256 blockNumber, string)
- signal to a watch-tower that polling should be attempted again at a specific block numberPollTryAtEpoch(uint256 timestamp, string)
- signal to a watch-tower that polling should be attempted again at a specific epoch (unix timestamp)PollNever(string)
- signal to a watch-tower that the conditional order should not be polled again (delete)
As these orders are not automatically indexed by the CoW Protocol, there needs to be some method of relaying them to the Order Book API for inclusion in a batch.
This is the responsibility of a watch-tower. CoW Protocol runs a watch-tower that will monitor the
ConditionalOrderCreated
event, and relay the discrete orders to the Order Book API.Last modified 24d ago