How to Handle Uniswap Withdrawals like an OG

Ian Moore, PhD
Coinmonks
Published in
8 min readOct 6, 2023

--

  • Illustrate problem when performing a withdraw using Uniswap
  • Walkthrough of the solution
  • Utilizes the UniswapPy python package

1. Introduction

An automated market maker (AMM) protocol is the mechanism used by decentralized exchanges (DEXs) and was first introduced by Uniswap, which was launched on the Ethereum mainnet in November 2018. These DEXs consist of liquidity pools (LPs) represented by various trading pairs (eg. ETH/USDC, ETH/WBTC, etc.) acting as the AMM. Trading activity within these LPs are governed via smart contract through the constant product trading (CPT) formula:

where x represents the number of tokens for one asset, y represents the number of tokens for the other, and k is a constant, which helps maintain asset price.

Simple enough, ok, if any of you remember first learning about this protocol, or are learning about CPT for the first time, you may be thinking, I have an ETH/USDC LP and want to withdraw some ETH, can’t I just withdraw it directly? You’ll quickly realize that just won’t cut it; see Fig 1. When providers add or withdraw funds from an LP using CPT, the apportioned value to each asset is split 1:1. For instance, say Ethereum is priced at $1,500 per ETH and a provider wants to withdraw from an ETH/USDC LP, then the provider would have to extract 1 ETH along with an additional $1,500 USDC for a total initial withdrawal value of $3,000 USD.

Fig 1: Clip from UniswapV2Router01.sol; on line 110, as a result of the burn, we see amounts from both assets in the pair being extracted simultaneously

Alright, now that we have that understood, we now have another little problem. In example (outlined in the next Section), we don’t want ETH and USDC from the LP, we only want ETH. That can easily be done in two steps: (step 1) withdraw both ETH and USDC, then; (step 2) trade your USDC for ETH, and leave with only ETH. Cool, now say we want 100 ETH our from the LP; so don’t we just cut in half and request 50 ETH and the equivalent amount in USDC, then swap the USDC for ETH and get our 100 ETH? Not quite, because after we perform our first extraction, the pool balances have changed slightly, and when we move to perform our second step, we will never quite get what the exact amount requested.

However, with a bit of algebra, we can get you that request, which we will cover in this walkthrough, and by the time you’re through the steps below you’ll be setting up your LP withdrawals … like an OG!

2. Problem Walkthrough

Here, we walk through the problem outlined in the previous Section, so we first begin by defining what I call the Indexing Problem. The indexing problem is: we want to extract ΔL (LP tokens) from an LP, and wish to determine how much of a specific token (ie, Δyi) required to meet that withdraw request.

Indexing problem (defined)

Given the definition of constant product trading (CPT) as:

where:
* x → reserve0 (r0)
* y → reserve1 (r1)
* Δx → swap x (a0)
* Δy → swap y (a1)
* L → total supply
* γ → fee (ie, 997/1000)

We define the indexing solution via the following system of equations:

where
* Δ yi → indexed token
* ΔL → liquidity deposit

This little system is quite useful in getting around many problems in Uniswap (which we discuss in detail here), including the one outlined in this walkthrough.


from uniswappy.cpt.factory import Factory
from uniswappy.process.liquidity import RemoveLiquidity
from uniswappy.process.swap import Swap

tkn = ERC20("TKN", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = tkn, symbol="LP", address="0x011")

factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount+100, tkn_amount, eth_amount+100, tkn_amount)

print('***\nInitial LP\n***')
lp.summary()

amt_out = 100
token_out = eth
trading_token = tkn
user_nm1 = 'user0'
#rate = 0

# Step 1: withdrawal
p_out = 0.5
removeLiq = RemoveLiquidity()
res = removeLiq.apply(lp, token_out, user_nm1, p_out*amt_out)

print('***\nLP post step 1\n***')
lp.summary()

# Step 2: swap
out = Swap().apply(lp, trading_token, user_nm1, res[trading_token.token_name])

print('***\nLP post step 2\n***')
lp.summary()

withdrawn = res[eth.token_name] + out

print('Total withdrawn is {:.6f} + {:.6f} = {:.6f} \
ETH'.format(p_out*amt_out, out, withdrawn))
print('Of the requested {} ETH, a total of {:.6f} ETH \
has been withdrawn when using a 50/50 split'.format(amt_out, withdrawn))

The above code block produces the output below which illustrates our problem when we naively assume a 50% cut for step 1:

## Output
***
Initial LP
***
Exchange ETH-TKN (LP)
Reserves: ETH = 1100, TKN = 100000
Liquidity: 10488.088481701516

***
LP post step 1
***
Exchange ETH-TKN (LP)
Reserves: ETH = 1050.0, TKN = 95454.54545454546
Liquidity: 10011.35718707872

***
LP post step 2
***
Exchange ETH-TKN (LP)
Reserves: ETH = 1002.4094194662908, TKN = 100000.0
Liquidity: 10011.35718707872

Total withdrawn is 50.000000 + 47.590581 = 97.590581 ETH
Of the requested 100 ETH, a total of 97.590581 ETH has been withdrawn
when using a 50/50 split

As we can see above, we get 97.590581 of our 100 ETH request.

Let’s now address the problem …

Using the system of equations outlined in the indexing problem above, the third equation (Eq. 3) can be rearranged as:

Using our system of equations again from the the indexing problem, plug the first equation (Eq. 1) and second equation (Eq. 2) into above, and we get:

The above equation gets reduced to the following quadratic:

Now, solve for L using calc_lp_settlement

def calc_lp_settlement(lp, token_in, itkn_amt):

if(token_in.token_name == lp.token1):
x = lp.reserve0
y = lp.reserve1
else:
x = lp.reserve1
y = lp.reserve0

L = lp.total_supply
gamma = 997

a1 = x*y/L
a2 = L
a = a1/a2
b = (1000*itkn_amt*x - itkn_amt*gamma*x + 1000*x*y + x*y*gamma)/(1000*L);
c = itkn_amt*x;

dL = (b*a2 - a2*math.sqrt(b*b - 4*a1*c/a2)) / (2*a1);
return dL

Let’s check it out

from uniswappy.cpt.factory import Factory

eth = ERC20("ETH", "0x09")
tkn = ERC20("TKN", "0x111")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = tkn, symbol="LP", address="0x011")

factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount+100, tkn_amount, eth_amount+100, tkn_amount)
lp.summary()

eth_amt = 100
dL = calc_lp_settlement(lp, eth, eth_amt)

### Output
Exchange ETH-TKN (LP)
Reserves: ETH = 1100, TKN = 100000
Liquidity: 10488.088481701516

A request of 100 ETH requires a settlement of 488.787567 LP token

Now, for correctness, we double check against the quadratic (solved above)

y = lp.reserve0
x = lp.reserve1
L = lp.total_supply
gamma = 997

(dL**2)*x*y/(L*L) - dL*((1000*eth_amt*x - eth_amt*gamma*x + 1000*x*y + x*y*gamma)/(1000*L)) + eth_amt*x

### Output
-5.587935447692871e-09

Excellent, not counting for floating point error, we effectively get zero, when checked against our solved quadratic. Now using ΔL, we can determine the actual splitting distribution to achieve seamless withdrawals. Reconsidering Eq. 3, we redefine Δy and Δy(swap) by portion α, thus:

Therefore, using Eq. 2 we calculate our distribution as:

Hence, using the above equation and L from our solver, we can calculate the withdraw distribution α via calc_withdraw_portion

def calc_withdraw_portion(lp, token_in, amt):

if(token_in.token_name == lp.token1):
x = lp.reserve0
y = lp.reserve1
else:
x = lp.reserve1
y = lp.reserve0

L = lp.total_supply
gamma = 997/1000

dL = calc_lp_settlement(lp, token_in, amt)
dx = dL*x/L
dy = dL*y/L
aswap = (gamma*dx)*(y-dy)/(x-dx+gamma*dx)

return dy/amt

Let’s now get the exact calculation:

alpha = calc_withdraw_portion(lp, eth, eth_amt)
print('The correct portion (for step 1) is {:.6f}'.format(alpha))

## Output
The correct portion (for step 1) is 0.512645

Now we have the exact portion for step 1; as you can see, its not quite the 50% as we naively assumed in the introduction Section. Let’s now run through the steps to a WithdrawSwap and compare above:

from uniswappy.cpt.factory import Factory
from uniswappy.process.liquidity import RemoveLiquidity
from uniswappy.process.swap import Swap

tkn = ERC20("TKN", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = tkn, symbol="LP", address="0x011")

factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount+100, tkn_amount, eth_amount+100, tkn_amount)

print('***\nInitial LP\n***')
lp.summary()

amt_out = 100
token_out = eth
user_nm = 'user0'

# Step 1: withdrawal
p_out = calc_withdraw_portion(lp, token_out, amt_out)
removeLiq = RemoveLiquidity()
res = removeLiq.apply(lp, token_out, user_nm, p_out*amt_out)

print('***\nLP post step 1\n***')
lp.summary()

# Step 2: swap
out = Swap().apply(lp, trading_token, user_nm, res[trading_token.token_name])

print('***\nLP post step 2\n***')
lp.summary()

withdrawn = res[eth.token_name] + out

print('Total withdrawn is {:.6f} + {:.6f} = {:.6f} \
ETH'.format(p_out*amt_out, out, withdrawn))
print('Of the requested {} ETH, a total of {:.6f} ETH \
has been withdrawn'.format(amt_out, withdrawn))
## Output

***
Initial LP
***
Exchange ETH-TKN (LP)
Reserves: ETH = 1100, TKN = 100000
Liquidity: 10488.088481701516

***
LP post step 1
***
Exchange ETH-TKN (LP)
Reserves: ETH = 1048.735527472211, TKN = 95339.59340656463
Liquidity: 9999.300914574964

***
LP post step 2
***
Exchange ETH-TKN (LP)
Reserves: ETH = 1000.0, TKN = 100000.0
Liquidity: 9999.300914574964

Total withdrawn is 51.264473 + 48.735527 = 100.000000 ETH
Of the requested 100 ETH, a total of 100.000000 ETH has been withdrawn

Excellent, we can see that we received our 100 ETH request! Finally, let’s check out our solution using WithdrawSwap as integrated into the UniswapPy python package.

from uniswappy.cpt.factory import Factory
from uniswappy.process.swap import WithdrawSwap

tkn = ERC20("TKN", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = tkn, symbol="LP", address="0x011")

factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount+100, tkn_amount, eth_amount+100, tkn_amount)
lp.summary()

amt_out = 100
out = WithdrawSwap().apply(lp, eth, user_nm, 100)
lp.summary()

print('Total withdrawn is {:.6f} ETH, as per request'.format(out))

## Output
Exchange ETH-TKN (LP)
Reserves: ETH = 1100, TKN = 100000
Liquidity: 10488.088481701516

Exchange ETH-TKN (LP)
Reserves: ETH = 1000.0, TKN = 100000.0
Liquidity: 9999.300914574964

Total withdrawn is 100.000000 ETH, as per request

We can see when using the proper portioning within WithdrawSwap, all 100 ETH have been accounted for in our withdrawal, as per request.

3. Summary

The work presented in this article is a larger body of work for doing things like: (a) simulate the behaviour of a simple liquidity pool [1]; (b) studying the effects of Impermanent Loss in an LP [2]; and © analyzing the risk profile and profitability in more advanced DeFi systems. Here, we provide the basic setup for those who are interested in getting into researching LPs. Please be sure to look out for future medium posts on this!

See GH repos for Jupyter notebook of behind this presentation

--

--