Files
zeekay c2bd4165e6 fix(indexer): scope V4 Initialize/Swap to 0x9999, guard poller, cap DEX first
Three confirmed findings in the explorer/graph indexer:

- MEDIUM: handleInitializeV4/handleSwapV4 seeded AMM pools/tokens/swaps from
  ANY emitting contract, so any contract could emit lookalike V4 events to
  create phantom pools/tokens and inflate the explorer's public TVL/volume
  aggregates. Gate both to the canonical 0x9999 PoolManager (idx.isPoolManager),
  mirroring handleDEXFill. The now-always-true inner Market guard in
  handleInitializeV4 is de-nested (one path).

- LOW (DoS): unguarded fixed-offset slices (token0[:8], l.Address[:8]) panic on
  a short/malformed RPC log, and the poll goroutine had no recover() -> one bad
  log crashed graphd. Route those symbol slices through the length-checked
  shortAddr helper, and wrap each poll in pollGuarded() which recovers any panic
  into an error so the loop continues.

- LOW (query-DoS): resolvers/dex parseLimit had no max cap. Clamp <1 -> 100 and
  cap min(limit, 1000), matching every other resolver namespace.

Tests:
- TestHandleV4_RejectsNonPoolManager: rogue Initialize/Swap seeds nothing; the
  same event from 0x9999 is honored.
- TestPoll_MalformedLogDoesNotCrash: malformed logs don't panic processLog, and
  pollGuarded converts a poll panic into an error.
- TestParseLimit_Caps: default 100, floor 1->100, hard cap 1000.
2026-06-21 18:25:55 -07:00

32 lines
1.2 KiB
Go

package dex
import "testing"
// LOW (query-DoS): the DEX (CLOB) resolvers must clamp `first` exactly like every
// other resolver namespace — default 100, floor 1→100, hard cap 1000. An
// unbounded `first` would let a single query pull the entire table.
func TestParseLimit_Caps(t *testing.T) {
cases := []struct {
name string
args map[string]interface{}
want int
}{
{"absent defaults to 100", map[string]interface{}{}, 100},
{"in range honored", map[string]interface{}{"first": 250}, 250},
{"string in range honored", map[string]interface{}{"first": "250"}, 250},
{"over cap clamped to 1000", map[string]interface{}{"first": 1_000_000}, 1000},
{"string over cap clamped", map[string]interface{}{"first": "999999999"}, 1000},
{"exactly cap honored", map[string]interface{}{"first": 1000}, 1000},
{"zero floors to 100", map[string]interface{}{"first": 0}, 100},
{"negative floors to 100", map[string]interface{}{"first": -5}, 100},
{"unparseable floors to 100", map[string]interface{}{"first": "not-a-number"}, 100},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := parseLimit(tc.args); got != tc.want {
t.Errorf("parseLimit(%v) = %d, want %d", tc.args, got, tc.want)
}
})
}
}