Contract behavior on Base
Dutch CoWs reference
This is what the contracts in src/ do on Base, chain id 8453. The plain-text copy for GitHub is reference.md. The integration steps are on the guide. The source comments say the software comes with no warranty.
Contracts
| Name | Address |
|---|---|
| Auctioneer | 0xE105d00b8E13Cb7f33Ea22cA2c2320f40A0E2c63 |
| Handler | 0xB417180c2FD563bE2fB7833E95908B4D243bB62b |
| Escrow implementation | 0x2607A4712ed8C0BE50A92f71f32f0cBE0C366de9 |
| ComposableCoW | 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 |
| Settlement | 0x9008D19f58AAbD9eD0D60971565AA8510560ab41 |
| Vault relayer | 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110 |
0xe537e7206526486A2b4fBe7679Ee779734462437 is RealDutchCowTest. It is not the integration path. place reverts unless the call carries ETH, and the contract does not forward tokens it receives.Who calls what
The integrator's contract holds the sell token and calls placeOrder. The auctioneer deploys an escrow clone. The escrow registers one conditional order on ComposableCoW, with DutchAuction as the handler.
These contracts do not post to the orderbook and do not run a solver. A CoW watch tower sees ConditionalOrderCreated and posts the order returned by getTradeableOrder. A solver calls GPv2Settlement.settle. Settlement asks the escrow for an EIP-1271 signature. The escrow forwards that check to ComposableCoW, which calls DutchAuction.verify.
Settlement pays the buy token to receiver from its own balance and pulls the sell token from the escrow through the vault relayer. Dutch CoWs does not forward the buy token. receiver is the contract that called placeOrder.
placeOrder
AuctionParams encodes the same way as Data. placeOrder is nonReentrant and reverts unless all of these hold:
sellAmount > 0andmaxAmount > 0maxAmount <= type(uint256).max / 10_000slippageBpsis 51 through 9998 (0.51% through 99.98%)validFor >= 25maxAmount * slippageBps / 10_000 >= validForreceiver == msg.senderstartBlock == block.number- the caller has approved the predicted escrow for at least
sellAmount
The escrow address is a CREATE2 clone. The salt is keccak256(abi.encode(msg.sender, params)) and the clone arguments are abi.encode(params). predictEscrow uses the caller's address, so a call from any other address returns a different escrow.
The escrow then requires the allowance to be at least sellAmount and strictly below 2 * sellAmount, and sellAmount to be at most type(uint256).max / 2. It pulls the whole allowance. If its balance is then below sellAmount, it reverts ShortAfterFee. It approves the vault relayer for exactly sellAmount and registers the order. placeOrder returns that conditional-order hash.
Price
floor = maxAmount * (10_000 - slippageBps) / 10_000
step = (maxAmount - floor) / validFor
On blocks startBlock through startBlock + 4 the quoted buy amount is maxAmount. From startBlock + 5:
buyAmount = maxAmount - (block.number - startBlock - 4) * step
Quoting stops when block.number >= startBlock + validFor. The last quoted block is startBlock + validFor - 1, which is five steps above floor, plus any remainder from the division. The auction does not quote floor itself.
verify accepts a settlement buy amount greater than or equal to the current price. A smaller amount reverts BelowFloor. A solver can pay more than the current price. It cannot pay less.
The posted order's validTo is block.timestamp plus ((blocksLeft * 2) / 10) seconds, and at least 1 second. That is 0.2 seconds per block still left, which matches a 200ms block.
Two different 0.1% amounts
The fill fee is a CoW partner fee. Every order uses app data 0x09eed5e4a791d7763f86d80bace7aac36a981efa30a39e45b9f65adf28d8b932. On the Base orderbook, orders with that hash carried volumeBps: 10 to 0x785fAA8D709D7CaAD63d5FbDB750D4a22600d30d, which is 0.1% of volume. The handler does not transfer it. verify rejects any other appData.
The refund tip is separate. After block.number > startBlock + validFor, anyone can call releaseFunds. The caller receives balance / 1000 of the escrow's sell-token balance, and the rest goes to receiver. If the balance is below 1000 raw units, the tip is zero and the owner receives all of it.
Quoting already stopped at startBlock + validFor. The first block that can release is startBlock + validFor + 1. releaseFunds removes the conditional order before it pays. If the token blocks the payout, the removal still stands and a later call tries the payout again.
When quoting stops
getTradeableOrder reverts PollNever("Filled") when the escrow's sell-token balance is below sellAmount or its allowance to the vault relayer is below sellAmount. Donating the sell token back does not restore the allowance, so it does not reopen the order. verify uses the same check and reverts Filled.
The posted order is a fill-or-kill sell. feeAmount is 0, partiallyFillable is false, and both balances are ERC-20. Sell token, buy token, receiver, and sell amount must match the auction or verify reverts FieldMismatch.
Revert strings
| String | Meaning |
|---|---|
| NothingToSell | sellAmount is 0. |
| SellingForNothing | maxAmount is 0. |
| PricePanicPreventer | maxAmount is above type(uint256).max / 10_000. |
| BadSlippage | slippageBps is outside 51..9998. |
| BadStart | startBlock is not the current block. |
| AuctionTooShort | validFor is below 25. |
| BeneficiaryOrdersNotAllowed | receiver is not the caller. |
| PreventStepSizeUnderflow | maxAmount * slippageBps / 10_000 is below validFor. |
| BadApproval | The allowance is outside the allowed range, or sellAmount exceeds type(uint256).max / 2. |
| ShortAfterFee | The balance after pulling the allowance is below sellAmount. |
| Filled | Escrow balance or vault allowance is below sellAmount. |
| BelowFloor | The settlement buy amount is below the current price. |
| OutsideWindow | The settlement block is outside the auction. |
| AuctionInProgress | releaseFunds was called too early. |
The complete list, including the poll reverts, is in reference.md.