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.