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

110 lines
4.1 KiB
Go

// Package dex provides resolvers for the native Lux DEX (D-Chain).
//
// The native DEX is a CLOB (central limit order book) with perpetuals,
// NOT an AMM. This indexes DexVM state via RPC.
//
// Entities: Order, Fill, Market, Position, FundingRate, Liquidation
package dex
import (
"context"
"fmt"
"github.com/luxfi/graph/storage"
)
// ResolverFunc matches engine.ResolverFunc.
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
func Register(resolvers map[string]ResolverFunc) {
resolvers["order"] = resolveOrder
resolvers["orders"] = resolveOrders
resolvers["fill"] = resolveFill
resolvers["fills"] = resolveFills
resolvers["market"] = resolveMarket
resolvers["markets"] = resolveMarkets
resolvers["perpPosition"] = resolvePerpPosition
resolvers["perpPositions"] = resolvePerpPositions
resolvers["fundingRate"] = resolveFundingRate
resolvers["fundingRates"] = resolveFundingRates
resolvers["liquidation"] = resolveLiquidation
resolvers["liquidations"] = resolveLiquidations
resolvers["orderbook"] = resolveOrderbook
resolvers["marketDayDatas"] = resolveMarketDayDatas
}
func resolveOrder(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("Order", fmt.Sprint(id))
}
return nil, fmt.Errorf("order requires id")
}
func resolveOrders(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("Order", parseLimit(args))
}
func resolveFill(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("Fill", fmt.Sprint(id))
}
return nil, fmt.Errorf("fill requires id")
}
func resolveFills(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("Fill", parseLimit(args))
}
func resolveMarket(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("Market", fmt.Sprint(id))
}
return nil, fmt.Errorf("market requires id")
}
func resolveMarkets(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("Market", parseLimit(args))
}
func resolvePerpPosition(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("PerpPosition", fmt.Sprint(id))
}
return nil, fmt.Errorf("perpPosition requires id")
}
func resolvePerpPositions(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("PerpPosition", parseLimit(args))
}
func resolveFundingRate(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FundingRate", fmt.Sprint(id))
}
return nil, fmt.Errorf("fundingRate requires id")
}
func resolveFundingRates(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FundingRate", parseLimit(args))
}
func resolveLiquidation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("Liquidation", fmt.Sprint(id))
}
return nil, fmt.Errorf("liquidation requires id")
}
func resolveLiquidations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("Liquidation", parseLimit(args))
}
func resolveOrderbook(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("Orderbook", fmt.Sprint(id))
}
return nil, fmt.Errorf("orderbook requires market id")
}
func resolveMarketDayDatas(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("MarketDayData", parseLimit(args))
}
func parseLimit(args map[string]interface{}) int {
limit := 100
if l, ok := args["first"]; ok {
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
}
if limit < 1 {
limit = 100
}
return min(limit, 1000)
}