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
EpochClosedandFlowBudgetslogs 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
EpochClosedevent; it reads as all-zero activity (GeneHook.sol:748-757, 880). - Gene transitions follow evolution, not time.
GeneTransitionlogs appear whenevolve()ormetabolize()processes closed epochs (GeneHook.sol:289, 312-329), so the log's block can be later than the epoch in itsepochfield. - IMMUNITY has three distinct facts.
immunityActive() == trueis pressure, a view. Only a minedMassAddedwith kind 2 is a deployment (GeneHook.sol:138-141).
#Finality
A record that people rely on stays fixed through a reorganisation. Two rules:
- Permanent records come from finalized blocks. Index logs at or below the
finalizedblock tag once, in order, and never revisit them. - The unfinalized window is replaceable. Re-fetch all logs above
finalizedon 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).