Documentation · Integrate
Rebuild the journal
How to rebuild GENE's history (epoch records, Mass, gene transitions, buybacks) from finalized GeneHook logs alone, and check it against the views.
The journal is the public history of a GENE deployment, rebuilt from the hook's logs. It needs no archive node: every value below comes from logs, and closed epoch records are permanent in storage. The inputs are HOOK_ADDRESS, listed on addresses, and GENESIS_BLOCK, the block of its Genesis log.
#Rules
- Only finalized logs emitted by the hook address make a record (see events and finality). Views,
eth_callresults, simulations and pending transactions make no record. - One record per event, in
(blockNumber, logIndex)order. Anyone re-indexing the chain then gets the same sequence. Sum several events only under a labelled total, for example "IMMUNITY deployed Y ETH this epoch". - IMMUNITY deployment comes only from
MassAddedwith kind 2.immunityActive()is pressure, a present-tense view; it describes no deployment, past or future. PlacementSkippedis not Mass. Nothing was committed and no reserve, cap or budget was used (GeneHook.sol:142-144, 619-621).
#What each log contributes
| Log | Journal content | Derived totals |
|---|---|---|
Genesis |
The genome hash the hook was built with | |
Launched |
Pool opened; genesis band ticks, liquidity, GENE seeded, dust burned | launchBlock = this log's block (GeneHook.sol:265, 272) |
MassAdded kind 0 |
The genesis position (registry index 0) | massLiquidity += liquidity (GeneHook.sol:432) |
MassAdded kinds 1, 2, 4 |
ETH bid Mass: METABOLISM, IMMUNITY, churn tier | massLiquidity += liquidity; ethCommitted += amount (GeneHook.sol:623-626, 665) |
MassAdded kind 3 |
GENE ask Mass | massLiquidity += liquidity; geneCommitted += amount (GeneHook.sol:623-626, 665) |
EpochClosed |
The closed epoch's stored record, net curve GENE, block-start extremes | Record for that epoch (GeneHook.sol:749-757) |
FlowBudgets |
Net-flow budget created for the next epoch | (GeneHook.sol:866-872) |
GeneTransition |
Observation start or reset; expression with its evidence hash | Gene state; EXPRESSIBLE is transient (GeneHook.sol:937-947) |
FeesHarvested |
Fees collected by metabolize()'s harvest pokes. Fees collected when a placement tops up an existing band have no log, so this is not the call's total (GeneHook.sol:554-559, 688) |
|
Regenerated |
Buyback: ETH spent, GENE burned, limit tick |
#Epoch records
EpochClosed.record is the same struct the hook stores for that epoch (GeneHook.sol:749-757). An epoch that passed with no touch has no EpochClosed log and no stored record; it reads as (0, 0, 0, 0, false) (GeneHook.sol:880). Encode it that way when you recompute an evidence hash over a range that contains it.
#Position registry
The hook registers a range the first time it adds liquidity there, before emitting MassAdded for it (GeneHook.sol:431-433, 623-626, 664, 699-705). The registry order is therefore the order in which each (tickLower, tickUpper) pair first appears in MassAdded logs, with the genesis band at index 0. Each registry entry's liquidity is the sum of liquidity over its MassAdded logs, because protocol positions only grow (GeneHook.sol:594-608).
#Rebuild with viem
// journal.mjs (Node 18+, npm install viem); uses poll() and the event ABI from index.mjs
import { formatEther } from "viem";
import { poll } from "./index.mjs";
const KIND = ["genesis", "METABOLISM bid", "IMMUNITY bid", "ask", "churn-tier bid"];
const GENE = ["GENESIS", "METABOLISM", "REGENERATION", "IMMUNITY"];
export function rebuild(logs) {
const records = new Map(); // epoch -> EpochRecord
const ranges = new Map(); // "lower:upper" -> liquidity, in first-seen order
const journal = [];
let massLiquidity = 0n, ethCommitted = 0n, geneCommitted = 0n, launchBlock = null;
for (const l of logs) {
const a = l.args;
const at = { block: l.blockNumber, logIndex: l.logIndex, tx: l.transactionHash };
switch (l.eventName) {
case "Launched":
launchBlock = l.blockNumber;
journal.push({ ...at, text: `Launched: genesis band ${a.tickLower}..${a.tickUpper}, ${formatEther(a.geneSeeded)} GENE seeded` });
break;
case "MassAdded": {
const k = Number(a.kind), key = `${a.tickLower}:${a.tickUpper}`;
ranges.set(key, (ranges.get(key) ?? 0n) + a.liquidity);
massLiquidity += a.liquidity;
if (k === 1 || k === 2 || k === 4) ethCommitted += a.amount;
if (k === 3) geneCommitted += a.amount;
if (k === 2) journal.push({ ...at, text: `IMMUNITY DEPLOYED ${formatEther(a.amount)} ETH OF PERMANENT MASS` });
else if (k !== 0) journal.push({ ...at, text: `${KIND[k]}: ticks ${a.tickLower}..${a.tickUpper}, amount ${k === 3 ? formatEther(a.amount) + " GENE" : formatEther(a.amount) + " ETH"}` });
break;
}
case "EpochClosed":
records.set(Number(a.epoch), a.record);
break;
case "GeneTransition":
if (Number(a.to) === 3) journal.push({ ...at, text: `${GENE[Number(a.gene)]} expressed at epoch ${a.epoch}, evidence ${a.evidenceHash}` });
break;
case "Regenerated":
journal.push({ ...at, text: `REGENERATION: ${formatEther(a.ethSpent)} ETH spent, ${formatEther(a.geneBurned)} GENE burned` });
break;
default:
break;
}
}
return { journal, records, ranges, massLiquidity, ethCommitted, geneCommitted, launchBlock };
}
const { finalLogs } = await poll();
const r = rebuild(finalLogs);
console.log(r.journal.map((e, i) => `${i + 1}. block ${e.block}: ${e.text}`).join("\n"));#Check the rebuild against the views
Read the views at the last finalized block you indexed and compare (see read state):
| Rebuilt from logs | Must equal | Source |
|---|---|---|
massLiquidity |
GeneHook.massLiquidity() |
GeneHook.sol:186, 432, 665 |
ethCommitted |
GeneHook.ethCommitted() |
GeneHook.sol:184, 624 |
geneCommitted |
GeneHook.geneCommitted() |
GeneHook.sol:185, 625 |
| range list, in order | GeneLens.positionCount and positionAt(i) |
GeneLens.sol:89-98 |
| liquidity per range | StateView getPositionInfo(poolId, HOOK_ADDRESS, lower, upper, bytes32(0)) |
GeneHook.sol:604 |
| record of each closed epoch | GeneLens.epochRecord(hook, e) |
GeneLens.sol:58-60 |
A mismatch means a missing or extra log in the index. Report it; do not hide or adjust it.
#Values logs cannot recover
Reachable depth (GeneLens.report), immunityActive() and gene progress counts are views; no GeneHook log records them. Reachable depth is pure math over the positions, their liquidity and the price (GeneLens.sol:229-259), so it can be recomputed for a past block if you also index the pool's PoolManager Swap logs. immunityActive() and gene progress counts depend on when evolution ran; reading them for a past block needs an archive node. A history rebuilt from logs alone marks them as not recorded rather than estimating them. Any page that shows depth, IMMUNITY or bids links the known limitations.
Next: verify evidence hashes.
Sources (15)
- src/GeneHook.sol:75-81
- src/GeneHook.sol:138-145
- src/GeneHook.sol:184-186
- src/GeneHook.sol:420-435
- src/GeneHook.sol:479-552
- src/GeneHook.sol:589
- src/GeneHook.sol:610-627
- src/GeneHook.sol:654-666
- src/GeneHook.sol:699-705
- src/GeneHook.sol:747-757
- src/GeneHook.sol:880
- src/GeneHook.sol:937-947
- src/GeneLens.sol:89-98
- src/GeneHook.sol:554-559
- src/GeneLens.sol:229-259
Paths are relative to the GENE repository at tag rc4-audit-candidate (aca5fcd).