Investigating trading strategies using algebra

or, how I showed a strategy on Uniswap V2 was not profitable

Thu 02 Nov 2023

Disclaimer

This is an In Progress post. It is incomplete and of poorer quality than my other posts. It’s an experiment in encouraging me to publish more often.

Introduction

The other day, while I was playing around with Uniswap V2 maths, I dreamt up a possible attack. The idea was simple.

  1. Swap token Y for token X
  2. Add a ship-load of liquidity
  3. Swap then Xs from step 1 back to token Y
  4. Withdraw the liquidity from step 2

The idea behind this strategy was that the slippage in step 3 would be much smaller than step 1 because of the added liquidity. Could this asymmetry yield an advantage?

I just didn’t know.

It seemed clear that I’d make a profit on step 3 vs step 1, but this needed to be offset against the additional asymmetry in steps 2 and 4. When you withdraw the liquidity in step 4 you get back a different mix of token X and Y than you put into the pool in step 2.

Maybe these two things would cancel each other out or even cause a net loss. Or perhaps, depending on the quantities involved, you sometimes got a profit and sometimes a loss. I needed to investigate this more closely.

To be clear upfront, I didn’t actually think this attack was going work. Uniswap V2 has been around for too long for someone — were it real — not to accidentally notice it.

Spoiler alert: the attack doesn’t work so you can stop reading now if you’re not interested in attacks that don’t work. But if you’re interested in seeing why, we’ll prove it with the help of a little algebra.

Defining our variables

In this and following sections I’ll using notation similar to that found in the book Automated Market Makers: A Practical Guide to Decentralized Exchanges and Cryptocurrency Trading. However, you can find similar presentations in many places on the web.

We will now derive an equation that depends only on the variables kk and ff.

Deriving the algebra

In the following derivation I have purposely left out fees. If I can make a profit in the fee-less scenario then, as long as it’s greater than 0.3%, we’ve made a profit overall. Accounting for fees would almost certainly make the algebra much more complicated.

Let’s get started.

Uniswap V2 is a constant product AMM which means it obeys the constant product rule. Thus, if we swap kk percent of the initial balance of token X, A0A_0. From the pool’s perspective we have:

(A0kA0)(B0+b0)=A0B0(1k)(A0B0+A0b0)=A0B0A0b0=A0B01kA0B0A0b0=A0B0A0B0+kA0B01k=kA0B01kb0=k1kB0 \begin{align*} (A_0 - kA_0)(B_0 + b_0) & = A_0B_0 \\ (1 - k)(A_0B_0 + A_0b_0) &= A_0B_0 \\ A_0b_0 &= {A_0B_0 \over 1 - k} - A_0B_0 \\ A_0b_0 &= {A_0B_0 - A_0B_0 + kA_0B_0 \over 1 - k} \\ &= {kA_0B_0 \over 1 - k} \\ b_0 &= {k \over 1 - k}B_0 \tag{0} \\ \end{align*}

Thus we paid k1kB0{k \over 1 - k}B_0 in token Y.

Now we calculate the new token quantities in the pool

A1=(1k)A0B1=B0+k1kB0=(1k1k+k1k)B0=11kB0 \begin{align*} A_1 & = (1 - k)A_0 \tag{1}\\ B_1 &= B_0 + {k \over 1 - k}B_0 \\ &= \left( {1-k \over 1-k} + {k \over 1 - k} \right) B_0 \\ &= {1 \over 1 - k}B_0 \tag{2} \\ \end{align*}

We now begin step 2, adding ff times the existing liquidity to the pool. To simplify things we denominate the value of the pool in terms of token Y. We also assume that the market price of the tokens does not change during this process.

poolValue=B0A0A1+B1=B0A0(1k)A0+11kB0=(1k)B0+11kB0=(1k)2+11kB0 \begin{align*} poolValue &= {B_0 \over A_0}A_1 + B_1 \\ &= {B_0 \over A_0}(1-k)A_0 + {1 \over 1 - k}B_0 \tag{by (1) and (2)} \\ &= (1 - k)B_0 + {1 \over 1 - k}B_0 \\ &= {(1 - k)^2 + 1 \over 1 - k}B_0 \\ \end{align*}

Thus the cost of adding ff times this liquidity is

cost=f(1k)2+f1kB0 \begin{align*} cost &= {f(1 - k)^2 + f \over 1 - k}B_0 \tag{3} \\ \end{align*}

The pool now has more liquidity. Using equations (1)(1) and (2)(2) we have:

A2=(1+f)A1=(1+f)(1k)A0B2=(1+f)B1=1+f1kB0 \begin{align*} A_2 & = (1 + f)A_1 & = (1 + f)(1 - k)A_0 \tag{4}\\ B_2 & = (1 + f)B_1 \\ & = {1 + f \over 1 - k}B_0 \tag{5} \end{align*}

We now enter step 3 of the process and sell the tokens we received in step 1. That is, we sell kA0kA_0 of token X. Using the constant product rule again we get:

b2=kA0B2A2+kA0=k(1+f)1kA0B0(1+f)(1k)A0+kA0=k1k(1+f)A0B0((1+ffkk)+k)A0=k(1+f)(1k)(1+ffk)B0 \begin{align*} b_2 &= {kA_0B_2 \over A_2 + kA_0} \\ &= {{k(1 + f)\over{1 - k}}A_0B_0} \over {(1 + f)(1 - k)A_0 + kA_0} \tag{by (4) and (5)} \\ &= {k \over 1 - k} \cdot {(1 + f)A_0B_0 \over {((1 + f - fk - k) + k})A_0} \\ &= {k(1 + f) \over (1-k)(1 + f - fk)}B_0 \tag{6} \end{align*}

The new quantities in the pool are now:

A3=A2+kA0=(1+f)(1k)A0+kA0=(1+ffk)A0B3=B2b2=1+f1kB0k(1+f)(1k)(1+ffk)B0=1+f1k(1k(1+ffk))B0=1+f1k1+ffkk1+ffkB0=1+f1k(1k)(1+f)1+ffkB0=(1+f)21+ffkB0 \begin{align*} A_3 &= A_2 + kA_0 \\ &= (1 + f)(1 - k)A_0 + kA_0 \\ &= (1 + f - fk)A_0 \tag{7}\\ B_3 &= B_2 - b_2 \\ &= {1 + f \over 1 - k}B_0 - {k(1 + f) \over (1-k)(1 + f - fk)}B_0 \tag{by (5)}\\ &= {1 + f \over 1 - k}({{1 - {k \over (1 + f -fk)}})}B_0 \\ &= {1 + f \over 1 - k}\cdot{1 + f - fk -k \over 1 + f - fk}B_0 \\ &= {{1 + f \over 1 - k}\cdot{(1 -k)(1 + f) \over 1 + f - fk}}B_0 \\ &= {(1 + f)^2 \over 1 + f - fk}B_0 \tag{8} \end{align*}

Now we perform step 4 and withdraw our liquidity which is f1+ff \over {1 + f} of the total liquidity. What is the return?

return=f1+fB0A0A3+B3=f1+f(B0A0(1+ffk)A0+(1+f)21+ffkB0)=(f(1+ffk)1+f+f(1+f)1+ffk)B0=f(1+ffk)2+f(1+f)2(1+f)(1+ffk)B0 \begin{align*} return &= {f \over 1 + f}{B_0 \over A_0}A_3 + B_3 \\ &= {f \over 1 + f} \left( {B_0 \over A_0}(1 + f - fk)A_0 + {(1 + f)^2 \over 1 + f - fk}B_0 \right) \tag{by (7) and (8)}\\ &= \left( {f(1 + f - fk) \over 1 + f} + {f(1 + f) \over 1 + f -fk} \right)B_0 \\ &= {f(1 + f - fk)^2 + f(1 + f)^2 \over (1 + f)(1 + f - fk)}B_0 \tag{9}\\ \end{align*}

Phew. We now have all the ingredients necessary to work out our profit (or lack thereof).

First we’ll work out our return on step 3 vs step 1 which is just b2b0b_2 - b_0.

b2b0=k(1+f)(1k)(1+ffk)B0k1kB0=k1k1+f(1+ffk)1+ffkB0=fk2(1k)(1+ffk)B0 \begin{align*} & b_2 - b_0 \\ =& {k(1 + f) \over (1-k)(1 + f - fk)}B_0 - {k \over 1 - k}B_0 \tag{by (0) and (6)}\\ =& {k \over 1 - k}{1 + f - (1 + f - fk) \over 1 + f - fk}B_0 \\ =& {fk^2 \over (1 - k)(1 + f -fk)}B_0 \tag{10} \end{align*}

Now we can work out the profit.

profit=(b2b0)+returncost=fk2(1k)(1+ffk)B0+ f(1+ffk)2+f(1+f)2(1+f)(1+ffk)B0f(1k)2+f1kB0=(fk2(1k)(1+ffk)+f(1+ffk)2+f(1+f)2(1+f)(1+ffk)f(1k)2+f1k)B0 \begin{align*} profit &= (b_2 - b_0) + return - cost \\ &= {fk^2 \over (1 - k)(1 + f -fk)}B_0 + \\ &\ {f(1 + f - fk)^2 + f(1 + f)^2 \over (1 + f)(1 + f - fk)}B_0 - {f(1 - k)^2 + f \over 1 - k}B_0 \tag {by (3), (9) and (10)}\\ &= \left({fk^2 \over (1 - k)(1 + f -fk)} + {f(1 + f - fk)^2 + f(1 + f)^2 \over (1 + f)(1 + f - fk)} - {f(1 - k)^2 + f \over 1 - k}\right)B_0 \\ \end{align*}

Conclusion

This is pretty messy algebra and it took me forever to get it right. I was worried that I’d missed some obvious opportunities for simplification so I passed it through Symbolab hoping it would come with something neater. When it didn’t I just kept it the way it was.

What’s interesting about the definition of profitprofit is that it’s one giant multiple of B0B_0 i.e. the initial number of token Y in the pool. If this multiple is greater than zero then we have a profit! We don’t need to know the value of B0B_0.

So I typed this enormous and unwieldy equation into GeoGebra’s 3D plot (substituting xx for kk and yy for ff). I also restricted the value of kk and ff (xx and yy) for which it was plotted.

It ended up looking like this in GeoGebra.

It produced the most beautiful plot.

You can view and manipulate the plot here.

As you can see, at no point does the surface go above the xyxy plane. That is, there is no place in the range 0<k<10 < k < 1 and 0<f0 < f where its value is positive. I love the way it looks like a flexible sheet being peeled away from the xyxy-plane.

This proves that you can’t make any profit using this strategy.

In fact, as kk grows larger you make increasingly larger losses. The factor ff seems to act as an amplifier. If kk is fixed then the larger ff is, the larger the loss is.

Postscript: “Pay no attention to the man behind the curtain”

You might see all the algebra above and find it off-putting. Rest assured, I had to work quite hard to finish the job. I made (too) many mistakes along the way. One thing that helped me was to first do a concrete example and then, at each stage, sanity check my algebra by plugging in those values and seeing if they matched.

Initially I made the mistake of calculating a concrete example with k=0.5k = 0.5 and f=1.0f = 1.0. Even when there were still mistakes in the algebra, this meant that my sanity checking would show none. The reason for this was that, in this case, 1k=0.51 - k = 0.5 and fanything=anythingf * anything = anything. I took the time to calculate another concrete example with k=0.3k = 0.3 and f=1.2f = 1.2 which helped much more in sanity checking. It was very unlikely that plugging in those values to incorrect algebraic equations would yield the same results as my concrete example.