From d95a0b2bb3fba853fbf2fd99d7098c7b0de88004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 11 Nov 2021 15:22:58 +0100 Subject: [PATCH 1/2] docs: added warning about trading illiquid pools/routes --- docs/getting-started.rst | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index fdc850c..7842c18 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -76,14 +76,14 @@ Quoting prices .. note:: - These methods assume a certain route for the swap to take, which may not be the optimal route. See :issue:`69` for details. + These methods assume a certain route for the swap to take, which may not be the optimal route. See :issue:`93` for details. There are two functions to retrieve the price for a given pair, one for specifying how much you get given a certain amount of the input token, and another for specifying how much you need to pay to receive a certain amount of the output token. :func:`~uniswap.Uniswap.get_price_input` ```````````````````````````````````````` -Returns the cost of the given number of input tokens, priced in the output token. +Returns the amount of output tokens you get for a given amount of input tokens. .. code:: python @@ -120,26 +120,34 @@ Making trades The same route assumptions and need for handling decimals apply here as those mentioned in the previous section. +.. warning:: + + Always check the expected price before executing a trade. It's important that you're using a pool with adequate liquidity, or else you may suffer significant losses! (see :issue:`198`) + + Use the Uniswap version with the most liquidity for your route, and if using v3, make sure you set the ``fee`` parameter to use the best pool. + :func:`~uniswap.Uniswap.make_trade` ``````````````````````````````````` .. code:: python - # Make a trade where the input qty being known parameters - uniswap.make_trade(eth, bat, 1*10**18) # sell 1 ETH for however many BAT - uniswap.make_trade(bat, eth, 1*10**18) # sell 1 BAT for however many ETH - uniswap.make_trade(bat, dai, 1*10**18) # sell 1 BAT for however many DAI - uniswap.make_trade(eth, bat, 1*10**18, "0x123...") # sell 1 ETH for however many BAT, and send the BAT to the provided address + # Make a trade by specifying the quantity of the input token you wish to sell + uniswap.make_trade(eth, bat, 1*10**18) # sell 1 ETH for BAT + uniswap.make_trade(bat, eth, 1*10**18) # sell 1 BAT for ETH + uniswap.make_trade(bat, dai, 1*10**18) # sell 1 BAT for DAI + uniswap.make_trade(eth, bat, 1*10**18, "0x123...") # sell 1 ETH for BAT, and send the BAT to the provided address + uniswap.make_trade(dai, usdc, 1*10**18, fee=500) # sell 1 DAI for USDC using the 0.05% fee pool (v3 only) :func:`~uniswap.Uniswap.make_trade_output` `````````````````````````````````````````` .. code:: python - # Make a trade where the output qty is known, based on the input parameters - uniswap.make_trade_output(eth, bat, 1*10**18) # buy however many ETH for 1 BAT - uniswap.make_trade_output(bat, eth, 1*10**18) # buy however many BAT for 1 ETH - uniswap.make_trade_output(bat, dai, 1*10**18, "0x123...") # buy however many BAT for 1 DAI, and send the BAT to the provided address + # Make a trade by specifying the quantity of the output token you wish to buy + uniswap.make_trade_output(eth, bat, 1*10**18) # buy ETH for 1 BAT + uniswap.make_trade_output(bat, eth, 1*10**18) # buy BAT for 1 ETH + uniswap.make_trade_output(bat, dai, 1*10**18, "0x123...") # buy BAT for 1 DAI, and send the BAT to the provided address + uniswap.make_trade_output(dai, usdc, 1*10**8, fee=500) # buy USDC for 1 DAI using the 0.05% fee pool (v3 only) Pool Methods (v1 only) From 350296fd0409e2f874364bd0e8ab08e60d5075d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 11 Nov 2021 15:24:05 +0100 Subject: [PATCH 2/2] feat: added estimate_price_impact helper function and example --- examples/price_impact.py | 63 ++++++++++++++++++++++++++++++++++++++++ uniswap/uniswap.py | 30 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 examples/price_impact.py diff --git a/examples/price_impact.py b/examples/price_impact.py new file mode 100644 index 0000000..15c3a0a --- /dev/null +++ b/examples/price_impact.py @@ -0,0 +1,63 @@ +from typing import List + +from web3 import Web3 + +from uniswap import Uniswap +from uniswap.types import AddressLike + +eth = Web3.toChecksumAddress("0x0000000000000000000000000000000000000000") +weth = Web3.toChecksumAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") +usdt = Web3.toChecksumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7") +vxv = Web3.toChecksumAddress("0x7d29a64504629172a429e64183d6673b9dacbfce") + + +def _perc(f: float) -> str: + return f"{round(f * 100, 3)}%" + + +def usdt_to_vxv_v2(): + """ + Checks impact for a pool with very little liquidity. + + This particular route caused a $14k loss for one user: https://github.com/uniswap-python/uniswap-python/discussions/198 + """ + uniswap = Uniswap(address=None, private_key=None, version=2) + + route: List[AddressLike] = [usdt, weth, vxv] + + # Compare the results with the output of: + # https://app.uniswap.org/#/swap?use=v2&inputCurrency=0xdac17f958d2ee523a2206206994597c13d831ec7&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce + qty = 10 * 10 ** 8 + + # price = uniswap.get_price_input(usdt, vxv, qty, route=route) / 10 ** 18 + # print(price) + + impact = uniswap.estimate_price_impact(usdt, vxv, qty, route=route) + # NOTE: Not sure why this differs from the quote in the UI? + # Getting -27% in the UI for 10 USDT, but this returns >95% + # The slippage for v3 (in example below) returns correct results. + print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") + + qty = 13900 * 10 ** 8 + impact = uniswap.estimate_price_impact(usdt, vxv, qty, route=route) + print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") + + +def eth_to_vxv_v3(): + """Checks price impact for a pool with liquidity.""" + uniswap = Uniswap(address=None, private_key=None, version=3) + + # Compare the results with the output of: + # https://app.uniswap.org/#/swap?use=v3&inputCurrency=ETH&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce + qty = 1 * 10 ** 18 + impact = uniswap.estimate_price_impact(eth, vxv, qty, fee=10000) + print(f"Impact for buying VXV on v3 with {qty / 10**18} ETH: {_perc(impact)}") + + qty = 100 * 10 ** 18 + impact = uniswap.estimate_price_impact(eth, vxv, qty, fee=10000) + print(f"Impact for buying VXV on v3 with {qty / 10**18} ETH: {_perc(impact)}") + + +if __name__ == "__main__": + usdt_to_vxv_v2() + eth_to_vxv_v3() diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 418c5ce..6c54b70 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1172,7 +1172,7 @@ def _calculate_max_output_token( # ------ Helpers ------------------------------------------------------------ - def get_token(self, address: AddressLike, abi_name:str="erc20") -> ERC20Token: + def get_token(self, address: AddressLike, abi_name: str = "erc20") -> ERC20Token: """ Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. """ @@ -1209,6 +1209,34 @@ def get_weth_address(self) -> ChecksumAddress: address = self.router.functions.WETH9().call() return address + def estimate_price_impact( + self, + token_in: AddressLike, + token_out: AddressLike, + amount_in: int, + fee: int = None, + route: Optional[List[AddressLike]] = None, + ) -> float: + """ + Returns the estimated price impact as a positive float (0.01 = 1%). + + NOTE: Work-in-progress. + + See ``examples/price_impact.py`` for an example which uses this. + """ + amount_small = 10 ** 2 + cost_small = self.get_price_input( + token_in, token_out, amount_small, fee=fee, route=route + ) + cost_amount = self.get_price_input( + token_in, token_out, amount_in, fee=fee, route=route + ) + + price_small = cost_small / amount_small + price_amount = cost_amount / amount_in + + return (price_small - price_amount) / price_small + # ------ Exchange ------------------------------------------------------------------ @supports([1, 2]) def get_fee_maker(self) -> float: