""" Vrismcost — deriving the ETH price from the chain itself. Cost basis needs a value for every ETH leg, and on this chain most real trades settle in ETH rather than the stablecoin. Without a rate, coverage sits near zero. With one, it jumps — so where the rate comes from decides whether the whole report can be defended. It does not come from a price feed. Historical state is pruned after about ten minutes, so a pool's reserves at an old block cannot be read back; and an external API would mean sending a user's blocks to a third party, which is the one thing this tool promises not to do. Logs survive, though, and that is the opening. Native ETH emits nothing, but WETH is an ordinary ERC-20 and emits Transfer like anything else. The first version of this read every transaction that happened to contain both a WETH leg and a USDG leg, took the largest of each, and divided. That was wrong, and the numbers said so: rates ranged from 180 to 62,000 USDG/ETH in a single window. On a chain where a trade averages five legs, the biggest WETH leg and the biggest USDG leg are usually different hops of a route — WETH into one pool, USDG out of another, with some third token in between. Dividing them compares two unrelated quantities. A swap is anchored by its pool. One contract receives one token and sends the other, inside the same transaction. So legs are matched by shared counterparty: if WETH went to address P and USDG came out of P, those two amounts are the two sides of one swap and their ratio is a real price. Each hop has its own pool, so multi-hop routes stop contaminating the sample instead of having to be excluded. What survives is then filtered on its own spread — median absolute deviation, not min-max, because one manipulated swap should not be able to widen the band enough to discredit an otherwise tight set. Every rate returned carries its evidence: how many pools agreed, how far apart they were, which transactions they came from. A rate you cannot audit is a guess with better manners. """ from __future__ import annotations from decimal import Decimal as D from statistics import median from fetcher import Fetcher, RpcError, TRANSFER_TOPIC # Discovered by scanning the busiest contracts on chain 4663, not hardcoded # from documentation. Verify these before trusting a report built on them. WETH = "0x0bd7d308f8e1639fab988df18a8011f41eacad73" USDG = "0x5fc5360d0400a0fd4f2af552add042d716f1d168" WETH_DECIMALS = 18 USDG_DECIMALS = 6 # not 18 — getting this wrong moves the rate by 10^12 MIN_WETH = D("0.02") # ignore dust legs; their ratios are noise MIN_USDG = D("20") MAD_LIMIT = D("3.5") # reject a sample this many deviations out # These are two of the busiest contracts on the chain: ~3,700 WETH legs and # ~4,100 USDG legs per 600 blocks. A window of a few thousand blocks blows # straight through the node's 10,000-log ceiling, so the default starts small # and the search narrows on refusal rather than widening into it. # # How small was measured, not guessed (13 Sep 2026, three blocks across the # chain's life). A half-width of 300 pulled 2.5-5.1 MB per attempt and kept # 198-1,039 samples, to satisfy a minimum of three. A half-width of 25 pulled # 0.18-0.39 MB and kept 11-65, with a median within 0.003-0.016% of the wide # window's -- inside the spread either window reports. The wide default was # paying thirteen times the bandwidth for no precision, and narrowing it halved # a full-history run over a real wallet, 276s to 134s. A quiet window still # widens by 3x until it has its three samples, exactly as before. DEFAULT_WINDOW = 25 MIN_WINDOW = 5 MIN_SAMPLES = 3 # Swaps further apart than this are not a price. Measured medians are ~0.03% # inside a 5-second window, with 0.1% at the 90th percentile; the worst window # found (router legs matched as if they were pools) spread 76% and priced ETH # a quarter low. MAX_SPREAD = D("0.03") def _amount(log: dict, decimals: int) -> D: data = log.get("data") or "0x" raw = int(data, 16) if data != "0x" else 0 return D(raw) / (D(10) ** decimals) def _legs(f: Fetcher, address: str, lo: int, hi: int, decimals: int): """Every Transfer of one token in a range, keyed by transaction. Returns None when the node refuses the window rather than an empty list. Conflating "too many logs" with "no trades here" is what made the first version report that the chain had no ETH price at all. """ r = f.paced_logs({"fromBlock": hex(lo), "toBlock": hex(hi), "address": address, "topics": [TRANSFER_TOPIC]}) if isinstance(r, Exception) or len(r) >= 10_000: return None out: dict[str, list] = {} for log in r: t = log.get("topics") or [] if len(t) < 3: continue amount = _amount(log, decimals) if amount <= 0: continue out.setdefault(log["transactionHash"], []).append( ("0x" + t[1][-40:].lower(), "0x" + t[2][-40:].lower(), amount)) return out def sample_rates(f: Fetcher, centre: int, window: int = DEFAULT_WINDOW, weth: str = WETH, usdg: str = USDG, weth_decimals: int = WETH_DECIMALS, usdg_decimals: int = USDG_DECIMALS): """Every WETH/USDG rate implied by a pool near `centre`. None means the node refused the window; an empty list means it answered and no pool traded the pair. The caller must be able to tell those apart. """ lo, hi = max(0, centre - window), centre + window w = _legs(f, weth, lo, hi, weth_decimals) if w is None: return None u = _legs(f, usdg, lo, hi, usdg_decimals) if u is None: return None out = [] for tx, wl in w.items(): ul = u.get(tx) if not ul: continue # Total each side moved per counterparty. A pool that took WETH and # paid out USDG in the same transaction just quoted a price. weth_in: dict[str, D] = {} weth_out: dict[str, D] = {} for frm, to, amt in wl: weth_in[to] = weth_in.get(to, D(0)) + amt weth_out[frm] = weth_out.get(frm, D(0)) + amt usdg_in: dict[str, D] = {} usdg_out: dict[str, D] = {} for frm, to, amt in ul: usdg_in[to] = usdg_in.get(to, D(0)) + amt usdg_out[frm] = usdg_out.get(frm, D(0)) + amt for pool in set(weth_in) | set(weth_out): # WETH in, USDG out: the pool bought ETH. wi, uo = weth_in.get(pool, D(0)), usdg_out.get(pool, D(0)) if wi >= MIN_WETH and uo >= MIN_USDG: out.append({"tx": tx, "pool": pool, "weth": wi, "usdg": uo, "rate": uo / wi, "side": "sold"}) continue # USDG in, WETH out: the pool sold ETH. wo, ui = weth_out.get(pool, D(0)), usdg_in.get(pool, D(0)) if wo >= MIN_WETH and ui >= MIN_USDG: out.append({"tx": tx, "pool": pool, "weth": wo, "usdg": ui, "rate": ui / wo, "side": "bought"}) return out def _reject_outliers(samples): """Median absolute deviation, which one bad swap cannot widen. A min-max band lets a single manipulated trade make a tight set look worthless; MAD asks how far the typical sample sits from the middle. """ if len(samples) < 4: return samples, 0 rates = sorted(s["rate"] for s in samples) mid = D(str(median(rates))) devs = sorted(abs(r - mid) for r in rates) mad = D(str(median(devs))) if mad == 0: return samples, 0 kept = [s for s in samples if abs(s["rate"] - mid) <= MAD_LIMIT * mad] return (kept, len(samples) - len(kept)) if len(kept) >= 3 else (samples, 0) def derive_native_price(f: Fetcher, block: int, window: int = DEFAULT_WINDOW, attempts: int = 7): """The ETH rate near a block, with the evidence behind it. Narrows when the node refuses the window, widens when the window was quiet or its swaps disagree. Returns None rather than a lonely number when the chain has nothing consistent to say: an unpriced leg is an honest gap, a fabricated rate is a wrong tax return. """ w = window for _ in range(attempts): samples = sample_rates(f, block, w) if samples is None: if w <= MIN_WINDOW: return None w = max(MIN_WINDOW, w // 3) continue if len(samples) >= MIN_SAMPLES: kept, dropped = _reject_outliers(samples) rates = sorted(s["rate"] for s in kept) mid = D(str(median(rates))) low, high = rates[0], rates[-1] spread = (high - low) / mid if mid else D(0) if spread > MAX_SPREAD: w *= 3 # disagreement is not a price: look wider continue return { "rate": mid, "samples": len(kept), "rejected": dropped, "pools": len({s["pool"] for s in kept}), "low": low, "high": high, "spreadPct": float(spread * 100), "blockCentre": block, "windowBlocks": w, "confidence": "high" if spread < D("0.01") else "medium", "evidence": [s["tx"] for s in kept[:5]], "source": "WETH/USDG matched across a shared pool", } w *= 3 return None class PriceOracle: """Caches derived rates per block bucket, so a scan of many transactions does not re-query the same neighbourhood over and over.""" def __init__(self, fetcher: Fetcher, bucket: int = 200_000): self.f = fetcher self.bucket = bucket self.cache: dict[int, dict | None] = {} self.misses = 0 self.reasons: dict[int, str] = {} def at(self, block: int) -> dict | None: key = block // self.bucket if key not in self.cache: try: got = derive_native_price(self.f, key * self.bucket + self.bucket // 2) reason = None if got else "no agreeing pools" except RpcError as e: # An unpriced window is a state the report already carries # honestly: its legs are exported as UNPRICED and listed. A scan # that dies here throws away every transfer it has already # proven. The transfer sweeps get no such leniency -- a missing # transfer is a wrong report, not a gap. got, reason = None, f"node unavailable ({e})" self.cache[key] = got if got is None: self.misses += 1 self.reasons[key] = reason return self.cache[key] def rate_at(self, block: int) -> D | None: got = self.at(block) return got["rate"] if got else None def summary(self) -> dict: got = [v for v in self.cache.values() if v] return { "buckets": len(self.cache), "resolved": len(got), "unresolved": self.misses, "unresolvedReasons": sorted(set(self.reasons.values())), "bucketBlocks": self.bucket, "rates": [{"block": v["blockCentre"], "rate": str(v["rate"]), "samples": v["samples"], "confidence": v["confidence"], "spreadPct": round(v["spreadPct"], 2)} for v in got], }