Coby's Dutch CoWs

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

NameAddress
Auctioneer0xE105d00b8E13Cb7f33Ea22cA2c2320f40A0E2c63
Handler0xB417180c2FD563bE2fB7833E95908B4D243bB62b
Escrow implementation0x2607A4712ed8C0BE50A92f71f32f0cBE0C366de9
ComposableCoW0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74
Settlement0x9008D19f58AAbD9eD0D60971565AA8510560ab41
Vault relayer0xC92E8bdf79f0507f65a392b0ab4667716BFE0110
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:

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

StringMeaning
NothingToSellsellAmount is 0.
SellingForNothingmaxAmount is 0.
PricePanicPreventermaxAmount is above type(uint256).max / 10_000.
BadSlippageslippageBps is outside 51..9998.
BadStartstartBlock is not the current block.
AuctionTooShortvalidFor is below 25.
BeneficiaryOrdersNotAllowedreceiver is not the caller.
PreventStepSizeUnderflowmaxAmount * slippageBps / 10_000 is below validFor.
BadApprovalThe allowance is outside the allowed range, or sellAmount exceeds type(uint256).max / 2.
ShortAfterFeeThe balance after pulling the allowance is below sellAmount.
FilledEscrow balance or vault allowance is below sellAmount.
BelowFloorThe settlement buy amount is below the current price.
OutsideWindowThe settlement block is outside the auction.
AuctionInProgressreleaseFunds was called too early.

The complete list, including the poll reverts, is in reference.md.