Skip to content

[MET-570] Extend circulating supply: exclude non-circulating holders#18

Draft
metajinglun wants to merge 6 commits into
masterfrom
feature/MET-570-futarchy-external-api
Draft

[MET-570] Extend circulating supply: exclude non-circulating holders#18
metajinglun wants to merge 6 commits into
masterfrom
feature/MET-570-futarchy-external-api

Conversation

@metajinglun

Copy link
Copy Markdown
Contributor

Summary

Extends the circulating-supply calculation with a dynamic, config-driven mechanism to
exclude operator-vetted non-circulating holders — external wallets (e.g. Laso),
vesting contracts, and other encumbered/protocol-owned holdings that are not "in the
hands of others."

Ticket asks addressed:

  • ✅ Exclude treasury owned tokens — already implemented (daoTreasuryTokens), unchanged
  • ✅ Exclude performance package tokens — already implemented (teamPerformancePackage), unchanged
  • ✅ Exclude external tokens earmarked for vesting/encumbered state — new
  • ✅ Exclude tokens in vesting contracts — new
  • ⏸️ Exclude protocol-owned pool tokens — see Deferred below

How it works

New env var EXCLUDED_CIRCULATING_WALLETS: comma-separated mint:wallet or
mint:wallet:label. For the queried mint, each configured holder's live on-chain
balance
(summed across all of the owner's token accounts for that mint, via
getParsedTokenAccountsByOwner) is subtracted from circulating supply and echoed back
under allocation.excludedHolders on /api/supply/:mint/circulating.

Scoped per-mint so only the vetted (mint, wallet) balance is ever excluded — a wallet
holding a circulating balance of an unrelated token is never wrongly subtracted.

Design invariants (match the existing supply path)

  • Fail loud, never quietly wrong: an RPC/infra failure propagates (→ 5xx); an
    unreadable parsed account throws rather than counting as 0. A silent 0 would overstate
    circulating supply — the exact mispricing this codebase guards against.
  • Zero behavior change when unconfigured: no configured holder for a mint ⇒ no extra
    RPC, identical output.
  • Fail fast on bad config: malformed EXCLUDED_CIRCULATING_WALLETS entries throw at
    startup (a dropped exclusion would overstate circulating); exact mint:wallet
    duplicates are de-duped to avoid double-subtraction.

Tests

bun run typecheck, bun test (136 pass), bun run deps:check all green. New coverage:
config parsing (valid/malformed/dedupe), getExcludedHolderBalances (multi-account sum,
zero, RPC-failure propagation, fail-loud on bad shape, cross-mint skip),
getSupplyInfo subtraction/clamp, and the route mapping/response.

Deferred / follow-ups (not blockers for this increment)

  • Meteora DAMM v2 fractional pool ownership: the @metadaoproject/programs SDK ships
    no DAMM v2 position decoder, and concentrated-liquidity share ≠ simple liquidity ratio.
    Hand-decoding position NFTs for a financial calc is too risky to land autonomously
    without live verification. Interim lever: list a protocol pool-position holder in
    EXCLUDED_CIRCULATING_WALLETS.
  • Pre-existing (separate ticket recommended): getSupplyInfo subtracts
    additionalTokenAllocation regardless of its claimed flag, while
    launchpadService.totalNonCirculating only counts it when !claimed. Correcting it
    also requires reworking the RNGR add-back (project memory records the RNGR hardcode as
    intentionally correct), which depends on RNGR's on-chain additionalTokensClaimed
    value — not verifiable here. Left untouched; this feature does not double-subtract on
    its own.
  • DB persistence / daily snapshots: explicitly out of the initial scope per the ticket.

🤖 Generated with Claude Code

metajinglun and others added 6 commits July 21, 2026 17:43
Add a config-driven mechanism to exclude external/vesting/encumbered wallet
holdings from circulating supply. EXCLUDED_CIRCULATING_WALLETS lists per-mint
(mint:wallet[:label]) holders whose live on-chain balance is subtracted from
that mint's circulating supply and echoed under allocation.excludedHolders.

Mirrors the existing daoTreasuryTokens path (derive ATA -> live balance ->
subtract) and its error contract: an absent token account is 0, any RPC failure
propagates (never a silent 0 that overstates circulating supply). No behavior
change and no extra RPC when no holders are configured for the mint.

Meteora DAMM v2 fractional pool-ownership and DB persistence are deferred
(see PLAN.md); a protocol pool-position holder can be excluded via config today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sum ALL of a holder's token accounts for the mint via
  getParsedTokenAccountsByOwner (not just the ATA) so multiple/non-associated
  accounts can't under-subtract and overstate circulating supply.
- Fail fast: malformed EXCLUDED_CIRCULATING_WALLETS entries now throw at startup
  instead of being silently skipped (a dropped exclusion overstates circulating).
- Add direct getExcludedHolderBalances coverage: multi-account sum, zero balance,
  RPC-failure propagation, and cross-mint skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Throw on an unreadable parsed token-account shape (missing/non-numeric
  tokenAmount.amount) instead of silently counting it as 0, which would
  overstate circulating supply.
- Include per-holder address/amount/label in the getSupplyInfo cache key so two
  distinct holder sets sharing an aggregate total can't return stale
  allocation.excludedHolders detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A duplicated EXCLUDED_CIRCULATING_WALLETS entry would resolve and subtract the
same live balance twice, understating circulating supply. Dedupe on mint:wallet
(first occurrence wins); per-mint scoping is preserved so the same wallet can
still be listed for different mints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ts (MET-570)

- Route test now captures the allocation forwarded to getSupplyInfo and asserts
  the mapped {amount,address,label}, guarding the supplyWithLaunchpadAllocation
  wiring against regressions.
- Reset config.circulating.excludedHolders in a file-level beforeEach so the
  no-launch/incomplete-launch cases never depend on ambient env config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…havior

The circulating endpoint subtracts the additional-token allocation as the code
currently does (regardless of claimed); drop the "unclaimed" qualifier so the
docs don't assert behavior the pre-existing code doesn't implement. The
claimed-vs-unclaimed correction is tracked as a separate pre-existing follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Package Protection

  • Lockfile + Socket scan: pass
  • Repo guard: pass
  • Phantom deps: pass

Repository Guard

Exact dependency versions

  • Status: pass
  • All dependencies, devDependencies, optionalDependencies, and peerDependencies use exact versions or workspace:*.

Package minimum age

  • Status: pass
  • All pinned external packages are at least 14 days old.

Sensitive wallet / program changes

  • Status: warn
  • Suspicious wallet routing, signing-path, or program-ID changes detected. This is a review hint, not a merge gate — CODEOWNERS + branch protection enforce the actual review requirement. Please take a closer look at the lines below.
  • High-sensitivity files touched: src/config.ts, src/services/solanaService.ts, src/services/launchpadService.ts
  • Matching diff lines:
  • example.env:54 Wallet or destination routing change -> +# encumbered/protocol-owned holdings — e.g. Laso's external wallet). Subtracted from
  • example.env:56 Wallet or destination routing change -> +# Comma-separated entries, each :or::.
  • example.env:57 Wallet or destination routing change -> +# Scoped per-mint so only the vetted (mint, wallet) balance is excluded. Default: none.
  • example.env:58 Wallet or destination routing change -> +# EXCLUDED_CIRCULATING_WALLETS=<mint>:<wallet>:vesting
  • src/config.ts:4 Wallet or destination routing change -> + * A wallet whose live on-chain balance of a specific mint is treated as
  • src/config.ts:10 Wallet or destination routing change -> + /** Base mint whose balance held by wallet is excluded from circulating supply. */
  • src/config.ts:12 Wallet or destination routing change -> + /** Wallet (owner) address that holds the encumbered tokens. */
  • src/config.ts:13 Wallet or destination routing change -> + wallet: PublicKey;
  • src/config.ts:21 Wallet or destination routing change -> + * Format: comma-separated entries, each : or
  • src/config.ts:22 Wallet or destination routing change -> + * ::. Whitespace is trimmed and blank entries (e.g. a
  • src/config.ts:26 Wallet or destination routing change -> + * A malformed NON-blank entry (missing mint/wallet, invalid base58 pubkey)
  • src/config.ts:33 Wallet or destination routing change -> + // Dedupe by mint:wallet — a duplicated env entry (copy/paste) would otherwise be
  • src/config.ts:44 Wallet or destination routing change -> + const wallet =
  • src/config.ts:51 Wallet or destination routing change -> + if (!mint || !wallet) {
  • src/config.ts:53 Wallet or destination routing change -> + Invalid EXCLUDED_CIRCULATING_WALLETS entry "${trimmed}" — expected ":" or "::",
  • src/config.ts:58 Wallet or destination routing change -> + // supply path compares mints) and wallet as a PublicKey for lookups.
  • src/config.ts:59 Public key construction change -> + new PublicKey(mint);
  • src/config.ts:60 Wallet or destination routing change; Public key construction change -> + const walletKey = new PublicKey(wallet); // throws if invalid
  • src/config.ts:61 Wallet or destination routing change -> + const dedupeKey = ${mint}:${wallet};
  • src/config.ts:64 Wallet or destination routing change -> + holders.push({ mint, wallet: walletKey, label });

Overall status: pass

Transitive supply-chain threats are covered by the Socket scanner (via @socketsecurity/bun-security-scanner in bunfig.toml), which runs on every bun install. This guard blocks merge on direct-dep pinning and age policy; the sensitive-diff section is a review hint, not a merge gate (CODEOWNERS handles the actual review requirement).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant