Repository: shazow/whatsabi
Pull request #144 (2024-10-28 18:07:12)

autoload: Add AutoloadResult.isFactory
Adds `AutoloadResult.isFactory` property for when a CREATE/CREATE2 opcode is present.

Will be useful for end-users to distinguish some proxy scenarios. (e.g. #142)

## TODO
- [x] Add tests


Patch:
diff --git a/src/auto.ts b/src/auto.ts
index 578aea1..33196ae 100644
--- a/src/auto.ts
+++ b/src/auto.ts
@@ -40,6 +40,15 @@ export type AutoloadResult = {
      * Note: Some proxies operate relative to a specific selector (such as DiamondProxy facets), in this case we'll need to specify a selector that we care about.
      */
     followProxies?: (selector?: string) => Promise<AutoloadResult>,
+
+    /**
+     * Set to true when a CREATE or CREATE2 opcode is present in the bytecode.
+     * This means that some of the results could have been mistaken from the embedded contract that gets created by the factory.
+     * For example, we can have a non-proxy contract which creates a proxy contract on call. WhatsABI may not yet be able to distinguish them reliably.
+     *
+     * @experimental
+     */
+    isFactory?: boolean;
 }
 
 
@@ -87,11 +96,11 @@ export type AutoloadConfig = {
 
 
     /**
-    * 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.
-    */
+     * 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;
 
     /**
@@ -99,6 +108,7 @@ export type AutoloadConfig = {
      * For now, this is primarily for event topics.
      *
      * @group Settings
+     * @experimental
      */
     enableExperimentalMetadata?: boolean;
 }
@@ -184,8 +194,8 @@ export async function autoload(address: string, config: AutoloadConfig): Promise
 
     const program = disasm(bytecode);
 
-    // FIXME: Sort them in some reasonable way
-    result.proxies = program.proxies;
+    result.proxies = program.proxies; // FIXME: Sort them in some reasonable way
+    result.isFactory = program.isFactory;
 
     // Mapping of address-to-valid-selectors. Non-empty mapping values will prune ABIs to the selectors before returning.
     // This is mainly to support multiple proxies and diamond proxies.
diff --git a/src/disasm.ts b/src/disasm.ts
index bd2269b..9ea9c6a 100644
--- a/src/disasm.ts
+++ b/src/disasm.ts
@@ -124,6 +124,8 @@ const interestingOpCodes : Set<OpCode> = new Set([
     opcodes.SLOAD, // Not pure
     opcodes.SSTORE, // Not view
     opcodes.REVERT,
+    opcodes.CREATE, // Factory
+    opcodes.CREATE2, // Factory
     // TODO: Add LOGs to track event emitters?
 ]);
 
@@ -151,6 +153,7 @@ export class Program {
     eventCandidates: Array<string>; // PUSH32 found before a LOG instruction
     proxySlots: Array<string>; // PUSH32 found that match known proxy slots
     proxies: Array<ProxyResolver>;
+    isFactory: boolean; // CREATE or CREATE2 detected
 
     init?: Program; // Program embedded as init code
 
@@ -161,6 +164,7 @@ export class Program {
         this.eventCandidates = [];
         this.proxySlots = [];
         this.proxies = [];
+        this.isFactory = false;
         this.init = init;
     }
 }
@@ -349,6 +353,11 @@ export function disasm(bytecode: string, config?: {onlyJumpTable: boolean}): Pro
             // Tag current function with interesting opcodes (not including above)
             if (interestingOpCodes.has(inst)) {
                 currentFunction.opTags.add(inst);
+
+                // CREATE/CREATE2 are part of interestingOpCodes so we can leverage this short circuit
+                if (inst === opcodes.CREATE || inst === opcodes.CREATE2) {
+                    p.isFactory = true;
+                }
             }
         }
 


Test patch:
diff --git a/src/__tests__/auto.test.ts b/src/__tests__/auto.test.ts
index a7f9b1a..8827c00 100644
--- a/src/__tests__/auto.test.ts
+++ b/src/__tests__/auto.test.ts
@@ -3,7 +3,7 @@ import { expect } from 'vitest';
 import { whatsabi } from "../index";
 import { autoload } from "../auto";
 
-import { test, online_test, makeProvider } from "./env";
+import { test, online_test, cached_test, makeProvider } from "./env";
 
 const TIMEOUT = 15000;
 
@@ -95,3 +95,20 @@ online_test('autoload loadContractResult verified etherscan', async ({ provider,
     expect(result.contractResult?.loaderResult?.Proxy).toBe("1");
     expect(result.contractResult?.loaderResult?.Implementation).toMatch(/^0x[0-9a-f]{40}$/);
 });
+
+cached_test('autoload isFactory', async ({ provider, env, withCache }) => {
+    const address = "0x7dB8637A5fd20BbDab1176BdF49C943A96F2E9c6"; // Factory that makes proxies
+
+    const code = await withCache(
+        `${address}_code`,
+        async () => {
+            return await provider.getCode(address)
+        },
+    )
+    const result = await autoload(address, {
+        provider: provider,
+        ...whatsabi.loaders.defaultsWithEnv(env),
+    });
+    expect(result.isFactory).toBeTruthy();
+});
+