Dutch auctions for CoW Protocol on Base
Coby's Dutch CoWs integration guide
Dutch CoWs allows smart contracts to place Dutch Auction orders directly to CoW Protocol. A contract, whether via a hook or a user interaction, can place an order filled by a verified solver without the developer needing to set up any off-chain infrastructure.
Integrate
Do these three calls in one transaction, from the contract that holds the sell token. receiver must be that contract and startBlock must be block.number. The interface is src/IDutchCoW.sol.
predictEscrow(params) returns the address the escrow will have.
Approve that address for sellAmount of the sell token.
placeOrder(params) deploys the escrow and registers the auction.
AuctionParams memory params = AuctionParams({
sellToken: sellToken,
buyToken: buyToken,
sellAmount: sellAmount,
maxAmount: maxAmount,
receiver: address(this),
startBlock: block.number,
validFor: validFor,
slippageBps: slippageBps
});
address escrow = auctioneer.predictEscrow(params);
Option A: standard approve. Use this when the token's approve returns a bool, which covers most ERC-20s.
IERC20(sellToken).approve(escrow, sellAmount);
auctioneer.placeOrder(params);
Some tokens, such as USDT, return nothing from approve. Option A reverts on those. Option B accepts both kinds of return, so use it when you have not tested the token.
Option B: low-level approve. Accepts a bool return or no return.
(bool ok, bytes memory data) = sellToken.call(abi.encodeWithSelector(0x095ea7b3, escrow, sellAmount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "approve");
auctioneer.placeOrder(params);
sellAmount and less than 2 * sellAmount. The escrow pulls the whole allowance. Approve a little more than sellAmount only when the token takes a transfer fee.Parameters:
slippageBps allows 51 to 9998, which is 0.51% to 99.98%. validFor must be at least 25 blocks. maxAmount * slippageBps / 10_000 must be at least validFor, or the price step rounds to zero and the call reverts.
partnerFee, volumeBps: 10, recipient 0x785fAA8D709D7CaAD63d5FbDB750D4a22600d30d). Your order's app data is the handler's APP_DATA constant, so it carries the fee with every fill.
How it works
Your contract calls the auctioneer. The auctioneer deploys a small escrow, pulls the sell token into it, and registers the order with ComposableCoW. The CoW watch tower posts the order to the orderbook. A solver fills it by paying the buy token to your contract and taking the sell token from the escrow.
The price stays at maxAmount for the first 5 blocks, then falls. slippageBps is the total drop in basis points of maxAmount. validFor is how many Base blocks that drop takes. Base blocks are 200ms. The floor is maxAmount * (10_000 - slippageBps) / 10_000.
If nobody fills it, anyone can call releaseFunds on the escrow once block.number is greater than startBlock + validFor. The sell tokens go back to your contract. When the balance is at least 1000 raw units, the caller keeps balance / 1000. The full behavior is in the reference.
Base Deployment Addresses
| Contract | Address |
|---|---|
| Auctioneer | 0xE105d00b8E13Cb7f33Ea22cA2c2320f40A0E2c63 |
| Handler | 0xB417180c2FD563bE2fB7833E95908B4D243bB62b |
| Escrow implementation | 0x2607A4712ed8C0BE50A92f71f32f0cBE0C366de9 |
| ComposableCoW | 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 |
| Settlement | 0x9008D19f58AAbD9eD0D60971565AA8510560ab41 |
| Vault relayer | 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110 |