Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions fee_allocator/accounting/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions fee_allocator/accounting/core_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions fee_allocator/accounting/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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())
Expand All @@ -54,6 +58,7 @@ class AlliancePool(BaseModel):
partner: str
eligibility_date: str
active: bool
auto_include: bool = False


class AllianceMember(BaseModel):
Expand Down
6 changes: 3 additions & 3 deletions fee_allocator/bribe_platforms/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
]
82 changes: 20 additions & 62 deletions fee_allocator/bribe_platforms/factory.py
Original file line number Diff line number Diff line change
@@ -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}")
81 changes: 31 additions & 50 deletions fee_allocator/bribe_platforms/paladin.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,68 +60,49 @@ 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,
2,
1,
total_reward_amount,
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
)


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"
base_dir = Path(__file__).parent.parent
with open(f"{base_dir}/abi/gauge.json", "r") as f:
gauge_abi = json.load(f)

if not usdc_found:
return False, f"USDC ({usdc}) not found in gauge reward tokens"
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:
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)}"
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"

except Exception:
return False, "Could not verify distributor"
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

except Exception as e:
return False, f"Validation error: {str(e)}"
return True, None

@property
def platform_name(self) -> str:
"""Platform identifier for reporting"""
return "paladin"

@property
Expand All @@ -133,6 +114,7 @@ def get_platform_for_market(self, market: str, voting_pool_override: Optional[st

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:
Expand All @@ -143,7 +125,6 @@ def check_all_gauge_requirements(self, pools: List[Any]) -> List[Dict]:
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:
Expand Down
56 changes: 9 additions & 47 deletions fee_allocator/fee_allocator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -304,22 +302,23 @@ 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(
{
"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,
},
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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)
Loading