diff --git a/fee_allocator/accounting/chains.py b/fee_allocator/accounting/chains.py index 0818ff44..5637f994 100644 --- a/fee_allocator/accounting/chains.py +++ b/fee_allocator/accounting/chains.py @@ -73,7 +73,6 @@ def __init__( self.ezkl_pools = requests.get(EZKL_POOLS_URL).json() pool_overrides_raw = requests.get(POOL_OVERRIDES_URL).json() - self.pool_overrides: Dict[str, PoolOverride] = { pool_id: PoolOverride(**override_data) for pool_id, override_data in pool_overrides_raw.items() @@ -84,7 +83,6 @@ def __init__( self.cache_dir.mkdir(exist_ok=True) self._chains: Union[dict[str, CorePoolChain], None] = None - self.aura_vebal_share: Union[Decimal, None] = None self.protocol_version = protocol_version @@ -118,16 +116,6 @@ def set_core_pool_chains_data(self): self._chains = _chains - def set_aura_vebal_share(self): - if not self.mainnet: - raise ValueError( - "mainnet must be initialized to calculate aura vebal share" - ) - - self.aura_vebal_share = self.mainnet.subgraph.calculate_aura_vebal_share( - self.mainnet.web3, self.mainnet.block_range[1] - ) - def set_initial_pool_allocation(self) -> None: """ sets the intial fee allocation for all pools for all chains diff --git a/fee_allocator/accounting/core_pools.py b/fee_allocator/accounting/core_pools.py index fa4e3fdd..ebbca03c 100644 --- a/fee_allocator/accounting/core_pools.py +++ b/fee_allocator/accounting/core_pools.py @@ -100,8 +100,6 @@ def __init__(self, data: PoolFeeData, chain: CorePoolChain): self.original_earned_fee_share = Decimal(0) self.earned_fee_share_of_chain_usd = self._earned_fee_share_of_chain_usd() self.total_to_incentives_usd = self._total_to_incentives_usd() - self.to_aura_incentives_usd = self._to_aura_incentives_usd() - self.to_bal_incentives_usd = self._to_bal_incentives_usd() self.to_dao_usd = self._to_dao_usd() self.to_vebal_usd = self._to_vebal_usd() self.to_partner_usd = self._to_partner_usd() @@ -166,29 +164,6 @@ def _total_to_incentives_usd(self) -> Decimal: to_distribute_to_incentives = core_fees * vote_incentive_pct return self.earned_fee_share_of_chain_usd * to_distribute_to_incentives - def _calculate_incentive_split(self, platform: str) -> Decimal: - # Alliance core pools get 100% to AURA - if self.is_alliance_core_pool: - return self.total_to_incentives_usd if platform == "aura" else Decimal(0) - - if self.voting_pool_override == "split" or self.voting_pool_override is None: - aura_share = self.chain.chains.aura_vebal_share - return self.total_to_incentives_usd * (aura_share if platform == "aura" else (1 - aura_share)) - - if self.voting_pool_override == platform: - return self.total_to_incentives_usd - elif self.voting_pool_override and self.voting_pool_override != platform: - return Decimal(0) - - aura_share = self.chain.chains.aura_vebal_share - return self.total_to_incentives_usd * (aura_share if platform == "aura" else (1 - aura_share)) - - def _to_aura_incentives_usd(self) -> Decimal: - return self._calculate_incentive_split("aura") - - def _to_bal_incentives_usd(self) -> Decimal: - return self._calculate_incentive_split("bal") - def _to_dao_usd(self) -> Decimal: core_fees = self._core_pool_allocation() beets_factor = self.chain.get_beets_factor() diff --git a/fee_allocator/accounting/models.py b/fee_allocator/accounting/models.py index f5f9b280..ecdf4c1d 100644 --- a/fee_allocator/accounting/models.py +++ b/fee_allocator/accounting/models.py @@ -2,7 +2,7 @@ from decimal import Decimal from typing import Dict, NewType, Optional -from bal_tools.ecosystem import HiddenHand +from bal_tools import StakeDAO Pools = Dict[NewType("PoolId", str), NewType("Symbol", str)] @@ -10,42 +10,25 @@ class PoolOverride(BaseModel): - """ - Represents pool-specific overrides for voting pool allocation and bribe platform. - """ - voting_pool_override: Optional[str] = None # "bal", "aura", or "split" - market_override: Optional[str] = None # "stakedao" or "paladin" to override default routing + voting_pool_override: Optional[str] = None + market_override: Optional[str] = None class GlobalFeeConfig(BaseModel): - """ - Represents the global fee configuration for the fee allocation process. - Models the data sourced from the FEE_CONSTANTS_URL endpoint. - """ - - min_aura_incentive: int - min_existing_aura_incentive: int - min_vote_incentive_amount: int + min_aura_incentive: int = 0 - # Core pool fee splits vebal_share_pct: Decimal dao_share_pct: Decimal vote_incentive_pct: Decimal - # Non-core pool fee splits noncore_vebal_share_pct: Decimal noncore_dao_share_pct: Decimal - # Beets fee split (https://forum.balancer.fi/t/bip-800-deploy-balancer-v3-on-op-mainnet) beets_share_pct: Decimal - # Default bribe platforms (can be overridden per-pool via market_override) - bal_bribe_platform: str = "hh" - aura_bribe_platform: str = "hh" - @model_validator(mode="after") def set_dynamic_min_aura_incentive(self): - self.min_aura_incentive = int(HiddenHand().get_min_aura_incentive()) + self.min_aura_incentive = StakeDAO().calculate_dynamic_min_incentive() return self diff --git a/fee_allocator/bribe_platforms/__init__.py b/fee_allocator/bribe_platforms/__init__.py index bdb0f48e..bcb9f252 100644 --- a/fee_allocator/bribe_platforms/__init__.py +++ b/fee_allocator/bribe_platforms/__init__.py @@ -1,13 +1,7 @@ from .base import BribePlatform -from .factory import get_platform -from .hiddenhand import HiddenHandPlatform -from .paladin import PaladinPlatform from .stakedao import StakeDAOPlatform __all__ = [ "BribePlatform", - "get_platform", - "HiddenHandPlatform", - "PaladinPlatform", "StakeDAOPlatform", ] diff --git a/fee_allocator/bribe_platforms/base.py b/fee_allocator/bribe_platforms/base.py index 045c6c45..c005766b 100644 --- a/fee_allocator/bribe_platforms/base.py +++ b/fee_allocator/bribe_platforms/base.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Optional, Tuple, Any, List +from typing import Dict, Optional, Tuple, Any import pandas as pd @@ -16,24 +16,12 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No Process bribes for this platform Args: - bribes_df: DataFrame with columns [target, platform, amount, bribe_platform] + bribes_df: DataFrame with columns [target, amount, is_alliance] builder: SafeTxBuilder instance for building transactions usdc: SafeContract instance for USDC token """ pass - def get_total_approval_amount(self, bribes_df: pd.DataFrame) -> int: - """ - Calculate total USDC amount that needs approval for this platform - - Args: - bribes_df: DataFrame with bribe information - - Returns: - Total amount in USDC wei that needs approval - """ - return 0 - @abstractmethod def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]: """ @@ -50,26 +38,5 @@ def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optiona @property @abstractmethod def platform_name(self) -> str: - """Return platform identifier for CSV/reporting""" + """Return platform identifier for reporting""" pass - - @property - @abstractmethod - def supported_markets(self) -> List[str]: - """Return list of supported markets (e.g., ['aura', 'balancer'])""" - pass - - @abstractmethod - def get_platform_for_market(self, market: str, voting_pool_override: Optional[str]) -> str: - """ - Get the platform name to use for a specific market. - This handles platform-specific routing logic. - - Args: - market: The market ('aura' or 'balancer') - voting_pool_override: The voting pool override setting - - Returns: - Platform name to use for this market (e.g., 'hh', 'paladin', 'stakedao') - """ - pass \ No newline at end of file diff --git a/fee_allocator/bribe_platforms/factory.py b/fee_allocator/bribe_platforms/factory.py deleted file mode 100644 index 9dd555e6..00000000 --- a/fee_allocator/bribe_platforms/factory.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Dict, Any -from .base import BribePlatform -from .hiddenhand import HiddenHandPlatform -from .paladin import PaladinPlatform -from .stakedao import StakeDAOPlatform - - -def get_platform(platform_name: str, book: Dict[str, str], run_config: Any) -> BribePlatform: - """ - Get platform instance based on platform name. - - Args: - platform_name: 'stakedao', 'paladin', or 'hh' - book: Address book dictionary - run_config: Run configuration object - - Returns: - BribePlatform instance - """ - if platform_name == "stakedao": - return StakeDAOPlatform(book, run_config) - elif platform_name == "paladin": - return PaladinPlatform(book, run_config) - elif platform_name == "hh": - return HiddenHandPlatform(book, run_config) - raise ValueError(f"Unknown platform: {platform_name}") diff --git a/fee_allocator/bribe_platforms/hiddenhand.py b/fee_allocator/bribe_platforms/hiddenhand.py deleted file mode 100644 index 26e3af20..00000000 --- a/fee_allocator/bribe_platforms/hiddenhand.py +++ /dev/null @@ -1,94 +0,0 @@ -from typing import Dict, Optional, Tuple, Any, List -import pandas as pd -from web3 import Web3 -from .base import BribePlatform -from bal_tools.safe_tx_builder import SafeContract -from fee_allocator.utils import get_hh_aura_target -from pathlib import Path - - -class HiddenHandPlatform(BribePlatform): - """HiddenHand bribe platform implementation""" - - def __init__(self, book: Dict[str, str], run_config: Any): - super().__init__(book, run_config) - self.bal_briber = book["hidden_hand2/balancer_briber"] - self.aura_briber = book["hidden_hand2/aura_briber"] - self.bribe_vault = book["hidden_hand2/bribe_vault"] - - def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> None: - """Process HiddenHand bribes for both Balancer and Aura markets""" - - if bribes_df.empty or bribes_df["amount"].sum() == 0: - return - - base_dir = Path(__file__).parent.parent - - bal_bribe_market = SafeContract( - self.bal_briber, - abi_file_path=f"{base_dir}/abi/bribe_market.json" - ) - aura_bribe_market = SafeContract( - self.aura_briber, - abi_file_path=f"{base_dir}/abi/bribe_market.json" - ) - - total_usdc = self.get_total_approval_amount(bribes_df) - if total_usdc > 0: - usdc.approve(self.bribe_vault, total_usdc + 1) - - for _, row in bribes_df.iterrows(): - if int(row["amount"]) == 0: - continue - - prop_hash = self._get_prop_hash(row["platform"], row["target"]) - mantissa = round(row["amount"] * 1e6) - - if row["platform"] == "balancer": - bal_bribe_market.depositBribe( - prop_hash, - self.book["tokens/USDC"], - mantissa, - 0, - 2 - ) - elif row["platform"] == "aura": - aura_bribe_market.depositBribe( - prop_hash, - self.book["tokens/USDC"], - mantissa, - 0, - 1 - ) - - def get_total_approval_amount(self, bribes_df: pd.DataFrame) -> int: - """Calculate total USDC that needs approval""" - if bribes_df.empty: - return 0 - return int(bribes_df["amount"].sum() * 1e6) - - def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]: - """HiddenHand doesn't have specific gauge requirements""" - return True, None - - @property - def platform_name(self) -> str: - """Platform identifier for reporting""" - return "hh" - - @property - def supported_markets(self) -> List[str]: - return ["aura", "balancer"] - - def get_platform_for_market(self, market: str, voting_pool_override: Optional[str]) -> str: - return "hh" - - @staticmethod - def _get_prop_hash(platform: str, target: str) -> str: - """Generate proposal hash for HiddenHand""" - if platform == "balancer": - prop = Web3.solidity_keccak(["address"], [Web3.to_checksum_address(target)]) - return f"0x{prop.hex().replace('0x', '')}" - if platform == "aura": - return get_hh_aura_target(target) - raise ValueError(f"platform {platform} not supported") \ No newline at end of file diff --git a/fee_allocator/bribe_platforms/paladin.py b/fee_allocator/bribe_platforms/paladin.py deleted file mode 100644 index f3b5d27a..00000000 --- a/fee_allocator/bribe_platforms/paladin.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Dict, Optional, Tuple, Any, List -import pandas as pd -from web3 import Web3 -from .base import BribePlatform -from bal_tools.safe_tx_builder import SafeContract -import json -from pathlib import Path -from fee_allocator.logger import logger - - -class PaladinPlatform(BribePlatform): - """Paladin Quest platform implementation""" - - def __init__(self, book: Dict[str, str], run_config: Any): - super().__init__(book, run_config) - self.bal_quest_board = book["paladin/QuestBoardV2_1"] - self.aura_quest_board = book["paladin/QuestBoardV2_1Aura"] - self.usdc_address = book["tokens/USDC"] - - def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> None: - """Process Paladin Quest bribes for both Balancer and Aura markets""" - - valid_bribes = bribes_df[bribes_df["amount"] > 0] - if valid_bribes.empty: - return - - base_dir = Path(__file__).parent.parent - - with open(f"{base_dir}/abi/paladin_quest_board.json", "r") as f: - paladin_abi = json.load(f) - - quest_boards = {} - platform_fee_ratios = {} - - for platform in ["balancer", "aura"]: - bribes = valid_bribes[valid_bribes["platform"] == platform] - if bribes.empty: - continue - - quest_board_addr = self.bal_quest_board if platform == "balancer" else self.aura_quest_board - quest_boards[platform] = SafeContract(quest_board_addr, abi=paladin_abi) - - w3_contract = self.run_config.mainnet.web3.eth.contract( - address=quest_board_addr, - abi=paladin_abi - ) - try: - platform_fee_ratios[platform] = w3_contract.functions.platformFeeRatio().call() - except Exception: - platform_fee_ratios[platform] = 400 - - total = sum(round(row["amount"] * 1e6) for _, row in bribes.iterrows()) - if total > 0: - usdc.approve(quest_board_addr, total) - - for _, row in valid_bribes.iterrows(): - mantissa = round(row["amount"] * 1e6) - platform = row["platform"] - quest_board = quest_boards[platform] - fee_ratio = platform_fee_ratios[platform] - - total_reward_amount = int(mantissa * 10000 / (10000 + fee_ratio)) - fee_amount = (total_reward_amount * fee_ratio) // 10000 - - reward_per_period = total_reward_amount // 2 - max_reward_per_vote = max(reward_per_period // 1000, 50) - min_reward_per_vote = 50 - - quest_board.createRangedQuest( - row["target"], # gauge - self.usdc_address, # rewardToken - "true", # startNextPeriod - 2, # duration - min_reward_per_vote, # minRewardPerVote - max_reward_per_vote, # maxRewardPerVote - total_reward_amount, # totalRewardAmount - fee_amount, # feeAmount - 0, # voteType (NORMAL) - 1, # closeType (ROLLOVER) - "[]" # voterList - ) - - - def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]: - base_dir = Path(__file__).parent.parent - with open(f"{base_dir}/abi/gauge.json", "r") as f: - gauge_abi = json.load(f) - - w3 = self.run_config.mainnet.web3 - usdc = Web3.to_checksum_address(self.usdc_address) - gauge = Web3.to_checksum_address(gauge_address) - contract = w3.eth.contract(address=gauge, abi=gauge_abi) - - reward_tokens = [contract.functions.reward_tokens(i).call() for i in range(8)] - if usdc not in reward_tokens: - return False, f"USDC ({usdc}) not found in gauge reward tokens" - - distributor = contract.functions.reward_data(usdc).call()[1] - if distributor.lower() not in [self.bal_quest_board.lower(), self.aura_quest_board.lower()]: - return False, f"Incorrect distributor {distributor}. Expected: {self.bal_quest_board} or {self.aura_quest_board}" - - return True, None - - @property - def platform_name(self) -> str: - return "paladin" - - @property - def supported_markets(self) -> List[str]: - return ["aura", "balancer"] - - def get_platform_for_market(self, market: str, voting_pool_override: Optional[str]) -> str: - return "paladin" - - def check_all_gauge_requirements(self, pools: List[Any]) -> List[Dict]: - """Check all Paladin gauges for requirements and return issues""" - - gauges_with_issues = [] - - for pool in pools: - if pool.market_override != "paladin": - continue - - valid, error_msg = self.validate_gauge_requirements(pool.gauge_address) - if not valid: - action_needed = [] - - if pool.to_bal_incentives_usd > 0: - action_needed.append(f"Balancer distributor ({self.bal_quest_board})") - if pool.to_aura_incentives_usd > 0: - action_needed.append(f"Aura distributor ({self.aura_quest_board})") - - if action_needed: - if "not found in gauge reward tokens" in error_msg: - action_msg = f"Add USDC ({self.usdc_address}) as reward token and set {' and '.join(action_needed)}" - elif "Incorrect distributor" in error_msg: - action_msg = f"Set {' and '.join(action_needed)}" - else: - action_msg = error_msg - - logger.warning(f"Paladin gauge {pool.gauge_address} missing requirements: {action_msg}") - gauges_with_issues.append({ - "gauge": pool.gauge_address, - "pool_id": pool.pool_id, - "chain": pool.chain.name, - "action": action_msg, - "amount": float(pool.total_to_incentives_usd) - }) - - return gauges_with_issues \ No newline at end of file diff --git a/fee_allocator/bribe_platforms/stakedao.py b/fee_allocator/bribe_platforms/stakedao.py index 41f9888b..d076020e 100644 --- a/fee_allocator/bribe_platforms/stakedao.py +++ b/fee_allocator/bribe_platforms/stakedao.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Tuple, Any, List +from typing import Dict, Optional, Tuple, Any import pandas as pd from web3 import Web3 from .base import BribePlatform @@ -12,9 +12,10 @@ import os -class StakeDAOPlatform(BribePlatform): - """StakeDAO VoteMarket v2 platform implementation for Balancer bribes""" +AURA_VEBAL_LOCKER = Web3.to_checksum_address("0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2") + +class StakeDAOPlatform(BribePlatform): SUPPORTED_L2_CHAINS = ["arbitrum", "optimism", "base", "polygon"] def __init__(self, book: Dict[str, str], run_config: Any): @@ -32,17 +33,12 @@ def __init__(self, book: Dict[str, str], run_config: Any): self.w3 = Web3Rpc("mainnet", os.environ.get("DRPC_KEY")) def _build_gauge_to_chain_map(self): - """Build a mapping of gauge addresses to their chains from the run config.""" - if not self.run_config or not hasattr(self.run_config, 'all_chains'): - return - for chain in self.run_config.all_chains: for pool in chain.core_pools: if pool.gauge_address: self._gauge_to_chain_cache[pool.gauge_address.lower()] = chain.name def _get_chain_selector(self, chain_id: int) -> int: - """Get CCIP chain selector for a given chain ID.""" base_dir = Path(__file__).parent.parent with open(f"{base_dir}/abi/laposte_adapter.json", 'r') as f: adapter_abi = json.load(f) @@ -60,7 +56,6 @@ def _get_chain_selector(self, chain_id: int) -> int: raise def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple) -> int: - """Calculate CCIP fee for cross-chain message.""" destination_selector = self._get_chain_selector(destination_chain_id) base_dir = Path(__file__).parent.parent @@ -81,23 +76,23 @@ def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple) ['(uint256,address,address,(address,uint256)[],bytes)'], [( destination_chain_id, - self.campaign_remote_manager_address, # to - self.campaign_remote_manager_address, # sender - [(self.usdc_address, campaign_params[6])], # token transfer + self.campaign_remote_manager_address, + self.campaign_remote_manager_address, + [(self.usdc_address, campaign_params[6])], payload_data )] ) gas_limit = 200000 - evm_extra_args_tag = bytes.fromhex('97a657c9') # EVMExtraArgsV1 tag + evm_extra_args_tag = bytes.fromhex('97a657c9') extra_args_data = encode(['uint256'], [gas_limit]) evm_extra_args = evm_extra_args_tag + extra_args_data ccip_message = { 'receiver': encode(['address'], [self.laposte_adapter_address]), 'data': laposte_message, - 'tokenAmounts': [], # No tokens via CCIP (handled by LaPoste) - 'feeToken': '0x0000000000000000000000000000000000000000', # Native ETH + 'tokenAmounts': [], + 'feeToken': '0x0000000000000000000000000000000000000000', 'extraArgs': evm_extra_args } @@ -106,17 +101,14 @@ def _calculate_ccip_fee(self, destination_chain_id: int, campaign_params: tuple) ccip_message ).call() - # 50% buffer for safety fee_with_buffer = int(fee * 1.50) logger.info(f"CCIP fee for chain {destination_chain_id}: {Web3.from_wei(fee_with_buffer, 'ether')} ETH (with 50% buffer)") return fee_with_buffer def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> None: - balancer_bribes = bribes_df[bribes_df["platform"] == "balancer"] - - if balancer_bribes.empty or balancer_bribes["amount"].sum() == 0: - logger.info("No Balancer bribes to process for StakeDAO") + if bribes_df.empty or bribes_df["amount"].sum() == 0: + logger.info("No bribes to process for StakeDAO") return base_dir = Path(__file__).parent.parent @@ -125,12 +117,12 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No abi_file_path=f"{base_dir}/abi/stakedao_marketv2.json" ) - total_usdc = sum(int(row["amount"] * 1e6) for _, row in balancer_bribes.iterrows() if row["amount"] > 0) + total_usdc = sum(int(row["amount"] * 1e6) for _, row in bribes_df.iterrows() if row["amount"] > 0) if total_usdc > 0: usdc.approve(self.campaign_remote_manager_address, total_usdc) logger.info(f"Approved {total_usdc / 1e6} USDC for StakeDAO CampaignRemoteManager") - for _, row in balancer_bribes.iterrows(): + for _, row in bribes_df.iterrows(): if int(row["amount"]) == 0: continue @@ -143,7 +135,6 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No mantissa = round(row["amount"] * 1e6) - # Mainnet gauges route to Arbitrum, L2 gauges stay on same chain if chain_name == "mainnet": destination_chain_name = "arbitrum" destination_chain_id = AddrBook.chain_ids_by_name["arbitrum"] @@ -156,17 +147,25 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No destination_book = AddrBook(destination_chain_name) vote_market_v2_address = destination_book.flatbook["stake_dao/votemarket_v2"] + is_alliance = row["is_alliance"] + voting_override = row.get("voting_pool_override") + + aura_only = is_alliance or voting_override == "aura" + bal_only = voting_override == "bal" + addresses = [AURA_VEBAL_LOCKER] if aura_only or bal_only else [] + is_whitelist = aura_only + campaign_params = ( - chain_id, # chainId (of the gauge) - gauge_address, # gauge - builder.safe_address, # manager - self.usdc_address, # rewardToken - 2, # numberOfPeriods - mantissa, # maxRewardPerVote - mantissa, # totalRewardAmount - [], # whitelist - "0x0000000000000000000000000000000000000000", # hook - False # isWhitelist + chain_id, + gauge_address, + builder.safe_address, + self.usdc_address, + 2, + mantissa, + mantissa, + addresses, + "0x0000000000000000000000000000000000000000", + is_whitelist, ) ccip_fee = self._calculate_ccip_fee(destination_chain_id, campaign_params) @@ -174,14 +173,14 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No campaign_manager.createCampaign( campaign_params, destination_chain_id, - 0, # additionalGasLimit (using default) + 0, vote_market_v2_address, value=ccip_fee ) eth_amount = Web3.from_wei(ccip_fee, 'ether') - logger.info(f"Created StakeDAO v2 bribe for {chain_name} gauge {gauge_address} (campaign on {destination_chain_name}): ${row['amount']:.2f} USDC (includes {eth_amount:.6f} ETH for CCIP)") - + mode_tag = " [AURA only]" if aura_only else " [BAL only]" if bal_only else "" + logger.info(f"Created StakeDAO v2 bribe for {chain_name} gauge {gauge_address} (campaign on {destination_chain_name}): ${row['amount']:.2f} USDC{mode_tag} (includes {eth_amount:.6f} ETH for CCIP)") def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]: """StakeDAO doesn't have specific gauge requirements""" @@ -190,18 +189,3 @@ def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optiona @property def platform_name(self) -> str: return "stakedao" - - @property - def supported_markets(self) -> List[str]: - return ["balancer"] - - def get_platform_for_market(self, market: str, voting_pool_override: Optional[str]) -> str: - if market == "aura": - return "hh" - - if market == "balancer": - if voting_pool_override == "aura": - return "hh" - return "stakedao" - - return "hh" \ No newline at end of file diff --git a/fee_allocator/constants.py b/fee_allocator/constants.py index 894b1489..1058ca26 100644 --- a/fee_allocator/constants.py +++ b/fee_allocator/constants.py @@ -3,6 +3,3 @@ PARTNER_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/partner_fee_share.json" EZKL_POOLS_URL = "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/outputs/ezkl_pools.json" POOL_OVERRIDES_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/pool_incentives_overrides.json" -SNAPSHOT_URL = "https://hub.snapshot.org/graphql?" -HH_API_URL = "https://api.hiddenhand.finance/proposal" -GAUGE_MAPPING_URL = "https://raw.githubusercontent.com/aurafinance/aura-contracts/main/tasks/snapshot/gauge_choices.json" diff --git a/fee_allocator/fee_allocator.py b/fee_allocator/fee_allocator.py index 6fcd59bd..b0247fc1 100644 --- a/fee_allocator/fee_allocator.py +++ b/fee_allocator/fee_allocator.py @@ -15,7 +15,7 @@ from fee_allocator.accounting import PROJECT_ROOT from fee_allocator.logger import logger from fee_allocator.payload_visualizer import save_markdown_report -from fee_allocator.bribe_platforms import get_platform +from fee_allocator.bribe_platforms import StakeDAOPlatform load_dotenv() @@ -70,23 +70,21 @@ def allocate(self, redistribute=True): Non-core pools: 82.5% veBAL, 17.5% DAO """ self.run_config.set_core_pool_chains_data() - self.run_config.set_aura_vebal_share() self.run_config.set_initial_pool_allocation() if redistribute: self.redistribute_fees() def redistribute_fees(self): """ - Redistributes fees among pools based on minimum incentive amounts and chain-specific rules. - - This method performs the following steps: - 1. Identifies pools with total incentives below the minimum threshold ($500) - 2. Redistributes fees from these pools to eligible pools above the threshold - 3. Recalculates incentive amounts for Aura and Balancer based on veBAL share - 4. Calls _handle_aura_min twice (with and without buffer) to enforce AURA minimums - 5. Calls _filter_dusty_bal_incentives to handle dust amounts and final redistribution + Redistributes fees among pools based on minimum incentive amounts. + + Pools with total incentives below the minimum threshold get their incentives + redistributed to eligible pools above the threshold. + """ - min_amount = self.run_config.fee_config.min_vote_incentive_amount + min_amount = self.run_config.fee_config.min_aura_incentive + logger.info(f"Redistribution threshold: ${min_amount}") + for chain in self.run_config.all_chains: pools_to_redistribute = [p for p in chain.core_pools if p.total_to_incentives_usd < min_amount] pools_to_receive = [p for p in chain.core_pools if p.total_to_incentives_usd >= min_amount] @@ -97,8 +95,6 @@ def redistribute_fees(self): pool.to_dao_usd += amount * self.run_config.fee_config.noncore_dao_share_pct pool.to_vebal_usd += amount * self.run_config.fee_config.noncore_vebal_share_pct pool.redirected_incentives_usd -= amount - pool.to_aura_incentives_usd = Decimal(0) - pool.to_bal_incentives_usd = Decimal(0) pool.total_to_incentives_usd = Decimal(0) continue @@ -107,8 +103,6 @@ def redistribute_fees(self): for pool in pools_to_redistribute: pool.redirected_incentives_usd -= pool.total_to_incentives_usd - pool.to_aura_incentives_usd = Decimal(0) - pool.to_bal_incentives_usd = Decimal(0) pool.total_to_incentives_usd = Decimal(0) for pool in pools_to_receive: @@ -117,13 +111,6 @@ def redistribute_fees(self): pool.total_to_incentives_usd += total pool.redirected_incentives_usd += total - pool.to_aura_incentives_usd += total if pool.is_alliance_core_pool else total * self.run_config.aura_vebal_share - pool.to_bal_incentives_usd += Decimal(0) if pool.is_alliance_core_pool else total * (1 - self.run_config.aura_vebal_share) - - self._handle_aura_min(buffer=0.25) - self._handle_aura_min() - self._filter_dusty_bal_incentives() - def generate_artifacts(self, include_bal_transfer: bool = True) -> Dict[str, Path]: """ Generates all fee allocation artifacts (CSVs and payload). @@ -135,14 +122,14 @@ def generate_artifacts(self, include_bal_transfer: bool = True) -> Dict[str, Pat alliance_path = self.generate_alliance_csv() partner_path = self.generate_partner_csv() noncore_path = self.generate_noncore_csv() - + payload_path = self.generate_bribe_payload( - bribe_path, + bribe_path, partner_csv=partner_path, alliance_csv=alliance_path, include_bal_transfer=include_bal_transfer ) - + return { "incentives_csv": incentives_path, "bribe_csv": bribe_path, @@ -152,143 +139,6 @@ def generate_artifacts(self, include_bal_transfer: bool = True) -> Dict[str, Pat "payload": payload_path } - def _handle_aura_min(self, buffer=0): - """ - Ensures all pools meet the minimum AURA incentive threshold. - - 1. Consolidate under-min pools by moving their AURA to BAL - 2. Redistribute that debt to pools above min by moving their BAL to AURA - 3. Rebalance to maintain target Aura/Balancer ratio - """ - min_aura_incentive = Decimal(self.run_config.fee_config.min_aura_incentive * (1 - buffer)) - for chain in self.run_config.all_chains: - while True: - debt_to_aura = Decimal(0) - pools_below_min: List[PoolFee] = [] - - for pool in chain.core_pools: - if pool.to_aura_incentives_usd < min_aura_incentive or pool.voting_pool_override == "bal": - debt_to_aura += pool.to_aura_incentives_usd - pools_below_min.append(pool) - - if not debt_to_aura: - break - - for pool in pools_below_min: - pool.to_bal_incentives_usd += pool.to_aura_incentives_usd - pool.to_aura_incentives_usd = Decimal(0) - - pools_over_min = [ - p for p in chain.core_pools - if p.to_aura_incentives_usd >= min_aura_incentive and p.to_bal_incentives_usd > 0 - ] - - if not pools_over_min: - break - - pools_over_min.sort(key=lambda p: p.to_bal_incentives_usd, reverse=True) - debt_remaining = debt_to_aura - total_available_bal = sum(p.to_bal_incentives_usd for p in pools_over_min) - - if total_available_bal == 0: - break - - transfers_made = False - for pool in pools_over_min: - if debt_remaining <= 0: - break - - pool_share = pool.to_bal_incentives_usd / total_available_bal - amount_to_transfer = min( - debt_remaining * pool_share, - pool.to_bal_incentives_usd, - max(Decimal(0), pool.to_aura_incentives_usd + pool.to_bal_incentives_usd - min_aura_incentive) - ) - - if amount_to_transfer > 0: - pool.to_aura_incentives_usd += amount_to_transfer - pool.to_bal_incentives_usd -= amount_to_transfer - debt_remaining -= amount_to_transfer - transfers_made = True - - if not transfers_made: - break - - self._rebalance_aura_bal_split(chain, min_aura_incentive) - - def _rebalance_aura_bal_split(self, chain, min_aura: Decimal): - pools = [p for p in chain.core_pools if not p.is_alliance_core_pool and not p.partner and p.total_to_incentives_usd > 0] - total = sum(p.to_aura_incentives_usd + p.to_bal_incentives_usd for p in pools) - if not total: - return - - to_move = total * self.run_config.aura_vebal_share - sum(p.to_aura_incentives_usd for p in pools) - - for pool in sorted(pools, key=lambda p: p.to_bal_incentives_usd if to_move > 0 else p.to_aura_incentives_usd, reverse=True): - if to_move > 0 and pool.to_bal_incentives_usd > 0 and pool.to_aura_incentives_usd >= min_aura: - move = min(pool.to_bal_incentives_usd, to_move) - pool.to_aura_incentives_usd += move - pool.to_bal_incentives_usd -= move - to_move -= move - elif to_move < 0: - available = pool.to_aura_incentives_usd - min_aura - if available > 0: - move = min(available, abs(to_move)) - pool.to_bal_incentives_usd += move - pool.to_aura_incentives_usd -= move - to_move += move - - def _filter_dusty_bal_incentives(self): - """ - Handles dust BAL amounts (<$75). Only moves to AURA if it results in meaningful AURA. - If a pool ends up with no meaningful incentives after dust handling, redistribute. - """ - min_aura_incentive = Decimal(self.run_config.fee_config.min_aura_incentive) - dust_threshold = Decimal(75) - - for chain in self.run_config.all_chains: - pools_to_zero = [] - - for pool in chain.core_pools: - if pool.total_to_incentives_usd == 0: - continue - - # If pool has dust BAL, try to move to AURA - if 0 < pool.to_bal_incentives_usd < dust_threshold: - potential_aura = pool.to_aura_incentives_usd + pool.to_bal_incentives_usd - if potential_aura >= min_aura_incentive: - pool.to_aura_incentives_usd = potential_aura - pool.to_bal_incentives_usd = Decimal(0) - - # After dust handling, if pool has no AURA and only dust BAL, it can't provide meaningful incentives - if pool.to_aura_incentives_usd < min_aura_incentive and pool.to_bal_incentives_usd < dust_threshold: - pools_to_zero.append(pool) - - # Redistribute from pools that can't provide meaningful incentives - if pools_to_zero: - pools_to_receive = [p for p in chain.core_pools if p not in pools_to_zero and p.total_to_incentives_usd > 0] - - if pools_to_receive: - total_to_redistribute = sum(p.total_to_incentives_usd for p in pools_to_zero) - total_weight = sum(p.total_earned_fees_usd_twap for p in pools_to_receive) - - # Zero out pools that can't provide meaningful incentives - for pool in pools_to_zero: - pool.redirected_incentives_usd -= pool.total_to_incentives_usd - pool.to_aura_incentives_usd = Decimal(0) - pool.to_bal_incentives_usd = Decimal(0) - pool.total_to_incentives_usd = Decimal(0) - - for pool in pools_to_receive: - weight = pool.total_earned_fees_usd_twap / total_weight - amount = total_to_redistribute * weight - pool.total_to_incentives_usd += amount - pool.redirected_incentives_usd += amount - if pool.to_aura_incentives_usd >= pool.to_bal_incentives_usd: - pool.to_aura_incentives_usd += amount - else: - pool.to_bal_incentives_usd += amount - def generate_bribe_csv( self, output_path: Path = Path("fee_allocator/allocations/output_for_msig") ) -> Path: @@ -302,23 +152,12 @@ def generate_bribe_csv( if not core_pool.gauge_address: logger.warning(f"Pool {core_pool.pool_id} has no gauge address") - bal_platform = core_pool.market_override or self.run_config.fee_config.bal_bribe_platform - aura_platform = core_pool.market_override or self.run_config.fee_config.aura_bribe_platform - output.append( { "target": core_pool.gauge_address, - "platform": "balancer", - "amount": round(core_pool.to_bal_incentives_usd, 4), - "bribe_platform": bal_platform, - }, - ) - output.append( - { - "target": core_pool.gauge_address, - "platform": "aura", - "amount": round(core_pool.to_aura_incentives_usd, 4), - "bribe_platform": aura_platform, + "amount": round(core_pool.total_to_incentives_usd, 4), + "is_alliance": core_pool.is_alliance_core_pool, + "voting_pool_override": core_pool.voting_pool_override, }, ) @@ -370,13 +209,11 @@ def generate_incentives_csv( "fees_to_dao": round(core_pool.to_dao_usd, 4), "fees_to_beets": round(core_pool.to_beets_usd, 4), "total_incentives": round(core_pool.total_to_incentives_usd, 4), - "aura_incentives": round(core_pool.to_aura_incentives_usd, 4), - "bal_incentives": round(core_pool.to_bal_incentives_usd, 4), "redirected_incentives": round( core_pool.redirected_incentives_usd, 4 ), - "reroute_incentives": 0, "last_join_exit": core_pool.last_join_exit_ts, + "is_alliance": core_pool.is_alliance_core_pool, "is_partner": any(pool.pool_id == core_pool.pool_id for pool in chain.alliance_pools) or core_pool.pool_id in chain.partner_pools_map, }, ) @@ -519,7 +356,7 @@ def generate_bribe_payload( include_bal_transfer: bool = True ) -> Path: """builds a safe payload from the bribe csv - + Args: input_csv: Path to the bribe CSV file output_path: Directory to save the payload JSON @@ -534,24 +371,18 @@ def generate_bribe_payload( bal = SafeContract(self.book["tokens/BAL"], abi_file_path=f"{base_dir}/abi/ERC20.json") df = pd.read_csv(input_csv) - - bribe_df = df[df["platform"].isin(["balancer", "aura"])] + + bribe_df = df[df["platform"].isna()] if "platform" in df.columns else df[df["amount"] > 0] + bribe_df = bribe_df[bribe_df["amount"] > 0] payment_df = df[df["platform"] == "payment"].iloc[0] beets_df = df[df["platform"] == "beets"].iloc[0] - platform_groups = {} - if not bribe_df.empty: - for platform_name in bribe_df["bribe_platform"].unique(): - platform_bribes = bribe_df[bribe_df["bribe_platform"] == platform_name] - if not platform_bribes.empty and platform_bribes["amount"].sum() > 0: - platform_groups[platform_name] = platform_bribes - dao_fee_usdc = round(payment_df["amount"] * 1e6) - 1000 # round down 0.1 cent beets_fee_usdc = round(beets_df["amount"] * 1e6) - 1000 # round down 0.1 cent - for platform_name, platform_bribes in platform_groups.items(): - platform = get_platform(platform_name, self.book, self.run_config) - platform.process_bribes(platform_bribes, builder, usdc) + if not bribe_df.empty: + platform = StakeDAOPlatform(self.book, self.run_config) + platform.process_bribes(bribe_df, builder, usdc) usdc.transfer(payment_df["target"], dao_fee_usdc) usdc.transfer(beets_df["target"], beets_fee_usdc) @@ -612,12 +443,10 @@ def recon(self) -> None: Checks: 1. No negative incentive amounts 2. Sum of percentage allocations equals 1 - 3. Aura veBAL share within target range - 4. Small delta between collected and distributed fees + 3. Small delta between collected and distributed fees """ total_fees = self.run_config.total_fees_collected_usd - total_aura = Decimal(0) - total_bal = Decimal(0) + total_incentives = Decimal(0) total_dao = Decimal(0) total_vebal = Decimal(0) total_partner = Decimal(0) @@ -626,15 +455,13 @@ def recon(self) -> None: for chain in self.run_config.all_chains: for pool in chain.core_pools: - assert pool.to_aura_incentives_usd >= 0, f"Negative aura incentives: {pool.to_aura_incentives_usd}" - assert pool.to_bal_incentives_usd >= 0, f"Negative bal incentives: {pool.to_bal_incentives_usd}" + assert pool.total_to_incentives_usd >= 0, f"Negative incentives: {pool.total_to_incentives_usd}" assert pool.to_dao_usd >= 0, f"Negative dao share: {pool.to_dao_usd}" assert pool.to_vebal_usd >= 0, f"Negative vebal share: {pool.to_vebal_usd}" assert pool.to_partner_usd >= 0, f"Negative partner share: {pool.to_partner_usd}" assert pool.to_beets_usd >= 0, f"Negative beets share: {pool.to_beets_usd}" - total_aura += pool.to_aura_incentives_usd - total_bal += pool.to_bal_incentives_usd + total_incentives += pool.total_to_incentives_usd total_dao += pool.to_dao_usd total_vebal += pool.to_vebal_usd total_partner += pool.to_partner_usd @@ -646,58 +473,48 @@ def recon(self) -> None: for noncore_pool in chain.alliance_noncore_fee_data: total_partner += chain.get_alliance_noncore_member_fee(noncore_pool.pool_id) - + for noncore_pool in chain.partner_noncore_fee_data: total_partner += chain.get_partner_noncore_fee(noncore_pool.pool_id) - # Total distributed includes all allocations including partner fees - total_distributed = total_aura + total_bal + total_dao + total_vebal + total_partner + total_beets + total_distributed = total_incentives + total_dao + total_vebal + total_partner + total_beets - # For percentage calculations, we need to check that everything sums to 100% if total_distributed > 0: - total_pct = total_distributed / total_distributed # This should always be 1 + total_pct = total_distributed / total_distributed assert abs(1 - total_pct) < Decimal('0.0001'), f"Percentages don't sum to 1: {total_pct}" - # Only check Aura share against BAL for core pool incentives - core_pool_incentives = total_aura + total_bal - aura_share = total_aura / core_pool_incentives if core_pool_incentives > 0 else Decimal(0) - total_core_fees_collected = Decimal(0) for chain in self.run_config.all_chains: - # Core pools get their share of collected fees based on earned/total_earned ratio if chain.total_fees_earned > 0: core_share = chain.total_earned_fees_usd_twap / chain.total_fees_earned total_core_fees_collected += chain.fees_collected * core_share - + total_noncore_fees_collected = Decimal(0) for chain in self.run_config.all_chains: - # Non-core fees are what's left after core pools if chain.total_fees_earned > 0: noncore_share = (chain.noncore_fees_collected + chain.alliance_noncore_fees_earned + chain.partner_noncore_fees_earned) / chain.total_fees_earned total_noncore_fees_collected += chain.fees_collected * noncore_share - + summary = { "feesCollected": float(round(total_fees, 2)), "totalDistributed": float(round(total_distributed, 2)), "feesNotDistributed": float(round(total_fees - total_distributed, 2)), "coreFees": float(round(total_core_fees_collected, 2)), "noncoreFees": float(round(total_noncore_fees_collected, 2)), - "auraIncentives": float(round(total_aura, 2)), - "balIncentives": float(round(total_bal, 2)), + "totalIncentives": float(round(total_incentives, 2)), "feesToDao": float(round(total_dao, 2)), "feesToVebal": float(round(total_vebal, 2)), "feesToPartners": float(round(total_partner, 2)), "feesToBeets": float(round(total_beets, 2)), - "auravebalShare": float(round(aura_share, 2)), - "auraIncentivesPct": float(round(total_aura / total_distributed, 4)) if total_distributed > 0 else 0, - "balIncentivesPct": float(round(total_bal / total_distributed, 4)) if total_distributed > 0 else 0, + "incentivesPct": float(round(total_incentives / total_distributed, 4)) if total_distributed > 0 else 0, "feesToDaoPct": float(round(total_dao / total_distributed, 4)) if total_distributed > 0 else 0, "feesToVebalPct": float(round(total_vebal / total_distributed, 4)) if total_distributed > 0 else 0, "feesToPartnersPct": float(round(total_partner / total_distributed, 4)) if total_distributed > 0 else 0, "feesToBeetsPct": float(round(total_beets / total_distributed, 4)) if total_distributed > 0 else 0, "createdAt": int(datetime.datetime.now().timestamp()), "periodStart": self.date_range[0], - "periodEnd": self.date_range[1] + "periodEnd": self.date_range[1], + "bribeThreshold": self.run_config.fee_config.min_aura_incentive } recon_file = Path(PROJECT_ROOT) / "fee_allocator/summaries" / f"{self.run_config.protocol_version}_recon.json" @@ -722,17 +539,13 @@ def generate_report(self, payload_path: Path, fee_files: List[Path] = None) -> P date_str = payload_name[3:] else: date_str = payload_name - + if self.run_config.protocol_version: report_name = f"{self.run_config.protocol_version}_{date_str}.md" else: report_name = f"{date_str}.md" - + reports_dir = Path(PROJECT_ROOT) / "fee_allocator" / "reports" report_path = reports_dir / report_name - - gauge_issues_path = Path(PROJECT_ROOT) / f"fee_allocator/allocations/{self.run_config.protocol_version}_paladin_gauge_status_{self.start_date}_{self.end_date}.json" - if not gauge_issues_path.exists(): - gauge_issues_path = None - - return save_markdown_report(payload_path, fee_files, output_path=report_path, gauge_issues_path=gauge_issues_path) \ No newline at end of file + + return save_markdown_report(payload_path, fee_files, output_path=report_path) \ No newline at end of file diff --git a/fee_allocator/payload_visualizer.py b/fee_allocator/payload_visualizer.py index e971cbc5..90dbefdc 100644 --- a/fee_allocator/payload_visualizer.py +++ b/fee_allocator/payload_visualizer.py @@ -16,8 +16,7 @@ class PayloadVisualizer: # Transaction group display order TRANSACTION_PRIORITY_ORDER = [ - "Aura Bribes", - "Balancer Bribes", + "Vote Incentives", "veBAL Transfers", "DAO Transfers", "Beets Transfers", @@ -125,24 +124,8 @@ def group_transactions(self, transactions: List[Dict]) -> Dict[str, List[Dict]]: to_addr = tx.get("to", "").lower() method = tx.get("contractMethod", {}).get("name", "") - if method == "depositBribe": - if to_addr == self.book.get("hidden_hand2/aura_briber", "").lower(): - groups["Aura Bribes"].append(tx) - elif to_addr == self.book.get("hidden_hand2/balancer_briber", "").lower(): - groups["Balancer Bribes"].append(tx) - elif method in ["createRangedQuest", "createFixedQuest"]: - # Paladin Quest Boards - to_addr_lower = to_addr.lower() - if to_addr_lower == "0xfeb352930ca196a80b708cdd5dcb4eca94805dab": # veBAL Quest Board - groups["Balancer Bribes"].append(tx) - elif to_addr_lower == "0xfd9f19a9b91becae3c8dabc36cdd1ea86fc1a222": # vlAURA Quest Board - groups["Aura Bribes"].append(tx) - elif method == "createCampaign": - # StakeDAO VoteMarket v2 - if to_addr.lower() == "0x53ad4cd1f1e52dd02aa9fc4a8250a1b74f351ca2": # CampaignRemoteManager - groups["Balancer Bribes"].append(tx) - elif method == "createBounty": - groups["Balancer Bribes"].append(tx) + if method == "createCampaign": + groups["Vote Incentives"].append(tx) elif method == "transfer": recipient = tx.get("contractInputsValues", {}).get("_to", "").lower() if recipient == self.book.get("maxiKeepers/veBalFeeInjector", "").lower(): @@ -224,7 +207,8 @@ def load_recon_data(self, payload_path: Path) -> Dict[str, Any]: if v2_data and v3_data: return { 'coreFees': v2_data.get('coreFees', 0) + v3_data.get('coreFees', 0), - 'noncoreFees': v2_data.get('noncoreFees', 0) + v3_data.get('noncoreFees', 0) + 'noncoreFees': v2_data.get('noncoreFees', 0) + v3_data.get('noncoreFees', 0), + 'bribeThreshold': v3_data.get('bribeThreshold') or v2_data.get('bribeThreshold') } elif v2_data: return v2_data @@ -239,59 +223,10 @@ def load_recon_data(self, payload_path: Path) -> Dict[str, Any]: return recon_data[-1] if recon_data else {} return {} - def load_gauge_issues(self, gauge_issues_path: Path = None) -> List[Dict]: - """Load gauge issues from paladin_gauge_status JSON if provided. - Can handle a single Path or a list of Paths. - """ - if not gauge_issues_path: - return [] - - # Handle both single path and list of paths - if isinstance(gauge_issues_path, list): - all_issues = [] - for path in gauge_issues_path: - if path and path.exists(): - with open(path) as f: - all_issues.extend(json.load(f)) - return all_issues - elif gauge_issues_path.exists(): - with open(gauge_issues_path) as f: - return json.load(f) - return [] - - def create_gauge_issues_table(self, gauge_issues: List[Dict]) -> Table: - """Create a table for gauge issues""" - table = Table( - title="[bold red]⚠️ Paladin Gauge Configuration Required[/bold red]", - box=box.ROUNDED, - title_style="bold red", - header_style="bold red", - border_style="red" - ) - - table.add_column("Gauge", style="dim") - table.add_column("Pool ID", style="dim") - table.add_column("Chain", style="dim") - table.add_column("Amount", style="bold", justify="right") - table.add_column("Action Required", style="yellow") - - for issue in gauge_issues: - table.add_row( - issue.get("gauge", ""), - issue.get("pool_id", ""), - issue.get("chain", ""), - f"${issue.get('amount', 0):,.2f}", - issue.get("action", "") - ) - - return table - def calculate_totals(self, groups: Dict[str, List[Dict]]) -> Dict[str, Decimal]: """Calculate all totals from grouped transactions""" totals = { "bribes_usdc": Decimal(0), - "aura_bribes_usdc": Decimal(0), - "bal_bribes_usdc": Decimal(0), "dao_usdc": Decimal(0), "vebal_usdc": Decimal(0), "vebal_bal": Decimal(0), @@ -299,52 +234,21 @@ def calculate_totals(self, groups: Dict[str, List[Dict]]) -> Dict[str, Decimal]: "alliance_usdc": Decimal(0), "beets_usdc": Decimal(0), } - + for group_name, txs in groups.items(): for tx in txs: - if "Bribe" in group_name: + if group_name == "Vote Incentives": method = tx.get("contractMethod", {}).get("name", "") - if method in ["createRangedQuest", "createFixedQuest"]: - if tx.get("contractInputsValues", {}).get("rewardToken", "").lower() == self.book.get("tokens/USDC", "").lower(): - # For Paladin, include both totalRewardAmount and feeAmount - total_reward = Decimal(tx["contractInputsValues"]["totalRewardAmount"]) - fee_amount = Decimal(tx["contractInputsValues"].get("feeAmount", "0")) - amount = total_reward + fee_amount - totals["bribes_usdc"] += amount - if group_name == "Aura Bribes": - totals["aura_bribes_usdc"] += amount - elif group_name == "Balancer Bribes": - totals["bal_bribes_usdc"] += amount - elif method == "createBounty": - if tx.get("contractInputsValues", {}).get("rewardToken", "").lower() == self.book.get("tokens/USDC", "").lower(): - amount = Decimal(tx["contractInputsValues"]["totalRewardAmount"]) - totals["bribes_usdc"] += amount - if group_name == "Balancer Bribes": - totals["bal_bribes_usdc"] += amount - elif method == "createCampaign": + if method == "createCampaign": params_str = tx.get("contractInputsValues", {}).get("params", "") - # Handle both JSON string and Python tuple formats if params_str.startswith('['): - # JSON format params = json.loads(params_str) else: - # Python tuple format (legacy) import ast params = ast.literal_eval(params_str) - token = params[3].lower() # reward token + token = params[3].lower() if token == self.book.get("tokens/USDC", "").lower(): - amount = Decimal(params[6]) # totalRewardAmount - totals["bribes_usdc"] += amount - if group_name == "Balancer Bribes": - totals["bal_bribes_usdc"] += amount - else: - if tx.get("contractInputsValues", {}).get("_token", "").lower() == self.book.get("tokens/USDC", "").lower(): - amount = Decimal(tx["contractInputsValues"]["_amount"]) - totals["bribes_usdc"] += amount - if group_name == "Aura Bribes": - totals["aura_bribes_usdc"] += amount - elif group_name == "Balancer Bribes": - totals["bal_bribes_usdc"] += amount + totals["bribes_usdc"] += Decimal(params[6]) elif group_name == "veBAL Transfers": if tx.get("to", "").lower() == self.book.get("tokens/USDC", "").lower(): totals["vebal_usdc"] += Decimal(tx["contractInputsValues"]["_value"]) @@ -367,50 +271,20 @@ def calculate_totals(self, groups: Dict[str, List[Dict]]) -> Dict[str, Decimal]: def extract_transaction_data(self, group_name: str, tx: Dict) -> Dict[str, str]: """Extract formatted data from a transaction based on its type""" data = {} - - if "Bribe" in group_name: - method = tx.get("contractMethod", {}).get("name", "") - if method in ["createRangedQuest", "createFixedQuest"]: - gauge = tx.get("contractInputsValues", {}).get("gauge", "") - data["col1"] = f"{gauge[:10]}..." if len(gauge) > 10 else gauge - # For Paladin, add totalRewardAmount + feeAmount to show full allocated amount - total_reward = int(tx.get("contractInputsValues", {}).get("totalRewardAmount", "0")) - fee_amount = int(tx.get("contractInputsValues", {}).get("feeAmount", "0")) - data["col2"] = self.format_amount(str(total_reward + fee_amount)) - data["col3"] = self.format_address(tx.get("contractInputsValues", {}).get("rewardToken", "")) - data["col4"] = "Paladin" - elif method == "createBounty": - gauge = tx.get("contractInputsValues", {}).get("gauge", "") - data["col1"] = f"{gauge[:10]}..." if len(gauge) > 10 else gauge - amount = tx.get("contractInputsValues", {}).get("totalRewardAmount", "0") - data["col2"] = self.format_amount(amount) - data["col3"] = self.format_address(tx.get("contractInputsValues", {}).get("rewardToken", "")) - data["col4"] = "StakeDAO v1" - elif method == "createCampaign": - params_str = tx.get("contractInputsValues", {}).get("params", "") - # Handle both JSON string and Python tuple formats - if params_str.startswith('['): - # JSON format - params = json.loads(params_str) - else: - # Python tuple format (legacy) - import ast - params = ast.literal_eval(params_str) - # params = (chainId, gauge, manager, token, periods, maxReward, totalReward, whitelist, hook, isWhitelist) - gauge = params[1] # gauge address - data["col1"] = f"{gauge[:10]}..." if len(gauge) > 10 else gauge - token = params[3] # reward token - data["col3"] = self.format_address(token) - amount = str(params[6]) # totalRewardAmount - data["col2"] = self.format_amount(amount) - data["col4"] = "StakeDAO v2" + + if group_name == "Vote Incentives": + params_str = tx.get("contractInputsValues", {}).get("params", "") + if params_str.startswith('['): + params = json.loads(params_str) else: - proposal = tx.get("contractInputsValues", {}).get("_proposal", "") - data["col1"] = f"{proposal[:10]}..." if len(proposal) > 10 else proposal - data["col2"] = self.format_amount(tx.get("contractInputsValues", {}).get("_amount", "0")) - data["col3"] = self.format_address(tx.get("contractInputsValues", {}).get("_token", "")) - data["col4"] = "HiddenHand" - + import ast + params = ast.literal_eval(params_str) + gauge = params[1] + data["col1"] = f"{gauge[:10]}..." if len(gauge) > 10 else gauge + data["col2"] = self.format_amount(str(params[6])) + data["col3"] = self.format_address(params[3]) + data["col4"] = "StakeDAO v2" + elif group_name in self.TRANSFER_GROUPS: data["col1"] = self.format_address(tx.get("contractInputsValues", {}).get("_to", "")) amount = tx.get("contractInputsValues", {}).get("_value", "0") @@ -435,8 +309,8 @@ def extract_transaction_data(self, group_name: str, tx: Dict) -> Dict[str, str]: def get_table_headers(self, group_name: str) -> List[str]: """Get table headers based on transaction group""" - if "Bribe" in group_name: - return ["Gauge/Proposal", "Amount", "Token", "Market"] + if group_name == "Vote Incentives": + return ["Gauge", "Amount", "Token", "Platform"] elif group_name in self.TRANSFER_GROUPS: return ["Recipient", "Amount", "Token"] elif group_name == "Token Approvals": @@ -496,15 +370,10 @@ def generate_markdown_summary(self, payload: Dict, groups: Dict[str, List[Dict]] md.append(f"**Total Transactions:** {total_txs}\n") if totals['bribes_usdc'] > 0: - aura_pct = (totals['aura_bribes_usdc'] / totals['bribes_usdc'] * 100).quantize(Decimal('0.01')) - bal_pct = (totals['bal_bribes_usdc'] / totals['bribes_usdc'] * 100).quantize(Decimal('0.01')) - md.append(f"**Vote Incentives:** ${totals['bribes_usdc']/Decimal(1e6):,.2f}") if 'vote_incentives_pct_of_core' in metrics: md.append(f" ({metrics['vote_incentives_pct_of_core']}% of core pool fees)") md.append("\n") - md.append(f" - Aura: ${totals['aura_bribes_usdc']/Decimal(1e6):,.2f} ({aura_pct}%)\n") - md.append(f" - Balancer: ${totals['bal_bribes_usdc']/Decimal(1e6):,.2f} ({bal_pct}%)\n") md.append(f"**DAO Fees:** ${totals['dao_usdc']/Decimal(1e6):,.2f} ({metrics.get('dao_pct', 0)}% of total)\n") md.append(f"**veBAL Fees:** ${totals['vebal_usdc']/Decimal(1e6):,.2f} ({metrics.get('vebal_pct', 0)}% of total)\n") @@ -544,51 +413,39 @@ def generate_markdown_table(self, group_name: str, transactions: List[Dict]) -> return "\n".join(md) - def export_markdown(self, payload_path: Path, fee_files: List[Path] = None, gauge_issues_path: Path = None) -> str: + def export_markdown(self, payload_path: Path, fee_files: List[Path] = None) -> str: """Export payload visualization as markdown""" - # Load payload payload = self.parse_payload(payload_path) - - # Load fee files if provided + total_fees_collected, fee_details_raw = self._load_fee_files(fee_files) fee_details = [f"- {detail}" for detail in fee_details_raw] - - # Group transactions + groups = self.group_transactions(payload["transactions"]) - + md = [] md.append(f"# Fee Allocator Payload Report") md.append(f"\n**File:** {payload_path.name}") md.append(f"**Created:** {payload['meta'].get('name', 'Unknown')}") - + if fee_details: md.append("\n## Fees Collected") md.extend(fee_details) md.append(f"**Total:** ${total_fees_collected/Decimal(1e6):,.2f}") - + md.append("") recon_data = self.load_recon_data(payload_path) - - # Add summary + md.append(self.generate_markdown_summary(payload, groups, total_fees_collected, recon_data)) - - # Check for gauge issues - gauge_issues = self.load_gauge_issues(gauge_issues_path) - if gauge_issues: - md.append("\n## ⚠️ Gauge Configuration Required\n") - md.append("| Gauge | Pool ID | Chain | Amount | Action Required |") - md.append("|-------|---------|-------|--------|-----------------|") - for issue in gauge_issues: - md.append(f"| {issue.get('gauge', '')} | {issue.get('pool_id', '')} | {issue.get('chain', '')} | ${issue.get('amount', 0):,.2f} | {issue.get('action', '')} |") - - # Add transaction tables in priority order + md.append("\n## Transaction Details") - + + md.append(f"\n**Bribe Threshold:** ${recon_data['bribeThreshold']}") + for group_name in self.TRANSACTION_PRIORITY_ORDER: if group_name in groups and groups[group_name]: md.append(self.generate_markdown_table(group_name, groups[group_name])) - + return "\n".join(md) def create_summary_panel(self, payload: Dict, groups: Dict[str, List[Dict]], total_fees_collected: Decimal = Decimal(0), recon_data: Dict = None) -> Panel: @@ -606,17 +463,12 @@ def create_summary_panel(self, payload: Dict, groups: Dict[str, List[Dict]], tot if totals['bribes_usdc'] > 0: lines.append("[yellow]USDC Allocations:[/yellow]") - + vote_incentives_line = f"• Vote Incentives: [bold]${totals['bribes_usdc']/Decimal(1e6):,.2f}[/bold]" if 'vote_incentives_pct_of_core' in metrics: vote_incentives_line += f" ({metrics['vote_incentives_pct_of_core']}% of core pool fees)" lines.append(vote_incentives_line) - - aura_pct = (totals['aura_bribes_usdc'] / totals['bribes_usdc'] * 100).quantize(Decimal('0.01')) - bal_pct = (totals['bal_bribes_usdc'] / totals['bribes_usdc'] * 100).quantize(Decimal('0.01')) - lines.append(f" → Aura: ${totals['aura_bribes_usdc']/Decimal(1e6):,.2f} ({aura_pct}%)") - lines.append(f" → Balancer: ${totals['bal_bribes_usdc']/Decimal(1e6):,.2f} ({bal_pct}%)") - + lines.append(f"• DAO Fees: [bold]${totals['dao_usdc']/Decimal(1e6):,.2f}[/bold] ({metrics.get('dao_pct', 0)}% of total)") lines.append(f"• veBAL Fees: [bold]${totals['vebal_usdc']/Decimal(1e6):,.2f}[/bold] ({metrics.get('vebal_pct', 0)}% of total)") @@ -675,8 +527,7 @@ def create_transaction_table(self, group_name: str, transactions: List[Dict]) -> """Create a table for a group of transactions""" # Color scheme based on transaction type color_map = { - "Aura Bribes": "cyan", - "Balancer Bribes": "blue", + "Vote Incentives": "cyan", "veBAL Transfers": "magenta", "DAO Transfers": "green", "Beets Transfers": "cyan", @@ -716,170 +567,136 @@ def create_transaction_table(self, group_name: str, transactions: List[Dict]) -> return table - def visualize_payload(self, payload_path: Path, fee_files: List[Path] = None, gauge_issues_path: Path = None): + def visualize_payload(self, payload_path: Path, fee_files: List[Path] = None): """Main visualization function""" self.console.clear() - - # Load payload + payload = self.parse_payload(payload_path) - - # Load fee files if provided total_fees_collected, fee_details = self._load_fee_files(fee_files) - - # Group transactions groups = self.group_transactions(payload["transactions"]) - - # Create header + header_text = f"[bold cyan]Fee Allocator Payload Visualization[/bold cyan]\n" header_text += f"[dim]File: {payload_path.name}[/dim]\n" header_text += f"[dim]Created: {payload['meta'].get('name', 'Unknown')}[/dim]" - + if fee_details: header_text += f"\n\n[bold yellow]Fees Collected:[/bold yellow]" for detail in fee_details: header_text += f"\n• {detail}" header_text += f"\n[bold]Total: ${total_fees_collected/Decimal(1e6):,.2f}[/bold]" - + header = Panel( header_text, box=box.DOUBLE, border_style="bright_cyan", padding=(1, 2) ) - + self.console.print(header) self.console.print() - - # Load recon data + recon_data = self.load_recon_data(payload_path) - - # Create and print summary + summary = self.create_summary_panel(payload, groups, total_fees_collected, recon_data) self.console.print(summary) self.console.print() - - gauge_issues = self.load_gauge_issues(gauge_issues_path) - if gauge_issues: - gauge_table = self.create_gauge_issues_table(gauge_issues) - self.console.print(gauge_table) - self.console.print() - + for group_name in self.TRANSACTION_PRIORITY_ORDER: if group_name in groups and groups[group_name]: table = self.create_transaction_table(group_name, groups[group_name]) self.console.print(table) self.console.print() - -def visualize_payload(payload_path: Path, fee_files: List[Path] = None, gauge_issues_path: Path = None): + +def visualize_payload(payload_path: Path, fee_files: List[Path] = None): """Convenience function to visualize a payload""" visualizer = PayloadVisualizer() - visualizer.visualize_payload(payload_path, fee_files, gauge_issues_path) + visualizer.visualize_payload(payload_path, fee_files) -def visualize_combined_payload(payload_path: Path, v2_fees_file: Path = None, v3_fees_file: Path = None, gauge_issues_path: Path = None): +def visualize_combined_payload(payload_path: Path, v2_fees_file: Path = None, v3_fees_file: Path = None): """Visualize a combined payload with fee comparison""" fee_files = [] if v2_fees_file and v2_fees_file.exists(): fee_files.append(v2_fees_file) if v3_fees_file and v3_fees_file.exists(): fee_files.append(v3_fees_file) - + visualizer = PayloadVisualizer() - visualizer.visualize_payload(payload_path, fee_files, gauge_issues_path) + visualizer.visualize_payload(payload_path, fee_files) -def export_markdown(payload_path: Path, fee_files: List[Path] = None, gauge_issues_path: Path = None) -> str: +def export_markdown(payload_path: Path, fee_files: List[Path] = None) -> str: """Export payload visualization as markdown""" visualizer = PayloadVisualizer() - return visualizer.export_markdown(payload_path, fee_files, gauge_issues_path) + return visualizer.export_markdown(payload_path, fee_files) -def export_combined_markdown(payload_path: Path, v2_fees_file: Path = None, v3_fees_file: Path = None, gauge_issues_path: Path = None) -> str: + +def export_combined_markdown(payload_path: Path, v2_fees_file: Path = None, v3_fees_file: Path = None) -> str: """Export combined payload visualization as markdown""" fee_files = [] if v2_fees_file and v2_fees_file.exists(): fee_files.append(v2_fees_file) if v3_fees_file and v3_fees_file.exists(): fee_files.append(v3_fees_file) - - return export_markdown(payload_path, fee_files, gauge_issues_path) + return export_markdown(payload_path, fee_files) -def save_markdown_report(payload_path: Path, fee_files: List[Path] = None, output_path: Path = None, gauge_issues_path: Path = None) -> Path: - """Generate and save payload report to reports directory - - Args: - payload_path: Path to the payload JSON file - fee_files: Optional list of fee collection JSON files - output_path: Optional output path. If not provided, saves to reports directory - gauge_issues_path: Optional path to gauge issues JSON file - - Returns: - Path to the saved report - """ + +def save_markdown_report(payload_path: Path, fee_files: List[Path] = None, output_path: Path = None) -> Path: + """Generate and save payload report to reports directory""" from fee_allocator.accounting import PROJECT_ROOT - - # Generate markdown content - markdown_content = export_markdown(payload_path, fee_files, gauge_issues_path) - + + markdown_content = export_markdown(payload_path, fee_files) + if output_path is None: - # Extract date from payload filename date_str = payload_path.stem if date_str.startswith(("v2_", "v3_")): date_str = date_str[3:] - - # Save to reports directory + reports_dir = Path(PROJECT_ROOT) / "fee_allocator" / "reports" reports_dir.mkdir(exist_ok=True, parents=True) output_path = reports_dir / f"{date_str}.md" - + output_path.write_text(markdown_content) print(f"\nMarkdown report saved to: {output_path}") return output_path -def save_combined_report(payload_path: Path, v2_fees_file: Path = None, v3_fees_file: Path = None, gauge_issues_path: Path = None) -> Path: - """Generate and save combined payload report to reports directory - - This is a convenience wrapper that handles the v2/v3 fee files specifically. - """ +def save_combined_report(payload_path: Path, v2_fees_file: Path = None, v3_fees_file: Path = None) -> Path: + """Generate and save combined payload report to reports directory""" fee_files = [] if v2_fees_file and v2_fees_file.exists(): fee_files.append(v2_fees_file) if v3_fees_file and v3_fees_file.exists(): fee_files.append(v3_fees_file) - - return save_markdown_report(payload_path, fee_files, gauge_issues_path=gauge_issues_path) + + return save_markdown_report(payload_path, fee_files) if __name__ == "__main__": import argparse - + parser = argparse.ArgumentParser(description="Visualize fee allocator payload") parser.add_argument("payload_file", type=Path, help="Path to payload JSON file") parser.add_argument("--markdown", action="store_true", help="Export as markdown instead of rich output") - parser.add_argument("--output", type=Path, help="Output file for markdown export (defaults to reports directory)") - parser.add_argument("--v2-fees", type=Path, help="Path to v2 fees JSON file") - parser.add_argument("--v3-fees", type=Path, help="Path to v3 fees JSON file") - parser.add_argument("--gauge-issues", type=Path, help="Path to paladin_gauge_status JSON file") - + parser.add_argument("--output", type=Path, help="Output file for markdown export") + parser.add_argument("--v2_fees", type=Path, help="Path to v2 fees JSON file") + parser.add_argument("--v3_fees", type=Path, help="Path to v3 fees JSON file") + args = parser.parse_args() - + if args.markdown: - markdown_content = export_combined_markdown(args.payload_file, args.v2_fees, args.v3_fees, args.gauge_issues) + markdown_content = export_combined_markdown(args.payload_file, args.v2_fees, args.v3_fees) if args.output: output_path = args.output else: - # Default to reports directory with same filename as payload reports_dir = Path(__file__).parent / "reports" reports_dir.mkdir(exist_ok=True) output_path = reports_dir / args.payload_file.with_suffix('.md').name - - if args.output or not output_path.exists(): - output_path.parent.mkdir(exist_ok=True, parents=True) - output_path.write_text(markdown_content) - print(f"Markdown exported to {output_path}") - else: - print(markdown_content) + + output_path.parent.mkdir(exist_ok=True, parents=True) + output_path.write_text(markdown_content) + print(f"Markdown exported to {output_path}") else: - visualize_combined_payload(args.payload_file, args.v2_fees, args.v3_fees, args.gauge_issues) + visualize_combined_payload(args.payload_file, args.v2_fees, args.v3_fees) diff --git a/fee_allocator/utils.py b/fee_allocator/utils.py index 8c6627f8..2dc3e63e 100644 --- a/fee_allocator/utils.py +++ b/fee_allocator/utils.py @@ -1,9 +1,6 @@ from datetime import datetime, timedelta from typing import Tuple, Optional import pytz -import requests -from fee_allocator.constants import HH_API_URL -from web3 import Web3 import os from dotenv import load_dotenv import json @@ -45,16 +42,6 @@ def get_last_thursday_odd_week(): return last_thursday_odd_utc -def get_hh_aura_target(target: str) -> str: - response = requests.get(f"{HH_API_URL}/aura") - options = response.json()["data"] - for option in options: - if Web3.to_checksum_address(option["proposal"]) == target: - return option["proposalHash"] - return False - - - def fetch_collected_fees(start_date: str, end_date: str, fees_file_name: str = None, protocol_version: str = "v2") -> dict: # If fees_file_name is provided, use that directly, else derive it from the start and end date if fees_file_name: diff --git a/main_combined.py b/main_combined.py index 2cf28b42..70c7276f 100644 --- a/main_combined.py +++ b/main_combined.py @@ -81,32 +81,13 @@ def main() -> None: v3_fee_file_name = args.v3_fees_file_name or f"v3_fees_{start_date}_{end_date}.json" v2_fee_file_path = Path(f"fee_allocator/fees_collected/{v2_fee_file_name}") v3_fee_file_path = Path(f"fee_allocator/fees_collected/{v3_fee_file_name}") - - # Collect gauge issues from both v2 and v3 - gauge_issues_paths = [] - v2_gauge_issues = Path(ROOT) / f"fee_allocator/allocations/v2_paladin_gauge_status_{v2_allocator.start_date}_{v2_allocator.end_date}.json" - v3_gauge_issues = Path(ROOT) / f"fee_allocator/allocations/v3_paladin_gauge_status_{v3_allocator.start_date}_{v3_allocator.end_date}.json" - if v2_gauge_issues.exists(): - gauge_issues_paths.append(v2_gauge_issues) - if v3_gauge_issues.exists(): - gauge_issues_paths.append(v3_gauge_issues) - + if not args.no_visualize: print("\n" + "="*80 + "\n") - visualize_combined_payload( - combined_payload_path, - v2_fee_file_path, - v3_fee_file_path, - gauge_issues_paths if gauge_issues_paths else None - ) + visualize_combined_payload(combined_payload_path, v2_fee_file_path, v3_fee_file_path) print("\n" + "="*80 + "\n") - save_combined_report( - combined_payload_path, - v2_fee_file_path, - v3_fee_file_path, - gauge_issues_paths if gauge_issues_paths else None - ) + save_combined_report(combined_payload_path, v2_fee_file_path, v3_fee_file_path) if __name__ == "__main__": diff --git a/tests/test_allocator.py b/tests/test_allocator.py index d3c74080..d1193249 100644 --- a/tests/test_allocator.py +++ b/tests/test_allocator.py @@ -46,11 +46,6 @@ def test_fee_allocator_allocation_process(allocator: FeeAllocator): # Verify core pool chains were initialized assert hasattr(allocator.run_config, '_chains') assert isinstance(allocator.run_config._chains, dict) - - # Verify aura vebal share was set - assert allocator.run_config.aura_vebal_share is not None - assert isinstance(allocator.run_config.aura_vebal_share, Decimal) - assert 0 <= allocator.run_config.aura_vebal_share <= 1 @pytest.fixture def allocated_allocator(allocator: FeeAllocator): @@ -150,31 +145,6 @@ def test_noncore_allocation(allocated_allocator: FeeAllocator): -def test_aura_bal_incentive_split(allocated_allocator: FeeAllocator): - """Test that incentives are split correctly between Aura and BAL""" - total_aura = Decimal(0) - total_bal = Decimal(0) - - for chain in allocated_allocator.run_config.all_chains: - for pool in chain.core_pools: - total_aura += pool.to_aura_incentives_usd - total_bal += pool.to_bal_incentives_usd - - total_incentives = total_aura + total_bal - - if total_incentives > 0: - actual_aura_share = total_aura / total_incentives - actual_bal_share = total_bal / total_incentives - - # Redistribution of incentives may deviate from the target, so a lenient check is used - assert actual_aura_share > Decimal('0.1'), \ - f"Aura share {actual_aura_share:.4f} is too low" - assert actual_bal_share > Decimal('0.1'), \ - f"BAL share {actual_bal_share:.4f} is too low" - assert abs(actual_aura_share + actual_bal_share - 1) < Decimal('0.001'), \ - "Aura and BAL shares don't add up to 100%" - - def test_beets_fee_split(allocator_exact: FeeAllocator): """Test that Beets fee split on Optimism is correctly allocated""" optimism_chain = None