Docs·Metrics reference

Metrics reference

THD and the fee meter

The protocol fee meter, the unlock it drives, the eleven buckets, and why an unlock percentage is meaningless without its denominator.

Version
1.3
Updated
2026-09-29
Source
Metric definitions v1.2 · Calibration — shared ratios

protocolFeeMeterWei — the meter#

protocolFeeMeterWei
unitwei
derivationProtocolFeeSplitter.totalProtocolFeesReceived()
sourcecontracts/src/launchpad/ProtocolFeeSplitter.sol
freshnessLive read. Monotonic — two write sites, both +=, no setter, no delete.
coverageFULL, but incomplete as a revenue figure. See below.
Published beside the meter, always
idReadWhy it must be visible
protocolFeeLifetimeWeilifetimeFeesReceived()THE revenue figure. Every wei from every armed payer, ever. No ceiling throttles it and finalise() does not freeze it.
protocolFeeRefusedByCeilingWeitotalRefusedByCeiling()Arrived, paid out in full, refused by the count for good. Monotonic. Replaced protocolFeePendingWei on 21-09-2026.
protocolFeeDailyWeidailyFeesWei(day), keyed timestamp / EPOCH_SECONDSArmed-payer receipts per UTC day. Drives the Spoils daily release, so it keeps recording after finalise().
protocolFeeUncountedWeitotalUncounted()Wei from unregistered senders, or received after finalise(). Never counted, by design.
protocolFeeCountedRemainingTodaycountedRemainingThisEpoch()How much of today’s ceiling is left.
protocolFeeFinalisedfinalised()One-way; every later receipt routes uncounted.

Both figures are monotonic and neither has a downward path — no setter, no delete, and no write-down function since discardPending(uint256,string) was removed on 21-09-2026. A published revenue figure that fell would be a bug in the publisher, never a fact about the chain.

Each receipt is one FeeRouted(from, counted, amount, toLiquidity, toTreasury, meterAfter, label), and meterAfter is the meter at that log — which makes the meter’s whole history re-derivable without a single archive state read. That matters on a chain whose public RPC keeps only 6,555 blocks of state — about thirty minutes on testnet 46630 and about eleven on mainnet 4663.

The unlock the meter drives#

// THDEmissionControl.totalUnlocked()
effectiveFees = min(feeSplitter.totalProtocolFeesReceived(), FEES_TO_FULL_EMISSION);
unlocked      = effectiveFees × 1_000_000_000e18 / FEES_TO_FULL_EMISSION;

thdUnlockBps  = unlocked × 10_000 / 1_000_000_000e18;
idRead
thdUnlockedBaseUnitstotalUnlocked()
thdRemainingToUnlockremainingToUnlock()
thdFeesToCompletionfeesToCompletion() — ceiling division
thdEmissionCompleteisComplete() — totalUnlocked() >= TOTAL_SUPPLY
thdEmissionCompletedAtemissionCompletedAt() — 0 until stampCompletion()
thdOutstandingBaseUnitstotalOutstanding() — accrued and unclaimed across the eleven buckets

There is no per-unlock event. The unlock is a pure function of the meter, so the event that records it is ProtocolFeeSplitter.FeeRouted.meterAfter.

thdSupply — total and circulating#

THD mints its entire supply to the emission contract at genesis and has no mint function, so:

totalSupply      = THD.totalSupply()                    // 1e27 minus every burn
circulating      = totalSupply
                 − THD.balanceOf(emission)              // never claimed by a bucket
                 − Σ THDMerkleDistributor.unclaimedBalance()
                 − Σ (THDVestedClaim.totalAllocation() − released)
                 − Σ THDImmediateClaim.claimable()

The bucket destinations come from the addresses file’s thdBuckets[]. There is no totalEmitted on the emission contract: total claimed is Σ claimedOf(bucket) over the eleven, or Σ Claimed.amount from logs.

The eleven buckets#

BUCKET_COUNT is 11 on every deployed THDEmissionControl, mainnet included. See the eleven allocations for what each is for and how it pays.

Immutable on the emission contract, with no setter
BucketbpsTHD
Traders2200220,000,000
Creators1500150,000,000
KOLs / Referrers1700170,000,000
Liquidity1200120,000,000
LP Providers1000100,000,000
Spoils1100110,000,000
BD / Marketing40040,000,000
Ecosystem Grants30030,000,000
Insurance Backstop30030,000,000
Team20020,000,000
Settlers10010,000,000

A shareBps read on chain that disagrees with the addresses file means the file describes a different deployment, not drift. A bucket whose destination reads as the zero address is a recorded fact — some destinations are deliberately left unset until the Safe that will hold them exists, and those buckets refuse to pay until then.

The THD/ETH pool#

idRead
thdPoolFeesToTreasuryWeiThdLiquidityManager.totalEthToRouter
thdPoolFeesFromPoolWeiThdLiquidityManager.totalEthFromPool
thdRoutedByPoolBaseUnitsThdLiquidityManager.totalThdToRouter — sent, not yet destroyed. The burn count is ThdFeeRouter.totalThdBurned, beside the burn.

Every collection is one FeesRouted(caller, ethToRouter, thdToRouter, router). Both legs go in full to the immutable ThdFeeRouter, which holds them until anybody flushes: the ETH on to thdPoolTreasury, the THD burned or recycled to ThdLpRewards according to the monthly holder vote. The ETH is not a protocol fee, it never passes the splitter, and it must never appear in feesProtocolWei.

The contract publishes an identity every balance figure must satisfy:

address(this).balance == totalEthReceived + totalEthFromPool − totalEthToPool − totalEthToRouter

Metering the pool’s fees would let THD trading unlock THD, which is why they are excluded structurally rather than by policy. See fees, by leg.