328 lines
12 KiB
Python
Executable File
328 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal, getcontext
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
|
|
getcontext().prec = 42
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
POLICY_PATH = ROOT / "config" / "extraction" / "mainnet-cwusdc-usdc-support-policy.json"
|
|
SMOM_ENV_PATH = ROOT / "smom-dbis-138" / ".env"
|
|
ROOT_ENV_PATH = ROOT / ".env"
|
|
DEPLOYMENT_STATUS = ROOT / "cross-chain-pmm-lps" / "config" / "deployment-status.json"
|
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
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 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
|
|
|
|
|
|
def merged_env_values() -> dict[str, str]:
|
|
values = {}
|
|
values.update(load_env_file(ROOT_ENV_PATH))
|
|
values.update(load_env_file(SMOM_ENV_PATH))
|
|
return values
|
|
|
|
|
|
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 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_uints(value: str, count: int) -> list[int]:
|
|
matches = [int(match) for match in UINT_RE.findall(value)]
|
|
if len(matches) < count:
|
|
raise ValueError(f"expected at least {count} integers, got {matches!r}")
|
|
return matches[:count]
|
|
|
|
|
|
def parse_address(value: str) -> str:
|
|
match = re.search(r"0x[a-fA-F0-9]{40}", value)
|
|
if not match:
|
|
raise ValueError(f"could not parse address from {value!r}")
|
|
return match.group(0)
|
|
|
|
|
|
def decimal_ratio(numerator: Decimal, denominator: Decimal) -> Decimal:
|
|
if denominator == 0:
|
|
return Decimal(0)
|
|
return numerator / denominator
|
|
|
|
|
|
def normalize_units(raw: int, decimals: int) -> Decimal:
|
|
return Decimal(raw) / (Decimal(10) ** decimals)
|
|
|
|
|
|
def compute_deviation_bps(price: Decimal, target: Decimal = Decimal("1")) -> Decimal:
|
|
if target == 0:
|
|
return Decimal(0)
|
|
return abs((price - target) / target) * Decimal(10000)
|
|
|
|
|
|
def resolve_rpc_url(policy: dict, env_values: dict[str, str]) -> str:
|
|
for key in policy["network"].get("rpcEnvKeys", []):
|
|
value = resolve_env_value(key, env_values)
|
|
if value:
|
|
return value.rstrip("\r\n")
|
|
raise RuntimeError("missing mainnet RPC URL")
|
|
|
|
|
|
def load_public_pair_from_policy(policy: dict, deployment_status: dict) -> dict:
|
|
configured = dict(policy["publicPair"])
|
|
chain = deployment_status["chains"][str(policy["network"]["chainId"])]
|
|
for row in chain.get("uniswapV2Pools", []):
|
|
if row.get("base") == configured["base"] and row.get("quote") == configured["quote"]:
|
|
configured.update(
|
|
{
|
|
"poolAddress": row.get("poolAddress", configured["poolAddress"]),
|
|
"factoryAddress": row.get("factoryAddress"),
|
|
"routerAddress": row.get("routerAddress"),
|
|
}
|
|
)
|
|
break
|
|
return configured
|
|
|
|
|
|
def query_uniswap_pair_health(rpc_url: str, pair: dict) -> dict:
|
|
pair_address = pair["poolAddress"]
|
|
if not pair_address or pair_address.lower() == ZERO_ADDRESS:
|
|
return {"live": False, "poolAddress": pair_address}
|
|
|
|
token0 = parse_address(cast_call(rpc_url, pair_address, "token0()(address)"))
|
|
token1 = parse_address(cast_call(rpc_url, pair_address, "token1()(address)"))
|
|
reserve0_raw, reserve1_raw, _ = parse_uints(cast_call(rpc_url, pair_address, "getReserves()(uint112,uint112,uint32)"), 3)
|
|
decimals0 = parse_uint(cast_call(rpc_url, token0, "decimals()(uint8)"))
|
|
decimals1 = parse_uint(cast_call(rpc_url, token1, "decimals()(uint8)"))
|
|
|
|
base_addr = pair.get("baseAddress")
|
|
quote_addr = pair.get("quoteAddress")
|
|
if not base_addr or not quote_addr:
|
|
raise RuntimeError("pair token addresses not supplied")
|
|
|
|
if token0.lower() == base_addr.lower():
|
|
base_raw, quote_raw = reserve0_raw, reserve1_raw
|
|
base_decimals, quote_decimals = decimals0, decimals1
|
|
elif token1.lower() == base_addr.lower():
|
|
base_raw, quote_raw = reserve1_raw, reserve0_raw
|
|
base_decimals, quote_decimals = decimals1, decimals0
|
|
else:
|
|
raise RuntimeError("pair tokens do not match configured base/quote")
|
|
|
|
base_units = normalize_units(base_raw, base_decimals)
|
|
quote_units = normalize_units(quote_raw, quote_decimals)
|
|
price = decimal_ratio(quote_units, base_units)
|
|
deviation_bps = compute_deviation_bps(price)
|
|
|
|
return {
|
|
"live": True,
|
|
"poolAddress": pair_address,
|
|
"token0": token0,
|
|
"token1": token1,
|
|
"baseReserveRaw": str(base_raw),
|
|
"quoteReserveRaw": str(quote_raw),
|
|
"baseReserveUnits": str(base_units),
|
|
"quoteReserveUnits": str(quote_units),
|
|
"priceQuotePerBase": str(price),
|
|
"deviationBps": str(deviation_bps),
|
|
}
|
|
|
|
|
|
def query_dodo_health(rpc_url: str, defended_venue: dict) -> dict:
|
|
pool_address = defended_venue["poolAddress"]
|
|
try:
|
|
mid_price_raw = parse_uint(cast_call(rpc_url, pool_address, "getMidPrice()(uint256)"))
|
|
base_reserve_raw, quote_reserve_raw = parse_uints(
|
|
cast_call(rpc_url, pool_address, "getVaultReserve()(uint256,uint256)"), 2
|
|
)
|
|
except Exception as exc:
|
|
return {
|
|
"live": False,
|
|
"poolAddress": pool_address,
|
|
"error": str(exc),
|
|
}
|
|
|
|
mid_price = normalize_units(mid_price_raw, 18)
|
|
deviation_bps = compute_deviation_bps(mid_price)
|
|
return {
|
|
"live": True,
|
|
"poolAddress": pool_address,
|
|
"midPrice": str(mid_price),
|
|
"deviationBps": str(deviation_bps),
|
|
"baseReserveRaw": str(base_reserve_raw),
|
|
"quoteReserveRaw": str(quote_reserve_raw),
|
|
}
|
|
|
|
|
|
def choose_flash_amount(policy: dict, deviation_bps: Decimal) -> int:
|
|
for row in policy["managedCycle"]["quoteAmountByDeviationBps"]:
|
|
if deviation_bps >= Decimal(row["minDeviationBps"]):
|
|
return int(row["flashQuoteAmountRaw"])
|
|
return 0
|
|
|
|
|
|
def build_decision(policy: dict, public_health: dict, defended_health: dict) -> dict:
|
|
thresholds = policy["thresholds"]
|
|
reasons: list[str] = []
|
|
severity = "ok"
|
|
action = "hold"
|
|
|
|
if not public_health.get("live"):
|
|
severity = "critical"
|
|
action = "manual_recover_public_pair"
|
|
reasons.append("public pair missing or unreadable")
|
|
deviation_bps = Decimal(0)
|
|
else:
|
|
deviation_bps = Decimal(public_health["deviationBps"])
|
|
base_units = Decimal(public_health["baseReserveUnits"])
|
|
quote_units = Decimal(public_health["quoteReserveUnits"])
|
|
thin_base = base_units < Decimal(thresholds["minBaseReserveUnits"])
|
|
thin_quote = quote_units < Decimal(thresholds["minQuoteReserveUnits"])
|
|
|
|
if thin_base or thin_quote:
|
|
severity = "thin"
|
|
action = "manual_reseed_public_lane"
|
|
reasons.append("public pair reserve depth below policy floor")
|
|
|
|
if deviation_bps >= Decimal(thresholds["criticalDeviationBps"]):
|
|
severity = "critical"
|
|
action = "run_managed_cycle"
|
|
reasons.append("public pair outside critical deviation corridor")
|
|
elif deviation_bps >= Decimal(thresholds["interveneDeviationBps"]):
|
|
severity = "intervene"
|
|
action = "run_managed_cycle"
|
|
reasons.append("public pair outside intervention corridor")
|
|
elif deviation_bps >= Decimal(thresholds["warnDeviationBps"]):
|
|
severity = "warn" if severity == "ok" else severity
|
|
if action == "hold":
|
|
action = "monitor"
|
|
reasons.append("public pair outside warn corridor")
|
|
|
|
if action == "run_managed_cycle" and not defended_health.get("live"):
|
|
severity = "blocked"
|
|
action = "manual_fix_defended_venue"
|
|
reasons.append("defended DODO venue unavailable")
|
|
|
|
flash_amount = choose_flash_amount(policy, deviation_bps) if action == "run_managed_cycle" else 0
|
|
if action == "run_managed_cycle" and flash_amount == 0:
|
|
severity = "warn"
|
|
action = "monitor"
|
|
reasons.append("deviation does not map to a configured flash amount")
|
|
|
|
return {
|
|
"severity": severity,
|
|
"action": action,
|
|
"flashQuoteAmountRaw": flash_amount,
|
|
"gasHoldbackTargetRaw": int(policy["managedCycle"]["defaultGasHoldbackTargetRaw"]),
|
|
"reasons": reasons,
|
|
}
|
|
|
|
|
|
def render_shell(result: dict) -> str:
|
|
decision = result["decision"]
|
|
public_pair = result["publicPair"]
|
|
defended = result["defendedVenue"]
|
|
values = {
|
|
"PUBLIC_PAIR_ADDRESS": public_pair["poolAddress"],
|
|
"DEFENDED_POOL_ADDRESS": defended["poolAddress"],
|
|
"PUBLIC_PAIR_DEVIATION_BPS": decision.get("publicDeviationBps", ""),
|
|
"DECISION_SEVERITY": decision["severity"],
|
|
"DECISION_ACTION": decision["action"],
|
|
"FLASH_QUOTE_AMOUNT_RAW": str(decision["flashQuoteAmountRaw"]),
|
|
"GAS_HOLDBACK_TARGET_RAW": str(decision["gasHoldbackTargetRaw"]),
|
|
"REASONS_JSON": json.dumps(decision["reasons"]),
|
|
}
|
|
lines = [
|
|
f"{key}={shlex.quote(value)}" for key, value in values.items()
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--shell", action="store_true", help="Emit shell-friendly KEY=VALUE lines.")
|
|
args = parser.parse_args()
|
|
|
|
policy = load_json(POLICY_PATH)
|
|
deployment_status = load_json(DEPLOYMENT_STATUS)
|
|
env_values = merged_env_values()
|
|
rpc_url = resolve_rpc_url(policy, env_values)
|
|
|
|
chain = deployment_status["chains"][str(policy["network"]["chainId"])]
|
|
public_pair = load_public_pair_from_policy(policy, deployment_status)
|
|
public_pair["baseAddress"] = chain["cwTokens"][public_pair["base"]]
|
|
public_pair["quoteAddress"] = chain["anchorAddresses"][public_pair["quote"]]
|
|
|
|
defended_venue = dict(policy["defendedVenue"])
|
|
public_health = query_uniswap_pair_health(rpc_url, public_pair)
|
|
defended_health = query_dodo_health(rpc_url, defended_venue)
|
|
decision = build_decision(policy, public_health, defended_health)
|
|
if public_health.get("live"):
|
|
decision["publicDeviationBps"] = public_health["deviationBps"]
|
|
|
|
result = {
|
|
"policy": {
|
|
"path": str(POLICY_PATH),
|
|
"thresholds": policy["thresholds"],
|
|
},
|
|
"publicPair": public_pair,
|
|
"publicPairHealth": public_health,
|
|
"defendedVenue": defended_venue,
|
|
"defendedVenueHealth": defended_health,
|
|
"decision": decision,
|
|
}
|
|
|
|
if args.shell:
|
|
print(render_shell(result))
|
|
else:
|
|
print(json.dumps(result, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|