today i go over one of my MEV bots, cowsol, which runs arbitrage strategies for CoW protocol.
set a nice soundtrack, grab your favorite beverage, and solve the puzzles with me.
💡 the cow protocol is a fully permissionless trading protocol that uses this novel idea of batch auctions to find prices and maximize liquidity, the “coincidence of wants”.
“i have what you want, you have what i want, let’s chat directly and maximize our surplus, instead of transferring this convo to external market makers or liquidity providers and paying their fees”.
in other words, instead of relying on mev-prone amms (e.g., “loss versus rebalancing" mev) or some concealed clob, cow protocol uses the network of 1) traders + 2) solvers for fair and trustless p2p trading.
using batch auctions means the settlement layer does not need to access on-chain liquidity.
moving the market off-chain protects traders against predacious players from the mev game (looking at ya, frontrunners & friends) and directly helps with better prices for trades (this is a hard problem, but you can initially think gas fees optimization and less slippage).
💡 cow swap is the first trading interface built on top of cow Protocol and acts as a “meta” dex aggregator, giving the user the best prices across aggregators or amms within a batch.
since cow swap leverages batch auctions:
there is no need for ordering the transactions within a batch and no value can be extracted by placing them in a certain order (preventing primary strategies used in mev such as frontrunners or sandwiches).
independent parties, the solvers, settle the trade on-chain. they can be a person or an entity that submits solutions that maximize trade surplus for a given batch (preventing manipulation surface of miners and frontrunners).
the existence of cows may significantly reduce the amount that has to be exchanged via external mev-prone protocols.
cow swap works on top of two main services:
the orderbook (database), which stores trade data.
the driver (pooling), which queries the orderbook for orders (solvable_orders)
, settles them with different solvers, and then executes trades on-chain.
here is how the order flow works in this model:
user approves the contract allowance manager to enable trading for a token.
users place limit sell/buy orders off-chain by signing a message with their trade details.
solvers pick up the off-chain orders and calculate the best way to settle them in a batch auction.
the protocol selects the best order settlement solution, maximizing trader welfare, with the best clearing prices in that batch.
users enjoy their new tokens.
instead of relying on liquidity providers, cow swap connects on-chain liquidity from different protocols. active market makers can observe the orderbook and place counter orders, creating a cow (and preventing trades to be settled by external liquidity).
on cow swap, trades can be initialized by using a signing method called pre-sign, which can be invoked by any contract. pre-signing an order in the settlement contract is equivalent to providing an off-chain signature for the orderId
.
additionally, the cow protocol has recently introduced their concept of smart orders, by providing a new form of on-chain signature verification through EIP-1271 support for off-chain signing.
custom validation logic can be built on orders (through implementing an isValidSignature
method), allowing smart contracts to have their own signature validation scheme (and facilitating things such as stop-loss orders, time weighted average price orders, and so much more jazz).
a cool example is the implementation of curved orders.
so how this protocol is performing?
the cow protocol team provides several dune boards for:
let’s conclude this section by looking at some numbers: batches with a large percentage of cow trades (like this one), batches with very large volumes (like this one), and batches with very large surplus for users (like this one).
💡 because cow protocol moves settlement to the application layer, transactions can be inspected in their explorer at explorer.cow.fi.
💡 in the cow protocol, multiple and independent solvers compete for finding the best solution to the batch auction problem (serving as matching engines that find the best execution paths for users).
an option to having the mev your transaction generates shared among the same old players or established firms, with cow protocol the solver role is outsourced to anyone who can prove they can do the job smartly (as opposed to knowing someone inside).
every few seconds, a competition round gives solvers the chance to find a solution and be rewarded for a batch reflecting the most recent orderbook state.
the winner is the one that maximizes the trade surplus by either 1) having the most optimal cow, 2) finding the best liquidity sources, or 3) combining both in settlement (taking into account total collected fees and execution cost).
are you up for the challenge, anon?
in the cow protocol, user orders are JSON
files that describe trader intent (plus some trade metadata):
sell_token
: token to be sold
buy_token
: token to be bought
sell_amount
: limit amount for tokens to be sold
buy_amount
: limit amount for tokens to be bought
is_sell_order
: if it's sell or buy order (order kind)
allow_partial_fill
: if False
, only fill-or-kill orders are executed
currently, cow swap only accepts limit orders.
💡 A limit order is an order to buy or sell with a restriction on the maximum price to be paid or the minimum price to be received (the "limit price").
this limit determines when an order can be executed:
limit price = sell amount / buy amount
>= executed buy amount / executed sell amount
for instance, a limit SELL order is specified by a maximum sell amount (the maximum amount that the user is willing to sell).
💡 a good rule of thumb is that the price impact of your order is about twice the size of your order relative to the pool.
for multiple execution paths (reserves) the best solution can be found in maximizing the surplus of an order:
surplus = exec buy amount - ( exec sell amount / limit price )
(the full source is open-sourced at my github)
here is the basic gist:
we start with a JSON
with an order that needs to be solved, it contains a dict
for orders
and a dict for amms
reserves, like this one:
cowsol parses the order data and starts searching for a solution to fill these orders. for the order above, this would be the solution:
by the way, for the order above, a simple algorithm for multidimensional unconstrained optimization without derivatives would suffice (of course, someone can always come up with an even faster very simple solution).
okay, let’s talk a little more about cowsol’s main strategies. we shall start with the simplest type.
we are developing solutions for (“no-market-maker”) spread arbitrage on uniswap v2 pools (and its forks). for a review of how uniswap v2 arbitrage works, check my other tool, bdex. i will assume you are familiar with constant-product amms.
💡 spread trades are the act of purchasing one security and selling another related security (legs) as a unit.
cowsol’s first strategy is very simple and straightforward, we solve for just one pool reserve, without intermediate tokens (just one leg):
STRATEGY 1: buy || sell token A ↠ sell || buy token C
in pictures, we want to solve this type of input orders instance:
cowsol’s entry point would:
load such JSON
order
create an instance of the OrderApi
class and then for each order in the batch:
parse the order
and the amms
dicts to a suitable in-memory format
create an instance of SpreadSolverApi
and run solve()
for the order, which will call ConstantProductAmmApi.
save the solution to JSON
so, what the OrderApi
class does? it pretty much looks at what type of order being input (for example, in this case, one-leg-trade
), and then it makes sure all the data is valid and ready to go.
as you could guess, the hot stuff happens inside SpreadSolverApi
. the public method solver()
takes the order type and run the appropriate strategy (one-leg-trade
, two-legs-trade
, etc.) for each pool reserve pairs from the given amms
dict:
every strategy follows the same high-level route, calling ConstantProductAmmApi
(which implements how uniswap v2 trades) to retrieve the constant product resulting data and surplus for the order:
what exact information does ConstantProductAmmApi
calculate?
first, it finds the executed sell or buy amount, which is derived by the constant-product amm equation and represents the retrieval of tokens B from selling an amount t
of tokens A in an a*b
pool reserve:
δ ≤ (b − a * b) / (a + t)
= (b * t) / (a + t)
from there, one can calculate other stuff such as prices, surplus, exchange rates, etc.
the JSON
solution for the order above is:
if you run cowsol CLI in DEBUG
mode, this is what you would see:
🐮 Solving orders/instance_1.json.
🐮 Order 0 is a sell order.
🐮 One-leg trade overview:
🐮 ➖ sell 1000_000000000000000000 of A, amm reserve: 10000_000000000000000000
🐮 ➕ buy 900_000000000000000000 of C, amm reserve: 10000_000000000000000000
🟨 Prior sell reserve: 10000_000000000000000000
🟨 Prior buy reserve: 10000_000000000000000000
🟨 Spot sell price 1.0
🟨 Spot buy price 1.0
🟨 AMM exec sell amount: 1000_000000000000000000
🟨 AMM exec buy amount: 909_090909090909090909
🟨 Updated sell reserve: 11000_000000000000000000
🟨 Updated buy reserve: 9090_909090909090909091
🟨 Market sell price 1.21
🟨 Market buy price 0.8264462809917356
🟨 Can fill: True
🐮 TOTAL SURPLUS: 9_090909090909090909
🐮 Results saved at solutions/solution_1_cowsol.json.
now, imagine a user order with a two-legged trade (e.g., A -> B -> C
), with only one option for each leg, so that it can be solved without optimization.
STRATEGY 2: buy || sell token A ↠ sell || buy token B ↠ buy || sell token C
here is an example of this type of input order instance:
this is another very straightforward order. the solver needs to calculate the data for the first leg, update the input data for the second leg, calculate the data for the second, and return the results:
This would be the solution for the input order instance above:
and if you are running cowsol CLI on DEBUG
mode, you would see:
🐮 Solving orders/instance_2.json.
🐮 Order 0 is a sell order.
🐮 FIRST LEG trade overview:
🐮 ➖ sell 1000_000000000000000000 of A
🐮 ➕ buy some amount of B2
🟨 Prior sell reserve: 10000_000000000000000000
🟨 Prior buy reserve: 20000_000000000000000000
🟨 Spot sell price 0.5
🟨 Spot buy price 2.0
🟨 AMM exec sell amount: 1000_000000000000000000
🟨 AMM exec buy amount: 1818_181818181818181818
🟨 Updated sell reserve: 11000_000000000000000000
🟨 Updated buy reserve: 18181_818181818181818180
🟨 Market sell price 0.605
🟨 Market buy price 1.6528925619834711
🟨 Can fill: True
🐮 SECOND LEG trade overview:
🐮 ➖ sell 1818_181818181818181818 of B2
🐮 ➕ buy some amount of C
🟨 Prior sell reserve: 15000_000000000000000000
🟨 Prior buy reserve: 10000_000000000000000000
🟨 Spot sell price 1.5
🟨 Spot buy price 0.6666666666666666
🟨 AMM exec sell amount: 1818_181818181818181818
🟨 AMM exec buy amount: 1081_081081081081081081
🟨 Updated sell reserve: 16818_181818181818181820
🟨 Updated buy reserve: 8918_918918918918918919
🟨 Market sell price 1.8856749311294765
🟨 Market buy price 0.5303140978816655
🟨 Can fill: True
🐮 TOTAL SURPLUS: 181_081081081081081081
🐮 Results saved at solutions/solution_2_cowsol.json.
in this third type of order, cowsol needs to solve for two-legged trades (e.g., A -> Bi -> C
), with multiple pool reserves for each leg (B1
, B2
, B3
,…), so that the order is completed by dividing it through multiple paths and optimizing for maximum total surplus.
STRATEGY 3: buy or sell token A ↠ sell or buy tokens Bi, i E [1,∞] ↠ buy or sell token C
here is an example of such an order:
let’s look at how solve()
would deal with this order:
for this simple case, this solution can be settled by a simple algorithm for scalar multidimensional unconstrained optimization, such as nelder-mead:
such that the solution JSON
would look like:
and if you are running cowsol cli on DEBUG
mode:
🐮 Solving orders/instance_3.json.
🐮 Order 0 is a sell order.
🐮 Using the best two execution simulations by surplus yield.
🐮 FIRST LEG trade overview:
🐮 ➖ sell 289_073705673240477696 of A
🐮 ➕ buy some amount of B1
🟨 Prior sell reserve: 10000_000000000000000000
🟨 Prior buy reserve: 20000_000000000000000000
🟨 Spot sell price 0.5
🟨 Spot buy price 2.0
🟨 AMM exec sell amount: 289_073705673240477696
🟨 AMM exec buy amount: 561_904237334502880476
🟨 Updated sell reserve: 10289_073705673240477700
🟨 Updated buy reserve: 19438_095762665497119520
🟨 Market sell price 0.5293251886038823
🟨 Market buy price 1.8891978343927718
🟨 Can fill: True
🐮 SECOND LEG trade overview:
🐮 ➖ sell 561_904237334502880476 of B1
🐮 ➕ buy some amount of C
🟨 Prior sell reserve: 23000_000000000000000000
🟨 Prior buy reserve: 15000_000000000000000000
🟨 Spot sell price 1.5333333333333334
🟨 Spot buy price 0.6521739130434783
🟨 AMM exec sell amount: 561_904237334502880476
🟨 AMM exec buy amount: 357_719965038404924081
🟨 Updated sell reserve: 23561_904237334502880480
🟨 Updated buy reserve: 14642_280034961595075920
🟨 Market sell price 1.609169076200932
🟨 Market buy price 0.6214387380354636
🟨 Can fill: True
🐮 FIRST LEG trade overview:
🐮 ➖ sell 710_926294326759522304 of A
🐮 ➕ buy some amount of B3
🟨 Prior sell reserve: 12000_000000000000000000
🟨 Prior buy reserve: 12000_000000000000000000
🟨 Spot sell price 1.0
🟨 Spot buy price 1.0
🟨 AMM exec sell amount: 710_926294326759522304
🟨 AMM exec buy amount: 671_163952522389243203
🟨 Updated sell reserve: 12710_926294326759522300
🟨 Updated buy reserve: 11328_836047477610756800
🟨 Market sell price 1.1219975504153292
🟨 Market buy price 0.8912675429904732
🟨 Can fill: True
🐮 SECOND LEG trade overview:
🐮 ➖ sell 671_163952522389243203 of B3
🐮 ➕ buy some amount of C
🟨 Prior sell reserve: 10000_000000000000000000
🟨 Prior buy reserve: 15000_000000000000000000
🟨 Spot sell price 0.6666666666666666
🟨 Spot buy price 1.5
🟨 AMM exec sell amount: 671_163952522389243203
🟨 AMM exec buy amount: 943_426540218806186671
🟨 Updated sell reserve: 10671_163952522389243200
🟨 Updated buy reserve: 14056_573459781193813330
🟨 Market sell price 0.7591582673440884
🟨 Market buy price 1.317248382868167
🟨 Can fill: True
🐮 TOTAL SURPLUS: 401_146505257211110752
🐮 Results saved at solutions/solution_3_cowsol.json.
cow protocol uses a fair and decentralized settlement through solvers. and solvers can be anyone who is able to solve the batch auction optimization problem the best.
in this post, i walked you through a poc for a simple solver bot to get started on the game. of course, things can get very complicated (but fun). for instance, you might want to add things such as support for concurrency, support for more than two pool reserves on two-legged trades, support for more than two legs (through, for example, bellman-ford algorithm), support for specific tokens and reserves (balancer’s weighted pools, uniswap v3, and forks, stable pools, private pools, external dexes, etc.), or whatever else your mind can create. find more solver documentation here.
ah, by the way, cow dao (the open organization of developers, traders, and market makers focused on fair and decentralized trading through the cow protocol) has several grants available for builders (including building solvers). they also open-sourced a solver template that can connect to the driver and place real orders.