on cowsol, my arb solver for cowย protocol

tl; dr

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.


๐ŸŽถ todayโ€™s mood


๐Ÿฎ๐Ÿงฉ piece #1: coincidence of wants

๐Ÿ’ก 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โ€.

๐Ÿซฑ๐Ÿปโ€๐Ÿซฒ๐Ÿฝ put it simply, itโ€™s a protocol for the good old no-middle-man free-market:

โ€œ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โ€.

https://swap.cow.fi/
https://swap.cow.fi/

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).

https://cow-protocol.medium.com/do-cows-slip-age-less-389150154f84
https://cow-protocol.medium.com/do-cows-slip-age-less-389150154f84

mev extraction should be aligned with the ethos of decentralization, right?

โ™‡

๐Ÿซก letโ€™s unpack.


๐Ÿฎ๐Ÿงฉ piece #2: cow swap overview

๐Ÿ’ก 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.

https://docs.cow.fi/overview/batch-auctions
https://docs.cow.fi/overview/batch-auctions

mev protection

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.

the orderbook and the driver

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:

  1. user approves the contract allowance manager to enable trading for a token.

  2. users place limit sell/buy orders off-chain by signing a message with their trade details.

  3. solvers pick up the off-chain orders and calculate the best way to settle them in a batch auction.

  4. the protocol selects the best order settlement solution, maximizing trader welfare, with the best clearing prices in that batch.

  5. users enjoy their new tokens.

no lps

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).

pre-signing order and off-chain signing

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.

data fun

so how this protocol is performing?

the cow protocol team provides several dune boards for:

a snapshot of CoW swap's yearly volume
a snapshot of CoW swap's yearly volume

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).

a batch with volume of $14mil, by a trader swapping stETH to WETH.
a batch with volume of $14mil, by a trader swapping stETH to WETH.

๐Ÿ’ก because cow protocol moves settlement to the application layer, transactions can be inspected in their explorer at explorer.cow.fi.

 batch volume of $1.3 mil, by a trader swapping USDC to WETH,  creating the largest surplus for september 2022: 37.7 ETH or a 5.25% surplus.
batch volume of $1.3 mil, by a trader swapping USDC to WETH, creating the largest surplus for september 2022: 37.7 ETH or a 5.25% surplus.

๐Ÿฎ๐Ÿงฉ piece #3: solvers

๐Ÿ’ก 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).

โš–๏ธโœจ for me, this is the killing part.

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?

orders

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.

surplus

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 )
https://dune.com/cryptok/cow.fi
https://dune.com/cryptok/cow.fi

๐Ÿ•น okay, now that we understand the protocol, letโ€™s look at our proof-of-concept solver, cowsol.

(the full source is open-sourced at my github)


๐Ÿฎ๐Ÿงฉ piece #4: cowsol, the arber

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:

๐ŸŒ of course, it can get very complicated with edge cases and harder optimization requirements. this is the beauty of the problem that you need to solve. nobody is going to tell you their solution, thatโ€™s how this game works.

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).

๐Ÿ• no free lunch, anon.


strategy #1: one-leg limit price trade

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:

  1. load such JSON order

  2. create an instance of the OrderApi class and then for each order in the batch:

    1. parse the order and the amms dicts to a suitable in-memory format

    2. create an instance of SpreadSolverApi and run solve() for the order, which will call ConstantProductAmmApi.

  3. 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.

strategy #2: two-legged limit price trade for a single execution path

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.

strategy #3: two-legged limit price trade for multiple execution paths

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.

๐Ÿฎ๐Ÿงฉ piece #5: asymptotic protopia

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.


โ–‡ โ™„

Subscribe to go outside labs
Receive the latest updates directly to yourย inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.