Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate univ3 badger/wbtc to badger/ebtc partially 50% #1546

Merged
merged 3 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading