diff --git a/networks.json b/networks.json index aab582c6..f92a1a8f 100644 --- a/networks.json +++ b/networks.json @@ -8,7 +8,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 12272146, - "storeEventsFrom": "25144091" + "storeEventsFrom": "25782545" }, "WeightedPoolFactory": { "address": "0x8E9aa87E45e92bad84D5F8DD1bff34Fb92637dE9", @@ -192,7 +192,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 15832990, - "storeEventsFrom": "87218537" + "storeEventsFrom": "92238634" }, "WeightedPoolFactory": { "address": "0x8E9aa87E45e92bad84D5F8DD1bff34Fb92637dE9", @@ -360,7 +360,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 222832, - "storeEventsFrom": "465171442" + "storeEventsFrom": "495852461" }, "WeightedPoolFactory": { "address": "0x7dFdEF5f355096603419239CE743BfaF1120312B", @@ -504,7 +504,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 24821598, - "storeEventsFrom": "46289479" + "storeEventsFrom": "47789878" }, "WeightedPoolV2Factory": { "address": "0xf302f9F50958c5593770FDf4d4812309fF77414f", @@ -584,7 +584,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 7003431, - "storeEventsFrom": "151886685" + "storeEventsFrom": "155732100" }, "WeightedPoolFactory": { "address": "0xdAE7e32ADc5d490a43cCba1f0c736033F2b4eFca", @@ -708,7 +708,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 26386141, - "storeEventsFrom": "86008797" + "storeEventsFrom": "93110686" }, "ComposableStablePoolV4Factory": { "address": "0x3B1eb8EB7b43882b385aB30533D9A2BeF9052a98", @@ -772,7 +772,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 3418831, - "storeEventsFrom": "10892082" + "storeEventsFrom": "11515633" }, "WeightedPoolV4Factory": { "address": "0x7920BFa1b2041911b354747CA7A6cDD2dfC50Cfd", @@ -828,7 +828,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 203079, - "storeEventsFrom": "32235974" + "storeEventsFrom": "33391890" }, "WeightedPoolV4Factory": { "address": "0x03F3Fb107e74F2EAC9358862E91ad3c692712054", @@ -888,7 +888,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 1196036, - "storeEventsFrom": "46291401" + "storeEventsFrom": "50136815" }, "WeightedPoolV4Factory": { "address": "0x4C32a8a8fDa4E24139B51b456B42290f51d6A1c4", @@ -916,7 +916,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 368300, - "storeEventsFrom": "71126495" + "storeEventsFrom": "77704412" }, "WeightedPoolV4Factory": { "address": "0x22f5b7FDD99076f1f20f8118854ce3984544D56d", @@ -940,7 +940,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 4708596, - "storeEventsFrom": "36280719" + "storeEventsFrom": "40126134" }, "WeightedPoolV4Factory": { "address": "0x9dA18982a33FD0c7051B19F0d7C76F2d5E7e017c", @@ -964,7 +964,7 @@ "Vault": { "address": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "startBlock": 8110317, - "storeEventsFrom": "39602283" + "storeEventsFrom": "43447698" }, "WeightedPoolV4Factory": { "address": "0xc3ccacE87f6d3A81724075ADcb5ddd85a8A1bB68", diff --git a/src/mappings/helpers/gyroECLP.ts b/src/mappings/helpers/gyroECLP.ts new file mode 100644 index 00000000..07f635e9 --- /dev/null +++ b/src/mappings/helpers/gyroECLP.ts @@ -0,0 +1,193 @@ +import { BigInt } from '@graphprotocol/graph-ts'; + +// GyroECLP pools store two parameter sets: the primary params (alpha, beta, c, s, lambda, 18 decimals) and the +// derived params (tauAlpha, tauBeta, u, v, w, z, dSq, 38 decimals, "Xp"). The pool constructor validates only +// bounds on the derived values - validateDerivedParamsLimits carries an explicit "does NOT check for internal +// consistency" disclaimer - so a pool can be deployed whose derived params do not correspond to its primary +// params. Two things go wrong when they do not, and each has been seen live: +// +// - a derived set that does not belong to c, s and the tau vectors breaks value conservation, so ordinary +// swaps pay out more than they take in; +// - tau vectors that do not belong to alpha and beta make the pool trade on a price range other than the one +// it advertises, while remaining perfectly self-consistent. +// +// The two checks below close both gaps using multiplication and division only. + +// 1e18 * 1e18. Divides an (18-dec * 18-dec * Xp) product back to Xp. +const E36 = BigInt.fromString('1000000000000000000000000000000000000'); + +// 1e18. +const E18 = BigInt.fromString('1000000000000000000'); + +// 1e38, one unit in Xp precision, and its square. +const ONE_XP = BigInt.fromString('100000000000000000000000000000000000000'); +const ONE_XP_SQ = ONE_XP.times(ONE_XP); + +// Largest per-value gap tolerated on the derived set, in Xp units. This is not slack for precision: honest pools +// sit at 0 to 1 wei, but a handful were built by a generator that divides the five values by dSq. Since dSq is +// c*c + s*s, which differs from 1 only by the 18-decimal rounding of the rotation components, the two conventions +// agree to about 1.1e20 in Xp units. 1e24 clears that spread by four orders of magnitude and still sits about +// seventeen orders below a real inconsistency. +const DERIVED_TOL_XP = BigInt.fromString('1000000000000000000000000'); + +// Relative tolerances, expressed as their inverse: a and b agree when |a - b| <= max(|a|, |b|) / TOL_INV. +// +// The tau identity holds to 1.2e-20 on the worst honest pool, so 1e-14 clears it by about six orders of magnitude +// while sitting roughly fourteen orders below a real mismatch. +const TAU_TOL_INV = BigInt.fromString('100000000000000'); + +// The tau vectors are normalized only to the 18-decimal rounding of c and s, so honest pools sit as far out as +// 1.3e-18. 1e-12 clears that by about six orders too. This check is a backstop: it is the only thing that catches +// both tau components being scaled by a common factor, which the ratio identity is blind to by construction. +const NORM_TOL_INV = BigInt.fromString('1000000000000'); + +const ZERO_BI = BigInt.fromI32(0); + +/** + * The raw, unscaled ECLP parameters as returned by getECLPParams(). + */ +export class EclpRawParams { + alpha: BigInt; + beta: BigInt; + c: BigInt; + s: BigInt; + lambda: BigInt; + tauAlphaX: BigInt; + tauAlphaY: BigInt; + tauBetaX: BigInt; + tauBetaY: BigInt; + u: BigInt; + v: BigInt; + w: BigInt; + z: BigInt; + dSq: BigInt; + + constructor( + alpha: BigInt, + beta: BigInt, + c: BigInt, + s: BigInt, + lambda: BigInt, + tauAlphaX: BigInt, + tauAlphaY: BigInt, + tauBetaX: BigInt, + tauBetaY: BigInt, + u: BigInt, + v: BigInt, + w: BigInt, + z: BigInt, + dSq: BigInt + ) { + this.alpha = alpha; + this.beta = beta; + this.c = c; + this.s = s; + this.lambda = lambda; + this.tauAlphaX = tauAlphaX; + this.tauAlphaY = tauAlphaY; + this.tauBetaX = tauBetaX; + this.tauBetaY = tauBetaY; + this.u = u; + this.v = v; + this.w = w; + this.z = z; + this.dSq = dSq; + } +} + +/** + * Divides an (18-dec * 18-dec * Xp) product back to Xp, truncating toward zero as the generators do. BigInt.div + * already truncates toward zero; the sign is handled explicitly so that does not have to be taken on trust. + */ +function toXp(n: BigInt): BigInt { + let q = n.abs().div(E36); + + return n.lt(ZERO_BI) ? q.neg() : q; +} + +function matchesAbsolute(stored: BigInt, expected: BigInt): boolean { + return stored.minus(expected).abs().le(DERIVED_TOL_XP); +} + +/** + * True when a and b agree to within a relative tolerance. Expressed as a division of the larger operand rather + * than a multiplication of the difference, so the comparison adds no width of its own. + */ +function matchesRelative(a: BigInt, b: BigInt, tolInverse: BigInt): boolean { + let scale = a.abs(); + let bAbs = b.abs(); + if (bAbs.gt(scale)) { + scale = bAbs; + } + + return a.minus(b).abs().le(scale.div(tolInverse)); +} + +/** + * True when u, v, w, z and dSq agree with the values c, s, tauAlpha and tauBeta imply: + * + * u = c*s * (tauBeta.x - tauAlpha.x) + * v = s*s * tauBeta.y + c*c * tauAlpha.y + * w = c*s * (tauBeta.y - tauAlpha.y) + * z = c*c * tauBeta.x + s*s * tauAlpha.x + * dSq = c*c + s*s + * + * This is not a reimplementation of the ECLP math the pools price with; it is the definition of the derived + * values, evaluated on the pool's own stored inputs. No transcendental function, no iteration, no precision loss. + */ +export function derivedParamsAreConsistent(p: EclpRawParams): boolean { + let cs = p.c.times(p.s); + let cc = p.c.times(p.c); + let ss = p.s.times(p.s); + + return ( + matchesAbsolute(p.u, toXp(cs.times(p.tauBetaX.minus(p.tauAlphaX)))) && + matchesAbsolute(p.v, toXp(ss.times(p.tauBetaY).plus(cc.times(p.tauAlphaY)))) && + matchesAbsolute(p.w, toXp(cs.times(p.tauBetaY.minus(p.tauAlphaY)))) && + matchesAbsolute(p.z, toXp(cc.times(p.tauBetaX).plus(ss.times(p.tauAlphaX)))) && + matchesAbsolute(p.dSq, toXp(cc.plus(ss).times(ONE_XP))) + ); +} + +/** + * True when the tau vector stored for a price bound is the one that bound implies. + * + * The pool's own math defines tau(px) = eta(zeta(px)) with eta(x) = (x, 1)/sqrt(x^2 + 1), so the square root is a + * common factor of both components and cancels in their ratio: + * + * tau.x / tau.y == zeta(px) == lambda * (c*px - s) / (c + s*px) + * + * Cross-multiplied so nothing is truncated before the comparison, with px, c, s and lambda at 18 decimals and the + * tau components at 38: + * + * tau.x * 1e18 * (c*1e18 + s*px) == tau.y * lambda * (c*px - s*1e18) + * + * The widest intermediate is about 330 bits, past int256 but far inside graph-node's BigInt, whose limit is + * 435412 bits and which arithmetic does not check against at all. + */ +function tauMatchesBound(p: EclpRawParams, px: BigInt, tauX: BigInt, tauY: BigInt): boolean { + let lhs = tauX.times(E18).times(p.c.times(E18).plus(p.s.times(px))); + let rhs = tauY.times(p.lambda).times(p.c.times(px).minus(p.s.times(E18))); + + return matchesRelative(lhs, rhs, TAU_TOL_INV); +} + +/** + * True when tauAlpha and tauBeta are the tau images of the pool's own alpha and beta. + */ +export function tauMatchesBounds(p: EclpRawParams): boolean { + return ( + tauMatchesBound(p, p.alpha, p.tauAlphaX, p.tauAlphaY) && tauMatchesBound(p, p.beta, p.tauBetaX, p.tauBetaY) + ); +} + +/** + * True when both tau vectors are unit vectors. Together with tauMatchesBounds, which fixes their direction, and + * the tau.y > 0 the pool enforces, this pins tau exactly. + */ +export function tauVectorsAreNormalized(p: EclpRawParams): boolean { + return ( + matchesRelative(p.tauAlphaX.times(p.tauAlphaX).plus(p.tauAlphaY.times(p.tauAlphaY)), ONE_XP_SQ, NORM_TOL_INV) && + matchesRelative(p.tauBetaX.times(p.tauBetaX).plus(p.tauBetaY.times(p.tauBetaY)), ONE_XP_SQ, NORM_TOL_INV) + ); +} diff --git a/src/mappings/poolFactory.ts b/src/mappings/poolFactory.ts index 839fdd50..f1f85e64 100644 --- a/src/mappings/poolFactory.ts +++ b/src/mappings/poolFactory.ts @@ -11,8 +11,14 @@ import { getFXOracle, } from './helpers/misc'; import { updatePoolWeights } from './helpers/weighted'; +import { + EclpRawParams, + derivedParamsAreConsistent, + tauMatchesBounds, + tauVectorsAreNormalized, +} from './helpers/gyroECLP'; -import { BigInt, Address, Bytes, ethereum } from '@graphprotocol/graph-ts'; +import { BigInt, Address, Bytes, ethereum, log } from '@graphprotocol/graph-ts'; import { PoolCreated } from '../types/WeightedPoolFactory/WeightedPoolFactory'; import { AaveLinearPoolCreated } from '../types/AaveLinearPoolV3Factory/AaveLinearPoolV3Factory'; @@ -555,6 +561,49 @@ function createGyroEPool(event: PoolCreated, poolTypeVersion: i32 = 1): void { let poolAddress: Address = event.params.pool; let poolContract = GyroEV2Pool.bind(poolAddress); + let eParamsCall = poolContract.try_getECLPParams(); + + // The pool constructor checks only bounds on the derived params, never that they belong to the primary params. + // A pool that fails either half of that missing check misprices against what it advertises, so it is not + // indexed at all: no Pool, no PoolContract, no token entities and no bump to vault.poolCount, because + // handleNewPool below only runs once the checks have passed. The Vault handlers already log and return when a + // Pool is missing, so the pool's swaps and joins are skipped with it. + if (!eParamsCall.reverted) { + let checkParams = eParamsCall.value.value0; + let checkDerived = eParamsCall.value.value1; + let raw = new EclpRawParams( + checkParams.alpha, + checkParams.beta, + checkParams.c, + checkParams.s, + checkParams.lambda, + checkDerived.tauAlpha.x, + checkDerived.tauAlpha.y, + checkDerived.tauBeta.x, + checkDerived.tauBeta.y, + checkDerived.u, + checkDerived.v, + checkDerived.w, + checkDerived.z, + checkDerived.dSq + ); + + let reason = ''; + if (!derivedParamsAreConsistent(raw)) { + reason = 'u, v, w, z and dSq are inconsistent with c, s, tauAlpha and tauBeta'; + } else if (!tauVectorsAreNormalized(raw)) { + reason = 'tauAlpha or tauBeta is not a unit vector'; + } else if (!tauMatchesBounds(raw)) { + reason = "tauAlpha or tauBeta does not correspond to the pool's alpha and beta"; + } + + if (reason != '') { + log.warning('GyroE pool {} not indexed: {}', [poolAddress.toHexString(), reason]); + + return; + } + } + let poolIdCall = poolContract.try_getPoolId(); let poolId = poolIdCall.value; @@ -565,7 +614,6 @@ function createGyroEPool(event: PoolCreated, poolTypeVersion: i32 = 1): void { pool.poolType = PoolType.GyroE; pool.poolTypeVersion = poolTypeVersion; - let eParamsCall = poolContract.try_getECLPParams(); if (!eParamsCall.reverted) { const params = eParamsCall.value.value0;