Skip to content

Commit

Permalink
Merge pull request #1546 from Badger-Finance/feat/tcl-migration
Browse files Browse the repository at this point in the history
feat: migrate univ3 badger/wbtc to badger/ebtc partially 50%
  • Loading branch information
petrovska-petro authored Jun 20, 2024
2 parents 705541a + 4d187cd commit dfdde7a
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 6 deletions.
41 changes: 35 additions & 6 deletions great_ape_safe/ape_api/uni_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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()}"
)
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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())

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions helpers/addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
172 changes: 172 additions & 0 deletions scripts/issue/1545/bip_105_execution.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit dfdde7a

Please sign in to comment.