Decisions
What we chose, what we rejected, and why. One entry each, newest at the bottom.
The test for whether something belongs here: would a competent person six months from now be tempted to undo this without knowing what we knew?
Open questions live at the bottom, marked open. An open question is not a blocker — it is something we have agreed not to assume.
D1 — No keepers. Nothing off-chain may be load-bearing
Settled. The protocol is fully on-chain. No bots, no cron, no backend job it depends on. Contracts cannot wake themselves, so state transitions piggyback on whoever is already transacting: graduation runs inside the buy that crosses the threshold, fees accrue on the swap that pays them.
A backend is allowed for chat, image uploads and history. Convenience only, never consensus. If turning it off breaks the protocol, the design is wrong.
Consequence: deadline-driven mechanics are excluded, because a timer fires precisely when there
is no activity to piggyback on. See ../888/notes/V2-IDEAS.md before adding any time-based
feature.
D2 — Contracts are upgradeable behind proxies, and we keep ownership
Settled, do not reopen. Flexibility to pivot is worth more than the credibility of immutability. The objection — that an upgradeable launchpad reads as a rug risk — is real but is a social problem, and in a young ecosystem endorsement from ecosystem leaders bridges it. That is a judgement outside the code.
Consequences, non-optional: storage layout is append-only, constructors disabled in favour of
initializers, __gap on anything inherited, and every upgrade rehearsed against forked mainnet
state before it is signed.
D3 — Monorepo, with each toolchain owning its own directory
Settled. Foundry, React, Node, Rust and Docker in one repo. Each area owns its build, its
lockfile, its CI job and its README. contracts/ imports nothing from the rest of the repo;
experiments/ is imported by nothing.
Rejected: separate repositories. Cross-repo changes to a protocol and its interface are the common case here, not the exception.
D4 — make as the task interface, with yarn workspaces scoped to the JS subtree
Settled. One entry point from the root for every toolchain, and CI runs the same targets a developer runs.
just was the technically better fit — no .PHONY tax, no tab syntax, arguments. Chosen against
deliberately: make is everywhere, and reading Makefiles is a permanently transferable skill worth
acquiring. The cost is accepted, with two guardrails: .PHONY on every target without exception
(the repo has test/, docs/ and web/ directories that will collide), and POSIX-ish syntax so
macOS make 3.81 and CI's GNU make 4.x agree.
Yarn workspaces cover web/ and services/ only. Stretching them over Foundry and Cargo does
not work and is not attempted.
D5 — Branch is master, straight to trunk
Settled. One developer, so work lands on master directly. When someone else joins,
dedicated branches merge into it. Commit subjects are [type] a sentence in prose.
No Claude attribution anywhere — not in commits, not in pull requests.
Amended 16 Sep 2026 (D30). Work still lands on master, which is now also what the testnet site
serves. A prod branch serves production, and moves only by a deliberate release from master.
D6 — Keys: keystore or throwaway, never a raw private key
Settled. cast wallet import and --account for anything touching a real chain; cast wallet new throwaways for local and testnet. .env holds RPCs and API keys, never a key or a
mnemonic. Separate accounts by blast radius — deployer, owner/upgrade authority, operator — and
the owner becomes a multisig before there is meaningful TVL.
Nothing in CI holds a key that can reach a real chain.
Amended 15 Sep 2026 (D28). Keystore accounts are named <net>-arb-launchpad-<role>: test- for
Arbitrum Sepolia, main- for One, and a role per blast radius — deployer, admin, and so on. So
test-arb-launchpad-deployer. Sepolia keys are keystores too. For now the only role is deployer,
which is also the default admin — D6's separation by blast radius waits for mainnet.
D7 — Contracts never deploy from CI
Settled. A contract deploy is a decision, not a consequence of a push. Rehearsed on a fork,
costed, announced, and signed by a named keystore account. web/ and services/ may deploy
automatically; contracts may not, on any trigger.
Reaffirmed 15 Sep 2026 for Sepolia (D28). No testnet exception. Fresh deploys and upgrades alike run by hand, from the developer's machine. Repeatable means one command, not a pipeline.
D8 — Documentation: notes/ is the record, Docusaurus renders it
Amended 11 Sep 2026. Originally this deferred Docusaurus until there was a v1 worth documenting. Scaffolding it early was chosen instead, so there is somewhere obvious for the first real page to go rather than a decision to make later under pressure.
notes/ carries findings, decisions and handoffs, and remains the working record.
docs/ is one Docusaurus site with two builds. docs/public/ is written for outsiders and is
the only thing a public build contains. DOCS_INTERNAL=1 additionally mounts ../notes at
/internal — rendered, never copied, so there is exactly one source of truth and nothing to
drift.
Amended again 16 Sep 2026 (D30), and the internal build is now hosted openly. The original rule
was that it is never hosted, because an authenticated internal site is infrastructure maintained for no
one. It is hosted now, at richcat-launchpad-docs.vercel.app/internal, built from master by
docs/scripts/vercel-build.sh. Deliberately without Deployment Protection — the developer chose
open over a login, having been shown what that means.
So notes/ is published writing. Every decision, finding, address and piece of strategy in it is
readable by anyone with the link and indexable by search engines. It was checked for keys, mnemonics
and credentials before the first deploy and carries none; keep it that way, and write anything that
should not be public somewhere else. notes/scratch/ is gitignored and never built.
The production project (prod) builds the public half alone, so nothing there depends on this.
Amended 20 Sep 2026: the internal site is password-protected, and notes/ stops being published
writing. Deployment Protection on the internal Vercel project only; notes/ does not move and
nothing about how we write changes. The reason is D39 — v1 pairs against RHUB and does not disclose
that RHUB is ours, which notes/ currently says on nearly every page. The GitHub repository is
private, so the hosted site was the only public leak.
Not urgent, and deliberately so. The developer's judgement, 20 Sep 2026: the site as it stands
today is not a problem — nobody is reading it, and nothing in it is dangerous so much as premature.
So this is the rule from here, applied when the staging environment is set up (PLAN.md phase 4),
not an incident to be handled tonight.
The public half is unaffected and stays open. notes/scratch/ remains gitignored either way.
Amended 21 Sep 2026: deferred to the design release, and the method above does not work.
Deferred by the developer, deliberately — the gate ships bundled with the redesign rather than on
its own. Unchanged: nothing in notes/ is dangerous so much as premature.
But "Deployment Protection on the internal Vercel project only" would not have protected
anything, and that is worth knowing before anyone implements it. Docusaurus serves each page's
prose from a content chunk under /assets/js/, outside any /internal path, and main.js — which
is public — carries the route table naming every notes page, so the chunk URLs are discoverable
rather than guessable. Demonstrated 21 Sep: notes/THREAT-MODEL.md was readable in full at
/assets/js/50f75c5f.88df5e88.js, 200 and unauthenticated.
So a path matcher is not the mechanism. Either the whole deployment is gated, or the two halves
become two Vercel projects — public building docs/public/ alone, internal building notes/
and gated wholesale — because separate deployments are the only thing that gives them separate
asset namespaces. The second is the shape to build when the public half genuinely goes public.
D9 — Testnet is a rehearsal, not a habit
Settled. Correctness is proved locally and on a pinned fork, which is faster and stricter. Arbitrum Sepolia exists for what a fork cannot rehearse: real wallets, real block times, explorers, indexers, aggregators, and the frontend against a chain it does not control.
We go there when the thing under test is integration with the world.
Amended 15 Sep 2026 (D28). Arbitrum Sepolia is now a standing environment the interface points at, kept current rather than visited per milestone. Unchanged: correctness is proved locally and on a fork first — nothing reaches Sepolia without a green local suite.
D10 — Seed contracts/ from the RHUB contracts
Settled in principle, not yet executed. The RHUB curve, inline graduation and FeeVault
seam are live on Arbitrum One and proved by use. Start from that code and change it deliberately
rather than from forge init.
Not a law — wrong-shaped code copied forward is worse than a blank file. Revisit per component.
D11 — Session continuity is mechanised, not remembered
Settled. Sessions are scoped, run for hours, are never compacted, and are sometimes resumed days later. A handoff that depends on someone remembering to write it will eventually not be written.
So: a SessionStart hook puts notes/STATUS.md and the open threads into context automatically,
and /handoff closes a thread and opens the next one's brief. notes/sessions/<scope>.md holds
what is in flight; notes/STATUS.md holds where the project stands.
D12 — A beacon per implementation version, so an upgrade never reaches a live launch
Settled. Curves are per launch and upgradeable, but an upgrade must only ever affect launches made after it. So each implementation release gets its own beacon, the factory points new launches at the newest one, and a launch is bound for life to the beacon that was current when it launched. New code cannot reach old money.
This is a refinement of D2, not a reopening of it: still upgradeable, still owned.
Rejected: one beacon for everything, the usual shape — a single upgrade rewrites every live curve mid-raise, which is exactly the thing we are ruling out. Rejected: plain clones, which give the same version isolation more cheaply but leave no way to fix a curve that is already live and holding money. A per-version beacon keeps one emergency hatch per cohort instead of none.
Consequence, non-optional: the curve's parameters are written once at initialization and have
no setter in any implementation. An upgrade may add functions; it may not re-solve the curve
under people who have already bought. Without that rule the emergency hatch is also a way to move
c mid-raise, and virtualReserve = threshold x (1-c)/(2c-1) stops being a guarantee.
D13 — The curve's shape: one knob, protocol-set, two-sided, no anti-snipe in v1
Settled. Four calls that together fix what BondingCurve is.
One shape parameter. RHUB's family, proven live: the curve sells its whole allocation, the
residue of supply seeds the pool, and the entire raise goes into the pool. The split c is the
only thing that changes the shape — rise = (c/(1-c))^2, and threshold and supply are pure
scale. Rejected: ../888's two-knob family, which buys independent steepness and pool depth at
the cost of a share of the raise that no longer seeds the pool and has to be given to somebody.
Parameters are protocol-set per launch, and live in factory config. The factory holds a
mutable config; each launch snapshots it at initialize and the curve has no setters. So
retuning affects future launches only — the same rule as D12, reached without a new
implementation. Only a code change needs a new beacon.
Amended 12 Sep 2026: the creator also picks the pairing, from an allowlist. The original wording
gave a creator only a name and a symbol, which made "pair your token with a stock" something the
protocol chose on their behalf — the opposite of the product. So launch() takes a quote asset, and
the factory refuses anything the owner has not registered.
Configuration is therefore keyed by quote, because the economics genuinely differ per asset: a threshold is denominated in the quote's own decimals, so 69,000 ARB, 20 WETH and 1 wBTC are all sensible and one number for all three is not. The migrator is per asset too, since a pool's LP fee and tick spacing are immutable inside it (D16) and a stock pair should not inherit an ARB pair's.
An allowlist rather than anything-goes, because an arbitrary quote is dangerous in ways the curve cannot detect: fee-on-transfer breaks Uniswap v4 settlement outright, a rebasing quote leaves the singleton holding surplus nobody can account for, and a honeypot traps every buyer on the curve with no exit. pons curates for the same reason; par instead requires a routable liquidity path. Stocks have to be onboarded one at a time regardless.
Selling back to the curve is allowed, with a fee. A launch that stalls below the threshold must still have an exit. The cost is a sell path with its own rounding discipline, which RHUB already has.
No anti-snipe in v1. Deliberate, and it has a permanent consequence worth writing down:
anti-snipe has to live inside buy(), so under D12 it cannot be retrofitted. Every launch made
before it ships is permanently unprotected. pons's decaying tax (99% to 0 over 5 seconds, buys
only, exempt addresses fixed at creation) is the shape to copy when it does.
D14 — A basket is a wrapper composed once, redeemed pro rata, and never rebalanced
Settled. Composable pairing — $MEME against 20% AAPL / 50% NVDA / 30% TSLA — is an ERC-20
wrapper holding the constituents, because a v4 pool holds exactly two currencies.
The percentages are converted to units at creation and never change. The creator states the weights by value; we solve them into a fixed number of units of each stock once, and that is the basket's composition. Mint and redeem are pure arithmetic — needing no price feed and no keeper, with arbitrage against the constituents holding the peg.
Amended 11 Sep 2026: mint and redeem are pro rata of what the basket actually holds, not a fixed unit list forever. The initial units set only the first deposit; after that one basket is a share of the holdings, the way a vault share is. Nothing rebalances and no new dependency appears — but three problems disappear for free:
- an in-kind dividend that arrives simply accrues to basket holders instead of being stuck;
- a rebasing rToken is absorbed by the wrapper, which is what stops it breaking the v4 pool. The PoolManager settles by balance delta, so a constituent whose balance moves out of band would otherwise leave unaccounted surplus in the singleton;
- stray transfers stop being trapped.
Originally written as fixed units forever. Do not reimplement that — it is the version that traps dividends, and the reason it looked simpler is that the problems it causes are all somewhere else.
The value weights therefore drift as prices move: if NVDA doubles it becomes a larger share of the basket. That is what an unrebalanced index does, and the interface has to say so plainly.
Rejected: fixed value weights, which is what most people assume the percentages mean. Holding 20/50/30 by value needs a price oracle and something that trades to restore it — an oracle and a keeper, both off-chain and both load-bearing. D1 forbids exactly that, and there is no piggyback trick available: a drifted weight is not an event anyone is transacting against.
Composition rules: the creator composes freely from the onboarded universe, bounded at at most 10 constituents and at least 10% each. Amended by D21: weights are given in basis points against owner-set reference units, which is what makes the floor enforceable. The protocol also publishes curated baskets as the default path in the interface. Same contracts either way.
The restriction only bites on mint and redeem. The basket is an unrestricted ERC-20 of our own
wrapping assets that may not be — so trading MEME/BASKET needs no permission from anybody, and
only creation and redemption are limited to parties allowed to hold the underlying. That is the
authorised-participant shape an ETF uses: retail never touches the restricted asset, and whoever is
allowlisted holds the peg.
A launched pool is welded to its basket forever, because the basket is a currency in the
PoolKey. A second version of a basket is a new basket for new launches, never a migration — the
same cohort rule as D12.
Still blocked on one binary answer: can a smart contract hold rTokens at all? If not, this
whole shape dies regardless of what we build. notes/references/reality-rtokens.md.
D15 — The graduated pool's shape, decided now and built later
Superseded in part by D32: the hook now charges, and it is per launch.
Settled in shape, deferred in code. v1 is a minimal launchpad and the pool layer is not being built yet. These are recorded so the decision is not remade from scratch, and because the research behind them was expensive.
An observing hook, not a charging one. beforeInitialize plus afterSwap, with no
return-delta flags. Fees are ordinary LP fees on the locked position; the hook exists so per-swap
behaviour — gacha packs minted on the swap that paid for them, per-swap scoring — is possible at
all, since a pool can never gain a hook later.
Uniswap's routing rule, confirmed: a form is required only if the hook uses a delta flag, has an
address starting 0x91, or targets major pairs. Everything else is auto-allowlisted. So this
shape is quotable by default, which ../launchpad is not — RHUB sets afterSwapReturnsDelta and
has been in the review queue since 7 Sep 2026 with no answer.
Two constraints that follow, both easy to lose:
- Never mine the hook address to start with
0x91. A salt search will find one eventually and it drops the pool into the review queue for no reason. - Hooks that need review must not be upgradeable through a proxy. Only bites if we trigger review, but it is why the next decision matters.
A fixed LP fee, chosen per launch from factory config — not a dynamic fee. Dynamic fee was the first choice and was amended before anything was written: RHUB's submission form put it in the same checkbox as delta flags, so it may drag an otherwise auto-allowlisted pool into the review queue, where an upgradeable hook is refused outright. A per-launch fixed fee gets the same benefit — retune between launches, learn from what happens — because factory config is snapshotted at launch anyway (D13). What is given up is retuning a live pool, which D12 says we should not do.
It also removes a trap: a dynamic-fee pool opens at 0% until the hook sets a rate, so it would have
needed afterInitialize purely to avoid free swaps at launch.
A locker with no withdrawal path holds the position. No function removes liquidity or transfers it out; fee collection is a zero-liquidity decrease. pons and par both ship this, and RHUB does the same thing inside the curve.
Creator and protocol split pool fees on fixed terms set by us and snapshotted per launch. A creator-chosen surcharge is what pons and par offer and may follow; it is not v1.
Still open: whether the hook itself is upgradeable. RHUB's is CREATE2 with no proxy and no
delegatecall, deliberately; D2 says otherwise; and Uniswap refuses upgradeable hooks that need
review. Decide when the hook is written.
D16 — The hook is a parameter of the migrator, and the pool is opened at launch
Settled. Two things, and the first dissolves a question that looked like an ordering problem.
Pool parameters live in the migrator, immutable. Hook address, LP fee and tick spacing are set at the migrator's construction, and a curve snapshots which migrator it uses at initialization. So a new hook is a new migrator, pointed at by factory config, reaching new launches only — the same cohort rule as D12, with no new plumbing. The migrator is therefore replaced, not upgraded: an upgradeable one could change the pool shape a launch was already promised.
So the migrator is built first with hook = address(0), and the hook drops in later without one
line of the migrator changing. notes/ARCHITECTURE.md listed L3 as an upgradeable singleton; it is
a replaceable one.
The pool is initialized at launch, not at graduation. This is a security fix, not tidiness.
PoolManager.initialize has no access control, re-initializing reverts with
PoolAlreadyInitialized, and every field of our PoolKey — token, quote, fee, tick spacing, hook —
is known the moment a token launches. So anyone could initialize our pool at a garbage price at any
time before graduation, and then graduation's own initialize reverts forever: the curve locks, the
retry always fails, and the launch is permanently stuck holding the whole raise. One
transaction, any launch. ../launchpad has this exposure live and has simply not been attacked.
Initializing at launch closes it, because the token address does not exist until the launch
transaction itself. The opening price is threshold / lpAllocation, deterministic from config and
known then.
It also explains something previously read as housekeeping: pons's hook checks in
beforeInitialize that the pool was registered by their factory. That is this defence, done the
other way. When our hook exists it gives the same protection more directly, and the launch-time
initialization can stay as belt and braces.
D17 — The creator sets the fee, inside a ceiling the protocol sets
Settled 12 Sep 2026, amending D13. D13 made every curve parameter protocol-set. That was right for the curve's shape and wrong for its fee: a launchpad where the creator cannot choose what their own token charges is not the product.
So the two are split. The protocol fixes the maximum — MAX_FEE_BPS, 10% each way, already in
BondingCurve — and the creator picks the rate and the recipient at launch. Neither can move
afterwards, by the same rule as everything else a launch snapshots: no setters, and an upgrade must
not reach a curve people have already bought on.
The curve shape and the graduation threshold stay protocol-set. Those are the numbers that decide whether a launch is extractive, and D13's reasoning for holding them still applies.
Fees start as a plain payment to a wallet. The creator names an address and the curve pushes there on every buy and sell. The consequence is worth stating because it is a simplification rather than a cost: it needs no per-launch accounting at all. O17 exists because everything currently lands in one shared vault and nothing records whose it is; a fee paid directly to the launch's own recipient is attributed by construction. O17 still blocks pool fees, whose sink is one shared address inside the migrator, and it still blocks anything that pays holders rather than one wallet.
Anything richer — holders, a buyback, a burn, weighting — is a later choice of what sits at that address, which is the fee-strategy seam doing its job.
The destination is an address, and that is the whole point. It can be a wallet on day one and a distributor contract later, with no change to the curve, the factory or any launch already trading. So starting simple forecloses nothing.
Where this is going: pushed, never claimed. The end state is that a holder's share arrives in
their wallet without them doing anything — no claim button, no accrued balance, no keeper.
../concept has measured gas budgets showing in-swap distribution to N holders is affordable on
Arbitrum, and ../888 shipped the piggyback pattern live. Deferred, not abandoned, and the two
things that make it hard are worth naming now so they are not discovered later: it needs the
holder set enumerated (the same problem as the score), and under D1 it has to borrow the gas of
somebody already transacting, because a contract cannot wake itself to pay anyone.
A claim model would have been the easy version and is deliberately not the target. It is also not a step towards it — an accrued-balance ledger is a different architecture, so building one now would be work to throw away.
Amended 14 Sep 2026, before building — four calls by the developer:
- Both fees exist. The protocol keeps its own curve fee (owner-set, to
FeeVault, zero today) and the creator's fee is added to it, paid to their wallet. The pons and par shape. - The ceiling caps the total a trader pays: protocol plus creator, at most
MAX_FEE_BPSeach way. A creator's maximum is whatever the protocol leaves. - A failed push never blocks a trade. USDC and rTokens have blocklists, so a pushed fee can revert. The trade goes through, the fee is held in the curve as owed, and anyone may retry the transfer. This is a failure path, not the claim model rejected above: nothing accrues while the recipient can be paid.
launchgains an overload carrying the fee terms. The three-argument form stays, meaning no creator fee, so every existing caller and the stage 0 suites are untouched (D19).
D18 — A stock whose balanceOf moves is never a quote asset, only a pro-rata wrapper of it
Scope, amended 13 Sep 2026: the rule is about the mechanism, not the asset class. Robinhood's
stock tokens scale a separate uiMultiplier and never touch balanceOf, so they are safe raw —
which is how par and pons pair against them with no wrapper (notes/references/robinhood-stock-tokens.md).
Reality's rTokens rescale balanceOf and are not.
Settled 13 Sep 2026 at stage 0 checkpoint 1. We adapt to the issuer rather than wait for it to change: the wrapper is what makes an rToken behave like a fixed-balance token, and it is entirely on chain.
Every tokenised stock on Arbitrum One rebases: Reality scales balances by an index on a split and
zeroes them on a delisting; xStocks and Backed use a multiplier; Dinari applies splits. Verified on
chain — an implementation byte-identical to rNVDA's doubled a token's supply in one block with no
transfer. notes/sessions/stage-0-pairing.md has the evidence.
BondingCurve and a v4 pool both keep their own books and never re-read a balance. So a raw rToken
as quote is broken by the first split: a forward split lets buyers take the curve at a fraction of
its price, a reverse split leaves the curve holding less than quoteCollected so graduate()
reverts with the raise inside, and in a pool it moves the singleton's balance for every pool
holding that token. Reality also pauses the token around every split.
A pro-rata wrapper absorbs all of that: a split changes what the wrapper holds, not what a share is
worth, and the wrapper keeps trading while the stock is paused. BasketWrapper already does this,
with one constituent as well as ten.
So the factory must refuse a raw asset whose balances rebase, and a single such stock is a one-constituent basket. Rejected: supporting rebasing quotes in the curve and a hook. Two new accounting paths and a hook that must re-sync the singleton, to avoid a wrapper that already exists and is tested.
What a wrapper does not absorb, and which stays the issuer's power: freezing or sanctioning the wrapper, delisting a constituent, and upgrading the token.
D19 — Stage 0 lives behind the quote seam, so it can be reworked without touching the core
Settled 13 Sep 2026, at the developer's request. Pairing is the foundation and we expect to come back to it. Coming back must never mean editing the curve, the token, the migrator or the vault, or disturbing a launch that already exists.
The rules:
- The core knows a quote only as an ERC-20 address registered with
setQuote. Nothing insrc/curve,src/token,src/factory,src/poolorsrc/feesimportssrc/assets, andsrc/assetsimports nothing from them. True today; kept true deliberately. - The seam has a contract, and stage 0's job is to meet it. A registered quote keeps a fixed balance (no rebase, no fee on transfer) and is a standard ERC-20. Anything that does not is wrapped until it does (D18). A pause or blocklist is allowed and becomes that launch's risk.
- Change arrives as new contracts, never as edits to live ones. A basket is welded to its pools
(D14), so a new basket shape or basket factory is a new deployment registered through
setQuote. Withdrawing an asset isenabled = false, which touches no live launch — already proved bytest_withdrawingAPairingLeavesExistingLaunchesAlone. - The core suite passes unchanged when stage 0 changes. Stage 0 tests live in their own files.
A change to
src/assetsthat needs a core test edited has crossed the seam and needs discussing.
Rejected: a basket registry inside LaunchFactory. It would be convenient, and it would make every
pairing change an upgrade of the one contract that holds every launch record.
D20 — The factory asks a quote source about quotes it does not know
Settled 13 Sep 2026. setQuote is owner-only, so without this every creator-composed basket
would wait on us to register it and composition would not be permissionless.
LaunchFactory gains one address, quoteSource, behind a narrow interface in src/interfaces. A
quote the owner has registered with setQuote is answered from the factory's own config and never
from the source, so withdrawing an asset cannot be undone by a source. Any other quote is asked of
the source, which returns the same three things setQuote stores: enabled, migrator, threshold. The
curve snapshots them at launch as it always has.
This is the one deliberate crossing of D19 rule 4: a single LaunchFactory upgrade and new core
tests, so that pairing work afterwards never needs another. The import boundary still holds — the
factory knows an interface, not src/assets.
Rejected: registering each basket by hand. No core change, and a person in the loop for every basket.
D21 — Baskets are shared per composition and sized by reference units
Settled 13 Sep 2026, answering O13 and amending D14's composition rules.
One basket per composition, at a CREATE2 address over its sorted constituents and units.
Identical baskets deduplicate and their mint and redeem market deepens. Accepted cost: a freeze, a
sanction or a bug on that wrapper reaches every launch paired to it. The developer expects this
may change. It stays cheap to change because the BasketFactory is upgradeable and sits behind
D20's seam: a different creation policy reaches new baskets only, and every basket already made
stays launchable.
Sizing without an oracle. The owner sets reference units for each onboarded stock — about $100 of it. A creator states weights in basis points; units are weight × reference units, fixed at creation (D14 unchanged from there: no rebalancing, weights drift). That makes two things checkable on chain that were not before:
- at least 10% each and 100% in total, which D14 required and nothing enforced;
- every basket starts near the same value, so one threshold, counted in baskets, serves them all. Without it a creator could compose a $0.01 basket and graduate on a trivial raise.
Reference units are configuration, snapshotted into a basket's units at creation. A stale reference mis-sizes new baskets only; it never touches a live one and nothing settles against it, so D1 holds. A payout asset for dividends (USDT for Reality) is added as a zero-unit slot outside the weights.
Curated baskets are the same contracts with an owner flag.
D22 — The router: ordinary quotes, a caller's path, one shared upgradeable router
Settled 15 Sep 2026, answering O19.
- Legs. ETH or any ERC20 → Uniswap v3 along a path the caller supplies → the launch's quote → its curve, or its v4 pool once graduated. Sells run the reverse. One call works at both stages.
- No currency list on chain. The caller's path and one final minimum are the whole policy, so the router promises no liquidity. "ARB, ETH, USDC" is a choice the interface makes.
- One router for every launch, reading each from the factory. UUPS, owned (D2). It holds nothing between calls.
- Never load-bearing. The curve and the pool stay directly callable. A crossing buy's excess is refunded in the quote asset.
- Knows nothing about baskets (D19):
make check-boundariestreatssrc/routeras core.
Stocks are out of the router for v1. No contract of ours can acquire rTokens: Reality's
minter reverts UserNotRegistered for anyone outside its identity registry, and its RFQ contract
has a caller allowlist. Getting in needs Reality to onboard us (KYB, API keys, our router
registered), which makes us the party routing retail into a product sold only to KYC'd non-US
persons. Dropped, not parked. A stock-paired launch is reachable only by people who already hold
the rTokens; if Bitget's announced rToken AMM lands on v3, a caller's path reaches it unchanged.
Rejected: an owner-set currency list (every thin route becomes our promise); a router per launch
(a deploy per launch, as RHUB's ZapRouter); basket mint-and-buy (helps only Bitget users).
D23 — A crossing buy without the gas to graduate reverts, rather than deferring
Settled 15 Sep 2026, amending the inline graduation carried over from RHUB.
Measured on a local chain: a crossing buy sent at eth_estimateGas (192k) succeeded, below
GRADUATION_GAS_FLOOR (500k), and left the curve locked and not graduated. Estimation searches for
the least gas that succeeds, and deferring is success — so every wallet, aggregator and router
defers, and the launch is frozen until someone calls graduate(). The CLAUDE.md batching trap in
another shape. RHUB's interface padded the gas; nothing else does.
So the gas shortfall reverts. Estimation then has to include graduation. Deferral stays for a migrator that genuinely fails — the recoverability D16 relies on. Accepted cost: a crossing buy costs graduation's gas (~450k locally), paid by whoever crosses.
Proof is a property, not an example: no gas limit may give a buy that succeeds, crosses and leaves the curve ungraduated.
Also dropped from v1: graduating a restricted quote through a permissioned pool. Nothing we pair
against needs it — contracts hold rTokens freely and BasketWrapper absorbs the rebase (D18).
D24 — Only a launch's own curve can seed its pool
Settled 15 Sep 2026. A fix, found opening stage 4.
PoolMigrator.migrate was permissionless. Anyone holding a sliver of a launch's token could call it
first with dust, mark the pool seeded, and every later graduation reverted AlreadySeeded: the curve
locked for good with the raise inside. Proved with a probe, ~1 ARB against a 69,000 ARB raise.
The factory now names the curve in prepare, inside the launch transaction, and migrate accepts
only that curve. Nobody can prepare first — the pool is opened in that same transaction and cannot
be opened twice.
D25 — Pool fees are split per launch when they are collected
Settled 15 Sep 2026, answering O17 for the creator split. Holder payouts still need their own answer.
- Where: the migrator collects per pool, so it already knows the launch. It splits there — the creator's share pushed to the launch's creator fee wallet, the rest to the vault. No vault per launch; the vault stays a per-currency treasury.
- Terms: the creator's share is factory config (starting at 50%), snapshotted into the pool at launch. Changing it reaches new launches only.
- No wallet: a launch with no creator fee wallet pays its whole pool fee to the protocol.
- Blocked wallet: held in the migrator and retryable by anyone, as curve fees are (D17).
- Trigger: the permissionless
collectFees, pulled by whoever wants paying. The in-swap hook (D15) arrives with holder payouts, which need it.
D26 — A launchpad for experiments: we build capabilities, not mechanics
Settled 15 Sep 2026 by the developer. A change of product, not of code.
A launchpad like every other is not a business. This one is for experiments — tokens whose point is
a novel mechanic, as ../888 was. So:
- Mechanics belong to experiments. Holder payouts and time weighting leave v1; so do lots, scoring and gacha packs as things we build. They remain the examples the infrastructure must host.
- Capabilities are ours: a per-launch hook, fee destination and balance observer, with isolation between launches. Today all three are global or per quote asset, not per launch.
- MVP first. The launchpad as it stands is finished before experiment infrastructure is designed. Stocks and composable pairs are standard, not experiments, and ship in the MVP (D27).
Expect this to reopen D15. Its observing-only, no-delta-flag hook keeps pools quotable in Uniswap's interface, but 888 charged inside the swap. Whether experiments may do that is the first hook question.
D27 — The MVP is the launchpad core, deployed on the local stack
Settled 15 Sep 2026 by the developer.
- Where: the whole launchpad deployed and usable on a local chain on the developer's machine. Local first, as CLAUDE.md already requires; Arbitrum Sepolia comes later, when the MVP is ready for it.
- Stocks are not in this pass. Standard, not experiments — but, like hooks, an effort of their own. Three separate efforts follow the MVP: stocks, hooks, and the modular infrastructure the experiments vision needs (D26). The stock and basket code stays in the repo, undeployed.
- Fees on, realistic, as settings in the deploy config.
- No web interface, no experiment seams. The web is built on the contracts, and the contracts are not final enough to build on. Experiment seams need creative work before the right calls can be made.
- No anti-snipe. Decided before the first mainnet launch, likely with the hook design (O12).
- Security bar: Slither with every finding triaged. The internal review was dropped on 15 Sep 2026 — nothing meaningful is deployed yet, and the contracts will change. External audit before mainnet.
D28 — A testnet launchpad with an interface, iterated on in place
Settled 15 Sep 2026 by the developer, amending D27.
The next sprint builds a web interface for the launchpad as it is and the process to deploy it to Arbitrum Sepolia and keep iterating there — the hook infrastructure being a likely first iteration. D27's "no web interface" is superseded. Mainnet stays out of scope.
The collisions, answered 15 Sep 2026: D6 and D7 keep their rules — contracts deploy and upgrade by hand, signed by local keystores under a new naming convention. D9 is amended: Sepolia is a standing environment. See each.
The preparation sprint (notes/sessions/prep-efforts.md) is not dropped; it waits.
D29 — The testnet launchpad: web first, WETH only, a committed book, Vercel
Settled 15 Sep 2026 by the developer (D28).
- Order: the web is built against the local stack first, then pointed at Sepolia.
- Quotes on Sepolia: WETH only, graduating at 0.01 ETH raised (lowered from 0.1 on 16 Sep 2026: Arbitrum Sepolia faucets gate on mainnet balance and history, so test ETH is scarce). A full end-to-end test costs about 0.012 ETH. Placeholder quote tokens and their v3 pools stay local.
- The Sepolia address book is committed — a standing environment's addresses are public and deliberate, and the web builds from them. Every other book stays ignored.
- The web is hosted on Vercel, deployed automatically (D7 allows it).
D30 — Shipping: Vercel's Git integration, master to testnet, prod to production
Settled 16 Sep 2026 by the developer.
- No heavy pipeline. The web ships through Vercel's Git integration; no deploy workflow in GitHub Actions and no Vercel token in GitHub. Vercel's build runs the typecheck, so a broken build never ships.
- The Vercel setup is in the repo, not in its dashboard:
vercel.jsonandweb/scripts/vercel-build.shcarry the framework, install, build, output directory and SPA rewrite, and the branch picks the chain. Importing the repository is the only dashboard step. master→ the testnet site (Arbitrum Sepolia).prod→ production (Arbitrum One), later. One web build, the chain chosen at build time; the two sites differ only in which address book they read.- Four projects, one repository, each configured by the
vercel.jsonin its own directory: the web and the documentation, each built frommasterfor testnet and fromprodfor production. Nothing built frommasterever points at Arbitrum One. The documentation follows the same split —masterbuilds the internal site (public, D8 as amended),prodthe public one. - Local stays the fast loop. One command brings up the chain, the contracts and the web.
- Iterations upgrade Sepolia in place — behind the existing proxies and beacons, new contracts alongside — so testnet rehearses the upgrade path mainnet will need (D2, D12). A fresh deploy only when storage layout forces it. Contract deploys stay manual (D7).
- Iterations follow: the first is pairing tokens with stocks.
D31 — Stock pairing is the first iteration on the live testnet
Settled 16 Sep 2026 by the developer.
The testnet launchpad is live and the interface ships from master (D28–D30), so iterations now mean
one feature deployed end to end at a time. Stock pairing goes first: BasketFactory and
BasketWrapper exist and are tested, including real rNVDA on a fork, so this is a deployment,
factory-configuration and interface sprint rather than a design one (D27 kept the code undeployed).
Hooks and the experiment seams (D26) follow. Before the first mainnet launch, not after: a pool's hook is part of its identity and can never be changed, so every launch made before the hook exists is permanently hookless. That is a constraint on mainnet timing, not on the testnet, where we redeploy.
D32 — One permanent hook that charges, and a module per launch behind it
Settled 17 Sep 2026. Supersedes D15's observing-only hook and D16's hook-per-migrator.
LaunchHook is one address every launch's pool carries, mined for 11 flags including all four
return-delta ones. What a launch does inside its trades is a module (ILaunchModule), named once
at launch and fixed for life. A module runs on three seams: the curve, the pool, and every token
transfer. ModuleSet runs several behind one address.
Why charge: D26's experiments take value inside a swap, and a hook can never gain flags later. The cost is accepted: these pools are absent from Uniswap's own interface until the allowlist review clears them, as RHUB still is. Aggregators route them.
Rules that follow, not optional:
- A module returns a rate, never an amount. The hook clamps it; an absolute delta that flips a trade's exactness reverts the swap uncatchably.
- Breaking is swallowed, refusing is honoured. A reverting module is skipped;
rejectreverts the trade. A swallowed check fails open, so every call sits behind a gas floor above its cap. - Nobody is exempt from a holding or buying limit, the creator least of all. Only structural holders and registered venues are.
- Upgrades are by cohort (D12): new curve beacon and new modules for future launches; the hook, migrator and factory stay.
D33 — master is a permanent staging environment, services included
Settled 20 Sep 2026 by the developer, amending D30. D30 pointed master at a testnet site.
Staging is now the whole system — web, documentation and services — standing and kept current,
not a thing visited per milestone.
A Contabo VPS carries the services; Supabase carries the database; Vercel carries the web. Access is non-interactive SSH, configured when there is something to deploy and not before.
Consequence: "it works locally" stops being the bar. A change is not landed until staging still runs, and staging is the rehearsal for production in the same way Sepolia rehearses mainnet (D9).
D34 — Services are Rust, in Docker Compose, one file per environment
Settled 20 Sep 2026 by the developer, answering O5 and part of O6. The first service is the indexer, and the developer chose to pay Rust's cost up front rather than rewrite a Node one later.
Recorded because it was argued: the case against was that throughput is not the constraint — we filter by our own addresses, so the event rate is ours and not Arbitrum's, and what actually breaks an indexer is a non-durable cursor, an unhandled reorg, backfill entangled with tailing, and non-idempotent writes. Those are design faults in any language. The developer's case is that the rewrite is real work and the shape of the problem will not change. Both were stated; this is the call, and it is not to be reopened on the grounds already heard.
- One Compose file per environment — local, staging, production. Not one file with overrides.
- Named volumes, never bind mounts, for data. Bind mounts only for source in local development, where hot reload needs them.
- Rust does not make
stylus/any closer (O6): a service is not a contract.
D35 — Supabase is managed, driven by the CLI, and never by the dashboard
Settled 20 Sep 2026. The developer has previously had to repair migration history by hand. That failure has one cause worth naming: a schema changed somewhere the repository could not see.
- Every schema change is a committed migration, authored with
supabase db diff. Nothing is ever changed in the Studio dashboard, on any project, for any reason. supabase db resetmust replay from zero, and CI proves it — reset, replay, regenerate types, fail if the committed types differ. That check is what would have prevented the repair.- One project per environment. A project shared between staging and production is the other common route to repairing history by hand.
- The browser reads, the indexer writes. Anon key and RLS for the web; the service role only from the VPS and the edge function.
- An MCP may inspect a live project. It may never be what applies a change, or the repository stops being the source of truth.
Rate limits are answered by caching immutable rows at the edge — a closed candle never changes — not by asking Supabase for more.
D36 — Anything that can be generated is generated, never written by hand
Settled 20 Sep 2026. Two type boundaries now exist where one did before, and a drifted type is a bug that compiles.
-
Supabase types are generated from the local database by
make supabase-types, intosupabase/types/database.types.ts.Amended 20 Sep 2026, on writing it: they are committed, not gitignored. The original wording copied
web/src/chain/generated.ts, which is rebuilt from a committed address book and needs nothing running. Types need a database, and Vercel builds the web without one — so gitignoring them means the build cannot typecheck. Committed generated code rots silently, which is whatmake supabase-checkand thesupabaseCI job exist to prevent: replay from zero, regenerate, fail on any difference. -
Rust ABI bindings are generated from
contracts/out/*.jsonat build time. This is what stops the indexer's decode path and the web's viem path disagreeing about an ABI after a contract change — closed by construction rather than by remembering.
D37 — The web is rebuilt, behind a chain layer and a token layer
Settled 20 Sep 2026 by the developer. The existing interface is kept as it is and superseded
rather than refactored; the new app is built beside it and cut over in one commit, because master
is a live staging site (D33) and must not go dark.
Two isolation rules, and they are the reason a rebuild is affordable:
- One chain layer. Addresses, ABIs and typed hooks live in
web/src/chain/; nothing else in the app imports wagmi or viem. Contracts will be added and replaced continuously, and that must touch one directory. - One token layer. No component hardcodes a colour, radius or spacing. The design is not final — changing it must be changing a theme file, not editing forty components.
Component libraries are for behaviour — modals, popovers, form primitives — never for appearance. A library's look is precisely what makes a redesign expensive.
D38 — The indexer observes. Nothing settles against it
Settled 20 Sep 2026. A restatement of D1 for the part of the system that will be tempted to break it. Once a database holds the price, the holder count and the points, making it authoritative is one convenient afternoon away.
- Every figure it serves is derived from chain events and rebuildable from zero. Drop the database and nothing is lost but time.
- No trade, fee, graduation or launch may require it to be running. The curve and the pool stay directly callable, as the router already is (D22).
- Badges and points are decorative. The moment they pay out, whoever computes them is consensus — and the scoring has to move on chain first.
- The word is indexer, never keeper. The distinction is the whole architecture.
D39 — v1 pairs against ARB and RHUB, and ships no new hook
Settled 20 Sep 2026 by the developer.
Two quotes. ARB and RHUB, both ordinary fixed-balance ERC20s, so the seam's contract (D19) is met with nothing to wrap. Stock and basket pairing stay built, tested and deployed nowhere (D31). Sepolia has no RHUB, so testnet needs a stand-in registered as a quote, as the local chain already does for ARB and USD.
No new hook in this cohort, and the distinction this rests on is the one to keep straight:
- A new module is a deployment, a
setModuleEnabled, and it reaches new launches the same day. Mechanics ship continuously after launch this way, and that is D32 working as designed. - A new hook is a permanent address, a new migrator, every quote repointed and a fresh Uniswap routing review. Launches made before it never get it.
So anything expressible as a rate, a refusal or a reaction to a transfer is a module. What this
cohort can never run is whatever needs a flag we did not mine — a dynamic fee, which wants
afterInitialize, and anything on donate. Nothing public may promise either.
D40 — Per-wallet data is authorised by a signed session, never by an address in a column
Settled 20 Sep 2026 by the developer. The watchlist follows the wallet rather than the browser, which means a write has to prove the wallet. Deciding this once matters because every per-wallet feature after it — notifications, a profile, anything saved — reuses the same session.
One signature per session. On connect, the web signs a domain-bound message with a nonce; an
edge function recovers the address, verifies it, and mints a Supabase JWT carrying the wallet as a
claim. RLS then enforces wallet = auth.jwt()->>'wallet', and a watchlist is readable only by its
owner. Writes afterwards go straight to PostgREST — a star click is one fast request — and the token
expires on its own.
The message must be impossible to mistake for a transaction: a plain personal_sign of
human-readable text with a nonce and a domain, never typed data a wallet could replay as anything
else. Nothing on chain is ever authorised by it.
Rejected: the address as a plain column, which lets anyone read or overwrite anyone's list and is noticed the week after launch. Rejected: an edge function on every write, which reuses the metadata function's shape but puts a function invocation in the path of a star click and makes us own session expiry by hand.
This is Supabase's own idiom, not a scheme of ours, and it stays inside D38: a session proves who you are to a database of preferences. Nothing settles against it.
D41 — Numbers are exact in the database and strings at the boundary
Settled 20 Sep 2026 by the developer. Three rules, and the third is the one that actually bites.
Smallest units, always. 0.1 ETH is stored as 100000000000000000. Nothing in the database is
ever a human-scaled number; a quote's decimals is stored on its launch, and only the interface
ever divides. A figure someone has already divided cannot be un-divided, and every rounding argument
after that is unwinnable.
Rates are integer basis points, exactly as the contracts hold them. 3300, never 3.3.
Durations are integer seconds. No fixed-point decimal anywhere for a rate.
Exact values are numeric(78,0) in the column and text at the API boundary. Postgres
numeric is arbitrary-precision decimal, not floating point, and 78 digits holds uint256 — max
about 1.16 x 10^77 — so nothing is lost. The danger is the client, not the column: a JSON number
parsed by JavaScript is a double and dies above 2^53. So every exact value is cast to text in the
view that exposes it, and no double ever touches an amount. Unverified: how PostgREST serialises
numeric by default. The cast makes it moot either way, which is why it is the rule rather than a
thing to look up.
Rejected: text as the storage type, which was the developer's first instinct and is right
about precision and wrong about everything else. Text cannot be summed and cannot be ordered —
ORDER BY puts "9" above "10" — and 24-hour volume is a SUM over trades, sorted. It ends in a
shadow numeric column to sort on, which is this decision reached by accident and without the naming.
The suffix carries the meaning, so a query is readable without looking anything up:
| no suffix | an amount in smallest units |
_x18 | scaled by 10^18 — a price is quote per whole token |
_bps | a rate in basis points |
_s | seconds |
_usd | deliberately approximate, double precision, for display and sorting only |
_usd is the deliberate exception and the reason the others are strict: a figure that exists to
sort a catalog does not need to be exact, and one that settles anything may never be a float.
Open questions
Agreed not to assume. Each becomes a decision above when it is settled.
O1 — Does the owner get bounded in code, or only by policy?
Every prior project fenced the owner with hard constants: rates tunable, ceilings immutable, fee recipient with no setter, so the owner's worst case is a knob at its limit rather than a withdrawal. That pattern predates the decision to make contracts upgradeable, and an upgrade authority can move a constant. So the fences are either kept as a statement of intent, replaced by a timelock on upgrades, or dropped as theatre. Not decided.
O2 — What shape is the multi-token curve?
RHUB's parameters are solved for one token: virtualReserve = threshold x (1-c)/(2c-1), so the
curve's closing price equals the pool's opening price. A launchpad has to let creators choose, and
every choice re-solves that equation. Which knobs are exposed, and which are fixed forever, is
open.
O3 — How are tokenised stocks paired?
Which issuer, and what their transfer restrictions imply. Permissioned v4 pools are the likely
mechanism — see the uniswap-permissioned-pools plugin. Market hours, oracle dependency and
transfer gating all interact with D1 and none of it is designed yet.
O4 — What does fee redistribution actually pay, and to whom?
../concept proved claim-free distribution to N holders on borrowed gas and measured the gas
budget. Whether the launchpad uses that, a claim model, or both, is open.
O5 — Backend stack, concretely — ANSWERED 20 Sep 2026, see D34 and D35
Node is settled; the rest is not. Which framework, which database, and what the Docker Compose topology looks like. Deferred until there is something for it to serve.
The answer: Rust services in Docker Compose, one file per environment (D34); managed Supabase for the database, driven by the CLI (D35). The thing for it to serve arrived — the indexer.
O8 — CI/CD for the documentation site
Not built, deliberately: there is nothing to document yet, so there is nothing to gate or
deploy. When docs/public/ carries real pages it needs a path-filtered workflow that runs
make build-docs — the broken-link check is the gate worth having — and a deploy of the public
build alone. The internal build must never reach that pipeline.
O6 — Rust, and whether Stylus earns its place — half answered 20 Sep 2026
Rust is in the stack by intent. No component has yet been identified that needs it. stylus/
stays empty until one is.
Rust has a component: the indexer (D34). Stylus still does not, and a service is not a contract
— so stylus/ stays empty and this question stays open on its own terms.
O7 — How Claude and the developer work together, in the parts not yet settled
Four questions with no answer yet. Ask rather than assume; settle each when it first bites.
- Planning depth. Written plan before every non-trivial change, or only for money and architecture? A gate that fires too often gets skimmed.
- Disagreement. Whether "state the concern once, then execute fully" is the rule, and whether a concern that survives being overruled gets logged here or dropped.
- What needs asking first. Mainnet transactions, spending money, anything public-facing, history rewrites, and the live RHUB contracts are the obvious candidates. Not confirmed.
- Subagents. Whether fanning out for search and review is worth more than one thread the developer can follow.
- Proposing the next step. Observed twice in the launchpad-core session and it cost a full reset both times: finishing a piece and then suggesting the next one unprompted, until the suggestions had accumulated into a backlog nobody had asked for and the actual scope was hard to see. The developer's own v1 was six bullets throughout. Working rule until this is settled: finish what was asked, report it, and stop.
Settled in the meantime: low autonomy. The launchpad is small enough to stay in one head, and keeping it there is worth more than speed.
O9 — Can a creator change the fee strategy after launch?
The whole architecture rests on fee policy being a swappable pointer. That pointer is also the
cleanest rug in the system: launch on "fees redistributed to holders", change it later to "fees to
me". Three shapes, none chosen — immutable at launch, timelocked and loudly visible, or a
protocol-curated set the creator picks from but cannot leave. Settle before FeeVault is written,
because it decides whether the setter exists at all.
O10 — The ERC20's extension point: one observer, or a small fixed set?
notes/VISION.md proposes exactly one nullable observer called from _update, so the token can
be frozen forever and the observer is what gets versioned. The alternative is a short fixed array,
which avoids a fan-out contract in the middle but multiplies the gas-cap and failure-swallow
surface. One is the recommendation; it is not decided.
O11 — Lot ordering when a live lot is newer than the dead one
Lots are consumed newest-first, and selling kills the newest live lot outright while the dead lot absorbs further selling. What is undecided is the case where a buy lands after a partial sale, leaving a live lot newer than the shield. Consuming the dead lot first preserves the invariant "at most one dead lot per wallet"; strict newest-first would not, and dead lots could accumulate without bound. The invariant is worth having, so dead-first is the recommendation — but it means "newest-first" is not literally true and the rule has to be written down as two steps, not one.
O12 — Anti-snipe: deferred, and it cannot be retrofitted
Built as AntiSnipeModule (D32); a launch opts in. Launches made without it stay unprotected.
Deferred deliberately, not forgotten. D13 ships v1 without it. The consequence is permanent and
is the reason this is written down: anti-snipe has to live inside buy(), and under D12 a live
curve never receives new code — so every launch made before it ships is unprotected forever.
When it is picked up, pons's shape is the one to copy: a tax on buys only, decaying from 99% to
zero over five seconds, buyer always nets at least 1% of spend, up to 32 exempt addresses fixed at
creation, and the tax flowing back into the launch. notes/references/pons.md has the detail.
pons v1's cruder alternative — the creator's buy alone in block 0, then per-wallet caps for two
blocks — is bypassed by splitting across wallets.
Decide before the first launch that matters, not before the first launch.
O13 — One basket token per composition, or one per launch?
Cannot be decided yet, and it is not a preference. It depends entirely on whether Reality's
rTokens enforce transfer restrictions in the token contract — notes/references/reality-rtokens.md
carries the question and who to ask.
- If rTokens transfer freely: one shared basket per composition, addressed by CREATE2 over the sorted constituents and their unit amounts. Identical compositions dedupe, the mint/redeem market deepens with use, and a basket is audited once. The cost is blast radius — one wrapper bug reaches every launch paired to it.
- If they carry a per-holder allowlist: every basket contract has to be allowlisted by the issuer before it can hold a single share, so one-per-launch means an issuer round-trip per launch and permissionless launching is gone. Sharing helps but does not fix it; the likely answer becomes one allowlisted vault holding every constituent, with basket tokens as claims against its internal ledger — maximal concentration of risk, and the only shape that keeps composition permissionless.
Do not pick one on general principles. Get the answer first.
The answer, 12 Sep 2026: rTokens transfer freely. No allowlist. The gates are a sanctions list, account freezes and a global pause (stage 0 thread). So the allowlist branch above is dead and the first branch applies. One consideration it did not have: a freeze or sanction lands on a wrapper address, so a shared basket concentrates that risk the same way it concentrates a bug.
ANSWERED 13 Sep 2026 — one per composition, see D21. Marked by the developer as likely to be revisited.
O14 — Does the graduated pool need a hook at all? — ANSWERED, see D15
Yes, but an observing one. A pool can never gain a hook after creation, so the cost of leaving it
out is that every reactive feature in notes/VISION.md is permanently impossible for that cohort.
A hook with no return-delta flags costs nothing in return: Uniswap auto-allowlists it, so the pool
stays quotable everywhere, which RHUB is not.
O15 — Is the token's observer a fixed implementation, or a repointable router?
LaunchToken.observer is immutable, because the token is. So whatever sits at that address is
the only thing that will ever watch a launch's balances move. Two shapes, and the choice decides
whether already-launched tokens can gain features:
- A fixed implementation. What a launch gets is what it keeps. Honest and inert, and it means lots, time weighting and scoring can only ever arrive for new launches — the same cohort rule as D12, applied to behaviour rather than money.
- A router we control, repointed later. Every existing token gains whatever we build next,
which is the only way the features in
notes/VISION.mdreach tokens launched before them. The cost is a standing power to change how an existing token's holders are measured, and therefore who earns from it.
The blast radius of a hostile observer is bounded either way: it cannot refuse, change or observe its way into moving a transfer, and the token caps its gas and swallows its failure. The question is not safety, it is whether retroactive change is legitimate here when D12 says it is not for the curve.
Nothing is built behind the seam yet, so this can wait — but not past the first launch that matters, because the address is written at construction.
O17 — Fees are accounted per currency, not per launch — creator split ANSWERED by D25; holders still open
FeeVault measures its own balance, which is what makes its accounting impossible to inflate — and
it is also why it cannot say whose fees are whose. Every launch quoted in the same asset
commingles in one total.
That is fine for a protocol treasury and it is not enough for two things we have already decided we want:
- D15's creator/protocol split, which needs to know which launch a fee came from;
- paying a token's own holders, which needs the same.
Three shapes, none chosen:
- A vault per launch, deployed by the factory. Clean attribution, one more contract per launch,
and the migrator's
feeSinkis currently immutable and shared — so it would have to look the vault up rather than hold it. - A shared vault with attributed deposits, where the curve and the migrator name the launch.
But naming an amount is exactly what
syncrefuses to trust, so the deposit would have to be measured per launch some other way. - Events only, and attribute off chain. Fine for display, and forbidden for distribution: D1 says nothing off-chain may be load-bearing, and a payout that reads an indexer is precisely that.
Not urgent while fees only accumulate. It blocks the first strategy that pays anyone.
O18 — Do a stock's dividends reach the token's holders?
The intent, stated plainly: holding a token paired against several stocks should feel like holding those stocks, dividends included.
What happens today is not that. A dividend paid to a BasketWrapper raises what every basket is
worth, including the baskets sitting in the launch's pool. That lifts the pool's quote side — good
for the token's price, and it pays no one. Proved in test/QuoteAssets.t.sol:286.
Why the gap is hard rather than an oversight:
- the dividend arrives at the basket, whose holder is the PoolManager, not any person;
- the pool's baskets back the launch's liquidity, so paying them out to token holders would be draining the pool — the thing D15's locker exists to make impossible;
- "who holds the token" is a set nobody has enumerated yet, which is the holder-score problem;
- and a dividend is not an event anybody is transacting against, so under D1 there is no gas to borrow. Something already happening has to carry it.
Not blocked on anything and not urgent, but it is the difference between "paired with stocks" and "holding stocks", and the answer may reach into the basket's design. Research before promising it.
O19 — The router: one asset in, one asset out, at both stages — ANSWERED, see D22
A token quoted against a stock or a basket is unbuyable by a normal user without one. The want is plain — pay in ETH, wBTC or wSOL, receive the token; sell the token, receive ETH — and it has to work identically on the curve and in the graduated pool.
Two paths, and only one of them is new.
Trading in the pair itself already works and needs no router: $MEME/$TSLA means buying $MEME
with $TSLA and selling it back for $TSLA, which is exactly what the curve and the pool already
do. Proved throughout test/QuoteAssets.t.sol. That door stays open whatever the router does.
A short list of routing currencies is what the router adds — ARB, ETH and USDC to start, chosen because they have the deep pools to route through. Keeping the list short is the point: each one is a path whose liquidity we are implicitly promising, and a thin route is worse than no route.
One consequence to settle when it is built: the usable routing set depends on what the quote asset has markets against, not on a global list. A tokenised stock will have USDC liquidity long before it has ETH liquidity, so either the list is per quote asset or the router hops through a hub (quote/USDC, then USDC/anything) and requires that one pool to exist.
What is not yet decided is how many legs we are willing to own:
- ETH to an ordinary quote is one swap, then
buy. Straightforward. - ETH to a basket is a swap per constituent, then
mint, thenbuy— and it needs every constituent to have a liquid market. Alternatively buy the basket itself, which assumes a BASKET/ETH market that will not exist early. - A restricted constituent cannot pass through the router at all unless the router is itself allowlisted to hold it. That is the authorised-participant shape again, and it may mean the basket has to be acquired rather than minted on the way in.
Two constraints already settled elsewhere apply: the router must use SafeERC20 (a quote that returns no bool otherwise looks like our bug), and it must never become load-bearing — the curve and the pool stay directly callable, or a broken router is a launch nobody can exit.