How CoW Protocol settles a trade (and what my TWAP router got wrong)
I had a TWAP order-router for CoW Protocol — split a big sell into time-sliced parts to cut price impact. It built, it had seventeen passing tests, and its core did this:
ICowVaultRelayer(cowRelayer).deposit(token, owner, amount);
ICowSettler(cowSettler).settle(orderUid);
Neither of those functions exists on mainnet. The contract was integrating with a CoW Protocol I’d imagined — and the tests passed because they mocked the imaginary interface. That’s the trap with on-chain integrations: “compiles + green against mocks” tells you nothing about whether you’re calling a real protocol. So here’s how CoW actually settles a trade, and the three things I had backwards.
Orders are intents, not swaps
On an AMM you submit a transaction that executes a swap against a pool. On CoW you sign an intent: “sell at most X of token A for at least Y of token B before time T.” You never execute anything. A signed order is just the GPv2Order.Data struct — tokens, amounts, limits, validTo, kind — and its EIP-712 digest is the order’s id.
Orders are collected over a short window into a batch. Then off-chain solvers compete for the right to settle the batch, each proposing a solution that routes the orders (matching them directly against each other where possible — “coincidence of wants” — and only hitting external AMMs for the remainder). The winning solver is the one that returns the most surplus to users, and only that solver calls:
function settle(IERC20[] tokens, uint256[] clearingPrices,
GPv2Trade.Data[] trades, GPv2Interaction.Data[][3] interactions)
external nonReentrant onlySolver;
Two things in that signature mattered for my bug. It’s onlySolver — an allow-listed, bonded set; a random contract cannot call settle. And it takes a clearingPrices array with one price per token: every order in the batch touching a given token settles at the same price. That uniform clearing price is the actual MEV-protection mechanism — within a batch, transaction ordering carries no value, so there’s no sandwich to run. (CoW settlement docs)
So my settle(orderUid) was wrong twice over: the real settle has a completely different shape, and my contract isn’t a solver and could never call it.
You approve the relayer, not the settlement contract
Here’s the subtle one. To trade on CoW you grant your ERC-20 allowance to the GPv2VaultRelayer (0xC92E…0110) — not to GPv2Settlement. Why a second contract? Because settle executes arbitrary interactions (calls into external liquidity). If your allowance pointed at the settlement contract, a malicious solver could craft an “interaction” that just calls transferFrom on your tokens. So CoW puts the allowance on a minimal relayer that only GPv2Settlement may call, and interactions to the relayer are forbidden. Funds can move only as part of a settlement that respects your signed order. (vault relayer docs)
My router approved the relayer and then reset the approval to zero in the same transaction, right after a synchronous deposit. But settlement is asynchronous — a solver fills your order minutes later. Resetting the allowance immediately would leave nothing for the relayer to pull when the fill actually happens. The approval has to persist. CoW’s own guidance is a single standing relayer approval.
A contract presigns; it doesn’t settle
If you can’t call settle, how does a contract place an order? CoW’s GPv2Signing mixin allows four schemes: an EOA’s EIP-712 or EthSign signature, a smart contract’s ERC-1271 isValidSignature, or PreSign — calling setPreSignature(orderUid, true) on-chain to flag an order as authorized. (GPv2Signing source)
That’s the hook my executor needed. Each slice is its own order (its own validTo window); when a slice comes due, the contract presigns that slice’s orderUid and leaves the rest to solvers. The whole fix was: approve the real relayer once, presign per slice, and delete the settle call entirely. The contract authorizes; the network settles. I also added a revokePresignature so cancelling can kill a presigned-but-unfilled slice — the honest caveat being that an in-flight slice stays fillable until its validTo otherwise.
Two ways to TWAP, and when to roll your own
The above is a self-hosted TWAP: your own keeper presigns slices on a timer. CoW also ships an official TWAP, and it’s worth knowing why it exists. It’s not a bespoke contract — it’s a conditional order on ComposableCoW. You register one order with a handler implementing getTradeableOrder(...); CoW’s watchtower calls that each block to get the part valid now and posts it, and the settlement contract calls the handler’s verify(...) through ERC-1271 so a solver can only ever fill the part the handler currently authorizes. That’s validated discretization — the chain itself enforces the schedule — plus on-chain cancellation and a keeper you don’t have to run.
A naive splitter gives all of that up: it trusts your off-chain service to presign the right thing, and any already-presigned part stays fillable until it expires. So I built both — a fixed CowTwapExecutor and a faithful ComposableCoW IConditionalOrder TWAP handler (part scheduling, span windows, the watchtower’s poll/abort signals, the validation guards) — to have the self-hosted path and the framework path side by side. The honest default is the framework; you roll your own only when you need logic the handler can’t express, and even then the right move is a new handler, not an off-chain loop.
The lesson
The contract compiled and its tests were green the whole time it was calling functions that don’t exist. Mocks test your code against your model of the protocol; they can’t tell you the model is fiction. The fix wasn’t really Solidity — it was reading the actual contracts until the shape of a real settlement (intent, relayer, presign, solver) replaced the shape I’d assumed. It’s the same theme as getting randomness right and binding a ZK proof: the hard part isn’t the code you write, it’s checking it against the system it actually has to live in.
The fix, the handler, and the 26 tests are in the repo.