Analyze Uniswap in Python using UniswapPy
- Introduction to the UniswapPy python package
- Utility for the package
- Overview on basic usage
February 2024: for all major protocols checkout our DeFiPy python package
April 2024: Uniswap V3 available ≥ v.1.3.0
1. Introduction
The purpose of this article is to get you quickly started with the basics of the UniswapPy python package. This package is a python re-factor of the original Uniswap V2 and V3 pairing codes and can be utilized for the purpose of analyzing and modelling its behaviour for designing new DeFi protocols or analyzing the behaviour of existing ones. Special features include:
- Abstracted Actions: Obfuscation is removed from standard Uniswap action events to help streamline analysis and lower line count; see articles titled: How to Handle Uniswap Withdrawals like an OG, and Setup your Uniswap Deposits like a Baller
- Indexing: Can calculate settlement LP token amounts given token amounts and vice versa; see article titled: The Uniswap Indexing Problem
- Simulation: Can simulate trading using Geometric Brownian Motion (GBM) process, or feed in actual raw price data to analyze behavior; see article titled: How to Simulate a Liquidity Pool for Decentralized Finance
- Randomized Events: Token amount and time delta models to simulate possible trading behavior
- Analytical Tools: Basic yield calculators and risk tools to assist in analyzing outcomes
2. Basic Usage
The basic tools to get started with the UniswapPy python package are as follows:
ERC20
: Token that implements the ERC20 standard within the Ethereum virtual machine (EVM)Factory
: Create liquidity pools for given token pairsExchange
: Refactor of the Uniswap V2 pairing code. Governs how Uniswap calls the liquidity pools; each exchange is associated with a single ERC20 token pairing. Maintains reserve asset holdings (ie, xₖ, yₖ), total liquidity pool token supply, and collected feesAddLiquidity
: Add liquidity to the pool given amount of one of the assets in the pair, the required quantity of the opposing token is done for youRemoveLiquidity
: Remove liquidity from the pool given amount of one of the assets in the pair, the required quantity of the opposing token is to perform the removal done for youSwapDeposit
: Process to swap approx. half of single token X for token Y (and vice verse) and deposit proceeds plus remaining other approximated half (exact proportion is mathematically calculated)WithdrawSwap
: Process to withdraw liquidity from LP and swap opposing token which is added to specifed token to receive a single amount of specified tokenLPQuote
: Useful tool for retrieving pricing and various settlement quotes from the liquidity pool
Setup a Liquidity Pool
To setup a liquidity pool, you must first create the tokens in the pair using the ERC20
object. Next, create a liquidity pool (LP) factory using Factory
object. Once this is setup, an unlimited amount of LPs can be created; the procedure for such is as follows:
from uniswappy.erc import ERC20
from uniswappy.cpt.factory import UniswapFactory
from uniswappy.utils.data import UniswapExchangeData
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
Swap(): swap one token for another
Swap one token for another using the constant product trading (CPT) mechanism (ie, xy = k)
from uniswappy.process.swap import Swap
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
out = Swap().apply(lp, dai, user_nm, 1000)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
out = Swap().apply(lp, dai, user_nm, 1000)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 999.00399301896, DAI = 1001000
Liquidity: 31622.776601683792
AddLiquidity(): add liquidity based on one token amount
To add liquidity to a Uniswap pool, equal amounts of each token, in accordance to the constant product trading (CPT) mechanism (ie, xy = k) is required. However, this function only requires you have one of the amounts, while the other portion is calculated for you; the procedure for such is as follows:
from uniswappy.process.liquidity import AddLiquidity
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
AddLiquidity().apply(lp, eth, user_nm, 10)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1010, DAI = 1010000.0
Liquidity: 31939.00436770063
RemoveLiquidity(): remove liquidity based on one token amount
To remove liquidity from a Uniswap pool, equal amounts of each token, in accordance to the constant product trading (CPT) mechanism (ie, xy = k) is required. However, this function only requires you have one of the amounts, while the other portion is calculated for you; the procedure for such is as follows:
from uniswappy.process.liquidity import RemoveLiquidity
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
RemoveLiquidity().apply(lp, eth, user_nm, 1)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 999.0, DAI = 999000.0
Liquidity: 31591.153825082107
SwapDeposit(): deposit liquidity given only one token
A SwapDeposit
is where a certain amount of a specific token is deposited into the LP under one function call; includes two steps:
- (Step 1) Perform approx. 50% swap for opposing token
- (Step 2) Using amount from step 1, perform 1:1 deposit
A portion of the incoming funds are swapped to achieve equal portions of both assets. These portions are then deposited into the LP. To ensure all the funds are deposited, we must determine the portion amount that must first get swapped, which will be something close to 50%. We will cover the math behind how the exact ratio is determined in a later article.
from uniswappy.process.deposit import SwapDeposit
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
amount_out = SwapDeposit().apply(lp, eth, user_nm, 1)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1001.0000000000001, DAI = 1000000.0
Liquidity: 31638.56029234534
WithdrawSwap(): withdraw liquidity denominated in only one token
A WithdrawSwap
is where a certain amount of a specific token is withdraw from LP under one operation; includes two steps:
- (Step 1) Split token amount into two parts and perform ~ 50/50 withdraw on one of the parts to receive tknA and tknB
- (Step 2) Swap remaining ~50% of tknB (from step 1) to receive only tknA
To ensure all the funds are withdrawn, we must determine the portion that requested token amount is split into, which is something close to 50%. We will cover the math behind how the exact ratio is determined in a later article.
from uniswappy.process.swap import WithdrawSwap
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
expected_amount_out = WithdrawSwap().apply(lp, eth, user_nm, 1)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 999.0000000000001, DAI = 1000000.0
Liquidity: 31606.937511796754
Retrieve token settlement amount given opposing token amount.
LPQuote(): retrieve various quotes from the LP
LPQuote
is a useful tool for retrieving pricing and various settlement quotes from the liquidity pool; the setup is as follows:
from uniswappy.cpt.quote import LPQuote
dai = ERC20("DAI", "0x111")
eth = ERC20("ETH", "0x09")
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP", address="0x011")
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exchg_data)
lp.add_liquidity(user_nm, eth_amount, dai_amount, eth_amount, dai_amount)
lp.summary()
#OUTPUT:
Exchange ETH-DAI (LP)
Reserves: ETH = 1000, DAI = 1000000
Liquidity: 31622.776601683792
Retrieve LP prices
p_eth = LPQuote().get_price(lp, eth)
p_dai = LPQuote().get_price(lp, dai)
print(f'The price of {eth.token_name} in {dai.token_name} is {p_eth}')
print(f'The price of {dai.token_name} in {eth.token_name} is {p_dai}')
#OUTPUT:
The price of ETH in DAI is 1000.0
The price of DAI in ETH is 0.001
Retrieve token settlement amount given opposing token amount.
amt_dai = LPQuote().get_amount(lp, eth, 1)
amt_eth = LPQuote().get_amount(lp, dai, 1)
print(f'1 {eth.token_name} token is worth {amt_dai} {dai.token_name}')
print(f'1 {dai.token_name} token is worth {amt_eth} {eth.token_name}')
#OUTPUT:
1 ETH token is worth 1000.0 DAI
1 DAI token is worth 0.001 ETH
Retrieve rebased token settlement amount given amount of LP token. We will cover the math behind how this is determined in a later article.
amt_eth = LPQuote(False).get_amount_from_lp(lp, eth, 1)
amt_dai = LPQuote().get_amount_from_lp(lp, eth, 1)
print(f'1 LP token is worth {amt_eth} {eth.token_name}')
print(f'1 LP token is worth {amt_dai} {dai.token_name}')
#OUTPUT:
1 LP token is worth 0.06314969086446823 ETH
1 LP token is worth 63.149690864468226 DAI
Retrieve LP token settlement amount given amount of asset token. We will cover the math behind how this is determined in a later article.
amt_eth_lp = LPQuote(False).get_lp_from_amount(lp, eth, 1)
amt_dai_lp = LPQuote(False).get_lp_from_amount(lp, dai, 1)
print(f'1 {eth.token_name} token is worth {amt_eth_lp} LP tokens')
print(f'1 {dai.token_name} token is worth {amt_dai_lp} LP tokens')
#OUTPUT:
1 ETH token is worth 15.839089887038602 LP tokens
1 DAI token is worth 0.01583514496167512 LP tokens
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 (c) 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
4. References
[1] I.C. Moore, Simulating a Liquidity Pool for Decentralized Finance, Syscoin Github repos, Accessed on: Mar 2023. [Online]. Available: https://github.com/icmoore/uniswappy/blob/main/notebooks/medium_articles/simple_simulation.ipynb
[2] I.C. Moore and J. Sidhu, An Analytical and Empirical Survey of Impermanent Loss, Syscoin Github repos, Accessed on: Mar 2023. [Online]. Available: https://github.com/syscoin/latex_docs/blob/main/impermanent_loss/article.pdf