Pools, events and errors
Uniswap v4 pools
Every graduated launch trades in one Uniswap v4 pool: the token against its stock, fee 0.20%, tick spacing 60, on
HedgeFunV2Hook. One hook instance serves every pool; each pool has its own ledger inside it.
If you have integrated other v4 launch hooks, three things here are different and worth checking before you assume them: fees are held in the hook until anyone sweeps them; the pool holds two locked positions, not one; and the buy tax is taken before the swap, so a price-limited buy reverts.
The pool
| currencies | the token and the stock; which is currency0 depends on address order, read poolKey() from the vault or stockIsCurrency0InTokenPool() from the treasury |
| fee | lpFee 2000 (0.20%), frozen per pool; the factory accepts 1 to 3000 for future launches |
| tick spacing | 60 |
| hook | HedgeFunV2Hook, version() 3, address flags 0x28CC |
| created | at graduation, by the factory, initialised at the curve's terminal price |
| registration | the factory registers the pool and its vault with registerGraduatedWithVault before initialisation; a pool the factory did not register cannot be initialised on this hook |
The two locked positions
V2LiquidityVault, one per pool, owns both positions directly; there is no LP token.
| position | contents | range |
|---|---|---|
| full range, salt 0 | 70% of the raise in stock, with about 8.20% of supply in tokens at the reference setting | the usable extremes |
| single-sided, salt 1 | about 12.49% of supply in tokens, no stock | from the next usable tick on the token side of spot to the usable extreme |
The vault has no function that removes liquidity, makes an arbitrary call or upgrades. collectFees() is its
only liquidity operation and runs with zero liquidity delta. Rounding residue stays in lockedSeedTokens and is
never reported as fees. Both positions count toward the pool's depth as the price moves into the surplus range.
The hook's callbacks
| callback | what it does |
|---|---|
beforeInitialize | answers only for a pool the factory registered, and only to the registered vault. Nobody opens a pool on this hook that the factory did not launch |
beforeAddLiquidity | admits only the pool's vault (NotSeeder otherwise). A third-party position would be a way to sell without paying the tax |
beforeRemoveLiquidity | reverts HookNotImplemented for everyone. That is the lock |
beforeSwap | on an exact-input buy (stock in, token out) by anyone but the treasury: takes floor(paid × taxBps / 10000) of the stock before the swap, mints it as a stock claim to the hook, emits Taxed, and lets paid − tax meet the pool. Returns a zero delta otherwise |
afterSwap | on a sell: takes the tax from the gross stock output. On an exact-output buy at the flat rate: takes r / (1 − r) of the stock input. On an exact-input buy: verifies the swap filled exactly paid − tax and reverts PartialFillRefused if it did not. The treasury's own swaps pass untaxed |
Permissions: BEFORE_INITIALIZE, BEFORE_ADD_LIQUIDITY, BEFORE_SWAP, BEFORE_SWAP_RETURNS_DELTA,
AFTER_SWAP, AFTER_SWAP_RETURNS_DELTA. Exact-output sells are refused (ExactOutputRefused).
Where fees sit
The tax is booked as ERC-6909 stock claims owned by the hook, per pool (accruedStock, owedProtocol,
owedCreator, owedTreasury). Nothing is paid per trade. sweep(poolId), callable by anyone, pays the
protocol's, the creator's and the treasury's shares in one call, each currency on its own leg; sweepTipBps is 0.
On a vault pool no fee is ever held in the token, so Swept.tokenBurned is 0.
The pool's own 0.20% fee accrues to the vault's positions in the input currency of each swap. collectFees()
moves the stock side to the treasury's buybackStock and burns the token side.
What the hook does not do
- It does not gate who may swap. Anyone may trade the pool through any v4 router.
- It does not tax transfers of the token.
HedgeFunTokenis a plain ERC20 and the hook sees swaps only. - It does not run an opening window on a graduated pool:
registerGraduatedWithVaultrefuses one, so the buy rate is the flat tax from the first swap, andbuyRateBps(poolId)equalssellRateBps(poolId). - It does not spike the sell rate after a buy-back:
spikeBpsis 0 on v2. - It does not convert fees. There is no token-denominated fee inventory and no owner conversion step.
The exact-input buy that reverts
Because beforeSwap has already taken the tax on the full payment, a buy that stops early at the caller's
sqrtPriceLimitX96 would have paid tax on stock it never traded. afterSwap refuses it with
PartialFillRefused(), which the PoolManager wraps as CustomRevert.WrappedError(hook, afterSwap.selector, PartialFillRefused(), HookCallFailed()). Bound the price with a minimum output, as HedgeFunV2TradeRouter does,
and the revert never occurs. Sells are taxed on what filled and may stop at a limit.
Errors shows how to decode it.
Events to index
| contract | event | what it tells an indexer |
|---|---|---|
| factory | Launched(id, symbol, token, treasury, hook, stock, creator) | a new launch |
| factory | CurveLaunched(id, curve, saleBps, virtualStock) | the curve and its fixed target |
| factory | Graduated(id, sqrtPriceX96, liquidity, stockSeeded, tokenSeeded, tokenBurned) | the crossing; tokenBurned is 0 on v2 |
| factory | GraduationCapitalSplit(id, lpStock, treasuryStock, treasuryBooked) | the 70/30 amounts and whether the treasury booked at once |
| factory | Listed, ListingGatesSet, DefaultsSet, PublicLaunchSet, LauncherSet, BandCeilingSet | owner actions |
| curve | Bought(caller, recipient, stockSpent, tokensOut, burned) | a curve buy; burned is the opening premium |
| curve | Sold(caller, recipient, tokenIn, stockOut, fee) | a curve sell |
| curve | TradeFeesAccrued(buying, fee, protocolFee, creatorFee, treasuryFee) | the split of one trade's tax |
| curve | FeesClaimed(recipient, amount), Ready(), Released(stockAmount, tokenAmount), OpeningTaxExemptionsFixed(recipients) | claims; the graduation hand-off; the frozen exemptions |
| curve deployer | CurveConfigSet, OpeningTaxExemptionsSet | the creator's window and exemptions |
| treasury deployer | StrategyKindSet, EngineConfigSet, KindRegistered, EngineKindRegistered, PolicyRegistered, PolicyDisabled, LpBpsSet, TreasuryCodeBound | the creator's kind choices and the registry |
| hook | Registered(id, token, stock, treasury, protocol, creator, rates) | the pool's frozen rates |
| hook | Taxed(id, selling, inToken, moved, tax, rateBps) | one pool trade's tax. moved is the stock the swap moved; on a buy the trader paid moved + tax |
| hook | Swept(id, tokenBurned, stockToTreasury, stockToProtocol, stockToCreator) | a payout; tokenBurned is 0 on a vault pool |
| hook | Claimed, CreatorProposed, CreatorVetoed, CreatorChanged, ProtocolChanged | payout administration |
| vault | Seeded(liquidity, amount0, amount1), SurplusSeeded(lower, upper, liquidity, tokens), SeedTokensLocked(amount) | the two positions |
| vault | FeesCollected(stockToTreasury, tokenBurned) | pool fees: stock to the buy-back budget, tokens burned |
| treasury, Lots family | LotBooked(id, qty, cost, fromTax), ProfitTaken(qty, cost, price, usdgToReserve, stockToBurn), Stopped(qty, cost, price), Buyback(stockSpent, tokenBurned) | the lot rule |
| treasury, Spot and Rebalance | StrategyExecuted, KeeperRewardPaid, GainToBuyback(gain, toBuyback, avgCost, price) | an engine action, its reward and the share of the gain sent to the burn |
| trade router | Bought(id, buyer, paymentAsset, recipient, paymentIn, tokensOut, stockRefund), Sold(id, seller, outputAsset, tokensSpent, outputAmount, tokenRefund) | routed trades |
| native router | NativeBought(id, buyer, nativeIn, tokensOut, stockRefund), NativeSold(id, seller, nativeOut, tokenRefund) | the same trade, attributed to the user |
| launch router | NativeLaunchedAndBought(id, creator, launchFee, nativeBuyIn, tokensOut, stockRefund) | a one-payment launch |
| upgrade controller | UpgradeScheduled(treasury, implementation, codeHash, dataHash, readyAt), UpgradeExecuted, UpgradeCancelled | the two-day notice and its outcome |
Curve, router and hook events are layers of one execution; count volume once. A native trade's underlying router
event names the native router as caller; attribute it to NativeBought.buyer. Keep three burns apart in a supply
chart: the opening premium (Bought.burned), buy-backs (Buyback.tokenBurned) and pool-fee burns
(FeesCollected.tokenBurned). Graduation burns nothing, and locked LP is not a supply reduction. Topic hashes are
in abi/SURFACE.md in the contracts repository; generate them against the deployed commit.
Errors
| error | from | when |
|---|---|---|
Restated() | factory | a default, listing, gate or registered choice moved since predict |
NotListed(), NotOpen(), BadRequest() | factory | the stock is not listed; public launch is closed and the caller is not an authorised launcher; a field is out of its bounds |
Unseedable(), SeedOverspent() | factory | the terms would leave the v4 seed empty, tiny or unpriceable; the seed spent more than its budget |
NotReady() | factory | graduate(id) on a curve that is not Ready |
BadCurveConfig() | curve deployer | a saleBps other than 7931, or a window over 180 s |
BadEngineConfig(), StopInsideExecutionFriction(), BadPolicy() | treasury deployer | engine words outside their floors; a stop inside slippage + pool fee + reward; a disabled or mismatched policy |
TreasuryDeployFailed() | treasury deployer | a constructor refused what the registry could not check by name (Cycle's rung floor) |
StageChanged(actual) | router | the curve graduated between the quote and the call; quote again |
Expired(), TooLittle(got), TooLittleStock(got) | router, curve | past the deadline; under minFinalOut; under minStockReceived |
PartialFill(spent) | router | input left unused and allowPartialFill was false |
BadPath(), WrongPool(), BadAmount() | router | a discontinuous or repeated route; a pool the factory's V3 factory did not create; a zero amount |
Closed(), Slippage(), Insolvent(), GraduationFailed(), UnsupportedTransfer() | curve | trading after graduation; under the caller's minimum; a solvency check; the graduation callback failed and the buy rolled back; a fee-on-transfer or surcharging stock |
NotSeeder(), WrongPool(), HookNotImplemented(), ExactOutputRefused() | hook | liquidity from anyone but the vault; a pool the factory never registered; a liquidity removal; an exact-output sell |
PartialFillRefused() | hook, wrapped by v4 | an exact-input buy stopped by a price limit |
NotDue(), Unhealthy(), Cooldown(), Slippage() | treasury | nothing to do; health() is false; the buy-back pace; a sale that moved nothing |
UseBuyback(), UseExecute() | treasury | Buy-back has no execute(); the inherited v1 selectors are off on v2 |
AlreadySeeded(), NotSeeded(), Busy(), InexactTransfer() | vault | a second seed; a collect before seeding; a reentrant callback; a transfer that did not move the exact amount |
NotReady(), InvalidUpgrade(), NotOwner() | upgrade controller | before readyAt; the candidate's code hash, calldata hash, config hash or schema does not match; a caller other than the owner |
PartialFillRefused, wrapped by v4
On an exact-input buy the hook has already taken the tax on the full payment in beforeSwap. If the swap then
stops at the caller's sqrtPriceLimitX96, afterSwap reverts PartialFillRefused() so the buyer is not taxed on
stock that never traded. The PoolManager wraps it: the revert a caller sees is
CustomRevert.WrappedError(address hook, bytes4 afterSwap.selector, bytes PartialFillRefused(), bytes HookCallFailed())
Decode the inner reason, or match on the WrappedError selector with the hook's address. A router that bounds
the price with a minimum output, as HedgeFunV2TradeRouter does, is never affected; a third-party router that
uses a price limit on a buy is. Sells are taxed on what actually filled and may stop at a limit.
Selectors for every error are in REFERENCE.md and abi/SURFACE.md in the contracts repository at the deployed
commit.