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

Commit 3c3b92f

Browse files
committed
add backfill automation for votemarket data
1 parent fa0c939 commit 3c3b92f

4 files changed

Lines changed: 268 additions & 32 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Backfill Aura/BAL Split
2+
3+
on:
4+
schedule:
5+
- cron: "0 12 * * 2" # Weekly Tuesday 12:00 UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
backfill:
10+
runs-on: ubuntu-latest
11+
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v6
19+
with:
20+
ref: biweekly-runs
21+
22+
- name: Setup Python
23+
uses: actions/setup-python@v6
24+
with:
25+
python-version: "3.10"
26+
27+
- name: Install dependencies
28+
run: pip3 install -r requirements.txt
29+
30+
- name: Run backfill
31+
run: python3 backfill_recon_aura_split.py
32+
33+
- name: Create PR
34+
uses: peter-evans/create-pull-request@v8
35+
with:
36+
commit-message: "task: backfill aura/bal split data in recon"
37+
title: "Backfill Aura/BAL Split in Recon"
38+
body: |
39+
Automated backfill of `auraIncentives`, `balIncentives`, and `auravebalShare` fields
40+
in recon JSON files using VoteMarket analytics data.
41+
42+
Entries with `auraIncentives == 0` and `totalIncentives > 0` were updated.
43+
branch: gha-aura-split-backfill
44+
branch-suffix: timestamp
45+
delete-branch: true
46+
labels: Aura-Split-Backfill

backfill_recon_aura_split.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import argparse
2+
import datetime
3+
import json
4+
from decimal import Decimal
5+
from pathlib import Path
6+
7+
import pandas as pd
8+
9+
from fee_allocator.votemarket_analytics import get_aura_share_per_gauge
10+
from fee_allocator.logger import logger
11+
12+
PROJECT_ROOT = Path(__file__).parent
13+
SUMMARIES_DIR = PROJECT_ROOT / "fee_allocator" / "summaries"
14+
INCENTIVES_DIR = PROJECT_ROOT / "fee_allocator" / "allocations" / "incentives"
15+
BRIBES_DIR = PROJECT_ROOT / "fee_allocator" / "allocations" / "output_for_msig"
16+
17+
18+
def _ts_to_date_str(ts: int) -> str:
19+
return datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc).strftime("%Y-%m-%d")
20+
21+
22+
def _load_gauge_data_from_incentives_csv(csv_path: Path) -> pd.DataFrame:
23+
df = pd.read_csv(csv_path)
24+
if "gauge_address" in df.columns and "voting_pool_override" in df.columns:
25+
return df
26+
return None
27+
28+
29+
def _load_gauge_data_from_bribe_csv(csv_path: Path) -> pd.DataFrame:
30+
if not csv_path.exists():
31+
return None
32+
df = pd.read_csv(csv_path)
33+
if "target" not in df.columns:
34+
return None
35+
bribe_rows = df[df["platform"].isna()] if "platform" in df.columns else df
36+
bribe_rows = bribe_rows[bribe_rows["amount"] > 0].copy()
37+
if bribe_rows.empty:
38+
return None
39+
bribe_rows = bribe_rows.rename(columns={"target": "gauge_address"})
40+
if "voting_pool_override" not in bribe_rows.columns:
41+
bribe_rows["voting_pool_override"] = ""
42+
bribe_rows["voting_pool_override"] = bribe_rows["voting_pool_override"].fillna("")
43+
return bribe_rows
44+
45+
46+
def _compute_aura_split(gauge_data: pd.DataFrame, gauge_aura_shares: dict) -> pd.DataFrame:
47+
aura_list = []
48+
bal_list = []
49+
for _, row in gauge_data.iterrows():
50+
total = Decimal(str(row.get("total_incentives", 0)))
51+
override = str(row.get("voting_pool_override", "")).strip()
52+
gauge = str(row.get("gauge_address", "")).strip().lower()
53+
54+
if override == "aura":
55+
aura_share = Decimal(1)
56+
elif override == "bal":
57+
aura_share = Decimal(0)
58+
elif gauge:
59+
aura_share = gauge_aura_shares.get(gauge, Decimal(0))
60+
else:
61+
aura_share = Decimal(0)
62+
63+
aura_list.append(float(round(total * aura_share, 4)))
64+
bal_list.append(float(round(total - total * aura_share, 4)))
65+
66+
gauge_data = gauge_data.copy()
67+
gauge_data["aura_incentives"] = aura_list
68+
gauge_data["bal_incentives"] = bal_list
69+
return gauge_data
70+
71+
72+
def _get_total_incentives(entry: dict) -> float:
73+
if "totalIncentives" in entry:
74+
return entry["totalIncentives"]
75+
aura = entry.get("auraIncentives", 0) or 0
76+
bal = entry.get("balIncentives", 0) or 0
77+
return aura + bal
78+
79+
80+
def backfill(dry_run: bool = False):
81+
for version in ["v2", "v3"]:
82+
recon_path = SUMMARIES_DIR / f"{version}_recon.json"
83+
if not recon_path.exists():
84+
logger.info(f"No recon file for {version}, skipping")
85+
continue
86+
87+
with open(recon_path) as f:
88+
data = json.load(f)
89+
90+
modified = False
91+
for entry in data:
92+
total_incentives = _get_total_incentives(entry)
93+
aura_incentives = entry.get("auraIncentives", 0) or 0
94+
95+
if aura_incentives != 0 or total_incentives == 0:
96+
continue
97+
98+
period_start = entry["periodStart"]
99+
period_end = entry["periodEnd"]
100+
start_str = _ts_to_date_str(period_start)
101+
end_str = _ts_to_date_str(period_end)
102+
103+
logger.info(f"[{version}] Processing period {start_str} to {end_str}")
104+
105+
gauge_aura_shares = get_aura_share_per_gauge(period_start, period_end)
106+
if not gauge_aura_shares:
107+
logger.info(f"[{version}] No VoteMarket data for {start_str}_{end_str}, skipping")
108+
continue
109+
110+
incentives_csv = INCENTIVES_DIR / f"{version}_incentives_{start_str}_{end_str}.csv"
111+
gauge_data = None
112+
113+
if incentives_csv.exists():
114+
gauge_data = _load_gauge_data_from_incentives_csv(incentives_csv)
115+
116+
if gauge_data is None:
117+
end_date = _ts_to_date_str(period_end)
118+
bribe_csv = BRIBES_DIR / f"{version}_bribes_{end_date}.csv"
119+
gauge_data = _load_gauge_data_from_bribe_csv(bribe_csv)
120+
121+
if gauge_data is None:
122+
logger.warning(f"[{version}] No gauge data found for {start_str}_{end_str}, skipping")
123+
continue
124+
125+
gauge_data = _compute_aura_split(gauge_data, gauge_aura_shares)
126+
127+
total_aura = sum(gauge_data["aura_incentives"])
128+
total_bal = sum(gauge_data["bal_incentives"])
129+
130+
logger.info(f"[{version}] {start_str}_{end_str}: aura={total_aura:.2f} bal={total_bal:.2f}")
131+
132+
if not dry_run:
133+
if incentives_csv.exists():
134+
full_df = pd.read_csv(incentives_csv)
135+
if "gauge_address" in full_df.columns:
136+
updated = _compute_aura_split(full_df, gauge_aura_shares)
137+
updated.to_csv(incentives_csv, index=False)
138+
logger.info(f"[{version}] Updated incentives CSV: {incentives_csv.name}")
139+
140+
entry["auraIncentives"] = round(total_aura, 2)
141+
entry["balIncentives"] = round(total_bal, 2)
142+
combined = total_aura + total_bal
143+
entry["auravebalShare"] = round(total_aura / combined, 2) if combined > 0 else 0
144+
total_distributed = entry.get("totalDistributed", entry.get("incentivesDistributed", 0))
145+
entry["auraIncentivesPct"] = round(total_aura / total_distributed, 4) if total_distributed > 0 else 0.0
146+
entry["balIncentivesPct"] = round(total_bal / total_distributed, 4) if total_distributed > 0 else 0.0
147+
modified = True
148+
149+
if modified and not dry_run:
150+
with open(recon_path, "w") as f:
151+
json.dump(data, f, indent=2)
152+
logger.info(f"[{version}] Wrote updated recon to {recon_path.name}")
153+
154+
155+
if __name__ == "__main__":
156+
parser = argparse.ArgumentParser(description="Backfill aura/bal split into recon JSON")
157+
parser.add_argument("--dry_run", action="store_true", help="Print what would be done without writing")
158+
args = parser.parse_args()
159+
backfill(dry_run=args.dry_run)

fee_allocator/fee_allocator.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -196,39 +196,26 @@ def generate_incentives_csv(
196196
) -> Path:
197197
logger.info("generating incentives csv")
198198

199-
aura_vebal_share = self.run_config.mainnet.subgraph.calculate_aura_vebal_share(
200-
self.run_config.mainnet.web3, self.run_config.mainnet.block_range[1]
201-
)
202-
logger.info(f"Aura veBAL share: {aura_vebal_share:.4f}")
203-
204199
output = []
205200
for chain in self.run_config.all_chains:
206201
for core_pool in chain.core_pools:
207202
total_incentives = core_pool.total_to_incentives_usd
208203

209-
if core_pool.voting_pool_override == "aura":
210-
pool_aura_share = Decimal(1)
211-
elif core_pool.voting_pool_override == "bal":
212-
pool_aura_share = Decimal(0)
213-
else:
214-
pool_aura_share = aura_vebal_share
215-
216-
aura_incentives = round(total_incentives * pool_aura_share, 4)
217-
bal_incentives = round(total_incentives - total_incentives * pool_aura_share, 4)
218-
219204
output.append(
220205
{
221206
"pool_id": core_pool.pool_id,
222207
"chain": chain.name,
223208
"symbol": core_pool.symbol,
209+
"gauge_address": core_pool.gauge_address or "",
210+
"voting_pool_override": core_pool.voting_pool_override or "",
224211
"bpt_price": round(core_pool.bpt_price, 4),
225212
"earned_fees": round(core_pool.total_earned_fees_usd_twap, 4),
226213
"fees_to_vebal": round(core_pool.to_vebal_usd, 4),
227214
"fees_to_dao": round(core_pool.to_dao_usd, 4),
228215
"fees_to_beets": round(core_pool.to_beets_usd, 4),
229216
"total_incentives": round(total_incentives, 4),
230-
"aura_incentives": aura_incentives,
231-
"bal_incentives": bal_incentives,
217+
"aura_incentives": Decimal(0),
218+
"bal_incentives": Decimal(0),
232219
"redirected_incentives": round(
233220
core_pool.redirected_incentives_usd, 4
234221
),
@@ -469,17 +456,12 @@ def recon(self) -> None:
469456
"""
470457
total_fees = self.run_config.total_fees_collected_usd
471458
total_incentives = Decimal(0)
472-
total_aura_incentives = Decimal(0)
473459
total_dao = Decimal(0)
474460
total_vebal = Decimal(0)
475461
total_partner = Decimal(0)
476462
total_distributed = Decimal(0)
477463
total_beets = Decimal(0)
478464

479-
aura_vebal_share = self.run_config.mainnet.subgraph.calculate_aura_vebal_share(
480-
self.run_config.mainnet.web3, self.run_config.mainnet.block_range[1]
481-
)
482-
483465
for chain in self.run_config.all_chains:
484466
for pool in chain.core_pools:
485467
assert pool.total_to_incentives_usd >= 0, f"Negative incentives: {pool.total_to_incentives_usd}"
@@ -494,11 +476,6 @@ def recon(self) -> None:
494476
total_partner += pool.to_partner_usd
495477
total_beets += pool.to_beets_usd
496478

497-
if pool.voting_pool_override == "aura":
498-
total_aura_incentives += pool.total_to_incentives_usd
499-
elif pool.voting_pool_override != "bal":
500-
total_aura_incentives += pool.total_to_incentives_usd * aura_vebal_share
501-
502479
total_dao += chain.noncore_to_dao_usd + chain.alliance_noncore_to_dao_usd + chain.partner_noncore_to_dao_usd
503480
total_vebal += chain.noncore_to_vebal_usd + chain.alliance_noncore_to_vebal_usd + chain.partner_noncore_to_vebal_usd
504481
total_beets += chain.noncore_to_beets_usd + chain.alliance_noncore_to_beets_usd + chain.partner_noncore_to_beets_usd
@@ -543,11 +520,11 @@ def recon(self) -> None:
543520
"feesToVebalPct": float(round(total_vebal / total_distributed, 4)) if total_distributed > 0 else 0,
544521
"feesToPartnersPct": float(round(total_partner / total_distributed, 4)) if total_distributed > 0 else 0,
545522
"feesToBeetsPct": float(round(total_beets / total_distributed, 4)) if total_distributed > 0 else 0,
546-
"auraIncentives": float(round(total_aura_incentives, 2)),
547-
"balIncentives": float(round(total_incentives - total_aura_incentives, 2)),
548-
"auravebalShare": float(round(total_aura_incentives / total_incentives, 2)) if total_incentives > 0 else 0,
549-
"auraIncentivesPct": float(round(total_aura_incentives / total_distributed, 4)) if total_distributed > 0 else 0,
550-
"balIncentivesPct": float(round((total_incentives - total_aura_incentives) / total_distributed, 4)) if total_distributed > 0 else 0,
523+
"auraIncentives": 0.0,
524+
"balIncentives": 0.0,
525+
"auravebalShare": 0,
526+
"auraIncentivesPct": 0.0,
527+
"balIncentivesPct": 0.0,
551528
"createdAt": int(datetime.datetime.now().timestamp()),
552529
"periodStart": self.date_range[0],
553530
"periodEnd": self.date_range[1],
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from decimal import Decimal
2+
from typing import Dict, List, Tuple
3+
import requests
4+
5+
from fee_allocator.logger import logger
6+
7+
BALANCER_METADATA_URL = "https://raw.githubusercontent.com/stake-dao/votemarket-analytics/main/analytics/votemarket-analytics/balancer/rounds-metadata.json"
8+
BALANCER_ROUND_URL = "https://raw.githubusercontent.com/stake-dao/votemarket-analytics/main/analytics/votemarket-analytics/balancer/{round_id}.json"
9+
VLAURA_METADATA_URL = "https://raw.githubusercontent.com/stake-dao/votemarket-analytics/main/analytics/votemarket-analytics/vlaura/balancer/rounds-metadata.json"
10+
VLAURA_ROUND_URL = "https://raw.githubusercontent.com/stake-dao/votemarket-analytics/main/analytics/votemarket-analytics/vlaura/balancer/{round_id}.json"
11+
12+
13+
def _fetch_json(url: str) -> dict:
14+
response = requests.get(url)
15+
response.raise_for_status()
16+
return response.json()
17+
18+
19+
def _find_matching_rounds(metadata: list, period_start: int, period_end: int) -> List[int]:
20+
return [r["id"] for r in metadata if r["endVoting"] > period_start and r["endVoting"] <= period_end]
21+
22+
23+
def _aggregate_deposited_per_gauge(round_url_template: str, round_ids: List[int]) -> Dict[str, float]:
24+
deposited = {}
25+
for rid in round_ids:
26+
data = _fetch_json(round_url_template.format(round_id=rid))
27+
for gauge in data["analytics"]:
28+
addr = gauge["gauge"].lower()
29+
deposited[addr] = deposited.get(addr, 0) + gauge["totalDeposited"]
30+
return deposited
31+
32+
33+
def get_aura_share_per_gauge(period_start: int, period_end: int) -> Dict[str, Decimal]:
34+
bal_metadata = _fetch_json(BALANCER_METADATA_URL)
35+
aura_metadata = _fetch_json(VLAURA_METADATA_URL)
36+
37+
bal_round_ids = _find_matching_rounds(bal_metadata, period_start, period_end)
38+
aura_round_ids = _find_matching_rounds(aura_metadata, period_start, period_end)
39+
40+
logger.info(f"VoteMarket rounds for period {period_start}-{period_end}: bal={bal_round_ids} aura={aura_round_ids}")
41+
42+
bal_deposited = _aggregate_deposited_per_gauge(BALANCER_ROUND_URL, bal_round_ids)
43+
aura_deposited = _aggregate_deposited_per_gauge(VLAURA_ROUND_URL, aura_round_ids)
44+
45+
shares = {}
46+
all_gauges = set(bal_deposited) | set(aura_deposited)
47+
for gauge in all_gauges:
48+
b = bal_deposited.get(gauge, 0)
49+
a = aura_deposited.get(gauge, 0)
50+
total = b + a
51+
shares[gauge] = Decimal(str(a / total)) if total > 0 else Decimal(0)
52+
53+
logger.info(f"VoteMarket per-gauge aura shares: { {g[:14]: float(round(s, 4)) for g, s in shares.items()} }")
54+
return shares

0 commit comments

Comments
 (0)