mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
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.
This commit is contained in:
@@ -1 +1,4 @@
|
||||
# rebuild 1776010882
|
||||
|
||||
# Build artifacts (CI builds binaries on ARC; never commit local builds)
|
||||
/graphd
|
||||
|
||||
+11
-2
@@ -92,8 +92,17 @@ func main() {
|
||||
stopReplicate := storage.StartReplicate(filepath.Join(*dataDir, "graph.db"))
|
||||
defer stopReplicate()
|
||||
|
||||
// Indexer: subscribe to EVM events via RPC
|
||||
idx := indexer.New(*rpcEndpoint, store)
|
||||
// Indexer: subscribe to EVM events via RPC. The AMM trust roots (V2/V3
|
||||
// factories) and the 0x9999 settlement manager default to the canonical Lux
|
||||
// mainnet addresses; override per-network (e.g. Zoo) via env. Events from any
|
||||
// other emitter are rejected so they cannot seed phantom pairs/pools/swaps or
|
||||
// inflate the public Factory/TVL/volume aggregates.
|
||||
idx := indexer.NewWithConfig(indexer.Config{
|
||||
RPC: *rpcEndpoint,
|
||||
FactoryV2: os.Getenv("FACTORY_V2"),
|
||||
FactoryV3: os.Getenv("FACTORY_V3"),
|
||||
PoolManager: os.Getenv("POOL_MANAGER"),
|
||||
}, store)
|
||||
go func() {
|
||||
if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
|
||||
slog.Error("indexer", "error", err)
|
||||
|
||||
@@ -558,3 +558,159 @@ func TestPoll_MalformedLogDoesNotCrash(t *testing.T) {
|
||||
t.Fatalf("pollGuarded error must identify the recovered panic, got %v", perr)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MEDIUM: V2/V3 AMM handlers must be scoped to the canonical factory.
|
||||
//
|
||||
// handlePairCreated/handlePoolCreated/handleSwapV2/handleSwapV3 fed the SAME
|
||||
// Factory/TVL/volume aggregates the V4 handlers now gate to 0x9999, but accepted
|
||||
// events from ANY emitter — a parallel path to seed phantom pairs/pools/swaps. The
|
||||
// fix gates pair/pool creation to the canonical factory addresses, and Swaps to the
|
||||
// pools those canonical events created. These tests prove a rogue emitter seeds
|
||||
// nothing and does not inflate the factory aggregate, while the canonical-source
|
||||
// event IS honored end-to-end (creation → swap).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Canonical factories used by these tests (distinct from the rogue address). They
|
||||
// are passed explicitly so the test does not depend on the package defaults.
|
||||
const (
|
||||
testFactoryV2 = "0x00000000000000000000000000000000000000F2"
|
||||
testFactoryV3 = "0x00000000000000000000000000000000000000F3"
|
||||
)
|
||||
|
||||
// word20InWord32 right-aligns a 20-byte address into a 32-byte ABI data word —
|
||||
// the encoding handlePairCreated/handlePoolCreated read the created pair/pool
|
||||
// address from (the last 20 bytes of the first data word).
|
||||
func word20InWord32(addr string) string {
|
||||
return fmt.Sprintf("%064s", strings.ToLower(strings.TrimPrefix(addr, "0x")))
|
||||
}
|
||||
|
||||
// pairCreatedLog builds a V2 PairCreated whose created-pair address is `pair`.
|
||||
// topics: [sig, token0, token1]; data word0 = pair (right-aligned), word1 = index.
|
||||
func pairCreatedLog(emitter, token0, token1, pair string) *logEntry {
|
||||
return &logEntry{
|
||||
Address: emitter,
|
||||
Topics: []string{SigPairCreated, addrTopic(token0), addrTopic(token1)},
|
||||
Data: "0x" + word20InWord32(pair) + word(big.NewInt(1)),
|
||||
BlockNumber: "0x1",
|
||||
TransactionHash: "0xpair",
|
||||
LogIndex: "0x0",
|
||||
}
|
||||
}
|
||||
|
||||
// poolCreatedLog builds a V3 PoolCreated whose created-pool address is `pool`.
|
||||
// topics: [sig, token0, token1, fee]; data word0 = pool (right-aligned).
|
||||
func poolCreatedLog(emitter, token0, token1 string, fee int64, pool string) *logEntry {
|
||||
return &logEntry{
|
||||
Address: emitter,
|
||||
Topics: []string{SigPoolCreated, addrTopic(token0), addrTopic(token1), topic32(fmt.Sprintf("%x", fee))},
|
||||
Data: "0x" + word20InWord32(pool),
|
||||
BlockNumber: "0x1",
|
||||
TransactionHash: "0xpool",
|
||||
LogIndex: "0x0",
|
||||
}
|
||||
}
|
||||
|
||||
// swapV2Log builds a V2 Swap emitted by `emitter` (the pair contract).
|
||||
func swapV2Log(emitter, sender, tx string, amt0In, amt1Out int64) *logEntry {
|
||||
return &logEntry{
|
||||
Address: emitter,
|
||||
Topics: []string{SigSwapV2, addrTopic(sender)},
|
||||
Data: "0x" + word(big.NewInt(amt0In)) + word(new(big.Int)) +
|
||||
word(new(big.Int)) + word(big.NewInt(amt1Out)),
|
||||
BlockNumber: "0x2", TransactionHash: tx, LogIndex: "0x0",
|
||||
}
|
||||
}
|
||||
|
||||
// swapV3Log builds a V3 Swap emitted by `emitter` (the pool contract); legs signed.
|
||||
func swapV3Log(emitter, sender, tx string, amt0, amt1 int64) *logEntry {
|
||||
return &logEntry{
|
||||
Address: emitter,
|
||||
Topics: []string{SigSwapV3, addrTopic(sender), addrTopic(sender)},
|
||||
Data: "0x" + signedWord(big.NewInt(amt0)) + signedWord(big.NewInt(amt1)),
|
||||
BlockNumber: "0x2", TransactionHash: tx, LogIndex: "0x0",
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleV2_RejectsNonFactory(t *testing.T) {
|
||||
const rogue = "0x000000000000000000000000000000000000dEaD"
|
||||
token0 := "0x0000000000000000000000000000000000000011"
|
||||
token1 := "0x0000000000000000000000000000000000000022"
|
||||
rt := "0x00000000000000000000000000000000000000a1" // rogue pair address
|
||||
ct := "0x00000000000000000000000000000000000000b1" // canonical pair address
|
||||
|
||||
s := newMemSQLiteStore(t)
|
||||
idx := NewWithConfig(Config{RPC: "http://unused", FactoryV2: testFactoryV2, FactoryV3: testFactoryV3}, s)
|
||||
|
||||
// --- Rogue PairCreated: nothing seeded, factory not inflated. ---
|
||||
idx.processLog(pairCreatedLog(rogue, token0, token1, rt))
|
||||
if p, _ := s.GetPool(nil, rt); p != nil {
|
||||
t.Error("rogue PairCreated must NOT seed a pair")
|
||||
}
|
||||
if tok, _ := s.GetByType("Token", token0); tok != nil {
|
||||
t.Error("rogue PairCreated must NOT seed a token")
|
||||
}
|
||||
if f, _ := s.GetFactory(nil, "1"); f != nil && asInt64(f.(map[string]interface{})["poolCount"]) != 0 {
|
||||
t.Errorf("rogue PairCreated must NOT inflate factory poolCount, got %v", f)
|
||||
}
|
||||
|
||||
// --- A Swap from the rogue (never-registered) pair: no swap recorded. ---
|
||||
idx.processLog(swapV2Log(rt, token0, "0xspoof", 1000, 2500))
|
||||
if sw, _ := s.GetSwaps(nil, 10, "timestamp", "desc", nil); sw != nil && len(sw.([]interface{})) != 0 {
|
||||
t.Fatalf("Swap from a rogue V2 pair must NOT be recorded, got %d", len(sw.([]interface{})))
|
||||
}
|
||||
|
||||
// --- Canonical PairCreated IS honored; its pair's Swap IS recorded. ---
|
||||
idx.processLog(pairCreatedLog(testFactoryV2, token0, token1, ct))
|
||||
if p, _ := s.GetPool(nil, ct); p == nil {
|
||||
t.Fatal("canonical PairCreated must seed the pair")
|
||||
}
|
||||
if f, _ := s.GetFactory(nil, "1"); f == nil || asInt64(f.(map[string]interface{})["poolCount"]) != 1 {
|
||||
t.Fatalf("canonical PairCreated must bump factory poolCount to 1, got %v", f)
|
||||
}
|
||||
idx.processLog(swapV2Log(ct, token0, "0xreal", 1000, 2500))
|
||||
sw, _ := s.GetSwaps(nil, 10, "timestamp", "desc", nil)
|
||||
if sw == nil || len(sw.([]interface{})) != 1 {
|
||||
t.Fatalf("Swap from the canonical V2 pair must be recorded exactly once, got %v", sw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleV3_RejectsNonFactory(t *testing.T) {
|
||||
const rogue = "0x000000000000000000000000000000000000dEaD"
|
||||
token0 := "0x0000000000000000000000000000000000000011"
|
||||
token1 := "0x0000000000000000000000000000000000000022"
|
||||
rt := "0x00000000000000000000000000000000000000a3" // rogue pool address
|
||||
ct := "0x00000000000000000000000000000000000000b3" // canonical pool address
|
||||
|
||||
s := newMemSQLiteStore(t)
|
||||
idx := NewWithConfig(Config{RPC: "http://unused", FactoryV2: testFactoryV2, FactoryV3: testFactoryV3}, s)
|
||||
|
||||
// --- Rogue PoolCreated: nothing seeded, factory not inflated. ---
|
||||
idx.processLog(poolCreatedLog(rogue, token0, token1, 3000, rt))
|
||||
if p, _ := s.GetPool(nil, rt); p != nil {
|
||||
t.Error("rogue PoolCreated must NOT seed a pool")
|
||||
}
|
||||
if f, _ := s.GetFactory(nil, "1"); f != nil && asInt64(f.(map[string]interface{})["poolCount"]) != 0 {
|
||||
t.Errorf("rogue PoolCreated must NOT inflate factory poolCount, got %v", f)
|
||||
}
|
||||
|
||||
// --- A Swap from the rogue (never-registered) pool: no swap recorded. ---
|
||||
idx.processLog(swapV3Log(rt, token0, "0xspoof", 1000, -2500))
|
||||
if sw, _ := s.GetSwaps(nil, 10, "timestamp", "desc", nil); sw != nil && len(sw.([]interface{})) != 0 {
|
||||
t.Fatalf("Swap from a rogue V3 pool must NOT be recorded, got %d", len(sw.([]interface{})))
|
||||
}
|
||||
|
||||
// --- Canonical PoolCreated IS honored; its pool's Swap IS recorded with signed legs. ---
|
||||
idx.processLog(poolCreatedLog(testFactoryV3, token0, token1, 3000, ct))
|
||||
if p, _ := s.GetPool(nil, ct); p == nil {
|
||||
t.Fatal("canonical PoolCreated must seed the pool")
|
||||
}
|
||||
if f, _ := s.GetFactory(nil, "1"); f == nil || asInt64(f.(map[string]interface{})["poolCount"]) != 1 {
|
||||
t.Fatalf("canonical PoolCreated must bump factory poolCount to 1, got %v", f)
|
||||
}
|
||||
idx.processLog(swapV3Log(ct, token0, "0xreal", 1000, 2500))
|
||||
sw, _ := s.GetSwaps(nil, 10, "timestamp", "desc", nil)
|
||||
if sw == nil || len(sw.([]interface{})) != 1 {
|
||||
t.Fatalf("Swap from the canonical V3 pool must be recorded exactly once, got %v", sw)
|
||||
}
|
||||
}
|
||||
|
||||
+67
-6
@@ -27,7 +27,8 @@ type Status struct {
|
||||
}
|
||||
|
||||
// Config tunes an Indexer. The zero value is valid: PoolManager defaults to the
|
||||
// canonical 0x9999 settlement address and StartBlock to 0 (index from genesis).
|
||||
// canonical 0x9999 settlement address, FactoryV2/FactoryV3 to the canonical Lux
|
||||
// mainnet AMM factories, and StartBlock to 0 (index from genesis).
|
||||
type Config struct {
|
||||
RPC string
|
||||
// PoolManager is the DEX settlement precompile address (0x9999). Logs from
|
||||
@@ -35,6 +36,15 @@ type Config struct {
|
||||
// Fill/Market entities. Lower-cased on construction for case-insensitive
|
||||
// compare against eth_getLogs `address` fields.
|
||||
PoolManager string
|
||||
// FactoryV2 is the canonical Uniswap-v2 factory. ONLY its PairCreated is
|
||||
// authoritative; pairs it creates are the only valid V2 Swap emitters. A
|
||||
// PairCreated from any other contract is rejected (otherwise anyone could
|
||||
// seed phantom pairs and inflate the public Factory/TVL/volume aggregates).
|
||||
// Lower-cased on construction.
|
||||
FactoryV2 string
|
||||
// FactoryV3 is the canonical Uniswap-v3 factory — same trust-root role as
|
||||
// FactoryV2 for PoolCreated and V3 Swaps. Lower-cased on construction.
|
||||
FactoryV3 string
|
||||
// StartBlock is the genesis-relative block to (re)index from. On a clean
|
||||
// chain relaunch the indexer rewinds to this value (see poll's reorg guard).
|
||||
StartBlock uint64
|
||||
@@ -70,6 +80,8 @@ const genesisBlockTag = "0x0"
|
||||
type Indexer struct {
|
||||
rpc string
|
||||
poolManager string // lower-cased 0x9999 settlement address
|
||||
factoryV2 string // lower-cased canonical V2 factory (PairCreated trust root)
|
||||
factoryV3 string // lower-cased canonical V3 factory (PoolCreated trust root)
|
||||
startBlock uint64
|
||||
store *storage.Store
|
||||
client *http.Client
|
||||
@@ -98,9 +110,19 @@ func NewWithConfig(cfg Config, store *storage.Store) *Indexer {
|
||||
if pm == "" {
|
||||
pm = LXSettleAddress
|
||||
}
|
||||
fv2 := cfg.FactoryV2
|
||||
if fv2 == "" {
|
||||
fv2 = LuxMainnet.FactoryV2
|
||||
}
|
||||
fv3 := cfg.FactoryV3
|
||||
if fv3 == "" {
|
||||
fv3 = LuxMainnet.FactoryV3
|
||||
}
|
||||
idx := &Indexer{
|
||||
rpc: cfg.RPC,
|
||||
poolManager: strings.ToLower(pm),
|
||||
factoryV2: strings.ToLower(fv2),
|
||||
factoryV3: strings.ToLower(fv3),
|
||||
startBlock: cfg.StartBlock,
|
||||
store: store,
|
||||
client: &http.Client{
|
||||
@@ -535,6 +557,14 @@ func topicAddr(topic string) string {
|
||||
}
|
||||
|
||||
func (idx *Indexer) handleSwapV2(l *logEntry, blockNum uint64, txHash, logIdx string) {
|
||||
// A V2 Swap is emitted by the pair contract, NOT the factory — so the gate is
|
||||
// "is the emitter a pair the canonical V2 factory created?". handlePairCreated
|
||||
// (gated to factoryV2) is the ONLY path that registers a pair, so a Swap from a
|
||||
// rogue pair was never registered and is dropped here. This closes the parallel
|
||||
// bypass into the same SeedSwap/factory volume aggregates the V4 path now gates.
|
||||
if !idx.isKnownPool(l.Address) {
|
||||
return
|
||||
}
|
||||
id := fmt.Sprintf("%s#%s", txHash, logIdx)
|
||||
sender := ""
|
||||
if len(l.Topics) > 1 {
|
||||
@@ -560,6 +590,12 @@ func (idx *Indexer) handleSwapV2(l *logEntry, blockNum uint64, txHash, logIdx st
|
||||
}
|
||||
|
||||
func (idx *Indexer) handleSwapV3(l *logEntry, blockNum uint64, txHash, logIdx string) {
|
||||
// Same trust model as handleSwapV2: a V3 Swap is emitted by the pool, which is
|
||||
// only registered via handlePoolCreated (gated to factoryV3). A Swap from an
|
||||
// unregistered (rogue) pool is dropped.
|
||||
if !idx.isKnownPool(l.Address) {
|
||||
return
|
||||
}
|
||||
id := fmt.Sprintf("%s#%s", txHash, logIdx)
|
||||
sender := ""
|
||||
if len(l.Topics) > 1 {
|
||||
@@ -579,7 +615,12 @@ func (idx *Indexer) handleSwapV3(l *logEntry, blockNum uint64, txHash, logIdx st
|
||||
}
|
||||
|
||||
func (idx *Indexer) handlePairCreated(l *logEntry) {
|
||||
if len(l.Topics) < 3 {
|
||||
// Scope to the canonical V2 factory (the trust root), exactly as the V4
|
||||
// handlers scope to 0x9999. A PairCreated from any other contract is rejected:
|
||||
// honoring it would let anyone seed phantom pairs/tokens and inflate the
|
||||
// explorer's public Factory/TVL/volume aggregates — and would register a rogue
|
||||
// pair address whose Swaps would then be accepted (the parallel bypass).
|
||||
if len(l.Topics) < 3 || !strings.EqualFold(l.Address, idx.factoryV2) {
|
||||
return
|
||||
}
|
||||
data := strings.TrimPrefix(l.Data, "0x")
|
||||
@@ -589,7 +630,9 @@ func (idx *Indexer) handlePairCreated(l *logEntry) {
|
||||
token0 := topicAddr(l.Topics[1])
|
||||
token1 := topicAddr(l.Topics[2])
|
||||
pair := "0x" + data[:64]
|
||||
pair = topicAddr("0x" + pair[len(pair)-40:])
|
||||
// Lower-case the pair key so isKnownPool's Swap-side lookup matches regardless
|
||||
// of how the RPC backend cased the emitter address (the V2 Swap's l.Address).
|
||||
pair = strings.ToLower(topicAddr("0x" + pair[len(pair)-40:]))
|
||||
|
||||
idx.store.SeedPool(pair, &storage.SeedPoolData{
|
||||
Token0: token0,
|
||||
@@ -602,7 +645,11 @@ func (idx *Indexer) handlePairCreated(l *logEntry) {
|
||||
}
|
||||
|
||||
func (idx *Indexer) handlePoolCreated(l *logEntry) {
|
||||
if len(l.Topics) < 4 {
|
||||
// Scope to the canonical V3 factory (the trust root), mirroring handlePairCreated
|
||||
// and the V4 0x9999 gate. A PoolCreated from a rogue contract is rejected so it
|
||||
// can neither inflate the Factory/TVL aggregates nor register a rogue pool whose
|
||||
// Swaps would be honored.
|
||||
if len(l.Topics) < 4 || !strings.EqualFold(l.Address, idx.factoryV3) {
|
||||
return
|
||||
}
|
||||
data := strings.TrimPrefix(l.Data, "0x")
|
||||
@@ -614,8 +661,9 @@ func (idx *Indexer) handlePoolCreated(l *logEntry) {
|
||||
feeHex := strings.TrimPrefix(l.Topics[3], "0x")
|
||||
fee, _ := strconv.ParseInt(feeHex, 16, 64)
|
||||
|
||||
// Pool address is in data (last 20 bytes of first 32-byte word if padded, or second word)
|
||||
pool := topicAddr("0x" + strings.TrimPrefix(l.Data, "0x"))
|
||||
// Pool address is in data (last 20 bytes of first 32-byte word if padded, or second word).
|
||||
// Lower-cased so isKnownPool's V3 Swap-side lookup matches the emitter regardless of RPC casing.
|
||||
pool := strings.ToLower(topicAddr("0x" + strings.TrimPrefix(l.Data, "0x")))
|
||||
|
||||
idx.store.SeedPool(pool, &storage.SeedPoolData{
|
||||
Token0: token0,
|
||||
@@ -834,6 +882,19 @@ func (idx *Indexer) isPoolManager(addr string) bool {
|
||||
return strings.EqualFold(addr, idx.poolManager)
|
||||
}
|
||||
|
||||
// isKnownPool reports whether a log address is a V2 pair / V3 pool that the
|
||||
// canonical factory created — i.e. one we persisted via handlePairCreated /
|
||||
// handlePoolCreated (both gated to the canonical factory). It is the Swap-side
|
||||
// half of the AMM trust root: the factory authorizes a pool, and only that pool's
|
||||
// Swaps are honored. eth_getLogs returns logs in block+logIndex order, so the
|
||||
// PairCreated/PoolCreated (in an earlier or the same block) is always processed
|
||||
// before the pool's first Swap. Pairs are seeded lower-cased (topicAddr), so the
|
||||
// lookup is case-insensitive.
|
||||
func (idx *Indexer) isKnownPool(addr string) bool {
|
||||
p, _ := idx.store.GetPool(nil, strings.ToLower(addr))
|
||||
return p != nil
|
||||
}
|
||||
|
||||
// shortAddr returns a stable short symbol for an unknown token address.
|
||||
func shortAddr(addr string) string {
|
||||
if len(addr) >= 8 {
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -71,5 +71,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -60,5 +60,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -60,5 +60,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -60,5 +60,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -89,5 +89,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package exchange
|
||||
|
||||
import "testing"
|
||||
|
||||
// LOW (query-DoS): the resolver limit must clamp `first` exactly like the dex
|
||||
// namespace — default 100, floor <1→100, hard cap 1000. Before the fix this
|
||||
// namespace's pl() capped at 1000 but had NO floor, so a negative/zero `first`
|
||||
// flowed straight to ListByType (and `first:-1` is "no limit" in SQLite — an
|
||||
// unbounded scan). This proves the floor now matches dex.
|
||||
func TestPL_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 := pl(tc.args); got != tc.want {
|
||||
t.Errorf("pl(%v) = %d, want %d", tc.args, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -114,5 +114,8 @@ func parseLimit(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
return limit
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package fhe
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -60,5 +60,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -71,5 +71,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -62,5 +62,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -62,5 +62,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -83,5 +83,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -84,5 +84,8 @@ func parseLimit(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
return limit
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -155,5 +155,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -60,5 +60,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -78,5 +78,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -146,5 +146,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -82,5 +82,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -71,5 +71,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -67,5 +67,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
@@ -79,5 +79,8 @@ func pl(args map[string]interface{}) int {
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
return min(limit, 1000)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user