From 10bcbfb2c1c83d876dd9b0e41aa629f9d304e0d3 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 18 Sep 2024 09:08:01 -0700 Subject: [PATCH] auto: fallbackLoad --- src/__tests__/auto.test.ts | 50 ++++++++++++++++++++++++++++++++++++++ src/auto.ts | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/__tests__/auto.test.ts b/src/__tests__/auto.test.ts index 7a65b19..5023a8b 100644 --- a/src/__tests__/auto.test.ts +++ b/src/__tests__/auto.test.ts @@ -70,3 +70,53 @@ online_test('autoload non-contract', async ({ provider, env }) => { }); expect(abi).toStrictEqual([]); }); + +online_test('autoload fallback', async ({ provider, env }) => { + const address = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; // USDT, available on both PulseChain and Mainnet (but only verified on mainnet) + const pulseChainProvider = makeProvider("https://pulsechain-rpc.publicnode.com"); + { + // Fails to find verified ABI on pulseChain + const result = await autoload(address, { + provider: pulseChainProvider, + signatureLookup: false, + abiLoader: false, + }); + expect(result.isVerified).toBeFalsy(); + expect(result.abi).not.toContainEqual({ + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ { "name": "", "type": "uint256", }], + "payable": false, + "stateMutability": "view", + "type": "function", + }); + } + { + // Succeeds by cross-loading mainnet + const result = await autoload(address, { + provider: pulseChainProvider, + signatureLookup: false, + abiLoader: false, + // onProgress: (phase: string, ...args: any[]) => { console.debug("Mainnet PROGRESS", phase, args); }, + fallbackLoad: { + provider, + signatureLookup: false, + // onProgress: (phase: string, ...args: any[]) => { console.debug("fallback PROGRESS", phase, args); }, + ...whatsabi.loaders.defaultsWithEnv(env), + } + }); + expect(result.isVerified).toBeTruthy(); + expect(result.abi).toContainEqual({ + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ { "name": "", "type": "uint256", }], + "payable": false, + "stateMutability": "view", + "type": "function", + }); + } +}); + + diff --git a/src/auto.ts b/src/auto.ts index 7def417..c078c84 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -82,6 +82,23 @@ export type AutoloadConfig = { */ followProxies?: boolean; + /** + * By default, we'll only try to load the ABI from the respective chain. + * But if it doesn't exist, we can check if the same verified contract exists on another network and use it instead. + * Suggest setting { signatureLookup: false } for this, as it would be redundant. + * + * @group Settings + */ + fallbackLoad?: AutoloadConfig; + + /** + * Keccak256 hash of the contract bytecode (0x-prefixed hex string). + * + * Confirm that the bytecode we get for the contract matches some existing assumed bytecode. + * This is used by fallbackLoad to verify that the bytecode matches on both chains. + */ + assertBytecodeHash?: string; + /** * Enable pulling additional metadata from WhatsABI's static analysis, still unreliable * @@ -159,6 +176,22 @@ export async function autoload(address: string, config: AutoloadConfig): Promise } if (!bytecode) return result; // Must be an EOA + // We only need to get the hash if we're asserting, so let's be lazy + let bytecodeHash : string = (config.assertBytecodeHash || config.fallbackLoad) ? keccak256(bytecode) : ""; + + if (config.assertBytecodeHash) { + if (!config.assertBytecodeHash.startsWith("0x")) { + throw new errors.AutoloadError(`assertBytecodeHash must be a hex string starting with 0x`, { + context: { address, assertBytecodeHash: config.assertBytecodeHash }, + }); + } + if (config.assertBytecodeHash !== bytecodeHash) { + throw new errors.AutoloadError(`Contract bytecode hash does not match assertBytecodeHash`, { + context: { address, bytecodeHash, assertBytecodeHash: config.assertBytecodeHash }, + }); + } + } + const program = disasm(bytecode); // FIXME: Sort them in some reasonable way @@ -214,6 +247,23 @@ export async function autoload(address: string, config: AutoloadConfig): Promise } } + if (config.fallbackLoad) { + onProgress("crossChainLoad", { address }); + try { + const r = await autoload( + address, + Object.assign({ assertBytecodeHash: bytecodeHash }, config.fallbackLoad), + ); + if (r.isVerified) { + result.abi = r.abi; + result.isVerified = true; // Bytecode is asserted, so we claim it's verified + return result; + } + } catch (error: any) { + if (onError("crossChainLoad", error) === true) return result; + } + } + // Load from code onProgress("abiFromBytecode", { address }); result.abi = abiFromBytecode(program);