smart-contracts/backlog/docs/CoW_Quick_Reference.md

8.4 KiB

CoW Protocol + MycoFi: Quick Reference Card

One-Page Overview

What is CoW?

Intent-based DEX with batch auctions. Users sign intents (not transactions). Solvers compete to execute batches. MEV-protected. Solvers pay gas.

Your Integration

User Intent (private)
  ↓ Watch Tower polls
  ↓ MycoConditionalOrder.getTradeableOrder()
  ↓ BondingCurveAdapter.quoteBuy/quoteSell
  ↓ Solver competition
  ↓ GPv2Settlement.settle() executes
  ↓ User receives tokens (MEV-safe)

Contract Reference

Contract Address (Base) Role
GPv2Settlement 0x9008D19f... Settlement executor
GPv2VaultRelayer 0xc92e8bdf... Token pull intermediary
MycoBondingCurve 0x... (deploy) Pricing logic
BondingCurveAdapter 0x... (deploy) Pre-interaction hook
MycoConditionalOrder 0x... (deploy) Order type handler

Key Interfaces

IConditionalOrder (implement this)

function getTradeableOrder(
    address owner,                      // Safe wallet
    address sender,                     // Usually Watch Tower
    bytes calldata staticInput,         // Encoded params
    bytes calldata offchainInput        // Encoded quote + expiry
) external view returns (GPv2Order.Data);

function verify(...) external view;    // Validation

GPv2Order.Data (return this)

struct Data {
    address sellToken;        // USDC for buy
    address buyToken;         // MYCO for buy
    address receiver;         // Order recipient
    uint256 sellAmount;       // Exact input
    uint256 buyAmount;        // Minimum output
    uint32 validTo;           // Expiry timestamp
    bytes32 appData;          // Metadata
    uint256 feeAmount;        // Usually 0
    bytes32 kind;             // KIND_SELL or KIND_BUY
    bool partiallyFillable;   // Usually false
    bytes32 sellTokenBalance; // BALANCE_ERC20
    bytes32 buyTokenBalance;  // BALANCE_ERC20
}

Error Handling

// In getTradeableOrder():

// Temporary (retry next block)
if (quote_expired) revert PollTryNextBlock("Quote expired");
if (insufficient_balance) revert PollTryNextBlock("Balance changed");

// Current state invalid (try later)
if (quote_below_minimum) revert OrderNotValid("Quote below min");

// Stop polling (permanent issue)
if (handler_misconfigured) revert PollNever("Config error");

Stateless Handler Pattern

contract MycoConditionalOrder is IConditionalOrder {
    // NO state variables (except immutable refs)
    // All logic is view-only in getTradeableOrder()

    function getTradeableOrder(
        address owner,
        address,
        bytes calldata staticInput,  // Order params
        bytes calldata offchainInput // Live quote from Watch Tower
    ) external view returns (GPv2Order.Data memory) {
        // 1. Decode staticInput (set once at order creation)
        OrderData memory params = abi.decode(staticInput, (OrderData));

        // 2. Decode offchainInput (fresh quote from Watch Tower)
        OffchainData memory quote = abi.decode(offchainInput, (OffchainData));

        // 3. Validate
        if (block.timestamp > quote.validTo) revert OrderNotValid("Expired");
        if (quote.quotedOutput < params.minOutputAmount) revert OrderNotValid("Below min");

        // 4. Generate order
        return GPv2Order.Data({
            sellToken: params.direction == BUY ? USDC : MYCO,
            buyToken: params.direction == BUY ? MYCO : USDC,
            sellAmount: params.inputAmount,
            buyAmount: quote.quotedOutput,  // From Watch Tower
            receiver: params.receiver,
            validTo: quote.validTo,
            // ... other fields
        });
    }
}

Watch Tower Polling Lifecycle

Every block (every 2 sec on Base):
  FOR each handler in registry:
    FOR each order:
      staticInput = stored params
      offchainInput = adapter.quoteBuy() + timestamp
      
      TRY getTradeableOrder(owner, sender, static, offchain)
        ✓ Valid? → Submit to solvers
        ✗ PollTryNextBlock? → Retry next block
        ✗ OrderNotValid? → Skip, retry later
        ✗ PollNever? → Remove from polling

      offchainInput = BondingCurveAdapter.quoteBuy()
      Submit to solver network

Token Flow During Settlement

Settlement.settle() called by solver:

1. BEFORE INTERACTIONS
   └─ Nothing (for basic buy/sell)

2. INPUT TRANSFERS (via VaultRelayer)
   └─ VaultRelayer pulls 500 USDC from user's Vault
   └─ Transfers to BondingCurveAdapter

3. INTERACTIONS
   └─ BondingCurveAdapter.executeBuyForCoW(500 USDC, 900 MYCO min)
      ├─ Calls curve.buy(500 USDC)
      ├─ Curve mints 1000 MYCO to adapter
      └─ Returns 1000 to settlement

4. OUTPUT TRANSFERS (via VaultRelayer)
   └─ VaultRelayer pulls 1000 MYCO from adapter
   └─ Transfers to user

5. AFTER INTERACTIONS
   └─ Nothing (for basic buy/sell)

Result: User -500 USDC, +1000 MYCO

Critical Security Checklist

  • Users approve Balancer Vault (NOT Settlement)
  • Adapter has onlySettlement modifier
  • Quote expiry validated (block.timestamp <= quote.validTo)
  • Min output enforced (quote.output >= params.minOutputAmount)
  • Reentrancy guarded on hook functions
  • No permanent state in handler (stateless)
  • VaultRelayer can only transfer TO Settlement

MEV Protection Summary

Mechanism How it Works
Private Order Flow Intents never in public mempool → Bots can't see you
Uniform Clearing Prices All USDC→MYCO trades in batch at same price → Order can't be reordered for profit
Coincidence of Wants Alice ↔ Bob direct match → No AMM slippage → No MEV possible

Testing Quick Start

// Test handler returns valid GPv2Order
GPv2Order.Data memory order = handler.getTradeableOrder(
    owner, watchtower, staticInput, offchainInput
);
assert(order.buyAmount >= minOutput);
assert(order.validTo > block.timestamp);

// Test adapter executes correctly
usdc.transfer(adapter, 500e6);
uint256 mycoOut = adapter.executeBuyForCoW(500e6, 900e18);
assert(mycoToken.balanceOf(adapter) >= 900e18);

// Test quote functions
uint256 quote = adapter.quoteBuy(500e6);
assert(quote == curve.calculateBuyReturn(500e6));

Deployment Checklist

Base Mainnet Addresses:

✓ USDC: 0x833589fC4D06F649c466dB920d0135aa6Df1cDEA
✓ GPv2Settlement: 0x9008D19f58AAbD9eD0D60971565AA8510560ab41
✓ VaultRelayer: 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110

Deploy (in order):
1. MycoToken
2. MycoBondingCurve (set basePrice, coeff, exponent, fee, treasury)
3. BondingCurveAdapter
4. MycoConditionalOrder
5. Register in ComposableCoW (request Watch Tower access)

API Rate Limits

Quote requests:     10/second
Order submission:   5/second
General endpoints:  100/minute

Common Errors & Solutions

Error Cause Fix
OrderNotValid("Quote expired") Watch Tower quote >30min old Fetch fresh quote
PollTryNextBlock("Insufficient balance") User moved tokens Wait for balance recovery
OnlySettlement() revert Called adapter directly Only Settlement can call
SlippageExceeded Quote < minOutputAmount Increase minOut or retry


Key Metrics to Track

Per order:
  - Quote latency (target <100ms)
  - Time to execution (target <60s)
  - Actual price vs quote (should be >=)
  - Gas paid by solver (should be <fees collected)

Per batch (daily):
  - Orders submitted / executed / failed
  - Average surplus per order
  - MEV extraction (should be ~0)
  - Watch Tower polling rate

Monthly:
  - Total volume
  - User count
  - Protocol revenue (accumulated fees)

Golden Rules

  1. Handlers are stateless — All logic in getTradeableOrder() view function
  2. Quotes are ephemeral — Each poll gets fresh quote from adapter
  3. Errors must be clear — Use correct error type (PollTryNextBlock vs OrderNotValid)
  4. Fund access is protected — Users approve Vault, not Settlement
  5. Solvers are untrusted — But can't break anything (settlement atomic)

Version: 1.0
Updated: 2026-04-03
Status: Ready to reference during implementation