Setup your Uniswap Deposits like a Baller
- Illustrate problem when performing a deposit 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.
Alright, 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 invest some ETH, can’t I just deposit it directly? You’ll come to 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 deposit into an ETH/USDC LP, then the provider would have to add 1 ETH combined with an additional $1,500 USDC for a total initial deposit value of $3,000 USD.
Ok, we have now another problem. In example (outlined in the next Section), we want to invest into an ETH/USDC LP and only have ETH in our wallet. No problem, that can be done in two steps: (step 1) swap some of your ETH for USDC, then; (step 2) deposit both ETH and USDC. No sweat, now say we want to deposit 100 ETH into the LP; so don’t we just cut out ETH in half and swap 50 ETH for the equivalent amount in USDC, then deposit the USDC / ETH combo? Not quite, because after we perform our swap, the pool balances have changed slightly, and when we move to perform our deposit, we will always end up with a little bit of one token or the other.
However, with a bit of footwork, we can set that deposit up … no problem, which we will cover in this walkthrough. By the time you’re through the steps below you’ll be setting up your LP deposits … like a high rollin’ baller!
2. Problem Walkthrough
Assume we want to deposit 100 ETH into a Uniswap ETH/USDC LP, so we will naively cut our ETH equally into two parts, then swap 50 ETH for USDC, then make our deposit:
eth = ERC20("ETH", "0x09")
usdc = ERC20("USDC", "0x111")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = usdc, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, usdc_amount, eth_amount, usdc_amount)
print('***\nInitial LP\n***')
lp.summary()
s_in = 100
alpha = 0.5
s_out = Swap().apply(lp, eth, user_nm, alpha*s_in)
print('***\nLP post step 1\n***')
lp.summary()
balance0 = alpha*s_in
balance1 = lp.quote(balance0, lp.reserve0, lp.reserve1)
lp.add_liquidity(user_nm, balance0, balance1, balance0, balance1)
print('***\nLP post step 2\n***')
lp.summary()
print('Given {} initial ETH:'.format(s_in))
print(' (step 1) {} ETH must first get swapped for {} '.format(alpha*s_in, s_out))
print(' (step 2) The received USDC gets deposited along with the remaining {} ETH'.format(balance0))
print('\nTotal deposited is {:.6f} + {:.6f} = {:.6f} ETH:'.format(alpha*s_in, balance0, alpha*s_in + balance0))
print('However, we have {} unaccounted USDC which need to be considered when using a 50/50 split'.format(colored(str(usdc_amount-lp.reserve1), 'red', attrs=['bold'])))
The above code block produces the output below which illustrates our problem when we naively assume a 50% cut:
***
Initial LP
***
Exchange ETH-USDC (LP)
Reserves: ETH = 1000, USDC = 1000000
Liquidity: 31622.776601683792
***
LP post step 1
***
Exchange ETH-USDC (LP)
Reserves: ETH = 1050.0, USDC = 952517.026241844
Liquidity: 31622.776601683792
***
LP post step 2
***
Exchange ETH-USDC (LP)
Reserves: ETH = 1100.0, USDC = 997874.9798724081
Liquidity: 33128.623106525876
Given 100 initial ETH:
(step 1) 50.0 ETH must first get swapped for 47482.973758155924
(step 2) The received USDC gets deposited along with the remaining 50.0 ETH
Total deposited is 50.000000 + 50.000000 = 100.000000 ETH:
However, we have 2125.02012759191 unaccounted USDC which need to be
considered when using a 50/50 split
As you can see, we have accrued a deficit of $2125 USDC in our reserve after making our 100 ETH deposit, which means we didn’t add in accordance to the proper proportioning.
Let’s now address the problem …
To ensure all funds are deposited, we must determine the correct portion α that must first get swapped in step 1, as determined by:
The 2-part swap-deposit is represented by this system of equations:
where
- Δx → amount token in
- Δy → amount opposing token out after swap
- α → portion of Δx swapped in
- x → reserve0
- y → reserve1
First, lets initialize a ETH/USDC LP:
usdc = ERC20("TKN", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = usdc, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, usdc_amount, eth_amount, usdc_amount)
s_in = 100
alpha = 0.5
y = lp.reserve1
x = lp.reserve0
Now plug above into first equation from our linear system, and see how many USDC we get when 50% of ETH is swapped for in first step:
s_out = (997*alpha*s_in*y)/(1000*x + 997*alpha*s_in)
print('For {} ETH, we get {:.2f} TKN with a 50% portion'.format(s_in, s_out))
#Output
For 100 ETH, we get 47482.97 USDC with a 50% portion
Now, lets check how many ETH gets swap-deposited into the LP when 50% of ETH is swapped in for step (1)
a1_out = alpha*s_in + s_out*(x + alpha*s_in)/(y - s_out)
print('Instead of {} ETH, we get {:.2f} ETH under a 50% portion'.format(s_in, a1_out))
#Output
Instead of 100 ETH, we get 102.34 ETH under a 50% portion
We can see that there is an imbalance in the system under a 50% distribution for step (1). To mitigate, we need to solve the linear system (above) for α to get a proper distribution, then plug eq. (1) into eq. (2), and we get:
which reduces to the following quadratic:
Now, solve for α, and we can calculate the correct distribution using calc_deposit_dist
def calc_deposit_portion(lp, token_in, dx):
if(token_in.token_name == lp.token0):
tkn_supply = lp.reserve0
else:
tkn_supply = lp.reserve1
a = 997*(dx**2)/(1000*tkn_supply)
b = dx*(1997/1000)
c = -dx
alpha = -(b - math.sqrt(b*b - 4*a*c)) / (2*a)
return alpha
Let’s get the correct distribution using our solution:
alpha = calc_deposit_portion(lp, eth, s_in)
print('The correct swap distrbution (for step 1) is {}'.format(alpha))
#Output
The correct swap distrbution (for step 1) is 0.4888217399419355
Now, using our calculated value for α, let’s check against our reduced quadratic, and we should expect to get zero:
997*(alpha**2)*(s_in**2)/(1000*x) + alpha*s_in*(1997/1000) - s_in
#Output
-5.684341886080802e-14
Excellent, not counting for floating point error, we effectively get zero. Finally, lets run through the steps to a SwapDeposit
and compare above:
usdc = ERC20("USDC", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = usdc, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, usdc_amount, eth_amount, usdc_amount)
print('***\nInitial LP\n***')
lp.summary()
s_in = 100
alpha = calc_deposit_portion(lp, eth, s_in)
s_out = Swap().apply(lp, eth, user_nm, alpha*s_in)
print('***\nLP post step 1\n***')
lp.summary()
balance1 = s_out
balance0 = lp.quote(s_out, lp.reserve1, lp.reserve0)
lp.add_liquidity(user_nm, balance0, balance1, balance0, balance1)
print('***\nLP post step 2\n***')
lp.summary()
print('Given {} initial ETH:'.format(s_in))
print(' (step 1) {} ETH must first get swapped for {} USDC'.format(alpha*s_in, s_out))
print(' (step 2) The received USDC gets deposited along with the remaining {} ETH'.format(balance0))
print('\nTotal deposited is {:.6f} + {:.6f} = {:.6f} ETH:'.format(alpha*s_in, balance0, alpha*s_in + balance0))
***
Initial LP
***
Exchange ETH-USDC (LP)
Reserves: ETH = 1000, USDC = 1000000
Liquidity: 31622.776601683792
***
LP post step 1
***
Exchange ETH-USDC (LP)
Reserves: ETH = 1048.8821739941936, USDC = 953529.2490856305
Liquidity: 31622.776601683792
***
LP post step 2
***
Exchange ETH-USDC (LP)
Reserves: ETH = 1100.0, USDC = 1000000.0
Liquidity: 33163.92929950274
Given 100 initial ETH:
(step 1) 48.88217399419355 ETH must first get swapped for 46470.75091436944 USDC
(step 2) The received USDC gets deposited along with the remaining 51.117826005806386 ETH
Total deposited is 48.882174 + 51.117826 = 100.000000 ETH
Excellent, we can see that we successfully deposited our 100 ETH ! Finally, let’s check out our solution using SwapDeposit
as integrated into the UniswapPy python package.
usdc = ERC20("USDC", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = usdc, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, usdc_amount, eth_amount, usdc_amount)
lp.summary()
s_in = 100
dep = SwapDeposit().apply(lp, eth, user_nm, s_in)
lp.summary()
# Output
Exchange ETH-USDC (LP)
Reserves: ETH = 1000, USDC = 1000000
Liquidity: 31622.776601683792
Exchange ETH-USDC (LP)
Reserves: ETH = 1100.0, USDC = 1000000.0
Liquidity: 33163.92929950274
We can see when using the proper portioning within SwapDeposit
, all 100 ETH have been accounted for in our deposit with no leaks with the USDC, 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