From 90923f7af7a59d251a0fc684ec8db90c4c8f86fb Mon Sep 17 00:00:00 2001 From: Petrovska Date: Fri, 19 Jan 2024 00:19:31 +0300 Subject: [PATCH 1/8] feat: intro univ3 and prep seeding scripting for ebtc/wbtc pool --- great_ape_safe/ape_api/__init__.py | 4 + .../ape_api/helpers/uni_v3/uni_v3.py | 86 ++++ .../ape_api/helpers/uni_v3/uni_v3_sdk.py | 148 ++++++ great_ape_safe/ape_api/uni_v3.py | 485 ++++++++++++++++++ helpers/addresses.py | 16 +- .../uniswap/INonFungiblePositionManager.sol | 268 ++++++++++ interfaces/uniswap/IQuoter.sol | 38 ++ interfaces/uniswap/ISwapRouter.sol | 67 +++ interfaces/uniswap/IUniswapV3Factory.sol | 45 ++ interfaces/uniswap/IUniswapV3Pool.sol | 206 ++++++++ scripts/univ3_management.py | 94 ++++ 11 files changed, 1456 insertions(+), 1 deletion(-) create mode 100644 great_ape_safe/ape_api/helpers/uni_v3/uni_v3.py create mode 100644 great_ape_safe/ape_api/helpers/uni_v3/uni_v3_sdk.py create mode 100644 great_ape_safe/ape_api/uni_v3.py create mode 100644 interfaces/uniswap/INonFungiblePositionManager.sol create mode 100644 interfaces/uniswap/IQuoter.sol create mode 100644 interfaces/uniswap/ISwapRouter.sol create mode 100644 interfaces/uniswap/IUniswapV3Factory.sol create mode 100644 interfaces/uniswap/IUniswapV3Pool.sol create mode 100644 scripts/univ3_management.py diff --git a/great_ape_safe/ape_api/__init__.py b/great_ape_safe/ape_api/__init__.py index 64f6155f..c6b365ac 100644 --- a/great_ape_safe/ape_api/__init__.py +++ b/great_ape_safe/ape_api/__init__.py @@ -1,6 +1,10 @@ from .ebtc import eBTC +from .uni_v3 import UniV3 class ApeApis: def init_ebtc(self): self.ebtc = eBTC(self) + + def init_uni_v3(self): + self.uni_v3 = UniV3(self) diff --git a/great_ape_safe/ape_api/helpers/uni_v3/uni_v3.py b/great_ape_safe/ape_api/helpers/uni_v3/uni_v3.py new file mode 100644 index 00000000..e8797437 --- /dev/null +++ b/great_ape_safe/ape_api/helpers/uni_v3/uni_v3.py @@ -0,0 +1,86 @@ +from math import floor +from rich.pretty import pprint + + +Q128 = 2 ** 128 + +LABELS = { + "pool_positions": [ + "liquidity", + "feeGrowthInside0LastX128", + "feeGrowthInside1LastX128", + "tokensOwed0", + "tokensOwed1", + ], + "positions": [ + "nonce", + "operator", + "token0", + "token1", + "fee", + "tickLower", + "tickUpper", + "liquidity", + "feeGrowthInside0LastX128", + "feeGrowthInside1LastX128", + "tokensOwed0", + "tokensOwed1", + ], + "ticks": [ + "liquidityGross", + "liquidityNet", + "feeGrowthOutside0X128", + "feeGrowthOutside1X128", + "tickCumulativeOutside", + "secondsPerLiquidityOutsideX128", + "secondsOutside", + "initialized", + ], +} + + +def print_position(nfp, position_id): + position_info = nfp.positions(position_id) + + position = dict(zip(LABELS["positions"], position_info)) + pprint(position) + + return position_info + + +def calc_accum_fees(feeGrowthInsideX128, feeGrowthInsideLastX128, liquidity): + # https://github.com/Uniswap/v3-core/blob/c05a0e2c8c08c460fb4d05cfdda30b3ad8deeaac/contracts/libraries/Position.sol#L60-L76 + return floor((feeGrowthInsideX128 - feeGrowthInsideLastX128) * liquidity / Q128) + + +def calc_all_accum_fees(nfp, v3_pool_obj, position_id): + """given a uni_v3 nfp manager, pool and position id, calculate its + accumulated fees expressed per underlying asset""" + + position = dict(zip(LABELS["positions"], nfp.positions(position_id))) + + lower = position["tickLower"] + upper = position["tickUpper"] + + ticks_lower = dict(zip(LABELS["ticks"], v3_pool_obj.ticks(lower))) + ticks_upper = dict(zip(LABELS["ticks"], v3_pool_obj.ticks(upper))) + + global0 = v3_pool_obj.feeGrowthGlobal0X128() + global1 = v3_pool_obj.feeGrowthGlobal1X128() + + outside_lower0 = ticks_lower["feeGrowthOutside0X128"] + outside_lower1 = ticks_lower["feeGrowthOutside1X128"] + + outside_upper0 = ticks_upper["feeGrowthOutside0X128"] + outside_upper1 = ticks_upper["feeGrowthOutside1X128"] + + inside0 = global0 - outside_lower0 - outside_upper0 + inside1 = global1 - outside_lower1 - outside_upper1 + + last0 = position["feeGrowthInside0LastX128"] + last1 = position["feeGrowthInside1LastX128"] + + return ( + calc_accum_fees(inside0, last0, position["liquidity"]), + calc_accum_fees(inside1, last1, position["liquidity"]), + ) diff --git a/great_ape_safe/ape_api/helpers/uni_v3/uni_v3_sdk.py b/great_ape_safe/ape_api/helpers/uni_v3/uni_v3_sdk.py new file mode 100644 index 00000000..6180e375 --- /dev/null +++ b/great_ape_safe/ape_api/helpers/uni_v3/uni_v3_sdk.py @@ -0,0 +1,148 @@ +from math import ceil + +BASE = 1.0001 +Q128 = 2 ** 128 +Q96 = 2 ** 96 +Q32 = 2 ** 32 +MAXUINT256 = 2 ** 256 - 1 + + +def maxLiquidityForAmount0(sqrtA, sqrtB, amount): + # https://github.com/Uniswap/v3-sdk/blob/d139f73823145a5ba5d90ef2f61ff33ff02b6a92/src/utils/maxLiquidityForAmounts.ts#L32-L41 + if sqrtA > sqrtB: + sqrtA, sqrtB = sqrtB, sqrtA + + numerator = (amount * sqrtA) * sqrtB + denominator = Q96 * (sqrtB - sqrtA) + + return numerator / denominator + + +def maxLiquidityForAmount1(sqrtA, sqrtB, amount): + # https://github.com/Uniswap/v3-sdk/blob/d139f73823145a5ba5d90ef2f61ff33ff02b6a92/src/utils/maxLiquidityForAmounts.ts#L50-L55 + if sqrtA > sqrtB: + sqrtA, sqrtB = sqrtB, sqrtA + + numerator = amount * Q96 + denominator = sqrtB - sqrtA + + return numerator / denominator + + +def maxLiquidityForAmounts(sqrtCurrent, sqrtA, sqrtB, amount0, amount1): + # https://github.com/Uniswap/v3-sdk/blob/d139f73823145a5ba5d90ef2f61ff33ff02b6a92/src/utils/maxLiquidityForAmounts.ts#L68-L91 + if sqrtCurrent <= sqrtA: + return maxLiquidityForAmount0(sqrtA, sqrtB, amount0) + elif sqrtCurrent < sqrtB: + liq0 = maxLiquidityForAmount0(sqrtCurrent, sqrtB, amount0) + liq1 = maxLiquidityForAmount1(sqrtA, sqrtCurrent, amount1) + return liq0 if liq0 < liq1 else liq1 + else: + return maxLiquidityForAmount1(sqrtA, sqrtB, amount1) + + +def getAmount0Delta(sqrtA, sqrtB, liquidity, roundUp=False): + # https://github.com/Uniswap/v3-sdk/blob/12f3b7033bd70210a4f117b477cdaec027a436f6/src/utils/sqrtPriceMath.ts#L25-L36 + if sqrtA > sqrtB: + sqrtA, sqrtB = sqrtB, sqrtA + + shift_liquidity = liquidity * (1 << 96) + sqrt_substraction = sqrtB - sqrtA + + numerator = (shift_liquidity * sqrt_substraction) / sqrtB + + return ceil(numerator / sqrtA) if roundUp else numerator / sqrtA + + +def getAmount1Delta(sqrtA, sqrtB, liquidity, roundUp=False): + # https://github.com/Uniswap/v3-sdk/blob/12f3b7033bd70210a4f117b477cdaec027a436f6/src/utils/sqrtPriceMath.ts#L38-L46 + if sqrtA > sqrtB: + sqrtA, sqrtB = sqrtB, sqrtA + + numerator = liquidity * (sqrtB - sqrtA) + denominator = Q96 + + return ceil(numerator / denominator) if roundUp else numerator / denominator + + +def getAmountsForLiquidity(sqrtCurrent, sqrtA, sqrtB, liquidity): + # https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/LiquidityAmounts.sol#L120 + if sqrtA > sqrtB: + sqrtA, sqrtB = sqrtB, sqrtA + + amount0 = 0 + amount1 = 0 + + if sqrtCurrent < sqrtA: + amount0 = getAmount0Delta(sqrtA, sqrtB, liquidity) + elif sqrtCurrent < sqrtB: + amount0 = getAmount0Delta(sqrtCurrent, sqrtB, liquidity) + amount1 = getAmount1Delta(sqrtA, sqrtCurrent, liquidity) + else: + amount1 = getAmount1Delta(sqrtA, sqrtB, liquidity) + + return amount0, amount1 + + +# https://github.com/Balt2/Uniswapv3Research/blob/main/SqrtPriceMath.py +def rshift(val, n): + return (val) >> n + + +def mulShift(val, mulBy): + return rshift(val * mulBy, 128) + + +def getSqrtRatioAtTick(tick): + absTick = abs(tick) + ratio = ( + 0xFFFCB933BD6FAD37AA2D162D1A594001 + if ((absTick & 0x1) != 0) + else 0x100000000000000000000000000000000 + ) + if (absTick & 0x2) != 0: + ratio = mulShift(ratio, 0xFFF97272373D413259A46990580E213A) + if (absTick & 0x4) != 0: + ratio = mulShift(ratio, 0xFFF2E50F5F656932EF12357CF3C7FDCC) + if (absTick & 0x8) != 0: + ratio = mulShift(ratio, 0xFFE5CACA7E10E4E61C3624EAA0941CD0) + if (absTick & 0x10) != 0: + ratio = mulShift(ratio, 0xFFCB9843D60F6159C9DB58835C926644) + if (absTick & 0x20) != 0: + ratio = mulShift(ratio, 0xFF973B41FA98C081472E6896DFB254C0) + if (absTick & 0x40) != 0: + ratio = mulShift(ratio, 0xFF2EA16466C96A3843EC78B326B52861) + if (absTick & 0x80) != 0: + ratio = mulShift(ratio, 0xFE5DEE046A99A2A811C461F1969C3053) + if (absTick & 0x100) != 0: + ratio = mulShift(ratio, 0xFCBE86C7900A88AEDCFFC83B479AA3A4) + if (absTick & 0x200) != 0: + ratio = mulShift(ratio, 0xF987A7253AC413176F2B074CF7815E54) + if (absTick & 0x400) != 0: + ratio = mulShift(ratio, 0xF3392B0822B70005940C7A398E4B70F3) + if (absTick & 0x800) != 0: + ratio = mulShift(ratio, 0xE7159475A2C29B7443B29C7FA6E889D9) + if (absTick & 0x1000) != 0: + ratio = mulShift(ratio, 0xD097F3BDFD2022B8845AD8F792AA5825) + if (absTick & 0x2000) != 0: + ratio = mulShift(ratio, 0xA9F746462D870FDF8A65DC1F90E061E5) + if (absTick & 0x4000) != 0: + ratio = mulShift(ratio, 0x70D869A156D2A1B890BB3DF62BAF32F7) + if (absTick & 0x8000) != 0: + ratio = mulShift(ratio, 0x31BE135F97D08FD981231505542FCFA6) + if (absTick & 0x10000) != 0: + ratio = mulShift(ratio, 0x9AA508B5B7A84E1C677DE54F3E99BC9) + if (absTick & 0x20000) != 0: + ratio = mulShift(ratio, 0x5D6AF8DEDB81196699C329225EE604) + if (absTick & 0x40000) != 0: + ratio = mulShift(ratio, 0x2216E584F5FA1EA926041BEDFE98) + if (absTick & 0x80000) != 0: + ratio = mulShift(ratio, 0x48A170391F7DC42444E8FA2) + + if tick > 0: + ratio = MAXUINT256 / ratio + + if ratio % Q32 > 0: + return ratio / Q32 + 1 + else: + return ratio / Q32 diff --git a/great_ape_safe/ape_api/uni_v3.py b/great_ape_safe/ape_api/uni_v3.py new file mode 100644 index 00000000..7b8d7a4a --- /dev/null +++ b/great_ape_safe/ape_api/uni_v3.py @@ -0,0 +1,485 @@ +import json +import os +from datetime import datetime +import math +from pathlib import Path + +from brownie import interface, chain, multicall, web3, ZERO_ADDRESS + +from helpers.addresses import registry + +# general helpers and sdk +from great_ape_safe.ape_api.helpers.uni_v3.uni_v3 import ( + print_position, + calc_all_accum_fees, +) +from great_ape_safe.ape_api.helpers.uni_v3.uni_v3_sdk import ( + getAmountsForLiquidity, + getSqrtRatioAtTick, + getAmount1Delta, + getAmount0Delta, + maxLiquidityForAmounts, + BASE, +) + + +class UniV3: + def __init__(self, safe): + self.safe = safe + + # contracts + self.nonfungible_position_manager = interface.INonFungiblePositionManager( + registry.eth.uniswap.NonfungiblePositionManager, owner=self.safe.account + ) + self.factory = interface.IUniswapV3Factory( + registry.eth.uniswap.factoryV3, owner=self.safe.account + ) + self.router = interface.ISwapRouter( + registry.eth.uniswap.routerV3, owner=self.safe.account + ) + self.quoter = interface.IQuoter( + registry.eth.uniswap.quoter, owner=self.safe.account + ) + + # constant helpers + self.Q128 = 2 ** 128 + self.deadline = 60 * 180 + self.slippage = 0.98 + + def _get_pool(self, position): + return interface.IUniswapV3Pool( + self.factory.getPool( + position["token0"], position["token1"], position["fee"] + ), + owner=self.safe.account, + ) + + def _build_multihop_path(self, path): + # given a token path, construct a multihop swap path by adding token pair pools with highest liquidity + # https://docs.uniswap.org/protocol/guides/swaps/multihop-swaps#input-parameters + multihop = [path[0].address] + for i in range(len(path) - 1): + fee_tiers = {100: 0, 3000: 0, 10000: 0} + for tier in fee_tiers.keys(): + pool_addr = self.factory.getPool(path[i], path[i + 1], tier) + + if pool_addr == ZERO_ADDRESS: + continue + + pool = interface.IUniswapV3Pool(pool_addr) + fee_tiers[tier] = pool.liquidity() + + if list(fee_tiers.values()).count(0) == 3: + raise Exception( + f"No liquidity found for {path[i].symbol()} - {path[i+1].symbol()}" + ) + + best_tier = max(fee_tiers, key=fee_tiers.get) + multihop.append(best_tier) + multihop.append(path[i + 1].address) + + return multihop + + def _encode_path(self, multihop_path): + path_encoded = b"" + for item in multihop_path: + if web3.isAddress(item): + path_encoded += web3.toBytes(hexstr=item) + else: + path_encoded += int.to_bytes(item, 3, byteorder="big") + return path_encoded + + def calc_min_amounts( + self, pool, token0_amount, token1_amount, lower_tick, upper_tick + ): + sqrtRatioX96, currentTick, _, _, _, _, _ = pool.slot0() + sqrtRatio_lower_tick = getSqrtRatioAtTick(lower_tick) + sqrtRatio_upper_tick = getSqrtRatioAtTick(upper_tick) + + amount0Min = 0 + amount1Min = 0 + + liquidity = maxLiquidityForAmounts( + sqrtRatioX96, + sqrtRatio_lower_tick, + sqrtRatio_upper_tick, + token0_amount, + token1_amount, + ) + + if currentTick < lower_tick: + # calc amount0Min + amount0Min = getAmount0Delta( + sqrtRatio_lower_tick, sqrtRatio_upper_tick, liquidity + ) + amount1Min = 0 + elif currentTick < upper_tick: + # calc both + amount0Min = getAmount0Delta(sqrtRatioX96, sqrtRatio_upper_tick, liquidity) + amount1Min = getAmount1Delta(sqrtRatio_lower_tick, sqrtRatioX96, liquidity) + else: + # calculate amount1Min + amount0Min = 0 + amount1Min = getAmount1Delta( + sqrtRatio_lower_tick, sqrtRatio_upper_tick, liquidity + ) + return amount0Min, amount1Min + + def get_amounts_for_liquidity( + self, pool_addr, lower_tick, upper_tick, liquidity=None + ): + pool = interface.IUniswapV3Pool(pool_addr) + liquidity = liquidity or pool.liquidity() + + sqrtRatioX96, _, _, _, _, _, _ = pool.slot0() + sqrtRatio_lower_tick = getSqrtRatioAtTick(lower_tick) + sqrtRatio_upper_tick = getSqrtRatioAtTick(upper_tick) + + amount0, amount1 = getAmountsForLiquidity( + sqrtRatioX96, sqrtRatio_lower_tick, sqrtRatio_upper_tick, liquidity + ) + + return liquidity, amount0, amount1 + + def burn_token_id(self, token_id, burn_nft=False): + """ + It will decrease the liquidity from a specific NFT + and collect the fees earned on it + optional: to completly burn the NFT + """ + position = self.nonfungible_position_manager.positions(token_id) + deadline = chain.time() + self.deadline + + pool = self._get_pool(position) + + liquidity, amount0Min, amount1Min = self.get_amounts_for_liquidity( + pool.address, + position["tickLower"], + position["tickUpper"], + liquidity=position["liquidity"], + ) + + # requires to remove all liquidity first + self.nonfungible_position_manager.decreaseLiquidity( + ( + token_id, + liquidity, + amount0Min * self.slippage, + amount1Min * self.slippage, + deadline, + ) + ) + + # grab also tokens owned, otherwise cannot burn. ref: https://etherscan.io/address/0xc36442b4a4522e871399cd717abdd847ab11fe88#code#F1#L379 + position = self.nonfungible_position_manager.positions(token_id) + + if position["tokensOwed0"] > 0 or position["tokensOwed1"] > 0: + print("\nTokens pendant of being collected. Collecting...") + + token0 = self.safe.contract(pool.token0()) + token1 = self.safe.contract(pool.token1()) + + token0_bal_init = token0.balanceOf(self.safe.address) + token1_bal_init = token1.balanceOf(self.safe.address) + + self.collect_fee(token_id) + + # check that increase the balance off-chain + if position["tokensOwed0"] > 0: + assert token0.balanceOf(self.safe.address) > token0_bal_init + if position["tokensOwed1"] > 0: + assert token1.balanceOf(self.safe.address) > token1_bal_init + + # usually we do not burn the nft, as it is more efficient to leave it empty and fill it up as needed + if burn_nft: + # needs to be liq = 0, cleared the pos, otherwise will revert! + self.nonfungible_position_manager.burn(token_id) + + def collect_fee(self, token_id): + """ + collect fees for individual token_id + """ + # docs: https://docs.uniswap.org/protocol/reference/periphery/NonfungiblePositionManager#collect + # https://docs.uniswap.org/protocol/reference/periphery/interfaces/INonfungiblePositionManager#collectparams + params = (token_id, self.safe.address, self.Q128 - 1, self.Q128 - 1) + + # revert to this snapshot in case the collected amounts are 0 + # using collect.call somehow always returns 0, even when there are + # indeed fees to collect + chain.snapshot() + + # https://etherscan.io/address/0xC36442b4a4522E871399CD717aBDD847Ab11FE88#code#F1#L314 + amount0, amount1 = self.nonfungible_position_manager.collect( + params + ).return_value + + if amount0 == 0 and amount1 == 0: + chain.revert() + + def collect_fees(self): + """ + loop over all token ids owned by the safe + to allow us to claim the fees earned on each range over time + """ + nfts_owned = self.nonfungible_position_manager.balanceOf(self.safe) - 1 + + if nfts_owned >= 0: + with multicall: + token_ids = [ + self.nonfungible_position_manager.tokenOfOwnerByIndex(self.safe, i) + for i in range(nfts_owned) + ] + + for token_id in token_ids: + self.collect_fee(token_id) + else: + print(f" === Safe ({self.safe.address}) does not own any NFT === ") + + def increase_liquidity( + self, token_id, token0, token1, token0_amount_topup, token1_amount_topup + ): + """ + Allows to increase liquidity of a specific NFT, + bare in if it is on an activiy NFT range, proportions will depend + on where the current tick is + """ + # docs: https://docs.uniswap.org/protocol/reference/periphery/NonfungiblePositionManager#increaseliquidity + position = self.nonfungible_position_manager.positions(token_id) + + pool = self._get_pool(position) + + lower_tick = position["tickLower"] + upper_tick = position["tickUpper"] + deadline = chain.time() + self.deadline + + # check allowances & approve for topup token amounts if needed + allowance0 = token0.allowance( + self.safe.address, self.nonfungible_position_manager + ) + allowance1 = token1.allowance( + self.safe.address, self.nonfungible_position_manager + ) + + if allowance0 < token0_amount_topup: + token0.approve(self.nonfungible_position_manager, token0_amount_topup) + if allowance1 < token1_amount_topup: + # badger token does not allow setting !=0 to a new value, 1st set to 0 + if allowance1 > 0 and token1.address == registry.eth.treasury_tokens.BADGER: + token1.approve(self.nonfungible_position_manager, 0) + token1.approve(self.nonfungible_position_manager, token1_amount_topup) + + # calcs for min amounts + # for now leave it just for our wbtc/badger pool "hardcoded" as for sometime doubt we will operate other univ3 pool + amount0Min, amount1Min = self.calc_min_amounts( + pool, token0_amount_topup, token1_amount_topup, lower_tick, upper_tick + ) + + # printout before increasing + print(f"Token ID: {token_id} status position prior to increase liquidity...") + position_before = print_position(self.nonfungible_position_manager, token_id) + print( + f" ===== amount0Min={amount0Min/10**token0.decimals()}, amount1Min={amount1Min/10**token1.decimals()} ===== \n" + ) + + # https://docs.uniswap.org/protocol/reference/periphery/interfaces/INonfungiblePositionManager#increaseliquidityparams + params = ( + token_id, + token0_amount_topup, + token1_amount_topup, + amount0Min * self.slippage, + amount1Min * self.slippage, + deadline, + ) + tx = self.nonfungible_position_manager.increaseLiquidity(params) + + liquidity_returned, amount0, amount1 = tx.return_value + + # printout after increasing + print(f"Token ID: {token_id} status position post increasing liquidity...") + position_after = print_position(self.nonfungible_position_manager, token_id) + + # include assert for liq increase & increase of fees earned + assert ( + position_before["liquidity"] + liquidity_returned + == position_after["liquidity"] + ) + + # greater or equal in case there was 0 swap activity in the pool + assert position_after["tokensOwed0"] >= position_before["tokensOwed0"] + assert position_after["tokensOwed1"] >= position_before["tokensOwed1"] + + # update the json file with new amounts and liquidity, given the token_id + path = os.path.dirname("scripts/TCL/positionData/") + directory = os.fsencode(path) + + for file in os.listdir(directory): + file_name = os.fsdecode(file) + + if str(token_id) in file_name: + data = open(f"scripts/TCL/positionData/{file_name}") + json_file = json.load(data) + tx_detail_json = Path(f"scripts/TCL/positionData/{file_name}") + + with tx_detail_json.open("w") as fp: + tx_data = { + "tokenId": token_id, + "liquidity": json_file["liquidity"] + liquidity_returned, + "amount0": json_file["amount0"] + + amount0 / 10 ** token0.decimals(), + "amount1": json_file["amount1"] + + amount1 / 10 ** token1.decimals(), + "lowerTick": lower_tick, + "upperTick": upper_tick, + } + json.dump(tx_data, fp, indent=4, sort_keys=True) + + def mint_position(self, pool_addr, range0, range1, token0_amount, token1_amount): + """ + Create a NFT on the desired range, adding the liquidity specified + with the params `token0_amount` & `token1_amonunt` + """ + # docs: https://docs.uniswap.org/protocol/reference/periphery/NonfungiblePositionManager#mint + + pool = interface.IUniswapV3Pool(pool_addr, owner=self.safe.account) + + token0 = self.safe.contract(pool.token0()) + token1 = self.safe.contract(pool.token1()) + + if token0_amount > 0: + token0.approve(self.nonfungible_position_manager, token0_amount) + if token1_amount > 0: + token1.approve(self.nonfungible_position_manager, token1_amount) + + decimals_diff = token1.decimals() - token0.decimals() + + # params for minting method + lower_tick = int(math.log((1 / range1) * 10 ** decimals_diff, BASE) // 60 * 60) + upper_tick = int(math.log((1 / range0) * 10 ** decimals_diff, BASE) // 60 * 60) + deadline = chain.time() + self.deadline + + # calcs for min amounts + amount0Min, amount1Min = self.calc_min_amounts( + pool, token0_amount, token1_amount, lower_tick, upper_tick + ) + # MintParams: https://docs.uniswap.org/protocol/reference/periphery/interfaces/INonfungiblePositionManager#mintparams + tx = self.nonfungible_position_manager.mint( + ( + token0.address, + token1.address, + pool.fee(), + lower_tick, + upper_tick, + token0_amount, + token1_amount, + amount0Min * self.slippage, + amount1Min * self.slippage, + self.safe.address, + deadline, + ) + ) + # grabbing this data, despite that token_id may differ till tx gets signed/mined + token_id, liquidity, amount0, amount1 = tx.return_value + date = datetime.now().strftime("%Y-%m-%d") + + # drop this data into a json for records in this directory + os.makedirs(f"scripts/TCL/positionData/", exist_ok=True) + file_name = f"{token_id}_{date}" + tx_detail_json = Path(f"scripts/TCL/positionData/{file_name}.json") + with tx_detail_json.open("w") as fp: + tx_data = { + "tokenId": token_id, + "liquidity": liquidity, + "amount0": amount0 / 10 ** token0.decimals(), + "amount1": amount1 / 10 ** token1.decimals(), + "lowerTick": lower_tick, + "upperTick": upper_tick, + } + json.dump(tx_data, fp, indent=4, sort_keys=True) + + def positions_info(self): + nfts_owned = self.nonfungible_position_manager.balanceOf(self.safe) - 1 + + if nfts_owned >= 0: + with multicall: + token_ids = [ + self.nonfungible_position_manager.tokenOfOwnerByIndex(self.safe, i) + for i in range(nfts_owned) + ] + + for token_id in token_ids: + print("owner:", self.nonfungible_position_manager.ownerOf(token_id)) + print_position(self.nonfungible_position_manager, token_id) + + position = self.nonfungible_position_manager.positions(token_id) + + pool = self._get_pool(position) + + fees = calc_all_accum_fees( + self.nonfungible_position_manager, pool, token_id + ) + + token0 = self.safe.contract(position["token0"]) + token1 = self.safe.contract(position["token1"]) + + print("accumulated fees:") + print(fees[0] / 10 ** token0.decimals(), token0.symbol()) + print(fees[1] / 10 ** token1.decimals(), token1.symbol()) + else: + print(f" === Safe ({self.safe.address}) does not own any NFT === ") + + def transfer_nft(self, token_id, new_owner): + """ + transfer the targeted token_id to the new owner + """ + # assert current owner + assert self.nonfungible_position_manager.ownerOf(token_id) == self.safe.address + + self.nonfungible_position_manager.transferFrom( + self.safe.address, new_owner, token_id + ) + + # assert new owner + assert self.nonfungible_position_manager.ownerOf(token_id) == new_owner + + def swap(self, path, mantissa, destination=None): + # https://docs.uniswap.org/protocol/guides/swaps/multihop-swaps + destination = self.safe.address if not destination else destination + + token_in, token_out = path[0], path[-1] + balance_token_out = token_out.balanceOf(self.safe) + + multihop_path = self._build_multihop_path(path) + path_encoded = self._encode_path(multihop_path) + + min_out = self.quoter.quoteExactInput.call(path_encoded, mantissa) * ( + self.slippage + ) + + params = ( + path_encoded, + destination, + web3.eth.getBlock(web3.eth.blockNumber).timestamp + self.deadline, + mantissa, + min_out, + ) + + token_in.approve(self.router, mantissa) + + tx = self.router.exactInput(params) + + assert token_out.balanceOf(destination) >= balance_token_out + min_out + + return tx.return_value + + def get_amount_out(self, path, mantissa_in, multihop_path=None): + if not multihop_path: + multihop_path = self._build_multihop_path(path) + + path_encoded = self._encode_path(multihop_path) + + out = self.quoter.quoteExactInput.call( + path_encoded, + mantissa_in, + ) + + return int(out * 10_000 // (10_000 + ((1 - self.slippage) * 1000))) diff --git a/helpers/addresses.py b/helpers/addresses.py index 13e24307..58563999 100644 --- a/helpers/addresses.py +++ b/helpers/addresses.py @@ -6,8 +6,22 @@ import json ADDRESSES_ETH = { - "ebtc": {"placeholder": "0x0000000000000000000000000000000000000000"}, + "assets": { + "wbtc": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "ebtc": "0x0000000000000000000000000000000000000000", + "liq": "0xD82fd4D6D62f89A1E50b1db69AD19932314aa408", + }, + "badger_wallets": { + "treasury_ops_multisig": "0x042B32Ac6b453485e357938bdC38e0340d4b9276", + "treasury_vault_multisig": "0xD0A7A8B98957b9CD3cFB9c0425AbE44551158e9e", + }, "ebtc_wallets": {"placeholder": "0x0000000000000000000000000000000000000000"}, + "uniswap": { + "factoryV3": "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "NonfungiblePositionManager": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", + "routerV3": "0xE592427A0AEce92De3Edee1F18E0157C05861564", + "quoter": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", + }, } ADDRESSES_SEPOLIA = { diff --git a/interfaces/uniswap/INonFungiblePositionManager.sol b/interfaces/uniswap/INonFungiblePositionManager.sol new file mode 100644 index 00000000..758b49b1 --- /dev/null +++ b/interfaces/uniswap/INonFungiblePositionManager.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: UNLICENSED +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.5.2. SEE SOURCE BELOW. !! +pragma solidity >=0.7.0 <0.9.0; +pragma experimental ABIEncoderV2; + +interface INonFungiblePositionManager { + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + event Collect( + uint256 indexed tokenId, + address recipient, + uint256 amount0, + uint256 amount1 + ); + event DecreaseLiquidity( + uint256 indexed tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); + event IncreaseLiquidity( + uint256 indexed tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function PERMIT_TYPEHASH() external view returns (bytes32); + + function WETH9() external view returns (address); + + function approve(address to, uint256 tokenId) external; + + function balanceOf(address owner) external view returns (uint256); + + function baseURI() external pure returns (string memory); + + function burn(uint256 tokenId) external payable; + + function collect(INonfungiblePositionManager.CollectParams memory params) + external + payable + returns (uint256 amount0, uint256 amount1); + + function createAndInitializePoolIfNecessary( + address token0, + address token1, + uint24 fee, + uint160 sqrtPriceX96 + ) external payable returns (address pool); + + function decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams memory params + ) external payable returns (uint256 amount0, uint256 amount1); + + function factory() external view returns (address); + + function getApproved(uint256 tokenId) external view returns (address); + + function increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams memory params + ) + external + payable + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); + + function isApprovedForAll(address owner, address operator) + external + view + returns (bool); + + function mint(INonfungiblePositionManager.MintParams memory params) + external + payable + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); + + function multicall(bytes[] memory data) + external + payable + returns (bytes[] memory results); + + function name() external view returns (string memory); + + function ownerOf(uint256 tokenId) external view returns (address); + + function permit( + address spender, + uint256 tokenId, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + function refundETH() external payable; + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) external; + + function selfPermit( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + function selfPermitAllowed( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + function selfPermitAllowedIfNecessary( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + function selfPermitIfNecessary( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + function setApprovalForAll(address operator, bool approved) external; + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + function sweepToken( + address token, + uint256 amountMinimum, + address recipient + ) external payable; + + function symbol() external view returns (string memory); + + function tokenByIndex(uint256 index) external view returns (uint256); + + function tokenOfOwnerByIndex(address owner, uint256 index) + external + view + returns (uint256); + + function tokenURI(uint256 tokenId) external view returns (string memory); + + function totalSupply() external view returns (uint256); + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function uniswapV3MintCallback( + uint256 amount0Owed, + uint256 amount1Owed, + bytes memory data + ) external; + + function unwrapWETH9(uint256 amountMinimum, address recipient) + external + payable; + + receive() external payable; +} + +interface INonfungiblePositionManager { + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } +} \ No newline at end of file diff --git a/interfaces/uniswap/IQuoter.sol b/interfaces/uniswap/IQuoter.sol new file mode 100644 index 00000000..29b3bae3 --- /dev/null +++ b/interfaces/uniswap/IQuoter.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.7.6; + +interface IQuoter { + function WETH9() external view returns (address); + + function factory() external view returns (address); + + function quoteExactInput(bytes memory path, uint256 amountIn) + external + returns (uint256 amountOut); + + function quoteExactInputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + uint256 amountIn, + uint160 sqrtPriceLimitX96 + ) external returns (uint256 amountOut); + + function quoteExactOutput(bytes memory path, uint256 amountOut) + external + returns (uint256 amountIn); + + function quoteExactOutputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + uint256 amountOut, + uint160 sqrtPriceLimitX96 + ) external returns (uint256 amountIn); + + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes memory path + ) external view; +} \ No newline at end of file diff --git a/interfaces/uniswap/ISwapRouter.sol b/interfaces/uniswap/ISwapRouter.sol new file mode 100644 index 00000000..154290bf --- /dev/null +++ b/interfaces/uniswap/ISwapRouter.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface ISwapRouter is IUniswapV3SwapCallback { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} \ No newline at end of file diff --git a/interfaces/uniswap/IUniswapV3Factory.sol b/interfaces/uniswap/IUniswapV3Factory.sol new file mode 100644 index 00000000..5302a38f --- /dev/null +++ b/interfaces/uniswap/IUniswapV3Factory.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +interface IUniswapV3Factory { + event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); + event OwnerChanged(address indexed oldOwner, address indexed newOwner); + event PoolCreated( + address indexed token0, + address indexed token1, + uint24 indexed fee, + int24 tickSpacing, + address pool + ); + + function createPool( + address tokenA, + address tokenB, + uint24 fee + ) external returns (address pool); + + function enableFeeAmount(uint24 fee, int24 tickSpacing) external; + + function feeAmountTickSpacing(uint24) external view returns (int24); + + function getPool( + address, + address, + uint24 + ) external view returns (address); + + function owner() external view returns (address); + + function parameters() + external + view + returns ( + address factory, + address token0, + address token1, + uint24 fee, + int24 tickSpacing + ); + + function setOwner(address _owner) external; +} \ No newline at end of file diff --git a/interfaces/uniswap/IUniswapV3Pool.sol b/interfaces/uniswap/IUniswapV3Pool.sol new file mode 100644 index 00000000..2e43f58e --- /dev/null +++ b/interfaces/uniswap/IUniswapV3Pool.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.5.0. SEE SOURCE BELOW. !! +pragma solidity ^0.6.0; + +interface IUniswapV3Pool { + event Burn( + address indexed owner, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount, + uint256 amount0, + uint256 amount1 + ); + event Collect( + address indexed owner, + address recipient, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount0, + uint128 amount1 + ); + event CollectProtocol( + address indexed sender, + address indexed recipient, + uint128 amount0, + uint128 amount1 + ); + event Flash( + address indexed sender, + address indexed recipient, + uint256 amount0, + uint256 amount1, + uint256 paid0, + uint256 paid1 + ); + event IncreaseObservationCardinalityNext( + uint16 observationCardinalityNextOld, + uint16 observationCardinalityNextNew + ); + event Initialize(uint160 sqrtPriceX96, int24 tick); + event Mint( + address sender, + address indexed owner, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount, + uint256 amount0, + uint256 amount1 + ); + event SetFeeProtocol( + uint8 feeProtocol0Old, + uint8 feeProtocol1Old, + uint8 feeProtocol0New, + uint8 feeProtocol1New + ); + event Swap( + address indexed sender, + address indexed recipient, + int256 amount0, + int256 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick + ); + + function burn( + int24 tickLower, + int24 tickUpper, + uint128 amount + ) external returns (uint256 amount0, uint256 amount1); + + function collect( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount0Requested, + uint128 amount1Requested + ) external returns (uint128 amount0, uint128 amount1); + + function collectProtocol( + address recipient, + uint128 amount0Requested, + uint128 amount1Requested + ) external returns (uint128 amount0, uint128 amount1); + + function factory() external view returns (address); + + function fee() external view returns (uint24); + + function feeGrowthGlobal0X128() external view returns (uint256); + + function feeGrowthGlobal1X128() external view returns (uint256); + + function flash( + address recipient, + uint256 amount0, + uint256 amount1, + bytes calldata data + ) external; + + function increaseObservationCardinalityNext( + uint16 observationCardinalityNext + ) external; + + function initialize(uint160 sqrtPriceX96) external; + + function liquidity() external view returns (uint128); + + function maxLiquidityPerTick() external view returns (uint128); + + function mint( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount, + bytes calldata data + ) external returns (uint256 amount0, uint256 amount1); + + function observations(uint256) + external + view + returns ( + uint32 blockTimestamp, + int56 tickCumulative, + uint160 secondsPerLiquidityCumulativeX128, + bool initialized + ); + + function observe(uint32[] calldata secondsAgos) + external + view + returns ( + int56[] memory tickCumulatives, + uint160[] memory secondsPerLiquidityCumulativeX128s + ); + + function positions(bytes32) + external + view + returns ( + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + function protocolFees() + external + view + returns (uint128 token0, uint128 token1); + + function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external; + + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); + + function snapshotCumulativesInside(int24 tickLower, int24 tickUpper) + external + view + returns ( + int56 tickCumulativeInside, + uint160 secondsPerLiquidityInsideX128, + uint32 secondsInside + ); + + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata data + ) external returns (int256 amount0, int256 amount1); + + function tickBitmap(int16) external view returns (uint256); + + function tickSpacing() external view returns (int24); + + function ticks(int24) + external + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized + ); + + function token0() external view returns (address); + + function token1() external view returns (address); +} \ No newline at end of file diff --git a/scripts/univ3_management.py b/scripts/univ3_management.py new file mode 100644 index 00000000..73a91ca0 --- /dev/null +++ b/scripts/univ3_management.py @@ -0,0 +1,94 @@ +from math import sqrt + +from great_ape_safe import GreatApeSafe +from great_ape_safe.ape_api.helpers.uni_v3.uni_v3_sdk import Q96 + +from brownie import interface +from helpers.addresses import r +from rich.console import Console + +C = Console() + +# --- Forum Scope --- +# link: https://forum.badger.finance/t/ebtc-launch-planning-peg-management-and-monetary-policy/6129#protocol-owned-liquidity-6 + +# misc. +FEE_TIER = 500 +PCT_10 = 0.1 +PCT_40 = 0.4 + + +def pool_creation_and_init_seeding(): + safe = GreatApeSafe(r.badger_wallets.treasury_vault_multisig) + safe.init_uni_v3() + + # tokens + wbtc = safe.contract(r.assets.wbtc) + # ebtc = safe.contract(r.assets.ebtc) + # @note this is a random token just to exemplify the scope of the script till ebtc is deployed + liq = safe.contract(r.assets.liq) + + # 1. pool creation: tick spacing should be end-up being `10` + # ref: https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Factory.sol#L26 + pool_address = safe.uni_v3.factory.createPool(liq, wbtc, FEE_TIER).return_value + C.print(f"[green]Pool address is: {pool_address}[/green]") + + # 2. pool initialize + pool = interface.IUniswapV3Pool(pool_address, owner=safe.account) + # @note: order of which is token0 & token1 depends on + # ref: https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Factory.sol#L41 + token0 = pool.token0() + token1 = pool.token1() + C.print(f"[green]Token0 is: {token0}[/green]") + C.print(f"[green]Token1 is: {token1}\n[/green]") + + # sqrt(p) * Q96. where "p" is price of wbtc in terms of ebtc. assume "init" at parity + sqrt_of_p = sqrt(1e18 / 10 ** wbtc.decimals()) + sqrt_price_x_96 = sqrt_of_p * Q96 + + # expect tick ~230270 & sqrtPriceX96 ~7922816251426434000000000000000000 + pool.initialize(sqrt_price_x_96) + sqrtPriceX96, tick, _, _, _, _, _ = pool.slot0() + C.print(f"[green]sqrtPriceX96={sqrtPriceX96}[/green]") + C.print(f"[green]tick={tick}\n[/green]") + + # 3. seed pool on four ranges + # POL target: 25ebtc/25wbtc + # Initial seeding is smaller size than final aim: 10%. 2.5/2.5 + price_0925 = 0.925 + price_099 = 0.99 + price_1 = 1 + price_101 = 1.01 + price_108 = 1.08 + + # @note these figures are note definitive, will be updated in the future! + token_0_init_seeding_amount = 2.5e8 if token0 == wbtc.address else 2.5e18 + token_1_init_seeding_amount = 2.5e18 if token1 == liq.address else 2.5e8 + + # single-side lp only ebtc + safe.uni_v3.mint_position( + pool, price_0925, price_099, token_0_init_seeding_amount * PCT_10 * 2, 0 + ) + + # range with active tick at [.99,1] & [1,1.01] + safe.uni_v3.mint_position( + pool, + price_099, + price_1, + token_0_init_seeding_amount * PCT_40, + token_1_init_seeding_amount * PCT_40, + ) + safe.uni_v3.mint_position( + pool, + price_1, + price_101, + token_0_init_seeding_amount * PCT_40, + token_1_init_seeding_amount * PCT_40, + ) + + # single-side only wbtc + safe.uni_v3.mint_position( + pool, price_101, price_108, 0, token_1_init_seeding_amount * PCT_10 * 2 + ) + + safe.post_safe_tx() From a835303e8fbc9fa19b1ec930811ba4586140f163 Mon Sep 17 00:00:00 2001 From: Petrovska Date: Tue, 30 Jan 2024 19:04:42 +0300 Subject: [PATCH 2/8] feat: helper to have cdps insights for target msig --- scripts/cdp_management_lens.py | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 scripts/cdp_management_lens.py diff --git a/scripts/cdp_management_lens.py b/scripts/cdp_management_lens.py new file mode 100644 index 00000000..44f5591f --- /dev/null +++ b/scripts/cdp_management_lens.py @@ -0,0 +1,80 @@ +from great_ape_safe import GreatApeSafe +from helpers.addresses import r +from rich.console import Console +from rich.table import Table + +C = Console() + +""" +The following methods are meant to provide insight of cdp's owned by specific msig: + - cdp id + - collateral and debt amounts + - ICR and TCR + - % of total collateral of the cdp's and system + - $ of total debt of the cdp's and system + - cdp list ordered by ICR +""" + + +def main(msig_address=r.badger_wallets.treasury_vault_multisig): + # table generation + table = Table(title=f"CDPs owned by {msig_address} ordered by ICR") + + table.add_column("Cdp ID", justify="right") + table.add_column("Collateral", justify="right") + table.add_column("Debt", justify="right") + table.add_column("ICR", justify="right") + table.add_column("cdp collateral vs total cdp's owned (%)", justify="right") + table.add_column("cdp debt vs total cdp's owned (%)", justify="right") + + safe = GreatApeSafe(msig_address) + safe.init_ebtc() + + # helpers vars + cdps_info = [] + total_collateral = 0 + total_debt = 0 + + # general TCR info of the system at current oracle price w/ all elements sync + feed_price = safe.ebtc.price_feed.fetchPrice.call() + current_tcr = safe.ebtc.cdp_manager.getSyncedTCR(feed_price) + C.print( + f"[cyan]System's TCR: {(current_tcr/1e16):.3f}%. Oracle price: {(feed_price/1e18):.3f}.\n[/cyan]" + ) + + cdps_safe_owned = safe.ebtc.sorted_cdps.getCdpsOf(safe) + for cdp_id in cdps_safe_owned: + C.print(f"[green]Inspecting cdp id: {cdp_id}\n[/green]") + ( + cdp_id_debt, + cdp_id_coll, + _, + _, + _, + _, + ) = safe.ebtc.cdp_manager.Cdps(cdp_id) + icr = safe.ebtc.cdp_manager.getSyncedICR(cdp_id, feed_price) + + # increase global vars + total_collateral += cdp_id_coll + total_debt += cdp_id_debt + + cdps_info.append((cdp_id, cdp_id_coll, cdp_id_debt, icr)) + + # reordering cdps by ICR + ICR_INDEX = 3 + cdps_info.sort(key=lambda x: x[ICR_INDEX]) + + # fill up table rows + for cdp_info in cdps_info: + table.add_row( + f"{cdp_info[0][:7]}...{cdp_info[0][-7:]}", + f"{(cdp_info[1] / 10 ** 18):.3f}", + f"{(cdp_info[2] / 10 ** 18):.3f}", + f"{(cdp_info[3] / 1e16):.3f}%", + f"{(cdp_info[1] / total_collateral * 100):.3f}%", + f"{(cdp_info[2] / total_debt * 100):.3f}%", + ) + + # table printout + C.print(table) From 954fa25b4df0b915d9e38a6625aac8a80e49c165 Mon Sep 17 00:00:00 2001 From: Petrovska Date: Wed, 31 Jan 2024 00:59:38 +0300 Subject: [PATCH 3/8] fix: stringify of the begining and end of cdp id for shorter representation on the table --- scripts/cdp_management_lens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cdp_management_lens.py b/scripts/cdp_management_lens.py index 44f5591f..389ce18e 100644 --- a/scripts/cdp_management_lens.py +++ b/scripts/cdp_management_lens.py @@ -68,7 +68,7 @@ def main(msig_address=r.badger_wallets.treasury_vault_multisig): # fill up table rows for cdp_info in cdps_info: table.add_row( - f"{cdp_info[0][:7]}...{cdp_info[0][-7:]}", + f"{str(cdp_info[0])[:7]}...{str(cdp_info[0])[-7:]}", f"{(cdp_info[1] / 10 ** 18):.3f}", f"{(cdp_info[2] / 10 ** 18):.3f}", f"{(cdp_info[3] / 1e16):.3f}%", From 97b47abdae2b93e8b1ff7840a65911f710edf186 Mon Sep 17 00:00:00 2001 From: Petrovska Date: Tue, 13 Feb 2024 17:53:19 +0400 Subject: [PATCH 4/8] test: assert pool creation test suite --- helpers/addresses.py | 1 + tests/conftest.py | 5 ++++ tests/univ3/conftest.py | 20 ++++++++++++++ tests/univ3/test_pool_creation.py | 45 +++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 tests/univ3/conftest.py create mode 100644 tests/univ3/test_pool_creation.py diff --git a/helpers/addresses.py b/helpers/addresses.py index 515de008..e4e02286 100644 --- a/helpers/addresses.py +++ b/helpers/addresses.py @@ -21,6 +21,7 @@ "NonfungiblePositionManager": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", "routerV3": "0xE592427A0AEce92De3Edee1F18E0157C05861564", "quoter": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", + "v3pool_wbtc_badger": "0xe15e6583425700993bd08F51bF6e7B73cd5da91B", }, "cow": { "vault_relayer": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", diff --git a/tests/conftest.py b/tests/conftest.py index 502d5ccb..15cc41a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,11 @@ def techops(): return GreatApeSafe(registry.sepolia.ebtc_wallets.techops_multisig) +@pytest.fixture +def treasury(): + return GreatApeSafe(registry.eth.badger_wallets.treasury_vault_multisig) + + @pytest.fixture def fee_recipient(): return GreatApeSafe(registry.sepolia.ebtc_wallets.fee_recipient_multisig) diff --git a/tests/univ3/conftest.py b/tests/univ3/conftest.py new file mode 100644 index 00000000..cb36b83c --- /dev/null +++ b/tests/univ3/conftest.py @@ -0,0 +1,20 @@ +import pytest +from helpers.addresses import registry +from brownie import interface + + +@pytest.fixture +def liq(treasury): + return treasury.contract(registry.eth.assets.liq) + + +@pytest.fixture +def wbtc(treasury): + return treasury.contract(registry.eth.assets.wbtc) + + +@pytest.fixture +def univ3_pool(treasury): + return interface.IUniswapV3Pool( + registry.eth.uniswap.v3pool_wbtc_badger, owner=treasury.account + ) diff --git a/tests/univ3/test_pool_creation.py b/tests/univ3/test_pool_creation.py new file mode 100644 index 00000000..ce152f88 --- /dev/null +++ b/tests/univ3/test_pool_creation.py @@ -0,0 +1,45 @@ +from math import sqrt, log + +from brownie import interface + +from great_ape_safe.ape_api.helpers.uni_v3.uni_v3_sdk import BASE, Q96 + +# misc. +FEE_TIER = 500 +PARITY_PAIR_VALUE = 1 + + +def test_pool_creation(treasury, liq, wbtc): + treasury.init_uni_v3() + + pool_address = treasury.uni_v3.factory.createPool(liq, wbtc, FEE_TIER).return_value + pool = interface.IUniswapV3Pool(pool_address, owner=treasury.account) + + initial_sqrt_price_x96, initial_tick, _, _, _, _, _ = pool.slot0() + + # assert the following conditions against the pool creation + assert pool.token0() == wbtc.address + assert pool.token1() == liq.address + assert pool.fee() == FEE_TIER + assert initial_sqrt_price_x96 == 0 + assert initial_tick == 0 + + # init pool at parity, similar w/ the intention of ebtc/wbtc + sqrt_of_p = sqrt(1e18 / 10 ** wbtc.decimals()) + sqrt_price_x_96 = sqrt_of_p * Q96 + + expected_tick = round((2 * log(sqrt_price_x_96 / Q96)) / log(BASE)) + + pool.initialize(sqrt_price_x_96) + initialized_sqrt_price_x96, initialized_tick, _, _, _, _, _ = pool.slot0() + + common_decimal_denominator = liq.decimals() - wbtc.decimals() + + # rounding here since in essence should be 0.99999... + p = round(((BASE ** initialized_tick) / (10 ** common_decimal_denominator))) + + # assert the following conditions against the pool after initialization state + assert initialized_tick > 0 and initialized_sqrt_price_x96 > 0 + assert initialized_tick == expected_tick + # proofs parity on the pair creation! + assert PARITY_PAIR_VALUE == p From b152e299fda642e3d14edc254c92d58b06b80c9b Mon Sep 17 00:00:00 2001 From: Petrovska Date: Tue, 13 Feb 2024 19:13:22 +0400 Subject: [PATCH 5/8] test: (univ3) position creation test --- great_ape_safe/ape_api/uni_v3.py | 2 + tests/univ3/test_position_creation.py | 54 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/univ3/test_position_creation.py diff --git a/great_ape_safe/ape_api/uni_v3.py b/great_ape_safe/ape_api/uni_v3.py index 7b8d7a4a..b217c2d6 100644 --- a/great_ape_safe/ape_api/uni_v3.py +++ b/great_ape_safe/ape_api/uni_v3.py @@ -396,6 +396,8 @@ def mint_position(self, pool_addr, range0, range1, token0_amount, token1_amount) } json.dump(tx_data, fp, indent=4, sort_keys=True) + return token_id + def positions_info(self): nfts_owned = self.nonfungible_position_manager.balanceOf(self.safe) - 1 diff --git a/tests/univ3/test_position_creation.py b/tests/univ3/test_position_creation.py new file mode 100644 index 00000000..ba1cba31 --- /dev/null +++ b/tests/univ3/test_position_creation.py @@ -0,0 +1,54 @@ +from math import sqrt, log + +from brownie import interface + +from great_ape_safe.ape_api.helpers.uni_v3.uni_v3_sdk import BASE, Q96 + +# misc. +FEE_TIER = 500 +PRICE_1 = 1 +PRICE_101 = 1.01 + + +def test_position_creation(treasury, liq, wbtc): + decimals_diff = liq.decimals() - wbtc.decimals() + + treasury.init_uni_v3() + + pool_address = treasury.uni_v3.factory.createPool(liq, wbtc, FEE_TIER).return_value + pool = interface.IUniswapV3Pool(pool_address, owner=treasury.account) + + sqrt_of_p = sqrt(1e18 / 10 ** wbtc.decimals()) + sqrt_price_x_96 = sqrt_of_p * Q96 + + pool.initialize(sqrt_price_x_96) + + token_0_init_seeding_amount = 2.5e8 + token_1_init_seeding_amount = 2.5e18 + + token_id = treasury.uni_v3.mint_position( + pool, + PRICE_1, + PRICE_101, + token_0_init_seeding_amount, + token_1_init_seeding_amount, + ) + + positions = treasury.uni_v3.nonfungible_position_manager.positions(token_id) + + # TODO: rounding to 4 decimal places, values are not matching the expected? + price_lower_ticket = round( + ((BASE ** positions["tickLower"]) / (10 ** decimals_diff)), 4 + ) + price_higher_tick = round( + ((BASE ** positions["tickUpper"]) / (10 ** decimals_diff)), 4 + ) + + assert ( + treasury.uni_v3.nonfungible_position_manager.ownerOf(token_id) + == treasury.account + ) + assert positions["token0"] == wbtc.address + assert positions["token1"] == liq.address + assert positions["liquidity"] > 0 + assert price_lower_ticket < price_higher_tick From 5241b91ba1edc62997a7f461694bc89c054014c2 Mon Sep 17 00:00:00 2001 From: Petrovska Date: Tue, 13 Feb 2024 19:24:01 +0400 Subject: [PATCH 6/8] feat: tick spacing is a dynamic var based on pool fee tier --- great_ape_safe/ape_api/uni_v3.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/great_ape_safe/ape_api/uni_v3.py b/great_ape_safe/ape_api/uni_v3.py index b217c2d6..c53f0960 100644 --- a/great_ape_safe/ape_api/uni_v3.py +++ b/great_ape_safe/ape_api/uni_v3.py @@ -342,6 +342,9 @@ def mint_position(self, pool_addr, range0, range1, token0_amount, token1_amount) pool = interface.IUniswapV3Pool(pool_addr, owner=self.safe.account) + # @note: each pool depending on its fee tier has a different tick spacing + tick_spacing = pool.tickSpacing() + token0 = self.safe.contract(pool.token0()) token1 = self.safe.contract(pool.token1()) @@ -353,8 +356,16 @@ def mint_position(self, pool_addr, range0, range1, token0_amount, token1_amount) decimals_diff = token1.decimals() - token0.decimals() # params for minting method - lower_tick = int(math.log((1 / range1) * 10 ** decimals_diff, BASE) // 60 * 60) - upper_tick = int(math.log((1 / range0) * 10 ** decimals_diff, BASE) // 60 * 60) + lower_tick = int( + math.log((1 / range1) * 10 ** decimals_diff, BASE) + // tick_spacing + * tick_spacing + ) + upper_tick = int( + math.log((1 / range0) * 10 ** decimals_diff, BASE) + // tick_spacing + * tick_spacing + ) deadline = chain.time() + self.deadline # calcs for min amounts From 9d360d763ed5c69b7e25fb393919e271bf74f4da Mon Sep 17 00:00:00 2001 From: Petrovska Date: Wed, 14 Feb 2024 23:44:21 +0400 Subject: [PATCH 7/8] chore: fix minor typo; --- tests/univ3/test_position_creation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/univ3/test_position_creation.py b/tests/univ3/test_position_creation.py index ba1cba31..57cd81d5 100644 --- a/tests/univ3/test_position_creation.py +++ b/tests/univ3/test_position_creation.py @@ -36,8 +36,7 @@ def test_position_creation(treasury, liq, wbtc): positions = treasury.uni_v3.nonfungible_position_manager.positions(token_id) - # TODO: rounding to 4 decimal places, values are not matching the expected? - price_lower_ticket = round( + price_lower_tick = round( ((BASE ** positions["tickLower"]) / (10 ** decimals_diff)), 4 ) price_higher_tick = round( @@ -51,4 +50,4 @@ def test_position_creation(treasury, liq, wbtc): assert positions["token0"] == wbtc.address assert positions["token1"] == liq.address assert positions["liquidity"] > 0 - assert price_lower_ticket < price_higher_tick + assert price_lower_tick < price_higher_tick From 390a7d92c3dd37f3d5d52806101cf6a2d707e0ea Mon Sep 17 00:00:00 2001 From: Petrovska Date: Wed, 14 Feb 2024 23:56:25 +0400 Subject: [PATCH 8/8] feat: use sync method from cdp mngr to inspect coll an debt figures for cdp id --- scripts/cdp_management_lens.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scripts/cdp_management_lens.py b/scripts/cdp_management_lens.py index 389ce18e..fa103fdf 100644 --- a/scripts/cdp_management_lens.py +++ b/scripts/cdp_management_lens.py @@ -45,14 +45,8 @@ def main(msig_address=r.badger_wallets.treasury_vault_multisig): cdps_safe_owned = safe.ebtc.sorted_cdps.getCdpsOf(safe) for cdp_id in cdps_safe_owned: C.print(f"[green]Inspecting cdp id: {cdp_id}\n[/green]") - ( - cdp_id_debt, - cdp_id_coll, - _, - _, - _, - _, - ) = safe.ebtc.cdp_manager.Cdps(cdp_id) + cdp_id_coll = safe.ebtc.cdp_manager.getSyncedCdpCollShares(cdp_id) + cdp_id_debt = safe.ebtc.cdp_manager.getSyncedCdpDebt(cdp_id) icr = safe.ebtc.cdp_manager.getSyncedICR(cdp_id, feed_price) # increase global vars