diff --git a/fee_allocator/accounting/chains.py b/fee_allocator/accounting/chains.py index 3430ce75..d0d6a5d3 100644 --- a/fee_allocator/accounting/chains.py +++ b/fee_allocator/accounting/chains.py @@ -135,13 +135,20 @@ def total_to_dao_usd(self) -> Decimal: return sum( [pool.to_dao_usd for chain in self.all_chains for pool in chain.core_pools] ) + + @property + @round(4) + def total_to_beets_usd(self) -> Decimal: + return sum( + [pool.to_beets_usd for chain in self.all_chains for pool in chain.core_pools] + ) @property @round(4) def total_to_incentives_usd(self) -> Decimal: return sum( [ - pool.total_to_incentives_usd + pool.to_dao_usd + pool.to_vebal_usd + pool.total_to_incentives_usd + pool.to_dao_usd + pool.to_vebal_usd + pool.to_beets_usd for chain in self.all_chains for pool in chain.core_pools ] @@ -263,7 +270,6 @@ def _cache_file_path(self) -> Path: return self.chains.cache_dir / filename def _load_core_pools_from_cache(self) -> list[PoolFeeData]: - logger.info(f"loading core pools from cache for {self.name}") cached_data = joblib.load(self._cache_file_path()) self.alliance_pools = cached_data.get('alliance_pools', []) self.alliance_noncore_fee_data = cached_data.get('alliance_noncore_fee_data', []) @@ -465,12 +471,22 @@ def noncore_fees_collected(self) -> Decimal: @property @require_pool_fee_data def noncore_to_dao_usd(self) -> Decimal: - return self.noncore_fees_collected * self.chains.fee_config.noncore_dao_share_pct + beets_share_pct = self.chains.fee_config.beets_share_pct if self.name == "optimism" else 0 + return self.noncore_fees_collected * (1 - beets_share_pct) * self.chains.fee_config.noncore_dao_share_pct @property @require_pool_fee_data def noncore_to_vebal_usd(self) -> Decimal: - return self.noncore_fees_collected * self.chains.fee_config.noncore_vebal_share_pct + beets_share_pct = self.chains.fee_config.beets_share_pct if self.name == "optimism" else 0 + return self.noncore_fees_collected * (1 - beets_share_pct) * self.chains.fee_config.noncore_vebal_share_pct + + @property + @require_pool_fee_data + def noncore_to_beets_usd(self) -> Decimal: + beets_share_pct = self.chains.fee_config.beets_share_pct if self.name == "optimism" else 0 + if beets_share_pct == 0: + return Decimal(0) + return self.noncore_fees_collected * beets_share_pct @property @require_pool_fee_data @@ -483,11 +499,20 @@ def alliance_noncore_fees_collected(self) -> Decimal: @property def alliance_noncore_to_dao_usd(self) -> Decimal: - return self.alliance_noncore_fees_collected * self.chains.alliance_config.alliance_fee_allocations["non_core"].dao_share_pct + beets_share_pct = self.chains.fee_config.beets_share_pct if self.name == "optimism" else 0 + return self.alliance_noncore_fees_collected * self.chains.alliance_config.alliance_fee_allocations["non_core"].dao_share_pct * (1 - beets_share_pct) @property def alliance_noncore_to_vebal_usd(self) -> Decimal: - return self.alliance_noncore_fees_collected * self.chains.alliance_config.alliance_fee_allocations["non_core"].vebal_share_pct + beets_share_pct = self.chains.fee_config.beets_share_pct if self.name == "optimism" else 0 + return self.alliance_noncore_fees_collected * self.chains.alliance_config.alliance_fee_allocations["non_core"].vebal_share_pct * (1 - beets_share_pct) + + @property + def alliance_noncore_to_beets_usd(self) -> Decimal: + beets_share_pct = self.chains.fee_config.beets_share_pct if self.name == "optimism" else 0 + if beets_share_pct == 0: + return Decimal(0) + return self.alliance_noncore_fees_collected * (1 - self.chains.alliance_config.alliance_fee_allocations["non_core"].partner_share_pct) * beets_share_pct @property @round(4) diff --git a/fee_allocator/accounting/core_pools.py b/fee_allocator/accounting/core_pools.py index a1fe4957..9ce8924a 100644 --- a/fee_allocator/accounting/core_pools.py +++ b/fee_allocator/accounting/core_pools.py @@ -96,6 +96,7 @@ def __init__(self, data: PoolFeeData, chain: CorePoolChain): self.to_dao_usd = self._to_dao_usd() self.to_vebal_usd = self._to_vebal_usd() self.to_partner_usd = self._to_partner_usd() + self.to_beets_usd = self._to_beets_usd() self.redirected_incentives_usd = Decimal(0) override_cls = overrides.get(self.pool_id) @@ -155,21 +156,23 @@ def _to_bal_incentives_usd(self) -> Decimal: def _to_dao_usd(self) -> Decimal: core_fees = self._core_pool_allocation() + beets_share_pct = self.chain.chains.fee_config.beets_share_pct if self.chain.name == "optimism" else 0 dao_share_pct = ( self.alliance_fee_config.dao_share_pct if (self.is_alliance_non_core_pool or self.is_alliance_pool) else self.chain.chains.fee_config.dao_share_pct ) - return self.earned_fee_share_of_chain_usd * core_fees * dao_share_pct + return self.earned_fee_share_of_chain_usd * core_fees * dao_share_pct * (1 - beets_share_pct) def _to_vebal_usd(self) -> Decimal: core_fees = self._core_pool_allocation() + beets_share_pct = self.chain.chains.fee_config.beets_share_pct if self.chain.name == "optimism" else 0 vebal_share_pct = ( self.alliance_fee_config.vebal_share_pct if (self.is_alliance_non_core_pool or self.is_alliance_pool) else self.chain.chains.fee_config.vebal_share_pct ) - return self.earned_fee_share_of_chain_usd * core_fees * vebal_share_pct + return self.earned_fee_share_of_chain_usd * core_fees * vebal_share_pct * (1 - beets_share_pct) def _to_partner_usd(self) -> Decimal: core_fees = self._core_pool_allocation() @@ -179,4 +182,10 @@ def _to_partner_usd(self) -> Decimal: * core_fees * self.alliance_fee_config.partner_share_pct ) - return Decimal(0) \ No newline at end of file + return Decimal(0) + + def _to_beets_usd(self) -> Decimal: + beets_share_pct = self.chain.chains.fee_config.beets_share_pct if self.chain.name == "optimism" else 0 + if beets_share_pct == 0: + return Decimal(0) + return (self.to_dao_usd + self.to_vebal_usd) \ No newline at end of file diff --git a/fee_allocator/accounting/models.py b/fee_allocator/accounting/models.py index 1f4b0c17..f419a0c6 100644 --- a/fee_allocator/accounting/models.py +++ b/fee_allocator/accounting/models.py @@ -25,6 +25,9 @@ class GlobalFeeConfig(BaseModel): # 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 class AlliancePool(BaseModel): diff --git a/fee_allocator/fee_allocator.py b/fee_allocator/fee_allocator.py index d7e42386..7b4fe446 100644 --- a/fee_allocator/fee_allocator.py +++ b/fee_allocator/fee_allocator.py @@ -263,6 +263,7 @@ def generate_bribe_csv( ) noncore_total_to_dao_usd = sum(chain.noncore_to_dao_usd + chain.alliance_noncore_to_dao_usd for chain in self.run_config.all_chains) + noncore_total_to_beets_usd = sum(chain.noncore_to_beets_usd + chain.alliance_noncore_to_beets_usd for chain in self.run_config.all_chains) output.append( { "target": "0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f", # DAO msig @@ -270,6 +271,13 @@ def generate_bribe_csv( "amount": self.run_config.total_to_dao_usd + noncore_total_to_dao_usd, } ) + output.append( + { + "target": self.book["multisigs/beets_treasury"], + "platform": "beets", + "amount": self.run_config.total_to_beets_usd + noncore_total_to_beets_usd, + } + ) df = pd.DataFrame(output) datetime_file_header = datetime.datetime.fromtimestamp( @@ -300,6 +308,7 @@ def generate_incentives_csv( "earned_fees": round(core_pool.total_earned_fees_usd_twap, 4), "fees_to_vebal": round(core_pool.to_vebal_usd, 4), "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), @@ -336,6 +345,7 @@ def generate_noncore_csv( output = [] for chain in self.run_config.all_chains: + beets_share_pct = self.run_config.fee_config.beets_share_pct if chain.name == "optimism" else Decimal(0) output.append({ "chain": chain.name, "total_fees_collected": round(chain.fees_collected, 4), @@ -343,10 +353,12 @@ def generate_noncore_csv( "noncore_fees": round(chain.noncore_fees_collected, 4), "noncore_to_dao": round(chain.noncore_to_dao_usd, 4), "noncore_to_vebal": round(chain.noncore_to_vebal_usd, 4), + "noncore_to_beets": round(chain.noncore_to_beets_usd, 4), "dao_share_pct": round(self.run_config.fee_config.noncore_dao_share_pct * 100, 2), - "vebal_share_pct": round(self.run_config.fee_config.noncore_vebal_share_pct * 100, 2) + "vebal_share_pct": round(self.run_config.fee_config.noncore_vebal_share_pct * 100, 2), + "beets_share_pct": round(beets_share_pct * 100, 2) }) - + df = pd.DataFrame(output) start_date = datetime.datetime.fromtimestamp(self.date_range[0]).date() end_date = datetime.datetime.fromtimestamp(self.date_range[1]).date() @@ -428,9 +440,11 @@ def generate_bribe_payload( df = pd.read_csv(input_csv) bribe_df = df[df["platform"].isin(["balancer", "aura"])] payment_df = df[df["platform"] == "payment"].iloc[0] + beets_df = df[df["platform"] == "beets"].iloc[0] total_bribe_usdc = sum(round(row["amount"] * 1e6) for _, row in bribe_df.iterrows()) 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 """bribe txs""" usdc.approve(self.book["hidden_hand2/bribe_vault"], total_bribe_usdc + 1) # 1 wei buffer @@ -448,6 +462,7 @@ def generate_bribe_payload( """transfer txs""" usdc.transfer(payment_df["target"], dao_fee_usdc) + usdc.transfer(beets_df["target"], beets_fee_usdc) partner_fee_usdc_spent = 0 if partner_csv: @@ -512,6 +527,7 @@ def recon(self) -> None: total_vebal = Decimal(0) total_partner = Decimal(0) total_distributed = Decimal(0) + total_beets = Decimal(0) for chain in self.run_config.all_chains: for pool in chain.core_pools: @@ -520,21 +536,24 @@ def recon(self) -> None: 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_dao += pool.to_dao_usd total_vebal += pool.to_vebal_usd total_partner += pool.to_partner_usd + total_beets += pool.to_beets_usd total_dao += chain.noncore_to_dao_usd + chain.alliance_noncore_to_dao_usd total_vebal += chain.noncore_to_vebal_usd + chain.alliance_noncore_to_vebal_usd + total_beets += chain.noncore_to_beets_usd + chain.alliance_noncore_to_beets_usd for noncore_pool in chain.alliance_noncore_fee_data: total_partner += chain.get_alliance_noncore_partner_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_distributed = total_aura + total_bal + total_dao + total_vebal + total_partner + total_beets # For percentage calculations, we need to check that everything sums to 100% if total_distributed > 0: @@ -559,12 +578,14 @@ def recon(self) -> None: "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, "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] diff --git a/fee_allocator/payload_visualizer.py b/fee_allocator/payload_visualizer.py index 0efe7c6c..c8639928 100644 --- a/fee_allocator/payload_visualizer.py +++ b/fee_allocator/payload_visualizer.py @@ -129,6 +129,8 @@ def group_transactions(self, transactions: List[Dict]) -> Dict[str, List[Dict]]: groups["veBAL Transfers"].append(tx) elif recipient == self.book.get("multisigs/dao", "0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f").lower(): groups["DAO Transfers"].append(tx) + elif recipient == self.book.get("multisigs/beets_treasury").lower(): + groups["Beets Transfers"].append(tx) else: # Check if it's a partner transfer groups["Partner Transfers"].append(tx) @@ -147,6 +149,7 @@ def calculate_totals(self, groups: Dict[str, List[Dict]]) -> Dict[str, Decimal]: "vebal_usdc": Decimal(0), "vebal_bal": Decimal(0), "partner_usdc": Decimal(0), + "beets_usdc": Decimal(0), } for group_name, txs in groups.items(): @@ -166,11 +169,13 @@ def calculate_totals(self, groups: Dict[str, List[Dict]]) -> Dict[str, Decimal]: totals["vebal_bal"] += Decimal(tx["contractInputsValues"]["_value"]) elif group_name == "DAO Transfers": totals["dao_usdc"] += Decimal(tx["contractInputsValues"]["_value"]) + elif group_name == "Beets Transfers": + totals["beets_usdc"] += Decimal(tx["contractInputsValues"]["_value"]) elif group_name == "Partner Transfers": if tx.get("to", "").lower() == self.book.get("tokens/USDC", "").lower(): totals["partner_usdc"] += Decimal(tx["contractInputsValues"]["_value"]) - - totals["total_usdc"] = totals["bribes_usdc"] + totals["dao_usdc"] + totals["vebal_usdc"] + totals["partner_usdc"] + + totals["total_usdc"] = totals["bribes_usdc"] + totals["dao_usdc"] + totals["vebal_usdc"] + totals["partner_usdc"] + totals["beets_usdc"] return totals def extract_transaction_data(self, group_name: str, tx: Dict) -> Dict[str, str]: @@ -182,7 +187,7 @@ def extract_transaction_data(self, group_name: str, tx: Dict) -> Dict[str, str]: data["col2"] = self.format_amount(tx.get("contractInputsValues", {}).get("_amount", "0")) data["col3"] = self.format_address(tx.get("contractInputsValues", {}).get("_token", "")) - elif group_name in ["veBAL Transfers", "DAO Transfers", "Partner Transfers"]: + elif group_name in ["veBAL Transfers", "DAO Transfers", "Partner Transfers", "Beets Transfers"]: data["col1"] = self.format_address(tx.get("contractInputsValues", {}).get("_to", "")) amount = tx.get("contractInputsValues", {}).get("_value", "0") token_addr = tx.get("to", "") @@ -208,7 +213,7 @@ 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"] - elif group_name in ["veBAL Transfers", "DAO Transfers", "Partner Transfers"]: + elif group_name in ["veBAL Transfers", "DAO Transfers", "Partner Transfers", "Beets Transfers"]: return ["Recipient", "Amount", "Token"] elif group_name == "Token Approvals": return ["Token", "Spender", "Amount"] @@ -233,6 +238,7 @@ def calculate_allocation_metrics(self, totals: Dict[str, Decimal], total_fees_co metrics['dao_pct'] = (totals['dao_usdc'] / totals['total_usdc'] * 100).quantize(Decimal('0.01')) metrics['vebal_pct'] = (totals['vebal_usdc'] / totals['total_usdc'] * 100).quantize(Decimal('0.01')) metrics['partner_pct'] = (totals['partner_usdc'] / totals['total_usdc'] * 100).quantize(Decimal('0.01')) + metrics['beets_pct'] = (totals['beets_usdc'] / totals['total_usdc'] * 100).quantize(Decimal('0.01')) # Calculate core pool fees core_pool_fees = self.calculate_core_pool_fees(recon_data) @@ -292,11 +298,13 @@ def generate_markdown_summary(self, payload: Dict, groups: Dict[str, List[Dict]] md.append(f"- **DAO Fees:** {self.format_amount(str(totals['dao_usdc']))} ({metrics['dao_pct']}% of distributed)") md.append(f"- **veBAL Fees:** {self.format_amount(str(totals['vebal_usdc']))} ({metrics['vebal_pct']}% of distributed)") md.append(f"- **Partner Fees:** {self.format_amount(str(totals['partner_usdc']))} ({metrics['partner_pct']}% of distributed)") + md.append(f"- **Beets Fees:** {self.format_amount(str(totals['beets_usdc']))} ({metrics['beets_pct']}% of distributed)") else: md.append(f"- **Vote Incentives:** {self.format_amount(str(totals['bribes_usdc']))}") md.append(f"- **DAO Fees:** {self.format_amount(str(totals['dao_usdc']))}") md.append(f"- **veBAL Fees:** {self.format_amount(str(totals['vebal_usdc']))}") md.append(f"- **Partner Fees:** {self.format_amount(str(totals['partner_usdc']))}") + md.append(f"- **Beets Fees:** {self.format_amount(str(totals['beets_usdc']))}") md.append("") if 'core_fees' in metrics and metrics.get('core_fees', 0) > 0: @@ -380,6 +388,7 @@ def export_markdown(self, payload_path: Path, fee_files: List[Path] = None) -> s "Balancer Bribes", "veBAL Transfers", "DAO Transfers", + "Beets Transfers", "Partner Transfers", "Token Approvals", "Other Bribes", @@ -421,11 +430,13 @@ def create_summary_panel(self, payload: Dict, groups: Dict[str, List[Dict]], tot lines.append(f"• DAO Fees: [blue]{self.format_amount(str(totals['dao_usdc']))}[/blue] [dim]({metrics['dao_pct']}% of total)[/dim]") lines.append(f"• veBAL Fees: [magenta]{self.format_amount(str(totals['vebal_usdc']))}[/magenta] [dim]({metrics['vebal_pct']}% of total)[/dim]") + lines.append(f"• Beets Fees: [cyan]{self.format_amount(str(totals['beets_usdc']))}[/cyan] [dim]({metrics['beets_pct']}% of total)[/dim]") lines.append(f"• Partner Fees: [yellow]{self.format_amount(str(totals['partner_usdc']))}[/yellow] [dim]({metrics['partner_pct']}% of total)[/dim]") else: lines.append(f"• Vote Incentives: [green]{self.format_amount(str(totals['bribes_usdc']))}[/green]") lines.append(f"• DAO Fees: [blue]{self.format_amount(str(totals['dao_usdc']))}[/blue]") lines.append(f"• veBAL Fees: [magenta]{self.format_amount(str(totals['vebal_usdc']))}[/magenta]") + lines.append(f"• Beets Fees: [cyan]{self.format_amount(str(totals['beets_usdc']))}[/cyan]") lines.append(f"• Partner Fees: [yellow]{self.format_amount(str(totals['partner_usdc']))}[/yellow]") # Add Allocation Validation section (matching markdown version) @@ -464,6 +475,7 @@ def create_transaction_table(self, group_name: str, transactions: List[Dict]) -> "Balancer Bribes": "blue", "veBAL Transfers": "magenta", "DAO Transfers": "green", + "Beets Transfers": "cyan", "Partner Transfers": "yellow", "Token Approvals": "dim", } @@ -549,6 +561,7 @@ def visualize_payload(self, payload_path: Path, fee_files: List[Path] = None): "Balancer Bribes", "veBAL Transfers", "DAO Transfers", + "Beets Transfers", "Partner Transfers", "Token Approvals", "Other Bribes", diff --git a/requirements.txt b/requirements.txt index f336efcb..efb3cb8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ joblib==1.5.1 html5lib==1.1 -rich==14.0.0 -git+https://github.com/BalancerMaxis/bal_addresses \ No newline at end of file +rich==13.7.0 +git+https://github.com/BalancerMaxis/bal_addresses diff --git a/tests/conftest.py b/tests/conftest.py index 13591bcd..aef2bca9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,7 @@ def chain(run_config, web3): @pytest.fixture def allocator(fee_period): - input_fees = {"mainnet": Decimal("10000000.0")} + input_fees = {"mainnet": Decimal("10000000.0"), "optimism": Decimal("10000.0")} return FeeAllocator( input_fees,