Skip to content

Commit

Permalink
Merge pull request #131 from shazow/autoload-contractResult
Browse files Browse the repository at this point in the history
autoload: AutoloadResult.ContractResult
  • Loading branch information
shazow authored Oct 1, 2024
2 parents da18e52 + fe8fa90 commit 2f7efc2
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 15 deletions.
20 changes: 18 additions & 2 deletions src/__tests__/auto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
54 changes: 41 additions & 13 deletions src/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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[],

Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -149,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,
Expand All @@ -170,13 +182,16 @@ 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;
const f = await diamondProxy.facets(provider, address);
Object.assign(facets, f);

} else if (result.proxies.length > 0) {
result.followProxies = async function(selector?: string): Promise<AutoloadResult> {
// 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);
Expand All @@ -196,7 +211,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.
Expand All @@ -213,16 +228,29 @@ 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) {
// 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;
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
Expand Down
8 changes: 8 additions & 0 deletions src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
Expand Down

0 comments on commit 2f7efc2

Please sign in to comment.