Skip to content

Commit e4572da

Browse files
committed
feat: USD pricing via Curve-native tricrypto oracle + Python schema alignment
Replace the Swap entity with PoolPrice, add Token/PoolPair/Registry to mirror the target Python schema. Compute usd_main_volume / usd_reference_volume / usd_fee from on-chain data only: seed hardcoded stablecoins at \$1, then propagate usdPrice to WBTC/WETH (and anything else) via cryptoswap swap ratios — no external price API. Also adds tricrypto2 (0xd51a44d3fae010294c616388b506acda1bfaae46) as a static LegacyTricryptoPool on mainnet so its USDT/WBTC/WETH swaps seed prices for WBTC and WETH from block one of its activity.
1 parent 49498df commit e4572da

9 files changed

Lines changed: 1128 additions & 145 deletions

File tree

config.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,23 @@ contracts:
55
- name: TricryptoFactoryNG
66
events:
77
- event: TricryptoPoolDeployed(address pool, string name, string symbol, address weth, address[3] coins, address math, bytes32 salt, uint256 packed_precisions, uint256 packed_A_gamma, uint256 packed_fee_params, uint256 packed_rebalancing_params, uint256 packed_prices, address deployer)
8+
field_selection:
9+
transaction_fields:
10+
- hash
811

912
- name: TwocryptoFactoryNG
1013
events:
1114
- event: TwocryptoPoolDeployed(address pool, string name, string symbol, address[2] coins, address math, bytes32 salt, uint256[2] precisions, uint256 packed_A_gamma, uint256 packed_fee_params, uint256 packed_rebalancing_params, uint256 packed_prices, address deployer)
15+
field_selection:
16+
transaction_fields:
17+
- hash
1218

1319
- name: TwocryptoFactoryV1
1420
events:
1521
- event: CryptoPoolDeployed(address token, address[2] coins, uint256 A, uint256 gamma, uint256 mid_fee, uint256 out_fee, uint256 allowed_extra_profit, uint256 fee_gamma, uint256 adjustment_step, uint256 admin_fee, uint256 ma_half_time, uint256 initial_price, address deployer)
22+
field_selection:
23+
transaction_fields:
24+
- hash
1625

1726
- name: CryptoPool
1827
events:
@@ -21,6 +30,16 @@ contracts:
2130
transaction_fields:
2231
- hash
2332

33+
# Legacy tricrypto pool (e.g. tricrypto2 on mainnet) that was deployed
34+
# manually rather than via a factory. Older TokenExchange event signature:
35+
# no `fee`, no `packed_price_scale` fields. 3-coin pool.
36+
- name: LegacyTricryptoPool
37+
events:
38+
- event: TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought)
39+
field_selection:
40+
transaction_fields:
41+
- hash
42+
2443
# Standalone 2-coin Curve crypto pool with donation features. Not deployed
2544
# by any of the tracked factories above, so it is registered statically by
2645
# address per chain. Pool entity is initialized lazily on the first event.
@@ -68,6 +87,9 @@ chains:
6887
- name: TwocryptoPool
6988
address:
7089
- 0x83f24023d15d835a213df24fd309c47dab5beb32
90+
- name: LegacyTricryptoPool
91+
address:
92+
- 0xd51a44d3fae010294c616388b506acda1bfaae46
7193
- name: CryptoPool
7294

7395
# Arbitrum

schema.graphql

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,53 @@ type GlobalState {
77
lastUpdatedTimestamp: BigInt!
88
}
99

10+
enum PoolType {
11+
TRICRYPTO_NG
12+
TWOCRYPTO_NG
13+
CRYPTO_V1
14+
TRICRYPTO_LEGACY
15+
TWOCRYPTO_STANDALONE
16+
}
17+
18+
enum PriceSource {
19+
STABLECOIN
20+
DERIVED
21+
}
22+
23+
enum PriceType {
24+
TOKEN_EXCHANGE
25+
LAST_PRICES
26+
}
27+
28+
type Registry {
29+
id: ID!
30+
chainId: Int!
31+
address: String!
32+
registryType: String!
33+
}
34+
35+
type Token {
36+
id: ID!
37+
chainId: Int!
38+
address: String!
39+
symbol: String!
40+
decimals: Int!
41+
isStablecoin: Boolean!
42+
usdPrice: BigDecimal
43+
priceSource: PriceSource
44+
lastPricedBlock: Int
45+
lastPricedTimestamp: BigInt
46+
}
47+
1048
type Pool {
1149
id: ID!
50+
chainId: Int!
51+
address: String!
52+
lpTokenAddress: String!
53+
symbol: String!
54+
name: String!
55+
poolType: PoolType!
56+
registry: Registry
1257
nCoins: Int!
1358
coinAddresses: [String!]!
1459
coinSymbols: [String!]!
@@ -17,10 +62,51 @@ type Pool {
1762
priceScales: [BigInt!]!
1863
balances: [BigInt!]!
1964
totalSwapCount: BigInt!
65+
tvlUsd: BigDecimal
66+
hasDonations: Boolean!
67+
isActive: Boolean!
68+
deploymentBlock: Int!
69+
deploymentTimestamp: BigInt!
70+
deploymentTxHash: String!
2071
lastUpdatedBlock: Int!
2172
lastUpdatedTimestamp: BigInt!
22-
swaps: [Swap!]! @derivedFrom(field: "pool")
2373
liquidityEvents: [LiquidityEvent!]! @derivedFrom(field: "pool")
74+
poolPairs: [PoolPair!]! @derivedFrom(field: "pool")
75+
poolPrices: [PoolPrice!]! @derivedFrom(field: "pool")
76+
}
77+
78+
type PoolPair {
79+
id: ID!
80+
pool: Pool! @index
81+
mainTokenIndex: Int!
82+
referenceTokenIndex: Int!
83+
mainToken: Token!
84+
referenceToken: Token!
85+
}
86+
87+
type PoolPrice {
88+
id: ID!
89+
chainId: Int!
90+
pool: Pool! @index
91+
poolPair: PoolPair! @index
92+
priceType: PriceType!
93+
soldId: Int!
94+
boughtId: Int!
95+
tokensSold: BigDecimal!
96+
tokensBought: BigDecimal!
97+
price: BigDecimal!
98+
blockNumber: Int! @index
99+
timestamp: BigInt! @index
100+
txHash: String!
101+
logIndex: Int
102+
isRelevant: Boolean!
103+
buyer: String! @index
104+
fee: BigDecimal
105+
usdMainPrice: BigDecimal
106+
usdMainVolume: BigDecimal
107+
usdReferencePrice: BigDecimal
108+
usdReferenceVolume: BigDecimal
109+
usdFee: BigDecimal
24110
}
25111

26112
enum LiquidityEventKind {
@@ -45,23 +131,3 @@ type LiquidityEvent {
45131
blockNumber: Int!
46132
txHash: String!
47133
}
48-
49-
type Swap {
50-
id: ID!
51-
pool: Pool! @index
52-
buyer: String! @index
53-
soldTokenIndex: Int!
54-
soldTokenSymbol: String!
55-
soldAmount: BigInt!
56-
soldDecimals: Int!
57-
boughtTokenIndex: Int!
58-
boughtTokenSymbol: String!
59-
boughtAmount: BigInt!
60-
boughtDecimals: Int!
61-
fee: BigInt!
62-
timestamp: BigInt! @index
63-
blockNumber: Int!
64-
txHash: String!
65-
lastPrices: [BigInt!]!
66-
priceScales: [BigInt!]!
67-
}

src/constants.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Hardcoded stablecoins treated as $1. Used to bootstrap the USD pricing
2+
// graph: any token paired with one of these in a Curve cryptoswap pool gets
3+
// its usdPrice derived from the swap ratio. WETH/WBTC aren't listed here —
4+
// they get priced via tricrypto pools where coin 0 is a stablecoin.
5+
//
6+
// Addresses are lower-cased for membership checks.
7+
8+
export const STABLECOINS: Record<number, Set<string>> = {
9+
// Ethereum mainnet
10+
1: new Set(
11+
[
12+
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
13+
"0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
14+
"0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
15+
"0x853d955aCEf822Db058eb8505911ED77F175b99e", // FRAX
16+
"0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", // crvUSD
17+
"0x0000000000085d4780B73119b644AE5ecd22b376", // TUSD
18+
"0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", // USDe
19+
"0x8E870D67F660D95d5be530380D0eC0bd388289E1", // USDP (Pax Dollar)
20+
"0x6c3ea9036406852006290770BEdFcAbA0e23A0e8", // PYUSD
21+
"0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", // GUSD
22+
"0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3", // MIM
23+
"0x57Ab1ec28D129707052df4dF418D58a2D46d5f51", // sUSD
24+
].map((a) => a.toLowerCase()),
25+
),
26+
// Arbitrum
27+
42161: new Set(
28+
[
29+
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC (native)
30+
"0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", // USDC.e (bridged)
31+
"0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT
32+
"0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", // DAI
33+
"0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F", // FRAX
34+
"0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5", // crvUSD
35+
"0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", // USDe
36+
"0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07", // TUSD
37+
].map((a) => a.toLowerCase()),
38+
),
39+
};
40+
41+
export function isStablecoin(chainId: number, address: string): boolean {
42+
return STABLECOINS[chainId]?.has(address.toLowerCase()) ?? false;
43+
}
44+
45+
export function tokenId(chainId: number, address: string): string {
46+
return `${chainId}_${address.toLowerCase()}`;
47+
}

src/handlers/CryptoPool.ts

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
import { CryptoPool, type EvmChainId } from "generated";
1+
import { CryptoPool, type EvmChainId, type PoolPrice } from "generated";
22
import { getPoolState } from "../effects.js";
3+
import { tokenId } from "../constants.js";
4+
import {
5+
computePricing,
6+
computeTvlUsd,
7+
pairIdForSwap,
8+
} from "../pricing.js";
39

410
CryptoPool.TokenExchange.handler(async ({ event, context }) => {
511
const chainId = event.chainId;
6-
const poolId = `${chainId}_${event.srcAddress}`;
12+
const poolId = `${chainId}_${event.srcAddress.toLowerCase()}`;
713
const pool = await context.Pool.get(poolId);
814
if (!pool) {
915
context.log.warn(`Pool ${poolId} not found — skipping swap`);
1016
return;
1117
}
1218

1319
const address = event.srcAddress;
14-
15-
// Single effect fetches balances, last_prices, and price_scale for all coins
1620
const { balances, lastPrices, priceScales } = await getPoolState(context, {
1721
chainId: chainId as EvmChainId,
1822
address,
@@ -23,38 +27,107 @@ CryptoPool.TokenExchange.handler(async ({ event, context }) => {
2327
const soldIdx = Number(event.params.sold_id);
2428
const boughtIdx = Number(event.params.bought_id);
2529

26-
context.Swap.set({
30+
const pairId = pairIdForSwap(pool, soldIdx, boughtIdx);
31+
const pair = await context.PoolPair.get(pairId);
32+
if (!pair) {
33+
context.log.warn(`PoolPair ${pairId} not found — skipping pricing`);
34+
return;
35+
}
36+
37+
const [mainToken, referenceToken] = await Promise.all([
38+
context.Token.get(pair.mainToken_id),
39+
context.Token.get(pair.referenceToken_id),
40+
]);
41+
if (!mainToken || !referenceToken) {
42+
context.log.warn(`Missing Token entity for pair ${pairId} — skipping`);
43+
return;
44+
}
45+
46+
const soldAddr = pool.coinAddresses[soldIdx]!;
47+
const boughtAddr = pool.coinAddresses[boughtIdx]!;
48+
const soldToken =
49+
soldAddr === mainToken.address
50+
? mainToken
51+
: soldAddr === referenceToken.address
52+
? referenceToken
53+
: await context.Token.get(tokenId(chainId, soldAddr));
54+
const boughtToken =
55+
boughtAddr === mainToken.address
56+
? mainToken
57+
: boughtAddr === referenceToken.address
58+
? referenceToken
59+
: await context.Token.get(tokenId(chainId, boughtAddr));
60+
if (!soldToken || !boughtToken) {
61+
context.log.warn(`Missing sold/bought Token for pool ${poolId}`);
62+
return;
63+
}
64+
65+
const pricing = computePricing(
66+
chainId,
67+
pair,
68+
{ main: mainToken, reference: referenceToken, sold: soldToken, bought: boughtToken },
69+
soldIdx,
70+
boughtIdx,
71+
event.params.tokens_sold,
72+
event.params.tokens_bought,
73+
event.params.fee,
74+
event.block,
75+
);
76+
77+
for (const updated of pricing.tokenUpdates) {
78+
context.Token.set(updated);
79+
}
80+
81+
const isRelevant =
82+
pricing.usdMainVolume !== undefined ||
83+
pricing.usdReferenceVolume !== undefined;
84+
85+
const poolPrice: PoolPrice = {
2786
id: `${chainId}_${event.block.number}_${event.logIndex}`,
87+
chainId,
2888
pool_id: poolId,
29-
buyer: event.params.buyer,
30-
soldTokenIndex: soldIdx,
31-
soldTokenSymbol: pool.coinSymbols[soldIdx] ?? "???",
32-
soldAmount: event.params.tokens_sold,
33-
soldDecimals: pool.coinDecimals[soldIdx] ?? 18,
34-
boughtTokenIndex: boughtIdx,
35-
boughtTokenSymbol: pool.coinSymbols[boughtIdx] ?? "???",
36-
boughtAmount: event.params.tokens_bought,
37-
boughtDecimals: pool.coinDecimals[boughtIdx] ?? 18,
38-
fee: event.params.fee,
39-
timestamp: BigInt(event.block.timestamp),
89+
poolPair_id: pair.id,
90+
priceType: "TOKEN_EXCHANGE",
91+
soldId: soldIdx,
92+
boughtId: boughtIdx,
93+
tokensSold: pricing.tokensSoldDecimal,
94+
tokensBought: pricing.tokensBoughtDecimal,
95+
price: pricing.price,
4096
blockNumber: event.block.number,
97+
timestamp: BigInt(event.block.timestamp),
4198
txHash: event.transaction.hash,
42-
lastPrices,
43-
priceScales,
44-
});
99+
logIndex: event.logIndex,
100+
isRelevant,
101+
buyer: event.params.buyer,
102+
fee: pricing.feeDecimal,
103+
usdMainPrice: pricing.usdMainPrice,
104+
usdMainVolume: pricing.usdMainVolume,
105+
usdReferencePrice: pricing.usdReferencePrice,
106+
usdReferenceVolume: pricing.usdReferenceVolume,
107+
usdFee: pricing.usdFee,
108+
};
109+
context.PoolPrice.set(poolPrice);
110+
111+
// Refresh Pool with new balances / price state and recompute TVL.
112+
const allTokens = await Promise.all(
113+
pool.coinAddresses.map((addr) => context.Token.get(tokenId(chainId, addr))),
114+
);
115+
const tvlUsd = computeTvlUsd(
116+
{ ...pool, balances },
117+
allTokens,
118+
);
45119

46-
// Update Pool state
47120
context.Pool.set({
48121
...pool,
49122
lastPrices,
50123
priceScales,
51124
balances,
52125
totalSwapCount: pool.totalSwapCount + 1n,
126+
tvlUsd,
53127
lastUpdatedBlock: event.block.number,
54128
lastUpdatedTimestamp: BigInt(event.block.timestamp),
55129
});
56130

57-
// Update GlobalState swap count
58131
const globalId = `${chainId}`;
59132
const global = await context.GlobalState.get(globalId);
60133
if (global) {

0 commit comments

Comments
 (0)