From add2df7835394606b5ef835431db552ab34bf723 Mon Sep 17 00:00:00 2001 From: jalbrekt85 Date: Sun, 14 Dec 2025 15:25:49 -0600 Subject: [PATCH 1/4] use default bribe platform from config, paladin integration adjusments --- fee_allocator/accounting/chains.py | 4 +- fee_allocator/accounting/core_pools.py | 8 +- fee_allocator/accounting/models.py | 11 ++- fee_allocator/bribe_platforms/__init__.py | 6 +- fee_allocator/bribe_platforms/factory.py | 82 +++++-------------- fee_allocator/bribe_platforms/paladin.py | 97 +++-------------------- fee_allocator/constants.py | 4 +- fee_allocator/fee_allocator.py | 56 +++---------- 8 files changed, 60 insertions(+), 208 deletions(-) diff --git a/fee_allocator/accounting/chains.py b/fee_allocator/accounting/chains.py index 95c6df6d..0818ff44 100644 --- a/fee_allocator/accounting/chains.py +++ b/fee_allocator/accounting/chains.py @@ -249,8 +249,8 @@ def _init_alliance_pools(self) -> None: continue tvl_threshold = thresholds.v2_min_tvl if protocol_version == 2 else thresholds.v3_min_tvl - - if tvl_threshold == 0: + + if tvl_threshold == 0 or pool.auto_include: self.alliance_pools.append(pool) logger.info(f"v{protocol_version} Alliance pool: {pool.pool_id} added as alliance pool") continue diff --git a/fee_allocator/accounting/core_pools.py b/fee_allocator/accounting/core_pools.py index e25825a4..fa4e3fdd 100644 --- a/fee_allocator/accounting/core_pools.py +++ b/fee_allocator/accounting/core_pools.py @@ -96,7 +96,7 @@ def __init__(self, data: PoolFeeData, chain: CorePoolChain): self.voting_pool_override = self._get_voting_pool_override() self.market_override = self._get_market_override() - + 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() @@ -138,10 +138,10 @@ def _get_partner_info(self): def _get_voting_pool_override(self): pool_override = self.chain.chains.pool_overrides.get(self.pool_id) return pool_override.voting_pool_override if pool_override else None - - def _get_market_override(self) -> str: + + def _get_market_override(self): pool_override = self.chain.chains.pool_overrides.get(self.pool_id) - return pool_override.market_override if pool_override else "hh" + return pool_override.market_override if pool_override else None def _earned_fee_share_of_chain_usd(self) -> Decimal: diff --git a/fee_allocator/accounting/models.py b/fee_allocator/accounting/models.py index c7306632..f5f9b280 100644 --- a/fee_allocator/accounting/models.py +++ b/fee_allocator/accounting/models.py @@ -11,10 +11,10 @@ class PoolOverride(BaseModel): """ - Represents pool-specific overrides for voting pool and market platforms. + Represents pool-specific overrides for voting pool allocation and bribe platform. """ - voting_pool_override: Optional[str] = None # "bal" or "aura" - market_override: str = "hh" # "hh" (HiddenHand) or "paladin" (Paladin Quest) + voting_pool_override: Optional[str] = None # "bal", "aura", or "split" + market_override: Optional[str] = None # "stakedao" or "paladin" to override default routing class GlobalFeeConfig(BaseModel): @@ -39,6 +39,10 @@ class GlobalFeeConfig(BaseModel): # 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()) @@ -54,6 +58,7 @@ class AlliancePool(BaseModel): partner: str eligibility_date: str active: bool + auto_include: bool = False class AllianceMember(BaseModel): diff --git a/fee_allocator/bribe_platforms/__init__.py b/fee_allocator/bribe_platforms/__init__.py index 279646fb..bdb0f48e 100644 --- a/fee_allocator/bribe_platforms/__init__.py +++ b/fee_allocator/bribe_platforms/__init__.py @@ -1,13 +1,13 @@ from .base import BribePlatform -from .factory import BribePlatformFactory +from .factory import get_platform from .hiddenhand import HiddenHandPlatform from .paladin import PaladinPlatform from .stakedao import StakeDAOPlatform __all__ = [ "BribePlatform", - "BribePlatformFactory", + "get_platform", "HiddenHandPlatform", "PaladinPlatform", "StakeDAOPlatform", -] \ No newline at end of file +] diff --git a/fee_allocator/bribe_platforms/factory.py b/fee_allocator/bribe_platforms/factory.py index 57692973..9dd555e6 100644 --- a/fee_allocator/bribe_platforms/factory.py +++ b/fee_allocator/bribe_platforms/factory.py @@ -1,68 +1,26 @@ -from typing import Dict, Any, List +from typing import Dict, Any from .base import BribePlatform from .hiddenhand import HiddenHandPlatform from .paladin import PaladinPlatform from .stakedao import StakeDAOPlatform -class BribePlatformFactory: - """Factory for creating bribe platform instances based on configuration""" - - _platforms = { - "hh": HiddenHandPlatform, - "paladin": PaladinPlatform, - "stakedao": StakeDAOPlatform, - } - - @classmethod - def register_platform(cls, config_key: str, platform_class: type): - """ - Register a new platform class - - Args: - config_key: The configuration key (e.g., 'hh', 'paladin', 'stakedao') - platform_class: The platform class that implements BribePlatform - """ - cls._platforms[config_key] = platform_class - - @classmethod - def get_platform(cls, platform_name: str, book: Dict[str, str], run_config: Any) -> BribePlatform: - """ - Get platform instance based on platform name - - Args: - platform_name: Platform name ('hh', 'paladin', 'stakedao') - book: Address book dictionary - run_config: Run configuration object - - Returns: - BribePlatform instance - - Raises: - ValueError: If platform_name is not recognized - """ - if not platform_name or platform_name == "hh": - return HiddenHandPlatform(book, run_config) - - platform_class = cls._platforms.get(platform_name) - if not platform_class: - raise ValueError(f"Unknown platform: {platform_name}. Available: {list(cls._platforms.keys())}") - - return platform_class(book, run_config) - - @classmethod - def get_supported_markets(cls, platform_name: str, book: Dict[str, str], run_config: Any) -> List[str]: - """ - Get list of supported markets for a platform - - Args: - platform_name: Platform name - book: Address book dictionary - run_config: Run configuration object - - Returns: - List of supported market names - """ - platform = cls.get_platform(platform_name, book, run_config) - return platform.supported_markets - +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/paladin.py b/fee_allocator/bribe_platforms/paladin.py index fb909e05..7c479dd0 100644 --- a/fee_allocator/bribe_platforms/paladin.py +++ b/fee_allocator/bribe_platforms/paladin.py @@ -1,11 +1,9 @@ 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): @@ -60,64 +58,30 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No fee_ratio = platform_fee_ratios[platform] total_reward_amount = int(mantissa * 10000 / (10000 + fee_ratio)) - fee_amount = mantissa - total_reward_amount + 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"], self.usdc_address, - True, + "true", 2, - 1, - total_reward_amount, + min_reward_per_vote, + max_reward_per_vote, total_reward_amount, fee_amount, 0, 1, - [] + "[]" ) def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]: - """Validate gauge has USDC as reward token with correct distributor""" - try: - 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) - - try: - usdc_found = usdc in [contract.functions.reward_tokens(i).call() for i in range(8)] - except Exception: - return False, "Gauge has incompatible implementation" - - if not usdc_found: - return False, f"USDC ({usdc}) not found in gauge reward tokens" - - try: - distributor = contract.functions.reward_data(usdc).call()[1] - has_correct_distributor = ( - distributor.lower() == self.bal_quest_board.lower() or - distributor.lower() == self.aura_quest_board.lower() - ) - - if not has_correct_distributor: - valid_distributors = [ - f"Balancer: {self.bal_quest_board}", - f"Aura: {self.aura_quest_board}" - ] - return False, f"Incorrect distributor. Valid: {', '.join(valid_distributors)}" - - except Exception: - return False, "Could not verify distributor" - - return True, None - - except Exception as e: - return False, f"Validation error: {str(e)}" + """No validation needed for ROLLOVER close type""" + return True, None @property def platform_name(self) -> str: @@ -129,41 +93,4 @@ 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 = [] - - # Determine which distributors are 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 + return "paladin" \ No newline at end of file diff --git a/fee_allocator/constants.py b/fee_allocator/constants.py index 894b1489..9009e1d5 100644 --- a/fee_allocator/constants.py +++ b/fee_allocator/constants.py @@ -1,5 +1,5 @@ -FEE_CONSTANTS_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/protocol_fees_constants.json" -ALLIANCE_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/alliance_fee_share.json" +FEE_CONSTANTS_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/726104745a4b4bdfc1f93ce1c44a1754df31d87b/config/protocol_fees_constants.json" +ALLIANCE_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/fe75ad42c2fca6702087165ec0ff77aea379085a/config/alliance_fee_share.json" 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" diff --git a/fee_allocator/fee_allocator.py b/fee_allocator/fee_allocator.py index e17f2a8a..6fcd59bd 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 BribePlatformFactory, PaladinPlatform +from fee_allocator.bribe_platforms import get_platform load_dotenv() @@ -129,9 +129,7 @@ def generate_artifacts(self, include_bal_transfer: bool = True) -> Dict[str, Pat Generates all fee allocation artifacts (CSVs and payload). """ logger.info("generating fee allocation artifacts") - - self._check_paladin_gauge_requirements() - + incentives_path = self.generate_incentives_csv() bribe_path = self.generate_bribe_csv() alliance_path = self.generate_alliance_csv() @@ -304,14 +302,15 @@ def generate_bribe_csv( if not core_pool.gauge_address: logger.warning(f"Pool {core_pool.pool_id} has no gauge address") - platform = BribePlatformFactory.get_platform(core_pool.market_override, self.book, self.run_config) + 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": platform.get_platform_for_market("balancer", core_pool.voting_pool_override), + "bribe_platform": bal_platform, }, ) output.append( @@ -319,7 +318,7 @@ def generate_bribe_csv( "target": core_pool.gauge_address, "platform": "aura", "amount": round(core_pool.to_aura_incentives_usd, 4), - "bribe_platform": platform.get_platform_for_market("aura", core_pool.voting_pool_override), + "bribe_platform": aura_platform, }, ) @@ -551,17 +550,8 @@ def generate_bribe_payload( beets_fee_usdc = round(beets_df["amount"] * 1e6) - 1000 # round down 0.1 cent for platform_name, platform_bribes in platform_groups.items(): - try: - platform = BribePlatformFactory.get_platform( - platform_name, - self.book, - self.run_config - ) - - platform.process_bribes(platform_bribes, builder, usdc) - except NotImplementedError as e: - logger.warning(f"Platform {platform_name} not yet implemented: {e}") - continue + platform = get_platform(platform_name, self.book, self.run_config) + platform.process_bribes(platform_bribes, builder, usdc) usdc.transfer(payment_df["target"], dao_fee_usdc) usdc.transfer(beets_df["target"], beets_fee_usdc) @@ -615,34 +605,6 @@ def generate_bribe_payload( builder.output_payload(output_path) return output_path - - def _check_paladin_gauge_requirements(self): - """Check Paladin gauges for requirements and log issues""" - paladin = PaladinPlatform(self.book, self.run_config) - gauges_with_issues = [] - - for chain in self.run_config.all_chains: - for pool in chain.core_pools: - if pool.market_override != "paladin": - continue - - is_valid, error_msg = paladin.validate_gauge_requirements(pool.gauge_address) - - if not is_valid: - pool.market_override = "hh" - logger.warning(f"Paladin gauge {pool.gauge_address} missing requirements, falling back to HiddenHand: {error_msg}") - gauges_with_issues.append({ - "gauge": pool.gauge_address, - "pool_id": pool.pool_id, - "chain": chain.name, - "action": error_msg, - "amount": float(pool.total_to_incentives_usd) - }) - - if gauges_with_issues: - issues_file = base_dir / "allocations" / f"{self.run_config.protocol_version}_paladin_gauge_status_{self.start_date}_{self.end_date}.json" - with open(issues_file, "w") as f: - json.dump(gauges_with_issues, f, indent=2) def recon(self) -> None: """ @@ -773,4 +735,4 @@ def generate_report(self, payload_path: Path, fee_files: List[Path] = None) -> P 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) + return save_markdown_report(payload_path, fee_files, output_path=report_path, gauge_issues_path=gauge_issues_path) \ No newline at end of file From 4886ceb71e7c1d38d0da8110ea8fee076fab9761 Mon Sep 17 00:00:00 2001 From: jalbrekt85 Date: Sun, 14 Dec 2025 15:41:28 -0600 Subject: [PATCH 2/4] add comments for createRangedQuest params --- fee_allocator/bribe_platforms/paladin.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fee_allocator/bribe_platforms/paladin.py b/fee_allocator/bribe_platforms/paladin.py index 7c479dd0..4b8d87d3 100644 --- a/fee_allocator/bribe_platforms/paladin.py +++ b/fee_allocator/bribe_platforms/paladin.py @@ -65,17 +65,17 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No min_reward_per_vote = 50 quest_board.createRangedQuest( - row["target"], - self.usdc_address, - "true", - 2, - min_reward_per_vote, - max_reward_per_vote, - total_reward_amount, - fee_amount, - 0, - 1, - "[]" + 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 ) From 821170da859ca3a25fda897fc4ef715c29a026c6 Mon Sep 17 00:00:00 2001 From: jalbrekt85 Date: Tue, 16 Dec 2025 12:15:28 -0600 Subject: [PATCH 3/4] revert config urls back to main --- fee_allocator/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fee_allocator/constants.py b/fee_allocator/constants.py index 9009e1d5..894b1489 100644 --- a/fee_allocator/constants.py +++ b/fee_allocator/constants.py @@ -1,5 +1,5 @@ -FEE_CONSTANTS_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/726104745a4b4bdfc1f93ce1c44a1754df31d87b/config/protocol_fees_constants.json" -ALLIANCE_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/fe75ad42c2fca6702087165ec0ff77aea379085a/config/alliance_fee_share.json" +FEE_CONSTANTS_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/protocol_fees_constants.json" +ALLIANCE_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/alliance_fee_share.json" 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" From 68b7e582ecfd5bba83211e6e6cc69164cde0a0d4 Mon Sep 17 00:00:00 2001 From: jalbrekt85 Date: Wed, 17 Dec 2025 11:08:35 -0600 Subject: [PATCH 4/4] add back gauge check functions --- fee_allocator/bribe_platforms/paladin.py | 60 ++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/fee_allocator/bribe_platforms/paladin.py b/fee_allocator/bribe_platforms/paladin.py index 4b8d87d3..f3b5d27a 100644 --- a/fee_allocator/bribe_platforms/paladin.py +++ b/fee_allocator/bribe_platforms/paladin.py @@ -1,9 +1,11 @@ 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): @@ -80,12 +82,27 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]: - """No validation needed for ROLLOVER close type""" + 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: - """Platform identifier for reporting""" return "paladin" @property @@ -93,4 +110,41 @@ 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" \ No newline at end of file + 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