mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
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.
32 lines
1.2 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|