Skip to main content

System Overview

ComposableCoW is built on a modular architecture that separates order authorization, validation, and execution. The system consists of several key components that work together to enable automated conditional trading on CoW Protocol.

Core Components

ComposableCoW Contract

The ComposableCoW contract is the central coordinator of the system. It implements the ISafeSignatureVerifier interface and manages order authorization and validation. Source: src/ComposableCoW.sol:23

Key Responsibilities

ComposableCoW tracks which conditional orders are authorized for each Safe:
// Mapping of owner's single orders
mapping(address => mapping(bytes32 => bool)) public singleOrders;

// Mapping of owner's merkle roots
mapping(address => bytes32) public roots;
Single Order Authorization:
function create(
    IConditionalOrder.ConditionalOrderParams calldata params, 
    bool dispatch
) public {
    if (!(address(params.handler) != address(0))) {
        revert InvalidHandler();
    }
    
    singleOrders[msg.sender][hash(params)] = true;
    
    if (dispatch) {
        emit ConditionalOrderCreated(msg.sender, params);
    }
}
Merkle Root Authorization:
function setRoot(bytes32 root, Proof calldata proof) public {
    roots[msg.sender] = root;
    emit MerkleRootSet(msg.sender, root, proof);
}
ComposableCoW validates orders through the EIP-1271 signature verification flow:
function isValidSafeSignature(
    Safe safe,
    address sender,
    bytes32 _hash,
    bytes32 _domainSeparator,
    bytes32, // typeHash
    bytes calldata encodeData,
    bytes calldata payload
) external view override returns (bytes4 magic) {
    // Decode payload
    PayloadStruct memory _payload = abi.decode(payload, (PayloadStruct));
    
    // Check authorization (single order or merkle proof)
    bytes32 ctx = _auth(address(safe), _payload.params, _payload.proof);
    
    // Decode the order
    GPv2Order.Data memory order = abi.decode(encodeData, (GPv2Order.Data));
    
    // Check swap guard if configured
    if (!(_guardCheck(address(safe), ctx, _payload.params, _payload.offchainInput, order))) {
        revert SwapGuardRestricted();
    }
    
    // Verify with the handler
    _payload.params.handler.verify(
        address(safe),
        sender,
        _hash,
        _domainSeparator,
        ctx,
        _payload.params.staticInput,
        _payload.offchainInput,
        order
    );
    
    return ERC1271.isValidSignature.selector;
}
For watchtowers and integrators, ComposableCoW provides a view function to generate tradeable orders:
function getTradeableOrderWithSignature(
    address owner,
    IConditionalOrder.ConditionalOrderParams calldata params,
    bytes calldata offchainInput,
    bytes32[] calldata proof
) external view returns (
    GPv2Order.Data memory order, 
    bytes memory signature
) {
    // Check authorization
    bytes32 ctx = _auth(owner, params, proof);
    
    // Ensure handler supports IConditionalOrderGenerator
    try IConditionalOrderGenerator(address(params.handler))
        .supportsInterface(type(IConditionalOrderGenerator).interfaceId) 
        returns (bool supported) {
        if (!supported) {
            revert InterfaceNotSupported();
        }
    } catch {
        revert InterfaceNotSupported();
    }
    
    // Generate the order
    order = IConditionalOrderGenerator(address(params.handler))
        .getTradeableOrder(owner, msg.sender, ctx, params.staticInput, offchainInput);
    
    // Check swap guard
    if (!(_guardCheck(owner, ctx, params, offchainInput, order))) {
        revert SwapGuardRestricted();
    }
    
    // Build signature for EIP-1271 verification
    signature = abi.encode(
        order, 
        PayloadStruct({params: params, offchainInput: offchainInput, proof: proof})
    );
}
ComposableCoW includes an on-chain storage system called “cabinet” for storing context values:
// Mapping of owner's on-chain storage slots
mapping(address => mapping(bytes32 => bytes32)) public cabinet;

function createWithContext(
    IConditionalOrder.ConditionalOrderParams calldata params,
    IValueFactory factory,
    bytes calldata data,
    bool dispatch
) external {
    create(params, dispatch);
    
    // Store the value from the factory
    cabinet[msg.sender][hash(params)] = factory.getValue(data);
}
This enables orders to reference dynamic values like creation timestamps:
// In a handler contract
uint256 t0 = uint256(composableCow.cabinet(owner, ctx));

ExtensibleFallbackHandler

The ExtensibleFallbackHandler is a Safe fallback handler that enables EIP-1271 signature verification with domain-specific verifiers. Deployment: 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 (all networks)

How It Works

// Set ComposableCoW as verifier for CoW Protocol's domain
ISignatureVerifierMuxer(address(safe)).setDomainVerifier(
    cowProtocolDomainSeparator,
    address(composableCow)
);
When CoW Protocol’s settlement contract calls isValidSignature() on the Safe:
  1. The call is forwarded to ExtensibleFallbackHandler
  2. The handler checks which verifier is registered for the CoW Protocol domain
  3. It delegates to ComposableCoW.isValidSafeSignature()
  4. ComposableCoW validates the conditional order and returns the magic value

Order Handlers

Order handlers implement the conditional logic for different order types. All handlers implement the IConditionalOrder interface. Source: src/interfaces/IConditionalOrder.sol

Handler Interface

interface IConditionalOrder {
    struct ConditionalOrderParams {
        IConditionalOrder handler;  // Address of the handler contract
        bytes32 salt;               // Unique identifier
        bytes staticInput;          // Order-specific parameters
    }

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

Order Generator Interface

Handlers that can generate orders implement IConditionalOrderGenerator:
interface IConditionalOrderGenerator is IConditionalOrder {
    function getTradeableOrder(
        address owner,
        address sender,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput
    ) external view returns (GPv2Order.Data memory);
}

BaseConditionalOrder

Most handlers extend BaseConditionalOrder which provides default verification logic: Source: src/BaseConditionalOrder.sol
abstract contract BaseConditionalOrder is IConditionalOrderGenerator {
    function verify(
        address owner,
        address sender,
        bytes32 _hash,
        bytes32 domainSeparator,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput,
        GPv2Order.Data calldata
    ) external view override {
        // Generate the order
        GPv2Order.Data memory generatedOrder = 
            getTradeableOrder(owner, sender, ctx, staticInput, offchainInput);
        
        // Verify the hash matches
        if (!(_hash == GPv2Order.hash(generatedOrder, domainSeparator))) {
            revert IConditionalOrder.OrderNotValid(INVALID_HASH);
        }
    }
    
    function getTradeableOrder(
        address owner,
        address sender,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput
    ) public view virtual override returns (GPv2Order.Data memory);
}

TWAP Handler

The TWAP handler splits an order into time-based parts. Source: src/types/twap/TWAP.sol Deployment: 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5
contract TWAP is BaseConditionalOrder {
    ComposableCoW public immutable composableCow;

    function getTradeableOrder(
        address owner, 
        address, 
        bytes32 ctx, 
        bytes calldata staticInput, 
        bytes calldata
    ) public view override returns (GPv2Order.Data memory order) {
        TWAPOrder.Data memory twap = abi.decode(staticInput, (TWAPOrder.Data));
        
        // If t0 is 0, get start time from cabinet
        if (twap.t0 == 0) {
            twap.t0 = uint256(composableCow.cabinet(owner, ctx));
        }
        
        // Generate the order for the current time window
        order = TWAPOrder.orderFor(twap);
        
        // Ensure order is within valid span
        if (!(block.timestamp <= order.validTo)) {
            revert IConditionalOrder.OrderNotValid(NOT_WITHIN_SPAN);
        }
    }
}

TWAP Order Logic

Source: src/types/twap/libraries/TWAPOrder.sol
library TWAPOrder {
    struct Data {
        IERC20 sellToken;
        IERC20 buyToken;
        address receiver;
        uint256 partSellAmount;  // Amount per part
        uint256 minPartLimit;    // Min buy amount per part
        uint256 t0;              // Start time
        uint256 n;               // Number of parts
        uint256 t;               // Interval between parts
        uint256 span;            // Valid duration within each interval
        bytes32 appData;
    }

    function orderFor(Data memory self) internal view 
        returns (GPv2Order.Data memory order) {
        validate(self);
        
        order = GPv2Order.Data({
            sellToken: self.sellToken,
            buyToken: self.buyToken,
            receiver: self.receiver,
            sellAmount: self.partSellAmount,
            buyAmount: self.minPartLimit,
            validTo: TWAPOrderMathLib.calculateValidTo(
                self.t0, self.n, self.t, self.span
            ).toUint32(),
            appData: self.appData,
            feeAmount: 0,
            kind: GPv2Order.KIND_SELL,
            partiallyFillable: false,
            sellTokenBalance: GPv2Order.BALANCE_ERC20,
            buyTokenBalance: GPv2Order.BALANCE_ERC20
        });
    }
}

Swap Guards

Swap guards provide additional validation logic for orders. Source: src/interfaces/ISwapGuard.sol
interface ISwapGuard {
    function verify(
        GPv2Order.Data calldata order,
        bytes32 ctx,
        IConditionalOrder.ConditionalOrderParams calldata params,
        bytes calldata offchainInput
    ) external view returns (bool);
}

Setting a Swap Guard

composableCow.setSwapGuard(ISwapGuard(guardAddress));
Example use cases:
  • Restrict trading to specific token pairs
  • Enforce maximum slippage limits
  • Require minimum time between trades
  • Implement multi-signature approval for large trades

Data Flow

Order Creation Flow

Order Execution Flow

Authorization Modes

Single Orders

Best for:
  • Small number of conditional orders (< 10)
  • Frequently updated orders
  • Orders that need individual management
// Create single order
composableCow.create(params, true);

// Check if authorized
bool isAuthorized = composableCow.singleOrders(
    address(safe), 
    keccak256(abi.encode(params))
);

// Remove authorization
composableCow.remove(keccak256(abi.encode(params)));

Merkle Trees

Best for:
  • Large number of conditional orders (> 10)
  • Static order sets
  • Gas-efficient batch operations
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

// Build merkle tree from order params
function buildMerkleTree(
    IConditionalOrder.ConditionalOrderParams[] memory orders
) internal pure returns (bytes32 root) {
    bytes32[] memory leaves = new bytes32[](orders.length);
    
    for (uint256 i = 0; i < orders.length; i++) {
        // Double hash each leaf
        leaves[i] = keccak256(abi.encodePacked(
            keccak256(abi.encode(orders[i]))
        ));
    }
    
    // Build tree and return root
    root = calculateRoot(leaves);
}

// Set root
composableCow.setRoot(root, ComposableCoW.Proof({location: 0, data: ""}));

// Get tradeable order with proof
(GPv2Order.Data memory order, bytes memory signature) = 
    composableCow.getTradeableOrderWithSignature(
        address(safe),
        params,
        offchainInput,
        proof  // Merkle proof bytes32[]
    );

Error Handling

Handlers use specific error types to communicate with watchtowers:
interface IConditionalOrder {
    // Order is invalid and should not be retried
    error OrderNotValid(string reason);
    
    // Retry in next block
    error PollTryNextBlock(string reason);
    
    // Retry at specific block
    error PollTryAtBlock(uint256 blockNumber, string reason);
    
    // Retry at specific timestamp
    error PollTryAtEpoch(uint256 timestamp, string reason);
    
    // Delete order, never retry
    error PollNever(string reason);
}
Example usage in a handler:
function getTradeableOrder(
    address owner,
    address,
    bytes32,
    bytes calldata staticInput,
    bytes calldata
) public view override returns (GPv2Order.Data memory) {
    Data memory data = abi.decode(staticInput, (Data));
    
    // Check if order can be executed yet
    if (block.timestamp < data.startTime) {
        revert IConditionalOrder.PollTryAtEpoch(
            data.startTime, 
            "too early"
        );
    }
    
    // Check if order expired
    if (block.timestamp > data.endTime) {
        revert IConditionalOrder.PollNever("order expired");
    }
    
    // Check balance
    if (data.sellToken.balanceOf(owner) < data.minBalance) {
        revert IConditionalOrder.OrderNotValid("insufficient balance");
    }
    
    // Generate and return order
    // ...
}

Security Considerations

Authorization Verification

ComposableCoW verifies authorization before calling handlers:
function _auth(
    address owner,
    IConditionalOrder.ConditionalOrderParams memory params,
    bytes32[] memory proof
) internal view returns (bytes32 ctx) {
    if (proof.length != 0) {
        // Merkle proof verification
        bytes32 leaf = keccak256(bytes.concat(hash(params)));
        if (!MerkleProof.verify(proof, roots[owner], leaf)) {
            revert ProofNotAuthed();
        }
    } else {
        // Single order verification
        ctx = hash(params);
        if (!singleOrders[owner][ctx]) {
            revert SingleOrderNotAuthed();
        }
    }
}

Handler Validation

Handlers must validate all inputs and ensure generated orders are correct:
  1. Input Validation: Check all parameters are within acceptable ranges
  2. Hash Verification: Ensure generated order hash matches expected hash
  3. Reentrancy Protection: Use view functions only, no state changes
  4. Oracle Validation: Verify oracle data is fresh and valid

Safe Configuration

Proper Safe setup is critical:
  1. Fallback Handler: Must be ExtensibleFallbackHandler
  2. Domain Verifier: Must be set to ComposableCoW for CoW Protocol domain
  3. Token Approvals: Only approve necessary amounts to GPv2VaultRelayer

Gas Optimization

Stateless Design

Conditional orders are stateless - all validation happens in view functions. This means:
  • No gas cost for unsuccessful order checks
  • Only pay gas when orders actually execute
  • Watchtowers can poll frequently without cost to users

Merkle Trees

For many orders, Merkle trees provide significant gas savings:
  • Single setRoot() call authorizes unlimited orders
  • Update root to add/remove multiple orders atomically
  • Proof verification is O(log n) complexity

Cabinet Storage

The cabinet storage system is optimized for common patterns:
  • Default slot (bytes32(0)) saves calldata for Merkle roots
  • Values stored only when needed, reducing unnecessary storage costs

Next Steps

Order Types

Explore all available conditional order types

Custom Handlers

Learn how to build your own conditional order handlers

Build docs developers (and LLMs) love