mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
Red re-verify found the V4 AMM-spoof gate left a parallel bypass.
MEDIUM — V2/V3 AMM handlers were UNGATED: handlePairCreated, handlePoolCreated,
handleSwapV2, handleSwapV3 accepted events from ANY emitter — a parallel path to
seed phantom pairs/pools/swaps and inflate the SAME public Factory/TVL/volume
aggregates the now-gated V4 handlers (0x9999) block. Mirror the V4 gate:
- PairCreated/PoolCreated now require the canonical FactoryV2/FactoryV3 emitter
(the AMM trust root), exactly as V4 requires 0x9999.
- V2/V3 Swaps are emitted by the pair/pool, not the factory, so the gate is
"is the emitter a pool the canonical factory created?" (isKnownPool). The
factory authorizes a pool; only that pool's Swaps are honored. A rogue
factory's PairCreated is rejected, so its pools are never registered, so its
Swaps are rejected — the whole parallel path is closed.
- Pair/pool keys lower-cased at seed so the Swap-side lookup is RPC-casing safe.
- Factory addresses configurable (Config.FactoryV2/V3 + FACTORY_V2/V3 env),
default to canonical Lux mainnet (LuxMainnet.Factory{V2,V3}); main wires env.
INFO — the per-namespace resolver limit lacked the `if limit < 1 { limit = 100 }`
floor that resolvers/dex has, so a negative/zero `first` flowed to ListByType
(`first:-1` = "no limit" in SQLite — an unbounded scan). Brought every pl()/
parseLimit() copy to the dex-canonical body (floor <1->100 + cap 1000); fhe and
platform also gained the previously-missing 1000 cap. All namespaces now identical.
Tests (go test ./...): indexer + resolvers/{dex,exchange,fhe,platform} green.
- TestHandleV2_RejectsNonFactory, TestHandleV3_RejectsNonFactory: rogue
PairCreated/PoolCreated/Swap seeds nothing and does not inflate the factory
aggregate; the canonical-source event IS honored end-to-end.
- TestPL_ClampsFirst / TestParseLimit_ClampsFirst (exchange/fhe/platform):
negative/zero floored to 100, capped at 1000.
32 lines
1.2 KiB
Go
32 lines
1.2 KiB
Go
package platform
|
|
|
|
import "testing"
|
|
|
|
// LOW (query-DoS): parseLimit must clamp `first` exactly like the dex namespace —
|
|
// default 100, floor <1→100, hard cap 1000. Before the fix this namespace's
|
|
// parseLimit had NEITHER a floor NOR a cap (`return limit`), so any `first` flowed
|
|
// straight to ListByType — an unbounded scan (and `first:-1` = "no limit" in
|
|
// SQLite). This proves both the floor and the cap now match dex.
|
|
func TestParseLimit_ClampsFirst(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},
|
|
{"over cap clamped to 1000", map[string]interface{}{"first": 1_000_000}, 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": -1}, 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)
|
|
}
|
|
})
|
|
}
|
|
}
|