Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.

Commit 1b3aeff

Browse files
authored
Merge pull request #262 from BalancerMaxis/paladin-integration-fixes
use default bribe platform from config, paladin integration adjustments
2 parents 7a6dce4 + 68b7e58 commit 1b3aeff

7 files changed

Lines changed: 77 additions & 171 deletions

File tree

fee_allocator/accounting/chains.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ def _init_alliance_pools(self) -> None:
249249
continue
250250

251251
tvl_threshold = thresholds.v2_min_tvl if protocol_version == 2 else thresholds.v3_min_tvl
252-
253-
if tvl_threshold == 0:
252+
253+
if tvl_threshold == 0 or pool.auto_include:
254254
self.alliance_pools.append(pool)
255255
logger.info(f"v{protocol_version} Alliance pool: {pool.pool_id} added as alliance pool")
256256
continue

fee_allocator/accounting/core_pools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __init__(self, data: PoolFeeData, chain: CorePoolChain):
9696

9797
self.voting_pool_override = self._get_voting_pool_override()
9898
self.market_override = self._get_market_override()
99-
99+
100100
self.original_earned_fee_share = Decimal(0)
101101
self.earned_fee_share_of_chain_usd = self._earned_fee_share_of_chain_usd()
102102
self.total_to_incentives_usd = self._total_to_incentives_usd()
@@ -138,10 +138,10 @@ def _get_partner_info(self):
138138
def _get_voting_pool_override(self):
139139
pool_override = self.chain.chains.pool_overrides.get(self.pool_id)
140140
return pool_override.voting_pool_override if pool_override else None
141-
142-
def _get_market_override(self) -> str:
141+
142+
def _get_market_override(self):
143143
pool_override = self.chain.chains.pool_overrides.get(self.pool_id)
144-
return pool_override.market_override if pool_override else "hh"
144+
return pool_override.market_override if pool_override else None
145145

146146

147147
def _earned_fee_share_of_chain_usd(self) -> Decimal:

fee_allocator/accounting/models.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111

1212
class PoolOverride(BaseModel):
1313
"""
14-
Represents pool-specific overrides for voting pool and market platforms.
14+
Represents pool-specific overrides for voting pool allocation and bribe platform.
1515
"""
16-
voting_pool_override: Optional[str] = None # "bal" or "aura"
17-
market_override: str = "hh" # "hh" (HiddenHand) or "paladin" (Paladin Quest)
16+
voting_pool_override: Optional[str] = None # "bal", "aura", or "split"
17+
market_override: Optional[str] = None # "stakedao" or "paladin" to override default routing
1818

1919

2020
class GlobalFeeConfig(BaseModel):
@@ -39,6 +39,10 @@ class GlobalFeeConfig(BaseModel):
3939
# Beets fee split (https://forum.balancer.fi/t/bip-800-deploy-balancer-v3-on-op-mainnet)
4040
beets_share_pct: Decimal
4141

42+
# Default bribe platforms (can be overridden per-pool via market_override)
43+
bal_bribe_platform: str = "hh"
44+
aura_bribe_platform: str = "hh"
45+
4246
@model_validator(mode="after")
4347
def set_dynamic_min_aura_incentive(self):
4448
self.min_aura_incentive = int(HiddenHand().get_min_aura_incentive())
@@ -54,6 +58,7 @@ class AlliancePool(BaseModel):
5458
partner: str
5559
eligibility_date: str
5660
active: bool
61+
auto_include: bool = False
5762

5863

5964
class AllianceMember(BaseModel):
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from .base import BribePlatform
2-
from .factory import BribePlatformFactory
2+
from .factory import get_platform
33
from .hiddenhand import HiddenHandPlatform
44
from .paladin import PaladinPlatform
55
from .stakedao import StakeDAOPlatform
66

77
__all__ = [
88
"BribePlatform",
9-
"BribePlatformFactory",
9+
"get_platform",
1010
"HiddenHandPlatform",
1111
"PaladinPlatform",
1212
"StakeDAOPlatform",
13-
]
13+
]
Lines changed: 20 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,26 @@
1-
from typing import Dict, Any, List
1+
from typing import Dict, Any
22
from .base import BribePlatform
33
from .hiddenhand import HiddenHandPlatform
44
from .paladin import PaladinPlatform
55
from .stakedao import StakeDAOPlatform
66

77

8-
class BribePlatformFactory:
9-
"""Factory for creating bribe platform instances based on configuration"""
10-
11-
_platforms = {
12-
"hh": HiddenHandPlatform,
13-
"paladin": PaladinPlatform,
14-
"stakedao": StakeDAOPlatform,
15-
}
16-
17-
@classmethod
18-
def register_platform(cls, config_key: str, platform_class: type):
19-
"""
20-
Register a new platform class
21-
22-
Args:
23-
config_key: The configuration key (e.g., 'hh', 'paladin', 'stakedao')
24-
platform_class: The platform class that implements BribePlatform
25-
"""
26-
cls._platforms[config_key] = platform_class
27-
28-
@classmethod
29-
def get_platform(cls, platform_name: str, book: Dict[str, str], run_config: Any) -> BribePlatform:
30-
"""
31-
Get platform instance based on platform name
32-
33-
Args:
34-
platform_name: Platform name ('hh', 'paladin', 'stakedao')
35-
book: Address book dictionary
36-
run_config: Run configuration object
37-
38-
Returns:
39-
BribePlatform instance
40-
41-
Raises:
42-
ValueError: If platform_name is not recognized
43-
"""
44-
if not platform_name or platform_name == "hh":
45-
return HiddenHandPlatform(book, run_config)
46-
47-
platform_class = cls._platforms.get(platform_name)
48-
if not platform_class:
49-
raise ValueError(f"Unknown platform: {platform_name}. Available: {list(cls._platforms.keys())}")
50-
51-
return platform_class(book, run_config)
52-
53-
@classmethod
54-
def get_supported_markets(cls, platform_name: str, book: Dict[str, str], run_config: Any) -> List[str]:
55-
"""
56-
Get list of supported markets for a platform
57-
58-
Args:
59-
platform_name: Platform name
60-
book: Address book dictionary
61-
run_config: Run configuration object
62-
63-
Returns:
64-
List of supported market names
65-
"""
66-
platform = cls.get_platform(platform_name, book, run_config)
67-
return platform.supported_markets
68-
8+
def get_platform(platform_name: str, book: Dict[str, str], run_config: Any) -> BribePlatform:
9+
"""
10+
Get platform instance based on platform name.
11+
12+
Args:
13+
platform_name: 'stakedao', 'paladin', or 'hh'
14+
book: Address book dictionary
15+
run_config: Run configuration object
16+
17+
Returns:
18+
BribePlatform instance
19+
"""
20+
if platform_name == "stakedao":
21+
return StakeDAOPlatform(book, run_config)
22+
elif platform_name == "paladin":
23+
return PaladinPlatform(book, run_config)
24+
elif platform_name == "hh":
25+
return HiddenHandPlatform(book, run_config)
26+
raise ValueError(f"Unknown platform: {platform_name}")

fee_allocator/bribe_platforms/paladin.py

Lines changed: 31 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -60,68 +60,49 @@ def process_bribes(self, bribes_df: pd.DataFrame, builder: Any, usdc: Any) -> No
6060
fee_ratio = platform_fee_ratios[platform]
6161

6262
total_reward_amount = int(mantissa * 10000 / (10000 + fee_ratio))
63-
fee_amount = mantissa - total_reward_amount
63+
fee_amount = (total_reward_amount * fee_ratio) // 10000
64+
65+
reward_per_period = total_reward_amount // 2
66+
max_reward_per_vote = max(reward_per_period // 1000, 50)
67+
min_reward_per_vote = 50
6468

6569
quest_board.createRangedQuest(
66-
row["target"],
67-
self.usdc_address,
68-
True,
69-
2,
70-
1,
71-
total_reward_amount,
72-
total_reward_amount,
73-
fee_amount,
74-
0,
75-
1,
76-
[]
70+
row["target"], # gauge
71+
self.usdc_address, # rewardToken
72+
"true", # startNextPeriod
73+
2, # duration
74+
min_reward_per_vote, # minRewardPerVote
75+
max_reward_per_vote, # maxRewardPerVote
76+
total_reward_amount, # totalRewardAmount
77+
fee_amount, # feeAmount
78+
0, # voteType (NORMAL)
79+
1, # closeType (ROLLOVER)
80+
"[]" # voterList
7781
)
7882

7983

8084
def validate_gauge_requirements(self, gauge_address: str) -> Tuple[bool, Optional[str]]:
81-
"""Validate gauge has USDC as reward token with correct distributor"""
82-
try:
83-
base_dir = Path(__file__).parent.parent
84-
with open(f"{base_dir}/abi/gauge.json", "r") as f:
85-
gauge_abi = json.load(f)
86-
87-
w3 = self.run_config.mainnet.web3
88-
usdc = Web3.to_checksum_address(self.usdc_address)
89-
gauge = Web3.to_checksum_address(gauge_address)
90-
contract = w3.eth.contract(address=gauge, abi=gauge_abi)
91-
92-
try:
93-
usdc_found = usdc in [contract.functions.reward_tokens(i).call() for i in range(8)]
94-
except Exception:
95-
return False, "Gauge has incompatible implementation"
85+
base_dir = Path(__file__).parent.parent
86+
with open(f"{base_dir}/abi/gauge.json", "r") as f:
87+
gauge_abi = json.load(f)
9688

97-
if not usdc_found:
98-
return False, f"USDC ({usdc}) not found in gauge reward tokens"
89+
w3 = self.run_config.mainnet.web3
90+
usdc = Web3.to_checksum_address(self.usdc_address)
91+
gauge = Web3.to_checksum_address(gauge_address)
92+
contract = w3.eth.contract(address=gauge, abi=gauge_abi)
9993

100-
try:
101-
distributor = contract.functions.reward_data(usdc).call()[1]
102-
has_correct_distributor = (
103-
distributor.lower() == self.bal_quest_board.lower() or
104-
distributor.lower() == self.aura_quest_board.lower()
105-
)
106-
107-
if not has_correct_distributor:
108-
valid_distributors = [
109-
f"Balancer: {self.bal_quest_board}",
110-
f"Aura: {self.aura_quest_board}"
111-
]
112-
return False, f"Incorrect distributor. Valid: {', '.join(valid_distributors)}"
94+
reward_tokens = [contract.functions.reward_tokens(i).call() for i in range(8)]
95+
if usdc not in reward_tokens:
96+
return False, f"USDC ({usdc}) not found in gauge reward tokens"
11397

114-
except Exception:
115-
return False, "Could not verify distributor"
98+
distributor = contract.functions.reward_data(usdc).call()[1]
99+
if distributor.lower() not in [self.bal_quest_board.lower(), self.aura_quest_board.lower()]:
100+
return False, f"Incorrect distributor {distributor}. Expected: {self.bal_quest_board} or {self.aura_quest_board}"
116101

117-
return True, None
118-
119-
except Exception as e:
120-
return False, f"Validation error: {str(e)}"
102+
return True, None
121103

122104
@property
123105
def platform_name(self) -> str:
124-
"""Platform identifier for reporting"""
125106
return "paladin"
126107

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

134115
def check_all_gauge_requirements(self, pools: List[Any]) -> List[Dict]:
135116
"""Check all Paladin gauges for requirements and return issues"""
117+
136118
gauges_with_issues = []
137119

138120
for pool in pools:
@@ -143,7 +125,6 @@ def check_all_gauge_requirements(self, pools: List[Any]) -> List[Dict]:
143125
if not valid:
144126
action_needed = []
145127

146-
# Determine which distributors are needed
147128
if pool.to_bal_incentives_usd > 0:
148129
action_needed.append(f"Balancer distributor ({self.bal_quest_board})")
149130
if pool.to_aura_incentives_usd > 0:

fee_allocator/fee_allocator.py

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from fee_allocator.accounting import PROJECT_ROOT
1616
from fee_allocator.logger import logger
1717
from fee_allocator.payload_visualizer import save_markdown_report
18-
from fee_allocator.bribe_platforms import BribePlatformFactory, PaladinPlatform
18+
from fee_allocator.bribe_platforms import get_platform
1919

2020
load_dotenv()
2121

@@ -129,9 +129,7 @@ def generate_artifacts(self, include_bal_transfer: bool = True) -> Dict[str, Pat
129129
Generates all fee allocation artifacts (CSVs and payload).
130130
"""
131131
logger.info("generating fee allocation artifacts")
132-
133-
self._check_paladin_gauge_requirements()
134-
132+
135133
incentives_path = self.generate_incentives_csv()
136134
bribe_path = self.generate_bribe_csv()
137135
alliance_path = self.generate_alliance_csv()
@@ -304,22 +302,23 @@ def generate_bribe_csv(
304302
if not core_pool.gauge_address:
305303
logger.warning(f"Pool {core_pool.pool_id} has no gauge address")
306304

307-
platform = BribePlatformFactory.get_platform(core_pool.market_override, self.book, self.run_config)
305+
bal_platform = core_pool.market_override or self.run_config.fee_config.bal_bribe_platform
306+
aura_platform = core_pool.market_override or self.run_config.fee_config.aura_bribe_platform
308307

309308
output.append(
310309
{
311310
"target": core_pool.gauge_address,
312311
"platform": "balancer",
313312
"amount": round(core_pool.to_bal_incentives_usd, 4),
314-
"bribe_platform": platform.get_platform_for_market("balancer", core_pool.voting_pool_override),
313+
"bribe_platform": bal_platform,
315314
},
316315
)
317316
output.append(
318317
{
319318
"target": core_pool.gauge_address,
320319
"platform": "aura",
321320
"amount": round(core_pool.to_aura_incentives_usd, 4),
322-
"bribe_platform": platform.get_platform_for_market("aura", core_pool.voting_pool_override),
321+
"bribe_platform": aura_platform,
323322
},
324323
)
325324

@@ -551,17 +550,8 @@ def generate_bribe_payload(
551550
beets_fee_usdc = round(beets_df["amount"] * 1e6) - 1000 # round down 0.1 cent
552551

553552
for platform_name, platform_bribes in platform_groups.items():
554-
try:
555-
platform = BribePlatformFactory.get_platform(
556-
platform_name,
557-
self.book,
558-
self.run_config
559-
)
560-
561-
platform.process_bribes(platform_bribes, builder, usdc)
562-
except NotImplementedError as e:
563-
logger.warning(f"Platform {platform_name} not yet implemented: {e}")
564-
continue
553+
platform = get_platform(platform_name, self.book, self.run_config)
554+
platform.process_bribes(platform_bribes, builder, usdc)
565555

566556
usdc.transfer(payment_df["target"], dao_fee_usdc)
567557
usdc.transfer(beets_df["target"], beets_fee_usdc)
@@ -615,34 +605,6 @@ def generate_bribe_payload(
615605
builder.output_payload(output_path)
616606

617607
return output_path
618-
619-
def _check_paladin_gauge_requirements(self):
620-
"""Check Paladin gauges for requirements and log issues"""
621-
paladin = PaladinPlatform(self.book, self.run_config)
622-
gauges_with_issues = []
623-
624-
for chain in self.run_config.all_chains:
625-
for pool in chain.core_pools:
626-
if pool.market_override != "paladin":
627-
continue
628-
629-
is_valid, error_msg = paladin.validate_gauge_requirements(pool.gauge_address)
630-
631-
if not is_valid:
632-
pool.market_override = "hh"
633-
logger.warning(f"Paladin gauge {pool.gauge_address} missing requirements, falling back to HiddenHand: {error_msg}")
634-
gauges_with_issues.append({
635-
"gauge": pool.gauge_address,
636-
"pool_id": pool.pool_id,
637-
"chain": chain.name,
638-
"action": error_msg,
639-
"amount": float(pool.total_to_incentives_usd)
640-
})
641-
642-
if gauges_with_issues:
643-
issues_file = base_dir / "allocations" / f"{self.run_config.protocol_version}_paladin_gauge_status_{self.start_date}_{self.end_date}.json"
644-
with open(issues_file, "w") as f:
645-
json.dump(gauges_with_issues, f, indent=2)
646608

647609
def recon(self) -> None:
648610
"""
@@ -773,4 +735,4 @@ def generate_report(self, payload_path: Path, fee_files: List[Path] = None) -> P
773735
if not gauge_issues_path.exists():
774736
gauge_issues_path = None
775737

776-
return save_markdown_report(payload_path, fee_files, output_path=report_path, gauge_issues_path=gauge_issues_path)
738+
return save_markdown_report(payload_path, fee_files, output_path=report_path, gauge_issues_path=gauge_issues_path)

0 commit comments

Comments
 (0)