Overview
Print is a launchpad. A launch creates a coin, a market for it and a vault, in one transaction. What is different is what the market's takings are for.
Every coin names one backing asset at birth — any ERC-20 that already trades on the chain. A cut of every trade against the coin is spent buying that asset on the open market and deposited in a vault that has no withdraw function. The only way anything leaves is a holder burning coins to claim their exact share.
So a Print coin has two numbers, not one: a market price, which does whatever the market does, and a floor — the backing held per coin — which is arithmetically incapable of falling. This page documents how that second number is enforced, in enough detail to check it against the contracts rather than take it on trust.
Lifecycle
01 Launch
Call launch(name, symbol, backingAsset). The factory deploys a
PrintCoin, which deploys its own PrintVault in its constructor.
The entire supply is created once, to the coin contract, as curve inventory. The factory
takes nothing and keeps no key.
02 Trade
Buys and sells both execute against the coin's own constant-product market, priced in the chain's native unit. No external pool is required and none is assumed.
03 Back
In the same transaction, the trade's fee is quoted against the backing asset's real pool, swapped, and delivered directly to the vault. The coin never holds the asset.
04 Redeem
redeem(amount) burns the caller's coins and pays out
amount × vaultBalance ÷ totalSupply of the backing asset. Unconditional, no
queue, no lock-up, no approval.
Architecture
Four contracts, no proxies, no upgrade path and no shared state between coins. Each launch gets its own coin and its own vault; nothing is pooled across launches.
PrintFactory
The registry and the deployer. Holds no funds, has no owner, and cannot touch a coin after creating it. Its only job is to deploy pairs and to be the single place the board reads from.
PrintCoin
ERC-20, constant-product market and fee router in one contract. Owns its vault and is the only address that can ever make it pay out.
PrintVault
Holds the backing asset for exactly one coin. No owner, no admin, no pause, no rescue, no upgrade — those functions are absent, not merely unused.
V4BackingRouter
A per-asset adapter that presents Uniswap V4 through the small V2-shaped interface the coin speaks. Custodies nothing; its pool key is immutable.
Note the direction of the last hop: the swap's recipient is set to the vault address, so the backing asset never passes through the coin contract and there is no window in which it sits somewhere it could be taken from.
The curve
Each coin carries its own market so it is liquid from the first block and never depends on anyone opening a pool for it. It is a constant-product market with a virtual reserve: the reserve is seeded with a notional balance that sets the opening price and can never be withdrawn, so early buyers are not trading against an empty book.
Fixed at construction. There is no mint function.
Sets the opening price. Never withdrawable, never paid out.
Constant product, priced in the native unit.
nativeReserve
minus virtualNative is the native actually held.
quoteBuy(nativeIn) returns (tokensOut, fee)
fee = nativeIn × feeBps / 10_000
net = nativeIn − fee
tokensOut = tokenReserve × net / (nativeReserve + net)
quoteSell(tokensIn) returns (nativeOut, fee)
gross = nativeReserve × tokensIn / (tokenReserve + tokensIn)
fee = gross × feeBps / 10_000
nativeOut = gross − fee
Buys move inventory. They never mint. The whole supply is issued once, to the coin
contract, and a buy transfers some of it out while a sell transfers it back. When the
inventory is exhausted a further buy reverts with SupplyExhausted() rather than
creating new coins. This is not a stylistic choice — the invariant below depends on it.
Both quote functions are view and are the same code the trade path uses, so a
quote and a fill cannot disagree except through another transaction landing in between.
buy takes minTokensOut and sell takes
minNativeOut; both revert with InsufficientOutput() rather than
filling you at a worse price.
The fee
One number, charged on both sides, spent immediately. It is not revenue, it is not held, and there is no address that can receive it other than the vault.
100 bps of the native sent in, taken before the curve maths.
100 bps of the gross proceeds, taken before you are paid.
Gas only. The factory charges nothing to deploy.
Every unit buys the paired asset. Nothing is skimmed.
feeBps > 1000
reverts in the constructor.
feeBps is
immutable. Not a setting, not a governance parameter.
Charging both sides is the point rather than a detail. On every other launchpad a sell is
purely extractive; here it contributes exactly what a buy of the same size does, so a coin
being dumped is still buying its own floor. test_SellAlsoBuysBacking and
test_EveryoneSells_VaultSurvivesAndStillRedeems exist to hold that property.
A launcher using launchWith may set a different feeBps, which is
then fixed forever for that coin. The constructor rejects anything above 1000. Whatever is
set, the destination is the same — there is no parameter anywhere that redirects it.
The backing swap
The fee does not accumulate. _spendOnBacking runs at the end of every
buy and every sell, and it does four things in order:
1. spend = pendingFees + newFee // retry anything queued
2. quote = router.getAmountsOut(spend, path) // real spot quote, not a guess
3. minOut = quote × (10_000 − maxSlippageBps) / 10_000
4. router.swapExactETHForTokens{value: spend}(minOut, path, address(vault), now)
Every step that can fail is wrapped. If the quote call reverts, if the quote comes back
zero, or if the swap itself reverts, the amount is written to pendingFees,
BackingBuyFailed is emitted, and the trade still succeeds. A missing or
broken pool can never brick trading, and the queued amount is not refundable to anyone —
it is retried on the next trade.
The swap's recipient parameter is the vault, so the asset is delivered straight there. On
success the contract emits BackingBought(nativeSpent, assetReceived), which is
the event an indexer should follow to reconstruct a vault's history.
The vault
One vault per coin, deployed by the coin in its own constructor, holding only that coin's
backing asset. Both of its stored addresses are immutable.
contract PrintVault
address immutable coin // the only permitted caller
IERC20 immutable asset // what it holds
balance() view returns uint256
payout(address to, uint256 amount) // reverts OnlyCoin() for everyone else
That is the entire contract. There is no owner, no admin, no
pause, no rescueTokens, no sweep, no
setX, no proxy and no upgrade path. These are not present-but-restricted; the
functions do not exist in the bytecode.
payout is the only outbound call, and it reverts with OnlyCoin()
unless msg.sender is the coin. The coin calls it from exactly one place —
redeem. So the full set of conditions under which an asset can leave a vault is:
a holder burned coins. Not the launcher, not the deployer, not a multisig, not a vote, not a
timelock expiring. test_RedeemIsTheOnlyWayOut asserts it.
Redeeming
redeem(uint256 amount) returns (uint256 backingOut)
backingOut = vault.balance() × amount / totalSupply // computed FIRST
balanceOf[msg.sender] -= amount
totalSupply -= amount // then supply falls
emit Transfer(msg.sender, address(0), amount)
emit Redeemed(msg.sender, amount, backingOut)
vault.payout(msg.sender, backingOut)
The ordering matters and is deliberate: the payout is computed against the pre-burn supply, then the supply is reduced. Computing it the other way round would pay a redeemer a share of a smaller denominator and quietly transfer value from everyone who did not redeem.
You receive the backing asset itself — the tokenised stock, the metal, the currency — not a
cash value for it. redeemableFor(amount) is a view returning exactly what a burn
would pay right now, so nothing has to be sent to find out.
There is no minimum, no cooldown, no whitelist and no fee on redemption. It does not care whether the market price is above or below the backing.
The invariant
backingPerToken() — vault.balance() × 1e18 ÷ totalSupply — never decreases.
Three structural facts make that exact rather than aspirational, and each one is load bearing:
- Supply is fixed at construction. Buys move inventory, so a cheap early buy cannot add coins to the denominator faster than it adds backing to the numerator.
- The vault only receives. Fees add to it; nothing other than a redemption subtracts from it.
- A redemption is proportional. It removes
amountfrom the denominator andvault × amount / supplyfrom the numerator — the same fraction of each.
before B / S
redeem a → (B − B·a/S) / (S − a)
= B(S − a)/S / (S − a)
= B / S // algebraically unchanged for everyone who did not redeem
This is why the fixed supply is not negotiable. On a launchpad that mints on every buy,
backing per coin can fall, and "only goes up" would be a claim about behaviour rather
than a property of the arithmetic. Here it is checked by
test_BackingPerTokenNeverFalls_MixedActivity and by
testFuzz_BackingPerTokenNeverFalls, which runs randomised buy/sell/redeem
sequences over Foundry's default 256 runs and asserts the ratio never falls between any two
consecutive operations.
What the invariant does not say: it says nothing about the coin's price, and nothing about the value of the backing. It fixes the number of units of the asset behind each coin. If the asset itself falls, the floor falls with it in dollar terms. See Risks.
A worked example
Default settings: supply 1e9, virtual reserve 30, feeBps 100. Rounded for
readability; the contract works in wei.
state nativeReserve 30.0 tokenReserve 1e9 vault 0 supply 1e9
backingPerToken = 0
buy 1.0 native
fee = 1.0 × 100 / 10_000 = 0.01
net = 0.99
tokensOut = 1e9 × 0.99 / (30 + 0.99) ≈ 31,945,789
reserves nativeReserve 30.99 tokenReserve ≈ 968,054,211
fee path 0.01 → quote → swap → vault receives ≈ 0.0099 worth of asset
supply unchanged at 1e9 — nothing minted
sell 10,000,000 back
gross = 30.99 × 1e7 / (968,054,211 + 1e7) ≈ 0.3168
fee = 0.0032 → also buys asset → vault
nativeOut ≈ 0.3136 to the seller
the vault grew on a SELL. backingPerToken rose.
redeem 1,000,000 coins (vault holding V, supply 1e9)
backingOut = V × 1e6 / 1e9 = V/1000
supply → 999,000,000
vault → V − V/1000
backingPerToken = (V − V/1000) / 999,000,000 = V/1e9 — identical
The third block is the one worth reading twice. The redeemer took a thousandth of the vault and removed a thousandth of the supply, and the floor for every remaining holder is exactly what it was.
Launching a coin
launch(string name, string symbol, address backing) returns (address coin)
// deploys on the defaults: supply 1e9, virtual 30, feeBps 100, slippage 3000
launchWith(
string name,
string symbol,
address backing,
uint256 supply, // must be > 0
uint256 virtualNative, // must be > 0, sets the opening price
uint16 feeBps, // ≤ 1000
uint16 maxSlippageBps // ≤ 5000
) returns (address coin)
The backing address must be a contract — address(0) or an address with no code
reverts with BadBacking(). Nothing else about it is curated. Two coins may back
the same asset; a coin's name has no relationship to what backs it.
The launcher is recorded as creator in the factory's Pair struct
and receives nothing else. No allocation, no fee share, no key, no privileged call.
A launcher's access to the vault is identical to a stranger's, which is none.
test_AnyoneCanLaunch and test_LaunchRegistersPair cover the path.
A launch emits Launched(coin, vault, backing, creator, name, symbol), with the
first three indexed.
Queued purchases
When a backing purchase cannot execute, the amount is held in pendingFees and
retried at the start of the next one. Three conditions queue instead of reverting:
- No router —
routerReadyis false because the adapter had no code at construction, or did not answer the probe. - No quote —
getAmountsOutreverted, or returned zero because the pool is uninitialised. - Bad fill — the swap reverted, typically because the output fell below
minOut.
Anyone can retry for free by calling sweepFees(), which is permissionless and
takes no arguments. If a coin launched before its asset had a pool,
setWrapped() re-probes the adapter; it is permissionless too and can only move
routerReady from false to true, never back.
Queued amounts are not withdrawable by anyone, including the launcher. There is no function
that sends them anywhere but the vault. The contract's receive() also books any
stray native it is sent as pending fees, so value cannot be stranded in it.
Slippage
The default band is 3000 bps — 30%, per coin, fixed at launch, and the constructor refuses anything above 5000. That reads loose until you measure a real fill. These are measured against a live pool on chain 4663, expressed as the share of the spot quote actually received:
99.67%99.32%95.95%83.92%75.73%The quote the guard compares against is a spot quote derived from the pool's
sqrtPriceX96. A spot quote ignores both the pool's own fee and price impact, so
it is structurally optimistic — a tight band would not protect anyone, it would simply mean
purchases queue forever and never reach the vault.
The asymmetry is worth stating plainly: a loose band costs a slightly worse fill on an
amount that is always small, and the vault gains either way. A tight band risks the vault
gaining nothing at all. test_SlippageGuardsHold and
test_ThinLiquidityDoesNotBrickTrading pin both ends.
The V4 adapter
Robinhood Chain runs Uniswap V4. The canonical V2 and V3 router addresses hold
unrelated contracts, so anything written against them would be talking to the wrong code.
V4BackingRouter is a thin adapter presenting V4 through the small V2-shaped
interface the coin speaks.
One adapter is deployed per backing asset, because a V4 pool key — fee tier, tick spacing
and hook address — is per-pool and cannot be derived. The key is stored flat in
immutable fields, so nobody can repoint a live coin at a different pool.
The constructor rejects any key whose currency0 is not the native unit.
sqrtPriceX96() // read straight from PoolManager storage
poolId = keccak256(abi.encode(key))
slot = keccak256(abi.encode(poolId, uint256(6))) // POOLS_SLOT = 6
return uint160(uint256(poolManager.extsload(slot))) // slot0, low 160 bits
getAmountsOut(amountIn, path)
out = (amountIn × sqrtP) >> 96
out = (out × sqrtP) >> 96 // two stages: sqrtP² overflows 256 bits
// sqrtP == 0 (uninitialised pool) quotes 0, and the caller queues the fee
The swap runs inside PoolManager.unlock. The callback checks
msg.sender is the pool manager, swaps zeroForOne with a negative
amountSpecified for exact input, unpacks amount1 from the low 128
bits of the returned BalanceDelta, reverts NothingOut() if the
output is zero or below minOut, then settles the native it owes and takes the
asset directly to the recipient.
On V4 with a native pool, WETH() legitimately returns
address(0) — native is currency0. The coin therefore cannot use
address(0) as its "no router" sentinel, and carries a separate
routerReady boolean instead. Conflating the two silently skips every swap.
For the same reason the router probe is a raw staticcall, not a
try/catch: Solidity's extcodesize check reverts before a
high-level call to a codeless address, and that revert is not catchable.
Reading the board
Everything the board shows comes from the factory. None of it requires trusting this site.
pairCount() view returns (uint256)
pairs(uint256 i) view returns
(address coin, address vault, address backing, address creator, uint64 createdAt)
indexOfCoin(address coin) view returns (uint256) // index + 1; 0 = not a Print coin
boardRow(uint256 i) view returns (
address coin, address backing, address creator, uint64 createdAt,
string name, string symbol, uint256 supply, uint256 vaultBalance,
uint256 backingPerToken, uint256 nativeReserve, uint256 tokenReserve)
boardRow is the one call a front end needs per row — it reaches into the coin
and its vault for you, so a full board is pairCount() plus one call per index
rather than five calls per coin.
Per-coin views: backingPerToken() (scaled by 1e18),
redeemableFor(amount), quoteBuy(nativeIn),
quoteSell(tokensIn), pendingFees(), routerReady(),
plus the standard ERC-20 surface. test_BoardRowReadsEverythingTheSiteNeeds
guards the shape.
Contract reference
The full external surface. Anything not listed here does not exist.
PrintFactory
launch(string,string,address) → addressDeploy a coin on the defaults.
launchWith(string,string,address,uint256,uint256,uint16,uint16) → addressDeploy with explicit curve parameters.
pairCount() → uint256Number of pairs ever created.
pairs(uint256) → (address,address,address,address,uint64)Raw registry row.
indexOfCoin(address) → uint256Index + 1, or 0 if unknown.
boardRow(uint256) → (…11 fields)Everything a board row needs.
router() → addressThe swap venue every coin from this factory uses. Immutable.
DEFAULT_SUPPLY / DEFAULT_VIRTUAL_NATIVE / DEFAULT_FEE_BPS / DEFAULT_SLIPPAGE_BPS1e9 · 30 · 100 · 3000. Constants.
event Launched(address indexed coin, address indexed vault, address indexed backing, address creator, string name, string symbol)Emitted once per launch.
error BadBacking()Backing address is zero or has no code.
PrintCoin
buy(uint256 minTokensOut) payable → uint256Buy off the curve with native. Reverts ZeroAmount, InsufficientOutput, SupplyExhausted.
sell(uint256 tokensIn, uint256 minNativeOut) → uint256Sell back to the curve. Reverts ZeroAmount, InsufficientBalance, InsufficientOutput, NativeTransferFailed.
redeem(uint256 amount) → uint256Burn coins, take the proportional share of the vault. The only path out.
sweepFees()Permissionless retry of queued backing purchases.
setWrapped()Permissionless re-probe of the router. One-way, false → true only.
backingPerToken() → uint256The floor, scaled by 1e18. Monotonically non-decreasing.
redeemableFor(uint256) → uint256What a burn would pay right now.
quoteBuy(uint256) → (uint256,uint256)Tokens out and the fee, for a native amount in.
quoteSell(uint256) → (uint256,uint256)Native out and the fee, for a token amount in.
vault() / backing() / router() / wrapped() / routerReady() / pendingFees()Public state. vault, backing and router are immutable.
virtualNative() / feeBps() / maxSlippageBps()Immutable curve parameters.
nativeReserve() / tokenReserve() / totalSupply() / balanceOf / allowanceLive reserves and the ERC-20 surface (transfer, approve, transferFrom, 18 decimals).
event Bought / Sold / Redeemed / BackingBought / BackingBuyFailedFollow BackingBought(nativeSpent, assetReceived) to reconstruct a vault.
PrintVault
balance() → uint256Backing currently held, in units of the asset.
payout(address,uint256)Reverts OnlyCoin() for every caller but the coin. Reverts TransferFailed() if the asset transfer returns false.
coin() / asset()Both immutable, set once at construction.
event PaidOut(address indexed to, uint256 amount)The only event, because there is only one way out.
Integrating
Reading a board, priced and floored, with nothing but an RPC endpoint:
// ethers v6
const factory = new Contract(FACTORY, [
"function pairCount() view returns (uint256)",
"function boardRow(uint256) view returns (address,address,address,uint64,string,string,uint256,uint256,uint256,uint256,uint256)"
], provider)
const n = await factory.pairCount()
for (let i = 0n; i < n; i++) {
const [coin,,,, name, symbol, supply, vault, bpt, nr, tr] = await factory.boardRow(i)
const price = Number(nr) / Number(tr) // native per coin, spot on the curve
const floor = Number(bpt) / 1e18 // asset units per coin
console.log(symbol, { price, floor, vault })
}
Buying, with a quote and an explicit slippage bound:
const coin = new Contract(COIN, [
"function quoteBuy(uint256) view returns (uint256,uint256)",
"function buy(uint256) payable returns (uint256)",
"function redeem(uint256) returns (uint256)",
"function redeemableFor(uint256) view returns (uint256)"
], signer)
const amountIn = parseEther("0.5")
const [out] = await coin.quoteBuy(amountIn)
const minOut = out * 99n / 100n // your own 1% tolerance
await coin.buy(minOut, { value: amountIn })
Two things to get right. indexOfCoin(addr) returns index + 1, so zero
means "not a Print coin" rather than "the first one" — check it before trusting an address
someone handed you. And backingPerToken() is scaled by 1e18 on top of the
asset's own decimals; divide by 1e18 to get asset units per coin, then by the asset's
decimals to get a human number.
Security model
What is deliberately absent is the substance of the design, so it is worth enumerating.
On any of the four contracts. No Ownable, no roles.
No proxy, no delegatecall, no implementation slot.
Nothing can halt trading or redemption.
Supply is written once, in the constructor.
One function, callable only by the coin, only on a burn.
Never custodies native or tokens at any point.
Reentrancy. sell pays the seller with a raw call before spending the
fee, so a malicious recipient gets control mid-transaction. Reserves and balances are all
written before that call, so a reentrant sell or redeem sees
correct state and simply trades against it — and every path a reentrant call can reach still
preserves the invariant, which is what the fuzz test asserts. The vault's
payout is the last statement in redeem, after supply is reduced.
Trust in the asset. The largest residual assumption is the backing token itself. A
fee-on-transfer or rebasing asset will deliver less to the vault than the swap reported, and
a malicious asset contract could refuse transfers out — payout reverts
TransferFailed() in that case. Print does not and cannot vet the assets people
pair to. Check what a coin is backed by before you buy it.
Not audited. No third party has reviewed this code. The test suite is described below and is the only assurance currently on offer.
The test suite
22 tests, all passing. 17 against local mocks, and 5 forked against live chain 4663 — a mock cannot tell you whether the chain's real DEX behaves the way you assumed, and in this case it did not.
testFuzz_BackingPerTokenNeverFallsRandomised buy/sell/redeem sequences, 256 runs, asserting the ratio never falls between consecutive operations.
test_RedeemLeavesEveryoneElsesBackingUnchangedThe proportionality property, directly.
test_EveryoneSells_VaultSurvivesAndStillRedeemsFull exit of every holder; the vault still pays out correctly.
test_PriceCanFallButBackingCannotSeparates the two numbers explicitly.
test_NoMintFunctionExistsAsserts the absence, rather than assuming it.
test_LaunchingWithNoRouterStillTradesA coin whose asset has no pool still trades; fees queue.
test_LiveSwapFillsTheVault forkReal fee, real V4 swap, real asset in the vault, on chain.
test_LiveRedeemReturnsRealAsset forkBurn on a forked chain and receive the real token.
test_Diagnose_SlippageBySize forkProduced the fill table in the Slippage section.
1. router.WETH() outside the probe. With no DEX deployed, every buy and
sell would have reverted — the extcodesize check fires before the call and is
not catchable. Fixed with a raw staticcall.
2. A placeholder quote of 1. Multiplied by the slippage factor it rounded to zero,
hit the zero-guard, and silently skipped every swap. Fixed by quoting from V4
extsload.
3. Sentinel collision on address(0). The adapter correctly reports
WETH() == address(0) because native is currency0 — but the coin read that as
"no router" and bailed. Fixed with a separate routerReady flag.
All three passed against mocks. This is the argument for forked tests, not a story about three fixes.
Contracts
Deployed on Robinhood Chain, chain id 4663. Coins and vaults are created per launch and should be resolved from the factory rather than hardcoded.
View the contracts on GitHub Open source, MIT. 22 tests, 5 run against the live chain.
0x2513271927998159670495ed24f58be49630365ddeploys every pair, holds nothing0xdc2ddcf89ca36d4c33ef1d7cc7f866bd01375e99the swap venue fees are spent throughA router is bound to one pool, so each stock has its own router and factory. Same contracts, same code, one pair per backing asset:
0x999c124879a9D9CB8717E0A85E5c4D7453586E6drouter 0x0F2e0a251C1AA4059C7A2DE927d0e82B97e173bb0x534A3d7d7725b226cC7A5110Ad89D6eB850A0E0Frouter 0x201eB9f02A76F49F95FE674e3866e00F12D984430x72CD1b9F2Bc8175A6416eA9983A3b96E660c268frouter 0x3b6EE3968f667C72D8F3D760Ae622836073690c20x995bD26540B94F755a724A814EEECD4C6E11faA8router 0xb6d6DeBDaf254b31696B1B3d7C51669660EF3B980x7a3D87181FB467d17C09cee76e654D6cfe6fE221router 0x7bd961756214321f458FAB77126EE5F5B4Fc20bB0x4d3CE2D0DE6F2197b0F2d56915DA4E19146893a7router 0x2A9B7789509A2A8981B8d3a9335489d39E996E7e0x8590CdEc87c1f6C70cE59f943D15E3e5Ff3996Ccrouter 0x0A0B4eE21F7847880112B5aC3f7349B236A2c6660xa5107e060F88b49AA88DF727c26D6c92416680D7router 0xBAc7239954666eceb972f6EbDf6d3fC4A90b5F2f0x072C7cb5855c7c0DB5a566B3E0e8A5Aa827a4e82router 0x06093Bd6dB0561C98864Eb5654c382b047EF67dePer launchresolve via boardRow(i) or the Launched eventPer launchdeployed by its coin, address in the same rowSupporting infrastructure this depends on, verified on chain rather than taken from a registry:
0x8366a39CC670B4001A1121B8F6A443A643e409510x58daec3116aae6D93017bAAea7749052E8a04fA7Live pools on this chain quote currency0 = address(0) — the native unit
directly, with no wrapping step.
Network
Robinhood Chain46630x1237ETH18Arbitrum Orbit L2, EVM equivalenthttps://rpc.mainnet.chain.robinhood.comhttps://robinhoodchain.blockscout.com46630rpc.testnet.chain.robinhood.comStatus
Where things stand, said here rather than discovered.
The board reads the chain directly
Every row is boardRow read from the factories above when the page loads, with
no indexer in between. Price comes from each coin's curve, the floor from its vault and the
asset's pool, and 24h volume from the coin's own trade events.
Unaudited
No third party has reviewed the contracts. The test suite is what exists.
Risks
Backing per coin cannot fall. What the asset is worth absolutely can.
A floor is not a bid. The coin can and often will trade under its backing.
Anyone can pair to anything. Fee-on-transfer and rebasing assets misbehave.
Names and symbols are not unique and not verified.
You receive the token, not dollars. Selling it is your problem.
Unreviewed code holding real value. Size your exposure accordingly.
A coin paired to something illiquid may hold its purchases as pendingFees for
a long time before they reach the vault. Pairs are launched by anyone with a wallet, are not
endorsed, reviewed or vouched for, and nothing here is financial advice. Transactions are
submitted by your own wallet and are irreversible.
FAQ
Can the team sell what is in the vault?
No, and neither can the launcher. The vault has no owner and no withdraw function. Its one outbound call rejects every caller except the coin, and the coin only makes it when a holder burns.
Can backing per coin ever go down?
No. Fees only add to the vault, supply is fixed, and a redemption removes coins and backing in the same proportion. The algebra is in The invariant and the fuzz test asserts it over 256 randomised runs.
What is stopping the launcher from taking an allocation?
The supply is minted to the coin contract as curve inventory, not to the deployer. A launcher who wants coins buys them on the curve like anyone else, paying the same fee into the same vault.
What happens when the curve sells out?
Further buys revert with SupplyExhausted(). Nothing mints, and the ratio is
untouched. Sells continue to work and return inventory to the curve.
Do I have to redeem to get value out?
No. Sell on the curve at the market price like any coin. Redeeming is the option that exists for when the market price is worse than the backing.
If I redeem, do other holders lose?
No. You take your slice and your coins are burned with it, so everyone else's backing per coin is exactly what it was. That is the whole content of the invariant.
Can a coin change what it is paired to?
No. backing is immutable, set in the constructor. Nor can the
fee, the slippage band, the virtual reserve or the supply change.
What if the backing asset has no pool yet?
Trading still works. Purchases queue in pendingFees and anyone can retry them
with sweepFees() once a pool exists. Nothing is lost and nothing is refundable.
Why is the slippage band so wide?
Because the guard compares against a spot quote, which ignores pool fees and price impact and is therefore always optimistic. A tight band would queue purchases forever rather than protect anyone. See Slippage for the measured fills.
Who pays gas for the backing purchase?
The trader, as part of their own trade. There is no keeper and no off-chain process the
system depends on — sweepFees() exists only as a permissionless retry.
Is there a Print token?
Print is the launchpad. Anything on the board is a coin somebody launched on it.