feat: who the wormhole exits pay, and how much of it is the protocol itself
The premise was that pools are the best surface for observing wormhole transfers, because payouts must leave a pattern in the batches. The pattern is there. Most of it is not payouts. Two things break the obvious reading. Batch *size* is aggregation: the pallet documents a public batch as one segment per inner private batch and a segment as one client, so a 120-output batch is an aggregator bundling unrelated people. And the most recurrent recipient is the aggregator itself — one address is credited in 91.5% of every public batch anyone submitted and in none of the private ones, which is the rebate minted to the proof's `aggregator_address`. QUANPOOL's mining address appears in 501 batches for 7.54 QTC total, 0.015 each: the fee share paid to whoever included the batch. Classified, three addresses take 76% of everything the exits pay. A cohort table that did not say so would be describing aggregator plumbing and calling it an economy, so the share is the section's eyebrow and its first sentence. What survives the subtraction is the finding worth shipping: among genuine recipients, 35 addresses paid twenty or more times take two thirds of the remaining value at twice everyone else's average credit. That is a recurring-payment shape and the page says it is no more than that — the chain records no sender, so a regular payee is as plausibly an exchange deposit or one person on a schedule as a pool meeting its obligations. Batch size is not rendered anywhere, on purpose. Closes #20 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BigUintDec } from "./BigUintDec";
|
||||
|
||||
/**
|
||||
* Credits out of the wormhole, grouped by what the address appears to be.
|
||||
*
|
||||
* This exists because the obvious reading of exit batches is wrong. Grouping
|
||||
* them by producing extrinsic gives a shape that looks exactly like payroll —
|
||||
* one sender, many recipients, repeating — and almost all of it is the
|
||||
* protocol's own plumbing:
|
||||
*
|
||||
* - an **aggregator's** rebate is minted on every bundle it submits, so one
|
||||
* address is credited in 91.5% of every public batch anyone sent, and three
|
||||
* such addresses take **76% of everything the exits pay**;
|
||||
* - a **miner's** fee share goes to whichever block author included the batch,
|
||||
* which is frequent and tiny — 0.015 QTC a time.
|
||||
*
|
||||
* Batch size is not reported at all, for the same reason: the pallet documents
|
||||
* a public batch as one segment per inner private batch and a segment as one
|
||||
* client, so a 120-output batch is an aggregator bundling unrelated people.
|
||||
* It measures aggregator throughput, not a payer's generosity.
|
||||
*
|
||||
* What survives the subtraction is real: among genuine recipients, 35
|
||||
* addresses paid twenty or more times take two thirds of the remaining value.
|
||||
* That is a recurring-payment shape. It is **consistent with** pool payouts and
|
||||
* is not proof of them — the chain records no sender, so nobody can be named,
|
||||
* and a regular payee could equally be an exchange deposit or one person moving
|
||||
* their own funds on a schedule.
|
||||
*/
|
||||
export type ExitCohort = {
|
||||
/**
|
||||
* Stable identifier for the class: `infrastructure`, `miner_fee`, or one
|
||||
* of `paid_once`, `paid_a_few`, `paid_often`, `paid_regularly`. The label
|
||||
* is the frontend's to choose; this is not a display string.
|
||||
*/
|
||||
kind: string,
|
||||
/**
|
||||
* Distinct addresses in the class.
|
||||
*/
|
||||
addresses: number,
|
||||
/**
|
||||
* How many times they were credited.
|
||||
*/
|
||||
credits: number,
|
||||
/**
|
||||
* Summed amount, smallest unit.
|
||||
*/
|
||||
amount: BigUintDec, };
|
||||
@@ -1,6 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ChainId } from "./ChainId";
|
||||
import type { DailyWormhole } from "./DailyWormhole";
|
||||
import type { ExitCohort } from "./ExitCohort";
|
||||
import type { TransferRoute } from "./TransferRoute";
|
||||
|
||||
/**
|
||||
@@ -57,6 +58,10 @@ exit_mint_failed: number,
|
||||
* Every transfer route, busiest first.
|
||||
*/
|
||||
routes: Array<TransferRoute>,
|
||||
/**
|
||||
* Who the exits pay, grouped by what each address appears to be.
|
||||
*/
|
||||
cohorts: Array<ExitCohort>,
|
||||
/**
|
||||
* Notes created per day, oldest first.
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,36 @@ function routeMeaning(produced_by: string, hidden: boolean): string {
|
||||
return hidden ? 'no sender recorded' : 'an ordinary transfer, sender recorded'
|
||||
}
|
||||
|
||||
/**
|
||||
* What to call each cohort class, and what it means.
|
||||
*
|
||||
* The identifiers come from the API and are stable; the wording is this
|
||||
* page's. Kept as one table so the two machinery classes cannot drift apart
|
||||
* from the explanation that stops them being read as payouts.
|
||||
*/
|
||||
const COHORTS: Record<string, { label: string; meaning: string }> = {
|
||||
infrastructure: {
|
||||
label: 'Aggregator rebates',
|
||||
meaning: 'credited in more than a fifth of every batch anyone submitted',
|
||||
},
|
||||
miner_fee: {
|
||||
label: 'Miner fee shares',
|
||||
meaning: 'paid to whichever block author included the batch',
|
||||
},
|
||||
paid_once: { label: 'Paid once', meaning: 'a single credit, ever' },
|
||||
paid_a_few: { label: 'Paid 2–4 times', meaning: '' },
|
||||
paid_often: { label: 'Paid 5–19 times', meaning: '' },
|
||||
paid_regularly: {
|
||||
label: 'Paid 20+ times',
|
||||
meaning: 'a recurring payee — the closest thing to a payroll the chain shows',
|
||||
},
|
||||
}
|
||||
|
||||
/** Whether a class is the protocol's own plumbing rather than somebody's payee. */
|
||||
function isMachinery(kind: string): boolean {
|
||||
return kind === 'infrastructure' || kind === 'miner_fee'
|
||||
}
|
||||
|
||||
export function WormholePanel({
|
||||
chain,
|
||||
decimals,
|
||||
@@ -92,6 +122,41 @@ export function WormholePanel({
|
||||
.filter((r) => !r.sender_hidden)
|
||||
.reduce((sum, r) => sum + r.transfers, 0)
|
||||
|
||||
// What share of everything the exits pay is the protocol paying itself. The
|
||||
// headline of the cohort table, because without it the bands below read as a
|
||||
// pool ledger. BigInt throughout: these are token amounts, and a Number would
|
||||
// lose precision well before the total does.
|
||||
const machineryShare = (() => {
|
||||
let total = 0n
|
||||
let machinery = 0n
|
||||
for (const c of data.cohorts) {
|
||||
let amount: bigint
|
||||
try {
|
||||
amount = BigInt(c.amount)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
total += amount
|
||||
if (isMachinery(c.kind)) machinery += amount
|
||||
}
|
||||
if (total === 0n) return null
|
||||
return Number((machinery * 1000n) / total) / 10
|
||||
})()
|
||||
|
||||
// Mean credit, computed from the summed amount rather than sent per-row: the
|
||||
// difference between the machinery's average and a payee's is most of what
|
||||
// separates them, and rounding it away would flatten the table.
|
||||
const each = (amount: string, credits: number): string => {
|
||||
if (credits === 0) return '—'
|
||||
let total: bigint
|
||||
try {
|
||||
total = BigInt(amount)
|
||||
} catch {
|
||||
return '—'
|
||||
}
|
||||
return tokens((total / BigInt(credits)).toString(), decimals, 3)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="panel" style={{ marginBottom: 26 }}>
|
||||
@@ -212,6 +277,79 @@ export function WormholePanel({
|
||||
behaving correctly rather than the address behaving suspiciously.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="panel" style={{ marginBottom: 26 }}>
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">Who the exits pay</h2>
|
||||
<span className="eyebrow">
|
||||
public batches only · amounts in {symbol}
|
||||
{machineryShare !== null && ` · ${machineryShare}% is plumbing`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="scroll-x">
|
||||
<table className="board">
|
||||
<caption className="visually-hidden">
|
||||
Credits out of the wormhole, grouped by what each address appears to be
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="left">
|
||||
Who
|
||||
</th>
|
||||
<th scope="col">Addresses</th>
|
||||
<th scope="col">Credits</th>
|
||||
<th scope="col">Amount</th>
|
||||
<th scope="col">Each</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.cohorts.map((c) => {
|
||||
const meta = COHORTS[c.kind]
|
||||
return (
|
||||
<tr key={c.kind}>
|
||||
<td className="left">
|
||||
<div className="call-cell">
|
||||
<span>{meta?.label ?? c.kind}</span>
|
||||
{meta?.meaning ? <span className="runtime-doc">{meta.meaning}</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td className="numeral">{fmtHeight(c.addresses)}</td>
|
||||
<td className="numeral">{fmtHeight(c.credits)}</td>
|
||||
<td className="numeral">{tokens(c.amount, decimals, 1)}</td>
|
||||
<td className="numeral">{each(c.amount, c.credits)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="panel-note">
|
||||
<strong>
|
||||
Most of what leaves the wormhole is the protocol paying itself
|
||||
{machineryShare !== null && `, ${machineryShare}% of it`}.
|
||||
</strong>{' '}
|
||||
An aggregator's rebate is minted on every bundle it submits, so a handful of
|
||||
addresses appear in almost every batch anyone sent — one of them in 91.5% of them, and in
|
||||
none of the private ones. A miner's fee share goes to whoever included the batch,
|
||||
which is why a mining address can be credited hundreds of times for a few hundredths of a{' '}
|
||||
{symbol} each. Neither is anybody's payout.
|
||||
</p>
|
||||
<p className="panel-note">
|
||||
Batch <em>size</em> is deliberately not on this page. A public batch carries one segment
|
||||
per inner private batch and a segment is one client, so a 120-output batch is an
|
||||
aggregator bundling unrelated people — it measures aggregator throughput, and reading it
|
||||
as one payer paying a hundred payees is the mistake this table exists to prevent.
|
||||
</p>
|
||||
<p className="panel-note">
|
||||
What survives the subtraction is the bottom rows, and they are the real finding: a small
|
||||
number of addresses paid over and over, taking most of the remaining value at a larger
|
||||
amount each time.{' '}
|
||||
<strong>That is a recurring-payment shape, and no more than that.</strong> The chain
|
||||
records no sender, so nothing here names a payer — and a regular payee could as easily be
|
||||
an exchange deposit, or one person moving their own funds on a schedule, as a pool meeting
|
||||
its obligations.
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user