Source your own order flow
A permissionless solution to any AMM's discoverability problem
DeFi promises to tokenize everything. Smart contract platforms offer flexibility to codify asset-specific trading strategies that execute trustlessly. With liquidity open to anyone, anywhere, markets compete on price alone. The best quote should win the trade.
But it doesn’t play out that way. A new AMM can go on-chain with deep liquidity and competitive quotes, and still never trade, because none of it matters without order flow.
Traders move through established channels: aggregators and routers your AMM isn’t wired into. They usually stick with the venues they know, even when they get a worse quote.
In 2024 we built a bespoke AMM, the Bill Broker, with pricing logic that traded SPOT (the flatcoin) around its fair market value. Bootstrapping volume proved hard. There are a few options to try, but no silver bullet:
Aggregators don’t let you list yourself. Their engineers have to write the connector, and the group that owns the source list (Matcha’s, for instance, runs through the 0x API) prioritizes venues that already carry volume. An unfamiliar AMM with no track record sits in a backlog they may never get to.
Searchers take their pound of flesh. Their bot keeps your price aligned with the market, taking real risk to do it: unaudited code and a winner-take-all race. They’re paid from the arbitrage spread, your value leaking to a mercenary. And their incentive runs against yours: the better your quote, the wider the spread, the more they take.
Self-arbitrage needs liquidity you don’t have. Running the bot yourself keeps the spread in-house, but only works when the asset already trades deep elsewhere. Newer assets don’t: the bot has nothing to arbitrage against until you’ve seeded liquidity on established AMMs.
A solution
In our latest iteration we built an integration with CoW Swap using Programmatic Orders and flash loans. The approach can work for any on-chain swap protocol facing the discoverability problem.
How it works
Instead of waiting and praying for order flow, the AMM publishes its own intent to trade into CoW's batch auction for any solver to fill.
Flash loans handle atomic, multi-party settlement between the AMM and counterparties from the CoW network.
1) The AMM. Your pool or trading strategy: it exposes swap methods over a pair, say BTC ↔ USD.
2) The publisher. An on-chain swapper contract that reads the AMM’s price and publishes a matching order, and an off-chain keeper that submits that order and its execution recipe to CoW’s orderbook.
3) Settlement. The winning solver executes everything in one atomic transaction: flash-borrow USD (from an on-chain pool like Morpho, or one you run yourself), swap that USD for BTC on the AMM, settle the trade against the CoW batch, and repay the loan from the proceeds.
Benefits
Permissionless. No connector, no listing, no approval. Your order goes straight into CoW’s public orderbook, live the moment you deploy.
Surplus capture. CoW settles in a batch auction where solvers compete to return the most surplus. Because your order is floored at the AMM’s own quote and names your own contract as receiver, a solver wins only by beating that floor, and everything above it flows back to the pool. Incentives stay aligned: the solver takes its nominal batch fee, and the surplus goes to your LPs.
No upfront capital. Someone has to front the USD to buy the BTC the pool is selling. The flash loan supplies it just in time and is repaid from the swap proceeds within the same settlement, so no one (not the AMM, not the solver) has to lock up inventory to be the counterparty. CoW supports this natively via CIP-66.
Conclusion
The approach clears all three dead ends: it’s permissionless, the swap surplus stays with your LPs, and it needs no capital up front.
/// @title IAmm
/// @notice The surface the publisher reads. Any AMM that can answer "what would
/// you trade right now, and what's the worst price you'd accept?" can be
/// published into CoW this way.
/// @dev Deliberately small: the publisher never touches the pricing logic, only
/// trusts that `quote` is a floor the AMM stands behind and `swap` honors.
/// All amounts are raw token units.
interface IAmm {
/// @notice The two tokens this AMM trades between.
function tokenA() external view returns (IERC20);
function tokenB() external view returns (IERC20);
/// @notice The swap the AMM wants right now — the read-side mirror of `swap`,
/// and what the published order is built from.
/// @return sellingTokenA True when the AMM wants to sell `tokenA`.
/// @return sellAmount Sell-token amount; 0 means nothing to publish.
/// @return minBuyAmount Buy-token floor for `sellAmount`.
function previewSwap()
external
view
returns (bool sellingTokenA, uint256 sellAmount, uint256 minBuyAmount);
/// @notice Swaps `amountIn` of `tokenIn` at the AMM's own quote.
/// @dev Called in the settlement pre-hook with the flash-borrowed counter-leg.
/// `minAmountOut` is the order's `sellAmount`, so a fill can never be
/// sourced for less than it commits to deliver.
function swap(
IERC20 tokenIn,
uint256 amountIn,
uint256 minAmountOut
) external returns (uint256 amountOut);
/// @notice The AMM's floor value of `amountIn` of `tokenIn` in the other token
/// — the unsized price reference the order is checked against.
/// @dev MUST scale with `amountIn`: the publisher uses it to bound an order
/// whose size it did not choose.
function quote(
IERC20 tokenIn,
uint256 amountIn
) external view returns (uint256 minOut);
}
/// @title AmmCowPublisher
/// @notice Publishes an AMM's own intent to trade into CoW Protocol: signs
/// fill-or-kill orders via ERC-1271, sources the sell inventory from the
/// AMM against a flash-borrowed counter-leg, and returns what it captures.
/// @dev Holds no working capital — the counter-leg is borrowed per settlement and
/// the fill repays it in the same tx; an unrepayable draw reverts the batch.
/// @dev The price floor (`_checkOrder`) only prevents a drain: it caps a
/// solver-authored fill at the AMM's own `quote`. It never captures surplus
/// — this contract reads the AMM, not the market, so routing surplus is the
/// batch auction's job.
contract AmmCowPublisher is IConditionalOrder, IERC1271, Ownable {
/// @notice ERC-1271 "signature valid" magic value.
bytes4 internal constant MAGIC_VALUE = IERC1271.isValidSignature.selector;
/// @notice Fixed salt for the one ComposableCoW registration.
bytes32 public constant SALT = keccak256('AmmCowPublisher');
/// @notice The AMM being published.
IAmm public amm;
/// @notice Borrower the pre-hook draws the counter-leg from, and the order's
/// `receiver`. Must have allowlisted this contract via `setReceiver`,
/// since that is where the lender pulls repayment from.
FlashLoanRouter public flashRouter;
// … remaining storage, errors and modifiers elided …
constructor(/* … */) Ownable(msg.sender) {
// … wiring and standing relayer approvals elided …
// Register the order once. From here it is discoverable by the orderbook —
// no listing, no connector, no approval.
composableCow_.create(
ConditionalOrderParams({
handler: IConditionalOrder(address(this)),
salt: SALT,
staticInput: ''
}),
true
);
}
// -------------------------------------------------------------------------
// ComposableCoW handler — discovery
/// @inheritdoc IConditionalOrder
/// @dev Builds the order from `amm.previewSwap()`, stamping `appData` from
/// `offchainInput`. Reverts `InvalidOrder` when no swap is warranted.
function getTradeableOrder(
address /* owner */,
address /* sender */,
bytes32 /* ctx */,
bytes calldata /* staticInput */,
bytes calldata offchainInput
) external view returns (GPv2Order.Data memory order) {
bytes32 appData = offchainInput.length > 0
? abi.decode(offchainInput, (bytes32))
: bytes32(0);
order = _buildOrder(appData);
if (order.sellAmount <= 0 || order.buyAmount <= 0) {
revert InvalidOrder('no capacity');
}
}
// -------------------------------------------------------------------------
// Settlement participation — pre-hook
/// @notice Pre-hook: draws the flash-borrowed counter-leg and sources the
/// order's `sellAmount` of inventory from the AMM against it.
/// @dev Trampoline-only. Every arg is solver-supplied but bounded: the AMM swap
/// gates `inputAmount` by direction, capacity and min-out, and the lender
/// reverts a draw the fill can't repay.
/// @dev Returns prior settlements' surplus to the AMM first (`_settleToAmm`), so
/// only this settlement's inventory is left to pull. MUST precede
/// `takeLoan`, or the sweep would take the borrowed counter-leg.
function provideInventory(
uint256 inputAmount,
uint256 sellAmount,
bool sellingTokenA
) external onlyTrampoline {
_settleToAmm();
flashRouter.takeLoan(inputAmount);
IERC20 tokenIn = sellingTokenA ? amm.tokenB() : amm.tokenA();
tokenIn.forceApprove(address(amm), inputAmount);
amm.swap(tokenIn, inputAmount, sellAmount);
}
// -------------------------------------------------------------------------
// ERC-1271 — authorize the fill
/// @inheritdoc IERC1271
/// @dev `signature` is the ABI-encoded order, bound to `hash` so no other order
/// can be authorized, then gated by `_checkOrder`.
function isValidSignature(
bytes32 hash,
bytes calldata signature
) external view returns (bytes4) {
GPv2Order.Data memory order = abi.decode(signature, (GPv2Order.Data));
if (GPv2Order.hash(order, settlement.domainSeparator()) != hash) {
revert InvalidOrder('hash');
}
_checkOrder(order);
return MAGIC_VALUE;
}
// -------------------------------------------------------------------------
// Internal
/// @dev Pins every solver-variable order field. Structural: receiver, fill-or-
/// kill sell, zero fee, unexpired, plain ERC20 balances, the AMM pair.
/// Price: `buyAmount` at or above the AMM's `quote` for `sellAmount`.
/// @dev The floor is the only on-chain price gate. The signature isn't bound to
/// what the keeper posted, so without it a solver could self-author a
/// `buyAmount ≈ 0` order and drain the sourced inventory; `quote` scales
/// the floor with `sellAmount`, capping the worst fill at the AMM's rate.
/// @dev Reads only the order and `block.timestamp`, never settlement state, so
/// the orderbook's submission `eth_call` accepts a well-formed order and it
/// stays acceptable until it expires.
function _checkOrder(GPv2Order.Data memory order) internal view {
if (order.receiver != address(flashRouter)) revert InvalidOrder('receiver');
if (order.kind != GPv2Order.KIND_SELL) revert InvalidOrder('kind');
if (order.partiallyFillable) revert InvalidOrder('partial');
if (order.feeAmount > 0) revert InvalidOrder('fee');
if (order.validTo < block.timestamp) revert InvalidOrder('expired');
// … token-pair and balance-kind checks elided …
if (order.buyAmount < amm.quote(order.sellToken, order.sellAmount)) {
revert InvalidOrder('price');
}
}
/// @dev Fill-or-kill sell, sized and floored by `amm.previewSwap()`. It enters
/// the batch as a market order with the AMM's quote as its floor: a solver
/// may fill it better, never worse.
function _buildOrder(
bytes32 appData
) internal view returns (GPv2Order.Data memory order) {
(bool sellingTokenA, uint256 sellAmount, uint256 buyAmount) = amm
.previewSwap();
(IERC20 sellToken, IERC20 buyToken) = sellingTokenA
? (amm.tokenA(), amm.tokenB())
: (amm.tokenB(), amm.tokenA());
order = GPv2Order.Data({
sellToken: sellToken,
buyToken: buyToken,
receiver: address(flashRouter),
sellAmount: sellAmount,
buyAmount: buyAmount,
validTo: uint32(block.timestamp + validToBufferSec),
appData: appData,
feeAmount: 0,
kind: GPv2Order.KIND_SELL,
partiallyFillable: false,
sellTokenBalance: GPv2Order.BALANCE_ERC20,
buyTokenBalance: GPv2Order.BALANCE_ERC20
});
}
}
/**
* Keeper: polls the AMM, builds the order the publisher would sign, and posts it
* to CoW's orderbook with the recipe a solver needs to settle it.
*
* The publisher holds no working capital — its sell side is created inside the
* settlement, when the pre-hook draws the flash-borrowed counter-leg and hands it
* to the AMM. Direction, `sellAmount` and the floor all come off the AMM's own
* `previewSwap` / `quote`, so what we post matches what the contract would sign.
*
* Every solver-variable field is bounded on-chain (the loan router pins the
* lender; the publisher's ERC-1271 pins the receiver, pair and floor). The
* preflight re-runs that same `isValidSignature` before posting, since the
* orderbook's rate limit is strict and rejections are deterministic.
*/
const SETTLEMENT = '0x9008D19f58AAbD9eD0D60971565AA8510560ab41';
/// Morpho Blue — zero flash-loan premium, so repayment equals the principal.
const MORPHO = '0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb';
const API = 'https://api.cow.fi/mainnet/api/v1';
// The two contracts you deploy for this AMM — set both before running.
const swapperAddr = '0x…'; // AmmCowPublisher: reads the AMM and signs the order
const loanRouterAddr = '0x…'; // MorphoFlashLoanRouter: funds and repays the settlement
/// GPv2Order.Data in EIP-712 field order — the ERC-1271 signature payload.
const ORDER_TUPLE =
'tuple(address sellToken,address buyToken,address receiver,uint256 sellAmount,uint256 buyAmount,uint32 validTo,bytes32 appData,uint256 feeAmount,bytes32 kind,bool partiallyFillable,bytes32 sellTokenBalance,bytes32 buyTokenBalance)';
// Direction decides the pair: the AMM pays out the token the order sells and
// pulls the other one, which is the leg the loan funds.
const [sellingTokenA, sellAmount]: [boolean, bigint] = await amm.previewSwap();
if (sellAmount === 0n) throw new Error('AMM advertises no capacity');
const [sellTokenAddr, buyTokenAddr] = sellingTokenA
? [tokenAAddr, tokenBAddr]
: [tokenBAddr, tokenAAddr];
// `buyAmount` is the publisher's ERC-1271 floor — anything lower is rejected by
// `_checkOrder`, so it's read straight off the AMM.
const buyAmount: bigint = await amm.quote(sellTokenAddr, sellAmount);
// The draw is the flash-borrowed counter-leg the pre-hook hands the AMM,
// denominated in the buy token. Its size sits between two bounds: the AMM swap
// must yield at least `sellAmount`, and the fill must repay at least the draw.
const loanAmount: bigint = args.loan ? BigInt(args.loan) : buyAmount;
// The pre-hook that sources the inventory, run by the solver inside the batch.
const hooks = {
pre: [
{
target: swapperAddr,
callData: swapper.interface.encodeFunctionData('provideInventory', [
loanAmount,
sellAmount,
sellingTokenA,
]),
gasLimit: '400000',
},
],
post: [],
};
// The execution recipe: the lender to draw from, the loan router that adapts its
// callback and receives the funds, and the hooks. CIP-66 lets a solver pick this
// up natively.
const appDataDoc = {
version: '1.3.0',
appCode: 'amm-cow-publisher',
metadata: {
flashloan: {
liquidityProvider: MORPHO,
protocolAdapter: loanRouterAddr,
receiver: loanRouterAddr,
token: buyTokenAddr,
amount: loanAmount.toString(),
},
hooks,
},
};
const appDataStr = JSON.stringify(appDataDoc);
const appDataHash = keccak256(toUtf8Bytes(appDataStr));
const order = {
sellToken: sellTokenAddr,
buyToken: buyTokenAddr,
receiver: loanRouterAddr,
sellAmount: sellAmount.toString(),
buyAmount: buyAmount.toString(),
validTo,
appData: appDataHash,
feeAmount: '0',
kind: 'sell',
partiallyFillable: false,
sellTokenBalance: 'erc20',
buyTokenBalance: 'erc20',
};
const digest = TypedDataEncoder.hash(DOMAIN, ORDER_TYPE, order);
// ERC-1271: the publisher decodes this ABI-encoded order in isValidSignature.
// There's no private key anywhere — the contract IS the signer.
const signature = AbiCoder.defaultAbiCoder().encode(
[ORDER_TUPLE],
[
[
sellTokenAddr,
buyTokenAddr,
loanRouterAddr,
sellAmount,
buyAmount,
validTo,
appDataHash,
0n,
keccak256(toUtf8Bytes('sell')),
false,
keccak256(toUtf8Bytes('erc20')),
keccak256(toUtf8Bytes('erc20')),
],
]
);
// … preflight: the loan router allowlists the publisher, the floor covers the
// principal, the AMM still offers this direction and size, the relayer
// allowance stands, and the publisher's ERC-1271 accepts this exact order …
// POST /orders registers the appData document itself when `appData` carries the
// full JSON, so a separate PUT /app_data is redundant.
const res = await fetch(`${API}/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...order,
signingScheme: 'eip1271',
signature,
from: swapperAddr,
appData: appDataStr,
appDataHash,
}),
});
/// @title IMorpho
/// @notice The Morpho Blue flash-loan surface.
/// @dev Morpho pushes `assets` to the caller, invokes `onMorphoFlashLoan`, then
/// pulls `assets` back — the caller must hold and have approved `assets` by
/// the time the callback returns. No premium: in equals out.
interface IMorpho {
/// @notice Flash-borrows `assets` of `token` to the caller.
function flashLoan(
address token,
uint256 assets,
bytes calldata data
) external;
}
/// @title MorphoFlashLoanRouter
/// @notice Sources Morpho Blue liquidity for a CoW settlement, lending it to an
/// allowlisted receiver for the settlement and repaying itself.
/// @dev CoW's Borrower role for one lender. Usable by a third party because
/// completing the loan needs no solver-authored interactions: Morpho pushes
/// the loan to its caller, and the CoW order names this contract as
/// `receiver`, so proceeds land where Morpho is already approved to pull.
/// @dev INVARIANT: never holds the borrowed token between txs — it transits one
/// settlement and any surplus is swept to the receiver. Not custody.
contract MorphoFlashLoanRouter is Ownable {
using SafeERC20 for IERC20;
/// @notice CoW flash-loan router, the only address that may borrow through us.
address public immutable cowRouter;
/// @notice Morpho Blue, the only lender this borrower accepts.
address public immutable lender;
/// @notice Contracts permitted to borrow through this router.
mapping(address => bool) public isReceiver;
/// @notice Token borrowed in the in-flight settlement.
/// @dev Transient: Morpho's callback reports `assets` but not the token.
address private transient loanToken;
/// @notice Receiver that took the in-flight loan, and the sweep's destination.
/// @dev Transient. Written only by `takeLoan`, so it can only ever name an
/// allowlisted contract.
address private transient activeReceiver;
// … constructor, errors, events and `setReceiver` elided …
/// @notice Takes a Morpho flash loan and hands control back to the CoW router.
/// @dev `lender_`, `token` and `amount` come from the order's appData and are
/// solver-influenced. Pinning `lender_` stops a solver from pointing us at
/// their own lender; an `amount` the settlement can't repay fails Morpho's
/// pull, so it needs no bound. One loan per settlement — nested callbacks
/// would overwrite the token and receiver this one still needs on exit.
function flashLoanAndCallBack(
address lender_,
IERC20 token,
uint256 amount,
bytes calldata callBackData
) external {
if (msg.sender != cowRouter) revert UnauthorizedCall();
if (lender_ != lender) revert UnauthorizedCall();
if (loanToken != address(0)) revert LoanInProgress();
loanToken = address(token);
IMorpho(lender_).flashLoan(address(token), amount, callBackData);
}
/// @notice Lends `amount` of the in-flight loan to the calling receiver.
/// @dev Callable by an allowlisted receiver from its CoW pre-hook — the one
/// frame where a receiver is `msg.sender`. The caller becomes the sweep's
/// destination; a different second receiver is rejected so it can't change
/// mid-settlement.
function takeLoan(uint256 amount) external {
if (!isReceiver[msg.sender]) revert UnauthorizedCall();
if (activeReceiver == address(0)) activeReceiver = msg.sender;
else if (activeReceiver != msg.sender) revert UnauthorizedCall();
IERC20(loanToken).safeTransfer(msg.sender, amount);
}
/// @notice Morpho's callback: runs the settlement, sweeps the surplus, then
/// authorizes repayment.
/// @dev The settlement runs inside this call, so the receiver draws the loan and
/// the proceeds arrive before it returns. Reverts `LoanNotTaken` if nothing
/// drew the loan (the surplus would have no owner). Morpho pulls right
/// after, so an unreturned loan reverts the tx.
function onMorphoFlashLoan(uint256 assets, bytes calldata data) external {
if (msg.sender != lender) revert UnauthorizedCall();
ICowFlashLoanRouter(cowRouter).borrowerCallBack(data);
address dest = activeReceiver;
if (dest == address(0)) revert LoanNotTaken();
IERC20 token = IERC20(loanToken);
uint256 held = token.balanceOf(address(this));
if (held > assets) token.safeTransfer(dest, held - assets);
token.forceApprove(lender, assets);
}
}