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
46 changes: 46 additions & 0 deletions .github/workflows/backfill_aura_split.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Backfill Aura/BAL Split

on:
schedule:
- cron: "0 8 * * 4" # Weekly Thursday 08:00 UTC
workflow_dispatch:

jobs:
backfill:
runs-on: ubuntu-latest

permissions:
contents: write
pull-requests: write

steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: biweekly-runs

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.10"

- name: Install dependencies
run: pip3 install -r requirements.txt

- name: Run backfill
run: python3 backfill_recon_aura_split.py

- name: Create PR
uses: peter-evans/create-pull-request@v8
with:
commit-message: "task: backfill aura/bal split data in recon"
title: "Backfill Aura/BAL Split in Recon"
body: |
Automated backfill of `auraIncentives`, `balIncentives`, and `auravebalShare` fields
in recon JSON files using VoteMarket analytics data.

Entries with `auraIncentives == 0` and `totalIncentives > 0` were updated.
branch: gha-aura-split-backfill
branch-suffix: timestamp
delete-branch: true
labels: Aura-Split-Backfill
175 changes: 175 additions & 0 deletions backfill_recon_aura_split.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import argparse
import datetime
import json
from decimal import Decimal
from pathlib import Path

import pandas as pd

from fee_allocator.votemarket_analytics import get_aura_share_per_gauge
from fee_allocator.logger import logger

PROJECT_ROOT = Path(__file__).parent
SUMMARIES_DIR = PROJECT_ROOT / "fee_allocator" / "summaries"
INCENTIVES_DIR = PROJECT_ROOT / "fee_allocator" / "allocations" / "incentives"
BRIBES_DIR = PROJECT_ROOT / "fee_allocator" / "allocations" / "output_for_msig"

STAKEDAO_MIGRATION_PERIOD_START = 1767225600


def _ts_to_date_str(ts: int) -> str:
return datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc).strftime("%Y-%m-%d")


def _load_gauge_data_from_incentives_csv(csv_path: Path) -> pd.DataFrame:
df = pd.read_csv(csv_path)
if "gauge_address" in df.columns and "voting_pool_override" in df.columns:
return df
return None


def _load_gauge_data_from_bribe_csv(csv_path: Path) -> pd.DataFrame:
if not csv_path.exists():
return None
df = pd.read_csv(csv_path)
if "target" not in df.columns:
return None
bribe_rows = df[df["platform"].isna()] if "platform" in df.columns else df
bribe_rows = bribe_rows[bribe_rows["amount"] > 0].copy()
if bribe_rows.empty:
return None
bribe_rows = bribe_rows.rename(columns={"target": "gauge_address", "amount": "total_incentives"})
if "voting_pool_override" not in bribe_rows.columns:
bribe_rows["voting_pool_override"] = ""
bribe_rows["voting_pool_override"] = bribe_rows["voting_pool_override"].fillna("")
return bribe_rows


def _compute_aura_split(gauge_data: pd.DataFrame, gauge_aura_shares: dict) -> pd.DataFrame:
aura_list = []
bal_list = []
for _, row in gauge_data.iterrows():
total = Decimal(str(row.get("total_incentives", 0)))
override = str(row.get("voting_pool_override", "")).strip()
gauge = str(row.get("gauge_address", "")).strip().lower()

if override == "aura":
aura_share = Decimal(1)
elif override == "bal":
aura_share = Decimal(0)
elif gauge:
aura_share = gauge_aura_shares.get(gauge, Decimal(0))
else:
aura_share = Decimal(0)

aura_list.append(float(round(total * aura_share, 4)))
bal_list.append(float(round(total - total * aura_share, 4)))

gauge_data = gauge_data.copy()
gauge_data["aura_incentives"] = aura_list
gauge_data["bal_incentives"] = bal_list
return gauge_data


def _get_total_incentives(entry: dict) -> float:
if "totalIncentives" in entry:
return entry["totalIncentives"]
aura = entry.get("auraIncentives", 0) or 0
bal = entry.get("balIncentives", 0) or 0
return aura + bal


def backfill(dry_run: bool = False):
for version in ["v2", "v3"]:
recon_path = SUMMARIES_DIR / f"{version}_recon.json"
if not recon_path.exists():
logger.info(f"No recon file for {version}, skipping")
continue

with open(recon_path) as f:
data = json.load(f)

modified = False
for entry in data:
if entry["periodStart"] < STAKEDAO_MIGRATION_PERIOD_START:
continue

total_incentives = _get_total_incentives(entry)
aura_incentives = entry.get("auraIncentives", 0) or 0
bal_incentives = entry.get("balIncentives", 0) or 0

if (aura_incentives + bal_incentives) != 0:
continue

if total_incentives == 0:
if "auraIncentives" not in entry:
entry["auraIncentives"] = 0.0
entry["balIncentives"] = 0.0
entry["auravebalShare"] = 0
entry["auraIncentivesPct"] = 0.0
entry["balIncentivesPct"] = 0.0
modified = True
continue

period_start = entry["periodStart"]
period_end = entry["periodEnd"]
start_str = _ts_to_date_str(period_start)
end_str = _ts_to_date_str(period_end)

logger.info(f"[{version}] Processing period {start_str} to {end_str}")

gauge_aura_shares = get_aura_share_per_gauge(period_start, period_end)
if not gauge_aura_shares:
logger.info(f"[{version}] No VoteMarket data for {start_str}_{end_str}, skipping")
continue

incentives_csv = INCENTIVES_DIR / f"{version}_incentives_{start_str}_{end_str}.csv"
gauge_data = None

if incentives_csv.exists():
gauge_data = _load_gauge_data_from_incentives_csv(incentives_csv)

if gauge_data is None:
end_date = _ts_to_date_str(period_end)
bribe_csv = BRIBES_DIR / f"{version}_bribes_{end_date}.csv"
gauge_data = _load_gauge_data_from_bribe_csv(bribe_csv)

if gauge_data is None:
logger.warning(f"[{version}] No gauge data found for {start_str}_{end_str}, skipping")
continue

gauge_data = _compute_aura_split(gauge_data, gauge_aura_shares)

total_aura = sum(gauge_data["aura_incentives"])
total_bal = sum(gauge_data["bal_incentives"])

logger.info(f"[{version}] {start_str}_{end_str}: aura={total_aura:.2f} bal={total_bal:.2f}")

if not dry_run:
if incentives_csv.exists():
full_df = pd.read_csv(incentives_csv)
if "gauge_address" in full_df.columns:
updated = _compute_aura_split(full_df, gauge_aura_shares)
updated.to_csv(incentives_csv, index=False)
logger.info(f"[{version}] Updated incentives CSV: {incentives_csv.name}")

entry["auraIncentives"] = round(total_aura, 2)
entry["balIncentives"] = round(total_bal, 2)
combined = total_aura + total_bal
entry["auravebalShare"] = round(total_aura / combined, 2) if combined > 0 else 0
total_distributed = entry.get("totalDistributed", entry.get("incentivesDistributed", 0))
entry["auraIncentivesPct"] = round(total_aura / total_distributed, 4) if total_distributed > 0 else 0.0
entry["balIncentivesPct"] = round(total_bal / total_distributed, 4) if total_distributed > 0 else 0.0
modified = True

if modified and not dry_run:
with open(recon_path, "w") as f:
json.dump(data, f, indent=2)
logger.info(f"[{version}] Wrote updated recon to {recon_path.name}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Backfill aura/bal split into recon JSON")
parser.add_argument("--dry_run", action="store_true", help="Print what would be done without writing")
args = parser.parse_args()
backfill(dry_run=args.dry_run)
6 changes: 5 additions & 1 deletion fee_allocator/accounting/core_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ 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
if pool_override and pool_override.voting_pool_override:
return pool_override.voting_pool_override
if self.is_alliance_core_pool:
return "aura"
return None

def _get_market_override(self):
pool_override = self.chain.chains.pool_overrides.get(self.pool_id)
Expand Down
22 changes: 18 additions & 4 deletions fee_allocator/fee_allocator.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,20 +195,27 @@ def generate_incentives_csv(
self, output_path: Path = Path("fee_allocator/allocations/incentives")
) -> Path:
logger.info("generating incentives csv")

output = []
for chain in self.run_config.all_chains:
for core_pool in chain.core_pools:
total_incentives = core_pool.total_to_incentives_usd

output.append(
{
"pool_id": core_pool.pool_id,
"chain": chain.name,
"symbol": core_pool.symbol,
"gauge_address": core_pool.gauge_address or "",
"voting_pool_override": core_pool.voting_pool_override or "",
"bpt_price": round(core_pool.bpt_price, 4),
"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),
"total_incentives": round(total_incentives, 4),
"aura_incentives": Decimal(0),
"bal_incentives": Decimal(0),
"redirected_incentives": round(
core_pool.redirected_incentives_usd, 4
),
Expand All @@ -219,7 +226,7 @@ def generate_incentives_csv(
)

df = pd.DataFrame(output)

sorted_df = df.sort_values(by=["chain", "earned_fees"], ascending=False)
output_path = (
PROJECT_ROOT / output_path / f"{self.run_config.protocol_version}_incentives_{self.start_date}_{self.end_date}.csv"
Expand Down Expand Up @@ -384,8 +391,10 @@ def generate_bribe_payload(
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)
if dao_fee_usdc > 0:
usdc.transfer(payment_df["target"], dao_fee_usdc)
if beets_fee_usdc > 0:
usdc.transfer(beets_df["target"], beets_fee_usdc)

alliance_fee_usdc_spent = 0
if alliance_csv:
Expand Down Expand Up @@ -511,6 +520,11 @@ def recon(self) -> None:
"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,
"auraIncentives": 0.0,
"balIncentives": 0.0,
"auravebalShare": 0,
"auraIncentivesPct": 0.0,
"balIncentivesPct": 0.0,
"createdAt": int(datetime.datetime.now().timestamp()),
"periodStart": self.date_range[0],
"periodEnd": self.date_range[1],
Expand Down
28 changes: 24 additions & 4 deletions fee_allocator/summaries/v2_recon.json
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,12 @@
"createdAt": 1768484870,
"periodStart": 1767225600,
"periodEnd": 1768435200,
"bribeThreshold": 490
"bribeThreshold": 490,
"auraIncentives": 0.0,
"balIncentives": 0.0,
"auravebalShare": 0,
"auraIncentivesPct": 0.0,
"balIncentivesPct": 0.0
},
{
"feesCollected": 17576.73,
Expand All @@ -450,7 +455,12 @@
"createdAt": 1769701777,
"periodStart": 1768435200,
"periodEnd": 1769644800,
"bribeThreshold": 490
"bribeThreshold": 490,
"auraIncentives": 0.0,
"balIncentives": 0.0,
"auravebalShare": 0,
"auraIncentivesPct": 0.0,
"balIncentivesPct": 0.0
},
{
"feesCollected": 31650.06,
Expand All @@ -471,7 +481,12 @@
"createdAt": 1770995647,
"periodStart": 1769644800,
"periodEnd": 1770854400,
"bribeThreshold": 390
"bribeThreshold": 390,
"auraIncentives": 0.0,
"balIncentives": 410.32,
"auravebalShare": 0.0,
"auraIncentivesPct": 0.0,
"balIncentivesPct": 0.013
},
{
"feesCollected": 18497.17,
Expand All @@ -492,6 +507,11 @@
"createdAt": 1772132372,
"periodStart": 1770854400,
"periodEnd": 1772064000,
"bribeThreshold": 410
"bribeThreshold": 410,
"auraIncentives": 0.0,
"balIncentives": 0.0,
"auravebalShare": 0,
"auraIncentivesPct": 0.0,
"balIncentivesPct": 0.0
}
]
Loading