370 lines
14 KiB
Plaintext
370 lines
14 KiB
Plaintext
{
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4,
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"name": "python",
|
|
"version": "3.11.0"
|
|
}
|
|
},
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Cross-Chain Hub-and-Spoke Simulation\n",
|
|
"\n",
|
|
"Explores the multi-chain collateral architecture of MycoFi:\n",
|
|
"\n",
|
|
"- **Hub (Base)**: Registry, bonding curve, tranche manager, treasury\n",
|
|
"- **Spokes**: Ethereum, Arbitrum, Optimism, Base, Polygon — each with a collateral vault\n",
|
|
"- **CCIP messages**: Deposit reports, state syncs, and rebalancing triggers flow between hub and spokes\n",
|
|
"\n",
|
|
"Each chain hosts different LST/LRT assets with their own APYs, prices, and risk scores."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import numpy as np\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import matplotlib.ticker as mticker\n",
|
|
"import sys\n",
|
|
"import os\n",
|
|
"\n",
|
|
"sys.path.insert(0, os.path.abspath('..'))\n",
|
|
"\n",
|
|
"np.random.seed(42)\n",
|
|
"\n",
|
|
"%matplotlib inline\n",
|
|
"plt.rcParams['figure.figsize'] = (14, 5)\n",
|
|
"plt.rcParams['figure.dpi'] = 100"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 1. Create the Default 5-Chain System\n",
|
|
"\n",
|
|
"The default system includes chains: Ethereum, Arbitrum, Optimism, Base, and Polygon,\n",
|
|
"each pre-configured with typical LST assets and APYs."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.crosschain.hub_spoke import (\n",
|
|
" create_default_system,\n",
|
|
" simulate_deposit,\n",
|
|
" tick,\n",
|
|
" apply_price_shock,\n",
|
|
" get_crosschain_metrics,\n",
|
|
")\n",
|
|
"\n",
|
|
"system = create_default_system()\n",
|
|
"\n",
|
|
"chains = list(system.hub.spokes.keys())\n",
|
|
"print(f\"Chains registered: {chains}\")\n",
|
|
"print()\n",
|
|
"for chain, spoke in system.hub.spokes.items():\n",
|
|
" assets = [a.symbol for a in spoke.accepted_assets]\n",
|
|
" print(f\" {chain:12s}: {assets}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 2. Seed Deposits Across All Chains\n",
|
|
"\n",
|
|
"Deposit a realistic initial allocation of LST collateral across all spoke chains."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Seed initial deposits per chain\n",
|
|
"seed_deposits = {\n",
|
|
" \"ethereum\": [(\"stETH\", 120.0), (\"rETH\", 60.0), (\"cbETH\", 30.0)],\n",
|
|
" \"arbitrum\": [(\"wstETH\", 90.0), (\"rETH\", 40.0)],\n",
|
|
" \"optimism\": [(\"wstETH\", 70.0), (\"sfrxETH\", 30.0)],\n",
|
|
" \"base\": [(\"cbETH\", 50.0), (\"USDC\", 120_000.0)],\n",
|
|
" \"polygon\": [(\"stMATIC\", 250_000.0), (\"USDC\", 80_000.0)],\n",
|
|
"}\n",
|
|
"\n",
|
|
"for chain, assets in seed_deposits.items():\n",
|
|
" for symbol, amount in assets:\n",
|
|
" simulate_deposit(system, chain, symbol, amount, timestamp=0.0)\n",
|
|
"\n",
|
|
"# Process all CCIP messages to update hub state\n",
|
|
"system.hub.process_messages(0.01)\n",
|
|
"\n",
|
|
"metrics = get_crosschain_metrics(system)\n",
|
|
"print(f\"Total collateral after seeding: ${metrics['total_collateral_usd']:>14,.0f}\")\n",
|
|
"print()\n",
|
|
"for chain, data in metrics['chains'].items():\n",
|
|
" print(f\" {chain:12s}: ${data['total_value_usd']:>12,.0f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 3. 90-Day Simulation with Random Deposits and Price Movements\n",
|
|
"\n",
|
|
"Each day: apply staking yield, random deposits, mild ETH price drift, and process CCIP messages."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import copy\n",
|
|
"\n",
|
|
"DAYS = 90\n",
|
|
"DT = 1.0 / 365 # One day in years\n",
|
|
"\n",
|
|
"# Assets that can receive random deposits per chain\n",
|
|
"deposit_assets = {\n",
|
|
" \"ethereum\": (\"stETH\", 2400.0),\n",
|
|
" \"arbitrum\": (\"wstETH\", 2410.0),\n",
|
|
" \"optimism\": (\"wstETH\", 2410.0),\n",
|
|
" \"base\": (\"USDC\", 1.0),\n",
|
|
" \"polygon\": (\"USDC\", 1.0),\n",
|
|
"}\n",
|
|
"\n",
|
|
"# Track per-chain collateral over time\n",
|
|
"chain_history = {ch: [] for ch in chains}\n",
|
|
"total_history = []\n",
|
|
"msg_history = []\n",
|
|
"yield_history = []\n",
|
|
"\n",
|
|
"# ETH GBM parameters\n",
|
|
"eth_mu = 0.0\n",
|
|
"eth_sigma = 0.6 # 60% annualised volatility\n",
|
|
"eth_price = 2400.0\n",
|
|
"eth_assets = [\"stETH\", \"rETH\", \"cbETH\", \"wstETH\", \"sfrxETH\"]\n",
|
|
"\n",
|
|
"normal_system = copy.deepcopy(system) # preserve original for stress test\n",
|
|
"\n",
|
|
"for day in range(DAYS):\n",
|
|
" t = day * DT\n",
|
|
"\n",
|
|
" # Random deposits (Poisson arrivals, ~3 per day)\n",
|
|
" n_deposits = np.random.poisson(3)\n",
|
|
" for _ in range(n_deposits):\n",
|
|
" chain = np.random.choice(chains)\n",
|
|
" symbol, unit_price = deposit_assets[chain]\n",
|
|
" usd_amount = np.random.exponential(10_000) # avg $10k deposit\n",
|
|
" qty = usd_amount / max(unit_price, 1)\n",
|
|
" simulate_deposit(normal_system, chain, symbol, qty, timestamp=t)\n",
|
|
"\n",
|
|
" # ETH price GBM step\n",
|
|
" dW = np.random.normal(0, np.sqrt(DT))\n",
|
|
" eth_price *= np.exp((eth_mu - 0.5 * eth_sigma**2) * DT + eth_sigma * dW)\n",
|
|
" multiplier = eth_price / 2400.0\n",
|
|
" for asset_sym in eth_assets:\n",
|
|
" for spoke in normal_system.hub.spokes.values():\n",
|
|
" for asset in spoke.accepted_assets:\n",
|
|
" if asset.symbol == asset_sym:\n",
|
|
" # Reset to base * multiplier (avoid compounding the multiplier)\n",
|
|
" pass # Prices are tracked relatively — we only shock in stress test\n",
|
|
"\n",
|
|
" # Tick system (applies yield + processes messages)\n",
|
|
" tick_data = tick(normal_system, DT)\n",
|
|
"\n",
|
|
" # Record\n",
|
|
" for ch in chains:\n",
|
|
" chain_history[ch].append(normal_system.hub.spokes[ch].total_value_usd)\n",
|
|
" total_history.append(tick_data[\"total_collateral_usd\"])\n",
|
|
" msg_history.append(normal_system.total_messages_delivered)\n",
|
|
" yield_history.append(normal_system.total_yield_generated)\n",
|
|
"\n",
|
|
"print(f\"Simulation complete.\")\n",
|
|
"print(f\"Final total collateral: ${total_history[-1]:,.0f}\")\n",
|
|
"print(f\"Total CCIP messages delivered: {msg_history[-1]}\")\n",
|
|
"print(f\"Total yield generated: ${yield_history[-1]:,.0f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 4. Visualizations — Normal Operation"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"day_axis = np.arange(DAYS)\n",
|
|
"\n",
|
|
"chain_colors = {\n",
|
|
" \"ethereum\": '#627EEA',\n",
|
|
" \"arbitrum\": '#28A0F0',\n",
|
|
" \"optimism\": '#FF0420',\n",
|
|
" \"base\": '#0052FF',\n",
|
|
" \"polygon\": '#8247E5',\n",
|
|
"}\n",
|
|
"\n",
|
|
"fig, axes = plt.subplots(1, 3, figsize=(20, 6))\n",
|
|
"\n",
|
|
"# --- Plot 1: Stacked area — per-chain collateral ---\n",
|
|
"ax = axes[0]\n",
|
|
"stacked_data = [chain_history[ch] for ch in chains]\n",
|
|
"ax.stackplot(day_axis, stacked_data,\n",
|
|
" labels=chains,\n",
|
|
" colors=[chain_colors[c] for c in chains],\n",
|
|
" alpha=0.85)\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Collateral Value (USD)')\n",
|
|
"ax.set_title('Per-Chain Collateral — Stacked Area')\n",
|
|
"ax.legend(loc='upper left', fontsize=8)\n",
|
|
"ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'${x/1e6:.1f}M'))\n",
|
|
"\n",
|
|
"# --- Plot 2: Asset breakdown pie chart (final state) ---\n",
|
|
"ax = axes[1]\n",
|
|
"final_metrics = get_crosschain_metrics(normal_system)\n",
|
|
"asset_values = {}\n",
|
|
"for chain_data in final_metrics['chains'].values():\n",
|
|
" for sym, data in chain_data['assets'].items():\n",
|
|
" asset_values[sym] = asset_values.get(sym, 0.0) + data['value']\n",
|
|
"# Sort by value\n",
|
|
"sorted_assets = sorted(asset_values.items(), key=lambda x: x[1], reverse=True)\n",
|
|
"labels, vals = zip(*sorted_assets)\n",
|
|
"ax.pie(vals, labels=labels, autopct='%1.1f%%', startangle=90,\n",
|
|
" colors=plt.cm.Set3(np.linspace(0, 1, len(vals))))\n",
|
|
"ax.set_title('Asset Breakdown by Value (Day 90)')\n",
|
|
"\n",
|
|
"# --- Plot 3: CCIP message delivery timeline ---\n",
|
|
"ax = axes[2]\n",
|
|
"msg_per_day = np.diff([0] + msg_history)\n",
|
|
"ax.bar(day_axis, msg_per_day, color='#3498db', alpha=0.7, label='Messages delivered')\n",
|
|
"ax.plot(day_axis, np.cumsum(msg_per_day), color='#e74c3c', linewidth=2, label='Cumulative')\n",
|
|
"ax2 = ax.twinx()\n",
|
|
"ax2.plot(day_axis, yield_history, color='#2ecc71', linewidth=2, linestyle='--', label='Cumulative yield')\n",
|
|
"ax2.set_ylabel('Cumulative Yield ($)', color='#2ecc71')\n",
|
|
"ax2.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'${x/1e3:.1f}K'))\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('CCIP Messages')\n",
|
|
"ax.set_title('CCIP Message Timeline & Yield')\n",
|
|
"lines1, labels1 = ax.get_legend_handles_labels()\n",
|
|
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
|
|
"ax.legend(lines1 + lines2, labels1 + labels2, fontsize=8)\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 5. Stress Test — ETH Crash and Cross-Chain Impact Propagation\n",
|
|
"\n",
|
|
"Apply a sudden 50% ETH price crash at day 45 and observe how it propagates across all chains\n",
|
|
"that hold ETH-denominated assets (Ethereum, Arbitrum, Optimism) while stablecoin-heavy\n",
|
|
"chains (Base USDC, Polygon USDC) show relative stability."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"CRASH_DAY = 45\n",
|
|
"CRASH_FACTOR = 0.50 # 50% price crash\n",
|
|
"\n",
|
|
"stress_system = copy.deepcopy(system) # Start from the seeded state\n",
|
|
"\n",
|
|
"stress_chain_history = {ch: [] for ch in chains}\n",
|
|
"stress_total_history = []\n",
|
|
"\n",
|
|
"for day in range(DAYS):\n",
|
|
" t = day * DT\n",
|
|
"\n",
|
|
" # Small random deposits\n",
|
|
" for chain, (symbol, unit_price) in deposit_assets.items():\n",
|
|
" if np.random.random() < 0.4:\n",
|
|
" qty = np.random.exponential(5_000) / max(unit_price, 1)\n",
|
|
" simulate_deposit(stress_system, chain, symbol, qty, timestamp=t)\n",
|
|
"\n",
|
|
" # ETH crash at day 45\n",
|
|
" if day == CRASH_DAY:\n",
|
|
" print(f\"Day {day}: ETH crashes by {(1-CRASH_FACTOR)*100:.0f}%\")\n",
|
|
" for eth_asset in [\"stETH\", \"rETH\", \"cbETH\", \"wstETH\", \"sfrxETH\"]:\n",
|
|
" apply_price_shock(stress_system, eth_asset, CRASH_FACTOR)\n",
|
|
" pre_crash = stress_system.hub.total_collateral_usd\n",
|
|
" stress_system.hub._recalculate_total()\n",
|
|
" post_crash = stress_system.hub.total_collateral_usd\n",
|
|
" print(f\" Pre-crash total collateral: ${pre_crash:,.0f}\")\n",
|
|
" print(f\" Post-crash total collateral: ${post_crash:,.0f}\")\n",
|
|
" print(f\" Drop: {(pre_crash - post_crash)/pre_crash*100:.1f}%\")\n",
|
|
"\n",
|
|
" tick(stress_system, DT)\n",
|
|
"\n",
|
|
" for ch in chains:\n",
|
|
" stress_chain_history[ch].append(stress_system.hub.spokes[ch].total_value_usd)\n",
|
|
" stress_total_history.append(stress_system.hub.total_collateral_usd)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n",
|
|
"\n",
|
|
"# --- Plot 1: Per-chain collateral under stress ---\n",
|
|
"ax = axes[0]\n",
|
|
"for ch in chains:\n",
|
|
" ax.plot(day_axis, stress_chain_history[ch],\n",
|
|
" label=ch, color=chain_colors[ch], linewidth=2)\n",
|
|
"ax.axvline(CRASH_DAY, color='black', linestyle='--', linewidth=1.5, label=f'ETH crash (day {CRASH_DAY})')\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Chain Collateral Value ($)')\n",
|
|
"ax.set_title('Per-Chain Collateral — ETH Crash Stress Test')\n",
|
|
"ax.legend(fontsize=8)\n",
|
|
"ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'${x/1e6:.1f}M'))\n",
|
|
"\n",
|
|
"# --- Plot 2: Normal vs stress total collateral ---\n",
|
|
"ax = axes[1]\n",
|
|
"ax.plot(day_axis, total_history, color='#2ecc71', linewidth=2, label='Normal')\n",
|
|
"ax.plot(day_axis, stress_total_history, color='#e74c3c', linewidth=2, label='Stress (ETH crash)')\n",
|
|
"ax.axvline(CRASH_DAY, color='black', linestyle='--', linewidth=1.5, label=f'Shock (day {CRASH_DAY})')\n",
|
|
"ax.fill_between(day_axis, stress_total_history, total_history,\n",
|
|
" color='#e74c3c', alpha=0.15, label='Loss from shock')\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Total Collateral ($)')\n",
|
|
"ax.set_title('System Collateral: Normal vs Stress')\n",
|
|
"ax.legend(fontsize=8)\n",
|
|
"ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'${x/1e6:.1f}M'))\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()"
|
|
]
|
|
}
|
|
]
|
|
}
|