Skip to main content

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​

currenciesthe token and the stock; which is currency0 depends on address order, read poolKey() from the vault or stockIsCurrency0InTokenPool() from the treasury
feelpFee 2000 (0.20%), frozen per pool; the factory accepts 1 to 3000 for future launches
tick spacing60
hookHedgeFunV2Hook, version() 3, address flags 0x28CC
createdat graduation, by the factory, initialised at the curve's terminal price
registrationthe 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.

positioncontentsrange
full range, salt 070% of the raise in stock, with about 8.20% of supply in tokens at the reference settingthe usable extremes
single-sided, salt 1about 12.49% of supply in tokens, no stockfrom 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​

callbackwhat it does
beforeInitializeanswers 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
beforeAddLiquidityadmits only the pool's vault (NotSeeder otherwise). A third-party position would be a way to sell without paying the tax
beforeRemoveLiquidityreverts HookNotImplemented for everyone. That is the lock
beforeSwapon 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
afterSwapon 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. HedgeFunToken is a plain ERC20 and the hook sees swaps only.
  • It does not run an opening window on a graduated pool: registerGraduatedWithVault refuses one, so the buy rate is the flat tax from the first swap, and buyRateBps(poolId) equals sellRateBps(poolId).
  • It does not spike the sell rate after a buy-back: spikeBps is 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​

contracteventwhat it tells an indexer
factoryLaunched(id, symbol, token, treasury, hook, stock, creator)a new launch
factoryCurveLaunched(id, curve, saleBps, virtualStock)the curve and its fixed target
factoryGraduated(id, sqrtPriceX96, liquidity, stockSeeded, tokenSeeded, tokenBurned)the crossing; tokenBurned is 0 on v2
factoryGraduationCapitalSplit(id, lpStock, treasuryStock, treasuryBooked)the 70/30 amounts and whether the treasury booked at once
factoryListed, ListingGatesSet, DefaultsSet, PublicLaunchSet, LauncherSet, BandCeilingSetowner actions
curveBought(caller, recipient, stockSpent, tokensOut, burned)a curve buy; burned is the opening premium
curveSold(caller, recipient, tokenIn, stockOut, fee)a curve sell
curveTradeFeesAccrued(buying, fee, protocolFee, creatorFee, treasuryFee)the split of one trade's tax
curveFeesClaimed(recipient, amount), Ready(), Released(stockAmount, tokenAmount), OpeningTaxExemptionsFixed(recipients)claims; the graduation hand-off; the frozen exemptions
curve deployerCurveConfigSet, OpeningTaxExemptionsSetthe creator's window and exemptions
treasury deployerStrategyKindSet, EngineConfigSet, KindRegistered, EngineKindRegistered, PolicyRegistered, PolicyDisabled, LpBpsSet, TreasuryCodeBoundthe creator's kind choices and the registry
hookRegistered(id, token, stock, treasury, protocol, creator, rates)the pool's frozen rates
hookTaxed(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
hookSwept(id, tokenBurned, stockToTreasury, stockToProtocol, stockToCreator)a payout; tokenBurned is 0 on a vault pool
hookClaimed, CreatorProposed, CreatorVetoed, CreatorChanged, ProtocolChangedpayout administration
vaultSeeded(liquidity, amount0, amount1), SurplusSeeded(lower, upper, liquidity, tokens), SeedTokensLocked(amount)the two positions
vaultFeesCollected(stockToTreasury, tokenBurned)pool fees: stock to the buy-back budget, tokens burned
treasury, Lots familyLotBooked(id, qty, cost, fromTax), ProfitTaken(qty, cost, price, usdgToReserve, stockToBurn), Stopped(qty, cost, price), Buyback(stockSpent, tokenBurned)the lot rule
treasury, Spot and RebalanceStrategyExecuted, KeeperRewardPaid, GainToBuyback(gain, toBuyback, avgCost, price)an engine action, its reward and the share of the gain sent to the burn
trade routerBought(id, buyer, paymentAsset, recipient, paymentIn, tokensOut, stockRefund), Sold(id, seller, outputAsset, tokensSpent, outputAmount, tokenRefund)routed trades
native routerNativeBought(id, buyer, nativeIn, tokensOut, stockRefund), NativeSold(id, seller, nativeOut, tokenRefund)the same trade, attributed to the user
launch routerNativeLaunchedAndBought(id, creator, launchFee, nativeBuyIn, tokensOut, stockRefund)a one-payment launch
upgrade controllerUpgradeScheduled(treasury, implementation, codeHash, dataHash, readyAt), UpgradeExecuted, UpgradeCancelledthe 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​

errorfromwhen
Restated()factorya default, listing, gate or registered choice moved since predict
NotListed(), NotOpen(), BadRequest()factorythe 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()factorythe terms would leave the v4 seed empty, tiny or unpriceable; the seed spent more than its budget
NotReady()factorygraduate(id) on a curve that is not Ready
BadCurveConfig()curve deployera saleBps other than 7931, or a window over 180 s
BadEngineConfig(), StopInsideExecutionFriction(), BadPolicy()treasury deployerengine words outside their floors; a stop inside slippage + pool fee + reward; a disabled or mismatched policy
TreasuryDeployFailed()treasury deployera constructor refused what the registry could not check by name (Cycle's rung floor)
StageChanged(actual)routerthe curve graduated between the quote and the call; quote again
Expired(), TooLittle(got), TooLittleStock(got)router, curvepast the deadline; under minFinalOut; under minStockReceived
PartialFill(spent)routerinput left unused and allowPartialFill was false
BadPath(), WrongPool(), BadAmount()routera discontinuous or repeated route; a pool the factory's V3 factory did not create; a zero amount
Closed(), Slippage(), Insolvent(), GraduationFailed(), UnsupportedTransfer()curvetrading 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()hookliquidity from anyone but the vault; a pool the factory never registered; a liquidity removal; an exact-output sell
PartialFillRefused()hook, wrapped by v4an exact-input buy stopped by a price limit
NotDue(), Unhealthy(), Cooldown(), Slippage()treasurynothing to do; health() is false; the buy-back pace; a sale that moved nothing
UseBuyback(), UseExecute()treasuryBuy-back has no execute(); the inherited v1 selectors are off on v2
AlreadySeeded(), NotSeeded(), Busy(), InexactTransfer()vaulta second seed; a collect before seeding; a reentrant callback; a transfer that did not move the exact amount
NotReady(), InvalidUpgrade(), NotOwner()upgrade controllerbefore 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.