Files
zeekay 7883fef008 ai, graph, fhe: price the work, enforce readOnly, refuse the impossible
graph 58.0 -> 99.1, graphvm 0 -> 100, ai 90.0 -> 96.2, inference 87.3 -> 98.0,
modelregistry 70.2 -> 98.9, aivmbridge 93.1 -> 99.8, anchor 98.1 -> 99.1,
fhe 33.0 -> 80.1.

graph charged one flat fee for any amount of work. The sharpest form was not
response size but TargetChains: decoded from caller JSON with no cap, one
GraphQL query per entry, so seven bytes of calldata bought an unbounded
fan-out. Response size is only known after the work, so the fee now reserves
the worst case up front and refunds down -- there is no deduction that can come
up short, and a failed run consumes the reservation rather than everything
supplied, so malformed input is not a cheap probe.

ai halted the chain from eight bytes. Two handlers bounded their length
prefixes in uint32, so near 2^32 the sum wrapped, the check passed and the
slice panicked. Reproduced at both 96 and 8 bytes. The correct int-based parser
already existed in the same package; the handlers just did not call it, and the
existing test only probed 0xFFFF, which never wraps.

fhe never enforced readOnly. It was threaded through Run and all 42 handlers
and read by none, so every op but decrypt and sealOutput wrote ciphertext,
metadata and ownership to the trie under STATICCALL. Decided once at dispatch;
an unknown selector counts as a writer.

fhe also allocated before it checked: five bytes of calldata asked for 34 GB
behind a flat 50,000 gas fee. div and rem were registered and priced at 655M
gas with no case in the switch, so both fell through and returned the zero
handle with a nil error -- success for a guaranteed no-op, with the real
implementations written and never wired. select decoded its control as a bare
Ciphertext when every ebool the precompile stores is a BitCiphertext, so it
could not have worked for any input; it surfaced only once failures stopped
being reported as success.

graph decodeArgs accepted truncated calldata as a shorter argument list, so
trimming bytes changed which variables a query expanded with. graphvm chose
resolvers by map iteration order, which is consensus-visible.

Deleted a dead second gas table that disagreed with the real one, an address
constant appearing only at its own declaration, and ten other symbols with no
reader.
2026-08-28 11:53:22 -07:00

323 lines
12 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package graph
import (
"encoding/json"
"maps"
"math/big"
)
// =====================================
// Oracle Query Helpers
// =====================================
// Price feed query IDs for quick access
const (
// Core price feeds
QueryOracleETHPrice QueryID = 0x1001 // ETH/USD price
QueryOracleLUXPrice QueryID = 0x1002 // LUX/USD price
QueryOracleBTCPrice QueryID = 0x1003 // BTC/USD price
QueryOracleTokenPrice QueryID = 0x1004 // Any token price by address
// AMM-derived prices (TWAP)
QueryTWAPPrice QueryID = 0x1010 // Time-weighted average price
QuerySpotPrice QueryID = 0x1011 // Current spot price from pool
// Aggregated feeds
QueryAllPrices QueryID = 0x1020 // All major token prices
QueryPriceHistory QueryID = 0x1021 // Price history for charting
)
// OracleQueries contains pre-built GraphQL queries for price feeds
var OracleQueries = map[QueryID]QueryTemplate{
QueryOracleETHPrice: {
ID: QueryOracleETHPrice,
Query: `query { bundle(id: "1") { ethPriceUSD } }`,
GasCost: GasQuerySimple,
MaxArgs: 0,
},
QueryOracleLUXPrice: {
ID: QueryOracleLUXPrice,
Query: `query { bundle(id: "1") { luxPriceUSD } }`,
GasCost: GasQuerySimple,
MaxArgs: 0,
},
QueryOracleTokenPrice: {
ID: QueryOracleTokenPrice,
Query: `query($token: String!) { token(id: $token) { derivedETH derivedLUX symbol decimals } bundle(id: "1") { ethPriceUSD luxPriceUSD } }`,
GasCost: GasQuerySimple,
MaxArgs: 1,
},
QueryTWAPPrice: {
ID: QueryTWAPPrice,
Query: `query($pool: String!, $period: Int!) { poolHourDatas(first: $period, where: { pool: $pool }, orderBy: periodStartUnix, orderDirection: desc) { token0Price token1Price periodStartUnix } }`,
GasCost: GasQueryComplex,
MaxArgs: 2,
},
QuerySpotPrice: {
ID: QuerySpotPrice,
Query: `query($pool: String!) { pool(id: $pool) { token0Price token1Price sqrtPrice tick token0 { symbol decimals } token1 { symbol decimals } } }`,
GasCost: GasQuerySimple,
MaxArgs: 1,
},
QueryAllPrices: {
ID: QueryAllPrices,
Query: `query { bundle(id: "1") { ethPriceUSD luxPriceUSD } tokens(first: 20, orderBy: totalValueLockedUSD, orderDirection: desc) { id symbol derivedETH totalValueLockedUSD } }`,
GasCost: GasQueryComplex,
MaxArgs: 0,
},
QueryPriceHistory: {
ID: QueryPriceHistory,
Query: `query($token: String!, $days: Int!) { tokenDayDatas(first: $days, where: { token: $token }, orderBy: date, orderDirection: desc) { date priceUSD open high low close volumeUSD } }`,
GasCost: GasQueryComplex,
MaxArgs: 2,
},
}
// =====================================
// AMM Query Helpers
// =====================================
// AMM query IDs
const (
// Pool queries
QueryAMMPoolByTokens QueryID = 0x2001 // Find pool by token pair
QueryAMMPoolLiquidity QueryID = 0x2002 // Pool liquidity depth
QueryAMMPoolFees QueryID = 0x2003 // Pool fee tier and earned fees
QueryAMMPoolVolume QueryID = 0x2004 // Pool volume stats
// Position queries
QueryAMMMyPositions QueryID = 0x2010 // User's LP positions
QueryAMMPositionValue QueryID = 0x2011 // Position value + fees earned
QueryAMMPositionRange QueryID = 0x2012 // Position tick range
// Swap queries
QueryAMMQuote QueryID = 0x2020 // Get swap quote
QueryAMMRoute QueryID = 0x2021 // Best route for swap
QueryAMMRecentSwaps QueryID = 0x2022 // Recent swaps in pool
// Liquidity queries
QueryAMMTickLiquidity QueryID = 0x2030 // Liquidity at each tick
QueryAMMLiquidityDepth QueryID = 0x2031 // Order book style depth
)
// AMMQueries contains pre-built GraphQL queries for AMM data
var AMMQueries = map[QueryID]QueryTemplate{
QueryAMMPoolByTokens: {
ID: QueryAMMPoolByTokens,
Query: `query($token0: String!, $token1: String!) { pools(where: { token0: $token0, token1: $token1 }) { id feeTier liquidity sqrtPrice tick volumeUSD totalValueLockedUSD token0Price token1Price } }`,
GasCost: GasQuerySimple,
MaxArgs: 2,
},
QueryAMMPoolLiquidity: {
ID: QueryAMMPoolLiquidity,
Query: `query($pool: String!) { pool(id: $pool) { liquidity sqrtPrice tick totalValueLockedToken0 totalValueLockedToken1 totalValueLockedUSD } ticks(first: 100, where: { pool: $pool }, orderBy: tickIdx) { tickIdx liquidityNet liquidityGross price0 price1 } }`,
GasCost: GasQueryComplex,
MaxArgs: 1,
},
QueryAMMPoolFees: {
ID: QueryAMMPoolFees,
Query: `query($pool: String!) { pool(id: $pool) { feeTier feeGrowthGlobal0X128 feeGrowthGlobal1X128 collectedFeesToken0 collectedFeesToken1 collectedFeesUSD feesUSD } }`,
GasCost: GasQuerySimple,
MaxArgs: 1,
},
QueryAMMPoolVolume: {
ID: QueryAMMPoolVolume,
Query: `query($pool: String!) { pool(id: $pool) { volumeToken0 volumeToken1 volumeUSD txCount } poolDayDatas(first: 7, where: { pool: $pool }, orderBy: date, orderDirection: desc) { date volumeUSD tvlUSD feesUSD } }`,
GasCost: GasQueryComplex,
MaxArgs: 1,
},
QueryAMMMyPositions: {
ID: QueryAMMMyPositions,
Query: `query($owner: String!) { positions(where: { owner: $owner, liquidity_gt: "0" }) { id pool { id token0 { symbol } token1 { symbol } feeTier } tickLower { tickIdx price0 } tickUpper { tickIdx price0 } liquidity depositedToken0 depositedToken1 collectedFeesToken0 collectedFeesToken1 } }`,
GasCost: GasQueryComplex,
MaxArgs: 1,
},
QueryAMMPositionValue: {
ID: QueryAMMPositionValue,
Query: `query($positionId: String!) { position(id: $positionId) { liquidity depositedToken0 depositedToken1 withdrawnToken0 withdrawnToken1 collectedToken0 collectedToken1 collectedFeesToken0 collectedFeesToken1 pool { token0Price token1Price token0 { decimals symbol } token1 { decimals symbol } } } }`,
GasCost: GasQuerySimple,
MaxArgs: 1,
},
QueryAMMQuote: {
ID: QueryAMMQuote,
Query: `query($tokenIn: String!, $tokenOut: String!, $amount: String!) { quote(tokenIn: $tokenIn, tokenOut: $tokenOut, amountIn: $amount) { amountOut priceImpact route { pools { id feeTier } } gasEstimate } }`,
GasCost: GasQueryComplex,
MaxArgs: 3,
},
QueryAMMRecentSwaps: {
ID: QueryAMMRecentSwaps,
Query: `query($pool: String!, $count: Int!) { swaps(first: $count, where: { pool: $pool }, orderBy: timestamp, orderDirection: desc) { timestamp sender recipient amount0 amount1 amountUSD sqrtPriceX96 tick } }`,
GasCost: GasQueryComplex,
MaxArgs: 2,
},
QueryAMMTickLiquidity: {
ID: QueryAMMTickLiquidity,
Query: `query($pool: String!, $tickLower: Int!, $tickUpper: Int!) { ticks(where: { pool: $pool, tickIdx_gte: $tickLower, tickIdx_lte: $tickUpper }, orderBy: tickIdx) { tickIdx liquidityNet liquidityGross price0 price1 } }`,
GasCost: GasQueryComplex,
MaxArgs: 3,
},
}
// =====================================
// Response Types
// =====================================
// PriceResponse represents a token price query result
type PriceResponse struct {
TokenAddress string `json:"tokenAddress"`
Symbol string `json:"symbol"`
PriceUSD *big.Int `json:"priceUSD"` // 18 decimals
PriceETH *big.Int `json:"priceETH"` // 18 decimals
PriceLUX *big.Int `json:"priceLUX"` // 18 decimals
Timestamp uint64 `json:"timestamp"`
Source string `json:"source"` // "pool", "oracle", "twap"
}
// PoolInfoResponse represents pool information
type PoolInfoResponse struct {
PoolAddress string `json:"poolAddress"`
Token0 string `json:"token0"`
Token1 string `json:"token1"`
Token0Symbol string `json:"token0Symbol"`
Token1Symbol string `json:"token1Symbol"`
FeeTier uint32 `json:"feeTier"`
Liquidity *big.Int `json:"liquidity"`
SqrtPriceX96 *big.Int `json:"sqrtPriceX96"`
Tick int32 `json:"tick"`
Token0Price *big.Int `json:"token0Price"` // 18 decimals
Token1Price *big.Int `json:"token1Price"` // 18 decimals
TVL_USD *big.Int `json:"tvlUSD"` // 18 decimals
Volume24h_USD *big.Int `json:"volume24hUSD"` // 18 decimals
Fees24h_USD *big.Int `json:"fees24hUSD"` // 18 decimals
}
// QuoteResponse represents a swap quote
type QuoteResponse struct {
AmountIn *big.Int `json:"amountIn"`
AmountOut *big.Int `json:"amountOut"`
PriceImpact *big.Int `json:"priceImpact"` // basis points (10000 = 100%)
GasEstimate uint64 `json:"gasEstimate"`
Route []string `json:"route"` // pool addresses
FeeTiers []uint32 `json:"feeTiers"`
}
// PositionResponse represents an LP position
type PositionResponse struct {
PositionID string `json:"positionId"`
Owner string `json:"owner"`
PoolAddress string `json:"poolAddress"`
Token0 string `json:"token0"`
Token1 string `json:"token1"`
TickLower int32 `json:"tickLower"`
TickUpper int32 `json:"tickUpper"`
Liquidity *big.Int `json:"liquidity"`
Amount0 *big.Int `json:"amount0"`
Amount1 *big.Int `json:"amount1"`
FeesEarned0 *big.Int `json:"feesEarned0"`
FeesEarned1 *big.Int `json:"feesEarned1"`
ValueUSD *big.Int `json:"valueUSD"` // 18 decimals
InRange bool `json:"inRange"`
}
// =====================================
// Helper Functions
// =====================================
// ParsePriceResponse parses a GraphQL price response
func ParsePriceResponse(data []byte) (*PriceResponse, error) {
var resp struct {
Data struct {
Token struct {
DerivedETH string `json:"derivedETH"`
DerivedLUX string `json:"derivedLUX"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
} `json:"token"`
Bundle struct {
EthPriceUSD string `json:"ethPriceUSD"`
LuxPriceUSD string `json:"luxPriceUSD"`
} `json:"bundle"`
} `json:"data"`
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
// Calculate USD price from derivedETH * ethPriceUSD. A missing or malformed number
// makes SetString return nil, and multiplying through a nil *big.Float panics — so a
// response that simply omits a price field must be refused here rather than crashing
// the caller.
derivedETH, ok := new(big.Float).SetString(resp.Data.Token.DerivedETH)
if !ok {
return nil, ErrInvalidQuery
}
ethPrice, ok := new(big.Float).SetString(resp.Data.Bundle.EthPriceUSD)
if !ok {
return nil, ErrInvalidQuery
}
priceFloat := new(big.Float).Mul(derivedETH, ethPrice)
// Convert to 18 decimal integer
scale := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
priceScaled := new(big.Float).Mul(priceFloat, scale)
priceUSD, _ := priceScaled.Int(nil)
return &PriceResponse{
Symbol: resp.Data.Token.Symbol,
PriceUSD: priceUSD,
Source: "pool",
}, nil
}
// ParsePoolInfoResponse parses a GraphQL pool response
func ParsePoolInfoResponse(data []byte) (*PoolInfoResponse, error) {
var resp struct {
Data struct {
Pool struct {
ID string `json:"id"`
Token0 struct{ Symbol string } `json:"token0"`
Token1 struct{ Symbol string } `json:"token1"`
FeeTier int `json:"feeTier"`
Liquidity string `json:"liquidity"`
SqrtPrice string `json:"sqrtPrice"`
Tick int `json:"tick"`
Token0Price string `json:"token0Price"`
Token1Price string `json:"token1Price"`
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
VolumeUSD string `json:"volumeUSD"`
} `json:"pool"`
} `json:"data"`
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
liquidity, _ := new(big.Int).SetString(resp.Data.Pool.Liquidity, 10)
sqrtPrice, _ := new(big.Int).SetString(resp.Data.Pool.SqrtPrice, 10)
return &PoolInfoResponse{
PoolAddress: resp.Data.Pool.ID,
Token0Symbol: resp.Data.Pool.Token0.Symbol,
Token1Symbol: resp.Data.Pool.Token1.Symbol,
FeeTier: uint32(resp.Data.Pool.FeeTier),
Liquidity: liquidity,
SqrtPriceX96: sqrtPrice,
Tick: int32(resp.Data.Pool.Tick),
}, nil
}
// init registers oracle and AMM queries
func init() {
// Add oracle queries to predefined queries
maps.Copy(PredefinedQueries, OracleQueries)
// Add AMM queries to predefined queries
maps.Copy(PredefinedQueries, AMMQueries)
}