diff --git a/fee_allocator/abi/GyroECLPPoolFactory.json b/fee_allocator/abi/GyroECLPPoolFactory.json new file mode 100644 index 00000000..940e0f7b --- /dev/null +++ b/fee_allocator/abi/GyroECLPPoolFactory.json @@ -0,0 +1,298 @@ +[ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + }, + { + "internalType": "address", + "name": "_gyroConfigAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "int256", + "name": "alpha", + "type": "int256" + }, + { + "internalType": "int256", + "name": "beta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "c", + "type": "int256" + }, + { + "internalType": "int256", + "name": "s", + "type": "int256" + }, + { + "internalType": "int256", + "name": "lambda", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Params", + "name": "eclpParams", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Vector2", + "name": "tauAlpha", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + }, + { + "internalType": "int256", + "name": "y", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.Vector2", + "name": "tauBeta", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "u", + "type": "int256" + }, + { + "internalType": "int256", + "name": "v", + "type": "int256" + }, + { + "internalType": "int256", + "name": "w", + "type": "int256" + }, + { + "internalType": "int256", + "name": "z", + "type": "int256" + }, + { + "internalType": "int256", + "name": "dSq", + "type": "int256" + } + ], + "internalType": "struct GyroECLPMath.DerivedParams", + "name": "derivedECLPParams", + "type": "tuple" + }, + { + "internalType": "address[]", + "name": "rateProviders", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "capManager", + "type": "address" + }, + { + "components": [ + { + "internalType": "bool", + "name": "capEnabled", + "type": "bool" + }, + { + "internalType": "uint120", + "name": "perAddressCap", + "type": "uint120" + }, + { + "internalType": "uint128", + "name": "globalCap", + "type": "uint128" + } + ], + "internalType": "struct ICappedLiquidity.CapParams", + "name": "capParams", + "type": "tuple" + }, + { + "internalType": "address", + "name": "pauseManager", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "pauseWindowDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodDuration", + "type": "uint256" + } + ], + "internalType": "struct ILocallyPausable.PauseParams", + "name": "pauseParams", + "type": "tuple" + } + ], + "name": "create", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getCreationCode", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCreationCodeContracts", + "outputs": [ + { + "internalType": "address", + "name": "contractA", + "type": "address" + }, + { + "internalType": "address", + "name": "contractB", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gyroConfigAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "isPoolFromFactory", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/fee_allocator/accounting/collectors.py b/fee_allocator/accounting/collectors.py index 2db5af05..f46f9d6b 100644 --- a/fee_allocator/accounting/collectors.py +++ b/fee_allocator/accounting/collectors.py @@ -8,7 +8,7 @@ def collect_fee_info( - pools: list[str], + pools: dict[str, str], chain: Chains, pools_now: list[dict], pools_shifted: list[Dict], @@ -22,7 +22,7 @@ def collect_fee_info( """ fees = {} token_fees = defaultdict(list) - for pool in pools: + for pool, symbol in pools.items(): poolutil = BalPoolsGauges(chain.value) if not poolutil.has_alive_preferential_gauge(pool): print( @@ -81,7 +81,7 @@ def collect_fee_info( ) token_fees_in_usd += Decimal(token_fee) * Decimal(token_price) fees[pool_snapshot_now["pool"]["id"]] = { - "symbol": pool_snapshot_now["pool"]["symbol"], + "symbol": symbol, "pool_addr": pool_snapshot_now["pool"]["address"], "bpt_token_fee": round(bpt_token_fee, 2), # One of two fields below should always be 0 because diff --git a/fee_allocator/accounting/fee_pipeline.py b/fee_allocator/accounting/fee_pipeline.py index 88989885..846ea2da 100644 --- a/fee_allocator/accounting/fee_pipeline.py +++ b/fee_allocator/accounting/fee_pipeline.py @@ -26,6 +26,7 @@ from fee_allocator.helpers import get_balancer_pool_snapshots from fee_allocator.helpers import get_block_by_ts from fee_allocator.helpers import get_twap_bpt_price +from fee_allocator.helpers import get_eclp_fee_split_pools def run_fees( @@ -35,7 +36,7 @@ def run_fees( output_file_name: str, fees_to_distribute: dict, mapped_pools_info: dict, -) -> dict: +) -> tuple[dict, Decimal]: """ This function is used to run the fee allocation process """ @@ -48,8 +49,10 @@ def run_fees( target_blocks = {} pool_snapshots = {} collected_fees = {} + collected_fees_eclp = {chain.value: {} for chain in Chains} incentives = {} bpt_twap_prices = {chain.value: {} for chain in Chains} + total_fees_to_gyro = Decimal(0) # Estimate mainnet current block to calculate aura veBAL share _target_mainnet_block = get_block_by_ts(timestamp_now, Chains.MAINNET.value) @@ -60,11 +63,19 @@ def run_fees( f"veBAL aura share at block {_target_mainnet_block}: {aura_vebal_share}" ) existing_aura_bribs: List[Dict] = fetch_hh_aura_bribs() + eclp_pools = get_eclp_fee_split_pools(web3_instances) # Collect all BPT prices: for chain in Chains: print(f"Collecting BPT prices for Chain {chain.value}") poolutil = BalPoolsGauges(chain.value) - listed_core_pools = core_pools.get(chain.value, None) + listed_eclp_pools = { + id: symbol + for id, symbol in eclp_pools.get(chain.value, {}).items() + if not poolutil.is_core_pool(id) + } + # combine eclp fee split pools with core pools, we can differentiate them after fee calcs based on the `-fee-split` symbol suffix + listed_core_pools = {**listed_eclp_pools, **core_pools.get(chain.value, None)} + pools = {} if listed_core_pools is None: continue @@ -121,7 +132,7 @@ def run_fees( f"{target_blocks[chain.value]}" ) collected_fees[chain.value] = collect_fee_info( - core_pools[chain.value], + pools, chain, pool_snapshots[chain.value][0], pool_snapshots[chain.value][1], @@ -130,6 +141,25 @@ def run_fees( bpt_twap_prices=bpt_twap_prices, ) + collected_fees_eclp[chain.value] = { + pool_id: fee_info + for pool_id, fee_info in collected_fees[chain.value].items() + if fee_info["symbol"].endswith("-fee-split") + } + total_fees_to_gyro += Decimal( + sum( + fee_info["bpt_token_fee_in_usd"] + fee_info["token_fees_in_usd"] + for fee_info in collected_fees_eclp[chain.value].values() + ) + / 2 + ) + + collected_fees[chain.value] = { + pool_id: fee_info + for pool_id, fee_info in collected_fees[chain.value].items() + if not fee_info["symbol"].endswith("-fee-split") + } + # Now we have all the data we need to run the fee allocation process logger.info(f"Running fee allocation for {chain.value}") _incentives = calc_and_split_incentives( @@ -171,4 +201,4 @@ def run_fees( PROJECT_ROOT, f"fee_allocator/allocations/{output_file_name}" ) incentives_df_sorted.to_csv(allocations_file_name) - return joint_incentives_data + return joint_incentives_data, total_fees_to_gyro diff --git a/fee_allocator/accounting/recon.py b/fee_allocator/accounting/recon.py index 30257a2e..fb8a9c4d 100644 --- a/fee_allocator/accounting/recon.py +++ b/fee_allocator/accounting/recon.py @@ -142,8 +142,8 @@ def recon_and_validate( def generate_and_save_input_csv( - fees: dict, period_ends: int, mapped_pools_info: Dict -) -> None: + fees: dict, period_ends: int, mapped_pools_info: Dict, fees_to_gyro: Decimal +) -> str: """ Function that generates and saves csv in format: target_root_gauge,platform,amount_of_incentives @@ -192,6 +192,14 @@ def generate_and_save_input_csv( "amount": round(dao_share, 4), } ) + # Add gyro share to the output + output.append( + { + "target": "", # gyro fee recipient + "platform": "payment", + "amount": round(fees_to_gyro, 4), + } + ) # Convert to dataframe and save to csv df = pd.DataFrame(output) diff --git a/fee_allocator/helpers.py b/fee_allocator/helpers.py index 42e676de..e41eab3f 100644 --- a/fee_allocator/helpers.py +++ b/fee_allocator/helpers.py @@ -10,8 +10,10 @@ from typing import Optional from typing import Union +from munch import Munch import requests from bal_tools import Subgraph +from fee_allocator.accounting.settings import Chains from gql import Client from gql import gql from gql.transport.requests import RequestsHTTPTransport @@ -143,6 +145,16 @@ class PoolBalance: }, } +GYRO_FACTORIES = { + "mainnet": "0x412a5B2e7a678471985542757A6855847D4931D5", + "arbitrum": "0xdCA5f1F0d7994A32BC511e7dbA0259946653Eaf6", + "polygon": "0x1a79A24Db0F73e9087205287761fC9C5C305926b", + "base": "0x15e86Be6084C6A5a8c17732D398dFbC2Ec574CEC", + "gnosis": "0x5d3Be8aaE57bf0D1986Ff7766cC9607B6cC99b89", + "avalanche": "0x41E9ac0bfed353c2dE21a980dA0EbB8A464D946A", + "zkevm": "0x5D56EA1B2595d2dbe4f5014b967c78ce75324f0c", +} + HH_AURA_URL = "https://api.hiddenhand.finance/proposal/aura" @@ -293,7 +305,11 @@ def fetch_token_price_balgql_timerange( ) ) result = client.execute(query) - prices = result["tokenGetHistoricalPrices"][0]["prices"] + try: + prices = result["tokenGetHistoricalPrices"][0]["prices"] + except IndexError: + print(f"No prices found for token {token_addr} on chain {chain}") + return None # Filter results so they are in between start_date and end_date timestamps # Sort result by timestamp desc time_sorted_prices = sorted(prices, key=lambda x: int(x["timestamp"]), reverse=True) @@ -376,3 +392,23 @@ def fetch_hh_aura_bribs() -> List[Dict]: if response_parsed["error"]: raise ValueError("HH API returned error") return response_parsed["data"] + + +def get_eclp_fee_split_pools(web3_instances: Munch[str, Web3]) -> Dict: + eclp_pools = {chain.value: {} for chain in Chains} + for pool in fetch_all_pools_info(): + pool_id, address, symbol, chain = ( + pool["id"], + Web3.to_checksum_address(pool["address"]), + pool["symbol"], + pool["chain"].lower(), + ) + if chain in eclp_pools.keys(): + factory = web3_instances[chain].eth.contract( + address=GYRO_FACTORIES[chain], abi=get_abi("GyroECLPPoolFactory") + ) + is_eclp_pool = factory.functions.isPoolFromFactory(address).call() + print(f"{chain}: {pool_id} is eclp pool: {is_eclp_pool}") + if is_eclp_pool: + eclp_pools[chain][pool_id] = f"{symbol}-fee-split" + return eclp_pools diff --git a/main.py b/main.py index 43f0dbb5..274c05f2 100644 --- a/main.py +++ b/main.py @@ -111,7 +111,7 @@ def main() -> None: ) web3_instances = Web3RpcByChain(DRPC_KEY) - collected_fees = run_fees( + collected_fees, fees_to_gyro = run_fees( web3_instances, ts_now, ts_in_the_past, @@ -130,7 +130,9 @@ def main() -> None: ts_in_the_past, target_aura_vebal_share, ) - csvfile = generate_and_save_input_csv(collected_fees, ts_now, mapped_pools_info) + csvfile = generate_and_save_input_csv( + collected_fees, ts_now, mapped_pools_info, fees_to_gyro + ) if output_file_name != "current_fees.csv": generate_payload(web3_instances["mainnet"], csvfile) diff --git a/v2 b/v2 index 4b2cd282..02cc77ba 160000 --- a/v2 +++ b/v2 @@ -1 +1 @@ -Subproject commit 4b2cd2821084060953f2a5616f6b1477d20730e2 +Subproject commit 02cc77ba690fcc71266d436d1f137cdd8fad8be0