Addresses
| contract | address |
|---|---|
HookFunLaunchpad | 0xad62349AB34abD94374B2f381aF1109CC85dbedd |
HookFunHook (the hook) | 0xC746877710f3C359d9E756b7135E041ED7F028cC |
FeeVaultV2 | 0xc48b086DcE0aC55aFa1B8D79c61785BAdcbF4810 |
QuoteRegistry | 0x4764Bc4E1f92e25e294075b54d077a03E386d51F |
CompositeQuoteGate | 0x34F4a3e7971E7E78acb767170eFBf0AE68E2A864 |
UniV3QuoteGate | 0x414A2Ed7CD98835d19a02a922cE37219ac569a65 |
SlipstreamQuoteGate | 0xd6BFF11730C3b3f94f1bC4a1885D5c36E3cC2A18 |
SampledV4QuoteGate | 0x380f2343956Afdd1A7fBeA2e46A5e4807D778cE2 |
V2QuoteGate | 0x729B4bd45E91B4633F7DC1fF77Dd7F954b7fD80F |
Rule modules
What gets deployed, in a single run (contracts/script/DeployBase.s.sol):
| contract | role |
|---|---|
HookFunLaunchpad | the 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 |
FeeVaultV2 | holds every launch's liquidity forever, collects and splits fees |
| 15 rule modules | the trading rules a launch can pick (see Trading rules, below) |
QuoteRegistry | pair asset eligibility: two bars, plus allow/deny overrides |
CompositeQuoteGate | asks every venue and keeps the best answer |
UniV3QuoteGate, SlipstreamQuoteGate, SampledV4QuoteGate, V2QuoteGate | depth 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 PoolManager | 0x498581fF718922c3f8e6A244956aF099B2652b2b |
| Uniswap v4 StateView (read only) | 0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71 |
| Uniswap v4 Quoter | 0x0d5e0F971ED27FBfF6c2837bf31316121532048D |
| Uniswap Universal Router (2.0) | 0x6fF5693b99212Da76ad316178A184AB56D299b43 |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| 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:
| field | role |
|---|---|
creatorBps | share of supply you keep, capped at 20% |
feeRoute | 0 keeps your fees, 1 buys back the coin and burns it, 2 gives them to your holders (final, needs a distributor) |
feeRecipient | zero means the launching wallet |
devBuyQuote | pair asset amount spent to buy your coin in the pool's first trade. Needs an approval to the launchpad first |
devBuyMinOut | the 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:
| swap | the pair asset is | the hook takes |
|---|---|---|
| buy, exact input | the amount paid | in × rate, before the swap |
| buy, exact output | what the pool charges | in × rate / (1 − rate) on top, after the swap |
| sell, exact input | what the pool pays out | out × rate from it, after the swap |
| sell, exact output | the amount asked for | out × 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.
