From 902ab4055ac2cae6a9c3fa5589ff027518eaa45d Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 29 Sep 2024 12:29:48 -0400 Subject: [PATCH 1/4] autoload: AutoloadResult{ContractResult} and AutoloadConfig{loadContractResult} Use ABILoader.getContract instead of loadABI when loadContractResult is set. --- src/__tests__/auto.test.ts | 20 +++++++++++++++-- src/auto.ts | 45 ++++++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/__tests__/auto.test.ts b/src/__tests__/auto.test.ts index c7a231f..d8a2f73 100644 --- a/src/__tests__/auto.test.ts +++ b/src/__tests__/auto.test.ts @@ -71,11 +71,27 @@ online_test('autoload non-contract', async ({ provider, env }) => { expect(abi).toStrictEqual([]); }); -online_test('autoload verified etherscan', async ({ provider, env }) => { +online_test('autoload verified multi', async ({ provider, env }) => { const address = "0x8f8ef111b67c04eb1641f5ff19ee54cda062f163"; // Uniswap v3 pool, verified on Etherscan and Sourcify const result = await autoload(address, { provider: provider, ...whatsabi.loaders.defaultsWithEnv(env), }); - expect(result.abiLoadedFrom?.name).toBeTruthy() + expect(result.abiLoadedFrom?.name).toBeTruthy(); +}); + +online_test('autoload loadContractResult verified etherscan', async ({ provider, env }) => { + const address = "0xc3d688b66703497daa19211eedff47f25384cdc3"; // Compound USDC proxy + const result = await autoload(address, { + provider: provider, + loadContractResult: true, + followProxies: false, + abiLoader: new whatsabi.loaders.EtherscanABILoader({ apiKey: env.ETHERSCAN_API_KEY }), + }); + expect(result.abiLoadedFrom?.name).toBe("EtherscanABILoader"); + expect(result.contractResult?.ok).toBeTruthy(); + expect(result.contractResult?.name).toBe("TransparentUpgradeableProxy"); + expect(result.contractResult?.compilerVersion).toBe("v0.8.15+commit.e14f2714"); + expect(result.contractResult?.loaderResult?.Proxy).toBe("1"); + expect(result.contractResult?.loaderResult?.Implementation).toBe("0x8a807d39f1d642dd8c12fe2e249fe97847f01ba0"); }); diff --git a/src/auto.ts b/src/auto.ts index f10be5f..11f6ff1 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -3,7 +3,7 @@ import { Fragment, FunctionFragment } from "ethers"; import type { AnyProvider } from "./providers.js"; import type { ABI, ABIFunction } from "./abi.js"; import { type ProxyResolver, DiamondProxyResolver } from "./proxies.js"; -import type { ABILoader, SignatureLookup } from "./loaders.js"; +import type { ABILoader, SignatureLookup, ContractResult } from "./loaders.js"; import * as errors from "./errors.js"; import { CompatibleProvider } from "./providers.js"; @@ -28,6 +28,9 @@ export type AutoloadResult = { /** Whether the `abi` is loaded from a verified source */ abiLoadedFrom?: ABILoader; + /** Full contract metadata result, only included if {@link AutoloadConfig.loadContractResult} is true. */ + contractResult?: ContractResult; + /** List of resolveable proxies detected in the contract */ proxies: ProxyResolver[], @@ -82,6 +85,15 @@ export type AutoloadConfig = { */ followProxies?: boolean; + + /** + * Load full contract metadata result, include it in {@link AutoloadResult.ContractResult} if successful. + * + * This changes the behaviour of autoload to use {@link ABILoader.getContract} instead of {@link ABILoader.loadABI}, + * which returns a larger superset result including all of the available verified contract metadata. + */ + loadContractResult?: boolean; + /** * Enable pulling additional metadata from WhatsABI's static analysis, still unreliable * @@ -213,16 +225,27 @@ export async function autoload(address: string, config: AutoloadConfig): Promise } try { - const addresses = Object.keys(facets); - const promises = addresses.map(addr => loader.loadABI(addr)); - const results = await Promise.all(promises); - const abis = Object.fromEntries(results.map((abi, i) => { - return [addresses[i], abi]; - })); - result.abi = pruneFacets(facets, abis); - if (result.abi.length > 0) { - result.abiLoadedFrom = abiLoadedFrom; - return result; + if (config.loadContractResult) { + const contractResult = await loader.getContract(address); + if (contractResult) { + result.contractResult = contractResult; + result.abi = contractResult.abi; + result.abiLoadedFrom = contractResult.loader; + return result; + } + } else { + // Load ABIs of all available facets and merge + const addresses = Object.keys(facets); + const promises = addresses.map(addr => loader.loadABI(addr)); + const results = await Promise.all(promises); + const abis = Object.fromEntries(results.map((abi, i) => { + return [addresses[i], abi]; + })); + result.abi = pruneFacets(facets, abis); + if (result.abi.length > 0) { + result.abiLoadedFrom = abiLoadedFrom; + return result; + } } } catch (error: any) { // TODO: Catch useful errors From bb9f2c91ecca2c4733bce5f1a156abf5e9428066 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 30 Sep 2024 11:09:36 -0400 Subject: [PATCH 2/4] auto: Dont follow DiamondProxies unless followProxies is set Fixes #132 --- src/auto.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/auto.ts b/src/auto.ts index 11f6ff1..b01762e 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -161,7 +161,7 @@ export async function autoload(address: string, config: AutoloadConfig): Promise try { bytecode = await provider.getCode(address); } catch (err) { - throw new errors.AutoloadError(`Failed to fetch contract code due to provider error: ${err instanceof Error ? err.message : String(err) }`, + throw new errors.AutoloadError(`Failed to fetch contract code due to provider error: ${err instanceof Error ? err.message : String(err)}`, { context: { address }, cause: err as Error, @@ -182,13 +182,30 @@ export async function autoload(address: string, config: AutoloadConfig): Promise }; if (result.proxies.length === 1 && result.proxies[0] instanceof DiamondProxyResolver) { - onProgress("loadDiamondFacets", { address }); const diamondProxy = result.proxies[0] as DiamondProxyResolver; - const f = await diamondProxy.facets(provider, address); - Object.assign(facets, f); + if (config.followProxies) { + onProgress("loadDiamondFacets", { address }); + const f = await diamondProxy.facets(provider, address); + Object.assign(facets, f); + } else { + result.followProxies = async function(selector?: string): Promise { + if (selector) { + // Follow a single selector for DiamondProxy + onProgress("followProxies", { resolver: diamondProxy, address }); + const resolved = await diamondProxy.resolve(provider, address, selector); + if (resolved !== undefined) return await autoload(resolved, config); + } + // Resolve all facets, unfortunately this requires doing the whole thing again with followProxy + // FIXME: Can we improve this codeflow easily? + // We could override the privider with a cached one here, but might be too magical and cause surprising bugs? + return await autoload(address, Object.assign({}, config, { followProxies: true })); + }; + } } else if (result.proxies.length > 0) { result.followProxies = async function(selector?: string): Promise { + // This attempts to follow the first proxy that resolves successfully. + // FIXME: If there are multiple proxies, should we attempt to merge them somehow? for (const resolver of result.proxies) { onProgress("followProxies", { resolver: resolver, address }); const resolved = await resolver.resolve(provider, address, selector); @@ -208,7 +225,7 @@ export async function autoload(address: string, config: AutoloadConfig): Promise onProgress("abiLoader", { address, facets: Object.keys(facets) }); const loader = abiLoader; - let abiLoadedFrom; + let abiLoadedFrom = loader; let originalOnLoad; if (loader instanceof MultiABILoader) { // This is a workaround for avoiding to change the loadABI signature, we can remove it if we use getContract instead. @@ -228,6 +245,8 @@ export async function autoload(address: string, config: AutoloadConfig): Promise if (config.loadContractResult) { const contractResult = await loader.getContract(address); if (contractResult) { + // We assume that a verified contract ABI contains all of the relevant resolved proxy functions + // so we don't need to mess with resolving facets and can return immediately. result.contractResult = contractResult; result.abi = contractResult.abi; result.abiLoadedFrom = contractResult.loader; From 3dc8f5226c6febfe9bce67622b9da2938767530f Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 1 Oct 2024 13:53:26 -0400 Subject: [PATCH 3/4] src/proxies.ts: Add typedoc example for DiamondProxyResolver.facets --- src/proxies.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/proxies.ts b/src/proxies.ts index aecd3cd..fcd67cf 100644 --- a/src/proxies.ts +++ b/src/proxies.ts @@ -26,6 +26,7 @@ * if (result.proxies.length > 0) console.log("Proxies detected:", result.proxies); * // Note that some proxies can only be resolved relative to a selector, like DiamondProxy. These will need to be resolved manually via result.proxies. * ``` + * * @example * Resolve a DiamondProxy: * ```ts @@ -39,6 +40,13 @@ * const implementationAddress = await resolver.resolve(provider, address, selector); * ``` * + * @example + * Get all facets and selectors for a DiamondProxy: + * ```ts + * // Let's say we have a result with a DiamondProxy in it, from the above example + * const diamondResolver = result.proxies[0] as DiamondProxyResolver; + * const facets = await diamondResolver.facets(provider, address); // All possible address -> selector[] mappings + * ``` */ import type { StorageProvider, CallProvider } from "./providers.js"; import { addSlotOffset, readArray, joinSlot } from "./slots.js"; From fe8fa90b53b7ea789c6a85e59e6a29e145622cdb Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 1 Oct 2024 14:06:43 -0400 Subject: [PATCH 4/4] src/auto.ts: Undo respecting followProxies for DiamondProxy Re-opens #132 --- src/auto.ts | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/auto.ts b/src/auto.ts index b01762e..e2d1355 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -182,25 +182,11 @@ export async function autoload(address: string, config: AutoloadConfig): Promise }; if (result.proxies.length === 1 && result.proxies[0] instanceof DiamondProxyResolver) { + // TODO: Respect config.followProxies, see https://github.com/shazow/whatsabi/issues/132 + onProgress("loadDiamondFacets", { address }); const diamondProxy = result.proxies[0] as DiamondProxyResolver; - if (config.followProxies) { - onProgress("loadDiamondFacets", { address }); - const f = await diamondProxy.facets(provider, address); - Object.assign(facets, f); - } else { - result.followProxies = async function(selector?: string): Promise { - if (selector) { - // Follow a single selector for DiamondProxy - onProgress("followProxies", { resolver: diamondProxy, address }); - const resolved = await diamondProxy.resolve(provider, address, selector); - if (resolved !== undefined) return await autoload(resolved, config); - } - // Resolve all facets, unfortunately this requires doing the whole thing again with followProxy - // FIXME: Can we improve this codeflow easily? - // We could override the privider with a cached one here, but might be too magical and cause surprising bugs? - return await autoload(address, Object.assign({}, config, { followProxies: true })); - }; - } + const f = await diamondProxy.facets(provider, address); + Object.assign(facets, f); } else if (result.proxies.length > 0) { result.followProxies = async function(selector?: string): Promise {