StainFun

04 — Docs

How StainFun
works

What launching a coin against another coin means, what decides what you can pair with, and where every fee goes.

Contracts

Deployed on Ink mainnet, chain ID 57073, at block 55754890. Every one of them is verified on Ink's Blockscout explorer, so the code below the links is the code that runs.

Your coin's own contract

Every coin launched here is a PairToken, and every one of them shows as verified without anyone submitting it. That is not a courtesy — PairToken takes no constructor arguments (it calls back into the launchpad for its name, symbol and supply), so the creation bytecode is byte-identical every time. One instance was verified; the explorer matches all the others, including coins that do not exist yet.

What that verified source says, and what it therefore cannot do: fixed supply set once in the constructor, no mint function, no transfer tax, no blacklist, no pause, no owner.

External contracts it uses

Uniswap V3 factory (launch venue and measurement)0x640887A9ba3A9C53Ed27D0F7e8246A4F933f3424
Velodrome Slipstream factory (measurement only)0x04625B046C69577EfC40e6c0Bb83CDBAfab5a55F
Velodrome Slipstream factory #2 (measurement only)0x718E46d0962A66942E233760a8bd6038Ce54EdCD
InkySwap factory (measurement only)0x458C5d5B75ccBA22651D2C5b61cB1EA1e0b0f95D
DyorSwap factory (measurement only)0x6c86ab200661512fDBd27Da4Bb87dF15609A2806
SquidSwap factory (measurement only)0x63b54dBBD2DAbf89D5c536746e534711f6094199
Uniswap V2 factory (measurement only)0xfe57A6BA1951F69aE2Ed4abe23e0f095DF500C04
Velodrome V2 factory (measurement only)0x31832f2a97Fd20664D76Cc421207669b55CE4BC0
Chainlink ETH/USD0x963d5d3aD2Dfd3fe759d376fF62A0963176DBdF5
Chainlink BTC/USD0x671A7714C8366F7E8732A230bfEFB69B2ab6f016

Calls worth knowing

Before launching

// Non-reverting. Returns whether it would be allowed, and why not if not.
registry.status(address quote)
  returns (bool eligible, Decision decision, Report report)

// The bar this particular asset has to clear — $10,000 if listed, $25,000 if not.
registry.requiredDepthUsdE8(address quote) returns (uint256)

Launching

launchpad.launch(LaunchConfig cfg) payable returns (address token, address pool)
launchpad.predictToken(address creator, bytes32 salt) returns (address)
launchpad.launchFee() returns (uint256)

LaunchConfig carries, beyond the obvious:

fieldwhat it does
creatorBpsshare of supply kept back for you, capped at 20%
feeRoute0 keeps your fees, 1 buys the coin back and burns it
feeRecipientzero means the launching wallet
devBuyQuotepair-asset amount spent buying your own coin as the pool's first trade; needs an approval to the launchpad first
devBuyMinOutleast you will accept from that buy — nobody can front-run it, so this guards against you and the pool disagreeing on the opening price

After launching

// Anyone can call. Proceeds only ever reach the recorded recipient, the sink and the treasury.
feeVault.collect(address token)

// Creator only. Both take effect from the next collect.
feeVault.setFeeRoute(address token, FeeRoute route)
feeVault.setFeeRecipient(address token, address recipient)

// Reads
launchpad.launchOf(address token)                              // a public mapping: an unnamed tuple
feeVault.launchOf(address token) returns (Launch)              // includes the pool address
feeVault.positionOf(address token) returns (liquidity, owed0, owed1)
feeVault.feeRouteOf(address token) returns (FeeRoute)
feeVault.feeRecipientOf(address token) returns (address)
feeVault.owed(address token, address who) returns (uint256)    // a payout that could not be pushed
feeVault.claim(address token)                                  // collect one

Where the liquidity lock actually lives

There is no lock contract and no locked LP token. A Uniswap V3 position only becomes an NFT when Uniswap's position manager wraps it; underneath it is a slot in the pool keyed by (owner, tickLower, tickUpper), and the fee vault minted into that slot directly. The usual "liquidity locked" detectors look for LP tokens or position NFTs sent to a burn address or a locker, and here there is nothing of the sort for them to find.

What holds instead is a property of FeeVault, and with its source published you can check it rather than believe it: no code path in the vault ever calls burn with a non-zero amount. Collecting fees calls burn with exactly zero — which only moves earned fees into the position's tokensOwed — and then collect. There is no withdraw, no emergency exit, and no owner function that adds one. Anyone can read the position with pool.positions(keccak256(abi.encodePacked(feeVault, tickLower, tickUpper))). The float cannot come back out — not by the creator, not by us.

Owner powers

The owner can retune the depth bars, the fee bounds, the launch fee, the treasury, the buyback sink, the curated list, the gates and their parameters. It cannot touch a launch's liquidity, change an existing launch's creator share, or redirect a creator's fees.

Ownership transfer is two-step everywhere: a handover that is never accepted leaves the current owner in place rather than sending the protocol to a wrong address.

Today the owner is a single EOA. It should be a multisig behind a timelock, and the contracts do not enforce that themselves.

None of this has been audited.