Pre-launch. RC4 code-frozen for external audit (rc4-audit-candidate, aca5fcd). Report pending. Status, updated 2026-10-08
GENESpecimen 001

Documentation · Integrate

Events and finality

Which events GeneHook emits, when each one fires, and how to index them so that a reorganised block never leaves a wrong record.

HOOK_ADDRESS is the hook address listed on addresses, and GENESIS_BLOCK is the block of its Genesis log.

#The nine events

All events come from the GeneHook address. A log with the same signature from any other address is not a GENE event.

Event Fires when Source
Genesis(bytes32 indexed genomeHash, address token, PoolId poolId) Once, in the constructor GeneHook.sol:131, 247
Launched(PoolId indexed poolId, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 geneSeeded, uint256 dustBurned) Once, at the end of launch(). Its block is launchBlock GeneHook.sol:132, 265, 272
EpochClosed(uint32 indexed epoch, EpochRecord record, int256 netGene, int24 maxTick, int24 minTick) At the first touch after an epoch boundary, for the last touched epoch only GeneHook.sol:133, 748-757
FlowBudgets(uint32 indexed epoch, uint256 bidBudget, uint256 immBudget, int24 pLowTick) In the same touch, only when the closing epoch funds a net-flow budget; one of the two amounts is zero GeneHook.sol:134-135, 838, 857, 866-872
GeneTransition(uint8 indexed gene, State from, State to, uint32 indexed epoch, bytes32 evidenceHash) Every gene state change. An expression emits two logs in one call (to EXPRESSIBLE, then to EXPRESSED) GeneHook.sol:136, 271, 908, 915, 928, 931, 945-946
FeesHarvested(uint256 eth, uint256 gene, uint256 positionsPoked) Every metabolize(). It covers the harvest pokes only; fees collected when a placement tops up an existing band are credited without an event, so it can understate the total collected GeneHook.sol:137, 554-559, 688
MassAdded(uint8 indexed kind, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 amount, int24 spotTick, int24 anchorTick) The genesis band at launch, and every placement GeneHook.sol:141, 433, 626
PlacementSkipped(uint8 indexed kind, int24 tickLower, int24 tickUpper, uint256 liquidity) A placement would exceed v4's per-tick liquidity ceiling; nothing was committed GeneHook.sol:142-144, 619-621
Regenerated(uint256 ethSpent, uint256 geneBurned, int24 spotTick, int24 limitTick) A regenerate() call that reaches its buyback swap GeneHook.sol:145, 571-589

PoolId encodes as bytes32 and State as uint8 in the ABI. EpochRecord is the tuple (uint96 revenue, uint32 activeBlocks, uint32 pathTicks, uint88 ethCommitted, bool closed) (GeneHook.sol:75-81). Swaps emit no GeneHook event; v4 core emits its own Swap event.

#MassAdded kinds and anchorTick

kind Meaning anchorTick reported (from the code) Source
0 genesis band the opening tick GeneHook.sol:433
1 METABOLISM bid, net-flow funded atlTick GeneHook.sol:484-485
2 IMMUNITY bid, net-sell funded max(spot, atlTick) in ticks GeneHook.sol:498-501
3 ask (GENE) min(spot, athTick) in ticks GeneHook.sol:544-546
4 churn-tier bid atlTick GeneHook.sol:516-518

Follow this table, not the NatSpec comment at GeneHook.sol:139-140, which describes kinds 1 and 4 differently.

amount is the principal actually added: ETH for kinds 1, 2 and 4, GENE for kinds 0 and 3 (GeneHook.sol:426, 433, 623-626). For kind 2, amount equals metabolize()'s ethToImmunity return value in that transaction (GeneHook.sol:501-504).

#Timing rules an indexer must know

  • Epoch records arrive late. An epoch's EpochClosed and FlowBudgets logs are emitted by the first swap or entry point after the boundary, not at the boundary block (GeneHook.sol:747-761).
  • Untouched epochs have no log. If a whole epoch passes with no touch, it gets no stored record and no EpochClosed event; it reads as all-zero activity (GeneHook.sol:748-757, 880).
  • Gene transitions follow evolution, not time. GeneTransition logs appear when evolve() or metabolize() processes closed epochs (GeneHook.sol:289, 312-329), so the log's block can be later than the epoch in its epoch field.
  • IMMUNITY has three distinct facts. immunityActive() == true is pressure, a view. Only a mined MassAdded with kind 2 is a deployment (GeneHook.sol:138-141).

#Finality

A record that people rely on stays fixed through a reorganisation. Two rules:

  1. Permanent records come from finalized blocks. Index logs at or below the finalized block tag once, in order, and never revisit them.
  2. The unfinalized window is replaceable. Re-fetch all logs above finalized on every poll and replace the previous window. A log that a reorganisation removed then simply disappears from the window.

Order logs by (blockNumber, logIndex). Create records from mined logs only, not from a view call, a simulation or a pending transaction.

// index.mjs   (Node 18+, npm install viem)
// env: RPC_URL, HOOK_ADDRESS and GENESIS_BLOCK (after deployment)
import { createPublicClient, http, parseAbi } from "viem";

const { RPC_URL, HOOK_ADDRESS } = process.env;
const GENESIS_BLOCK = BigInt(process.env.GENESIS_BLOCK);
const CHUNK = 5000n;

export const geneEvents = parseAbi([
  "event Genesis(bytes32 indexed genomeHash, address token, bytes32 poolId)",
  "event Launched(bytes32 indexed poolId, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 geneSeeded, uint256 dustBurned)",
  "event EpochClosed(uint32 indexed epoch, (uint96 revenue, uint32 activeBlocks, uint32 pathTicks, uint88 ethCommitted, bool closed) record, int256 netGene, int24 maxTick, int24 minTick)",
  "event FlowBudgets(uint32 indexed epoch, uint256 bidBudget, uint256 immBudget, int24 pLowTick)",
  "event GeneTransition(uint8 indexed gene, uint8 from, uint8 to, uint32 indexed epoch, bytes32 evidenceHash)",
  "event FeesHarvested(uint256 eth, uint256 gene, uint256 positionsPoked)",
  "event MassAdded(uint8 indexed kind, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 amount, int24 spotTick, int24 anchorTick)",
  "event PlacementSkipped(uint8 indexed kind, int24 tickLower, int24 tickUpper, uint256 liquidity)",
  "event Regenerated(uint256 ethSpent, uint256 geneBurned, int24 spotTick, int24 limitTick)",
]);

const client = createPublicClient({ transport: http(RPC_URL) });

async function fetchLogs(from, to) {
  const out = [];
  for (let a = from; a <= to; a += CHUNK) {
    const b = a + CHUNK - 1n < to ? a + CHUNK - 1n : to;
    const logs = await client.getLogs({ address: HOOK_ADDRESS, events: geneEvents, fromBlock: a, toBlock: b, strict: true });
    out.push(...logs.filter((l) => l.address.toLowerCase() === HOOK_ADDRESS.toLowerCase()));
  }
  return out.sort((x, y) => (x.blockNumber === y.blockNumber ? x.logIndex - y.logIndex : x.blockNumber < y.blockNumber ? -1 : 1));
}

const finalLogs = [];
let finalTo = GENESIS_BLOCK - 1n;

export async function poll() {
  const finalized = (await client.getBlock({ blockTag: "finalized" })).number;
  const latest = await client.getBlockNumber();
  if (finalized > finalTo) {
    finalLogs.push(...(await fetchLogs(finalTo + 1n, finalized)));
    finalTo = finalized;
  }
  // replaced in full on every poll
  const window = latest > finalTo ? await fetchLogs(finalTo + 1n, latest) : [];
  return { finalLogs, window, finalized, latest };
}

const { finalLogs: f, window: w, finalized } = await poll();
console.log(`${f.length} finalized logs up to block ${finalized}; ${w.length} logs in the unfinalized window`);

Next: rebuild the journal from these logs, and verify evidence hashes. Event reference: events.

Sources (14)
  • src/GeneHook.sol:130-145
  • src/GeneHook.sol:247
  • src/GeneHook.sol:271-272
  • src/GeneHook.sol:280-291
  • src/GeneHook.sol:355-366
  • src/GeneHook.sol:433
  • src/GeneHook.sol:479-552
  • src/GeneHook.sol:589
  • src/GeneHook.sol:610-627
  • src/GeneHook.sol:688
  • src/GeneHook.sol:728-780
  • src/GeneHook.sol:834-873
  • src/GeneHook.sol:879-947
  • src/GeneHook.sol:554-559

Paths are relative to the GENE repository at tag rc4-audit-candidate (aca5fcd).