Skip to main content

Prerequisites

Before you begin, ensure you have:
  • A Safe multisig wallet (if you don’t have one, create it at safe.global)
  • Tokens to trade and enough ETH for gas fees
  • Basic understanding of Ethereum transactions and Safe operations

Setup Your Safe

To use ComposableCoW, your Safe must be configured with the proper fallback handler and domain verifier.
1

Set the Fallback Handler

Your Safe needs to use ExtensibleFallbackHandler to support EIP-1271 signature verification with ComposableCoW.
// ExtensibleFallbackHandler address (same on all supported networks)
address handler = 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5;

// From your Safe, call setFallbackHandler
safe.setFallbackHandler(handler);
If you’re creating a new Safe, you can set the fallback handler during deployment. For existing Safes, use the Safe UI to propose and execute this transaction.
2

Set the Domain Verifier

Configure ComposableCoW as the signature verifier for CoW Protocol’s domain.
// ComposableCoW address (same on all supported networks)
address composableCow = 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74;

// Get GPv2Settlement domain separator
bytes32 domainSeparator = GPv2Settlement.domainSeparator();

// Set ComposableCoW as the domain verifier
ISignatureVerifierMuxer(address(safe)).setDomainVerifier(
    domainSeparator,
    composableCow
);
This step is critical. Without setting ComposableCoW as the domain verifier, your conditional orders will not be validated correctly.

Create Your First Conditional Order

Let’s create a TWAP (Time-Weighted Average Price) order to split a large trade into smaller parts executed over time.

Example: 30-Day TWAP

Alice wants to sell 12,000,000 DAI for at least 7,500 WETH over 30 days, executing one part per day.
1

Define the TWAP Parameters

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {TWAPOrder} from "composable-cow/src/types/twap/libraries/TWAPOrder.sol";

TWAPOrder.Data memory twapData = TWAPOrder.Data({
    sellToken: IERC20(DAI_ADDRESS),
    buyToken: IERC20(WETH_ADDRESS),
    receiver: address(0),              // address(0) means Safe receives
    partSellAmount: 400_000 * 1e18,    // 400,000 DAI per part
    minPartLimit: 250 * 1e18,          // 250 WETH minimum per part
    t0: block.timestamp,               // Start now
    n: 30,                             // 30 parts total
    t: 86400,                          // 86400 seconds (1 day) between parts
    span: 0,                           // 0 = valid for entire interval
    appData: keccak256("my-twap-order")
});
Parameters explained:
  • partSellAmount: Amount to sell in each individual trade (12M DAI ÷ 30 = 400K DAI)
  • minPartLimit: Minimum amount to receive per trade (7,500 WETH ÷ 30 = 250 WETH)
  • t0: Start time (unix timestamp)
  • n: Number of parts
  • t: Time interval between parts (in seconds)
  • span: Duration within each interval when order is valid (0 = entire interval)
2

Create ConditionalOrderParams

import {IConditionalOrder} from "composable-cow/src/interfaces/IConditionalOrder.sol";
import {ComposableCoW} from "composable-cow/src/ComposableCoW.sol";

// TWAP handler address (same on all networks)
address twapHandler = 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5;

IConditionalOrder.ConditionalOrderParams memory params = 
    IConditionalOrder.ConditionalOrderParams({
        handler: IConditionalOrder(twapHandler),
        salt: keccak256(abi.encodePacked("alice-dai-weth-twap")),
        staticInput: abi.encode(twapData)
    });
The combination of handler, salt, and staticInput must be unique for your Safe. Use a descriptive salt to avoid collisions with other orders.
3

Approve Token Spending

Grant the CoW Protocol vault relayer permission to spend your tokens.
// GPv2VaultRelayer address
address vaultRelayer = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110;

// Approve the total amount needed (30 parts × 400,000 DAI)
IERC20(DAI_ADDRESS).approve(vaultRelayer, 12_000_000 * 1e18);
This approval transaction must be executed by your Safe before creating the conditional order.
4

Create the Order

ComposableCoW composableCow = ComposableCoW(0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74);

// Create the order with dispatch = true to notify watchtowers
composableCow.create(params, true);
Setting dispatch = true emits a ConditionalOrderCreated event that watchtowers monitor. They will automatically submit orders to CoW Protocol when conditions are met.
5

Execute as Batched Transaction

For the best user experience, batch the approval and order creation into a single Safe transaction:
import {MultiSend} from "safe/libraries/MultiSend.sol";
import {Enum} from "safe/common/Enum.sol";

// Encode the approval call
bytes memory approvalCall = abi.encodeCall(
    IERC20.approve,
    (vaultRelayer, 12_000_000 * 1e18)
);

// Encode the create call
bytes memory createCall = abi.encodeCall(
    ComposableCoW.create,
    (params, true)
);

// Build MultiSend transaction
bytes memory multiSendData = abi.encodePacked(
    uint8(Enum.Operation.Call),
    DAI_ADDRESS,
    uint256(0),
    uint256(approvalCall.length),
    approvalCall,
    uint8(Enum.Operation.Call),
    address(composableCow),
    uint256(0),
    uint256(createCall.length),
    createCall
);

// Execute via MultiSend
safe.execTransaction(
    MULTISEND_ADDRESS,
    0,
    abi.encodeCall(MultiSend.multiSend, multiSendData),
    Enum.Operation.DelegateCall,
    // ... other Safe transaction parameters
);

Monitoring Your Order

Once created, you can monitor your conditional order’s execution:
1

View on CoW Explorer

Visit CoW Explorer and search for your Safe address to see all executed trades.
2

Query Order Status

Check if your conditional order can generate a tradeable order:
try composableCow.getTradeableOrderWithSignature(
    address(yourSafe),
    params,
    bytes(""),  // offchainInput (empty for TWAP)
    new bytes32[](0)  // proof (empty for single orders)
) returns (GPv2Order.Data memory order, bytes memory signature) {
    // Order is valid and can be submitted
    console.log("Valid order:");
    console.log("Sell amount:", order.sellAmount);
    console.log("Buy amount:", order.buyAmount);
    console.log("Valid until:", order.validTo);
} catch Error(string memory reason) {
    // Order conditions not met
    console.log("Order not valid:", reason);
}

Advanced: Using Time Spans

To restrict when orders can execute within each interval, use the span parameter. For example, to only trade during business hours:
TWAPOrder.Data memory businessHoursTwap = TWAPOrder.Data({
    sellToken: IERC20(DAI_ADDRESS),
    buyToken: IERC20(WETH_ADDRESS),
    receiver: address(0),
    partSellAmount: 400_000 * 1e18,
    minPartLimit: 250 * 1e18,
    t0: block.timestamp,
    n: 30,
    t: 86400,              // 24 hour intervals
    span: 28800,           // Only valid for first 8 hours of each day
    appData: keccak256("business-hours-twap")
});
This example sets span = 28800 (8 hours), so each part is only valid from t0 + (i × t) to t0 + (i × t) + span, allowing weekend-only or weekday-only trading strategies.

Canceling an Order

To cancel a single conditional order:
// Calculate the order hash
bytes32 orderHash = keccak256(abi.encode(params));

// Remove the order
composableCow.remove(orderHash);
Canceling an order does not revoke token approvals. If you want to prevent any token transfers, revoke the GPv2VaultRelayer approval separately.

Next Steps

Architecture

Learn how ComposableCoW components work together

Order Types

Explore other conditional order types like Stop Loss and Good After Time

Example: Creating a Stop Loss Order

Here’s a quick example of creating a different order type:
import {StopLoss} from "composable-cow/src/types/StopLoss.sol";

// StopLoss handler address
address stopLossHandler = 0x412c36e5011cd2517016d243a2dfb37f73a242e7;

StopLoss.Data memory stopLossData = StopLoss.Data({
    sellToken: IERC20(GNO_ADDRESS),
    buyToken: IERC20(USDC_ADDRESS),
    sellAmount: 1000 * 1e18,
    buyAmount: 100_000 * 1e6,
    appData: bytes32(0),
    receiver: address(0),
    isSellOrder: true,
    isPartiallyFillable: false,
    validTo: uint32(block.timestamp + 30 days),
    sellTokenPriceOracle: IAggregatorV3Interface(GNO_USD_ORACLE),
    buyTokenPriceOracle: IAggregatorV3Interface(USDC_USD_ORACLE),
    strike: 95 * 1e18,  // Trigger if GNO falls below $95
    maxTimeSinceLastOracleUpdate: 3600  // 1 hour
});

IConditionalOrder.ConditionalOrderParams memory stopLossParams = 
    IConditionalOrder.ConditionalOrderParams({
        handler: IConditionalOrder(stopLossHandler),
        salt: keccak256("my-stop-loss"),
        staticInput: abi.encode(stopLossData)
    });

// Create the stop loss order
composableCow.create(stopLossParams, true);
This stop loss order will automatically trigger when the GNO price falls below $95, protecting your position from further losses.

Build docs developers (and LLMs) love