Skip to main content

The six that carry your V3 subgraph

Byte-identical to Uniswap V3, emitted by every pool type:
STABLE pools emit the same shapes: ranges as MIN_TICK/MAX_TICK, and the Swap tick derived from the price — in tick space, zero means exactly 1:1, so emitting a literal zero would report a wrong price on every stable swap.

Handlers that need changing

V3’s four fields are uint8 (a four-bit denominator); these are shares out of ten thousand. A subgraph indexing this event needs its handler changed. Fee accounting needs one more input: the pool’s fee coin. The trading events keep V3’s shape, but a pool can take every fee in one fixed token (feeToken(), announced by FeeTokenChanged). On such a pool, fee growth and protocol fees accrue in that token only — a V3 handler that infers the fee side from a Swap’s direction books half the trades on the wrong side.

New pool events (no V3 counterpart)

STABLE adds AmplificationChanged(uint32 old, uint32 new) and AmplificationRampStarted(uint32 start, uint32 target, uint32 endTime). There is no IncreaseObservationCardinalityNext on the pool — the TWAP is a plugin, and that event lives on the oracle module.

Factory

This is your discovery stream — for pools listed by anyone, and for launchpad graduations (the launchpad’s own Graduated names the pool; the pool’s birth logs here). A pool born with a non-default fee coin emits FeeTokenChanged from its constructor, so that log lands before its PoolCreated.

The compatibility event

When governance has it switched on (compatibilityEvent(), toggled with CompatibilityEventChanged(bool)), the factory also emits a copy with Uniswap V3’s exact signature:
Do not build on the compatibility event. Subscribe to the native PoolCreated above. The V3-shaped copy carries no fee and puts the pool type — 0, 1 or 2 — in the slot a V3 handler reads as a fee, so anything that renders or routes on that field is wrong from the first pool. It is also a governance switch: it may be off on any deployment, and an indexer that depends on it stops discovering pools the moment it is. It exists only so third-party tooling nobody can modify still finds pools.

Governance and listing

Listers gaining or losing the pool-creation role show up as OpenZeppelin’s RoleGranted / RoleRevoked on the factory, with role = POOL_CREATOR_ROLE().

Position manager

The V3 trio is signature-identical, down to the indexing:
One addition that will silently corrupt a V3-shaped position tracker if unhandled:
A position can move to a new range keeping its token id — no DecreaseLiquidity + Collect + IncreaseLiquidity sequence describes it on the manager. Without a handler, a repositioned NFT appears to sit at its old ticks forever. (The pool underneath still emits its own Burn/Collect/Mint, and optionally a Swap, in the same transaction.) The manager also records a farmed position’s fee share when it is deferred, and when that deferred share is finally collected:

Plugin modules (index if you track these features)

  • Limit orders: OrderPlaced(owner, fillTick, sellingToken0, liquidity), OrderCancelled(...), OrdersFilled(fillTick, sellingToken0, epoch, amount0, amount1), OrderClaimed(...), and SettlementIncomplete(settledThrough, target) — a swap can cross more ticks than one transaction settles; unsettled orders still rest and still fill later.
  • Oracle: IncreaseObservationCardinalityNext(old, new) on the oracle module.
  • Dynamic fees: FeeCurveChanged(baseFee, maxFee, sensitivity, twapPeriod) when the curve is retuned. The fee itself moves without an event: the plugin sets it as each swap begins and the pool stores it silently, so the fee a swap paid is the pool’s slot0().fee right after it.
  • Security: StatusChanged(previous, current), GuardianChanged(guardian).
  • Active liquidity management: RebalanceTriggered(centreTick) and RebalanceFailed(bytes reason) — a failed rebalance never blocks the swap, so this event is the only signal; index it if you track managed ranges. Configuration: RebalanceTargetChanged(target), RebalanceConditionChanged(fastPeriod, slowPeriod, threshold, minInterval, maxSpotDeviation).
  • Farming: IncentiveCreated(virtualPool, pool, rewardTokens), Entered/Exited(virtualPool, tokenId, ...), RewardAdded, RateChanged.
  • Vaults: VaultCreated(vault, pool, owner, tickLower, tickUpper) on the vault factory; Deposited/Withdrawn/Rebalanced, PositionOpened(tokenId), and the reward ledger events (Harvested, RewardClaimed) on each vault.

Indexer rules of thumb

  1. Start from the deployment’s start blocks; discover pools from the factory’s native PoolCreated — never the V3-shaped compatibility event — positions from the manager, launches from the launchpad factory.
  2. feeGrowthGlobal*X128 may overflow uint256 by design — index it as a wrapping counter, not a monotone value.
  3. The emitted position range is always the pool-resolved one, so CP/STABLE logs never carry a caller’s fictional range.
  4. Protocol-fee accounting: SetFeeProtocol (changed shape) + CollectProtocol + FeeTokenChanged (which side it accrues on).