Composable CoW is a framework for creating conditional orders on CoW Protocol. It enables any wallet capable of ERC-1271 signatures to define orders that become tradeable when specific conditions are met (price thresholds, time windows, balance triggers, and so forth).
ERC-1271 is the standard for smart contract signature verification, allowing contracts to validate signatures on behalf of their owners. This includes Safe wallets, Argent, Sequence, and other smart contract wallets.
The architecture separates two distinct execution paths:
- Settlement Path — on-chain verification during trade execution (gas-sensitive).
- Polling Path — off-chain queries by monitoring services (gas-irrelevant).
This separation ensures settlement remains gas-efficient while providing rich metadata for off-chain infrastructure.
- Single source of truth:
generateOrder()contains all order generation logic. - Lean settlement: No metadata structs; only constant string errors for debugging.
- Rich polling: Structured results with scheduling hints for monitoring services.
- Verdict and fill state are orthogonal: handlers produce a verdict (
GeneratorResult); observed fill state is composed by the registry, never by a handler. - No code duplication: Polling wraps the same core logic used by settlement.
- Handler purity: the generated order is a pure function of
owner,ctx,staticInput,offchainInputand block state. The generation surface takes no caller identity at all, and handlers must not branch onmsg.sender(see Caller Identity). Off-chain simulation soundness depends on this.
IConditionalOrder
├── Errors (bytes4 reasonCode: the selector of a handler-declared error)
│ ├── OrderNotValid(bytes4 reasonCode)
│ ├── PollTryNextBlock(bytes4 reasonCode)
│ ├── PollTryAtTimestamp(uint256 timestamp, bytes4 reasonCode)
│ └── PollTryAtBlock(uint256 blockNumber, bytes4 reasonCode)
├── ConditionalOrderParams struct
├── generateOrder() - core order generation
└── verify() - settlement validation
IConditionalOrderGenerator : IConditionalOrder, IERC165
├── GeneratorResultCode enum (the handler's verdict)
├── GeneratorResult struct
├── poll() - structured, non-reverting polling
├── tryGenerateOrder() - probe returning full revert data
├── getNextPollTimestamp() - scheduling hints
└── describeOrder() - human-readable status
IOrderManifest (sidecar, own ERC-165 id)
├── Cardinality enum (EXACT, CAPPED, UNBOUNDED)
├── ManifestInfo struct (cardinality, totalOrders)
├── ManifestEntry struct (index, order, validFrom, isActive)
├── getManifestInfo() - order cardinality info
└── getManifestPage() - paginated order enumeration
The registry-facing polling types live on ComposableCow itself:
ComposableCow
├── FillStatus enum (NONE, PARTIALLY_FILLED, FILLED, INVALIDATED)
└── PollResult struct (GeneratorResult generator, FillStatus fill, uint256 filledAmount)
New capabilities are added as sidecar interfaces with their own ERC-165 ids (as IOrderManifest is), never by widening IConditionalOrderGenerator: its interface id gates the polling path in ComposableCow, and changing it would reject every deployed handler.
CoW Settlement
│
▼
Wallet.isValidSignature(hash, signature) ERC-1271 verification
│
▼
[Wallet-specific routing] e.g., Safe's ExtensibleFallbackHandler
│
▼
ComposableCow.isValidSafeSignature(...) Signature validation
│
├── _auth() Verify merkle proof or single order
├── _guardCheck() Optional swap guard
│
└── handler.verify(...) LEAN PATH
│
▼
generateOrder() Core logic, reverts if invalid
│
└── hash check Verify order matches
Note: The function isValidSafeSignature works with any ERC-1271-compatible wallet that routes signature verification to ComposableCow. The name reflects the original Safe integration, but the interface is wallet-agnostic.
Gas considerations:
- Error reasons use constant strings (minimal allocation).
- No polling structs are constructed.
- No polling metadata calls.
- Minimal computation beyond core validation.
Monitoring Service
│
▼
ComposableCow.getTradeableOrderWithSignature(...)
│
├── _auth() Verify authorization
│
└── _poll() ERC-165 gate + handler.poll(...)
│
▼
try generateOrder() Same core logic
│
├── Success:
│ ├── getNextPollTimestamp()
│ ├── describeOrder()
│ └── GeneratorResult(POST, order, hints)
│
└── Revert:
├── decode error selector
└── GeneratorResult(WAIT_* / TRY_NEXT_BLOCK / INVALID, waitUntil, reasonCode)
│
▼ (verdict == POST)
_getFilledAmount() Compose the fill overlay from GPv2Settlement
│
├── 0 → fill = NONE
├── type(uint).max → fill = INVALIDATED (invalidateOrder, not a fill)
├── >= totalAmount → fill = FILLED
└── otherwise → fill = PARTIALLY_FILLED
│
▼
signature emitted iff verdict == POST and the order is postable:
fill == NONE, or
fill == PARTIALLY_FILLED and order.partiallyFillable
(after the optional swap guard check)
Characteristics:
- Returns a structured
PollResult; never reverts for order conditions (only for authorization and handler-interface failures). - The handler's verdict and the observed fill state are orthogonal: a
POSTverdict coexists withPARTIALLY_FILLED.POSTasserts that the discrete order is valid, not that a consumer must act on it. - A signature is withheld only for fill states the chain proves terminal:
FILLED,INVALIDATED, and any recorded fill on a fill-or-kill order. A partial fill on apartiallyFillableorder is not terminal, so a valid signature is still returned. - Note that a recorded fill also implies the discrete order was already accepted by the orderbook and settled, so it stays solvable there until
validTo. Continued filling of the remainder does not depend on a monitoring service re-posting it. The registry is an on-chain view and cannot observe orderbook state, so it reports validity and leaves the posting decision to the consumer rather than asserting that the order is still listed. - Includes scheduling hints (
nextPollTimestampandwaitUntil). - Carries machine-readable reason selectors for debugging; names resolve from the handler ABI.
- If the swap guard restricts the order, the registry reports it in the
PollResult.restrictionoverlay (SWAP_GUARD) and no signature is emitted; the generator verdict is never overwritten - a guardedPOSTstays visible. Restriction is owner-reversible and its lifecycle is observable viaSwapGuardSet(clear =address(0)). INVALIDis uniformly terminal. With restriction expressed as an overlay, the only producer ofINVALIDis the handler's ownOrderNotValid; consumers never need a reason-selector branch for control flow.checkOrder()returns the same composedPollResultthrough the same_pollhelper (including the ERC-165 handler gate), without building the signature. The swap guard is not consulted bycheckOrder; it is enforced at signature build time and during settlement.
Caller identity lives on the validation surface (IConditionalOrder.verify), not the generation surface (IConditionalOrderGenerator).
generateOrder is reached three ways, and msg.sender differs between them:
| Path | Invocation | msg.sender in the handler |
|---|---|---|
Settlement (verify) |
internal call | the registry |
Polling (poll) |
this.generateOrder |
the handler itself |
Manifest (getManifestPage) |
this.generateOrder |
the handler itself |
The external calls exist for try/catch semantics, which is what makes a handler's revert decodable into a verdict. The msg.sender divergence is a consequence.
msg.sendermust never be consulted. A handler branching on it derives one order under settlement and another under polling, so a monitoring service can propose an order that fails to settle. Not enforced on-chain.- Caller identity cannot reach generation.
generateOrder,pollandtryGenerateOrdertake nosender, so an order that varies with the settling party is not expressible.
verify keeps its sender: a single call path, and gating on the settling party by reverting is legitimate. A validator-only handler, implementing IConditionalOrder without being a generator, makes no polling promise and may gate freely.
Settlement-time gating is therefore not previewable by poll or getManifestPage.
There are no stringly errors anywhere on the error surface. The framework errors carry
a bytes4 reasonCode: the selector of a custom error the handler declares (e.g.
error StrikeNotReached(); passed as StrikeNotReached.selector). Declared errors are
part of the handler ABI, so any ABI-aware consumer resolves a reasonCode to a name
without a bespoke table, and revert data stays fixed-size:
| Error | Meaning | Monitoring service Action |
|---|---|---|
OrderNotValid(bytes4) |
Permanent failure | Stop polling |
PollTryNextBlock(bytes4) |
Transient, retry soon | Poll next block |
PollTryAtTimestamp(uint256, bytes4) |
Wait for time | Schedule at timestamp |
PollTryAtBlock(uint256, bytes4) |
Wait for block | Schedule at block |
PollNeedsOffchainInput(bytes4) |
Needs constructed offchainInput |
Acquire input (see docs/discovery.md on order modules) or park - do not re-poll empty on a schedule |
BaseConditionalOrder.poll() decodes reverts from generateOrder() into verdicts. Only OrderNotValid is terminal; everything unrecognized is treated as transient so a recoverable fault reschedules instead of permanently killing a valid order off-chain:
| Revert | Verdict | reasonCode |
|---|---|---|
OrderNotValid(code) |
INVALID |
decoded code |
PollTryNextBlock(code) |
TRY_NEXT_BLOCK |
decoded code |
PollTryAtTimestamp(t, code) |
WAIT_TIMESTAMP (waitUntil = t) |
decoded code |
PollTryAtBlock(b, code) |
WAIT_BLOCK (waitUntil = b) |
decoded code |
PollNeedsOffchainInput(code) |
NEEDS_INPUT (acquire input or park - never a timed retry) |
decoded code |
Panic(subcode) |
TRY_NEXT_BLOCK |
0x4e487b71 (the Panic selector) |
Error(string) (bare require) |
TRY_NEXT_BLOCK |
0x08c379a0 (the Error selector) |
| any other custom error | TRY_NEXT_BLOCK |
the caught selector |
| empty / malformed revert data | TRY_NEXT_BLOCK |
bytes4(0) |
For full diagnostics - Panic sub-codes, Error(string) messages, or an unrecognized
error's arguments - call tryGenerateOrder, which returns (success, order, revertData)
as ordinary return data: the complete ABI-encoded inner error arrives without any RPC
revert-data handling, making it composable in multicalls and batch probes.
Handlers can only ever produce a verdict; fill state is deliberately not representable on the generator surface:
enum GeneratorResultCode {
POST, // A discrete order is ready to be posted
WAIT_TIMESTAMP, // Wait until waitUntil (unix timestamp)
WAIT_BLOCK, // Wait until waitUntil (block number)
TRY_NEXT_BLOCK, // Transient condition, retry next block
INVALID // Permanently invalid, stop polling
}
struct GeneratorResult {
GeneratorResultCode code;
GPv2Order.Data order; // Valid iff code == POST
uint256 nextPollTimestamp; // POST: when to poll for the next order
uint256 waitUntil; // WAIT_*: when to retry
bytes4 reasonCode; // Selector behind a non-POST verdict; bytes4(0) for POST
}Composed by ComposableCow, never by a handler:
enum FillStatus {
NONE, // No fill observed
PARTIALLY_FILLED, // 0 < filledAmount < total
FILLED, // filledAmount >= total
INVALIDATED // order was cancelled via invalidateOrder
}
struct PollResult {
IConditionalOrderGenerator.GeneratorResult generator;
FillStatus fill; // Only meaningful when the verdict is POST
uint256 filledAmount; // Raw GPv2Settlement.filledAmount (uint256.max when invalidated)
}| Verdict | Meaning | Monitoring service Action |
|---|---|---|
POST |
Order ready to post | Submit to CoW Protocol API (if the fill overlay allows) |
WAIT_TIMESTAMP |
Wait for specific time | Schedule poll at waitUntil |
WAIT_BLOCK |
Wait for specific block | Schedule poll at block waitUntil |
TRY_NEXT_BLOCK |
Transient condition | Poll again next block |
INVALID |
Permanently invalid | Stop polling this order |
| FillStatus | Meaning | Signature emitted? |
|---|---|---|
NONE |
Untouched | Yes (verdict POST) |
PARTIALLY_FILLED |
Partially filled | Only if order.partiallyFillable |
FILLED |
Fully filled | No |
INVALIDATED |
Cancelled on-chain (invalidateOrder) |
No |
INVALIDATED is distinct from FILLED: GPv2Settlement.invalidateOrder sets filledAmount to type(uint256).max, which is a cancellation, not a fill.
filledAmount is denominated per order kind, matching GPv2Settlement.computeTradeExecution: a sell order accumulates executedSellAmount bounded by order.sellAmount, a buy order accumulates executedBuyAmount bounded by order.buyAmount. The overlay compares against the matching field, so the same observed amount can be a complete fill of one side and a partial fill of the other.
The overlay is keyed by orderUid, which is H(order, domainSeparator) || owner || validTo. It therefore reports the fill state of the discrete order this poll produced, not a running total across every order a conditional order has ever produced.
That is a deliberate limit rather than an omission, because the registry cannot maintain a running total:
- every polling entry point is
view, so none can write an accumulator; - nothing notifies the registry of a settlement —
GPv2Settlementhas no callback, and the registry's only interaction with it is a single keyed read; GPv2Settlementstoresmapping(bytes => uint256) filledAmountand exposes no aggregate to read instead.
Computing a total on read would mean enumerating every discrete order ever derived from the context and summing each one's filledAmount. Only the handler knows that set (via IOrderManifest), it is unbounded in general, and the registry gates on IConditionalOrderGenerator rather than the manifest, so it cannot rely on it.
A bounded-total intent is expressible today by making the discrete order stable: with fixed parameters and validTo, the order maps to one orderUid, and that single entry is the accumulator. Per-order tracking is exactly right for such a handler.
The gap is only for an intent whose discrete order deliberately varies between polls, for example one whose acceptable price moves. There the total is not a chain-observable quantity at any single key, and tracking it belongs to the monitoring service, which already sees every poll and can accumulate per context off-chain.
| Value | Meaning |
|---|---|
0 |
Use order.validTo + 1 as default (POLL_AT_VALIDTO) |
> 0 |
Poll at this specific timestamp |
type(uint256).max |
Final order, stop polling after fill (POLL_NEVER) |
function generateOrder(...) public view returns (GPv2Order.Data memory) {
// Validate conditions - use require with custom errors (Solidity 0.8.30+)
require(!expired, OrderNotValid(OrderExpired.selector));
require(conditionMet, PollTryNextBlock(ConditionNotMet.selector));
// Build and return order
return GPv2Order.Data(...);
}
function getNextPollTimestamp(...) external pure returns (uint256) {
return POLL_NEVER; // single shot
}Handlers must reject degenerate zero-amount orders (OrderNotValid(ZeroAmount.selector)): a fill-or-kill order with sellAmount == 0 settles without ever incrementing filledAmount, so the native replay guard never trips and the order would be indefinitely replayable.
function generateOrder(...) public view returns (GPv2Order.Data memory) {
// Before the start is a WAIT, not a permanent failure
require(block.timestamp >= startTime, PollTryAtTimestamp(startTime, BeforeTwapStart.selector));
require(block.timestamp < endTime, OrderNotValid(AfterTwapFinish.selector));
// Between parts (outside the span) is a scheduling gap:
// PollTryAtTimestamp(nextPartStart, NotWithinSpan.selector), or
// OrderNotValid(AfterTwapFinish.selector) when no part remains.
return buildPartOrder(currentPart);
}
function getNextPollTimestamp(...) external view returns (uint256) {
uint256 part = calculateCurrentPart();
if (part + 1 >= numParts) return POLL_NEVER; // underflow-safe form
return startTime + ((part + 1) * frequency);
}function generateOrder(...) public view returns (GPv2Order.Data memory) {
require(funded, OrderNotValid(NotFunded.selector));
return GPv2Order.Data(...);
}
function getNextPollTimestamp(...) external pure returns (uint256) {
return POLL_AT_VALIDTO; // Use validTo + 1, perpetually repeating
}One poll yields one discrete order, but a conditional order is not limited to one simultaneously postable order. generateOrder may key the discrete order off offchainInput (e.g. abi.encode(index)), and verify() re-derives from the same offchainInput carried in each order's signature payload — so N simultaneously-postable orders per (handler, salt, staticInput) are sound, settle independently, and need no interface change. IOrderManifest.getManifestPage provides the enumeration.
Worked example — a multi-currency DCA buying N tokens per window:
function generateOrder(address owner, bytes32 ctx, bytes calldata staticInput, bytes calldata offchainInput)
public view returns (GPv2Order.Data memory)
{
Data memory data = abi.decode(staticInput, (Data)); // data.legs: token/amount pairs
uint256 leg = abi.decode(offchainInput, (uint256)); // which leg to cut
require(leg < data.legs.length, OrderNotValid(NoSuchLeg.selector));
return buildLegOrder(data, leg); // window logic as usual
}A monitoring service enumerates the currently active entries via the manifest and polls once per leg with the leg index as offchainInput. The same pattern covers a simplified AMM (index 0 = buy, 1 = sell) and sequenced strategies (the follow-up entry appears in the manifest once the first leg fills).
The IOrderManifest interface enables enumeration of the discrete orders a conditional order will produce. This is useful for analytics, UI previews, and order lifecycle tracking. It is a sidecar interface with its own ERC-165 id, feature-detected by consumers and never consulted on the settlement path. The manifest mirrors the information poll already exposes — it is not a second source of truth.
Cardinality names how totalOrders is to be read:
| Cardinality | Description | Example |
|---|---|---|
EXACT |
totalOrders is the exact count |
TWAP with n parts |
CAPPED |
totalOrders is an upper cap; actual count is dynamic |
Future order types |
UNBOUNDED |
No meaningful count (totalOrders is 0) |
PerpetualStableSwap |
struct ManifestInfo {
Cardinality cardinality;
uint256 totalOrders; // Exact for EXACT, cap for CAPPED, 0 for UNBOUNDED
}struct ManifestEntry {
uint256 index; // Order index (0-indexed)
GPv2Order.Data order; // The discrete order
uint256 validFrom; // When this order becomes valid
bool isActive; // Whether currently within validity window
}The validFrom field is needed because GPv2Order.Data only contains validTo.
getManifestPage returns (entries, hasMore, status) and guarantees that an empty page with hasMore == true is unreachable: a consumer advancing offset += entries.length and stopping at hasMore == false always terminates.
- UNBOUNDED handlers expose only index 0 (the current discrete order) and return
hasMore == falseon every branch;offset > 0yields an empty final page. - When a page is empty because the order cannot currently be generated,
statuscarries the verdictpollwould return for the same conditions, so a not-yet-active order is distinguishable from a permanently invalid one:
struct ManifestStatus {
GeneratorResultCode code; // the same verdict `poll` would return
uint256 waitUntil; // block or timestamp to retry at
bytes4 reasonCode; // the handler-declared selector
}- An ordinary page reports
POSTwith zerowaitUntilandreasonCode, including a page that is empty only becauseoffsetis past the end. Both return no entries, and onlycodeseparates "nothing here" from "nothing yet".
| Order Type | Cardinality | totalOrders | Behavior |
|---|---|---|---|
| TWAP | EXACT | n (number of parts) | All n parts with timing; degenerate parameters yield an empty manifest |
| StopLoss | EXACT | 1 | Single order from generateOrder() |
| GoodAfterTime | EXACT | 1 | Single order from generateOrder() |
| TradeAboveThreshold | EXACT | 1 | Single order from generateOrder() |
| PerpetualStableSwap | UNBOUNDED | 0 | Current order at index 0; pagination always terminates |
BaseConditionalOrder provides a default manifest implementation for single-shot orders:
getManifestInfo()returnsEXACTwithtotalOrders: 1.getManifestPage()wrapsgenerateOrder()for a single entry, surfacing thepollverdict instatuswhen generation is not currently possible.
| Event | Description |
|---|---|
MerkleRootSet(address indexed owner, bytes32 root, Proof proof, bytes context) |
Merkle root updated |
ConditionalOrderCreated(address indexed owner, ConditionalOrderParams params, bytes context) |
Order created with dispatch=true |
ConditionalOrderRemoved(address indexed owner, bytes32 indexed orderHash) |
Order deauthorized |
SwapGuardSet(address indexed owner, ISwapGuard swapGuard) |
Swap guard updated |
On the *WithContext paths, the cabinet is written before the event fires and context carries the resolved cabinet value (abi.encode(bytes32 value)), so an indexer reacting to the event observes a consistent cabinet without an extra read. On the plain paths context is empty bytes.
| Function | Path | Returns |
|---|---|---|
isValidSafeSignature() |
Settlement | bytes4 magic value |
getTradeableOrderWithSignature() |
Polling | (PollResult, bytes signature) |
checkOrder() |
Polling | PollResult |
Orders are authorized via:
- Single orders:
singleOrders[owner][hash(params)] = true - Merkle roots:
roots[owner] = merkleRoot
The _auth() function verifies authorization and returns the context key as follows:
- Merkle orders:
ctx = bytes32(0). - Single orders:
ctx = hash(params).
The cabinet mapping stores per-order context:
mapping(address owner => mapping(bytes32 ctx => bytes32 value)) public cabinet;This is used by TWAP to store dynamic start times set at order creation.
ComposableCow is designed to work with any smart contract wallet that implements ERC-1271 (isValidSignature). The integration requires the wallet to route signature verification requests to ComposableCow.
- Order Creation: The wallet owner authorizes conditional orders via
create()orsetRoot(). - Signature Verification: When CoW Protocol settlement calls
isValidSignature(hash, signature)on the wallet, it routes the call to ComposableCow. - Validation: ComposableCow verifies authorization and validates the order via
generateOrder().
| Wallet Type | Integration Method |
|---|---|
| Safe | ExtensibleFallbackHandler with domain verifier |
| Other ERC-1271 | Extend ERC1271Forwarder abstract contract |
The ERC1271Forwarder abstract contract provides a ready-made integration for any ERC-1271 wallet. Extend this contract to add ComposableCow support:
import {ERC1271Forwarder} from "./ERC1271Forwarder.sol";
contract MyWallet is ERC1271Forwarder {
constructor(ComposableCow _composableCow) ERC1271Forwarder(_composableCow) {}
// ... wallet implementation
}The forwarder:
- Receives
isValidSignature(bytes32 _hash, bytes signature)calls. - Decodes the signature as
(GPv2Order.Data, ComposableCow.PayloadStruct). - Verifies that the order hash matches the provided hash.
- Forwards the request to
ComposableCow.isValidSafeSignature()for order validation.
For wallets that cannot extend ERC1271Forwarder, implement the forwarding manually:
- Decode the signature to extract
GPv2Order.DataandComposableCow.PayloadStruct. - Verify that
GPv2Order.hash(order, domainSeparator) == _hash. - Call
composableCow.isValidSafeSignature(owner, sender, hash, domainSeparator, typeHash, encodedOrder, encodedPayload).
- Extend
BaseConditionalOrder. - Implement
generateOrder():- Validate conditions using
require(condition, CustomError(reason)). - Declare reason errors at file level (e.g.,
error MyCondition();) and pass their selectors:require(ok, PollTryNextBlock(MyCondition.selector)). - Reject degenerate zero-amount orders with
OrderNotValid(ZeroAmount.selector). - Build and return
GPv2Order.Data.
- Validate conditions using
- Override
getNextPollTimestamp()if not using the default:- Return
POLL_AT_VALIDTO(0) for 'use validTo + 1'. - Return
POLL_NEVER(type(uint256).max) for single-shot orders. - Return a specific timestamp for multi-part orders.
- Return
- Optionally override
describeOrder()for better UX. - Override manifest functions if not single-shot:
getManifestInfo()— return the appropriate cardinality; validate parameters before computing counts.getManifestPage()— implement pagination for multi-part orders; respect the pagination contract (an empty page withhasMore == truemust be unreachable).- For UNBOUNDED orders, expose only index 0 and return
hasMore == falseon every branch.
| Operation | Settlement Path | Polling Path |
|---|---|---|
generateOrder() |
Yes | Yes |
| Hash verification | Yes | No |
getNextPollTimestamp() |
No | Yes |
describeOrder() |
No | Yes |
| Error reason selectors | Yes (bytes4 constants) | Yes (bytes4 constants) |
| Result struct construction | No | Yes |
The settlement path executes only what is necessary for validation. Error reason strings use compile-time constants to minimize gas overhead while providing useful debugging information.
This fork introduces significant architectural changes from cowprotocol/composable-cow. The following sections document all breaking changes for migration purposes.
| Change | Upstream | This Fork |
|---|---|---|
| Error renamed and retyped | PollTryAtEpoch(uint256, string) |
PollTryAtTimestamp(uint256, bytes4) |
| Error removed | PollNever(string) |
Was unused; use OrderNotValid for permanent conditions |
| Errors retyped | string reason payloads |
bytes4 reasonCode (selector of a handler-declared error) |
| Function added | - | generateOrder() (moved from IConditionalOrderGenerator) |
| Event changed | ConditionalOrderCreated(address indexed, ConditionalOrderParams) |
gains a bytes context parameter (topic0 changes) |
Migration: Replace PollTryAtEpoch with PollTryAtTimestamp, and replace revert PollNever(reason) with revert OrderNotValid(reason).
| Change | Upstream | This Fork |
|---|---|---|
| Function removed | getTradeableOrder() |
Use generateOrder() (in base interface) |
| Enum added | - | GeneratorResultCode |
| Struct added | - | GeneratorResult |
| Function added | - | poll() returning GeneratorResult |
| Function added | - | getNextPollTimestamp() |
| Function added | - | describeOrder() |
| Function added | - | tryGenerateOrder() returning full revert data |
| Error + verdict added | - | PollNeedsOffchainInput(bytes4) → NEEDS_INPUT |
| Struct changed | Proof { uint256 location; bytes data } |
Proof { string[] uris; bytes32[] blobVersionedHashes } (URI mirrors + in-tx blob verification; MerkleRootSet topic0 rotates) |
| Struct changed | PollResult { generator, fill, filledAmount } |
+ Restriction restriction overlay (INVALID uniformly terminal) |
| Behavior | setRoot(0, proof) unvalidated |
zero root requires an empty proof (ProofDataMalformed) |
| Interfaces added | - | IOrderDescriptor, IOrderModule sidecars (own ERC-165 ids; advertised only when committed) |
| Constructors changed | order types take no descriptor args | order types take (string[] descriptorUris, bytes32 descriptorDigest) |
The ERC-165 interface id of IConditionalOrderGenerator therefore changes: handlers deployed against the upstream interface will not satisfy the new id and are rejected by the polling path of a registry compiled against this fork.
Migration: Rename getTradeableOrder() to generateOrder(), and implement getNextPollTimestamp() and describeOrder() (or use the defaults from BaseConditionalOrder).
| Change | Upstream | This Fork |
|---|---|---|
| Return type changed | getTradeableOrderWithSignature() returns (GPv2Order.Data, bytes) |
returns (PollResult, bytes) |
| Function added | - | checkOrder() returns (PollResult) |
| Types added | - | FillStatus, PollResult |
| Event added | - | ConditionalOrderRemoved(address indexed, bytes32 indexed) |
| Events changed | MerkleRootSet, ConditionalOrderCreated |
gain a bytes context parameter (topic0 changes); cabinet written before emit |
| State added | - | settlement (CoWSettlement immutable) |
| Feature added | - | Fill overlay via GPv2Settlement.filledAmount() (incl. INVALIDATED detection) |
| Behavior changed | swap-guard restriction reverts on the polling path | returned as an INVALID verdict with reason "swap guard restricted" (settlement path still reverts) |
| Behavior changed | conditional-order errors revert through getTradeableOrderWithSignature |
decoded into the returned verdict; only authorization / interface failures revert |
Migration: Update callers of getTradeableOrderWithSignature() to handle the PollResult struct:
// Upstream
(GPv2Order.Data memory order, bytes memory sig) = composableCow.getTradeableOrderWithSignature(...);
// This fork
(ComposableCow.PollResult memory result, bytes memory sig) = composableCow.getTradeableOrderWithSignature(...);
if (
result.generator.code == IConditionalOrderGenerator.GeneratorResultCode.POST
&& sig.length > 0
) {
GPv2Order.Data memory order = result.generator.order;
// ... submit order
}| Change | Upstream | This Fork |
|---|---|---|
| Function renamed | getTradeableOrder() (abstract) |
generateOrder() (abstract) |
| Function added | - | poll() (concrete implementation) |
| Function added | - | getNextPollTimestamp() (virtual, default: POLL_AT_VALIDTO) |
| Function added | - | describeOrder() (virtual, default: "order ready") |
| Interface added | - | Implements IOrderManifest |
| Function added | - | getManifestInfo() (virtual, default: EXACT/1) |
| Function added | - | getManifestPage() (virtual, default: single entry with status) |
| Constant added | - | POLL_AT_VALIDTO = 0 |
| Constant added | - | POLL_NEVER = type(uint256).max |
Migration: Rename getTradeableOrder() to generateOrder(). The base class now provides a poll() implementation that wraps generateOrder() with try/catch.
interface IOrderManifest {
enum Cardinality { EXACT, CAPPED, UNBOUNDED }
struct ManifestInfo { Cardinality cardinality; uint256 totalOrders; }
struct ManifestEntry { uint256 index; GPv2Order.Data order; uint256 validFrom; bool isActive; }
struct ManifestStatus { GeneratorResultCode code; uint256 waitUntil; bytes4 reasonCode; }
function getManifestInfo(...) external view returns (ManifestInfo memory);
function getManifestPage(...) external view returns (ManifestEntry[] memory, bool hasMore, ManifestStatus memory status);
}Migration: No action is required for existing order types if extending BaseConditionalOrder (which provides a default single-shot implementation). Override for multi-part orders such as TWAP.
| Change | Upstream | This Fork |
|---|---|---|
| Function added | - | filledAmount(bytes orderUid) returns (uint256) |
This addition enables the fill overlay (FillStatus) in the registry poll path.
| Upstream | This Fork |
|---|---|
getTradeableOrder() |
generateOrder() |
PollTryAtEpoch |
PollTryAtTimestamp |
| Item | Replacement |
|---|---|
PollNever error |
OrderNotValid error (permanent conditions) |
GPv2Interaction re-export from IConditionalOrder.sol |
Import from GPv2Settlement.sol directly (was unused here) |