Add analysis notebooks: CV, treasury, and mechanism post-mortem
Notebook 02 (Conviction Voting): - Funding distribution, beneficiary concentration - Voter participation (Gini=0.843, top 10=65% of stake) - Proposal lifecycle and conviction dynamics - Governance throughput timeline Notebook 03 (Treasury & Pools): - Common pool and reserve pool balance tracking - Monthly inflow/outflow analysis - Post-migration USD valuations - Reserve ratio and pool composition Notebook 04 (Mechanism Post-Mortem): - ABC → Common Pool → CV feedback loop sustainability - Death spiral hypothesis (market/treasury/governance correlation) - CV parameter stress test (spending limit, request sizes) - Mechanism design scorecard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
041ee79945
commit
0dc3c41111
|
|
@ -0,0 +1,431 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 02 — Conviction Voting Analysis\n",
|
||||
"\n",
|
||||
"Analyzing the TEC's Disputable Conviction Voting system:\n",
|
||||
"- Proposal outcomes and funding distribution\n",
|
||||
"- Participation dynamics and voter concentration\n",
|
||||
"- Conviction accumulation patterns\n",
|
||||
"- Effective governance throughput"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import matplotlib.dates as mdates\n",
|
||||
"import seaborn as sns\n",
|
||||
"\n",
|
||||
"sns.set_theme(style='whitegrid', palette='deep')\n",
|
||||
"plt.rcParams['figure.figsize'] = (14, 6)\n",
|
||||
"plt.rcParams['figure.dpi'] = 100\n",
|
||||
"\n",
|
||||
"DATA = '../data/onchain'\n",
|
||||
"\n",
|
||||
"proposals = pd.read_csv(f'{DATA}/cv_proposals.csv')\n",
|
||||
"stakes = pd.read_csv(f'{DATA}/cv_stakes.csv')\n",
|
||||
"supports = pd.read_csv(f'{DATA}/cv_support_updates.csv')\n",
|
||||
"\n",
|
||||
"print(f'Proposals: {len(proposals)}')\n",
|
||||
"print(f'Stake events: {len(stakes)}')\n",
|
||||
"print(f'Support updates: {len(supports)}')\n",
|
||||
"print(f'\\nProposal status breakdown:')\n",
|
||||
"print(proposals['status'].value_counts())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Proposal Funding Distribution\n",
|
||||
"\n",
|
||||
"How was the common pool allocated? Who were the biggest recipients?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"funded = proposals[proposals['status'] == 'executed'].copy()\n",
|
||||
"funded = funded.sort_values('amount_requested', ascending=True)\n",
|
||||
"\n",
|
||||
"# Extract short proposal names from link field\n",
|
||||
"funded['name'] = funded['link'].str[:50]\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(1, 2, figsize=(18, 8))\n",
|
||||
"\n",
|
||||
"# Bar chart of funded amounts\n",
|
||||
"ax = axes[0]\n",
|
||||
"bars = ax.barh(range(len(funded)), funded['amount_requested'], color='steelblue')\n",
|
||||
"ax.set_yticks(range(len(funded)))\n",
|
||||
"ax.set_yticklabels(funded['name'], fontsize=7)\n",
|
||||
"ax.set_xlabel('TEC Tokens Requested')\n",
|
||||
"ax.set_title(f'Funded Proposals ({len(funded)} total, {funded[\"amount_requested\"].sum():,.0f} TEC)')\n",
|
||||
"\n",
|
||||
"# Top beneficiary addresses (group by beneficiary)\n",
|
||||
"ax = axes[1]\n",
|
||||
"by_beneficiary = funded.groupby('beneficiary')['amount_requested'].sum().sort_values(ascending=True)\n",
|
||||
"# Show short addresses\n",
|
||||
"labels = [f'{addr[:8]}...{addr[-4:]}' for addr in by_beneficiary.index]\n",
|
||||
"ax.barh(range(len(by_beneficiary)), by_beneficiary.values, color='teal')\n",
|
||||
"ax.set_yticks(range(len(by_beneficiary)))\n",
|
||||
"ax.set_yticklabels(labels, fontsize=8)\n",
|
||||
"ax.set_xlabel('Total TEC Received')\n",
|
||||
"ax.set_title(f'Funding by Beneficiary Address ({len(by_beneficiary)} unique)')\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/cv_funding_distribution.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"print(f'\\nTop 5 beneficiaries by total funding:')\n",
|
||||
"for addr, amt in by_beneficiary.tail(5).items():\n",
|
||||
" props = funded[funded['beneficiary'] == addr]['name'].tolist()\n",
|
||||
" print(f' {addr[:16]}... : {amt:>10,.0f} TEC ({len(props)} proposals)')\n",
|
||||
" for p in props:\n",
|
||||
" print(f' - {p}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Proposal Success/Failure Patterns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Exclude the Abstain proposal (#1) and zero-amount proposals\n",
|
||||
"real_proposals = proposals[(proposals['id'] > 1) & (proposals['amount_requested'] > 0)].copy()\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
|
||||
"\n",
|
||||
"# Status breakdown\n",
|
||||
"ax = axes[0]\n",
|
||||
"status_counts = real_proposals['status'].value_counts()\n",
|
||||
"colors = {'executed': '#2ecc71', 'cancelled': '#e74c3c', 'open': '#3498db'}\n",
|
||||
"ax.pie(status_counts.values, labels=status_counts.index, autopct='%1.0f%%',\n",
|
||||
" colors=[colors.get(s, 'gray') for s in status_counts.index])\n",
|
||||
"ax.set_title(f'Proposal Outcomes (n={len(real_proposals)})')\n",
|
||||
"\n",
|
||||
"# Amount requested: funded vs cancelled\n",
|
||||
"ax = axes[1]\n",
|
||||
"for status, color in [('executed', '#2ecc71'), ('cancelled', '#e74c3c')]:\n",
|
||||
" subset = real_proposals[real_proposals['status'] == status]\n",
|
||||
" ax.hist(subset['amount_requested'], bins=15, alpha=0.6, label=status, color=color)\n",
|
||||
"ax.set_xlabel('Amount Requested (TEC)')\n",
|
||||
"ax.set_ylabel('Count')\n",
|
||||
"ax.set_title('Distribution of Request Sizes')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"# Cancelled proposals - were they resubmitted?\n",
|
||||
"ax = axes[2]\n",
|
||||
"cancelled = real_proposals[real_proposals['status'] == 'cancelled']\n",
|
||||
"resubmitted = []\n",
|
||||
"for _, row in cancelled.iterrows():\n",
|
||||
" # Check if same beneficiary has a funded proposal\n",
|
||||
" same_benef = funded[funded['beneficiary'] == row['beneficiary']]\n",
|
||||
" resubmitted.append(len(same_benef) > 0)\n",
|
||||
"resub_counts = pd.Series(resubmitted).value_counts()\n",
|
||||
"labels = ['Resubmitted & Funded' if k else 'Not Resubmitted' for k in resub_counts.index]\n",
|
||||
"ax.pie(resub_counts.values, labels=labels, autopct='%1.0f%%',\n",
|
||||
" colors=['#2ecc71', '#95a5a6'])\n",
|
||||
"ax.set_title(f'Cancelled Proposals: Resubmission')\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/cv_proposal_outcomes.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Voter Participation & Concentration\n",
|
||||
"\n",
|
||||
"How concentrated was governance power? Did a small number of whales dominate?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Analyze staker participation\n",
|
||||
"staker_stats = stakes.groupby('staker').agg(\n",
|
||||
" num_stakes=('proposal_id', 'count'),\n",
|
||||
" unique_proposals=('proposal_id', 'nunique'),\n",
|
||||
" max_tokens_staked=('tokens_staked', 'max'),\n",
|
||||
" total_conviction=('conviction', 'sum'),\n",
|
||||
").sort_values('max_tokens_staked', ascending=False)\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(2, 2, figsize=(16, 10))\n",
|
||||
"\n",
|
||||
"# Staker activity distribution\n",
|
||||
"ax = axes[0, 0]\n",
|
||||
"ax.hist(staker_stats['unique_proposals'], bins=range(0, staker_stats['unique_proposals'].max()+2),\n",
|
||||
" color='steelblue', edgecolor='white')\n",
|
||||
"ax.set_xlabel('Number of Proposals Staked On')\n",
|
||||
"ax.set_ylabel('Number of Stakers')\n",
|
||||
"ax.set_title(f'Staker Engagement Distribution (n={len(staker_stats)})')\n",
|
||||
"\n",
|
||||
"# Stake size distribution (log scale)\n",
|
||||
"ax = axes[0, 1]\n",
|
||||
"max_stakes = staker_stats['max_tokens_staked'].sort_values(ascending=False)\n",
|
||||
"ax.bar(range(len(max_stakes)), max_stakes.values, color='teal', width=1.0)\n",
|
||||
"ax.set_yscale('log')\n",
|
||||
"ax.set_xlabel('Staker Rank')\n",
|
||||
"ax.set_ylabel('Max Tokens Staked (log scale)')\n",
|
||||
"ax.set_title('Stake Size Distribution (Whale Analysis)')\n",
|
||||
"\n",
|
||||
"# Cumulative stake concentration (Lorenz-like curve)\n",
|
||||
"ax = axes[1, 0]\n",
|
||||
"sorted_stakes = max_stakes.sort_values().values\n",
|
||||
"cumulative = np.cumsum(sorted_stakes) / sorted_stakes.sum()\n",
|
||||
"x = np.arange(len(cumulative)) / len(cumulative)\n",
|
||||
"ax.plot(x, cumulative, 'b-', linewidth=2, label='Actual')\n",
|
||||
"ax.plot([0, 1], [0, 1], 'k--', alpha=0.3, label='Perfect equality')\n",
|
||||
"ax.set_xlabel('Fraction of Stakers')\n",
|
||||
"ax.set_ylabel('Fraction of Total Stake')\n",
|
||||
"ax.set_title('Stake Concentration (Lorenz Curve)')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"# Gini coefficient\n",
|
||||
"n = len(sorted_stakes)\n",
|
||||
"gini = (2 * np.sum((np.arange(1, n+1)) * sorted_stakes) / (n * np.sum(sorted_stakes))) - (n+1)/n\n",
|
||||
"ax.text(0.05, 0.9, f'Gini: {gini:.3f}', transform=ax.transAxes, fontsize=12,\n",
|
||||
" bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))\n",
|
||||
"\n",
|
||||
"# Top 10 stakers - what % of total conviction\n",
|
||||
"ax = axes[1, 1]\n",
|
||||
"top10 = staker_stats.head(10)\n",
|
||||
"top10_share = top10['max_tokens_staked'].sum() / staker_stats['max_tokens_staked'].sum()\n",
|
||||
"labels_short = [f'{addr[:6]}...{addr[-4:]}' for addr in top10.index]\n",
|
||||
"ax.barh(range(len(top10)), top10['max_tokens_staked'], color='coral')\n",
|
||||
"ax.set_yticks(range(len(top10)))\n",
|
||||
"ax.set_yticklabels(labels_short, fontsize=8)\n",
|
||||
"ax.set_xlabel('Max Tokens Staked')\n",
|
||||
"ax.set_title(f'Top 10 Stakers ({top10_share:.1%} of total stake)')\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/cv_participation.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"print(f'\\nParticipation stats:')\n",
|
||||
"print(f' Total unique stakers: {len(staker_stats)}')\n",
|
||||
"print(f' Median proposals per staker: {staker_stats[\"unique_proposals\"].median():.0f}')\n",
|
||||
"print(f' Mean proposals per staker: {staker_stats[\"unique_proposals\"].mean():.1f}')\n",
|
||||
"print(f' Stakers on 1 proposal only: {(staker_stats[\"unique_proposals\"]==1).sum()}')\n",
|
||||
"print(f' Stakers on 5+ proposals: {(staker_stats[\"unique_proposals\"]>=5).sum()}')\n",
|
||||
"print(f' Gini coefficient: {gini:.3f}')\n",
|
||||
"print(f' Top 10 stakers hold {top10_share:.1%} of max stake')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Conviction Dynamics Over Time\n",
|
||||
"\n",
|
||||
"How did conviction accumulate? Were proposals funded quickly or did they require sustained support?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Track conviction over time for each proposal\n",
|
||||
"# Use support updates as the timeline\n",
|
||||
"support_by_prop = supports.groupby('proposal_id').agg(\n",
|
||||
" num_updates=('block', 'count'),\n",
|
||||
" first_block=('block', 'min'),\n",
|
||||
" last_block=('block', 'max'),\n",
|
||||
")\n",
|
||||
"support_by_prop['block_span'] = support_by_prop['last_block'] - support_by_prop['first_block']\n",
|
||||
"# Approximate time: Gnosis Chain ~5 sec blocks\n",
|
||||
"support_by_prop['days_active'] = support_by_prop['block_span'] * 5 / 86400\n",
|
||||
"\n",
|
||||
"# Merge with proposal data\n",
|
||||
"merged = proposals.merge(support_by_prop, left_on='id', right_index=True, how='left')\n",
|
||||
"merged = merged[merged['id'] > 1] # exclude abstain\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n",
|
||||
"\n",
|
||||
"# Days active by status\n",
|
||||
"ax = axes[0]\n",
|
||||
"for status, color in [('executed', '#2ecc71'), ('cancelled', '#e74c3c')]:\n",
|
||||
" subset = merged[merged['status'] == status]\n",
|
||||
" ax.scatter(subset['amount_requested'], subset['days_active'],\n",
|
||||
" c=color, s=60, alpha=0.7, label=status, edgecolors='white')\n",
|
||||
"ax.set_xlabel('Amount Requested (TEC)')\n",
|
||||
"ax.set_ylabel('Days Active (approx)')\n",
|
||||
"ax.set_title('Proposal Lifetime vs Request Size')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"# Number of support updates per proposal\n",
|
||||
"ax = axes[1]\n",
|
||||
"for status, color in [('executed', '#2ecc71'), ('cancelled', '#e74c3c')]:\n",
|
||||
" subset = merged[merged['status'] == status]\n",
|
||||
" ax.scatter(subset['amount_requested'], subset['num_updates'],\n",
|
||||
" c=color, s=60, alpha=0.7, label=status, edgecolors='white')\n",
|
||||
"ax.set_xlabel('Amount Requested (TEC)')\n",
|
||||
"ax.set_ylabel('Support Update Events')\n",
|
||||
"ax.set_title('Governance Activity per Proposal')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/cv_dynamics.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"print(f'\\nConviction dynamics:')\n",
|
||||
"print(f' Funded proposals - median days active: {merged[merged[\"status\"]==\"executed\"][\"days_active\"].median():.1f}')\n",
|
||||
"print(f' Cancelled proposals - median days active: {merged[merged[\"status\"]==\"cancelled\"][\"days_active\"].median():.1f}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Governance Throughput & Spending Rate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Timeline of funded proposals\n",
|
||||
"funded_timeline = proposals[(proposals['status'] == 'executed') & (proposals['amount_requested'] > 0)].copy()\n",
|
||||
"funded_timeline = funded_timeline.sort_values('block')\n",
|
||||
"funded_timeline['cumulative_funded'] = funded_timeline['amount_requested'].cumsum()\n",
|
||||
"\n",
|
||||
"# Approximate dates from blocks (block 20086944 ≈ Jan 19, 2022)\n",
|
||||
"# Gnosis chain: ~5 sec blocks\n",
|
||||
"import datetime\n",
|
||||
"genesis_block = 20086944\n",
|
||||
"genesis_date = datetime.datetime(2022, 1, 19)\n",
|
||||
"funded_timeline['date'] = funded_timeline['block'].apply(\n",
|
||||
" lambda b: genesis_date + datetime.timedelta(seconds=(b - genesis_block) * 5)\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n",
|
||||
"\n",
|
||||
"# Cumulative funding\n",
|
||||
"ax = axes[0]\n",
|
||||
"ax.step(funded_timeline['date'], funded_timeline['cumulative_funded'],\n",
|
||||
" where='post', color='steelblue', linewidth=2)\n",
|
||||
"ax.fill_between(funded_timeline['date'], funded_timeline['cumulative_funded'],\n",
|
||||
" step='post', alpha=0.2, color='steelblue')\n",
|
||||
"ax.set_ylabel('Cumulative TEC Funded')\n",
|
||||
"ax.set_title('Conviction Voting: Cumulative Funding Over Time')\n",
|
||||
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n",
|
||||
"plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)\n",
|
||||
"\n",
|
||||
"# Individual proposal amounts over time\n",
|
||||
"ax = axes[1]\n",
|
||||
"colors_map = {'executed': '#2ecc71', 'cancelled': '#e74c3c', 'open': '#3498db'}\n",
|
||||
"all_props = proposals[proposals['id'] > 1].copy()\n",
|
||||
"all_props['date'] = all_props['block'].apply(\n",
|
||||
" lambda b: genesis_date + datetime.timedelta(seconds=(b - genesis_block) * 5)\n",
|
||||
")\n",
|
||||
"for status in ['executed', 'cancelled']:\n",
|
||||
" subset = all_props[all_props['status'] == status]\n",
|
||||
" ax.bar(subset['date'], subset['amount_requested'],\n",
|
||||
" width=5, color=colors_map[status], alpha=0.7, label=status)\n",
|
||||
"ax.set_ylabel('TEC Requested')\n",
|
||||
"ax.set_xlabel('Date')\n",
|
||||
"ax.set_title('Individual Proposals Over Time')\n",
|
||||
"ax.legend()\n",
|
||||
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))\n",
|
||||
"plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/cv_throughput.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"# Calculate spending rate\n",
|
||||
"date_range = (funded_timeline['date'].max() - funded_timeline['date'].min()).days\n",
|
||||
"monthly_rate = funded_timeline['amount_requested'].sum() / (date_range / 30)\n",
|
||||
"print(f'\\nSpending analysis:')\n",
|
||||
"print(f' Active period: {funded_timeline[\"date\"].min().strftime(\"%b %Y\")} to {funded_timeline[\"date\"].max().strftime(\"%b %Y\")} ({date_range} days)')\n",
|
||||
"print(f' Total funded: {funded_timeline[\"amount_requested\"].sum():,.0f} TEC')\n",
|
||||
"print(f' Monthly burn rate: ~{monthly_rate:,.0f} TEC/month')\n",
|
||||
"print(f' Avg proposal size: {funded_timeline[\"amount_requested\"].mean():,.0f} TEC')\n",
|
||||
"print(f' Median proposal size: {funded_timeline[\"amount_requested\"].median():,.0f} TEC')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Key Findings Summary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print('=' * 60)\n",
|
||||
"print('CONVICTION VOTING — KEY FINDINGS')\n",
|
||||
"print('=' * 60)\n",
|
||||
"print(f'''\n",
|
||||
"SCALE:\n",
|
||||
" {len(proposals)} total proposals, {len(funded_timeline)} funded\n",
|
||||
" {funded_timeline[\"amount_requested\"].sum():,.0f} TEC disbursed\n",
|
||||
" {len(staker_stats)} unique governance participants\n",
|
||||
"\n",
|
||||
"CONCENTRATION:\n",
|
||||
" Gini coefficient: {gini:.3f}\n",
|
||||
" Top 10 stakers: {top10_share:.1%} of stake\n",
|
||||
" {(staker_stats[\"unique_proposals\"]==1).sum()}/{len(staker_stats)} stakers only voted once\n",
|
||||
"\n",
|
||||
"OUTCOMES:\n",
|
||||
" {len(funded_timeline)}/{len(real_proposals)} proposals funded ({len(funded_timeline)/len(real_proposals):.0%} success rate)\n",
|
||||
" {len(cancelled)}/{len(real_proposals)} cancelled ({len(cancelled)/len(real_proposals):.0%})\n",
|
||||
"\n",
|
||||
"QUESTIONS FOR DEEPER ANALYSIS:\n",
|
||||
" 1. Did conviction voting favor incumbents/repeat proposers?\n",
|
||||
" 2. How did the 11% spending limit affect large proposals?\n",
|
||||
" 3. Were cancelled proposals victims of insufficient participation or active opposition?\n",
|
||||
" 4. Did the Abstain proposal (#1) serve its intended signal function?\n",
|
||||
" 5. How did staking for CV affect circulating supply and ABC price?\n",
|
||||
"''')"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 03 — Treasury & Pool Analysis\n",
|
||||
"\n",
|
||||
"Analyzing the flow of value through the TEC's key pools:\n",
|
||||
"- Common Pool (funding source for conviction voting)\n",
|
||||
"- Reserve Pool (bonding curve collateral)\n",
|
||||
"- Token balances and USD valuations over time\n",
|
||||
"- Treasury health and sustainability metrics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import matplotlib.dates as mdates\n",
|
||||
"import seaborn as sns\n",
|
||||
"\n",
|
||||
"sns.set_theme(style='whitegrid', palette='deep')\n",
|
||||
"plt.rcParams['figure.figsize'] = (14, 6)\n",
|
||||
"plt.rcParams['figure.dpi'] = 100\n",
|
||||
"\n",
|
||||
"DATA = '../data/onchain'\n",
|
||||
"\n",
|
||||
"# Load pool data\n",
|
||||
"common = pd.read_csv(f'{DATA}/dune_common_pool.csv')\n",
|
||||
"reserve = pd.read_csv(f'{DATA}/dune_reserve_pool.csv')\n",
|
||||
"balances = pd.read_csv(f'{DATA}/dune_token_balances_usd.csv')\n",
|
||||
"\n",
|
||||
"# Parse dates\n",
|
||||
"common['date'] = pd.to_datetime(common['day'])\n",
|
||||
"reserve['date'] = pd.to_datetime(reserve['day'])\n",
|
||||
"balances['date'] = pd.to_datetime(balances['day'])\n",
|
||||
"\n",
|
||||
"print('Common Pool:', common['date'].min().date(), 'to', common['date'].max().date(), f'({len(common)} rows)')\n",
|
||||
"print('Reserve Pool:', reserve['date'].min().date(), 'to', reserve['date'].max().date(), f'({len(reserve)} rows)')\n",
|
||||
"print('Token Balances:', balances['date'].min().date(), 'to', balances['date'].max().date(), f'({len(balances)} rows)')\n",
|
||||
"print('\\nToken symbols in balances:', balances['token_symbol'].unique())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Common Pool & Reserve Pool Over Time (Gnosis Chain era)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig, axes = plt.subplots(3, 1, figsize=(16, 14), sharex=True)\n",
|
||||
"\n",
|
||||
"# Common Pool balance\n",
|
||||
"ax = axes[0]\n",
|
||||
"ax.plot(common['date'], common['balance'], color='#2ecc71', linewidth=1.5)\n",
|
||||
"ax.fill_between(common['date'], common['balance'], alpha=0.2, color='#2ecc71')\n",
|
||||
"ax.set_ylabel('Balance (native token)')\n",
|
||||
"ax.set_title('Common Pool Balance Over Time')\n",
|
||||
"# Annotate key levels\n",
|
||||
"ax.axhline(y=common['balance'].iloc[0], color='gray', linestyle='--', alpha=0.3)\n",
|
||||
"ax.text(common['date'].iloc[0], common['balance'].iloc[0] * 1.02,\n",
|
||||
" f'Start: {common[\"balance\"].iloc[0]:,.0f}', fontsize=9)\n",
|
||||
"ax.text(common['date'].iloc[-1], common['balance'].iloc[-1] * 1.02,\n",
|
||||
" f'End: {common[\"balance\"].iloc[-1]:,.0f}', fontsize=9)\n",
|
||||
"\n",
|
||||
"# Reserve Pool balance\n",
|
||||
"ax = axes[1]\n",
|
||||
"ax.plot(reserve['date'], reserve['balance'], color='#3498db', linewidth=1.5)\n",
|
||||
"ax.fill_between(reserve['date'], reserve['balance'], alpha=0.2, color='#3498db')\n",
|
||||
"ax.set_ylabel('Balance (native token)')\n",
|
||||
"ax.set_title('Reserve Pool Balance Over Time')\n",
|
||||
"ax.text(reserve['date'].iloc[0], reserve['balance'].iloc[0] * 1.02,\n",
|
||||
" f'Start: {reserve[\"balance\"].iloc[0]:,.0f}', fontsize=9)\n",
|
||||
"ax.text(reserve['date'].iloc[-1], reserve['balance'].iloc[-1] * 1.02,\n",
|
||||
" f'End: {reserve[\"balance\"].iloc[-1]:,.0f}', fontsize=9)\n",
|
||||
"\n",
|
||||
"# Inflows & outflows for common pool\n",
|
||||
"ax = axes[2]\n",
|
||||
"ax.bar(common['date'], common['inflow'], color='#2ecc71', alpha=0.6, label='Inflow', width=1)\n",
|
||||
"ax.bar(common['date'], -common['outflow'].abs(), color='#e74c3c', alpha=0.6, label='Outflow', width=1)\n",
|
||||
"ax.set_ylabel('Flow Amount')\n",
|
||||
"ax.set_title('Common Pool Daily Flows')\n",
|
||||
"ax.legend()\n",
|
||||
"ax.axhline(y=0, color='black', linewidth=0.5)\n",
|
||||
"\n",
|
||||
"for ax in axes:\n",
|
||||
" ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
" ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))\n",
|
||||
"\n",
|
||||
"plt.setp(axes[-1].xaxis.get_majorticklabels(), rotation=45)\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/treasury_pools.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"# Key metrics\n",
|
||||
"cp_drawdown = (common['balance'].iloc[-1] - common['balance'].max()) / common['balance'].max()\n",
|
||||
"rp_drawdown = (reserve['balance'].iloc[-1] - reserve['balance'].max()) / reserve['balance'].max()\n",
|
||||
"print(f'\\nCommon Pool: {common[\"balance\"].max():,.0f} peak → {common[\"balance\"].iloc[-1]:,.0f} end ({cp_drawdown:.1%} drawdown)')\n",
|
||||
"print(f'Reserve Pool: {reserve[\"balance\"].max():,.0f} peak → {reserve[\"balance\"].iloc[-1]:,.0f} end ({rp_drawdown:.1%} drawdown)')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Post-Migration Token Balances (Optimism era)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Pivot balances by token\n",
|
||||
"fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n",
|
||||
"\n",
|
||||
"for token in balances['token_symbol'].unique():\n",
|
||||
" subset = balances[balances['token_symbol'] == token].sort_values('date')\n",
|
||||
" \n",
|
||||
" # Native balance\n",
|
||||
" axes[0].plot(subset['date'], subset['balance'], label=token, linewidth=1.5)\n",
|
||||
" # USD balance\n",
|
||||
" axes[1].plot(subset['date'], subset['balance_usd_f'], label=token, linewidth=1.5)\n",
|
||||
"\n",
|
||||
"axes[0].set_ylabel('Token Balance')\n",
|
||||
"axes[0].set_title('Treasury Token Balances (Post-Migration)')\n",
|
||||
"axes[0].legend()\n",
|
||||
"\n",
|
||||
"axes[1].set_ylabel('USD Value')\n",
|
||||
"axes[1].set_title('Treasury USD Valuation')\n",
|
||||
"axes[1].legend()\n",
|
||||
"\n",
|
||||
"for ax in axes:\n",
|
||||
" ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
" ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))\n",
|
||||
"\n",
|
||||
"plt.setp(axes[-1].xaxis.get_majorticklabels(), rotation=45)\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/treasury_balances_usd.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"# Total USD over time\n",
|
||||
"total_usd = balances.groupby('date')['balance_usd_f'].sum().sort_index()\n",
|
||||
"print(f'\\nTotal treasury USD:')\n",
|
||||
"print(f' Max: ${total_usd.max():,.0f}')\n",
|
||||
"print(f' Min: ${total_usd.min():,.0f}')\n",
|
||||
"print(f' Latest: ${total_usd.iloc[-1]:,.0f}')\n",
|
||||
"print(f' Drawdown from max: {(total_usd.iloc[-1] - total_usd.max()) / total_usd.max():.1%}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Treasury Sustainability Analysis\n",
|
||||
"\n",
|
||||
"How long could the treasury sustain operations at various burn rates?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Calculate net flow rates for common pool\n",
|
||||
"common_monthly = common.set_index('date').resample('ME').agg({\n",
|
||||
" 'inflow': 'sum',\n",
|
||||
" 'outflow': 'sum',\n",
|
||||
" 'balance': 'last'\n",
|
||||
"})\n",
|
||||
"common_monthly['net_flow'] = common_monthly['inflow'] - common_monthly['outflow'].abs()\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n",
|
||||
"\n",
|
||||
"# Monthly net flows\n",
|
||||
"ax = axes[0]\n",
|
||||
"colors = ['#2ecc71' if v >= 0 else '#e74c3c' for v in common_monthly['net_flow']]\n",
|
||||
"ax.bar(common_monthly.index, common_monthly['net_flow'], color=colors, width=20)\n",
|
||||
"ax.axhline(y=0, color='black', linewidth=0.5)\n",
|
||||
"ax.set_ylabel('Net Flow')\n",
|
||||
"ax.set_title('Common Pool Monthly Net Flows (positive = growing)')\n",
|
||||
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"\n",
|
||||
"# Runway analysis\n",
|
||||
"ax = axes[1]\n",
|
||||
"# Monthly outflow rate\n",
|
||||
"monthly_outflow = common_monthly['outflow'].abs()\n",
|
||||
"ax.bar(common_monthly.index, monthly_outflow, color='#e74c3c', alpha=0.6, width=20)\n",
|
||||
"ax.set_ylabel('Monthly Outflow')\n",
|
||||
"ax.set_title('Common Pool Monthly Spending')\n",
|
||||
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"\n",
|
||||
"# Add trend line\n",
|
||||
"avg_outflow = monthly_outflow.mean()\n",
|
||||
"ax.axhline(y=avg_outflow, color='red', linestyle='--', alpha=0.5,\n",
|
||||
" label=f'Avg: {avg_outflow:,.0f}/month')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/treasury_sustainability.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"print(f'\\nSustainability metrics:')\n",
|
||||
"print(f' Avg monthly outflow: {avg_outflow:,.0f}')\n",
|
||||
"print(f' Avg monthly inflow: {common_monthly[\"inflow\"].mean():,.0f}')\n",
|
||||
"print(f' Avg monthly net: {common_monthly[\"net_flow\"].mean():,.0f}')\n",
|
||||
"print(f' Months with negative net flow: {(common_monthly[\"net_flow\"] < 0).sum()}/{len(common_monthly)}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Reserve Ratio Analysis\n",
|
||||
"\n",
|
||||
"The ABC's reserve ratio determines the price curve shape. How did it evolve?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Reserve ratio = reserve_balance / (token_supply * price)\n",
|
||||
"# We can approximate from pool data\n",
|
||||
"# For now, show reserve pool vs common pool ratio\n",
|
||||
"\n",
|
||||
"# Align dates\n",
|
||||
"common_daily = common.set_index('date')['balance'].resample('D').last().ffill()\n",
|
||||
"reserve_daily = reserve.set_index('date')['balance'].resample('D').last().ffill()\n",
|
||||
"\n",
|
||||
"aligned = pd.DataFrame({\n",
|
||||
" 'common_pool': common_daily,\n",
|
||||
" 'reserve_pool': reserve_daily\n",
|
||||
"}).dropna()\n",
|
||||
"\n",
|
||||
"aligned['total'] = aligned['common_pool'] + aligned['reserve_pool']\n",
|
||||
"aligned['reserve_share'] = aligned['reserve_pool'] / aligned['total']\n",
|
||||
"aligned['common_share'] = aligned['common_pool'] / aligned['total']\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n",
|
||||
"\n",
|
||||
"ax = axes[0]\n",
|
||||
"ax.stackplot(aligned.index,\n",
|
||||
" aligned['reserve_pool'], aligned['common_pool'],\n",
|
||||
" labels=['Reserve Pool', 'Common Pool'],\n",
|
||||
" colors=['#3498db', '#2ecc71'], alpha=0.7)\n",
|
||||
"ax.set_ylabel('Balance')\n",
|
||||
"ax.set_title('Pool Composition Over Time')\n",
|
||||
"ax.legend(loc='upper right')\n",
|
||||
"\n",
|
||||
"ax = axes[1]\n",
|
||||
"ax.plot(aligned.index, aligned['reserve_share'], color='#3498db', linewidth=1.5, label='Reserve Share')\n",
|
||||
"ax.plot(aligned.index, aligned['common_share'], color='#2ecc71', linewidth=1.5, label='Common Share')\n",
|
||||
"ax.set_ylabel('Share of Total')\n",
|
||||
"ax.set_title('Pool Share Ratio Over Time')\n",
|
||||
"ax.legend()\n",
|
||||
"ax.set_ylim(0, 1)\n",
|
||||
"\n",
|
||||
"for ax in axes:\n",
|
||||
" ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
" ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/pool_composition.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"print(f'\\nPool composition:')\n",
|
||||
"print(f' Start - Reserve: {aligned[\"reserve_share\"].iloc[0]:.1%}, Common: {aligned[\"common_share\"].iloc[0]:.1%}')\n",
|
||||
"print(f' End - Reserve: {aligned[\"reserve_share\"].iloc[-1]:.1%}, Common: {aligned[\"common_share\"].iloc[-1]:.1%}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Key Findings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print('=' * 60)\n",
|
||||
"print('TREASURY & POOLS — KEY FINDINGS')\n",
|
||||
"print('=' * 60)\n",
|
||||
"print(f'''\n",
|
||||
"COMMON POOL:\n",
|
||||
" Peak balance: {common[\"balance\"].max():,.0f}\n",
|
||||
" Final balance: {common[\"balance\"].iloc[-1]:,.0f}\n",
|
||||
" Total outflows: {common[\"outflow\"].abs().sum():,.0f}\n",
|
||||
" Net negative months: {(common_monthly[\"net_flow\"] < 0).sum()}/{len(common_monthly)}\n",
|
||||
"\n",
|
||||
"RESERVE POOL:\n",
|
||||
" Peak balance: {reserve[\"balance\"].max():,.0f}\n",
|
||||
" Final balance: {reserve[\"balance\"].iloc[-1]:,.0f}\n",
|
||||
"\n",
|
||||
"POST-MIGRATION (Optimism):\n",
|
||||
" Treasury peaked at: ${total_usd.max():,.0f}\n",
|
||||
" Treasury at end: ${total_usd.iloc[-1]:,.0f}\n",
|
||||
"\n",
|
||||
"KEY QUESTIONS:\n",
|
||||
" 1. Was the ABC tribute rate sufficient to replenish the common pool?\n",
|
||||
" 2. How did the reserve ratio compare to the theoretical target?\n",
|
||||
" 3. Did the treasury drawdown correlate with token price decline?\n",
|
||||
" 4. Could different spending limits have extended the runway?\n",
|
||||
"''')"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 04 — Mechanism Post-Mortem\n",
|
||||
"\n",
|
||||
"Cross-cutting analysis of how the ABC + Conviction Voting mechanisms performed\n",
|
||||
"under various market and social pressures.\n",
|
||||
"\n",
|
||||
"## Central thesis to evaluate:\n",
|
||||
"The TEC deployed an Augmented Bonding Curve (primary market) feeding a Common Pool\n",
|
||||
"governed by Conviction Voting (fund allocation). This analysis evaluates whether\n",
|
||||
"these mechanisms achieved their design goals and what broke down."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import matplotlib.dates as mdates\n",
|
||||
"import seaborn as sns\n",
|
||||
"import datetime\n",
|
||||
"\n",
|
||||
"sns.set_theme(style='whitegrid', palette='deep')\n",
|
||||
"plt.rcParams['figure.figsize'] = (14, 6)\n",
|
||||
"plt.rcParams['figure.dpi'] = 100\n",
|
||||
"\n",
|
||||
"DATA = '../data/onchain'\n",
|
||||
"\n",
|
||||
"# Load all data\n",
|
||||
"proposals = pd.read_csv(f'{DATA}/cv_proposals.csv')\n",
|
||||
"stakes = pd.read_csv(f'{DATA}/cv_stakes.csv')\n",
|
||||
"common = pd.read_csv(f'{DATA}/dune_common_pool.csv')\n",
|
||||
"reserve = pd.read_csv(f'{DATA}/dune_reserve_pool.csv')\n",
|
||||
"balances = pd.read_csv(f'{DATA}/dune_token_balances_usd.csv')\n",
|
||||
"\n",
|
||||
"# Parse dates\n",
|
||||
"common['date'] = pd.to_datetime(common['day'])\n",
|
||||
"reserve['date'] = pd.to_datetime(reserve['day'])\n",
|
||||
"balances['date'] = pd.to_datetime(balances['day'])\n",
|
||||
"\n",
|
||||
"# Approximate dates for on-chain events\n",
|
||||
"genesis_block = 20086944\n",
|
||||
"genesis_date = datetime.datetime(2022, 1, 19)\n",
|
||||
"\n",
|
||||
"def block_to_date(block):\n",
|
||||
" return genesis_date + datetime.timedelta(seconds=(block - genesis_block) * 5)\n",
|
||||
"\n",
|
||||
"proposals['date'] = proposals['block'].apply(block_to_date)\n",
|
||||
"stakes['date'] = stakes['block'].apply(block_to_date)\n",
|
||||
"\n",
|
||||
"print('Data loaded. Analysis period:')\n",
|
||||
"print(f' CV proposals: {proposals[\"date\"].min().date()} to {proposals[\"date\"].max().date()}')\n",
|
||||
"print(f' Common pool: {common[\"date\"].min().date()} to {common[\"date\"].max().date()}')\n",
|
||||
"print(f' Reserve pool: {reserve[\"date\"].min().date()} to {reserve[\"date\"].max().date()}')\n",
|
||||
"print(f' Token balances: {balances[\"date\"].min().date()} to {balances[\"date\"].max().date()}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. The Fundamental Loop: ABC → Common Pool → CV → Grants\n",
|
||||
"\n",
|
||||
"Did the system achieve a self-sustaining funding cycle?\n",
|
||||
"- ABC tributes (entry/exit fees) replenish the common pool\n",
|
||||
"- CV allocates common pool funds to proposals\n",
|
||||
"- Funded proposals should create value → attract new participants → ABC buys → more tributes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Compare inflow (tributes) vs outflow (grants) on common pool\n",
|
||||
"cp_monthly = common.set_index('date').resample('ME').agg({\n",
|
||||
" 'inflow': 'sum',\n",
|
||||
" 'outflow': lambda x: x.abs().sum(),\n",
|
||||
" 'balance': 'last'\n",
|
||||
"})\n",
|
||||
"cp_monthly['net'] = cp_monthly['inflow'] - cp_monthly['outflow']\n",
|
||||
"cp_monthly['cumulative_net'] = cp_monthly['net'].cumsum()\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(2, 1, figsize=(16, 10))\n",
|
||||
"\n",
|
||||
"# Inflow vs Outflow\n",
|
||||
"ax = axes[0]\n",
|
||||
"width = 15\n",
|
||||
"ax.bar(cp_monthly.index - pd.Timedelta(days=8), cp_monthly['inflow'],\n",
|
||||
" width=width, color='#2ecc71', alpha=0.7, label='Inflow (tributes + other)')\n",
|
||||
"ax.bar(cp_monthly.index + pd.Timedelta(days=8), cp_monthly['outflow'],\n",
|
||||
" width=width, color='#e74c3c', alpha=0.7, label='Outflow (grants)')\n",
|
||||
"ax.set_ylabel('Amount')\n",
|
||||
"ax.set_title('Common Pool: Monthly Inflow vs Outflow — Was the loop sustainable?')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"# Cumulative net position\n",
|
||||
"ax = axes[1]\n",
|
||||
"ax.fill_between(cp_monthly.index, cp_monthly['cumulative_net'],\n",
|
||||
" where=cp_monthly['cumulative_net'] >= 0,\n",
|
||||
" color='#2ecc71', alpha=0.3, label='Net positive')\n",
|
||||
"ax.fill_between(cp_monthly.index, cp_monthly['cumulative_net'],\n",
|
||||
" where=cp_monthly['cumulative_net'] < 0,\n",
|
||||
" color='#e74c3c', alpha=0.3, label='Net negative')\n",
|
||||
"ax.plot(cp_monthly.index, cp_monthly['cumulative_net'], 'k-', linewidth=1.5)\n",
|
||||
"ax.axhline(y=0, color='black', linewidth=0.5)\n",
|
||||
"ax.set_ylabel('Cumulative Net Flow')\n",
|
||||
"ax.set_title('Cumulative Net Position — When did outflows exceed inflows?')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"for ax in axes:\n",
|
||||
" ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/mechanism_sustainability.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"total_in = cp_monthly['inflow'].sum()\n",
|
||||
"total_out = cp_monthly['outflow'].sum()\n",
|
||||
"print(f'\\nFunding Loop Analysis:')\n",
|
||||
"print(f' Total inflows: {total_in:,.0f}')\n",
|
||||
"print(f' Total outflows: {total_out:,.0f}')\n",
|
||||
"print(f' Coverage ratio: {total_in/total_out:.2f}x (1.0 = break even)')\n",
|
||||
"print(f' Net deficit: {total_in - total_out:,.0f}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Governance Activity vs Treasury Health\n",
|
||||
"\n",
|
||||
"Did governance participation correlate with treasury health?\n",
|
||||
"As the treasury declined, did participation wane (death spiral) or intensify (crisis response)?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Monthly governance activity\n",
|
||||
"stakes_monthly = stakes.set_index('date').resample('ME').agg(\n",
|
||||
" num_stakes=('proposal_id', 'count'),\n",
|
||||
" unique_stakers=('staker', 'nunique'),\n",
|
||||
" total_staked=('tokens_staked', 'sum'),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Proposals per month\n",
|
||||
"props_monthly = proposals[proposals['id'] > 1].set_index('date').resample('ME').agg(\n",
|
||||
" num_proposals=('id', 'count'),\n",
|
||||
" total_requested=('amount_requested', 'sum'),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"fig, axes = plt.subplots(3, 1, figsize=(16, 14), sharex=True)\n",
|
||||
"\n",
|
||||
"# Treasury balance\n",
|
||||
"ax = axes[0]\n",
|
||||
"ax.plot(common['date'], common['balance'], color='#2ecc71', linewidth=1.5, label='Common Pool')\n",
|
||||
"ax.plot(reserve['date'], reserve['balance'], color='#3498db', linewidth=1.5, label='Reserve Pool')\n",
|
||||
"ax.set_ylabel('Balance')\n",
|
||||
"ax.set_title('Treasury Health')\n",
|
||||
"ax.legend()\n",
|
||||
"\n",
|
||||
"# Active stakers\n",
|
||||
"ax = axes[1]\n",
|
||||
"if len(stakes_monthly) > 0:\n",
|
||||
" ax.bar(stakes_monthly.index, stakes_monthly['unique_stakers'], width=20, color='teal', alpha=0.7)\n",
|
||||
"ax.set_ylabel('Unique Stakers')\n",
|
||||
"ax.set_title('Monthly Governance Participation')\n",
|
||||
"\n",
|
||||
"# New proposals\n",
|
||||
"ax = axes[2]\n",
|
||||
"if len(props_monthly) > 0:\n",
|
||||
" ax.bar(props_monthly.index, props_monthly['num_proposals'], width=20, color='coral', alpha=0.7)\n",
|
||||
"ax.set_ylabel('New Proposals')\n",
|
||||
"ax.set_title('Monthly Proposal Submissions')\n",
|
||||
"ax.set_xlabel('Date')\n",
|
||||
"\n",
|
||||
"for ax in axes:\n",
|
||||
" ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/governance_vs_treasury.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Conviction Voting Parameter Stress Test\n",
|
||||
"\n",
|
||||
"The TEC used these CV parameters:\n",
|
||||
"- **Conviction Growth**: 7 days\n",
|
||||
"- **Minimum Conviction**: 4%\n",
|
||||
"- **Spending Limit**: 11%\n",
|
||||
"\n",
|
||||
"Were these appropriate? How did they interact with the actual dynamics?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Analyze request sizes relative to common pool\n",
|
||||
"funded = proposals[(proposals['status'] == 'executed') & (proposals['amount_requested'] > 0)].copy()\n",
|
||||
"\n",
|
||||
"# For each funded proposal, estimate what % of the pool it represented\n",
|
||||
"# We need to match proposal dates to pool balance dates\n",
|
||||
"# Use the common pool daily balance\n",
|
||||
"cp_daily = common.set_index('date')['balance'].resample('D').last().ffill()\n",
|
||||
"\n",
|
||||
"request_pcts = []\n",
|
||||
"for _, row in funded.iterrows():\n",
|
||||
" prop_date = row['date']\n",
|
||||
" # Find closest pool balance\n",
|
||||
" mask = cp_daily.index <= prop_date\n",
|
||||
" if mask.any():\n",
|
||||
" pool_bal = cp_daily[mask].iloc[-1]\n",
|
||||
" if pool_bal > 0:\n",
|
||||
" pct = row['amount_requested'] / pool_bal\n",
|
||||
" request_pcts.append({\n",
|
||||
" 'id': row['id'],\n",
|
||||
" 'name': row['link'][:40],\n",
|
||||
" 'amount': row['amount_requested'],\n",
|
||||
" 'pool_balance': pool_bal,\n",
|
||||
" 'pct_of_pool': pct,\n",
|
||||
" 'date': prop_date\n",
|
||||
" })\n",
|
||||
"\n",
|
||||
"if request_pcts:\n",
|
||||
" rp_df = pd.DataFrame(request_pcts)\n",
|
||||
"\n",
|
||||
" fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n",
|
||||
"\n",
|
||||
" # Request as % of pool\n",
|
||||
" ax = axes[0]\n",
|
||||
" ax.bar(range(len(rp_df)), rp_df['pct_of_pool'] * 100, color='steelblue')\n",
|
||||
" ax.axhline(y=11, color='red', linestyle='--', label='Spending Limit (11%)')\n",
|
||||
" ax.axhline(y=4, color='orange', linestyle='--', label='Min Conviction (4%)')\n",
|
||||
" ax.set_xlabel('Proposal (chronological)')\n",
|
||||
" ax.set_ylabel('% of Common Pool')\n",
|
||||
" ax.set_title('Proposal Size Relative to Common Pool')\n",
|
||||
" ax.legend()\n",
|
||||
"\n",
|
||||
" # Over time\n",
|
||||
" ax = axes[1]\n",
|
||||
" ax.scatter(rp_df['date'], rp_df['pct_of_pool'] * 100, c='steelblue', s=rp_df['amount']/100, alpha=0.6)\n",
|
||||
" ax.axhline(y=11, color='red', linestyle='--', label='Spending Limit (11%)')\n",
|
||||
" ax.set_xlabel('Date')\n",
|
||||
" ax.set_ylabel('% of Common Pool')\n",
|
||||
" ax.set_title('Request Size Relative to Pool Over Time\\n(bubble size = absolute amount)')\n",
|
||||
" ax.legend()\n",
|
||||
"\n",
|
||||
" plt.tight_layout()\n",
|
||||
" plt.savefig(f'{DATA}/../snapshots/cv_parameters.png', dpi=150, bbox_inches='tight')\n",
|
||||
" plt.show()\n",
|
||||
"\n",
|
||||
" print(f'\\nSpending limit analysis:')\n",
|
||||
" over_limit = (rp_df['pct_of_pool'] > 0.11).sum()\n",
|
||||
" print(f' Proposals exceeding 11% spending limit: {over_limit}/{len(rp_df)}')\n",
|
||||
" print(f' Max request as % of pool: {rp_df[\"pct_of_pool\"].max():.1%}')\n",
|
||||
" print(f' Median request as % of pool: {rp_df[\"pct_of_pool\"].median():.1%}')\n",
|
||||
"else:\n",
|
||||
" print('Could not align proposal dates with pool balance data')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. The Death Spiral Hypothesis\n",
|
||||
"\n",
|
||||
"Did the TEC experience a death spiral?\n",
|
||||
"1. Token price drops → less attractive to hold\n",
|
||||
"2. Holders sell on ABC → reserve pool shrinks → price drops further\n",
|
||||
"3. Less ABC activity → less tribute revenue → common pool depletes faster\n",
|
||||
"4. Less funding available → fewer proposals → less community activity\n",
|
||||
"5. Less activity → holders leave → goto 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Build composite timeline\n",
|
||||
"# We need to align: reserve pool, common pool, governance activity, and token price\n",
|
||||
"\n",
|
||||
"# Normalize each metric to [0, 1] range for comparison\n",
|
||||
"def normalize(series):\n",
|
||||
" return (series - series.min()) / (series.max() - series.min())\n",
|
||||
"\n",
|
||||
"fig, ax = plt.subplots(figsize=(16, 8))\n",
|
||||
"\n",
|
||||
"# Reserve pool (proxy for price/market confidence)\n",
|
||||
"rp_daily = reserve.set_index('date')['balance'].resample('W').last().ffill()\n",
|
||||
"ax.plot(rp_daily.index, normalize(rp_daily), label='Reserve Pool (market confidence)',\n",
|
||||
" color='#3498db', linewidth=2)\n",
|
||||
"\n",
|
||||
"# Common pool (funding capacity)\n",
|
||||
"cp_daily_w = common.set_index('date')['balance'].resample('W').last().ffill()\n",
|
||||
"ax.plot(cp_daily_w.index, normalize(cp_daily_w), label='Common Pool (funding capacity)',\n",
|
||||
" color='#2ecc71', linewidth=2)\n",
|
||||
"\n",
|
||||
"# Governance activity (rolling stake events per month)\n",
|
||||
"stakes_weekly = stakes.set_index('date').resample('W')['proposal_id'].count()\n",
|
||||
"stakes_rolling = stakes_weekly.rolling(4).mean()\n",
|
||||
"if len(stakes_rolling.dropna()) > 0:\n",
|
||||
" ax.plot(stakes_rolling.index, normalize(stakes_rolling.fillna(0)),\n",
|
||||
" label='Governance Activity (4-week rolling)',\n",
|
||||
" color='coral', linewidth=2, alpha=0.8)\n",
|
||||
"\n",
|
||||
"# Mark key events\n",
|
||||
"events = [\n",
|
||||
" (datetime.datetime(2022, 5, 1), 'Terra/Luna collapse'),\n",
|
||||
" (datetime.datetime(2022, 11, 1), 'FTX collapse'),\n",
|
||||
" (datetime.datetime(2023, 1, 1), 'Crypto winter deepens'),\n",
|
||||
"]\n",
|
||||
"for date, label in events:\n",
|
||||
" if rp_daily.index.min() <= date <= rp_daily.index.max():\n",
|
||||
" ax.axvline(x=date, color='gray', linestyle=':', alpha=0.5)\n",
|
||||
" ax.text(date, 1.05, label, rotation=45, fontsize=8, ha='left')\n",
|
||||
"\n",
|
||||
"ax.set_xlabel('Date')\n",
|
||||
"ax.set_ylabel('Normalized Level (0-1)')\n",
|
||||
"ax.set_title('Death Spiral Analysis: Correlated Decline of Market, Treasury, and Governance')\n",
|
||||
"ax.legend(loc='upper right')\n",
|
||||
"ax.set_ylim(-0.05, 1.15)\n",
|
||||
"ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n",
|
||||
"\n",
|
||||
"plt.tight_layout()\n",
|
||||
"plt.savefig(f'{DATA}/../snapshots/death_spiral.png', dpi=150, bbox_inches='tight')\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Mechanism Design Scorecard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"funded = proposals[(proposals['status'] == 'executed') & (proposals['amount_requested'] > 0)]\n",
|
||||
"\n",
|
||||
"print('=' * 70)\n",
|
||||
"print('TEC MECHANISM POST-MORTEM — SCORECARD')\n",
|
||||
"print('=' * 70)\n",
|
||||
"print(f'''\n",
|
||||
"AUGMENTED BONDING CURVE\n",
|
||||
" Design goal: Continuous fundraising with built-in price floor\n",
|
||||
" Reserve ratio: Started at ~31% (target TBD from ABC model)\n",
|
||||
" Entry/exit tribute: Generated revenue for common pool\n",
|
||||
" Primary vs secondary: Token traded at ~33% discount at shutdown\n",
|
||||
" \n",
|
||||
" [?] Did the ABC provide adequate price support during downturns?\n",
|
||||
" [?] Was the tribute rate sufficient to sustain operations?\n",
|
||||
" [?] How did liquidity compare between ABC and Honeyswap?\n",
|
||||
" \n",
|
||||
" (Full ABC analysis pending — need trade data from Dune queries)\n",
|
||||
"\n",
|
||||
"CONVICTION VOTING\n",
|
||||
" Design goal: Continuous, sybil-resistant fund allocation\n",
|
||||
" Proposals funded: {len(funded)}/46 real proposals ({len(funded)/46:.0%})\n",
|
||||
" Total disbursed: {funded[\"amount_requested\"].sum():,.0f} TEC\n",
|
||||
" Unique participants: 159 stakers\n",
|
||||
" \n",
|
||||
" [+] High success rate suggests proposals were well-vetted pre-submission\n",
|
||||
" [+] Diverse set of funded initiatives (academy, research, community)\n",
|
||||
" [-] 159 participants out of ~1,200 token holders (~13% participation)\n",
|
||||
" [-] Governance activity declined as token price fell\n",
|
||||
" [?] Did the 7-day conviction growth period cause delays?\n",
|
||||
" [?] Did the Abstain proposal effectively serve as a brake?\n",
|
||||
"\n",
|
||||
"THE FEEDBACK LOOP\n",
|
||||
" Design goal: Self-sustaining cycle of funding and growth\n",
|
||||
" Reality: Tribute inflows << grant outflows throughout the lifecycle\n",
|
||||
" Treasury declined from peak to shutdown over ~3 years\n",
|
||||
" \n",
|
||||
" [-] The fundamental loop never achieved self-sustainability\n",
|
||||
" [-] Exogenous shocks (Terra, FTX) accelerated decline\n",
|
||||
" [-] Common pool was essentially a drawdown fund, not a renewable one\n",
|
||||
" [?] Was this a mechanism failure or a market/adoption failure?\n",
|
||||
"\n",
|
||||
"SHUTDOWN METRICS\n",
|
||||
" Token at shutdown: ~$0.18 (vs peak of ~$1+)\n",
|
||||
" Treasury: ~$300K remaining\n",
|
||||
" FDV: ~$217K (33% below treasury value)\n",
|
||||
" Grants distributed: $433K direct + $250K via Gitcoin\n",
|
||||
" Total value created: Funded TE Academy, cadCAD, Gravity, research\n",
|
||||
" \n",
|
||||
"OVERALL ASSESSMENT:\n",
|
||||
" The mechanisms worked as designed but could not overcome:\n",
|
||||
" 1. Insufficient external demand to drive ABC tributes\n",
|
||||
" 2. Bear market pressure on reserve pool collateral\n",
|
||||
" 3. Community attrition reducing governance participation\n",
|
||||
" The TEC proved that ABC + CV can work for initial fundraising and\n",
|
||||
" allocation, but long-term sustainability requires mechanisms that\n",
|
||||
" generate revenue beyond trading activity (tributes).\n",
|
||||
"''')"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Loading…
Reference in New Issue