for devs

Contracts

Live on Base mainnet, chain ID 8453, since block 52,206,460. Every contract below is verified on BaseScan: click an address to read its source.

Addresses

Rule modules

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

contractrole
HookFunLaunchpadthe one transaction launch, with the chosen rules, launchWithEth included
HookFunHook (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, Aerodrome Slipstream, Uniswap v4 and V2 style venues

Your coin's contract

Every coin launched here is a HookFunToken, and every one shows as verified without anyone submitting it. The reason is technical: HookFunToken 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 BaseScan 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 PoolManager0x498581fF718922c3f8e6A244956aF099B2652b2b
Uniswap v4 StateView (read only)0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71
Uniswap v4 Quoter0x0d5e0F971ED27FBfF6c2837bf31316121532048D
Uniswap Universal Router (2.0)0x6fF5693b99212Da76ad316178A184AB56D299b43
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
Uniswap V3 factory (measurement only)0x33128a8fC17869897dcE68Ed026d694621f6FDfD
Aerodrome Slipstream factories (measurement only)0x5e7BB104d84c7CB9B682AaC2F3d509f5F406809A, 0xf8f2eB4940CFE7d13603DDDD87f123820Fc061Ef, 0xaDe65c38CD4849aDBA595a4323a8C7DdfE89716a
Uniswap V2, SushiSwap V2, classic Aerodrome factories (measurement only)0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6, 0x71524B4f93c58fcbF659783284E38825f0622859, 0x420DD381b31aEf6683db6B902084cB0FFECe40Da
Chainlink ETH/USD (8 decimals)0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70
Chainlink BTC/USD (8 decimals)0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F
KyberSwap router (the ETH leg of a dev buy on a token pair)0x6131B5fae19EA4f9D964eAc0408E4408b66337b5

The anchors are WETH (native ETH counts as WETH), USDC (held at $1) and cbBTC. Anything that reads a Chainlink feed calls decimals() instead of assuming it.

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: $15,000 if listed, $50,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 (KyberSwap, 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 HookFunHook 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 net, 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.