๐ค solvers are a key component in the cow protocol, serving as the matching engines that find the best execution paths for user orders. in this project, i implements a solver from scratch, running certain arbitrage strategies.
๐ more details, check my mirror post, on cowsol, an arb solver for CoW protocol.
Spread trades are the act of purchasing one security and selling another related security (legs) as a unit.
- One-leg limit price trade.
- In this type of order (e.g.,
orders/instance_1.json
), we have a limit price and one pool reserve (e.g.,A -> C
).
- In this type of order (e.g.,
- Two-legged limit price trade for a single execution path.
- In this type of order (e.g.,
orders/instance_2.json
), we have a two-legged trade (e.g.,A -> B -> C
), with only one option for each leg and it can be solved without any optimization.
- In this type of order (e.g.,
- Two-legs limit price trade for multiple execution paths.
- In this type of order (e.g.,
orders/instance_3.json
), we have a two-legged trade (e.g.,A -> B -> C
) with multiple pool reserves for each leg (e.g.,B1
,B2
,B3
), so the order can be completed dividing it through multiple paths and optimizing for maximum total surplus.
- In this type of order (e.g.,
- Support for constant-product AMMs, such as Uniswap v2 (and its forks), where pools are represented by two token balances.
- Support for limit price orders for single order instances.
- Support for limit price orders for multiple orders on a single reserve instance.
- Support for limit price orders for multiple orders on multiple reserve instances.
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
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 (liquidity sources), we choose the best solution by maximizing the surplus of an order:
surplus = exec_buy_amount - exec_sell_amount / limit_price
All amounts are expressed by non-negative integer numbers, represented in atoms (i.e., multiples of _
) to results to denote decimal position, allowing easier reading.
User orders describe a trading intent.
sell_token
: token to be sold (added to the amm pool).buy_token
: token to be bought (removed from the amm pool).sell_amount
: limit amount for tokens to be sold.buy_amount
: limit amount for tokens to be bought.exec_sell_amount
: how many tokens get sold after order execution.exec_buy_amount
: how many tokens get bought after order execution.allow_partial_fill
: ifFalse
, only fill-or-kill orders are executed.is_sell_order
: if it's sell or buy order.
amm_exec_buy_amount
: how many tokens the amm "buys" (gets) from the user, and it's the sum of allexec_sell_amount
of each path (leg) in the order execution.amm_exec_sell_amount
: how many tokens the amm "sells" (gives) to the user, and it's the sum of allexec_buy_amount
of each path (leg) in the order execution.market_price
: the price to buy/sell a token through the user order specs.prior_price
: the price to buy/sell a token prior to being altered by the order.prior_sell_token_reserve
: the initial reserve amount of the "sell" token, prior to being altered by the order.prior_buy_token_reserve
: the initial reserve amount of the "buy" token, prior to being altered by the order.updated_sell_token_reserve
: the reserve amount of the "sell" token after being altered by the order.updated_buy_token_reserve
: the reserve amount of the "buy" token after being altered by the order.
python3 -m venv venv
source ./venv/bin/activate
make install_deps
cp .env.example .env
vim .env
make install
Test your installation:
cowsol
cowsol -a <order file>
Example output:
๐ฎ AMMs available for orders/instance_1.json
{ 'AC': { 'reserves': { 'A': '10000_000000000000000000',
'C': '10000_000000000000000000'}}}
cowsol -o <order file>
Example output:
๐ฎ Orders for orders/instance_1.json
{ '0': { 'allow_partial_fill': False,
'buy_amount': '900_000000000000000000',
'buy_token': 'C',
'is_sell_order': True,
'sell_amount': '1000_000000000000000000',
'sell_token': 'A'}}
cowsol -s orders/instance_1.json
For example, for this user order instance:
{
"orders": {
"0": {
"sell_token": "A",
"buy_token": "C",
"sell_amount": "1000_000000000000000000",
"buy_amount": "900_000000000000000000",
"allow_partial_fill": false,
"is_sell_order": true
}
},
"amms": {
"AC": {
"reserves": {
"A": "10000_000000000000000000",
"C": "10000_000000000000000000"
}
}
}
}
Generates this output (logging set to DEBUG
):
๐ฎ 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.
And this solution:
{
"amms": {
"AC": {
"sell_token": "C",
"buy_token": "A",
"exec_buy_amount": "1000_000000000000000000",
"exec_sell_amount": "909_090909090909090909"
}
},
"orders": {
"0": {
"allow_partial_fill": false,
"is_sell_order": true,
"buy_amount": "900_000000000000000000",
"sell_amount": "1000_000000000000000000",
"buy_token": "C",
"sell_token": "A",
"exec_buy_amount": "909_090909090909090909",
"exec_sell_amount": "1000_000000000000000000"
}
}
}
Note:
- Input orders are located at
orders/
. - Solutions are located at
solutions/
.
cowsol -s orders/instance_2.json
For example, this user order instance:
{
"orders": {
"0": {
"sell_token": "A",
"buy_token": "C",
"sell_amount": "1000_000000000000000000",
"buy_amount": "900_000000000000000000",
"allow_partial_fill": false,
"is_sell_order": true
}
},
"amms": {
"AB2": {
"reserves": {
"A": "10000_000000000000000000",
"B2": "20000_000000000000000000"
}
},
"B2C": {
"reserves": {
"B2": "15000_000000000000000000",
"C": "10000_000000000000000000"
}
}
}
}
Generates this (DEBUG
) output:
๐ฎ 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.
And this solution:
{
"amms": {
"AB2": {
"sell_token": "B2",
"buy_token": "A",
"exec_buy_amount": "1000_000000000000000000",
"exec_sell_amount": "1818_181818181818181818"
},
"B2C": {
"sell_token": "C",
"buy_token": "B2",
"exec_buy_amount": "1818_181818181818181818",
"exec_sell_amount": "1081_081081081081081081"
}
},
"orders": {
"0": {
"allow_partial_fill": false,
"is_sell_order": true,
"buy_amount": "900_000000000000000000",
"sell_amount": "1000_000000000000000000",
"buy_token": "C",
"sell_token": "A",
"exec_buy_amount": "1081_081081081081081081",
"exec_sell_amount": "1000_000000000000000000"
}
}
}
cowsol -s orders/instance_3.json
For example, this user order instance:
{
"orders": {
"0": {
"sell_token": "A",
"buy_token": "C",
"sell_amount": "1000_000000000000000000",
"buy_amount": "900_000000000000000000",
"allow_partial_fill": false,
"is_sell_order": true
}
},
"amms": {
"AB1": {
"reserves": {
"A": "10000_000000000000000000",
"B1": "20000_000000000000000000"
}
},
"AB2": {
"reserves": {
"A": "20000_000000000000000000",
"B2": "10000_000000000000000000"
}
},
"AB3": {
"reserves": {
"A": "12000_000000000000000000",
"B3": "12000_000000000000000000"
}
},
"B1C": {
"reserves": {
"B1": "23000_000000000000000000",
"C": "15000_000000000000000000"
}
},
"B2C": {
"reserves": {
"B2": "10000_000000000000000000",
"C": "15000_000000000000000000"
}
},
"B3C": {
"reserves": {
"B3": "10000_000000000000000000",
"C": "15000_000000000000000000"
}
}
}
}
Generates this (DEBUG
) output:
๐ฎ 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.
And this solution:
{
"amms": {
"AB1": {
"sell_token": "B1",
"buy_token": "A",
"exec_sell_amount": "561_904237334502880476",
"exec_buy_amount": "289_073705673240477696"
},
"B1C": {
"sell_token": "C",
"buy_token": "B1",
"exec_sell_amount": "357_719965038404924081",
"exec_buy_amount": "561_904237334502880476"
},
"AB3": {
"sell_token": "B3",
"buy_token": "A",
"exec_sell_amount": "671_163952522389243203",
"exec_buy_amount": "710_926294326759522304"
},
"B3C": {
"sell_token": "C",
"buy_token": "B3",
"exec_sell_amount": "943_426540218806186671",
"exec_buy_amount": "671_163952522389243203"
}
},
"orders": {
"0": {
"allow_partial_fill": false,
"is_sell_order": true,
"buy_amount": "900_000000000000000000",
"sell_amount": "1000_000000000000000000",
"buy_token": "C",
"sell_token": "A",
"exec_buy_amount": "1301_146505257211110752",
"exec_sell_amount": "1000_000000000000000000"
}
}
}
Note: the derivation for the optimization equation for this strategy can be seen here.
- Add support for more than two legs.
- Add support for more than two pools on two-legged trade.
- Add multiple path graph weighting and cyclic arbitrage detection using the Bellman-Ford algorithm, so that we can optimize by multiple paths without necessarily dividing the order through them. This would allow obtaining arbitrage profit through finding profitable negative cycles (e.g.,
A -> B -> C -> D -> A
).
- Add support for Balancer's weighted pools.
- Add support for Uniswap v3 and forks.
- Add support for stable pools.
- Implement support for AMM fees.
- Add support for concurrency (
async
), so tasks could run in parallel adding temporal advantage to the solver. - Benchmark and possibly add other optimization algorithms options.
- Add an actual execution class (through CoW server or directly to the blockchains).
- Finish implementing end-to-end BUY limit orders.
- Add unit tests.
- add the bot server (on docker).