# --- AERODROME MANAGER (BASE CHAIN) --- # Adapted from uniswap_manager.py for Aerodrome Slipstream on Base. # Key Differences: # 1. Base Chain RPC & Tokens (WETH/USDC) # 2. Aerodrome Slipstream Contract Addresses # 3. Dynamic Tick Spacing (vs hardcoded 10) # 4. Separate Status File (aerodrome_status.json) import os import sys import time import json import re import logging import math from decimal import Decimal, getcontext from datetime import datetime from typing import Optional, Dict, Tuple, Any, List from web3 import Web3 from web3.exceptions import TimeExhausted, ContractLogicError from eth_account import Account from eth_account.signers.local import LocalAccount from dotenv import load_dotenv # --- IMPORTS FOR KPI --- try: from tools.kpi_tracker import log_kpi_snapshot except ImportError: logging.warning("KPI Tracker not found. Performance logging disabled.") log_kpi_snapshot = None # Set Decimal precision high enough for EVM math getcontext().prec = 60 # --- LOGGING SETUP --- current_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.append(current_dir) # Ensure logs directory exists log_dir = os.path.join(current_dir, 'logs') os.makedirs(log_dir, exist_ok=True) # Logger Name changed to avoid conflict LOGGER_NAME = "AERODROME_MANAGER" try: from logging_utils import setup_logging logger = setup_logging("normal", LOGGER_NAME) except ImportError: logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(LOGGER_NAME) # Custom Filter for Millisecond Unix Timestamp class UnixMsLogFilter(logging.Filter): def filter(self, record): record.unix_ms = int(record.created * 1000) return True # Add File Handler log_file = os.path.join(log_dir, 'aerodrome_manager.log') file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(logging.INFO) file_handler.addFilter(UnixMsLogFilter()) formatter = logging.Formatter('%(unix_ms)d, %(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) logger.addHandler(file_handler) # --- ABIs (Standard V3/Slipstream Compatible) --- NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads(''' [ {"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"}, {"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}, {"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}, {"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"} ] ''') UNISWAP_V3_POOL_ABI = json.loads(''' [ {"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "tickSpacing", "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], "stateMutability": "view", "type": "function"} ] ''') ERC20_ABI = json.loads(''' [ {"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"} ] ''') UNISWAP_V3_FACTORY_ABI = json.loads(''' [ {"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"} ] ''') SWAP_ROUTER_ABI = json.loads(''' [ {"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"} ] ''') WETH9_ABI = json.loads(''' [ {"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"}, {"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"} ] ''') # --- CONFIGURATION (BASE CHAIN / AERODROME) --- # Aerodrome Slipstream Addresses (Base) NONFUNGIBLE_POSITION_MANAGER_ADDRESS = "0x827922686190790b37229fd06084350e74485b72" AERODROME_SWAP_ROUTER_ADDRESS = "0xBE6D8f0d05027F14e266dCC1E844717068f6f296" # Base Chain Tokens WETH_ADDRESS = "0x4200000000000000000000000000000000000006" USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" STATUS_FILE = "aerodrome_status.json" # Separate status file for Base MONITOR_INTERVAL_SECONDS = 666 CLOSE_POSITION_ENABLED = True OPEN_POSITION_ENABLED = True REBALANCE_ON_CLOSE_BELOW_RANGE = True TARGET_INVESTMENT_VALUE_USDC = 2000 INITIAL_HEDGE_CAPITAL_USDC = 2000 RANGE_WIDTH_PCT = Decimal("0.02") # +/- 1% SLIPPAGE_TOLERANCE = Decimal("0.02") TRANSACTION_TIMEOUT_SECONDS = 30 # --- HELPER FUNCTIONS --- def clean_address(addr: str) -> str: """Ensure address is checksummed.""" if not Web3.is_address(addr): raise ValueError(f"Invalid address: {addr}") return Web3.to_checksum_address(addr) def to_decimal(value: Any, decimals: int = 0) -> Decimal: """Convert value to Decimal, optionally scaling down by decimals.""" if isinstance(value, Decimal): return value return Decimal(value) / (Decimal(10) ** decimals) def to_wei_int(value: Decimal, decimals: int) -> int: """Convert Decimal value to integer Wei representation.""" return int(value * (Decimal(10) ** decimals)) def get_gas_params(w3: Web3) -> Dict[str, int]: """Get dynamic gas parameters for EIP-1559 (Base Chain).""" latest_block = w3.eth.get_block("latest") base_fee = latest_block['baseFeePerGas'] # Base chain fees are low, but we prioritize max_priority_fee = w3.eth.max_priority_fee or Web3.to_wei(0.05, 'gwei') # Max Fee = Base Fee * 1.25 + Priority Fee max_fee = int(base_fee * 1.25) + max_priority_fee return { 'maxFeePerGas': max_fee, 'maxPriorityFeePerGas': max_priority_fee } def send_transaction_robust( w3: Web3, account: LocalAccount, func_call: Any, value: int = 0, gas_limit: Optional[int] = None, extra_msg: str = "" ) -> Optional[Any]: try: tx_params = { 'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address), 'value': value, 'chainId': w3.eth.chain_id, } gas_fees = get_gas_params(w3) tx_params.update(gas_fees) try: if hasattr(func_call, 'call'): func_call.call({'from': account.address, 'value': value}) estimated_gas = func_call.estimate_gas({'from': account.address, 'value': value}) else: estimated_gas = 200000 tx_params['gas'] = gas_limit if gas_limit else int(estimated_gas * 1.2) if hasattr(func_call, 'build_transaction'): tx = func_call.build_transaction(tx_params) else: raise ValueError("Invalid function call object") except ContractLogicError as e: logger.error(f"❌ Simulation/Estimation failed for {extra_msg}: {e}") return None signed_tx = account.sign_transaction(tx) tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) logger.info(f"📤 Sent {extra_msg} | Hash: {tx_hash.hex()}") receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=TRANSACTION_TIMEOUT_SECONDS) if receipt.status == 1: logger.info(f"✅ Executed {extra_msg} | Block: {receipt.blockNumber}") return receipt else: logger.error(f"❌ Transaction Reverted {extra_msg} | Hash: {tx_hash.hex()}") return None except TimeExhausted: logger.error(f"⌛ Transaction Timeout {extra_msg} - Check Mempool") return None except Exception as e: logger.error(f"❌ Transaction Error {extra_msg}: {e}") return None def price_from_sqrt_price_x96(sqrt_price_x96: int, token0_decimals: int, token1_decimals: int) -> Decimal: sqrt_price = Decimal(sqrt_price_x96) q96 = Decimal(2) ** 96 price = (sqrt_price / q96) ** 2 adjustment = Decimal(10) ** (token0_decimals - token1_decimals) return price * adjustment def price_from_tick(tick: int, token0_decimals: int, token1_decimals: int) -> Decimal: price = Decimal("1.0001") ** tick adjustment = Decimal(10) ** (token0_decimals - token1_decimals) return price * adjustment def get_sqrt_ratio_at_tick(tick: int) -> int: return int((1.0001 ** (tick / 2)) * (2 ** 96)) def get_amounts_for_liquidity(sqrt_ratio_current: int, sqrt_ratio_a: int, sqrt_ratio_b: int, liquidity: int) -> Tuple[int, int]: if sqrt_ratio_a > sqrt_ratio_b: sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a amount0 = 0 amount1 = 0 Q96 = 1 << 96 if sqrt_ratio_current <= sqrt_ratio_a: amount0 = (liquidity * Q96 // sqrt_ratio_a) - (liquidity * Q96 // sqrt_ratio_b) amount1 = 0 elif sqrt_ratio_current < sqrt_ratio_b: amount0 = (liquidity * Q96 // sqrt_ratio_current) - (liquidity * Q96 // sqrt_ratio_b) amount1 = (liquidity * (sqrt_ratio_current - sqrt_ratio_a)) // Q96 else: amount1 = (liquidity * (sqrt_ratio_b - sqrt_ratio_a)) // Q96 amount0 = 0 return amount0, amount1 # --- CORE LOGIC --- def get_position_details(w3: Web3, npm_contract, factory_contract, token_id: int): try: position_data = npm_contract.functions.positions(token_id).call() (nonce, operator, token0_address, token1_address, fee, tickLower, tickUpper, liquidity, feeGrowthInside0, feeGrowthInside1, tokensOwed0, tokensOwed1) = position_data token0_contract = w3.eth.contract(address=token0_address, abi=ERC20_ABI) token1_contract = w3.eth.contract(address=token1_address, abi=ERC20_ABI) token0_symbol = token0_contract.functions.symbol().call() token1_symbol = token1_contract.functions.symbol().call() token0_decimals = token0_contract.functions.decimals().call() token1_decimals = token1_contract.functions.decimals().call() pool_address = factory_contract.functions.getPool(token0_address, token1_address, fee).call() if pool_address == '0x0000000000000000000000000000000000000000': return None, None pool_contract = w3.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI) return { "token0_address": token0_address, "token1_address": token1_address, "token0_symbol": token0_symbol, "token1_symbol": token1_symbol, "token0_decimals": token0_decimals, "token1_decimals": token1_decimals, "fee": fee, "tickLower": tickLower, "tickUpper": tickUpper, "liquidity": liquidity, "pool_address": pool_address }, pool_contract except Exception as e: logger.error(f"❌ Error fetching position details for ID {token_id}: {e}") return None, None def get_pool_dynamic_data(pool_contract) -> Optional[Dict[str, Any]]: try: slot0 = pool_contract.functions.slot0().call() return {"sqrtPriceX96": slot0[0], "tick": slot0[1]} except Exception as e: logger.error(f"❌ Pool data fetch failed: {e}") return None def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_value_token1: Decimal, decimals0, decimals1, sqrt_price_current_x96) -> Tuple[int, int]: sqrt_price_current = get_sqrt_ratio_at_tick(current_tick) sqrt_price_lower = get_sqrt_ratio_at_tick(tick_lower) sqrt_price_upper = get_sqrt_ratio_at_tick(tick_upper) price_t0_in_t1 = price_from_sqrt_price_x96(sqrt_price_current_x96, decimals0, decimals1) L_test = 1 << 128 amt0_test_wei, amt1_test_wei = get_amounts_for_liquidity(sqrt_price_current, sqrt_price_lower, sqrt_price_upper, L_test) amt0_test = Decimal(amt0_test_wei) / Decimal(10**decimals0) amt1_test = Decimal(amt1_test_wei) / Decimal(10**decimals1) value_test = (amt0_test * price_t0_in_t1) + amt1_test if value_test <= 0: return 0, 0 scale = investment_value_token1 / value_test final_amt0_wei = int(Decimal(amt0_test_wei) * scale) final_amt1_wei = int(Decimal(amt1_test_wei) * scale) return final_amt0_wei, final_amt1_wei def ensure_allowance(w3: Web3, account: LocalAccount, token_address: str, spender_address: str, amount_needed: int) -> bool: try: token_c = w3.eth.contract(address=token_address, abi=ERC20_ABI) allowance = token_c.functions.allowance(account.address, spender_address).call() if allowance >= amount_needed: return True logger.info(f"🔓 Approving {token_address} for {spender_address}...") if allowance > 0: send_transaction_robust(w3, account, token_c.functions.approve(spender_address, 0), extra_msg="Reset Allowance") receipt = send_transaction_robust( w3, account, token_c.functions.approve(spender_address, amount_needed), extra_msg=f"Approve {token_address}" ) return receipt is not None except Exception as e: logger.error(f"❌ Allowance check/approve failed: {e}") return False def check_and_swap_for_deposit(w3: Web3, router_contract, account: LocalAccount, token0: str, token1: str, amount0_needed: int, amount1_needed: int, sqrt_price_x96: int, d0: int, d1: int) -> bool: token0 = clean_address(token0) token1 = clean_address(token1) token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI) token1_c = w3.eth.contract(address=token1, abi=ERC20_ABI) bal0 = token0_c.functions.balanceOf(account.address).call() bal1 = token1_c.functions.balanceOf(account.address).call() deficit0 = max(0, amount0_needed - bal0) deficit1 = max(0, amount1_needed - bal1) weth_lower = WETH_ADDRESS.lower() # Auto-wrap ETH on Base if (deficit0 > 0 and token0.lower() == weth_lower) or (deficit1 > 0 and token1.lower() == weth_lower): eth_bal = w3.eth.get_balance(account.address) gas_reserve = Web3.to_wei(0.005, 'ether') # Lower gas reserve for Base available_eth = max(0, eth_bal - gas_reserve) wrap_needed = 0 if token0.lower() == weth_lower: wrap_needed += deficit0 if token1.lower() == weth_lower: wrap_needed += deficit1 amount_to_wrap = min(wrap_needed, available_eth) if amount_to_wrap > 0: logger.info(f"🌯 Wrapping {Web3.from_wei(amount_to_wrap, 'ether')} ETH...") weth_c = w3.eth.contract(address=WETH_ADDRESS, abi=WETH9_ABI) receipt = send_transaction_robust(w3, account, weth_c.functions.deposit(), value=amount_to_wrap, extra_msg="Wrap ETH") if receipt: bal0 = token0_c.functions.balanceOf(account.address).call() bal1 = token1_c.functions.balanceOf(account.address).call() deficit0 = max(0, amount0_needed - bal0) deficit1 = max(0, amount1_needed - bal1) if deficit0 == 0 and deficit1 == 0: return True price_0_in_1 = price_from_sqrt_price_x96(sqrt_price_x96, d0, d1) token_in, token_out = None, None amount_in = 0 buffer_multiplier = Decimal("1.02") if deficit0 > 0 and bal1 > amount1_needed: cost_in_t1 = Decimal(deficit0) / Decimal(10**d0) * price_0_in_1 amount_in_needed = int(cost_in_t1 * Decimal(10**d1) * buffer_multiplier) surplus1 = bal1 - amount1_needed if surplus1 >= amount_in_needed: token_in, token_out = token1, token0 amount_in = amount_in_needed logger.info(f"🧮 Calc: Need {deficit0} T0. Cost ~{amount_in_needed} T1. Surplus: {surplus1}") else: logger.warning(f"❌ Insufficient Surplus T1. Need {amount_in_needed}, Have {surplus1}") elif deficit1 > 0 and bal0 > amount0_needed: if price_0_in_1 > 0: cost_in_t0 = (Decimal(deficit1) / Decimal(10**d1)) / price_0_in_1 amount_in_needed = int(cost_in_t0 * Decimal(10**d0) * buffer_multiplier) surplus0 = bal0 - amount0_needed if surplus0 >= amount_in_needed: token_in, token_out = token0, token1 amount_in = amount_in_needed logger.info(f"🧮 Calc: Need {deficit1} T1. Cost ~{amount_in_needed} T0. Surplus: {surplus0}") else: logger.warning(f"❌ Insufficient Surplus T0. Need {amount_in_needed}, Have {surplus0}") if token_in and amount_in > 0: logger.info(f"🔄 Swapping {amount_in} {token_in} to cover deficit...") if not ensure_allowance(w3, account, token_in, AERODROME_SWAP_ROUTER_ADDRESS, amount_in): return False params = ( token_in, token_out, 500, account.address, int(time.time()) + 120, amount_in, 0, 0 ) receipt = send_transaction_robust(w3, account, router_contract.functions.exactInputSingle(params), extra_msg="Swap Surplus") if receipt: bal0 = token0_c.functions.balanceOf(account.address).call() bal1 = token1_c.functions.balanceOf(account.address).call() if bal0 >= amount0_needed and bal1 >= amount1_needed: return True else: logger.warning(f"⚠️ Swap executed but still short? T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}") return False logger.warning(f"❌ Insufficient funds. T0: {bal0}/{amount0_needed}, T1: {bal1}/{amount1_needed}") return False def mint_new_position(w3: Web3, npm_contract, account: LocalAccount, token0: str, token1: str, amount0: int, amount1: int, tick_lower: int, tick_upper: int) -> Optional[Dict]: logger.info("🚀 Minting new position on Aerodrome...") if not ensure_allowance(w3, account, token0, NONFUNGIBLE_POSITION_MANAGER_ADDRESS, amount0): return None if not ensure_allowance(w3, account, token1, NONFUNGIBLE_POSITION_MANAGER_ADDRESS, amount1): return None amount0_min = int(Decimal(amount0) * (Decimal(1) - SLIPPAGE_TOLERANCE)) amount1_min = int(Decimal(amount1) * (Decimal(1) - SLIPPAGE_TOLERANCE)) params = ( token0, token1, 500, tick_lower, tick_upper, amount0, amount1, amount0_min, amount1_min, account.address, int(time.time()) + 180 ) receipt = send_transaction_robust(w3, account, npm_contract.functions.mint(params), extra_msg="Mint Position") if receipt and receipt.status == 1: try: transfer_topic = Web3.keccak(text="Transfer(address,address,uint256)").hex() increase_liq_topic = Web3.keccak(text="IncreaseLiquidity(uint256,uint128,uint256,uint256)").hex() minted_data = {'token_id': None, 'amount0': 0, 'amount1': 0} for log in receipt.logs: topics = [t.hex() for t in log['topics']] if topics[0] == transfer_topic: if "0000000000000000000000000000000000000000" in topics[1]: minted_data['token_id'] = int(topics[3], 16) if topics[0] == increase_liq_topic: data = log['data'].hex() if data.startswith('0x'): data = data[2:] minted_data['amount0'] = int(data[64:128], 16) minted_data['amount1'] = int(data[128:192], 16) if minted_data['token_id']: d0, d1 = 18, 6 fmt_amt0 = Decimal(minted_data['amount0']) / Decimal(10**d0) fmt_amt1 = Decimal(minted_data['amount1']) / Decimal(10**d1) logger.info(f"✅ POSITION OPENED | ID: {minted_data['token_id']} | Deposited: {fmt_amt0:.6f} WETH + {fmt_amt1:.2f} USDC") return minted_data except Exception as e: logger.warning(f"Minted but failed to parse details: {e}") return None def decrease_liquidity(w3: Web3, npm_contract, account: LocalAccount, token_id: int, liquidity: int) -> bool: if liquidity == 0: return True logger.info(f"📉 Decreasing Liquidity for {token_id}...") params = ( token_id, liquidity, 0, 0, int(time.time()) + 180 ) receipt = send_transaction_robust(w3, account, npm_contract.functions.decreaseLiquidity(params), extra_msg=f"Decrease Liq {token_id}") if receipt and receipt.status == 1: try: decrease_topic = Web3.keccak(text="DecreaseLiquidity(uint256,uint128,uint256,uint256)").hex() amt0, amt1 = 0, 0 for log in receipt.logs: topics = [t.hex() for t in log['topics']] if topics[0] == decrease_topic and int(topics[1], 16) == token_id: data = log['data'].hex()[2:] amt0 = int(data[64:128], 16) amt1 = int(data[128:192], 16) break d0, d1 = 18, 6 fmt_amt0 = Decimal(amt0) / Decimal(10**d0) fmt_amt1 = Decimal(amt1) / Decimal(10**d1) logger.info(f"📉 POSITION CLOSED | ID: {token_id} | Withdrawn: {fmt_amt0:.6f} WETH + {fmt_amt1:.2f} USDC") except Exception as e: logger.warning(f"Closed but failed to parse details: {e}") return True return False def collect_fees(w3: Web3, npm_contract, account: LocalAccount, token_id: int) -> bool: logger.info(f"💰 Collecting Fees for {token_id}...") max_val = 2**128 - 1 params = (token_id, account.address, max_val, max_val) receipt = send_transaction_robust(w3, account, npm_contract.functions.collect(params), extra_msg=f"Collect Fees {token_id}") return receipt is not None def load_status_data() -> List[Dict]: if not os.path.exists(STATUS_FILE): return [] try: with open(STATUS_FILE, 'r') as f: return json.load(f) except: return [] def save_status_data(data: List[Dict]): with open(STATUS_FILE, 'w') as f: json.dump(data, f, indent=2) def update_position_status(token_id: int, status: str, extra_data: Dict = {}): data = load_status_data() entry = next((item for item in data if item.get('token_id') == token_id), None) if not entry: if status in ["OPEN", "PENDING_HEDGE"]: entry = {"type": "AUTOMATIC", "token_id": token_id} data.append(entry) else: return entry['status'] = status entry.update(extra_data) if status == "CLOSED": entry['timestamp_close'] = int(time.time()) save_status_data(data) logger.info(f"💾 Updated Position {token_id} status to {status}") def main(): logger.info("🔷 Aerodrome Manager (Base Chain) Starting...") load_dotenv(override=True) rpc_url = os.environ.get("BASE_RPC_URL") # UPDATED ENV VAR private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY") if not rpc_url or not private_key: logger.error("❌ Missing BASE_RPC_URL or MAIN_WALLET_PRIVATE_KEY in .env") return w3 = Web3(Web3.HTTPProvider(rpc_url)) if not w3.is_connected(): logger.error("❌ Could not connect to Base RPC") return account = Account.from_key(private_key) logger.info(f"👤 Wallet: {account.address}") npm = w3.eth.contract(address=clean_address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), abi=NONFUNGIBLE_POSITION_MANAGER_ABI) factory_addr = npm.functions.factory().call() factory = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI) router = w3.eth.contract(address=clean_address(AERODROME_SWAP_ROUTER_ADDRESS), abi=SWAP_ROUTER_ABI) while True: try: status_data = load_status_data() open_positions = [p for p in status_data if p.get('status') == 'OPEN'] active_auto_pos = next((p for p in open_positions if p.get('type') == 'AUTOMATIC'), None) if active_auto_pos: token_id = active_auto_pos['token_id'] pos_details, pool_c = get_position_details(w3, npm, factory, token_id) if pos_details: pool_data = get_pool_dynamic_data(pool_c) current_tick = pool_data['tick'] tick_lower = pos_details['tickLower'] tick_upper = pos_details['tickUpper'] in_range = tick_lower <= current_tick < tick_upper current_price = price_from_tick(current_tick, pos_details['token0_decimals'], pos_details['token1_decimals']) lower_price = price_from_tick(tick_lower, pos_details['token0_decimals'], pos_details['token1_decimals']) upper_price = price_from_tick(tick_upper, pos_details['token0_decimals'], pos_details['token1_decimals']) status_msg = "✅ IN RANGE" if in_range else "⚠️ OUT OF RANGE" # Simulated Fees unclaimed0, unclaimed1, total_fees_usd = 0, 0, 0 try: fees_sim = npm.functions.collect((token_id, "0x0000000000000000000000000000000000000000", 2**128-1, 2**128-1)).call({'from': account.address}) unclaimed0 = to_decimal(fees_sim[0], pos_details['token0_decimals']) unclaimed1 = to_decimal(fees_sim[1], pos_details['token1_decimals']) total_fees_usd = (unclaimed0 * current_price) + unclaimed1 except: pass # PnL Calc initial_value = Decimal(str(active_auto_pos.get('target_value', 0))) curr_amt0_wei, curr_amt1_wei = get_amounts_for_liquidity( pool_data['sqrtPriceX96'], get_sqrt_ratio_at_tick(tick_lower), get_sqrt_ratio_at_tick(tick_upper), pos_details['liquidity'] ) curr_amt0 = Decimal(curr_amt0_wei) / Decimal(10**pos_details['token0_decimals']) curr_amt1 = Decimal(curr_amt1_wei) / Decimal(10**pos_details['token1_decimals']) current_pos_value_usd = (curr_amt0 * current_price) + curr_amt1 pnl_unrealized = current_pos_value_usd - initial_value total_pnl_usd = pnl_unrealized + total_fees_usd logger.info(f"Position {token_id}: {status_msg} | Price: {current_price:.4f} [{lower_price:.4f} - {upper_price:.4f}] | TotPnL: ${total_pnl_usd:.2f} (Fees: ${total_fees_usd:.2f})") if not in_range and CLOSE_POSITION_ENABLED: logger.warning(f"🛑 Closing Position {token_id} (Out of Range)") update_position_status(token_id, "CLOSING") if decrease_liquidity(w3, npm, account, token_id, pos_details['liquidity']): collect_fees(w3, npm, account, token_id) update_position_status(token_id, "CLOSED") elif OPEN_POSITION_ENABLED: logger.info("🔍 No active position. Analyzing Aerodrome market...") token0 = clean_address(WETH_ADDRESS) token1 = clean_address(USDC_ADDRESS) fee = 500 # 0.05% fee tier on Aerodrome Slipstream WETH/USDC pool_addr = factory.functions.getPool(token0, token1, fee).call() pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI) pool_data = get_pool_dynamic_data(pool_c) if pool_data: tick = pool_data['tick'] # Get Tick Spacing dynamically tick_spacing = pool_c.functions.tickSpacing().call() tick_delta = int(math.log(1 + float(RANGE_WIDTH_PCT)) / math.log(1.0001)) tick_lower = (tick - tick_delta) // tick_spacing * tick_spacing tick_upper = (tick + tick_delta) // tick_spacing * tick_spacing d0, d1 = 18, 6 investment_val_dec = Decimal(str(TARGET_INVESTMENT_VALUE_USDC)) amt0, amt1 = calculate_mint_amounts(tick, tick_lower, tick_upper, investment_val_dec, d0, d1, pool_data['sqrtPriceX96']) if check_and_swap_for_deposit(w3, router, account, token0, token1, amt0, amt1, pool_data['sqrtPriceX96'], d0, d1): minted = mint_new_position(w3, npm, account, token0, token1, amt0, amt1, tick_lower, tick_upper) if minted: entry_price = float(price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)) fmt_amt0 = float(Decimal(minted['amount0']) / Decimal(10**d0)) fmt_amt1 = float(Decimal(minted['amount1']) / Decimal(10**d1)) actual_value = (fmt_amt0 * entry_price) + fmt_amt1 new_position_data = { "type": "AUTOMATIC", "target_value": round(float(actual_value), 2), "entry_price": round(entry_price, 2), "amount0_initial": round(fmt_amt0, 4), "amount1_initial": round(fmt_amt1, 2), "range_upper": round(float(price_from_tick(tick_upper, d0, d1)), 2), "range_lower": round(float(price_from_tick(tick_lower, d0, d1)), 2), "timestamp_open": int(time.time()) } update_position_status(minted['token_id'], "OPEN", new_position_data) sleep_time = MONITOR_INTERVAL_SECONDS if active_auto_pos else 37 time.sleep(sleep_time) except KeyboardInterrupt: logger.info("👋 Exiting...") break except Exception as e: logger.error(f"❌ Main Loop Error: {e}") time.sleep(60) if __name__ == "__main__": main()