@gearbox-protocol/sdk 18.2.0-next.10 → 18.2.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/cjs/dev/AccountOpener.js +1 -1
  2. package/dist/cjs/dev/withdrawalUtils.js +1 -1
  3. package/dist/cjs/history/trace/extractTransfers.js +1 -1
  4. package/dist/cjs/onchain/accounts/CreditAccountsServiceV310.js +2 -2
  5. package/dist/cjs/onchain/accounts/liquidations/LiquidationsService.js +1 -1
  6. package/dist/cjs/onchain/accounts/withdrawal-compressor/RedemptionLoggerV310Contract.js +1 -2
  7. package/dist/cjs/onchain/accounts/withdrawal-compressor/WithdrawalCompressorV310Contract.js +1 -2
  8. package/dist/cjs/onchain/accounts/withdrawal-compressor/WithdrawalCompressorV311Contract.js +1 -2
  9. package/dist/cjs/onchain/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.js +1 -1
  10. package/dist/cjs/onchain/base/TokensMeta.js +3 -3
  11. package/dist/cjs/onchain/core/createAddressProvider.js +1 -1
  12. package/dist/cjs/onchain/market/adapters/contracts/AccountMigratorAdapterContract.js +1 -1
  13. package/dist/cjs/onchain/market/adapters/contracts/ERC4626AdapterContract.js +1 -1
  14. package/dist/cjs/onchain/market/credit/CreditFacadeV310BaseContract.js +1 -1
  15. package/dist/cjs/onchain/market/pool/PoolV310Contract.js +1 -1
  16. package/dist/cjs/onchain/market/rwa/createDegenNFT.js +1 -1
  17. package/dist/cjs/onchain/market/zapper/IETHZapperContract.js +1 -2
  18. package/dist/cjs/onchain/market/zapper/ZapperContract.js +1 -1
  19. package/dist/cjs/onchain/preview/preview/previewPoolPositionOperation.js +2 -2
  20. package/dist/cjs/onchain/utils/viem/simulateWithPriceUpdates.js +1 -1
  21. package/dist/cjs/permissionless/api/index.js +26 -0
  22. package/dist/cjs/permissionless/api/oracles/PermissionlessOracles.js +191 -0
  23. package/dist/cjs/permissionless/api/oracles/index.js +28 -0
  24. package/dist/cjs/permissionless/api/oracles/schemas.js +17 -0
  25. package/dist/cjs/permissionless/api/oracles/score/formula.js +99 -0
  26. package/dist/cjs/permissionless/api/oracles/score/index.js +8 -0
  27. package/dist/cjs/permissionless/api/oracles/score/run.js +88 -0
  28. package/dist/cjs/permissionless/api/oracles/score/types.js +1 -0
  29. package/dist/cjs/permissionless/api/oracles/stress/classify.js +258 -0
  30. package/dist/cjs/permissionless/api/oracles/stress/index.js +22 -0
  31. package/dist/cjs/permissionless/api/oracles/stress/override.js +125 -0
  32. package/dist/cjs/permissionless/api/oracles/stress/run.js +432 -0
  33. package/dist/cjs/permissionless/api/oracles/stress/score.js +98 -0
  34. package/dist/cjs/permissionless/api/oracles/stress/types.js +1 -0
  35. package/dist/cjs/permissionless/index.js +27 -0
  36. package/dist/cjs/permissionless/utils/index.js +1 -0
  37. package/dist/cjs/permissionless/utils/price-update/get-prices.js +39 -0
  38. package/dist/cjs/permissionless/utils/price-update/index.js +1 -0
  39. package/dist/esm/permissionless/api/index.js +7 -1
  40. package/dist/esm/permissionless/api/oracles/PermissionlessOracles.js +193 -2
  41. package/dist/esm/permissionless/api/oracles/index.js +10 -1
  42. package/dist/esm/permissionless/api/oracles/schemas.js +16 -1
  43. package/dist/esm/permissionless/api/oracles/score/formula.js +96 -0
  44. package/dist/esm/permissionless/api/oracles/score/index.js +4 -0
  45. package/dist/esm/permissionless/api/oracles/score/run.js +87 -0
  46. package/dist/esm/permissionless/api/oracles/score/types.js +1 -0
  47. package/dist/esm/permissionless/api/oracles/stress/classify.js +256 -0
  48. package/dist/esm/permissionless/api/oracles/stress/index.js +6 -0
  49. package/dist/esm/permissionless/api/oracles/stress/override.js +119 -0
  50. package/dist/esm/permissionless/api/oracles/stress/run.js +427 -0
  51. package/dist/esm/permissionless/api/oracles/stress/score.js +95 -0
  52. package/dist/esm/permissionless/api/oracles/stress/types.js +1 -0
  53. package/dist/esm/permissionless/index.js +8 -2
  54. package/dist/esm/permissionless/utils/index.js +2 -2
  55. package/dist/esm/permissionless/utils/price-update/get-prices.js +39 -1
  56. package/dist/esm/permissionless/utils/price-update/index.js +2 -2
  57. package/dist/types/permissionless/api/index.d.ts +10 -2
  58. package/dist/types/permissionless/api/oracles/PermissionlessOracles.d.ts +115 -1
  59. package/dist/types/permissionless/api/oracles/index.d.ts +12 -2
  60. package/dist/types/permissionless/api/oracles/schemas.d.ts +47 -0
  61. package/dist/types/permissionless/api/oracles/score/formula.d.ts +46 -0
  62. package/dist/types/permissionless/api/oracles/score/index.d.ts +4 -0
  63. package/dist/types/permissionless/api/oracles/score/run.d.ts +21 -0
  64. package/dist/types/permissionless/api/oracles/score/types.d.ts +85 -0
  65. package/dist/types/permissionless/api/oracles/stress/classify.d.ts +57 -0
  66. package/dist/types/permissionless/api/oracles/stress/index.d.ts +6 -0
  67. package/dist/types/permissionless/api/oracles/stress/override.d.ts +59 -0
  68. package/dist/types/permissionless/api/oracles/stress/run.d.ts +73 -0
  69. package/dist/types/permissionless/api/oracles/stress/score.d.ts +57 -0
  70. package/dist/types/permissionless/api/oracles/stress/types.d.ts +272 -0
  71. package/dist/types/permissionless/api/oracles/types.d.ts +58 -0
  72. package/dist/types/permissionless/index.d.ts +11 -3
  73. package/dist/types/permissionless/utils/index.d.ts +2 -2
  74. package/dist/types/permissionless/utils/price-update/get-prices.d.ts +16 -0
  75. package/dist/types/permissionless/utils/price-update/index.d.ts +2 -2
  76. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/permissionless/api/oracles/score/formula.ts
3
+ /**
4
+ * How well a market guards an asset's price, from its two feeds alone.
5
+ *
6
+ * Two questions, and the points are lost for each separately:
7
+ *
8
+ * - Would the reserve disagree with a main feed that has gone wrong? Only if
9
+ * it is a second feed on other sources. A missing reserve, a
10
+ * `ZeroPriceFeed`, the main feed again, and a different feed on the same
11
+ * sources all answer no.
12
+ * - Is either feed bounded? A `BoundedPriceFeed` caps the answer from above,
13
+ * and a Gearbox LP feed clamps its exchange rate to a window.
14
+ *
15
+ * Independence costs more than the bound, 6 points against 5, because it is
16
+ * the broader protection: a bounded feed caps only from above, and an LP
17
+ * feed's window holds only its rate, not the price of what the rate is in.
18
+ * Either failure alone lands a market at 4 or 5; both together at 0.
19
+ */
20
+ const INDEPENDENCE_WEIGHT = 6;
21
+ const BOUND_WEIGHT = 5;
22
+ /**
23
+ * The feed types that bound what they answer: the `BoundedPriceFeed`, and
24
+ * every Gearbox LP feed, all of which clamp the rate they read.
25
+ */
26
+ const BOUNDED_TYPE = /::(BOUNDED|ERC4626|CURVE_\w+|WSTETH|YEARN|MELLOW\w*|BALANCER\w*)$/;
27
+ const ZERO_TYPE = "PRICE_FEED::ZERO";
28
+ /**
29
+ * The share of the two sides' sources that are not shared, by key.
30
+ *
31
+ * Nothing on either side — two hardcoded prices — is 0, because a reserve is
32
+ * there to be a second opinion and neither of them is one.
33
+ */
34
+ function independenceOf(main, reserve) {
35
+ const a = new Set(main.map((source) => source.key));
36
+ const b = new Set(reserve.map((source) => source.key));
37
+ const union = /* @__PURE__ */ new Set([...a, ...b]);
38
+ if (union.size === 0) return 0;
39
+ return 1 - [...a].filter((key) => b.has(key)).length / union.size;
40
+ }
41
+ /** Down to the half point, so that a score is never rounded up into a band. */
42
+ function toHalf(value) {
43
+ return Math.floor(value * 2 + 1e-9) / 2;
44
+ }
45
+ /**
46
+ * One market's score for one asset.
47
+ *
48
+ * `reserve` is `null` where the market holds no reserve for the asset at all.
49
+ * A source is passed per side and keyed by the caller, which is where it is
50
+ * decided that an unnamed source is its own contract and a named one is its
51
+ * group.
52
+ */
53
+ function scoreMarket(market, main, reserve) {
54
+ const reasons = [];
55
+ let hasReserve = true;
56
+ if (reserve === null) {
57
+ hasReserve = false;
58
+ reasons.push("no reserve feed");
59
+ } else if (reserve.feed.contractType === ZERO_TYPE) {
60
+ hasReserve = false;
61
+ reasons.push("reserve is a ZeroPriceFeed");
62
+ } else if (reserve.feed.feed.toLowerCase() === main.feed.feed.toLowerCase()) {
63
+ hasReserve = false;
64
+ reasons.push("reserve is the main feed");
65
+ }
66
+ const reserveSources = hasReserve && reserve ? reserve.sources : [];
67
+ const independence = hasReserve ? independenceOf(main.sources, reserveSources) : 0;
68
+ const reserveKeys = new Set(reserveSources.map((source) => source.key));
69
+ const mainKeys = new Set(main.sources.map((source) => source.key));
70
+ const shared = main.sources.filter((source) => reserveKeys.has(source.key));
71
+ const names = (sources) => [...new Set(sources.map((source) => source.name))];
72
+ if (hasReserve && independence < 1) reasons.push(shared.length > 0 ? `main and reserve share ${names(shared).join(", ")}` : "neither feed reads a price source");
73
+ const unverified = hasReserve && [...main.sources.filter((source) => !reserveKeys.has(source.key)), ...reserveSources.filter((source) => !mainKeys.has(source.key))].some((source) => !source.identified);
74
+ const bounded = BOUNDED_TYPE.test(main.feed.contractType) || reserve !== null && BOUNDED_TYPE.test(reserve.feed.contractType);
75
+ if (!bounded) reasons.push("neither feed is bounded");
76
+ const score = toHalf(Math.max(0, 10 - 6 * (1 - independence) - 5 * (bounded ? 0 : 1)));
77
+ return {
78
+ ...market,
79
+ main: {
80
+ ...main.feed,
81
+ sources: names(main.sources)
82
+ },
83
+ reserve: reserve ? {
84
+ ...reserve.feed,
85
+ sources: names(reserve.sources)
86
+ } : null,
87
+ hasReserve,
88
+ independence,
89
+ bounded,
90
+ sharedSources: names(shared),
91
+ unverified,
92
+ score,
93
+ reasons
94
+ };
95
+ }
96
+ //#endregion
97
+ exports.BOUND_WEIGHT = BOUND_WEIGHT;
98
+ exports.INDEPENDENCE_WEIGHT = INDEPENDENCE_WEIGHT;
99
+ exports.scoreMarket = scoreMarket;
@@ -0,0 +1,8 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_permissionless_api_oracles_score_formula = require("./formula.js");
3
+ const require_permissionless_api_oracles_score_run = require("./run.js");
4
+ require("./types.js");
5
+ exports.BOUND_WEIGHT = require_permissionless_api_oracles_score_formula.BOUND_WEIGHT;
6
+ exports.INDEPENDENCE_WEIGHT = require_permissionless_api_oracles_score_formula.INDEPENDENCE_WEIGHT;
7
+ exports.runAssetScores = require_permissionless_api_oracles_score_run.runAssetScores;
8
+ exports.scoreMarket = require_permissionless_api_oracles_score_formula.scoreMarket;
@@ -0,0 +1,88 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_permissionless_api_oracles_stress_classify = require("../stress/classify.js");
3
+ const require_permissionless_api_oracles_stress_run = require("../stress/run.js");
4
+ const require_permissionless_api_oracles_score_formula = require("./formula.js");
5
+ //#region src/permissionless/api/oracles/score/run.ts
6
+ /**
7
+ * How a source is told apart from another when two feeds are compared.
8
+ *
9
+ * A named source by its group, since two Chainlink aggregators are one
10
+ * provider failing together. An unnamed one by its contract, since nothing
11
+ * says what else it fails with — so the same contract on both sides is
12
+ * shared, and two different ones are not.
13
+ */
14
+ function toScoreSource(source) {
15
+ return {
16
+ key: source.identified ? `group:${source.group}` : `contract:${source.target}`,
17
+ name: source.group,
18
+ identified: source.identified
19
+ };
20
+ }
21
+ /**
22
+ * Every asset of one chain, scored by how its running markets price it.
23
+ *
24
+ * One read of every oracle's feed tree and one of the LP feeds' rate
25
+ * contracts, both multicalls: the score is a reading of the trees and needs
26
+ * no simulation.
27
+ **/
28
+ async function runAssetScores({ client, chainId, slots, compressor, logger }) {
29
+ const running = slots.filter((slot) => !slot.isShutdown);
30
+ const oracles = [...new Set(running.map((slot) => slot.priceOracle.toLowerCase()))];
31
+ const { nodes, unreadableOracles } = await require_permissionless_api_oracles_stress_run.readForest(client, compressor, oracles);
32
+ for (const oracle of unreadableOracles) logger?.warn(`price oracle ${oracle} of chain ${chainId} would not report its feed tree; the markets on it are not scored`);
33
+ const { targets } = await require_permissionless_api_oracles_stress_run.readRateTargets(client, [...nodes.values()].filter((node) => require_permissionless_api_oracles_stress_run.RATE_FEED_TYPE.test(node.contractType)).map((node) => node.address));
34
+ const { sources } = require_permissionless_api_oracles_stress_classify.classifySources(chainId, [...nodes.values()], targets, () => null);
35
+ const byTarget = new Map(sources.filter((source) => source.perturbable).map((source) => [source.target, toScoreSource(source)]));
36
+ const feedOf = (slot) => slot.priceFeed.toLowerCase();
37
+ const reach = require_permissionless_api_oracles_stress_classify.sourcesByFeed([...new Set(running.map(feedOf))], nodes, sources);
38
+ const sourcesOf = (feed) => [...reach.get(feed) ?? []].map((target) => byTarget.get(target)).filter((source) => source !== void 0);
39
+ const positions = /* @__PURE__ */ new Map();
40
+ for (const slot of running) {
41
+ const key = `${slot.market.toLowerCase()}:${slot.token.toLowerCase()}`;
42
+ const position = positions.get(key) ?? {};
43
+ position[slot.reserve ? "reserve" : "main"] = slot;
44
+ positions.set(key, position);
45
+ }
46
+ const byAsset = /* @__PURE__ */ new Map();
47
+ for (const { main, reserve } of positions.values()) {
48
+ if (!main || main.isUnderlying) continue;
49
+ const asset = main.token.toLowerCase();
50
+ const entry = byAsset.get(asset) ?? {
51
+ symbol: main.tokenSymbol,
52
+ markets: []
53
+ };
54
+ byAsset.set(asset, entry);
55
+ const mainNode = nodes.get(feedOf(main));
56
+ if (!mainNode) continue;
57
+ if (mainNode.contractType === "PRICE_FEED::ZERO") continue;
58
+ const side = (slot) => ({
59
+ feed: {
60
+ feed: feedOf(slot),
61
+ name: slot.priceFeedName,
62
+ contractType: nodes.get(feedOf(slot))?.contractType ?? ""
63
+ },
64
+ sources: sourcesOf(feedOf(slot))
65
+ });
66
+ entry.markets.push(require_permissionless_api_oracles_score_formula.scoreMarket({
67
+ market: main.market,
68
+ marketName: main.marketName,
69
+ marketConfigurator: main.marketConfigurator,
70
+ curatorName: main.curatorName
71
+ }, side(main), reserve ? side(reserve) : null));
72
+ }
73
+ return [...byAsset].map(([asset, { symbol, markets }]) => {
74
+ const sorted = markets.sort((a, b) => a.score - b.score);
75
+ const worst = sorted[0] ?? null;
76
+ return {
77
+ chainId,
78
+ asset,
79
+ symbol,
80
+ score: worst?.score ?? null,
81
+ unverified: sorted.some((market) => market.unverified),
82
+ worst,
83
+ markets: sorted
84
+ };
85
+ });
86
+ }
87
+ //#endregion
88
+ exports.runAssetScores = runAssetScores;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
@@ -0,0 +1,258 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/permissionless/api/oracles/stress/classify.ts
3
+ /**
4
+ * The feed types whose price is a protocol's exchange rate read off a
5
+ * contract, mapped to the group that rate belongs to.
6
+ *
7
+ * These are not leaves: an LP feed also reads the price of whatever the rate
8
+ * is denominated in, and that child is somebody else's source. What belongs to
9
+ * the protocol is the rate alone, which is why these are perturbed at the
10
+ * contract the feed reads it from rather than at the feed.
11
+ *
12
+ * `null` means the protocol is the vault's own and has to come from
13
+ * {@link VAULT_GROUPS} or, failing that, from the feed's description.
14
+ */
15
+ const RATE_TYPE_GROUPS = {
16
+ WSTETH: "Lido",
17
+ CURVE_STABLE: "Curve",
18
+ CURVE_CRYPTO: "Curve",
19
+ CURVE_TWAP: "Curve",
20
+ YEARN: "Yearn",
21
+ MELLOW_LRT: "Mellow",
22
+ BALANCER_STABLE: "Balancer",
23
+ BALANCER_WEIGHTED: "Balancer",
24
+ ERC4626: null
25
+ };
26
+ /**
27
+ * Which protocol an ERC-4626 vault belongs to, by the symbol its feed is
28
+ * described with.
29
+ *
30
+ * Curated, and deliberately short: a vault not named here becomes a group of
31
+ * one under its own symbol, which is still a correct partition — one vault is
32
+ * one source — and is flagged so a reader knows nothing claims it fails
33
+ * alongside anything else.
34
+ */
35
+ const VAULT_GROUPS = {
36
+ sUSDS: "Sky",
37
+ sDAI: "Sky",
38
+ sUSDe: "Ethena",
39
+ sUSDf: "Falcon",
40
+ wstUSR: "Resolv",
41
+ syrupUSDC: "Maple",
42
+ hgETH: "Kelp",
43
+ rstETH: "Mellow",
44
+ Re7LRT: "Mellow",
45
+ pzETH: "Mellow",
46
+ DVstETH: "Mellow",
47
+ tETH: "Treehouse"
48
+ };
49
+ /**
50
+ * Rules over a Chainlink-shaped feed's own description, in order.
51
+ *
52
+ * Only reached for a leaf Gearbox did not deploy, which is where the provider
53
+ * is not already stated. The first match wins, so the specific patterns come
54
+ * before the general shape of a Chainlink pair.
55
+ */
56
+ const DESCRIPTION_RULES = [
57
+ {
58
+ via: "description names Redstone",
59
+ match: /redstone/i,
60
+ group: "Redstone"
61
+ },
62
+ {
63
+ via: "description names Pyth",
64
+ match: /\bpyth\b/i,
65
+ group: "Pyth"
66
+ },
67
+ {
68
+ via: "description names Pendle",
69
+ match: /pendle/i,
70
+ group: "Pendle"
71
+ },
72
+ {
73
+ via: "description names Chronicle",
74
+ match: /chronicle/i,
75
+ group: "Chronicle"
76
+ },
77
+ {
78
+ via: "description names API3",
79
+ match: /\bapi3\b/i,
80
+ group: "API3"
81
+ },
82
+ {
83
+ via: "description names Chaos",
84
+ match: /chaos\s*labs/i,
85
+ group: "Chaos Labs"
86
+ },
87
+ {
88
+ via: "Midas naming convention",
89
+ match: /^m(TBILL|BASIS|EDGE|F-ONE|GLOBAL|Re7YIELD|[A-Z]{2,}[0-9]*)\s*\//,
90
+ group: "Midas"
91
+ },
92
+ {
93
+ via: "exchange-rate adapter, grouped by its token",
94
+ match: /^([A-Za-z0-9+]+)\s*[/-]\s*[A-Za-z0-9+]+\s+exchange\s+rate/i,
95
+ group: (m) => m[1]
96
+ }
97
+ ];
98
+ /**
99
+ * Whether a description is a Chainlink pair: `BASE / QUOTE`, spaced, in the
100
+ * shape their aggregators have published for years.
101
+ *
102
+ * Paired with a version of 6, which is what `EACAggregatorProxy` answers and
103
+ * what the imitators — version 0 or 1, mostly — do not. Either alone would be
104
+ * wrong: plenty of adapters describe themselves as a spaced pair, and plenty
105
+ * of Chainlink feeds are read through a wrapper that reports another version.
106
+ */
107
+ const CHAINLINK_PAIR = /^[A-Za-z0-9+]+ \/ [A-Za-z0-9+]+$/;
108
+ /** Feed types that are their own answer, with no source behind them at all. */
109
+ const SELF_CONTAINED = {
110
+ CONSTANT: "Constant",
111
+ ZERO: "Zero"
112
+ };
113
+ /** The type without the `PRICE_FEED::` the bytecode repository prefixes it with. */
114
+ function bareType(contractType) {
115
+ return contractType.replace(/^PRICE_FEED::/, "");
116
+ }
117
+ /**
118
+ * Which group a leaf price feed belongs to, and how that was decided.
119
+ *
120
+ * The order is what makes it defensible: what Gearbox deployed says who it
121
+ * reads, so those types are taken first; then the description rules, most
122
+ * specific first; then the Chainlink shape; and anything left is its own group
123
+ * under its own description, flagged.
124
+ */
125
+ function classifyLeaf(node) {
126
+ const type = bareType(node.contractType);
127
+ const selfContained = SELF_CONTAINED[type];
128
+ if (selfContained) return {
129
+ group: selfContained,
130
+ identified: true,
131
+ via: "Gearbox feed type"
132
+ };
133
+ if (type === "REDSTONE" || type === "PYTH" || type === "CHAINLINK") return {
134
+ group: type === "PYTH" ? "Pyth" : type === "REDSTONE" ? "Redstone" : "Chainlink",
135
+ identified: true,
136
+ via: "Gearbox feed type"
137
+ };
138
+ for (const rule of DESCRIPTION_RULES) {
139
+ const m = node.description.match(rule.match);
140
+ if (m) return {
141
+ group: typeof rule.group === "function" ? rule.group(m) : rule.group,
142
+ identified: true,
143
+ via: rule.via
144
+ };
145
+ }
146
+ if (CHAINLINK_PAIR.test(node.description) && node.version === 6) return {
147
+ group: "Chainlink",
148
+ identified: true,
149
+ via: "Chainlink pair description at interface version 6"
150
+ };
151
+ return {
152
+ group: node.description || node.address,
153
+ identified: false,
154
+ via: "unrecognised: grouped alone under its own description"
155
+ };
156
+ }
157
+ /**
158
+ * Which group a rate-bearing feed's rate belongs to.
159
+ *
160
+ * The type names the protocol for everything but an ERC-4626 vault, where the
161
+ * protocol is the vault's own and is looked up by the symbol the feed is
162
+ * described with — `"sUSDS / USD LP price feed"` is Sky's.
163
+ */
164
+ function classifyRate(node) {
165
+ const type = bareType(node.contractType);
166
+ const named = RATE_TYPE_GROUPS[type];
167
+ if (named) return {
168
+ group: named,
169
+ identified: true,
170
+ via: "Gearbox feed type"
171
+ };
172
+ const symbol = node.description.match(/^([A-Za-z0-9+.-]+)\s*\//)?.[1];
173
+ const protocol = symbol ? VAULT_GROUPS[symbol] : void 0;
174
+ if (protocol) return {
175
+ group: protocol,
176
+ identified: true,
177
+ via: "curated vault table"
178
+ };
179
+ return {
180
+ group: symbol ?? node.address,
181
+ identified: false,
182
+ via: "vault not in the curated table: grouped alone under its symbol"
183
+ };
184
+ }
185
+ /**
186
+ * Every source behind a chain's feed trees.
187
+ *
188
+ * A source per contract, not per feed: two of an oracle's trees carrying the
189
+ * same aggregator is one source read twice, and the test varies it once.
190
+ *
191
+ * `rateTargets` says which contract each rate-bearing feed reads its rate
192
+ * from — the feed's own `lpContract`, read separately because the compressor
193
+ * does not report it. A feed missing from it is left out with a reason, since
194
+ * perturbing the whole feed instead would charge its protocol with a failure
195
+ * of whatever prices the thing the rate is denominated in.
196
+ **/
197
+ function classifySources(chainId, nodes, rateTargets, baselineOf) {
198
+ const sources = [];
199
+ const unresolved = [];
200
+ const seen = /* @__PURE__ */ new Set();
201
+ for (const node of nodes) {
202
+ const isRate = bareType(node.contractType) in RATE_TYPE_GROUPS;
203
+ if (!isRate && node.underlyingFeeds.length > 0) continue;
204
+ const target = isRate ? rateTargets.get(node.address) : node.address;
205
+ if (!target) {
206
+ unresolved.push(node);
207
+ continue;
208
+ }
209
+ if (seen.has(target)) continue;
210
+ seen.add(target);
211
+ sources.push({
212
+ chainId,
213
+ kind: isRate ? "rate" : "feed",
214
+ target,
215
+ feed: node.address,
216
+ contractType: node.contractType,
217
+ description: node.description,
218
+ perturbable: !(bareType(node.contractType) in SELF_CONTAINED),
219
+ baseline: baselineOf(target),
220
+ ...isRate ? classifyRate(node) : classifyLeaf(node)
221
+ });
222
+ }
223
+ return {
224
+ sources,
225
+ unresolved
226
+ };
227
+ }
228
+ /**
229
+ * The sources each of the given feeds is priced through, following the trees
230
+ * down from every one of them.
231
+ *
232
+ * A feed is reached by a source when the source sits anywhere beneath it, at
233
+ * any depth: a market reading a composite of a composite of a Chainlink
234
+ * aggregator is a market Chainlink can move. Memoised over the whole forest,
235
+ * so a node shared by twenty trees is walked once.
236
+ **/
237
+ function sourcesByFeed(roots, nodes, sources) {
238
+ const sourceOfFeed = /* @__PURE__ */ new Map();
239
+ for (const source of sources) sourceOfFeed.set(source.feed, source.target);
240
+ const memo = /* @__PURE__ */ new Map();
241
+ const walk = (feed, path) => {
242
+ const done = memo.get(feed);
243
+ if (done) return done;
244
+ if (path.has(feed)) return /* @__PURE__ */ new Set();
245
+ const found = /* @__PURE__ */ new Set();
246
+ const own = sourceOfFeed.get(feed);
247
+ if (own) found.add(own);
248
+ path.add(feed);
249
+ for (const child of nodes.get(feed)?.underlyingFeeds ?? []) for (const source of walk(child, path)) found.add(source);
250
+ path.delete(feed);
251
+ memo.set(feed, found);
252
+ return found;
253
+ };
254
+ return new Map(roots.map((root) => [root, walk(root, /* @__PURE__ */ new Set())]));
255
+ }
256
+ //#endregion
257
+ exports.classifySources = classifySources;
258
+ exports.sourcesByFeed = sourcesByFeed;
@@ -0,0 +1,22 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_permissionless_api_oracles_stress_classify = require("./classify.js");
3
+ const require_permissionless_api_oracles_stress_override = require("./override.js");
4
+ const require_permissionless_api_oracles_stress_score = require("./score.js");
5
+ const require_permissionless_api_oracles_stress_run = require("./run.js");
6
+ require("./types.js");
7
+ exports.DEFAULT_SCENARIOS = require_permissionless_api_oracles_stress_run.DEFAULT_SCENARIOS;
8
+ exports.RATE_FEED_TYPE = require_permissionless_api_oracles_stress_run.RATE_FEED_TYPE;
9
+ exports.UNBOUNDED_DOWN = require_permissionless_api_oracles_stress_override.UNBOUNDED_DOWN;
10
+ exports.UNBOUNDED_UP = require_permissionless_api_oracles_stress_override.UNBOUNDED_UP;
11
+ exports.classifySources = require_permissionless_api_oracles_stress_classify.classifySources;
12
+ exports.groupOverride = require_permissionless_api_oracles_stress_override.groupOverride;
13
+ exports.perturb = require_permissionless_api_oracles_stress_override.perturb;
14
+ exports.rateStub = require_permissionless_api_oracles_stress_override.rateStub;
15
+ exports.readForest = require_permissionless_api_oracles_stress_run.readForest;
16
+ exports.readRateTargets = require_permissionless_api_oracles_stress_run.readRateTargets;
17
+ exports.roundDataStub = require_permissionless_api_oracles_stress_override.roundDataStub;
18
+ exports.runStressTest = require_permissionless_api_oracles_stress_run.runStressTest;
19
+ exports.scoreGroup = require_permissionless_api_oracles_stress_score.scoreGroup;
20
+ exports.slotHarm = require_permissionless_api_oracles_stress_score.slotHarm;
21
+ exports.sourcesByFeed = require_permissionless_api_oracles_stress_classify.sourcesByFeed;
22
+ exports.verdictOf = require_permissionless_api_oracles_stress_score.verdictOf;
@@ -0,0 +1,125 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/permissionless/api/oracles/stress/override.ts
3
+ /**
4
+ * Making a contract lie for the length of one `eth_call`.
5
+ *
6
+ * No fork and no simulation harness: an `eth_call` carries a state override,
7
+ * so the aggregator's code is replaced for that call alone and everything
8
+ * above it — the composite feeds, their bounds, the oracle's own checks — runs
9
+ * as deployed, on real state, at the current block. What comes back is what
10
+ * the market would have read.
11
+ **/
12
+ /** A word, as the 32 bytes `PUSH32` wants, two's complement for negatives. */
13
+ function word(value) {
14
+ return BigInt.asUintN(256, value).toString(16).padStart(64, "0");
15
+ }
16
+ /**
17
+ * Runtime code answering every call with the same five words.
18
+ *
19
+ * Deliberately selector-blind. A price feed is read through more than
20
+ * `latestRoundData` — `decimals`, `version`, sometimes `latestAnswer` — and
21
+ * dispatching on each would mean knowing which of them every caller in a tree
22
+ * reaches for. Five words decode as the round data; a caller expecting one
23
+ * word reads the first, which is why `roundId` is set to the 8 decimals every
24
+ * Gearbox feed reports rather than to a round number.
25
+ *
26
+ * `answeredInRound` is the same 8, so the `answeredInRound >= roundId` check
27
+ * the older consumers make passes.
28
+ **/
29
+ function roundDataStub(answer, updatedAt) {
30
+ return `0x${[
31
+ `7f${word(8n)}600052`,
32
+ `7f${word(answer)}602052`,
33
+ `7f${word(updatedAt)}604052`,
34
+ `7f${word(updatedAt)}606052`,
35
+ `7f${word(8n)}608052`,
36
+ "60a06000f3"
37
+ ].join("")}`;
38
+ }
39
+ /**
40
+ * Runtime code answering every call with one word.
41
+ *
42
+ * What an exchange rate getter returns, whichever of them the feed calls:
43
+ * `stEthPerToken`, `convertToAssets`, `get_virtual_price`, `pricePerShare`
44
+ * and `getRate` all answer a single `uint256`, so the stub does not need to
45
+ * know which.
46
+ **/
47
+ function rateStub(rate) {
48
+ return `0x7f${word(rate)}60005260206000f3`;
49
+ }
50
+ /**
51
+ * The two unbounded ends, as far as the types involved can carry them.
52
+ *
53
+ * "Infinitely large" has to stop somewhere: a price is an `int256` and the
54
+ * arithmetic above it multiplies, so a genuinely enormous answer overflows
55
+ * inside the composite feed and reverts — which would be scored as the
56
+ * oracle resisting, when all that happened is that the test asked something
57
+ * the type cannot hold. `1e12` times the current answer is past any bound,
58
+ * any sanity check and any collateral ratio, and still multiplies.
59
+ */
60
+ const UNBOUNDED_UP = 0xe8d4a51000;
61
+ /** The other end: a thousandth of a trillionth, which is as near zero. */
62
+ const UNBOUNDED_DOWN = 1e-12;
63
+ /**
64
+ * A double as an exact integer ratio.
65
+ *
66
+ * Multipliers here span twenty-four orders of magnitude, so the obvious
67
+ * `BigInt(Math.round(m * 1e9))` is not available: at the top of the range the
68
+ * product is past what a double carries exactly and the multiplier quietly
69
+ * stops being the one that was asked for. Going through the decimal expansion
70
+ * keeps all fifteen digits a double has and puts the magnitude in the
71
+ * exponent, where it costs nothing.
72
+ */
73
+ function asRatio(multiplier) {
74
+ const [mantissa = "0", exponent = "0"] = multiplier.toExponential(14).split("e");
75
+ const digits = BigInt(mantissa.replace(".", ""));
76
+ const power = Number(exponent) - 14;
77
+ return power >= 0 ? {
78
+ num: digits * 10n ** BigInt(power),
79
+ den: 1n
80
+ } : {
81
+ num: digits,
82
+ den: 10n ** BigInt(-power)
83
+ };
84
+ }
85
+ /**
86
+ * What a source should answer under a multiplier, given what it answers now.
87
+ *
88
+ * Floored at one rather than at zero: a multiplier small enough to round the
89
+ * baseline away would be testing a feed reporting nothing, which is the revert
90
+ * case and is a different scenario with a different meaning.
91
+ */
92
+ function perturb(baseline, multiplier) {
93
+ const { num, den } = asRatio(Number.isFinite(multiplier) ? Math.min(Math.max(multiplier, UNBOUNDED_DOWN), UNBOUNDED_UP) : multiplier > 0 ? UNBOUNDED_UP : UNBOUNDED_DOWN);
94
+ const out = baseline * num / den;
95
+ return out === 0n ? 1n : out;
96
+ }
97
+ /**
98
+ * The state override that makes every source of a group answer a multiple of
99
+ * what it answers now.
100
+ *
101
+ * One override per contract, so a group whose sources share an aggregator
102
+ * perturbs it once. Sources with nothing behind them and sources that are not
103
+ * answering are both left out, for the reasons on
104
+ * {@link StressSource.perturbable} and {@link StressSource.baseline} — an
105
+ * empty result is a group there is nothing to simulate about.
106
+ **/
107
+ function groupOverride(sources, multiplier, updatedAt) {
108
+ const overrides = [];
109
+ for (const source of sources) {
110
+ if (!source.perturbable || source.baseline === null) continue;
111
+ const value = perturb(source.baseline, multiplier);
112
+ overrides.push({
113
+ address: source.target,
114
+ code: source.kind === "rate" ? rateStub(value) : roundDataStub(value, updatedAt)
115
+ });
116
+ }
117
+ return overrides;
118
+ }
119
+ //#endregion
120
+ exports.UNBOUNDED_DOWN = UNBOUNDED_DOWN;
121
+ exports.UNBOUNDED_UP = UNBOUNDED_UP;
122
+ exports.groupOverride = groupOverride;
123
+ exports.perturb = perturb;
124
+ exports.rateStub = rateStub;
125
+ exports.roundDataStub = roundDataStub;