Files
proxmox/scripts/verify/plan-mainnet-cwusdc-usdc-repeg.py

410 lines
18 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from decimal import Decimal, ROUND_CEILING, getcontext
from pathlib import Path
getcontext().prec = 42
ROOT = Path(__file__).resolve().parents[2]
LATEST_SNAPSHOT = ROOT / "reports" / "status" / "mainnet-cwusdc-usdc-preflight-latest.json"
POLICY_PATH = ROOT / "config" / "extraction" / "mainnet-cwusdc-usdc-support-policy.json"
ROOT_ENV_PATH = ROOT / ".env"
SMOM_ENV_PATH = ROOT / "smom-dbis-138" / ".env"
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
DEFAULT_CWUSDC = "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a"
DEFAULT_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
SIX_DECIMALS = Decimal(10) ** 6
ADDRESS_RE = re.compile(r"0x[a-fA-F0-9]{40}")
UINT_RE = re.compile(r"\b\d+\b")
def load_json(path: Path) -> dict:
return json.loads(path.read_text())
def load_env_file(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not path.exists():
return values
for raw_line in path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
def merged_env_values() -> dict[str, str]:
values: dict[str, str] = {}
values.update(load_env_file(ROOT_ENV_PATH))
values.update(load_env_file(SMOM_ENV_PATH))
return values
def resolve_env_value(key: str, env_values: dict[str, str], seen: set[str] | None = None) -> str:
if seen is None:
seen = set()
if key in seen:
return env_values.get(key, "")
seen.add(key)
value = os.environ.get(key) or env_values.get(key, "")
if value.startswith("${") and value.endswith("}"):
inner = value[2:-1]
target = inner.split(":-", 1)[0]
fallback = inner.split(":-", 1)[1] if ":-" in inner else ""
resolved = resolve_env_value(target, env_values, seen)
return resolved or fallback
return value.rstrip("\r\n")
def normalize_units(raw: int, decimals: int = 6) -> Decimal:
return Decimal(raw) / (Decimal(10) ** decimals)
def units_to_raw(units: Decimal, decimals: int = 6) -> int:
scale = Decimal(10) ** decimals
return int((units * scale).to_integral_value(rounding=ROUND_CEILING))
def decimal_max(a: Decimal, b: Decimal) -> Decimal:
return a if a >= b else b
def parse_uint(value: str) -> int:
matches = UINT_RE.findall(value)
if not matches:
raise ValueError(f"could not parse integer from {value!r}")
return int(matches[0])
def parse_address(value: str) -> str:
match = ADDRESS_RE.search(value)
if not match:
raise ValueError(f"could not parse address from {value!r}")
return match.group(0)
def cast_call(rpc_url: str, target: str, signature: str, *args: str) -> str:
cmd = ["cast", "call", target, signature, *args, "--rpc-url", rpc_url]
return subprocess.check_output(cmd, text=True).strip()
def query_balance(rpc_url: str, token: str, holder: str) -> int:
return parse_uint(cast_call(rpc_url, token, "balanceOf(address)(uint256)", holder))
def derive_holder_from_private_key(env_values: dict[str, str]) -> str:
private_key = resolve_env_value("PRIVATE_KEY", env_values) or resolve_env_value("KEEPER_PRIVATE_KEY", env_values)
if not private_key or "${" in private_key:
return ""
output = subprocess.check_output(["cast", "wallet", "address", "--private-key", private_key], text=True).strip()
return parse_address(output)
def shell_quote(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def command_block(lines: list[str]) -> str:
return "\n".join(lines)
def funding_status(required_raw: int, available_raw: int, decimals: int = 6) -> dict:
shortfall_raw = max(required_raw - available_raw, 0)
return {
"requiredRaw": str(required_raw),
"requiredUnits": str(normalize_units(required_raw, decimals)),
"availableRaw": str(available_raw),
"availableUnits": str(normalize_units(available_raw, decimals)),
"shortfallRaw": str(shortfall_raw),
"shortfallUnits": str(normalize_units(shortfall_raw, decimals)),
"covered": shortfall_raw == 0,
}
def build_plan(snapshot: dict, policy: dict, env_values: dict[str, str], holder_override: str) -> dict:
rpc_url = resolve_env_value("ETHEREUM_MAINNET_RPC", env_values)
if not rpc_url:
raise RuntimeError("missing ETHEREUM_MAINNET_RPC")
summary = snapshot["summary"]
public_health = snapshot["health"]["publicPairHealth"]
defended_health = snapshot["health"]["defendedVenueHealth"]
treasury = snapshot.get("treasuryManager") or {}
base_reserve_raw = int(summary["defendedBaseReserveRaw"])
quote_reserve_raw = int(summary["defendedQuoteReserveRaw"])
target_reserve_raw = max(base_reserve_raw, quote_reserve_raw)
add_quote_raw = max(base_reserve_raw - quote_reserve_raw, 0)
add_base_raw = max(quote_reserve_raw - base_reserve_raw, 0)
min_base_units = Decimal(str(policy["thresholds"]["minBaseReserveUnits"]))
min_quote_units = Decimal(str(policy["thresholds"]["minQuoteReserveUnits"]))
public_base_units = Decimal(str(summary["publicPairBaseReserveUnits"]))
public_quote_units = Decimal(str(summary["publicPairQuoteReserveUnits"]))
public_base_shortfall_units = decimal_max(min_base_units - public_base_units, Decimal(0))
public_quote_shortfall_units = decimal_max(min_quote_units - public_quote_units, Decimal(0))
public_base_shortfall_raw = units_to_raw(public_base_shortfall_units)
public_quote_shortfall_raw = units_to_raw(public_quote_shortfall_units)
max_automated_raw = int(policy["managedCycle"]["maxAutomatedFlashQuoteAmountRaw"])
manager_available_raw = int(treasury.get("availableQuoteRaw") or 0)
holder = holder_override or derive_holder_from_private_key(env_values)
cwusdc = resolve_env_value("CWUSDC_MAINNET", env_values) or DEFAULT_CWUSDC
usdc = resolve_env_value("USDC_MAINNET", env_values) or DEFAULT_USDC
manager = snapshot["resolvedAddresses"].get("treasuryManager") or ""
receiver = snapshot["resolvedAddresses"].get("receiver") or ""
defended_pool = snapshot["health"]["defendedVenue"]["poolAddress"]
public_pair = snapshot["health"]["publicPair"]["poolAddress"]
integration = resolve_env_value("DODO_PMM_INTEGRATION_MAINNET", env_values)
router = resolve_env_value("CHAIN_1_UNISWAP_V2_ROUTER", env_values)
holder_state = None
holder_usdc_raw = 0
holder_cwusdc_raw = 0
holder_blockers: list[str] = []
if holder and holder.lower() != ZERO_ADDRESS:
try:
holder_cwusdc_raw = query_balance(rpc_url, cwusdc, holder)
holder_usdc_raw = query_balance(rpc_url, usdc, holder)
holder_state = {
"address": holder,
"cwusdcBalanceRaw": str(holder_cwusdc_raw),
"cwusdcBalanceUnits": str(normalize_units(holder_cwusdc_raw)),
"usdcBalanceRaw": str(holder_usdc_raw),
"usdcBalanceUnits": str(normalize_units(holder_usdc_raw)),
}
except Exception as exc:
holder_blockers.append(f"holder balance query failed: {exc}")
manager_funding = funding_status(max_automated_raw, manager_available_raw)
defended_quote_funding = funding_status(add_quote_raw, holder_usdc_raw)
public_base_funding = funding_status(public_base_shortfall_raw, holder_cwusdc_raw)
public_quote_funding = funding_status(public_quote_shortfall_raw, holder_usdc_raw)
blockers: list[str] = []
warnings = snapshot.get("warnings") or []
if add_base_raw > 0:
blockers.append("defended pool needs base-side top-up logic; current plan only supports quote-side top-up for this rail")
if add_quote_raw > 0 and holder_state and not defended_quote_funding["covered"]:
blockers.append(
"operator wallet does not hold enough USDC to restore defended pool reserve parity; external funding is required"
)
if public_base_shortfall_raw > 0 and holder_state and not public_base_funding["covered"]:
blockers.append(
"operator wallet does not hold enough cWUSDC to reseed the public pair to policy floor; external mint/bridge is required"
)
if public_quote_shortfall_raw > 0 and holder_state and not public_quote_funding["covered"]:
blockers.append(
"operator wallet does not hold enough USDC to reseed the public pair to policy floor"
)
if manager_funding["covered"] is False and holder_state and holder_usdc_raw < max_automated_raw:
blockers.append("operator wallet cannot fully fund even one max-sized automated defense cycle from current USDC balance")
if not integration:
blockers.append("missing DODO_PMM_INTEGRATION_MAINNET")
if not router:
blockers.append("missing CHAIN_1_UNISWAP_V2_ROUTER")
if any("defended quote query failed" in warning for warning in warnings):
blockers.append("defended pool quote preview reverted; set MIN_BASE_OUT_RAW manually before any quote-in trade")
operator_commands = {
"rerunPreflight": "bash scripts/verify/snapshot-mainnet-cwusdc-usdc-preflight.sh",
"rerunPlan": "bash scripts/verify/plan-mainnet-cwusdc-usdc-repeg.sh",
}
if manager and manager.lower() != ZERO_ADDRESS:
operator_commands["fundManagerUsdc"] = command_block(
[
"source smom-dbis-138/scripts/load-env.sh >/dev/null",
f"export USDC={usdc}",
f"export MANAGER={manager}",
f"export AMOUNT_RAW={max_automated_raw}",
'cast send "$USDC" \'transfer(address,uint256)(bool)\' "$MANAGER" "$AMOUNT_RAW" \\',
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
]
)
if integration and add_quote_raw > 0:
operator_commands["tradeDefendedPoolQuoteIn"] = command_block(
[
"source smom-dbis-138/scripts/load-env.sh >/dev/null",
f"export CWUSDC={cwusdc}",
f"export USDC={usdc}",
f"export INTEGRATION={integration}",
f"export POOL={defended_pool}",
f"export QUOTE_IN_RAW={add_quote_raw}",
"export MIN_BASE_OUT_RAW=REPLACE_AFTER_DRY_RUN",
'cast send "$USDC" \'approve(address,uint256)(bool)\' "$INTEGRATION" "$QUOTE_IN_RAW" \\',
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
'cast send "$INTEGRATION" \'swapExactIn(address,address,uint256,uint256)\' "$POOL" "$USDC" "$QUOTE_IN_RAW" "$MIN_BASE_OUT_RAW" \\',
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
]
)
if router and public_base_shortfall_raw > 0 and public_quote_shortfall_raw > 0:
operator_commands["reseedPublicPair"] = command_block(
[
"source smom-dbis-138/scripts/load-env.sh >/dev/null",
f"export ROUTER={router}",
f"export CWUSDC={cwusdc}",
f"export USDC={usdc}",
f"export BASE_AMOUNT_RAW={public_base_shortfall_raw}",
f"export QUOTE_AMOUNT_RAW={public_quote_shortfall_raw}",
'export DEADLINE="$(( $(date +%s) + 3600 ))"',
'export SIGNER="$(cast wallet address --private-key "$PRIVATE_KEY")"',
'cast send "$CWUSDC" \'approve(address,uint256)(bool)\' "$ROUTER" "$BASE_AMOUNT_RAW" \\',
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
'cast send "$USDC" \'approve(address,uint256)(bool)\' "$ROUTER" "$QUOTE_AMOUNT_RAW" \\',
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
'cast send "$ROUTER" \'addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)\' \\',
' "$CWUSDC" "$USDC" "$BASE_AMOUNT_RAW" "$QUOTE_AMOUNT_RAW" "$BASE_AMOUNT_RAW" "$QUOTE_AMOUNT_RAW" "$SIGNER" "$DEADLINE" \\',
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
]
)
recommended_actions = [
{
"step": "fund_manager_for_one_max_cycle",
"quoteAmountRaw": str(max_automated_raw),
"quoteAmountUnits": str(normalize_units(max_automated_raw)),
"status": "ready" if manager_funding["covered"] else "needs_usdc",
},
{
"step": "sell_usdc_into_defended_pool_toward_simple_1_to_1_reserve_parity",
"baseAmountRaw": str(add_base_raw),
"quoteAmountRaw": str(add_quote_raw),
"quoteAmountUnits": str(normalize_units(add_quote_raw)),
"status": "ready" if add_quote_raw == 0 or defended_quote_funding["covered"] else "needs_usdc",
},
{
"step": "reseed_public_pair_to_policy_floor",
"baseAmountRaw": str(public_base_shortfall_raw),
"baseAmountUnits": str(normalize_units(public_base_shortfall_raw)),
"quoteAmountRaw": str(public_quote_shortfall_raw),
"quoteAmountUnits": str(normalize_units(public_quote_shortfall_raw)),
"status": (
"ready"
if public_base_shortfall_raw == 0
or (public_base_funding["covered"] and public_quote_funding["covered"])
else "needs_inventory"
),
},
]
return {
"generatedAt": datetime.now(timezone.utc).isoformat(),
"mode": "read_only_repeg_plan",
"snapshotPath": str(LATEST_SNAPSHOT),
"policyPath": str(POLICY_PATH),
"inferenceNotes": [
"Defended-pool 1:1 sizing is inferred from equal 6-decimal matched-rail tokens and reserve-balance parity.",
"DODO PMM mid-price can still differ from reserve ratio; rerun preflight after each funding action.",
"Public-pair reseed target uses the current policy reserve floors, not a smaller cosmetic liquidity target.",
],
"resolvedAddresses": {
"holder": holder or None,
"cwusdc": cwusdc,
"usdc": usdc,
"publicPair": public_pair,
"defendedPool": defended_pool,
"treasuryManager": manager or None,
"receiver": receiver or None,
"dodoIntegration": integration or None,
"uniswapV2Router": router or None,
},
"defendedVenue": {
"midPrice": summary["defendedMidPrice"],
"deviationBps": summary["defendedDeviationBps"],
"baseReserveRaw": str(base_reserve_raw),
"baseReserveUnits": str(normalize_units(base_reserve_raw)),
"quoteReserveRaw": str(quote_reserve_raw),
"quoteReserveUnits": str(normalize_units(quote_reserve_raw)),
"simpleReserveParity": {
"targetReservePerSideRaw": str(target_reserve_raw),
"targetReservePerSideUnits": str(normalize_units(target_reserve_raw)),
"addBaseRaw": str(add_base_raw),
"addBaseUnits": str(normalize_units(add_base_raw)),
"addQuoteRaw": str(add_quote_raw),
"addQuoteUnits": str(normalize_units(add_quote_raw)),
},
},
"publicLane": {
"pairAddress": public_pair,
"priceQuotePerBase": public_health["priceQuotePerBase"],
"deviationBps": summary["publicPairDeviationBps"],
"baseReserveUnits": str(public_base_units),
"quoteReserveUnits": str(public_quote_units),
"policyFloorBaseUnits": str(min_base_units),
"policyFloorQuoteUnits": str(min_quote_units),
"policyFloorBaseShortfallRaw": str(public_base_shortfall_raw),
"policyFloorBaseShortfallUnits": str(normalize_units(public_base_shortfall_raw)),
"policyFloorQuoteShortfallRaw": str(public_quote_shortfall_raw),
"policyFloorQuoteShortfallUnits": str(normalize_units(public_quote_shortfall_raw)),
},
"automation": {
"managerAvailableQuoteRaw": str(manager_available_raw),
"managerAvailableQuoteUnits": str(normalize_units(manager_available_raw)),
"maxAutomatedFlashQuoteAmountRaw": str(max_automated_raw),
"maxAutomatedFlashQuoteAmountUnits": str(normalize_units(max_automated_raw)),
"managerFundingForOneMaxCycle": manager_funding,
},
"holderState": holder_state,
"holderFundingChecks": {
"defendedQuoteTopUp": defended_quote_funding,
"publicPairBaseTopUp": public_base_funding,
"publicPairQuoteTopUp": public_quote_funding,
},
"recommendedActions": recommended_actions,
"operatorCommands": operator_commands,
"warnings": warnings,
"blockers": holder_blockers + blockers,
"status": {
"canFullyReachSimple1To1WithCurrentHolder": len(holder_blockers + blockers) == 0,
"needsExternalFunding": (
not defended_quote_funding["covered"]
or not public_base_funding["covered"]
or not public_quote_funding["covered"]
),
"canFundManagerFromCurrentHolder": holder_usdc_raw >= max_automated_raw if holder_state else None,
},
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--snapshot", default=str(LATEST_SNAPSHOT), help="Path to a preflight snapshot JSON.")
parser.add_argument("--holder", default="", help="Optional holder address to inventory-check instead of deriving from PRIVATE_KEY.")
parser.add_argument("--out", help="Write the plan JSON to this file.")
args = parser.parse_args()
snapshot_path = Path(args.snapshot)
if not snapshot_path.exists():
raise RuntimeError(f"missing snapshot file: {snapshot_path}")
snapshot = load_json(snapshot_path)
policy = load_json(POLICY_PATH)
env_values = merged_env_values()
plan = build_plan(snapshot, policy, env_values, args.holder.strip())
output = json.dumps(plan, indent=2)
if args.out:
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(output + "\n")
print(output)
return 0
if __name__ == "__main__":
sys.exit(main())