├── .gitignore ├── README.md ├── arbitrage.py ├── latexify.py ├── liquidation.py ├── output └── empty └── two-asset.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pdf 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CFMM Optimal Routing 2 | This repository contains the code needed to generate the figures used in the paper 3 | [Optimal Routing for Constant Function Market Makers](https://stanford.edu/~guillean/papers/cfmm-routing.pdf). 4 | 5 | ## Requirements 6 | The requirements for running these examples are: 7 | - `NumPy` 8 | - `Matplotlib` 9 | - `Cvxpy` (see [here](https://www.cvxpy.org/install/index.html) for installation) 10 | 11 | In order to generate the figures as done in the paper you will also need a working TeX distribution. 12 | 13 | ## How to run 14 | All examples are self-contained and can be run directly, e.g.: 15 | ```bash 16 | python arbitrage.py 17 | ``` 18 | The figures were generated by running 19 | ```bash 20 | python two-asset.py 21 | ``` 22 | but note that this requires a working TeX distribution. (This can be avoided by commenting out any call to `latexify` in `two-asset.py` which requires `ps` as a backend for plotting.) 23 | -------------------------------------------------------------------------------- /arbitrage.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | 4 | # Problem data 5 | global_indices = list(range(4)) 6 | local_indices = [ 7 | [0, 1, 2, 3], 8 | [0, 1], 9 | [1, 2], 10 | [2, 3], 11 | [2, 3] 12 | ] 13 | 14 | reserves = list(map(np.array, [ 15 | [4, 4, 4, 4], 16 | [10, 1], 17 | [1, 5], 18 | [40, 50], 19 | [10, 10] 20 | ])) 21 | 22 | fees = [ 23 | .998, 24 | .997, 25 | .997, 26 | .997, 27 | .999 28 | ] 29 | 30 | # "Market value" of tokens (say, in a centralized exchange) 31 | market_value = [ 32 | 1.5, 33 | 10, 34 | 2, 35 | 3 36 | ] 37 | 38 | # Build local-global matrices 39 | n = len(global_indices) 40 | m = len(local_indices) 41 | 42 | A = [] 43 | for l in local_indices: 44 | n_i = len(l) 45 | A_i = np.zeros((n, n_i)) 46 | for i, idx in enumerate(l): 47 | A_i[idx, i] = 1 48 | A.append(A_i) 49 | 50 | # Build variables 51 | deltas = [cp.Variable(len(l), nonneg=True) for l in local_indices] 52 | lambdas = [cp.Variable(len(l), nonneg=True) for l in local_indices] 53 | 54 | psi = cp.sum([A_i @ (L - D) for A_i, D, L in zip(A, deltas, lambdas)]) 55 | 56 | # Objective is to maximize "total market value" of coins out 57 | obj = cp.Maximize(market_value @ psi) 58 | 59 | # Reserves after trade 60 | new_reserves = [R + gamma_i*D - L for R, gamma_i, D, L in zip(reserves, fees, deltas, lambdas)] 61 | 62 | # Trading function constraints 63 | cons = [ 64 | # Balancer pool with weights 4, 3, 2, 1 65 | cp.geo_mean(new_reserves[0], p=np.array([4, 3, 2, 1])) >= cp.geo_mean(reserves[0]), 66 | 67 | # Uniswap v2 pools 68 | cp.geo_mean(new_reserves[1]) >= cp.geo_mean(reserves[1]), 69 | cp.geo_mean(new_reserves[2]) >= cp.geo_mean(reserves[2]), 70 | cp.geo_mean(new_reserves[3]) >= cp.geo_mean(reserves[3]), 71 | 72 | # Constant sum pool 73 | cp.sum(new_reserves[4]) >= cp.sum(reserves[4]), 74 | new_reserves[4] >= 0, 75 | 76 | # Arbitrage constraint 77 | psi >= 0 78 | ] 79 | 80 | # Set up and solve problem 81 | prob = cp.Problem(obj, cons) 82 | prob.solve() 83 | 84 | print(f"Total output value: {prob.value}") -------------------------------------------------------------------------------- /latexify.py: -------------------------------------------------------------------------------- 1 | #Original Author: Prof. Nipun Batra 2 | # nipunbatra.github.io 3 | from math import sqrt 4 | import matplotlib 5 | 6 | SPINE_COLOR = 'gray' 7 | 8 | def latexify(fig_width=None, fig_height=None, font_size=12, columns=2): 9 | """Set up matplotlib's RC params for LaTeX plotting. 10 | Call this before plotting a figure. 11 | Parameters 12 | ---------- 13 | fig_width : float, optional, inches 14 | fig_height : float, optional, inches 15 | columns : {1, 2} 16 | """ 17 | 18 | # code adapted from https://scipy.github.io/old-wiki/pages/Cookbook/Matplotlib/LaTeX_Examples.html 19 | 20 | # Width and max height in inches for IEEE journals taken from 21 | # computer.org/cms/Computer.org/Journal%20templates/transactions_art_guide.pdf 22 | 23 | assert(columns in [1,2]) 24 | 25 | if fig_width is None: 26 | fig_width = 3.39 if columns==1 else 6.9 # width in inches 27 | 28 | if fig_height is None: 29 | golden_mean = (sqrt(5)-1.0)/2.0 # Aesthetic ratio 30 | fig_height = fig_width*golden_mean # height in inches 31 | 32 | MAX_HEIGHT_INCHES = 8.0 33 | if fig_height > MAX_HEIGHT_INCHES: 34 | print("WARNING: fig_height too large:" + fig_height + 35 | "so will reduce to" + MAX_HEIGHT_INCHES + "inches.") 36 | fig_height = MAX_HEIGHT_INCHES 37 | 38 | # NB (bart): default font-size in latex is 11. This should exactly match 39 | # the font size in the text if the figwidth is set appropriately. 40 | # Note that this does not hold if you put two figures next to each other using 41 | # minipage. You need to use subplots. 42 | params = {'backend': 'ps', 43 | 'text.latex.preamble': ['\\usepackage{gensymb}'], 44 | 'axes.labelsize': font_size, # fontsize for x and y labels (was 12 and before 10) 45 | 'axes.titlesize': font_size, 46 | 'font.size': font_size, # was 12 and before 10 47 | 'legend.fontsize': font_size, # was 12 and before 10 48 | 'xtick.labelsize': font_size, 49 | 'ytick.labelsize': font_size, 50 | 'text.usetex': True, 51 | 'figure.figsize': [fig_width,fig_height], 52 | 'font.family': 'serif' 53 | } 54 | 55 | matplotlib.rcParams.update(params) 56 | 57 | 58 | def format_axes(ax): 59 | 60 | for spine in ['top', 'right']: 61 | ax.spines[spine].set_visible(False) 62 | 63 | for spine in ['left', 'bottom']: 64 | ax.spines[spine].set_color(SPINE_COLOR) 65 | ax.spines[spine].set_linewidth(0.5) 66 | 67 | ax.xaxis.set_ticks_position('bottom') 68 | ax.yaxis.set_ticks_position('left') 69 | 70 | for axis in [ax.xaxis, ax.yaxis]: 71 | axis.set_tick_params(direction='out', color=SPINE_COLOR) 72 | 73 | return ax 74 | -------------------------------------------------------------------------------- /liquidation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | 4 | # Problem data 5 | global_indices = list(range(5)) 6 | local_indices = [ 7 | [0, 1, 2, 3, 4], 8 | [0, 1], 9 | [2, 3], 10 | [3, 4], 11 | [3, 4] 12 | ] 13 | 14 | reserves = list(map(np.array, [ 15 | [4, 4, 4, 4, 4], 16 | [10, 1], 17 | [1, 5], 18 | [40, 50], 19 | [10, 10] 20 | ])) 21 | 22 | fees = [ 23 | .998, 24 | .997, 25 | .997, 26 | .997, 27 | .999 28 | ] 29 | 30 | current_assets = [ 31 | 2, 32 | 1, 33 | 3, 34 | 5, 35 | 10 36 | ] 37 | 38 | # Build local-global matrices 39 | n = len(global_indices) 40 | m = len(local_indices) 41 | 42 | A = [] 43 | for l in local_indices: 44 | n_i = len(l) 45 | A_i = np.zeros((n, n_i)) 46 | for i, idx in enumerate(l): 47 | A_i[idx, i] = 1 48 | A.append(A_i) 49 | 50 | # Build variables 51 | deltas = [cp.Variable(len(l), nonneg=True) for l in local_indices] 52 | lambdas = [cp.Variable(len(l), nonneg=True) for l in local_indices] 53 | 54 | psi = cp.sum([A_i @ (L - D) for A_i, D, L in zip(A, deltas, lambdas)]) 55 | 56 | # Objective is to liquidate everything into token 4 57 | obj = cp.Maximize(psi[4]) 58 | 59 | # Reserves after trade 60 | new_reserves = [R + gamma_i*D - L for R, gamma_i, D, L in zip(reserves, fees, deltas, lambdas)] 61 | 62 | # Trading function constraints 63 | cons = [ 64 | # Balancer pool with weights 5, 4, 3, 2, 1 65 | cp.geo_mean(new_reserves[0], p=np.array([5, 4, 3, 2, 1])) >= cp.geo_mean(reserves[0]), 66 | 67 | # Uniswap v2 pools 68 | cp.geo_mean(new_reserves[1]) >= cp.geo_mean(reserves[1]), 69 | cp.geo_mean(new_reserves[2]) >= cp.geo_mean(reserves[2]), 70 | cp.geo_mean(new_reserves[3]) >= cp.geo_mean(reserves[3]), 71 | 72 | # Constant sum pool 73 | cp.sum(new_reserves[4]) >= cp.sum(reserves[4]), 74 | new_reserves[4] >= 0, 75 | 76 | # Liquidate all assets, except 4 77 | psi[0] + current_assets[0] == 0, 78 | psi[1] + current_assets[1] == 0, 79 | psi[2] + current_assets[2] == 0, 80 | psi[3] + current_assets[3] == 0 81 | ] 82 | 83 | # Set up and solve problem 84 | prob = cp.Problem(obj, cons) 85 | prob.solve() 86 | 87 | print(f"Total liquidated value: {psi.value[4]}") -------------------------------------------------------------------------------- /output/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angeris/cfmm-routing-code/65311c1661ee96961a00f2bb07f70013f912d707/output/empty -------------------------------------------------------------------------------- /two-asset.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import matplotlib.pyplot as plt 4 | from latexify import latexify 5 | 6 | # Problem data 7 | global_indices = list(range(3)) 8 | local_indices = [ 9 | [0, 1, 2], 10 | [0, 1], 11 | [1, 2], 12 | [0, 2], 13 | [0, 2] 14 | ] 15 | 16 | reserves = list(map(np.array, [ 17 | [3, .2, 1], 18 | [10, 1], 19 | [1, 10], 20 | 21 | # Note that there is arbitrage in the next two pools: 22 | [20, 50], 23 | [10, 10] 24 | ])) 25 | 26 | fees = np.array([ 27 | .98, 28 | .99, 29 | .96, 30 | .97, 31 | .99 32 | ]) 33 | 34 | amounts = np.linspace(0, 50) 35 | 36 | u_t = np.zeros(len(amounts)) 37 | 38 | all_values = [np.zeros((len(l), len(amounts))) for l in local_indices] 39 | 40 | for j, t in enumerate(amounts): 41 | current_assets = np.array([ 42 | t, 43 | 0, 44 | 0, 45 | ]) 46 | 47 | # Build local-global matrices 48 | n = len(global_indices) 49 | m = len(local_indices) 50 | 51 | A = [] 52 | for l in local_indices: 53 | n_i = len(l) 54 | A_i = np.zeros((n, n_i)) 55 | for i, idx in enumerate(l): 56 | A_i[idx, i] = 1 57 | A.append(A_i) 58 | 59 | # Build variables 60 | deltas = [cp.Variable(len(l), nonneg=True) for l in local_indices] 61 | lambdas = [cp.Variable(len(l), nonneg=True) for l in local_indices] 62 | 63 | psi = cp.sum([A_i @ (L - D) for A_i, D, L in zip(A, deltas, lambdas)]) 64 | 65 | # Objective is to trade t of asset 1 for a maximum amount of asset 3 66 | obj = cp.Maximize(psi[2]) 67 | 68 | # Reserves after trade 69 | new_reserves = [R + gamma_i*D - L for R, gamma_i, D, L in zip(reserves, fees, deltas, lambdas)] 70 | 71 | # Trading function constraints 72 | cons = [ 73 | # Balancer pool with weights 3, 2, 1 74 | cp.geo_mean(new_reserves[0], p=np.array([3, 2, 1])) >= cp.geo_mean(reserves[0], p=np.array([3, 2, 1])), 75 | 76 | # Uniswap v2 pools 77 | cp.geo_mean(new_reserves[1]) >= cp.geo_mean(reserves[1]), 78 | cp.geo_mean(new_reserves[2]) >= cp.geo_mean(reserves[2]), 79 | cp.geo_mean(new_reserves[3]) >= cp.geo_mean(reserves[3]), 80 | 81 | # Constant sum pool 82 | cp.sum(new_reserves[4]) >= cp.sum(reserves[4]), 83 | new_reserves[4] >= 0, 84 | 85 | # Allow all assets at hand to be traded 86 | psi + current_assets >= 0 87 | ] 88 | 89 | # Set up and solve problem 90 | prob = cp.Problem(obj, cons) 91 | prob.solve() 92 | 93 | for k in range(len(local_indices)): 94 | all_values[k][:, j] = lambdas[k].value - deltas[k].value 95 | 96 | print(f"Total liquidated value: {psi.value[2]}") 97 | for i in range(5): 98 | print(f"Market {i}, delta: {deltas[i].value}, lambda: {lambdas[i].value}") 99 | 100 | u_t[j] = obj.value 101 | 102 | latexify(fig_width=6, fig_height=3.5) 103 | for k in range(len(local_indices)): 104 | curr_value = all_values[k] 105 | for i in range(curr_value.shape[0]): 106 | coin_out = curr_value[i, :] 107 | plt.plot(amounts, coin_out, label=f"$(\\Lambda_{{{k+1}}} - \\Delta_{{{k+1}}})_{{{i+1}}}$") 108 | 109 | plt.legend(loc='center left', bbox_to_anchor=(1, .5)) 110 | plt.xlabel("$t$") 111 | plt.savefig("output/all_plot.pdf", bbox_inches="tight") 112 | 113 | latexify(fig_width=4, fig_height=3) 114 | plt.plot(amounts, u_t, "k") 115 | plt.ylim([0, np.max(u_t)+2]) 116 | plt.ylabel("$u(t)$") 117 | plt.xlabel("$t$") 118 | plt.savefig("output/u_plot.pdf", bbox_inches="tight") --------------------------------------------------------------------------------