diff --git a/great_ape_safe/ape_api/uni_v3.py b/great_ape_safe/ape_api/uni_v3.py index 820b0665..0df3bab7 100644 --- a/great_ape_safe/ape_api/uni_v3.py +++ b/great_ape_safe/ape_api/uni_v3.py @@ -62,7 +62,7 @@ def _build_multihop_path(self, path): # 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} + fee_tiers = {100: 0, 500: 0, 3000: 0, 10000: 0} for tier in fee_tiers.keys(): pool_addr = self.factory.getPool(path[i], path[i + 1], tier) @@ -72,7 +72,7 @@ def _build_multihop_path(self, path): pool = interface.IUniswapV3Pool(pool_addr) fee_tiers[tier] = pool.liquidity() - if list(fee_tiers.values()).count(0) == 3: + if list(fee_tiers.values()).count(0) == len(fee_tiers): raise Exception( f"No liquidity found for {path[i].symbol()} - {path[i+1].symbol()}" ) @@ -144,11 +144,12 @@ def get_amounts_for_liquidity( return liquidity, amount0, amount1 - def burn_token_id(self, token_id, burn_nft=False): + def burn_token_id(self, token_id, withdraw_partial_percentage=0, 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 + Returns: Amount withdrawn, without fees collected """ position = self.nonfungible_position_manager.positions(token_id) deadline = chain.time() + self.deadline @@ -162,8 +163,14 @@ def burn_token_id(self, token_id, burn_nft=False): liquidity=position["liquidity"], ) + # allows for partial withdrawal (conditionally) + if withdraw_partial_percentage > 0: + liquidity *= withdraw_partial_percentage + amount0Min *= withdraw_partial_percentage + amount1Min *= withdraw_partial_percentage + # requires to remove all liquidity first - self.nonfungible_position_manager.decreaseLiquidity( + tx = self.nonfungible_position_manager.decreaseLiquidity( ( token_id, liquidity, @@ -172,6 +179,9 @@ def burn_token_id(self, token_id, burn_nft=False): deadline, ) ) + event = tx.events["DecreaseLiquidity"][0] + amount0 = event["amount0"] # Actual amount0 withdrawn from position + amount1 = event["amount1"] # Actual amount1 withdrawn from position # grab also tokens owned, otherwise cannot burn. ref: https://etherscan.io/address/0xc36442b4a4522e871399cd717abdd847ab11fe88#code#F1#L379 position = self.nonfungible_position_manager.positions(token_id) @@ -198,6 +208,8 @@ def burn_token_id(self, token_id, burn_nft=False): # needs to be liq = 0, cleared the pos, otherwise will revert! self.nonfungible_position_manager.burn(token_id) + return amount0, amount1 + def collect_fee(self, token_id): """ collect fees for individual token_id @@ -345,6 +357,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()) @@ -356,8 +371,22 @@ 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( + range1 if decimals_diff == 0 else (1 / range1) * 10 ** decimals_diff, + BASE, + ) + // tick_spacing + * tick_spacing + ) + upper_tick = int( + math.log( + range0 if decimals_diff == 0 else (1 / range0) * 10 ** decimals_diff, + BASE, + ) + // tick_spacing + * tick_spacing + ) deadline = chain.time() + self.deadline # calcs for min amounts diff --git a/helpers/addresses.py b/helpers/addresses.py index 811be54a..c73be5ea 100644 --- a/helpers/addresses.py +++ b/helpers/addresses.py @@ -256,6 +256,7 @@ "LIQ": "0xD82fd4D6D62f89A1E50b1db69AD19932314aa408", "LIQLIT": "0x03C6F0Ca0363652398abfb08d154F114e61c4Ad8", "LUSD": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", + "EBTC": "0x661c70333AA1850CcDBAe82776Bb436A0fCfeEfB", }, # every slp token listed in treasury tokens above must also be listed here. # the lp_tokens in this list are processed by scount to determine holdings diff --git a/scripts/issue/1545/bip_105_execution.py b/scripts/issue/1545/bip_105_execution.py new file mode 100644 index 00000000..64fb1815 --- /dev/null +++ b/scripts/issue/1545/bip_105_execution.py @@ -0,0 +1,172 @@ +from math import sqrt + +from great_ape_safe import GreatApeSafe +from great_ape_safe.ape_api.helpers.uni_v3.uni_v3_sdk import BASE, Q96 + +from helpers.addresses import r +from brownie import interface +from rich.console import Console + +C = Console() + +""" +Active range: https://app.uniswap.org/pools/255188 : 15,780.30 - 31,840.00 +Upper ranges (BADGER only): + 1. https://app.uniswap.org/pools/198350 : 7,962.9 - 15,780.30 + 2. https://app.uniswap.org/pools/167046 : 3,994.14 - 7,962.9 + 3. https://app.uniswap.org/pools/158625 : 1,991.45 - 3,994.14 + 4. https://app.uniswap.org/pools/151049 : 998.898 - 1,991.45 +""" + +# Constants: active range, upper ranges and BIP parameter +ACTIVE_RANGE_NFT_ID = 255188 +UPPER_RANGE_NFT_IDS = [198350, 167046, 158625, 151049] +HALF_LIQUIDITY_PCT = 0.5 + +safe = GreatApeSafe(r.badger_wallets.treasury_vault_multisig) +safe.init_uni_v3() + +# tokens +badger = safe.contract(r.treasury_tokens.BADGER) +ebtc = safe.contract(r.treasury_tokens.EBTC) +wbtc = safe.contract(r.treasury_tokens.WBTC) + +# decimals for calculating tick to prices ratio +decimals_diff = badger.decimals() - wbtc.decimals() + +# Scope: +# 1. Migrates active range from BADGER/WBTC to BADGER/EBTC, partially (50%) +# 2. Migrates upper ranges from BADGER/WBTC to BADGER/EBTC, partially (50%) +def main(): + # existing univ3 pool + univ3_badger_wbtc = safe.contract(r.uniswap.v3pool_wbtc_badger) + + # snap + safe.take_snapshot(tokens=[badger, ebtc, wbtc]) + + # 1. Withdraw 50% of active range + prev_badger_balance = badger.balanceOf(safe) + prev_wbtc_balance = wbtc.balanceOf(safe) + + amount_wbtc, amount_badger = safe.uni_v3.burn_token_id( + ACTIVE_RANGE_NFT_ID, HALF_LIQUIDITY_PCT + ) + C.print( + f"[green]WBTC fee: {(wbtc.balanceOf(safe) - prev_wbtc_balance - amount_wbtc) / 1e8} from active range withdrawal[/green]" + ) + C.print( + f"[green]BADGER fee: {(badger.balanceOf(safe) - prev_badger_balance - amount_badger) / 1e18} from active range withdrawal[/green]" + ) + + # 2. Buy eBTC with withdrawn WBTC funds + C.print(f"[green]WBTC to sell for eBTC: {amount_wbtc}[/green]") + ebtc_balance = safe.uni_v3.swap([wbtc, ebtc], amount_wbtc) + + # 3. Pool creation (BADGER/EBTC) and initilization + pool_ebtc_badger_address = safe.uni_v3.factory.createPool( + ebtc, badger, univ3_badger_wbtc.fee() + ).return_value + C.print(f"[green]Pool address is: {pool_ebtc_badger_address}[/green]") + + ebtc_badger_pool = interface.IUniswapV3Pool( + pool_ebtc_badger_address, owner=safe.account + ) + + # NOTE: token0 will be BADGER + # ref: https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Factory.sol#L41 + C.print(f"[green]Token0 is: {ebtc_badger_pool.token0()}[/green]") + C.print(f"[green]Token1 is: {ebtc_badger_pool.token1()}\n[/green]") + + # aim to initialize at same tick as existing badger/wbtc pool + _, current_tick_badger_wbtc_pool, _, _, _, _, _ = univ3_badger_wbtc.slot0() + C.print( + f"[green]Current badger/wbtc pool tick: {current_tick_badger_wbtc_pool}\n[/green]" + ) + current_price_pool_badger_wbtc = ( + 1 / (BASE ** current_tick_badger_wbtc_pool) * 10 ** decimals_diff + ) + C.print(f"[green]Current price: {current_price_pool_badger_wbtc}\n[/green]") + + sqrt_price_x_96 = sqrt(current_price_pool_badger_wbtc) * Q96 + ebtc_badger_pool.initialize(sqrt_price_x_96) + _, current_tick_badger_ebtc_pool, _, _, _, _, _ = ebtc_badger_pool.slot0() + C.print( + f"[green]Current badger/ebtc pool tick: {current_tick_badger_ebtc_pool}\n[/green]" + ) + + # 4. Create/mirror active range BADGER/EBTC + active_range_0, active_range_1 = _range_prices( + safe.uni_v3.nonfungible_position_manager, ACTIVE_RANGE_NFT_ID, decimals_diff + ) + + C.print( + f"[green]BADGER amount to deposit in active range: {amount_badger / 1e18}[/green]" + ) + C.print( + f"[green]eBTC amount to deposit in active range: {ebtc_balance / 1e18}[/green]" + ) + + safe.uni_v3.mint_position( + pool_ebtc_badger_address, + active_range_0, + active_range_1, + amount_badger, + ebtc_balance, + ) + + # 5. Migrate upper ranges + for nft_id in UPPER_RANGE_NFT_IDS: + range_0, range_1 = _range_prices( + safe.uni_v3.nonfungible_position_manager, nft_id, decimals_diff + ) + + badger_bal_before = badger.balanceOf(safe.address) + wbtc_bal_before = wbtc.balanceOf(safe.address) + amount_wbtc, amount_badger = safe.uni_v3.burn_token_id( + nft_id, HALF_LIQUIDITY_PCT + ) + assert amount_wbtc == 0, "WBTC should be 0" + C.print( + f"[green]wBTC fee: {(wbtc.balanceOf(safe.address) - wbtc_bal_before) / 1e8} from upper range withdrawal[/green]" + ) + C.print( + f"[green]Badger fee: {(badger.balanceOf(safe.address) - badger_bal_before - amount_badger) / 1e18} from upper range withdrawal[/green]" + ) + + # NOTE: ensure clean BADGER approval. Force to set back to zero due to its nature + # otherwise may revert in the internal approval of the class + badger.approve(safe.uni_v3.nonfungible_position_manager.address, 0) + + C.print( + f"[green]BADGER amount to deposit in upper range: {amount_badger / 1e18}[/green]" + ) + + # NOTE: deposit exactly what was obtained from the withdrawal of the upper range, excluding any fees collected + safe.uni_v3.mint_position( + pool_ebtc_badger_address, + range_0, + range_1, + amount_badger, + 0, # should be theoretically 0 eBTC + ) + + safe.post_safe_tx() + + +def _range_prices(position_manager, token_id, decimals_diff): + C.print(f"[green]Inspecting ticks for token id {token_id}...[/green]") + position = position_manager.positions(token_id) + + tick_lower = position["tickLower"] + tick_upper = position["tickUpper"] + C.print(f"[green]tickLower: {tick_lower}[/green]") + C.print(f"[green]tickLower: {tick_upper}\n[/green]") + + range_0 = 1 / (BASE ** tick_lower) * 10 ** decimals_diff + range_1 = 1 / (BASE ** tick_upper) * 10 ** decimals_diff + C.print(f"[green]range_0: {range_0}[/green]") + C.print(f"[green]range_1: {range_1}\n[/green]") + C.print(f"[green]price0: {1/range_0}[/green]") # To match UniV3 UI + C.print(f"[green]price1: {1/range_1}\n[/green]") # To match UniV3 UI + + return range_0, range_1