Files
zeekay 705e38c45e fix(indexer): gate V2/V3 AMM handlers to canonical factory; floor parseLimit everywhere
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.
2026-06-21 20:27:48 -07:00

154 lines
6.5 KiB
Go

// Package securities provides resolvers for security tokens.
//
// Indexes ERC-3643 + ONCHAINID surfaces:
// - issuances, transfers, dividends, compliance records (core token)
// - identity registry actions (IdentityRegistry contract)
// - claim lifecycle (ONCHAINID ClaimAdded/Removed/Changed)
// - transfer-agent actions (Recovery, AddressFrozen, TokensFrozen)
//
// Entities: SecurityIssuance, SecurityTransfer, SecurityDividend,
//
// SecurityCompliance, SecurityStats, OnchainIdClaim, TransferAgentAction,
// FrozenAccount, FrozenTokens, IdentityRegistryAction.
package securities
import (
"context"
"fmt"
"github.com/luxfi/graph/storage"
)
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
func Register(resolvers map[string]ResolverFunc) {
// Core token surfaces.
resolvers["securityIssuance"] = resolveSecurityIssuance
resolvers["securityIssuances"] = resolveSecurityIssuances
resolvers["securityTransfer"] = resolveSecurityTransfer
resolvers["securityTransfers"] = resolveSecurityTransfers
resolvers["securityDividend"] = resolveSecurityDividend
resolvers["securityDividends"] = resolveSecurityDividends
resolvers["securityCompliance"] = resolveSecurityCompliance
resolvers["securityCompliances"] = resolveSecurityCompliances
resolvers["securityStats"] = resolveSecurityStats
// ERC-3643 + ONCHAINID surfaces.
resolvers["onchainIdClaim"] = resolveOnchainIdClaim
resolvers["onchainIdClaims"] = resolveOnchainIdClaims
resolvers["transferAgentAction"] = resolveTransferAgentAction
resolvers["transferAgentActions"] = resolveTransferAgentActions
resolvers["frozenAccount"] = resolveFrozenAccount
resolvers["frozenAccounts"] = resolveFrozenAccounts
resolvers["frozenTokens"] = resolveFrozenTokensRec
resolvers["frozenTokensList"] = resolveFrozenTokensList
resolvers["identityRegistryAction"] = resolveIdentityRegistryAction
resolvers["identityRegistryActions"] = resolveIdentityRegistryActions
}
func resolveSecurityIssuance(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("SecurityIssuance", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityIssuance requires id")
}
func resolveSecurityIssuances(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityIssuance", pl(args))
}
func resolveSecurityTransfer(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("SecurityTransfer", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityTransfer requires id")
}
func resolveSecurityTransfers(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityTransfer", pl(args))
}
func resolveSecurityDividend(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("SecurityDividend", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityDividend requires id")
}
func resolveSecurityDividends(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityDividend", pl(args))
}
func resolveSecurityCompliance(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("SecurityCompliance", fmt.Sprint(id))
}
return nil, fmt.Errorf("securityCompliance requires id")
}
func resolveSecurityCompliances(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("SecurityCompliance", pl(args))
}
func resolveSecurityStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
return s.GetByType("SecurityStats", "1")
}
// ONCHAINID claim resolvers (ClaimAdded / ClaimRemoved / ClaimChanged).
func resolveOnchainIdClaim(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("OnchainIdClaim", fmt.Sprint(id))
}
return nil, fmt.Errorf("onchainIdClaim requires id")
}
func resolveOnchainIdClaims(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("OnchainIdClaim", pl(args))
}
// Transfer-agent action resolvers (Recovery / AddressFrozen / TokensFrozen).
func resolveTransferAgentAction(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("TransferAgentAction", fmt.Sprint(id))
}
return nil, fmt.Errorf("transferAgentAction requires id")
}
func resolveTransferAgentActions(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("TransferAgentAction", pl(args))
}
// Frozen-account snapshot (current state of AddressFrozen on a holder).
func resolveFrozenAccount(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FrozenAccount", fmt.Sprint(id))
}
return nil, fmt.Errorf("frozenAccount requires id")
}
func resolveFrozenAccounts(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FrozenAccount", pl(args))
}
// Frozen-tokens snapshot (per-holder partial freeze amount).
func resolveFrozenTokensRec(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("FrozenTokens", fmt.Sprint(id))
}
return nil, fmt.Errorf("frozenTokens requires id")
}
func resolveFrozenTokensList(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("FrozenTokens", pl(args))
}
// IdentityRegistry actions (IdentityRegistered / IdentityRemoved / IdentityStored / CountryUpdated).
func resolveIdentityRegistryAction(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
if id, ok := args["id"]; ok {
return s.GetByType("IdentityRegistryAction", fmt.Sprint(id))
}
return nil, fmt.Errorf("identityRegistryAction requires id")
}
func resolveIdentityRegistryActions(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
return s.ListByType("IdentityRegistryAction", pl(args))
}
func pl(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)
}