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:
- Safe executes a transaction where
to = the Safe’s own address
- The call data encodes
setDomainVerifier(domainSeparator, composableCowAddress)
- 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:
- Set the fallback handler on the Safe to ExtensibleFallbackHandler (
0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5)
- Register ComposableCoW as domain verifier by calling
setDomainVerifier from the Safe to itself
- 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
-
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>
-
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
-
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.
-
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)
| Contract | Address |
|---|
| ComposableCoW | 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 |
| ExtensibleFallbackHandler | 0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5 |
| GPv2Settlement | 0x9008D19f58AAbD9eD0D60971565AA8510560ab41 |
These addresses are the same on Ethereum, Gnosis Chain, Arbitrum One, Base, Polygon, Avalanche, Lens, BNB Chain, Linea, Plasma, Ink, and Sepolia.