a chain's factories come from its config, never from a default

One process indexes six chains, and the AMM factories were resolved by
falling back to Lux mainnet's when a Config left them blank. Every chain
that did not name its own therefore matched PoolCreated against Lux's
factory and denominated prices in WLUX. The zoo subgraph registered no
Zoo pool and quoted Lux's coin under Zoo's slug.

A default is only honest when one answer is right everywhere. 0x9999 is;
a factory address is not. FactoryV2, FactoryV3 and Native now come from
Config alone, so a chain that names none indexes none — the failure is
visible instead of wrong. The hardcoded per-network tables go with them:
they had no readers, and Zoo's entry had drifted to Lux's addresses.

ZUSD joins the stable anchors, so Zoo's book prices in dollars.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-11 11:08:20 -07:00
co-authored by Hanzo Dev
parent 80a4098363
commit 502f1496e9
4 changed files with 38 additions and 52 deletions
+24
View File
@@ -684,6 +684,30 @@ func TestHandleV2_RejectsNonFactory(t *testing.T) {
}
}
// A chain whose factories were never declared must index no AMM at all. The
// failure this pins down is not a crash: it is a Zoo indexer that quietly
// adopted Lux's factory address and served Lux's pools as Zoo's.
func TestUndeclaredFactory_IndexesNothing(t *testing.T) {
token0 := "0x0000000000000000000000000000000000000011"
token1 := "0x0000000000000000000000000000000000000022"
pool := "0x00000000000000000000000000000000000000c3"
s := newMemSQLiteStore(t)
idx := NewWithConfig(Config{RPC: "http://unused"}, s)
// The address a previous build reached for when none was configured.
const otherChainFactory = "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84"
idx.processLog(context.Background(), poolCreatedLog(otherChainFactory, token0, token1, 3000, pool))
if p, _ := s.GetPool(nil, pool); p != nil {
t.Fatal("an undeclared chain must not adopt another chain's factory")
}
idx.processLog(context.Background(), swapV3Log(pool, token0, "0xspoof", 1000, -2500))
if sw, _ := s.GetSwaps(nil, 10, "timestamp", "desc", nil); sw != nil && len(sw.([]interface{})) != 0 {
t.Fatalf("a swap from an unregistered pool must not be recorded, got %d", len(sw.([]interface{})))
}
}
func TestHandleV3_RejectsNonFactory(t *testing.T) {
const rogue = "0x000000000000000000000000000000000000dEaD"
token0 := "0x0000000000000000000000000000000000000011"
+4 -32
View File
@@ -190,35 +190,7 @@ const (
// (CLOB) graph schema only ingests genuine fills.
const LXSettleAddress = "0x0000000000000000000000000000000000009999"
// Known factory addresses per network.
type NetworkConfig struct {
Name string `json:"name" yaml:"name"`
ChainID int64 `json:"chainId" yaml:"chain_id"`
FactoryV2 string `json:"factoryV2" yaml:"factory_v2"`
FactoryV3 string `json:"factoryV3" yaml:"factory_v3"`
WETH string `json:"weth" yaml:"weth"` // wrapped native token
StableTokens []string `json:"stableTokens" yaml:"stable_tokens"`
}
// Lux network configs — same addresses as exchange subgraphs.
var LuxMainnet = NetworkConfig{
Name: "lux",
ChainID: 96369,
FactoryV2: "0xD173926A10A0C4eCd3A51B1422270b65Df0551c1",
FactoryV3: "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84",
WETH: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", // WLUX
StableTokens: []string{
"0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", // LUSD
},
}
var ZooMainnet = NetworkConfig{
Name: "zoo",
ChainID: 200200,
FactoryV2: "0xF034942c1140125b5c278aE9cEE1B488e915B2FE",
FactoryV3: "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84",
WETH: "0x5491216406daB99b7032b83765F36790E27F8A61", // WLUX
StableTokens: []string{
"0xb2ee1CE7b84853b83AA08702aD0aD4D79711882D", // LUSDC
},
}
// Which factories are canonical, and what the native unit is, are properties of
// the CHAIN being indexed — they arrive in Config, from whoever knows which
// chain this is. Naming one chain's addresses here would make every other
// chain quote its neighbour's pools.
+9 -19
View File
@@ -26,9 +26,11 @@ type Status struct {
IndexedEvents uint64 `json:"indexedEvents"`
}
// Config tunes an Indexer. The zero value is valid: PoolManager defaults to the
// canonical 0x9999 settlement address, FactoryV2/FactoryV3 to the canonical Lux
// mainnet AMM factories, and StartBlock to 0 (index from genesis).
// Config tunes an Indexer. PoolManager defaults to the 0x9999 settlement
// address, which every chain shares, and StartBlock to 0 (index from genesis).
// FactoryV2, FactoryV3 and Native have no defaults: they differ per chain, and
// a guess would be a claim that one chain's pools belong to another. Left
// empty, the matching surface stays empty.
type Config struct {
RPC string
// PoolManager is the DEX settlement precompile address (0x9999). Logs from
@@ -49,7 +51,7 @@ type Config struct {
// format quotes every token against it — `token.derivedETH` is a price in
// native units and `bundle.ethPriceUSD` is what one native unit is worth —
// so this address is what makes a price expressible at all. Lower-cased on
// construction. Empty => Lux mainnet's.
// construction. Empty => no native anchor, so no token quotes a price.
Native string
// StartBlock is the genesis-relative block to (re)index from. On a clean
// chain relaunch the indexer rewinds to this value (see poll's reorg guard).
@@ -140,24 +142,12 @@ func NewWithConfig(cfg Config, store *storage.Store) *Indexer {
if pm == "" {
pm = LXSettleAddress
}
fv2 := cfg.FactoryV2
if fv2 == "" {
fv2 = LuxMainnet.FactoryV2
}
fv3 := cfg.FactoryV3
if fv3 == "" {
fv3 = LuxMainnet.FactoryV3
}
native := cfg.Native
if native == "" {
native = LuxMainnet.WETH
}
idx := &Indexer{
rpc: cfg.RPC,
poolManager: strings.ToLower(pm),
factoryV2: strings.ToLower(fv2),
factoryV3: strings.ToLower(fv3),
native: strings.ToLower(native),
factoryV2: strings.ToLower(cfg.FactoryV2),
factoryV3: strings.ToLower(cfg.FactoryV3),
native: strings.ToLower(cfg.Native),
label: cfg.Label,
startBlock: cfg.StartBlock,
store: store,
+1 -1
View File
@@ -73,7 +73,7 @@ const selBalanceOf = "0x70a08231"
var stableSymbols = map[string]bool{
"USDC": true, "USDT": true, "DAI": true, "LUSD": true, "USDS": true,
"BUSD": true, "FRAX": true, "TUSD": true, "USDP": true, "PYUSD": true,
"USDE": true, "GUSD": true,
"USDE": true, "GUSD": true, "ZUSD": true,
}
// priceRelaxRounds bounds the outward relaxation from the stable anchors. Each