CoW Order Debugging
This skill covers how to debug Kapan's CoW Protocol limit orders, from order creation through settlement.
Architecture Overview
User → MultiplyEvmModal → useCowOrder → KapanOrderManager → ComposableCoW
↓
KapanOrderHandler (generates GPv2 orders)
↓
CoW Watchtower (monitors & submits)
↓
FlashLoanRouter → KapanCowAdapter → Settlement
Key Contracts (Base)
| Contract | Address | Purpose |
|----------|---------|---------|
| KapanOrderManager | 0x12F80f5Fff3C0CCC283c7b2A2cC9742ddf8c093A | Stores order context, executes hooks |
| KapanOrderHandler | 0x9a503a4489ebeb2e6f13adcd28f315e972b63371 | IConditionalOrder - generates GPv2 orders |
| KapanCowAdapter | 0xF6342053a12AdBc92C03831BF88029608dB4C0B6 | Flash loan borrower, funds OrderManager |
| KapanRouter | 0xFA3B0Efb7E26CDd22F8b467B153626Ce5d34D64F | Executes lending operations |
Orders Not Reaching Solvers (Autopilot Filtering)
Symptom: Order shows as "open" on CoW Explorer but never fills, even for liquid pairs.
The Two Receiver Concepts
CoW Protocol has TWO different "receiver" fields that serve different purposes:
appData.metadata.flashloan.receiver- Used by CoW API for balance override validation during order submissionGPv2Order.receiver- Used by autopilot to decide whether to skip balance filtering before sending to solvers
The Autopilot Balance Filtering Gap
CoW Protocol's autopilot has a gap in flash loan handling:
- API validation (
orderbookcrate): Correctly usesflashloan.receiverfrom appData for balance override - Autopilot (
solvable_orders.rs): Ignores flashloan metadata entirely, only skips balance filtering whenGPv2Order.receiver == Settlement
// CoW autopilot hack - only skips balance check if receiver is Settlement
if order.data.receiver.as_ref() == Some(&settlement_contract) {
return true; // Skip balance filtering - TODO: replace with proper detection
}
What This Means for Kapan
- Our
GPv2Order.receiver = OrderManager(set by KapanOrderHandler) - THIS IS CORRECT - Autopilot runs balance check on OrderManager
- OrderManager doesn't have tokens pre-flash-loan
- Orders may get filtered out before reaching solvers (not always)
CRITICAL: Do NOT Change GPv2Order.receiver
NEVER set GPv2Order.receiver = Settlement for flash loan orders. This was tried and it BREAKS the flow:
- With
receiver = Settlement, buyToken goes to Settlement after swap - But
_executePostHook()checksbalanceOf(address(this))at OrderManager - OrderManager has 0 buyToken because it went to Settlement
- Post-hook fails!
The appData.metadata.flashloan.receiver is different - that's for CoW API balance validation, NOT for the GPv2Order.
Autopilot Filtering Reality
In practice, autopilot filtering is not as aggressive as feared. Orders with flash loan metadata usually get through because:
- The appData.flashloan tells solvers to use flash loans
- Solvers that support flash loans will pick up the order
- The order may take longer to fill but will eventually fill for liquid pairs
If orders aren't filling, check other causes first (token liquidity, solver routing, etc.).
Exotic Token Routing (GHO, PT Tokens)
Symptom: Orders for GHO, Pendle PT tokens, or other exotic assets never fill.
Why This Happens
Standard CoW solvers focus on high-volume pairs (WETH/USDC/USDT/DAI). They may not have routes for:
- GHO: Aave's stablecoin, limited DEX liquidity (Balancer, some Curve)
- PT tokens: Pendle principal tokens, very niche (Pendle AMM only)
- Other low-liquidity or specialized tokens
Diagnosis
If orders for common pairs (WETH → USDC) fill but exotic pairs don't, it's a solver routing issue, not balance filtering.
Options
- Wait for liquidity - As tokens get more DEX depth, solvers add routes
- Custom solver - Run a solver with 1inch/Paraswap integration for better routing
- Different token - Use more liquid alternatives if available
Common Failure Points
1. OrderHandler Not Set
Symptom: createOrder reverts with InvalidHandler()
Check:
cast call <OrderManager> "orderHandler()" --rpc-url <RPC>
# Should NOT be 0x0000...0000
Fix: Run npx hardhat deploy --tags KapanOrderHandler --network <chain>
2. Order Not Registered on ComposableCoW
Symptom: Order created but watchtower ignores it
Check:
# Get order hash from OrderManager logs
cast logs --from-block <block> --address <OrderManager> --rpc-url <RPC>
# Verify on ComposableCoW
cast call 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 \
"singleOrders(address,bytes32)(bool)" <OrderManager> <cowOrderHash> --rpc-url <RPC>
3. EIP-1271 Signature Invalid
Symptom: GPv2: invalid eip1271 signature
Cause: Order parameters in signature don't match what handler returns
Debug:
- Get the order from handler:
const [order, sig] = await composableCoW.getTradeableOrderWithSignature(
orderManagerAddr, cowParams, "0x", []
);
- Compare with what you're passing to
settle() receivermust ALWAYS be OrderManager (for both flash loan and non-flash-loan orders)- If receiver is anything else, post-hook will fail
4. Missing Token Approval (PullToken fails)
Symptom: ErrorNotEnoughAllowance() (selector: 0xc2139725)
Check:
cast call <token> "allowance(address,address)(uint256)" <user> <router> --rpc-url <RPC>
Cause: Frontend didn't execute approval transaction before/during order creation
Debug flow:
- Check
authorizeInstructionsreturns approval call:
const [targets, data] = await router.authorizeInstructions([pullInstruction], userAddress);
- Verify approval is in the transaction batch
- Check if approval was actually executed on-chain
5. Flash Loan Lender Not Configured
Symptom: Adapter reverts on flash loan callback
Check:
cast call <Adapter> "allowedLenders(address)(bool)" <lenderAddress> --rpc-url <RPC>
cast call <Adapter> "getLenderType(address)(uint8)" <lenderAddress> --rpc-url <RPC>
# Type: 1=Aave, 2=Morpho
Known Issues / TODOs
isFlashLoanOrder Flag Purpose
The KapanOrderParams struct has an isFlashLoanOrder boolean. This flag is used for:
- Frontend logic - Determines instruction building flow
- OrderManager state - Tracks that this is a flash loan order
IMPORTANT: The isFlashLoanOrder flag does NOT change GPv2Order.receiver. Receiver is ALWAYS OrderManager because:
- OrderManager._executePostHook() needs the bought tokens at
address(this) - Setting receiver to Settlement would break this flow
// KapanOrderHandler.sol - CORRECT, DO NOT CHANGE
address receiver = address(orderManager); // ALWAYS OrderManager
Status: Working as intended. Do not change receiver logic.
Decoding Order Instructions
Use scripts/decode-order.ts to inspect stored order:
FORK_CHAIN=base MAINNET_FORKING_ENABLED=true npx hardhat run scripts/decode-order.ts --network hardhat
Key things to verify:
- PullToken user matches order creator
- PushToken target is KapanCowAdapter (for flash loan repayment)
- Borrow amount matches flash loan + fee
- DepositCollateral uses correct market context
Testing Real Orders
The test file test/v2/CowRealFlow.fork.ts can simulate settling real orders:
FORK_CHAIN=base MAINNET_FORKING_ENABLED=true FORK_BLOCK=latest \
npx hardhat test test/v2/CowRealFlow.fork.ts --grep "<order-id>"
Test Structure
- Direct settle (bypasses flash loan, better errors):
await settlement.connect(solver).settle(tokens, prices, [trade], interactions);
- Full flash loan flow:
await flashLoanRouter.connect(solver).flashLoanAndSettle(loans, settlementCalldata);
If direct settle works but flash loan fails, the issue is in the flash loan callback chain.
Flash Loan Settlement Test
test/v2/CowFlashLoanSettlement.fork.ts tests the full flash loan flow:
- Uses
receiver = OrderManager(the correct production setting) - Simulates solver behavior with pre-fund and interactions
- Verifies the complete flash loan → swap → post-hook → repay flow
Note: The test manually builds settlement calldata because it's testing what solvers do. Production orders go through Watchtower → Solvers who build the settlement.
Hook Execution Flow
Pre-hooks (executed before swap):
fundOrder(token, recipient, amount)on Adapter - transfers flash-loaned tokens to OrderManagerexecutePreHookBySalt(user, salt)on OrderManager - runs pre-instructions (usually empty for flash loan orders)
Post-hooks (executed after swap):
executePostHookBySalt(user, salt)on OrderManager - runs post-instructions:- PullToken (user's margin)
- Add (combine swap output + margin)
- Approve (for lending protocol)
- DepositCollateral (to Morpho/Aave)
- Borrow (to repay flash loan)
- PushToken (send borrowed tokens to Adapter for repayment)
Flash Loan Repayment
For Morpho flash loans, the Adapter must have the borrowed amount + 0 fee (Morpho is free) to repay.
The post-hook borrows from user's position and pushes to Adapter:
User's Morpho Position → Borrow WETH → PushToken → KapanCowAdapter → repay Morpho
Debugging Console Logs
The contracts have console.log statements. In fork tests:
fundOrder: START
fundOrder: amount = 18399910693677945
fundOrder: recipient = 0x12f80f5fff3c0ccc283c7b2a2cc9742ddf8c093a
fundOrder: adapter balance = 18399910693677945
fundOrder: COMPLETE
If you see _executePostHook: router call FAILED with low-level error, decode the selector:
cast 4byte <selector>
Key Files
packages/hardhat/contracts/v2/cow/KapanOrderManager.sol- Order storage and hook executionpackages/hardhat/contracts/v2/cow/KapanOrderHandler.sol:90- GPv2 order generation (receiver logic)packages/hardhat/contracts/v2/cow/KapanCowAdapter.sol- Flash loan handlingpackages/nextjs/hooks/useCowOrder.tsx- Frontend order creationpackages/nextjs/components/modals/MultiplyEvmModal.tsx- UI and instruction buildingpackages/nextjs/utils/cow/appData.ts:417- AppData flashloan.receiver settingpackages/hardhat/test/v2/CowRealFlow.fork.ts- Integration testspackages/hardhat/test/v2/CowFlashLoanSettlement.fork.ts- Flash loan with receiver=Settlement test
Quick Diagnosis Checklist
- [ ] OrderHandler set on OrderManager?
- [ ] Order registered on ComposableCoW?
- [ ] AppData registered with CoW API?
- [ ] User approved Router for margin token?
- [ ] User has credit delegation for borrow?
- [ ] Flash loan lender allowed on Adapter?
- [ ] VaultRelayer approved for sell token?
- [ ] Is token exotic (GHO, PT)? May need custom solver
- [ ] Flash loan order? Check autopilot filtering section