Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-taxis-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aave-dao/toolbox": minor
---

add `getChainlinkFeed` to resolve a feed from its proxy or secondary proxy address
1 change: 1 addition & 0 deletions packages/toolbox/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./math/binary";
export * from "./math/slot";
export * from "./ecosystem/addresses";
export * from "./ecosystem/explorers";
export * from "./ecosystem/chainlinkFeeds";
export * from "./ecosystem/tenderly";
export * from "./ecosystem/tenderly.types";
export * from "./ecosystem/event-db";
Expand Down
64 changes: 64 additions & 0 deletions packages/toolbox/src/ecosystem/chainlinkFeeds.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { getChainlinkFeed } from "./chainlinkFeeds";
import { ChainId } from "./chainIds";

describe("ecosystem:chainlinkFeeds", () => {
it("should resolve a feed by its proxy", () => {
const feed = getChainlinkFeed({
chainId: ChainId.arc,
address: "0x84EA90AC252Dc437031461836DB5164219147905",
});
expect(feed).toMatchObject({
name: "SVR USDC / USD",
proxyAddress: "0x84EA90AC252Dc437031461836DB5164219147905",
decimals: 8,
matchedOn: "proxy",
});
});

it("should resolve a feed by its secondary proxy", () => {
// upstream moved base eth/usd into `eth-usd-shared-svr-2` as the secondary
const feed = getChainlinkFeed({
chainId: ChainId.base,
address: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
});
expect(feed).toMatchObject({
name: "SVR ETH / USD",
proxyAddress: "0x50015f8b17fb2C290Dde41fDc246ed0dcEE93a8b",
secondaryProxyAddress: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
matchedOn: "secondary",
});
});

it("should be case insensitive", () => {
const feed = getChainlinkFeed({
chainId: ChainId.mainnet,
address: "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419",
});
expect(feed?.name).toEqual("ETH / USD");
});

it("should not resolve an address across chains", () => {
expect(
getChainlinkFeed({
chainId: ChainId.polygon,
address: "0x84EA90AC252Dc437031461836DB5164219147905",
}),
).toBeUndefined();
});

it("should return undefined for unknown addresses and chains", () => {
expect(
getChainlinkFeed({
chainId: ChainId.mainnet,
address: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
}),
).toBeUndefined();
expect(
getChainlinkFeed({
chainId: 1337,
address: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
}),
).toBeUndefined();
});
});
69 changes: 69 additions & 0 deletions packages/toolbox/src/ecosystem/chainlinkFeeds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Address } from "viem";
import { chainlinkFeeds } from "./generated/chainlinkFeeds";

export type ChainlinkFeed = {
contractAddress: Address;
proxyAddress: Address;
secondaryProxyAddress?: Address;
decimals: number;
name: string;
};

export type ChainlinkFeedMatch = ChainlinkFeed & {
matchedOn: "proxy" | "secondary";
};

// upstream lists some feeds without a proxy, so the generated type is wider than it looks
type GeneratedFeed = Omit<ChainlinkFeed, "proxyAddress"> & {
proxyAddress: Address | null;
};

const indexes = new Map<number, Map<string, ChainlinkFeedMatch>>();

function getIndex(chainId: number) {
const cached = indexes.get(chainId);
if (cached) return cached;

const feeds = chainlinkFeeds[
String(chainId) as keyof typeof chainlinkFeeds
] as readonly GeneratedFeed[] | undefined;
const index = new Map<string, ChainlinkFeedMatch>();
if (feeds) {
const withProxy = feeds.filter(
(feed): feed is ChainlinkFeed => feed.proxyAddress !== null,
);
for (const feed of withProxy) {
index.set(feed.proxyAddress.toLowerCase(), {
...feed,
matchedOn: "proxy",
});
}
// a proxy of one feed can never be shadowed by the secondary of another
for (const feed of withProxy) {
if (!feed.secondaryProxyAddress) continue;
const key = feed.secondaryProxyAddress.toLowerCase();
if (index.has(key)) continue;
index.set(key, { ...feed, matchedOn: "secondary" });
}
}
indexes.set(chainId, index);
return index;
}

/**
* Resolves a feed from any address it answers to. Chainlink sometimes demotes a
* standalone feed to the `secondaryProxyAddress` of an SVR entry, which keeps
* the address live while removing it as a `proxyAddress`, so `matchedOn` tells
* the caller which of the two contracts it actually asked about.
* @param chainId Id of the chain the address lives on
* @param address Feed proxy or secondary proxy address
*/
export function getChainlinkFeed({
chainId,
address,
}: {
chainId: number;
address: Address;
}): ChainlinkFeedMatch | undefined {
return getIndex(chainId).get(address.toLowerCase());
}
Loading