8.4 KiB
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
onlySettlementmodifier - 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 |
Links
- Docs: https://docs.cow.fi/
- GitHub: https://github.com/cowprotocol/
- SDK:
@cowprotocol/sdk(npm) - Forum: https://forum.cow.fi/
- API Base: https://api.cow.fi/base/
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
- Handlers are stateless — All logic in
getTradeableOrder()view function - Quotes are ephemeral — Each poll gets fresh quote from adapter
- Errors must be clear — Use correct error type (PollTryNextBlock vs OrderNotValid)
- Fund access is protected — Users approve Vault, not Settlement
- Solvers are untrusted — But can't break anything (settlement atomic)
Version: 1.0
Updated: 2026-04-03
Status: Ready to reference during implementation