@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.
- package/dist/cjs/dev/AccountOpener.js +1 -1
- package/dist/cjs/dev/withdrawalUtils.js +1 -1
- package/dist/cjs/history/trace/extractTransfers.js +1 -1
- package/dist/cjs/onchain/accounts/CreditAccountsServiceV310.js +2 -2
- package/dist/cjs/onchain/accounts/liquidations/LiquidationsService.js +1 -1
- package/dist/cjs/onchain/accounts/withdrawal-compressor/RedemptionLoggerV310Contract.js +1 -2
- package/dist/cjs/onchain/accounts/withdrawal-compressor/WithdrawalCompressorV310Contract.js +1 -2
- package/dist/cjs/onchain/accounts/withdrawal-compressor/WithdrawalCompressorV311Contract.js +1 -2
- package/dist/cjs/onchain/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.js +1 -1
- package/dist/cjs/onchain/base/TokensMeta.js +3 -3
- package/dist/cjs/onchain/core/createAddressProvider.js +1 -1
- package/dist/cjs/onchain/market/adapters/contracts/AccountMigratorAdapterContract.js +1 -1
- package/dist/cjs/onchain/market/adapters/contracts/ERC4626AdapterContract.js +1 -1
- package/dist/cjs/onchain/market/credit/CreditFacadeV310BaseContract.js +1 -1
- package/dist/cjs/onchain/market/pool/PoolV310Contract.js +1 -1
- package/dist/cjs/onchain/market/rwa/createDegenNFT.js +1 -1
- package/dist/cjs/onchain/market/zapper/IETHZapperContract.js +1 -2
- package/dist/cjs/onchain/market/zapper/ZapperContract.js +1 -1
- package/dist/cjs/onchain/preview/preview/previewPoolPositionOperation.js +2 -2
- package/dist/cjs/onchain/utils/viem/simulateWithPriceUpdates.js +1 -1
- package/dist/cjs/permissionless/api/index.js +26 -0
- package/dist/cjs/permissionless/api/oracles/PermissionlessOracles.js +191 -0
- package/dist/cjs/permissionless/api/oracles/index.js +28 -0
- package/dist/cjs/permissionless/api/oracles/schemas.js +17 -0
- package/dist/cjs/permissionless/api/oracles/score/formula.js +99 -0
- package/dist/cjs/permissionless/api/oracles/score/index.js +8 -0
- package/dist/cjs/permissionless/api/oracles/score/run.js +88 -0
- package/dist/cjs/permissionless/api/oracles/score/types.js +1 -0
- package/dist/cjs/permissionless/api/oracles/stress/classify.js +258 -0
- package/dist/cjs/permissionless/api/oracles/stress/index.js +22 -0
- package/dist/cjs/permissionless/api/oracles/stress/override.js +125 -0
- package/dist/cjs/permissionless/api/oracles/stress/run.js +432 -0
- package/dist/cjs/permissionless/api/oracles/stress/score.js +98 -0
- package/dist/cjs/permissionless/api/oracles/stress/types.js +1 -0
- package/dist/cjs/permissionless/index.js +27 -0
- package/dist/cjs/permissionless/utils/index.js +1 -0
- package/dist/cjs/permissionless/utils/price-update/get-prices.js +39 -0
- package/dist/cjs/permissionless/utils/price-update/index.js +1 -0
- package/dist/esm/permissionless/api/index.js +7 -1
- package/dist/esm/permissionless/api/oracles/PermissionlessOracles.js +193 -2
- package/dist/esm/permissionless/api/oracles/index.js +10 -1
- package/dist/esm/permissionless/api/oracles/schemas.js +16 -1
- package/dist/esm/permissionless/api/oracles/score/formula.js +96 -0
- package/dist/esm/permissionless/api/oracles/score/index.js +4 -0
- package/dist/esm/permissionless/api/oracles/score/run.js +87 -0
- package/dist/esm/permissionless/api/oracles/score/types.js +1 -0
- package/dist/esm/permissionless/api/oracles/stress/classify.js +256 -0
- package/dist/esm/permissionless/api/oracles/stress/index.js +6 -0
- package/dist/esm/permissionless/api/oracles/stress/override.js +119 -0
- package/dist/esm/permissionless/api/oracles/stress/run.js +427 -0
- package/dist/esm/permissionless/api/oracles/stress/score.js +95 -0
- package/dist/esm/permissionless/api/oracles/stress/types.js +1 -0
- package/dist/esm/permissionless/index.js +8 -2
- package/dist/esm/permissionless/utils/index.js +2 -2
- package/dist/esm/permissionless/utils/price-update/get-prices.js +39 -1
- package/dist/esm/permissionless/utils/price-update/index.js +2 -2
- package/dist/types/permissionless/api/index.d.ts +10 -2
- package/dist/types/permissionless/api/oracles/PermissionlessOracles.d.ts +115 -1
- package/dist/types/permissionless/api/oracles/index.d.ts +12 -2
- package/dist/types/permissionless/api/oracles/schemas.d.ts +47 -0
- package/dist/types/permissionless/api/oracles/score/formula.d.ts +46 -0
- package/dist/types/permissionless/api/oracles/score/index.d.ts +4 -0
- package/dist/types/permissionless/api/oracles/score/run.d.ts +21 -0
- package/dist/types/permissionless/api/oracles/score/types.d.ts +85 -0
- package/dist/types/permissionless/api/oracles/stress/classify.d.ts +57 -0
- package/dist/types/permissionless/api/oracles/stress/index.d.ts +6 -0
- package/dist/types/permissionless/api/oracles/stress/override.d.ts +59 -0
- package/dist/types/permissionless/api/oracles/stress/run.d.ts +73 -0
- package/dist/types/permissionless/api/oracles/stress/score.d.ts +57 -0
- package/dist/types/permissionless/api/oracles/stress/types.d.ts +272 -0
- package/dist/types/permissionless/api/oracles/types.d.ts +58 -0
- package/dist/types/permissionless/index.d.ts +11 -3
- package/dist/types/permissionless/utils/index.d.ts +2 -2
- package/dist/types/permissionless/utils/price-update/get-prices.d.ts +16 -0
- package/dist/types/permissionless/utils/price-update/index.d.ts +2 -2
- package/package.json +1 -1
|
@@ -32,6 +32,16 @@ const latestRoundDataAbi = [{
|
|
|
32
32
|
stateMutability: "view",
|
|
33
33
|
type: "function"
|
|
34
34
|
}];
|
|
35
|
+
const decimalsAbi = [{
|
|
36
|
+
inputs: [],
|
|
37
|
+
name: "decimals",
|
|
38
|
+
outputs: [{
|
|
39
|
+
name: "",
|
|
40
|
+
type: "uint8"
|
|
41
|
+
}],
|
|
42
|
+
stateMutability: "view",
|
|
43
|
+
type: "function"
|
|
44
|
+
}];
|
|
35
45
|
function getLatestRoundDataCall(priceFeed) {
|
|
36
46
|
return {
|
|
37
47
|
address: priceFeed,
|
|
@@ -41,6 +51,34 @@ function getLatestRoundDataCall(priceFeed) {
|
|
|
41
51
|
};
|
|
42
52
|
}
|
|
43
53
|
/**
|
|
54
|
+
* The scale each feed's answer is in, in one multicall.
|
|
55
|
+
*
|
|
56
|
+
* Part of the Chainlink interface every feed in a Gearbox tree implements,
|
|
57
|
+
* and the only way to read an answer that is not an oracle's own: a feed an
|
|
58
|
+
* oracle holds reports 8 decimals by construction, while the feeds beneath a
|
|
59
|
+
* composite are whatever was deployed — an ETH/USD aggregator at 8 and a
|
|
60
|
+
* rate leg at 18 sit under the same parent.
|
|
61
|
+
*
|
|
62
|
+
* `null` is a feed that would not answer, which a caller should treat as
|
|
63
|
+
* unknown rather than defaulting it to 8.
|
|
64
|
+
*/
|
|
65
|
+
async function getFeedDecimals({ client, priceFeeds }) {
|
|
66
|
+
if (priceFeeds.length === 0) return {};
|
|
67
|
+
const results = await client.multicall({
|
|
68
|
+
contracts: priceFeeds.map((priceFeed) => ({
|
|
69
|
+
address: priceFeed,
|
|
70
|
+
abi: decimalsAbi,
|
|
71
|
+
functionName: "decimals",
|
|
72
|
+
args: []
|
|
73
|
+
})),
|
|
74
|
+
allowFailure: true
|
|
75
|
+
});
|
|
76
|
+
return Object.fromEntries(priceFeeds.map((priceFeed, index) => {
|
|
77
|
+
const result = results[index];
|
|
78
|
+
return [priceFeed, result?.status === "success" ? Number(result.result) : null];
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
44
82
|
* What the feeds report as they stand, in one multicall.
|
|
45
83
|
*
|
|
46
84
|
* What {@link getPrices} does without the part that makes it expensive: no
|
|
@@ -107,5 +145,6 @@ async function getPrices({ client, priceFeeds, chunkSize = 10, gasLimit }) {
|
|
|
107
145
|
}), {});
|
|
108
146
|
}
|
|
109
147
|
//#endregion
|
|
148
|
+
exports.getFeedDecimals = getFeedDecimals;
|
|
110
149
|
exports.getLatestPrices = getLatestPrices;
|
|
111
150
|
exports.getPrices = getPrices;
|
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_permissionless_utils_price_update_get_prices = require("./get-prices.js");
|
|
3
3
|
const require_permissionless_utils_price_update_get_updatable_feeds = require("./get-updatable-feeds.js");
|
|
4
4
|
const require_permissionless_utils_price_update_get_price_update_tx = require("./get-price-update-tx.js");
|
|
5
|
+
exports.getFeedDecimals = require_permissionless_utils_price_update_get_prices.getFeedDecimals;
|
|
5
6
|
exports.getLatestPrices = require_permissionless_utils_price_update_get_prices.getLatestPrices;
|
|
6
7
|
exports.getPriceUpdateTx = require_permissionless_utils_price_update_get_price_update_tx.getPriceUpdateTx;
|
|
7
8
|
exports.getPrices = require_permissionless_utils_price_update_get_prices.getPrices;
|
|
@@ -6,10 +6,16 @@ import "./chains/index.js";
|
|
|
6
6
|
import { deriveEffectiveStalenessPeriod } from "./deploy/effective-staleness.js";
|
|
7
7
|
import { DEFAULT_DEPLOY_SALT, PermissionlessDeploy, priceFeedSalt } from "./deploy/PermissionlessDeploy.js";
|
|
8
8
|
import "./deploy/index.js";
|
|
9
|
+
import { classifySources, sourcesByFeed } from "./oracles/stress/classify.js";
|
|
10
|
+
import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride, perturb, rateStub, roundDataStub } from "./oracles/stress/override.js";
|
|
11
|
+
import { scoreGroup, slotHarm, verdictOf } from "./oracles/stress/score.js";
|
|
12
|
+
import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, runStressTest } from "./oracles/stress/run.js";
|
|
13
|
+
import { BOUND_WEIGHT, INDEPENDENCE_WEIGHT, scoreMarket } from "./oracles/score/formula.js";
|
|
14
|
+
import { runAssetScores } from "./oracles/score/run.js";
|
|
9
15
|
import { GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, PermissionlessOracles } from "./oracles/PermissionlessOracles.js";
|
|
10
16
|
import { InstanceOwnerTransactions } from "./transactions/InstanceOwnerTransactions.js";
|
|
11
17
|
import { GearboxPermissionless } from "./GearboxPermissionless.js";
|
|
12
18
|
import "./oracles/index.js";
|
|
13
19
|
import "./transactions/index.js";
|
|
14
20
|
import "./types.js";
|
|
15
|
-
export { ChainsNamespace, DEFAULT_DEPLOY_SALT, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GearboxPermissionless, InstanceOwnerTransactions, PermissionlessAnalytics, PermissionlessChainUnreachableError, PermissionlessDeploy, PermissionlessNotAttachedError, PermissionlessOracles, PriceFeedNotInStoreError, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError, deriveEffectiveStalenessPeriod, priceFeedSalt };
|
|
21
|
+
export { BOUND_WEIGHT, ChainsNamespace, DEFAULT_DEPLOY_SALT, DEFAULT_SCENARIOS, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GearboxPermissionless, INDEPENDENCE_WEIGHT, InstanceOwnerTransactions, PermissionlessAnalytics, PermissionlessChainUnreachableError, PermissionlessDeploy, PermissionlessNotAttachedError, PermissionlessOracles, PriceFeedNotInStoreError, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError, RATE_FEED_TYPE, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, deriveEffectiveStalenessPeriod, groupOverride, perturb, priceFeedSalt, rateStub, readForest, readRateTargets, roundDataStub, runAssetScores, runStressTest, scoreGroup, scoreMarket, slotHarm, sourcesByFeed, verdictOf };
|
|
@@ -6,9 +6,11 @@ import "../../../onchain/index.js";
|
|
|
6
6
|
import { AbstractOffchainNamespace } from "../../../offchain/AbstractOffchainNamespace.js";
|
|
7
7
|
import "../../deployment/addresses.js";
|
|
8
8
|
import { AddressProviderContract } from "../../bindings/address-provider.js";
|
|
9
|
-
import { getLatestPrices, getPrices } from "../../utils/price-update/get-prices.js";
|
|
9
|
+
import { getFeedDecimals, getLatestPrices, getPrices } from "../../utils/price-update/get-prices.js";
|
|
10
10
|
import { byCentrality, feedKey, pricesByFeed, toCap, totalQuotas } from "./enrich.js";
|
|
11
|
-
import { assetDetailsSchema, assetSummariesSchema, priceFeedDetailsSchema, priceFeedStoresSchema } from "./schemas.js";
|
|
11
|
+
import { assetDetailsSchema, assetSummariesSchema, marketSlotsSchema, priceFeedDetailsSchema, priceFeedStoresSchema } from "./schemas.js";
|
|
12
|
+
import { runStressTest } from "./stress/run.js";
|
|
13
|
+
import { runAssetScores } from "./score/run.js";
|
|
12
14
|
import { erc20Abi } from "viem";
|
|
13
15
|
//#region src/permissionless/api/oracles/PermissionlessOracles.ts
|
|
14
16
|
const GET_PRICES_GAS_LIMIT_BY_CHAIN_ID = { 143: 100000000n };
|
|
@@ -93,6 +95,31 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
/**
|
|
98
|
+
* Every market slot in force, with the feed holding it and what the market
|
|
99
|
+
* has riding on it.
|
|
100
|
+
*
|
|
101
|
+
* The chain's whole slot table, which {@link feed} and {@link asset} answer
|
|
102
|
+
* one subject's slice of. What reads it this way is analysis over every
|
|
103
|
+
* market at once — which feed holds which slot, and what breaks if it
|
|
104
|
+
* lies — and asking a feed at a time would be a few hundred requests for
|
|
105
|
+
* the same rows.
|
|
106
|
+
*
|
|
107
|
+
* Served as indexed, with nothing folded in: the quotas are in each
|
|
108
|
+
* market's own underlying, as everywhere else, and pricing them is
|
|
109
|
+
* {@link getPrices} over the `underlyingPriceFeed` column.
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* const slots = await permissionless.oracles.slots({ chainId: 1 });
|
|
113
|
+
* ```
|
|
114
|
+
**/
|
|
115
|
+
async slots({ chainId } = {}) {
|
|
116
|
+
return this.getData({
|
|
117
|
+
path: "/pfs/slots",
|
|
118
|
+
query: { chainId: chainId?.toString() },
|
|
119
|
+
schema: marketSlotsSchema
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
96
123
|
* One feed of the store: what it is, which assets it may price, and which
|
|
97
124
|
* market slots it currently fills.
|
|
98
125
|
*
|
|
@@ -289,6 +316,111 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
289
316
|
}).sort(byCentrality);
|
|
290
317
|
}
|
|
291
318
|
/**
|
|
319
|
+
* What each source of price information can do to the chain's markets if it
|
|
320
|
+
* lies.
|
|
321
|
+
*
|
|
322
|
+
* The feeds under a chain's oracles are partitioned by where their numbers
|
|
323
|
+
* actually come from — Chainlink, Redstone, Pyth, Lido, Curve, each vault's
|
|
324
|
+
* own issuer — on the grounds that a source fails as a whole: whoever
|
|
325
|
+
* trusts one Chainlink aggregator trusts the operator behind all of them.
|
|
326
|
+
* Each group is then made to report a multiple of what it reports now, and
|
|
327
|
+
* every feed the markets read is read again inside that same call.
|
|
328
|
+
*
|
|
329
|
+
* No fork and no harness. The lie is a state override on an `eth_call`, so
|
|
330
|
+
* the composite feeds above it, their bounds and the oracle's own checks all
|
|
331
|
+
* run as deployed, against real state, at the current block — what comes
|
|
332
|
+
* back is what the market would have read.
|
|
333
|
+
*
|
|
334
|
+
* Each slot is scored by how its collateral moved *against its market's own
|
|
335
|
+
* underlying*, because that is what an account is valued by: a group that
|
|
336
|
+
* moves both legs has moved nothing. Beyond the two unbounded ends, each
|
|
337
|
+
* group is bisected for the multiple at which something first breaks, which
|
|
338
|
+
* is the figure that separates a source held in check by a bound from one
|
|
339
|
+
* nothing is checking.
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* const report = await permissionless.oracles.stressTest({ chainId: 1 });
|
|
343
|
+
* for (const group of report.groups) {
|
|
344
|
+
* console.log(group.verdict);
|
|
345
|
+
* }
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* Report {@link StressReport.unidentifiedUsd} alongside anything taken from
|
|
349
|
+
* this: it is the quota priced through a source the partition could only
|
|
350
|
+
* name after its own description, and nothing says what that fails with.
|
|
351
|
+
*
|
|
352
|
+
* Reads the chain directly, so the client has to have attached — see
|
|
353
|
+
* {@link GearboxPermissionless.attach}. Costs one request for the slots and
|
|
354
|
+
* then, per group, one `eth_call` per scenario plus the bisection's; a whole
|
|
355
|
+
* chain comes to a minute or so, and `thresholds: false` cuts it to seconds.
|
|
356
|
+
*
|
|
357
|
+
* @throws When the client has not attached, when the chain has no client
|
|
358
|
+
* configured, or when no PriceFeedCompressor is deployed on it — none of the
|
|
359
|
+
* three leaves anything to simulate.
|
|
360
|
+
**/
|
|
361
|
+
/**
|
|
362
|
+
* Every asset's score out of 10 for how its markets guard its price.
|
|
363
|
+
*
|
|
364
|
+
* A market scores 10 when its reserve feed is a second feed on other
|
|
365
|
+
* sources than the main one, and one of the two is bounded — a
|
|
366
|
+
* `BoundedPriceFeed` or a Gearbox LP feed. It loses 6 points as the two
|
|
367
|
+
* feeds' sources overlap — all of them if there is no reserve, it is a
|
|
368
|
+
* `ZeroPriceFeed`, or it is the main feed again — and 5 if neither is
|
|
369
|
+
* bounded. An asset scores its worst running market that takes it as
|
|
370
|
+
* collateral.
|
|
371
|
+
*
|
|
372
|
+
* Read off the chain, one multicall of feed trees per chain, so the score
|
|
373
|
+
* is of the trees as they stand rather than as last indexed. A chain with
|
|
374
|
+
* no client configured is left out with a warning rather than failing the
|
|
375
|
+
* rest.
|
|
376
|
+
*
|
|
377
|
+
* ```ts
|
|
378
|
+
* const scores = await permissionless.oracles.assetScores({ chainId: 1 });
|
|
379
|
+
* ```
|
|
380
|
+
*
|
|
381
|
+
* @throws {PermissionlessNotAttachedError} before
|
|
382
|
+
* {@link GearboxPermissionless.attach} has resolved.
|
|
383
|
+
**/
|
|
384
|
+
async assetScores({ chainId } = {}) {
|
|
385
|
+
const slots = await this.slots({ chainId });
|
|
386
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const slot of slots) byChain.set(slot.chainId, [...byChain.get(slot.chainId) ?? [], slot]);
|
|
388
|
+
return (await Promise.all([...byChain].map(async ([id, chainSlots]) => {
|
|
389
|
+
let client;
|
|
390
|
+
try {
|
|
391
|
+
client = this.#chains.client(id);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
this.logger?.warn(`chain ${id} has no client, leaving its assets unscored: ${describe(e)}`);
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
398
|
+
return await runAssetScores({
|
|
399
|
+
client,
|
|
400
|
+
chainId: id,
|
|
401
|
+
slots: chainSlots,
|
|
402
|
+
compressor,
|
|
403
|
+
logger: this.logger ?? void 0
|
|
404
|
+
});
|
|
405
|
+
} catch (e) {
|
|
406
|
+
this.logger?.warn(`chain ${id} would not report its feed trees, leaving its assets unscored: ${describe(e)}`);
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
}))).flat();
|
|
410
|
+
}
|
|
411
|
+
async stressTest({ chainId, ...options }) {
|
|
412
|
+
const client = this.#chains.client(chainId);
|
|
413
|
+
const compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
414
|
+
return runStressTest({
|
|
415
|
+
client,
|
|
416
|
+
chainId,
|
|
417
|
+
compressor,
|
|
418
|
+
slots: await this.slots({ chainId }),
|
|
419
|
+
logger: this.logger ?? void 0,
|
|
420
|
+
...options
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
292
424
|
* The feeds one feed reads from, recursively.
|
|
293
425
|
*
|
|
294
426
|
* Answers for any feed the chain has indexed, store member or not: a
|
|
@@ -353,6 +485,65 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
353
485
|
price: prices.get(chainId)?.[address.toLowerCase()] ?? null
|
|
354
486
|
}));
|
|
355
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* The scale each of the named feeds answers in.
|
|
490
|
+
*
|
|
491
|
+
* The companion to {@link getPrices} for the callers whose feeds are not
|
|
492
|
+
* all an oracle's: everything an oracle holds reports 8 decimals, but the
|
|
493
|
+
* feeds under a composite are whatever was deployed, and a rate leg at 18
|
|
494
|
+
* read as 8 is wrong by ten orders of magnitude rather than visibly wrong.
|
|
495
|
+
*
|
|
496
|
+
* Separate from the price because the two keep different company: this is
|
|
497
|
+
* fixed for the life of the contract and worth caching for the session,
|
|
498
|
+
* while a price is re-read whenever the reader asks. One multicall per
|
|
499
|
+
* chain either way.
|
|
500
|
+
*
|
|
501
|
+
* `null` is a feed that would not answer, or one on a chain with no client
|
|
502
|
+
* configured.
|
|
503
|
+
*
|
|
504
|
+
* ```ts
|
|
505
|
+
* const [{ decimals }] = await permissionless.oracles.getDecimals([
|
|
506
|
+
* { chainId: 1, address: feed },
|
|
507
|
+
* ]);
|
|
508
|
+
* ```
|
|
509
|
+
*
|
|
510
|
+
* @returns One entry per feed asked for, in the order asked.
|
|
511
|
+
**/
|
|
512
|
+
async getDecimals(feeds) {
|
|
513
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const { chainId, address } of feeds) {
|
|
515
|
+
const addresses = byChain.get(chainId) ?? /* @__PURE__ */ new Set();
|
|
516
|
+
addresses.add(address.toLowerCase());
|
|
517
|
+
byChain.set(chainId, addresses);
|
|
518
|
+
}
|
|
519
|
+
const chainIds = [...byChain.keys()];
|
|
520
|
+
const results = await Promise.all(chainIds.map((chainId) => this.#decimalsOf(chainId, [...byChain.get(chainId) ?? []])));
|
|
521
|
+
const decimals = new Map(chainIds.map((id, i) => [id, results[i] ?? {}]));
|
|
522
|
+
return feeds.map(({ chainId, address }) => ({
|
|
523
|
+
chainId,
|
|
524
|
+
address,
|
|
525
|
+
decimals: decimals.get(chainId)?.[address.toLowerCase()] ?? null
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
/** One chain's half of {@link getDecimals}. */
|
|
529
|
+
async #decimalsOf(chainId, priceFeeds) {
|
|
530
|
+
let client;
|
|
531
|
+
try {
|
|
532
|
+
client = this.#chains.client(chainId);
|
|
533
|
+
} catch (e) {
|
|
534
|
+
this.logger?.warn(`chain ${chainId} has no client, reporting its ${priceFeeds.length} feed(s) without a scale: ${describe(e)}`);
|
|
535
|
+
return {};
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
return await getFeedDecimals({
|
|
539
|
+
client,
|
|
540
|
+
priceFeeds
|
|
541
|
+
});
|
|
542
|
+
} catch (e) {
|
|
543
|
+
this.logger?.warn(`chain ${chainId} would not answer decimals for its ${priceFeeds.length} feed(s): ${describe(e)}`);
|
|
544
|
+
return {};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
356
547
|
/** One chain's half of {@link getPrices}: the cheap pass, then the rest. */
|
|
357
548
|
async #pricesOf(chainId, priceFeeds) {
|
|
358
549
|
let client;
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { classifySources, sourcesByFeed } from "./stress/classify.js";
|
|
2
|
+
import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride, perturb, rateStub, roundDataStub } from "./stress/override.js";
|
|
3
|
+
import { scoreGroup, slotHarm, verdictOf } from "./stress/score.js";
|
|
4
|
+
import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, runStressTest } from "./stress/run.js";
|
|
5
|
+
import { BOUND_WEIGHT, INDEPENDENCE_WEIGHT, scoreMarket } from "./score/formula.js";
|
|
6
|
+
import { runAssetScores } from "./score/run.js";
|
|
7
|
+
import { GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, PermissionlessOracles } from "./PermissionlessOracles.js";
|
|
8
|
+
import "./score/index.js";
|
|
9
|
+
import "./stress/index.js";
|
|
2
10
|
import "./types.js";
|
|
11
|
+
export { BOUND_WEIGHT, DEFAULT_SCENARIOS, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, INDEPENDENCE_WEIGHT, PermissionlessOracles, RATE_FEED_TYPE, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, groupOverride, perturb, rateStub, readForest, readRateTargets, roundDataStub, runAssetScores, runStressTest, scoreGroup, scoreMarket, slotHarm, sourcesByFeed, verdictOf };
|
|
@@ -75,6 +75,21 @@ const priceFeedMarketSchema = marketSchema.extend({
|
|
|
75
75
|
reserve: z.boolean(),
|
|
76
76
|
priceOracle: addressSchema
|
|
77
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* {@link PermissionlessMarketSlot} — the same slot read from the chain's end,
|
|
80
|
+
* so the feed is a column rather than what was asked for.
|
|
81
|
+
**/
|
|
82
|
+
const marketSlotSchema = priceFeedMarketSchema.extend({
|
|
83
|
+
chainId: z.number(),
|
|
84
|
+
underlyingToken: addressSchema,
|
|
85
|
+
isShutdown: z.boolean(),
|
|
86
|
+
priceFeed: addressSchema,
|
|
87
|
+
priceFeedName: z.string().nullable(),
|
|
88
|
+
priceFeedInStore: z.boolean(),
|
|
89
|
+
underlyingPriceFeed: addressSchema.nullable()
|
|
90
|
+
});
|
|
91
|
+
/** Every slot in scope, across whichever chains the read covered. */
|
|
92
|
+
const marketSlotsSchema = z.array(marketSlotSchema);
|
|
78
93
|
/** {@link PermissionlessIndexedPriceFeedDetails} */
|
|
79
94
|
const priceFeedDetailsSchema = z.looseObject({
|
|
80
95
|
chainId: z.number(),
|
|
@@ -121,4 +136,4 @@ const assetSummarySchema = z.looseObject({
|
|
|
121
136
|
/** The list of {@link PermissionlessAssetSummary}, already ordered. */
|
|
122
137
|
const assetSummariesSchema = z.array(assetSummarySchema);
|
|
123
138
|
//#endregion
|
|
124
|
-
export { assetDetailsSchema, assetMarketFeedSchema, assetMarketSchema, assetQuotaSchema, assetSchema, assetSummariesSchema, assetSummarySchema, priceFeedAssetSchema, priceFeedDetailsSchema, priceFeedMarketSchema, priceFeedSchema, priceFeedStoreSchema, priceFeedStoresSchema };
|
|
139
|
+
export { assetDetailsSchema, assetMarketFeedSchema, assetMarketSchema, assetQuotaSchema, assetSchema, assetSummariesSchema, assetSummarySchema, marketSlotSchema, marketSlotsSchema, priceFeedAssetSchema, priceFeedDetailsSchema, priceFeedMarketSchema, priceFeedSchema, priceFeedStoreSchema, priceFeedStoresSchema };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//#region src/permissionless/api/oracles/score/formula.ts
|
|
2
|
+
/**
|
|
3
|
+
* How well a market guards an asset's price, from its two feeds alone.
|
|
4
|
+
*
|
|
5
|
+
* Two questions, and the points are lost for each separately:
|
|
6
|
+
*
|
|
7
|
+
* - Would the reserve disagree with a main feed that has gone wrong? Only if
|
|
8
|
+
* it is a second feed on other sources. A missing reserve, a
|
|
9
|
+
* `ZeroPriceFeed`, the main feed again, and a different feed on the same
|
|
10
|
+
* sources all answer no.
|
|
11
|
+
* - Is either feed bounded? A `BoundedPriceFeed` caps the answer from above,
|
|
12
|
+
* and a Gearbox LP feed clamps its exchange rate to a window.
|
|
13
|
+
*
|
|
14
|
+
* Independence costs more than the bound, 6 points against 5, because it is
|
|
15
|
+
* the broader protection: a bounded feed caps only from above, and an LP
|
|
16
|
+
* feed's window holds only its rate, not the price of what the rate is in.
|
|
17
|
+
* Either failure alone lands a market at 4 or 5; both together at 0.
|
|
18
|
+
*/
|
|
19
|
+
const INDEPENDENCE_WEIGHT = 6;
|
|
20
|
+
const BOUND_WEIGHT = 5;
|
|
21
|
+
/**
|
|
22
|
+
* The feed types that bound what they answer: the `BoundedPriceFeed`, and
|
|
23
|
+
* every Gearbox LP feed, all of which clamp the rate they read.
|
|
24
|
+
*/
|
|
25
|
+
const BOUNDED_TYPE = /::(BOUNDED|ERC4626|CURVE_\w+|WSTETH|YEARN|MELLOW\w*|BALANCER\w*)$/;
|
|
26
|
+
const ZERO_TYPE = "PRICE_FEED::ZERO";
|
|
27
|
+
/**
|
|
28
|
+
* The share of the two sides' sources that are not shared, by key.
|
|
29
|
+
*
|
|
30
|
+
* Nothing on either side — two hardcoded prices — is 0, because a reserve is
|
|
31
|
+
* there to be a second opinion and neither of them is one.
|
|
32
|
+
*/
|
|
33
|
+
function independenceOf(main, reserve) {
|
|
34
|
+
const a = new Set(main.map((source) => source.key));
|
|
35
|
+
const b = new Set(reserve.map((source) => source.key));
|
|
36
|
+
const union = /* @__PURE__ */ new Set([...a, ...b]);
|
|
37
|
+
if (union.size === 0) return 0;
|
|
38
|
+
return 1 - [...a].filter((key) => b.has(key)).length / union.size;
|
|
39
|
+
}
|
|
40
|
+
/** Down to the half point, so that a score is never rounded up into a band. */
|
|
41
|
+
function toHalf(value) {
|
|
42
|
+
return Math.floor(value * 2 + 1e-9) / 2;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One market's score for one asset.
|
|
46
|
+
*
|
|
47
|
+
* `reserve` is `null` where the market holds no reserve for the asset at all.
|
|
48
|
+
* A source is passed per side and keyed by the caller, which is where it is
|
|
49
|
+
* decided that an unnamed source is its own contract and a named one is its
|
|
50
|
+
* group.
|
|
51
|
+
*/
|
|
52
|
+
function scoreMarket(market, main, reserve) {
|
|
53
|
+
const reasons = [];
|
|
54
|
+
let hasReserve = true;
|
|
55
|
+
if (reserve === null) {
|
|
56
|
+
hasReserve = false;
|
|
57
|
+
reasons.push("no reserve feed");
|
|
58
|
+
} else if (reserve.feed.contractType === ZERO_TYPE) {
|
|
59
|
+
hasReserve = false;
|
|
60
|
+
reasons.push("reserve is a ZeroPriceFeed");
|
|
61
|
+
} else if (reserve.feed.feed.toLowerCase() === main.feed.feed.toLowerCase()) {
|
|
62
|
+
hasReserve = false;
|
|
63
|
+
reasons.push("reserve is the main feed");
|
|
64
|
+
}
|
|
65
|
+
const reserveSources = hasReserve && reserve ? reserve.sources : [];
|
|
66
|
+
const independence = hasReserve ? independenceOf(main.sources, reserveSources) : 0;
|
|
67
|
+
const reserveKeys = new Set(reserveSources.map((source) => source.key));
|
|
68
|
+
const mainKeys = new Set(main.sources.map((source) => source.key));
|
|
69
|
+
const shared = main.sources.filter((source) => reserveKeys.has(source.key));
|
|
70
|
+
const names = (sources) => [...new Set(sources.map((source) => source.name))];
|
|
71
|
+
if (hasReserve && independence < 1) reasons.push(shared.length > 0 ? `main and reserve share ${names(shared).join(", ")}` : "neither feed reads a price source");
|
|
72
|
+
const unverified = hasReserve && [...main.sources.filter((source) => !reserveKeys.has(source.key)), ...reserveSources.filter((source) => !mainKeys.has(source.key))].some((source) => !source.identified);
|
|
73
|
+
const bounded = BOUNDED_TYPE.test(main.feed.contractType) || reserve !== null && BOUNDED_TYPE.test(reserve.feed.contractType);
|
|
74
|
+
if (!bounded) reasons.push("neither feed is bounded");
|
|
75
|
+
const score = toHalf(Math.max(0, 10 - 6 * (1 - independence) - 5 * (bounded ? 0 : 1)));
|
|
76
|
+
return {
|
|
77
|
+
...market,
|
|
78
|
+
main: {
|
|
79
|
+
...main.feed,
|
|
80
|
+
sources: names(main.sources)
|
|
81
|
+
},
|
|
82
|
+
reserve: reserve ? {
|
|
83
|
+
...reserve.feed,
|
|
84
|
+
sources: names(reserve.sources)
|
|
85
|
+
} : null,
|
|
86
|
+
hasReserve,
|
|
87
|
+
independence,
|
|
88
|
+
bounded,
|
|
89
|
+
sharedSources: names(shared),
|
|
90
|
+
unverified,
|
|
91
|
+
score,
|
|
92
|
+
reasons
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { BOUND_WEIGHT, INDEPENDENCE_WEIGHT, scoreMarket };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { classifySources, sourcesByFeed } from "../stress/classify.js";
|
|
2
|
+
import { RATE_FEED_TYPE, readForest, readRateTargets } from "../stress/run.js";
|
|
3
|
+
import { scoreMarket } from "./formula.js";
|
|
4
|
+
//#region src/permissionless/api/oracles/score/run.ts
|
|
5
|
+
/**
|
|
6
|
+
* How a source is told apart from another when two feeds are compared.
|
|
7
|
+
*
|
|
8
|
+
* A named source by its group, since two Chainlink aggregators are one
|
|
9
|
+
* provider failing together. An unnamed one by its contract, since nothing
|
|
10
|
+
* says what else it fails with — so the same contract on both sides is
|
|
11
|
+
* shared, and two different ones are not.
|
|
12
|
+
*/
|
|
13
|
+
function toScoreSource(source) {
|
|
14
|
+
return {
|
|
15
|
+
key: source.identified ? `group:${source.group}` : `contract:${source.target}`,
|
|
16
|
+
name: source.group,
|
|
17
|
+
identified: source.identified
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Every asset of one chain, scored by how its running markets price it.
|
|
22
|
+
*
|
|
23
|
+
* One read of every oracle's feed tree and one of the LP feeds' rate
|
|
24
|
+
* contracts, both multicalls: the score is a reading of the trees and needs
|
|
25
|
+
* no simulation.
|
|
26
|
+
**/
|
|
27
|
+
async function runAssetScores({ client, chainId, slots, compressor, logger }) {
|
|
28
|
+
const running = slots.filter((slot) => !slot.isShutdown);
|
|
29
|
+
const oracles = [...new Set(running.map((slot) => slot.priceOracle.toLowerCase()))];
|
|
30
|
+
const { nodes, unreadableOracles } = await readForest(client, compressor, oracles);
|
|
31
|
+
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`);
|
|
32
|
+
const { targets } = await readRateTargets(client, [...nodes.values()].filter((node) => RATE_FEED_TYPE.test(node.contractType)).map((node) => node.address));
|
|
33
|
+
const { sources } = classifySources(chainId, [...nodes.values()], targets, () => null);
|
|
34
|
+
const byTarget = new Map(sources.filter((source) => source.perturbable).map((source) => [source.target, toScoreSource(source)]));
|
|
35
|
+
const feedOf = (slot) => slot.priceFeed.toLowerCase();
|
|
36
|
+
const reach = sourcesByFeed([...new Set(running.map(feedOf))], nodes, sources);
|
|
37
|
+
const sourcesOf = (feed) => [...reach.get(feed) ?? []].map((target) => byTarget.get(target)).filter((source) => source !== void 0);
|
|
38
|
+
const positions = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const slot of running) {
|
|
40
|
+
const key = `${slot.market.toLowerCase()}:${slot.token.toLowerCase()}`;
|
|
41
|
+
const position = positions.get(key) ?? {};
|
|
42
|
+
position[slot.reserve ? "reserve" : "main"] = slot;
|
|
43
|
+
positions.set(key, position);
|
|
44
|
+
}
|
|
45
|
+
const byAsset = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const { main, reserve } of positions.values()) {
|
|
47
|
+
if (!main || main.isUnderlying) continue;
|
|
48
|
+
const asset = main.token.toLowerCase();
|
|
49
|
+
const entry = byAsset.get(asset) ?? {
|
|
50
|
+
symbol: main.tokenSymbol,
|
|
51
|
+
markets: []
|
|
52
|
+
};
|
|
53
|
+
byAsset.set(asset, entry);
|
|
54
|
+
const mainNode = nodes.get(feedOf(main));
|
|
55
|
+
if (!mainNode) continue;
|
|
56
|
+
if (mainNode.contractType === "PRICE_FEED::ZERO") continue;
|
|
57
|
+
const side = (slot) => ({
|
|
58
|
+
feed: {
|
|
59
|
+
feed: feedOf(slot),
|
|
60
|
+
name: slot.priceFeedName,
|
|
61
|
+
contractType: nodes.get(feedOf(slot))?.contractType ?? ""
|
|
62
|
+
},
|
|
63
|
+
sources: sourcesOf(feedOf(slot))
|
|
64
|
+
});
|
|
65
|
+
entry.markets.push(scoreMarket({
|
|
66
|
+
market: main.market,
|
|
67
|
+
marketName: main.marketName,
|
|
68
|
+
marketConfigurator: main.marketConfigurator,
|
|
69
|
+
curatorName: main.curatorName
|
|
70
|
+
}, side(main), reserve ? side(reserve) : null));
|
|
71
|
+
}
|
|
72
|
+
return [...byAsset].map(([asset, { symbol, markets }]) => {
|
|
73
|
+
const sorted = markets.sort((a, b) => a.score - b.score);
|
|
74
|
+
const worst = sorted[0] ?? null;
|
|
75
|
+
return {
|
|
76
|
+
chainId,
|
|
77
|
+
asset,
|
|
78
|
+
symbol,
|
|
79
|
+
score: worst?.score ?? null,
|
|
80
|
+
unverified: sorted.some((market) => market.unverified),
|
|
81
|
+
worst,
|
|
82
|
+
markets: sorted
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { runAssetScores };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|