Skip to main content

Overview

Custom conditional orders are the core of ComposableCoW’s flexibility. By implementing the IConditionalOrder or IConditionalOrderGenerator interface, you can create sophisticated trading strategies that execute based on on-chain conditions.
Most custom orders should extend BaseConditionalOrder, which handles signature verification and implements IConditionalOrderGenerator for you.

Understanding the Interfaces

IConditionalOrder

The base interface that all conditional orders must implement:
interface IConditionalOrder {
    /**
     * Verify if a given discrete order is valid.
     * @param owner the contract who is the owner of the order
     * @param sender the `msg.sender` of the transaction
     * @param _hash the hash of the order
     * @param domainSeparator the domain separator used to sign the order
     * @param ctx the context key (bytes32(0) if merkle tree, otherwise H(params))
     * @param staticInput the static input for all discrete orders
     * @param offchainInput dynamic off-chain input for this discrete order
     * @param order GPv2Order.Data of the discrete order to be verified
     */
    function verify(
        address owner,
        address sender,
        bytes32 _hash,
        bytes32 domainSeparator,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput,
        GPv2Order.Data calldata order
    ) external view;
}

IConditionalOrderGenerator

Extends IConditionalOrder to generate orders on-chain:
interface IConditionalOrderGenerator is IConditionalOrder, IERC165 {
    /**
     * Get a tradeable order that can be posted to the CoW Protocol API.
     * @param owner the contract who is the owner of the order
     * @param sender the `msg.sender` of the parent `isValidSignature` call
     * @param ctx the context (bytes32(0) if merkle tree, otherwise H(params))
     * @param staticInput the static input for all discrete orders
     * @param offchainInput dynamic off-chain input for this discrete order
     * @return the tradeable order for submission to CoW Protocol API
     */
    function getTradeableOrder(
        address owner,
        address sender,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput
    ) external view returns (GPv2Order.Data memory);
}

BaseConditionalOrder

The BaseConditionalOrder abstract contract simplifies implementation:
abstract contract BaseConditionalOrder is IConditionalOrderGenerator {
    /**
     * @dev Automatically verifies that the generated order matches the hash
     */
    function verify(
        address owner,
        address sender,
        bytes32 _hash,
        bytes32 domainSeparator,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput,
        GPv2Order.Data calldata
    ) external view override {
        GPv2Order.Data memory generatedOrder = getTradeableOrder(
            owner, sender, ctx, staticInput, offchainInput
        );
        
        if (!(_hash == GPv2Order.hash(generatedOrder, domainSeparator))) {
            revert IConditionalOrder.OrderNotValid("invalid hash");
        }
    }
    
    /**
     * @dev Implement this function to define your order logic
     */
    function getTradeableOrder(
        address owner,
        address sender,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput
    ) public view virtual override returns (GPv2Order.Data memory);
    
    function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
        return interfaceId == type(IConditionalOrderGenerator).interfaceId 
            || interfaceId == type(IERC165).interfaceId;
    }
}

Why Extend BaseConditionalOrder?

  • Automatic order hash verification
  • ERC165 interface support included
  • Focus on order generation logic only
  • Battle-tested verification flow

Example 1: TradeAboveThreshold

This order type trades when the owner’s balance exceeds a threshold:
contract TradeAboveThreshold is BaseConditionalOrder {
    struct Data {
        IERC20 sellToken;
        IERC20 buyToken;
        address receiver;
        uint32 validityBucketSeconds;
        uint256 threshold;
        bytes32 appData;
    }
    
    function getTradeableOrder(
        address owner,
        address,
        bytes32,
        bytes calldata staticInput,
        bytes calldata
    ) public view override returns (GPv2Order.Data memory order) {
        // Decode the static input
        TradeAboveThreshold.Data memory data = abi.decode(staticInput, (Data));
        
        // Check if the condition is met
        uint256 balance = data.sellToken.balanceOf(owner);
        if (!(balance >= data.threshold)) {
            revert IConditionalOrder.PollTryNextBlock("balance insufficient");
        }
        
        // Generate the order
        order = GPv2Order.Data({
            sellToken: data.sellToken,
            buyToken: data.buyToken,
            receiver: data.receiver,
            sellAmount: balance, // Sell entire balance
            buyAmount: 1, // Minimum buy (market order)
            validTo: validToBucket(data.validityBucketSeconds),
            appData: data.appData,
            feeAmount: 0,
            kind: GPv2Order.KIND_SELL,
            partiallyFillable: false,
            sellTokenBalance: GPv2Order.BALANCE_ERC20,
            buyTokenBalance: GPv2Order.BALANCE_ERC20
        });
    }
}

Key Concepts

1

Define your data structure

Create a struct with all the parameters your order needs. This gets ABI-encoded as staticInput.
2

Decode staticInput

Decode the staticInput bytes into your data struct.
3

Check conditions

Verify that the order should be tradeable. If not, revert with an appropriate error.
4

Generate the order

Construct and return a GPv2Order.Data struct with the order details.

Example 2: PerpetualStableSwap

A more complex example that perpetually swaps between two tokens:
contract PerpetualStableSwap is BaseConditionalOrder {
    struct Data {
        IERC20 tokenA;
        IERC20 tokenB;
        uint32 validityBucketSeconds;
        uint256 halfSpreadBps; // Basis points markup
        bytes32 appData;
    }
    
    function getTradeableOrder(
        address owner,
        address,
        bytes32,
        bytes calldata staticInput,
        bytes calldata
    ) public view override returns (GPv2Order.Data memory order) {
        Data memory data = abi.decode(staticInput, (Data));
        
        // Determine which token to sell based on balances
        uint256 balanceA = data.tokenA.balanceOf(owner);
        uint256 balanceB = data.tokenB.balanceOf(owner);
        
        IERC20 sellToken;
        IERC20 buyToken;
        uint256 sellAmount;
        uint256 buyAmount;
        
        if (balanceA > balanceB) {
            sellToken = data.tokenA;
            buyToken = data.tokenB;
            sellAmount = balanceA - balanceB;
            // Apply spread
            buyAmount = (sellAmount * (10000 - data.halfSpreadBps)) / 10000;
        } else {
            sellToken = data.tokenB;
            buyToken = data.tokenA;
            sellAmount = balanceB - balanceA;
            buyAmount = (sellAmount * (10000 - data.halfSpreadBps)) / 10000;
        }
        
        // Ensure the order is funded
        if (!(sellAmount > 0)) {
            revert IConditionalOrder.OrderNotValid("not funded");
        }
        
        order = GPv2Order.Data({
            sellToken: sellToken,
            buyToken: buyToken,
            receiver: address(0), // Self
            sellAmount: sellAmount,
            buyAmount: buyAmount,
            validTo: validToBucket(data.validityBucketSeconds),
            appData: data.appData,
            feeAmount: 0,
            kind: GPv2Order.KIND_SELL,
            partiallyFillable: false,
            sellTokenBalance: GPv2Order.BALANCE_ERC20,
            buyTokenBalance: GPv2Order.BALANCE_ERC20
        });
    }
}

Error Handling for Watchtowers

Use specific revert errors to communicate with watchtowers:
// Order is not valid right now, try again next block
revert IConditionalOrder.PollTryNextBlock("balance insufficient");

// Order will be valid at a specific block
revert IConditionalOrder.PollTryAtBlock(blockNumber, "wait for oracle update");

// Order will be valid at a specific timestamp
revert IConditionalOrder.PollTryAtEpoch(timestamp, "order still locked");

// Order should never be polled again (permanent)
revert IConditionalOrder.PollNever("expired");

// Generic invalid order
revert IConditionalOrder.OrderNotValid("condition not met");
Choose the right error type! PollNever tells watchtowers to stop monitoring this order permanently. Use PollTryNextBlock for temporary conditions.

Using Context and Cabinet

Access stored values from Value Factories:
function getTradeableOrder(
    address owner,
    address,
    bytes32 ctx,
    bytes calldata staticInput,
    bytes calldata
) public view override returns (GPv2Order.Data memory) {
    // Access the ComposableCoW contract
    ComposableCoW composableCow = ComposableCoW(msg.sender);
    
    // Read the cabinet value (e.g., creation timestamp)
    bytes32 storedValue = composableCow.cabinet(owner, ctx);
    uint256 creationTime = uint256(storedValue);
    
    // Use the stored value in your logic
    uint256 timePassed = block.timestamp - creationTime;
    
    // ... rest of your order logic
}

Using Off-chain Input

The offchainInput parameter allows watchtowers to provide dynamic data:
function getTradeableOrder(
    address owner,
    address,
    bytes32,
    bytes calldata staticInput,
    bytes calldata offchainInput
) public view override returns (GPv2Order.Data memory) {
    Data memory data = abi.decode(staticInput, (Data));
    
    // Decode off-chain input (e.g., oracle signature, proof)
    if (offchainInput.length > 0) {
        (uint256 oraclePrice, bytes memory signature) = abi.decode(
            offchainInput,
            (uint256, bytes)
        );
        
        // Verify and use the oracle price
        // ...
    }
    
    // ... generate order
}

Validity Bucketing

Use validity buckets to prevent order hash collisions:
function validToBucket(uint32 bucketSeconds) internal view returns (uint32) {
    uint32 currentTime = uint32(block.timestamp);
    return currentTime - (currentTime % bucketSeconds) + bucketSeconds;
}
This ensures orders queried at different times within the same bucket have identical hashes.
Common bucket sizes:
  • 60 seconds: Frequently updating orders
  • 300 seconds (5 min): Standard use case
  • 3600 seconds (1 hour): Slowly changing orders

Testing Your Custom Order

1

Deploy your handler

Deploy your conditional order contract to your target network.
2

Encode your parameters

Prepare the staticInput by encoding your data struct:
bytes memory staticInput = abi.encode(Data({
    sellToken: IERC20(tokenAddress),
    buyToken: IERC20(otherTokenAddress),
    // ... other parameters
}));
3

Create the conditional order

Call ComposableCoW.create with your parameters:
IConditionalOrder.ConditionalOrderParams memory params = 
    IConditionalOrder.ConditionalOrderParams({
        handler: IConditionalOrder(handlerAddress),
        salt: bytes32(uint256(1)),
        staticInput: staticInput
    });

composableCow.create(params, true);
4

Test order generation

Call getTradeableOrderWithSignature to verify your handler works:
(GPv2Order.Data memory order, bytes memory signature) = 
    composableCow.getTradeableOrderWithSignature(
        owner,
        params,
        "", // offchainInput
        new bytes32[](0) // proof (empty for single orders)
    );

Best Practices

Gas Efficiency

Keep getTradeableOrder gas-efficient - it’s called frequently by watchtowers

Deterministic Logic

Same inputs should always produce the same output within a validity bucket

Clear Error Messages

Use descriptive revert messages to help debugging

Interface Support

Implement ERC165 properly (automatic with BaseConditionalOrder)
Security Considerations:
  • Validate all external calls and oracle data
  • Be careful with arithmetic (use SafeMath if needed)
  • Consider reentrancy implications
  • Test edge cases thoroughly

Advanced: Non-Generator Orders

If you need more control, implement IConditionalOrder directly without getTradeableOrder. This requires the discrete order to be provided off-chain, and you only verify it:
contract CustomVerifyOnly is IConditionalOrder, IERC165 {
    function verify(
        address owner,
        address sender,
        bytes32 _hash,
        bytes32 domainSeparator,
        bytes32 ctx,
        bytes calldata staticInput,
        bytes calldata offchainInput,
        GPv2Order.Data calldata order
    ) external view override {
        // Custom verification logic
        // Check if the provided order meets your conditions
        
        // Verify the hash matches
        require(
            _hash == GPv2Order.hash(order, domainSeparator),
            "hash mismatch"
        );
        
        // Your custom checks...
    }
    
    function supportsInterface(bytes4 interfaceId) external view override returns (bool) {
        return interfaceId == type(IConditionalOrder).interfaceId
            || interfaceId == type(IERC165).interfaceId;
    }
}

Parameter Types

ParameterTypePurpose
owneraddressThe Safe that owns this order
senderaddressThe msg.sender calling isValidSignature
ctxbytes32Context key for cabinet lookups
staticInputbytesYour encoded parameters (set at creation)
offchainInputbytesDynamic data from watchtower (changes per query)

Next Steps

Swap Guards

Add restrictions to your custom orders

Value Factories

Store on-chain state for your orders

Architecture

Learn how orders are monitored and submitted

Build docs developers (and LLMs) love