for devs

Contracts

For Ink mainnet, chain ID 57073, deployed at block 57796016. Every contract is verified on Ink Explorer (Blockscout): click an address to read its source.

Addresses

Rule modules

What gets deployed, in a single run (contracts/script/DeployInk.s.sol):

contractrole
OctopadLaunchpadthe one transaction launch, with the chosen rules, launchWithEth included
OctopadHook (the hook)takes each launch's fee on the pair asset side of every swap, and runs the launch's trading rules
FeeVaultV2holds every launch's liquidity forever, collects and splits fees
15 rule modulesthe trading rules a launch can pick (see Trading rules, below)
QuoteRegistrypair asset eligibility: two bars, plus allow/deny overrides
CompositeQuoteGateasks every venue and keeps the best answer
UniV3QuoteGate, SlipstreamQuoteGate, SampledV4QuoteGate, V2QuoteGatedepth measurement on Uniswap V3, Velodrome Slipstream, Uniswap v4 and V2 style venues

Your coin's contract

Every coin launched here is a OctopadToken, and every one shows as verified without anyone submitting it. The reason is technical: OctopadToken takes no constructor arguments (it calls back the launchpad for its name, ticker and supply), so every coin from a launchpad has the same bytecode, apart from a few values set at launch. Verifying one coin on Ink Explorer is enough for the explorer to recognize all the others, including ones that don't exist yet.

What that verified source says, and so what it can't do: fixed supply set once in the constructor, no mint function, no transfer tax, no blacklist, no pause, no owner. One thing is set at launch: if the coin's rules watch transfers (Bag cap, Hot potato), the coin reports each transfer to the hook (host()), which can refuse one that breaks a rule. A coin without such a rule has no host and calls nobody.

External contracts used

Uniswap v4 PoolManager0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32
Uniswap v4 PositionManager0x1b35d13a2e2528f192637f14b05f0dc0e7deb566
Uniswap v4 StateView (read only)0x76fd297e2d437cd7f76d50f01afe6160f86e9990
Uniswap v4 Quoter0x3972c00f7ed4885e145823eb7c655375d275a1c5
Uniswap Universal Router (2.0)0x112908dac86e20e7241b0927479ea3bf935d1fa0
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
Uniswap V3 factory (measurement only)0x640887A9ba3A9C53Ed27D0F7e8246A4F933f3424
Velodrome Slipstream factories (measurement only)0x04625B046C69577EfC40e6c0Bb83CDBAfab5a55F, 0x718E46d0962A66942E233760a8bd6038Ce54EdCD
Velodrome classic, InkySwap V2, DYORSwap, Uniswap V2 factories (measurement only)0x31832f2a97Fd20664D76Cc421207669b55CE4BC0, 0x458C5d5B75ccBA22651D2C5b61cB1EA1e0b0f95D, 0x6c86ab200661512fDBd27Da4Bb87dF15609A2806, 0xfe57A6BA1951F69aE2Ed4abe23e0f095DF500C04
Chainlink ETH/USD (8 decimals)0x963d5d3aD2Dfd3fe759d376fF62A0963176DBdF5
Chainlink BTC/USD (8 decimals)0x671A7714C8366F7E8732A230bfEFB69B2ab6f016
Chainlink L2 sequencer uptime0xFB6acA74A4069b69C4383e8BE8f7D34e4aFeC3Fb
LI.FI Diamond (the ETH leg of a dev buy on a token pair)0x864b314D4C5a0399368609581d3E8933a63b9232

The anchors are WETH (native ETH counts as WETH), USDT0, USDC and USDG (each held at $1) and kBTC. Anything that reads a Chainlink feed calls decimals() instead of assuming it. Ink's ETH and BTC feeds update at least hourly, so the feed age bound is set well past an hour.

Useful calls

Before launch

// Never reverts. Says whether the asset would be accepted, and if not, why.
registry.status(address quote)
  returns (bool eligible, Decision decision, Report report)

// The bar this asset must clear: $10,000 if listed, $25,000 otherwise.
registry.requiredDepthUsdE8(address quote) returns (uint256)

Registering a v4 pool

Every other venue can be looked up: a factory answers getPair(a, b) or getPool(a, b, fee). The Uniswap v4 singleton can't. A pool there is identified by the hash of its key, and the key holds a 160 bit hook address: you can verify it, never enumerate it. So a v4 pool has to be registered once for the gate to see it. Anyone can do it, once, for everyone.

v4Gate.registerPool(PoolKey key) returns (address token)
v4Gate.registerLaunch(address token) returns (address)   // for a coin launched here
v4Gate.probe(PoolKey key) returns (uint256 usdE8, address anchor)   // read only, changes nothing

Nothing like this for V2 style venues, Uniswap V3 or Slipstream: they're looked up directly, so a coin with a market there is usable as soon as its address is pasted.

Launch

launchpad.launch(LaunchConfig cfg, RulesConfig rules) payable returns (address token, bytes32 poolId)
// Dev buy paid in ETH: an allowed router (the LI.FI Diamond, the Universal Router) swaps it into
// the pair asset, inside the launch.
launchpad.launchWithEth(LaunchConfig cfg, EthDevBuy swap, RulesConfig rules) payable returns (address token, bytes32 poolId)
launchpad.predictToken(address creator, bytes32 salt) returns (address)
launchpad.launchFee() returns (uint256)

Beyond the obvious, LaunchConfig holds:

fieldrole
creatorBpsshare of supply you keep, capped at 20%
feeRoute0 keeps your fees, 1 buys back the coin and burns it, 2 gives them to your holders (final, needs a distributor)
feeRecipientzero means the launching wallet
devBuyQuotepair asset amount spent to buy your coin in the pool's first trade. Needs an approval to the launchpad first
devBuyMinOutthe minimum you accept for that buy. Nobody can front-run it: this floor guards against you and the pool disagreeing on the opening price

RulesConfig is { RuleSpec[] rules; bool graduates; int24 graduationTick }: up to six { rule, params } pairs (an allowed module and its ABI encoded params), and optionally a price at which every rule switches off for good. With no rules, the list is empty.

After launch

// Anyone can call. Funds only go to the recorded recipient or to the pot, the sink
// and the treasury, or to the burn.
feeVault.collect(address token)

// Creator only. Takes effect at the next collect. HolderRewards (2) is final.
feeVault.setFeeRoute(address token, FeeRoute route)
feeVault.setFeeRecipient(address token, address recipient)

// Reads
launchpad.launchOf(address token)                              // public mapping: an unnamed tuple
feeVault.launchOf(address token) returns (Launch)              // includes the PoolKey
feeVault.feeRouteOf(address token) returns (FeeRoute)
feeVault.feeRecipientOf(address token) returns (address)
feeVault.owed(address token, address who) returns (uint256)    // a payout that couldn't go out
feeVault.claim(address token)                                  // claim it
feeVault.buybackSink() returns (address)                       // where the protocol share goes

// All in the pair asset
feeVault.pendingFees(address token) returns (uint256)          // taken by the hook, not collected yet
feeVault.totalFees(address token) returns (uint256)            // everything taken since launch
feeVault.feeRateOf(address token) returns (uint24)             // the launch's rate, in hundredths of a bip

The hook

Every launch opens a pool with no Uniswap LP fee, with OctopadHook as its hook. The hook takes the launch's rate on the pair asset leg of every swap (what a buyer pays, or what a seller receives), never on the coin:

swapthe pair asset isthe hook takes
buy, exact inputthe amount paidin × rate, before the swap
buy, exact outputwhat the pool chargesin × rate / (1 − rate) on top, after the swap
sell, exact inputwhat the pool pays outout × rate from it, after the swap
sell, exact outputthe amount asked forout × rate / (1 − rate) extra to the pool, before the swap

Every row comes to the same rate of the gross pair asset amount. The fee is minted to FeeVaultV2 as a Uniswap v4 claim (ERC-6909) during the swap. No token moves mid swap, so no token can block trading, and collect turns it into the pair asset and splits it. Only the launchpad can open a pool on the hook, and the rate and rules are written once, at that moment. Nothing changes them afterwards. A rule can add a rate to a swap (Fading exit tax, Swell fee): it's taken in the same place, the total is capped at 50%, and it goes to the launch. The one exception is the King of the hill share, which the hook credits to the current king, who claims it with hook.claim(currency, to). The vault's buyback swaps are the only ones the hook doesn't charge.

Only the vault can add liquidity to a launch's pool. A third party's range position would be a limit order, a way to trade against the pool without a swap, and so without its rules.

// One per charged swap. The swap's total fee is toLaunch + toPayee; payee is zero unless a rule paid an account; volume is the gross pair asset leg.
event FeeTaken(bytes32 indexed poolId, address indexed trader, uint256 toLaunch, address indexed payee, uint256 toPayee, uint256 volume);
event PoolRegistered(bytes32 indexed poolId, address indexed coin, uint24 fee, bool quoteIsCurrency0);
event RulesSet(bytes32 indexed poolId, address[] rules, bool graduates, int24 graduationTick);
event Graduated(bytes32 indexed poolId, int24 priceTick);
event Claimed(address indexed account, Currency indexed currency, address to, uint256 amount);

hook.feeOf(PoolId id) returns (uint24)                 // the launch's rate
hook.rulesOf(PoolId id) returns (RuleSlot[])           // the pool's rules, in run order
hook.configOf(PoolId id) returns (PoolConfig)          // coin, creator, graduation target, record price
hook.owed(address account, uint256 currencyId) returns (uint256)   // pending rule winnings

Routers that go straight to Uniswap v4 (the Universal Router, Uniswap's quoter) handle hooked pools natively. An aggregator has to integrate a hook before routing through it. So the site trades these coins through the Universal Router itself, with empty hookData.

Trading rules

A launch can pick up to six rules, set at launch, forever: Anti-sniper, Bag cap, Fading exit tax, Anti-flip, Whale grip, Swell fee, King of the hill, Hot potato, Ping-pong, Tide, Market hours, Conviction cap, Club only, Entangled, Duel. Each is a small module the hook calls around every swap (and for a few, on every coin transfer): it can refuse the trade, add a fee, or keep score. For rules, the trader is the wallet that signed the transaction (tx.origin), whatever the router. The creator's dev buy is marked as the launch, and rules that would make a launch impossible let it through. No combination of rules can stop a holder from selling forever: any rule that blocks sells is limited in time.

The owner allows modules once (hook.setRule); a launch can only name allowed modules. The full reference (callbacks, params, limits) is in the repo's contracts/RULES.md, and each module's header documents its params exactly.

Holder rewards

feeVault.holderRewardsDistributor() returns (address)   // zero until set; route 2 is refused until then
feeVault.rewardsPot(address token) returns (uint256)    // pair asset waiting for the coin's holders
feeVault.pendingRewards(address token) returns (uint256) // always zero: route 2 never sells
feeVault.pendingBuyback(address token) returns (uint256) // pair asset held back by the price cap

// Distributor only. Lengths must match; the total can't exceed the pot.
feeVault.payHolderRewards(address token, address[] recipients, uint256[] amounts)

event HolderRewardsAdded(address indexed token, uint256 coinSold, uint256 soldFor, uint256 added, uint256 pot);
event HolderRewardsPaid(address indexed token, address indexed quote, address[] recipients, uint256[] amounts, uint256 total, uint256 pot);

Where the lock really is

There's no lock contract and no locked LP token, because Uniswap v4 has no LP tokens at all. A position is a storage slot keyed by (owner, tickLower, tickUpper, salt). The usual "locked liquidity" detectors look for LP tokens sent to a burn address or a locker. Here, they find nothing of the kind.

What holds instead is a property of FeeVaultV2, and its published source lets you check it instead of trusting it: no code path in the vault passes a negative liquidityDelta. Fee collection calls modifyLiquidity with a delta of exactly zero. No withdraw, no emergency exit, no owner function to add one. The float can't come back out, not through the creator, not through us.

Owner powers

The owner can set the depth bars, fee bounds, launch cost, treasury, buyback sink, rewards distributor (replace it, never remove it), the curated list, the gates and their params, the rule modules new launches can pick, and the holiday calendar for the Market hours rule. It can't touch a launch's liquidity, change the creator share, rate or rules of an existing launch, or redirect a creator's fees.

Ownership transfer is two step everywhere: a handover that's never accepted leaves the current owner in place instead of sending the protocol to a wrong address.

The owner should be a multisig behind a timelock. The contracts don't enforce that themselves.

None of this is audited.