Skip to main content

Troubleshooting

This guide covers the most common issues developers hit when setting up ComposableCoW, based on frequent questions in the CoW Protocol Discord.

Setup issues

setDomainVerifier transaction reverts

The most common setup error. The call to setDomainVerifier must be made from the Safe to itself — not to the ExtensibleFallbackHandler.
setDomainVerifier is a function on the ExtensibleFallbackHandler, but it must be called via the Safe’s execTransaction. The Safe calls itself, and the fallback handler intercepts the call.
Correct flow:
  1. Safe executes a transaction where to = the Safe’s own address
  2. The call data encodes setDomainVerifier(domainSeparator, composableCowAddress)
  3. The ExtensibleFallbackHandler (set as the Safe’s fallback handler) processes it
Wrong: Calling setDomainVerifier directly on the ExtensibleFallbackHandler address — this will revert or have no effect.

Correct setup sequence

The setup must happen in this exact order:
  1. Set the fallback handler on the Safe to ExtensibleFallbackHandler (0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5)
  2. Register ComposableCoW as domain verifier by calling setDomainVerifier from the Safe to itself
  3. Create programmatic orders via ComposableCoW.create()
If step 1 is not done before step 2, the setDomainVerifier call will silently fail (no fallback handler to intercept it).

Verifying setup is correct

Check both configurations on-chain: 1. Verify fallback handler:
# Read the Safe's fallback handler slot
cast storage <SAFE_ADDRESS> \
  0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5
Expected: 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 (ExtensibleFallbackHandler) 2. Verify domain verifier:
# Call domainVerifiers on the Safe (via fallback handler)
cast call <SAFE_ADDRESS> \
  "domainVerifiers(address,bytes32)(address)" \
  <SAFE_ADDRESS> \
  <DOMAIN_SEPARATOR>
Expected: 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 (ComposableCoW) The domain separator for CoW Protocol is chain-specific. You can read it from the settlement contract:
cast call 0x9008D19f58AAbD9eD0D60971565AA8510560ab41 "domainSeparator()(bytes32)"

MultiSend atomic setup

To do both steps atomically in a single Safe transaction:
// Transaction 1: Set fallback handler
to: <SAFE_ADDRESS>
data: setFallbackHandler(0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5)

// Transaction 2: Register domain verifier (Safe calls itself)
to: <SAFE_ADDRESS>
data: setDomainVerifier(<DOMAIN_SEPARATOR>, 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74)
Bundle via Safe’s MultiSend contract.

Order not appearing in explorer

After creating a programmatic order, it should appear in CoW Explorer. If it doesn’t:

1. Check if the watch-tower picked it up

The watch-tower monitors ConditionalOrderCreated events. If it hasn’t indexed your order:
  • Was the transaction confirmed? Check the tx hash on the block explorer
  • Did the event fire? Look for ConditionalOrderCreated in the transaction logs
  • Is the watch-tower running for your chain? The public watch-tower covers Ethereum, Gnosis Chain, Arbitrum One, Base, Polygon, Avalanche, Lens, BNB Chain, Linea, Plasma, Ink, and Sepolia
  • Is there a delay? The watch-tower processes blocks periodically — wait a few minutes

2. Check if the order is currently valid

The watch-tower calls getTradeableOrder() on your handler. If it reverts, the order won’t be posted. Common reasons:
  • Balance insufficient — the Safe doesn’t hold enough sell tokens
  • Time condition not met — for TWAP, the current part hasn’t started yet
  • Guard rejection — a swap guard is blocking the order

3. Run your own watch-tower for debugging

# Clone and run with debug logging, monitoring only your Safe
git clone https://github.com/cowprotocol/watch-tower
cd watch-tower
yarn install

LOG_LEVEL=DEBUG yarn cli run \
  --config-path ./config.json \
  --only-owner <YOUR_SAFE_ADDRESS> \
  --one-shot
The --one-shot flag processes once and exits. --dry-run shows what orders would be posted without actually posting them.

Foundry / Forge dependency issues

When installing ComposableCoW with Forge, Safe library dependencies may not resolve automatically.

Missing SignatureVerifierMuxer.sol

forge install cowprotocol/composable-cow
If this fails to resolve Safe dependencies, add the remappings manually:
# foundry.toml or remappings.txt
[profile.default]
remappings = [
    "composable-cow/=lib/composable-cow/src/",
    "safe/=lib/composable-cow/lib/safe-contracts/contracts/",
    "safe-contracts/=lib/composable-cow/lib/safe-contracts/contracts/",
]
You may also need to install the Safe contracts explicitly:
cd lib/composable-cow
forge install safe-global/safe-contracts --no-commit

Order stopped being polled unexpectedly

If your programmatic order was being polled by the Watch Tower but silently stopped without being cancelled or fulfilled, the most likely cause is an unrecognized revert error from your handler contract.

Root cause

When the Watch Tower calls getTradeableOrderWithSignature on your handler and the call reverts, it attempts to decode the revert data against a set of known custom errors:
  • OrderNotValid(string)
  • PollTryNextBlock
  • PollTryAtBlock(uint256)
  • PollTryAtEpoch(uint256, uint256)
  • PollNever(string)
If the revert bytes do not match any of these, the Watch Tower classifies the result as UNEXPECTED_ERROR and permanently stops polling the order. There is no notification to the order creator.

Common triggers

  • Arithmetic overflow/underflow — Solidity 0.8+ reverts with a Panic(uint256) error code, which the Watch Tower does not recognize as a polling hint
  • Failed external calls — If your handler calls another contract that reverts with its own custom error, those bytes propagate up and won’t match the known set
  • Generic revert() or require(false) — These produce empty or non-standard revert data
  • Out-of-gas in sub-calls — Can produce unexpected revert behavior

Debugging steps

  1. Reproduce the revert locally. Call getTradeableOrderWithSignature with the same parameters the Watch Tower uses (owner, params, offchainInput, proof) using cast call or a Foundry test:
    cast call <COMPOSABLE_COW> \
      "getTradeableOrderWithSignature(address,(address,bytes32,bytes),bytes,bytes32[])" \
      <OWNER> "(handler,salt,staticInput)" "0x" "[]" \
      --rpc-url <RPC_URL>
    
  2. Inspect the revert data. If the call fails, check the raw revert bytes. Compare the first 4 bytes (the error selector) against the known selectors:
    • OrderNotValid: 0x16f2d3f3
    • PollTryNextBlock: 0x44882c68
    • PollTryAtBlock: 0x2d6e06d5
    • PollTryAtEpoch: 0x4184e129
  3. Check for arithmetic edge cases. Review your handler for divisions that could hit zero denominators, multiplications that could overflow, or timestamp arithmetic that could underflow.
  4. Run a local Watch Tower with --one-shot --only-owner <YOUR_SAFE> and LOG_LEVEL=DEBUG to see the exact error classification.

Best practice: defensive error handling

Wrap your handler’s core logic in a try/catch so that any unexpected failure maps to a recognized error:
function getTradeableOrder(
    address owner,
    address sender,
    bytes32 ctx,
    bytes calldata staticInput,
    bytes calldata offchainInput
) external view override returns (GPv2Order.Data memory order) {
    try this._getTradeableOrder(owner, sender, ctx, staticInput, offchainInput)
        returns (GPv2Order.Data memory result)
    {
        return result;
    } catch Error(string memory reason) {
        // Solidity require() / revert(string) — surface as OrderNotValid
        revert IConditionalOrder.OrderNotValid(reason);
    } catch (bytes memory) {
        // Panic, custom error from sub-call, or any other revert
        revert IConditionalOrder.OrderNotValid("unexpected internal error");
    }
}
This ensures the Watch Tower always receives a recognized error and continues polling the order on subsequent blocks.
This is a critical pattern for any custom handler. Without it, a single unexpected revert permanently removes your order from the Watch Tower’s polling cycle with no notification. See Watch Tower Polling — On-chain errors for full details.

Salt and replay protection

Each programmatic order needs a unique salt. If you reuse a salt for the same handler + parameters + Safe, the order will be a duplicate.
  • Use keccak256(abi.encodePacked(block.timestamp, nonce)) or similar for unique salts
  • The salt is used to compute the order’s storage key in ComposableCoW
  • Cancelling an order frees its salt for reuse

Contract addresses (all chains)

ContractAddress
ComposableCoW0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74
ExtensibleFallbackHandler0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5
GPv2Settlement0x9008D19f58AAbD9eD0D60971565AA8510560ab41
These addresses are the same on Ethereum, Gnosis Chain, Arbitrum One, Base, Polygon, Avalanche, Lens, BNB Chain, Linea, Plasma, Ink, and Sepolia.
Last modified on March 12, 2026