diff --git a/examples/sevm.ts b/examples/sevm.ts new file mode 100755 index 0000000..244de03 --- /dev/null +++ b/examples/sevm.ts @@ -0,0 +1,74 @@ +#!/usr/bin/env -S ts-node-script --esm + +import { readFileSync } from "fs"; +import { ethers } from "ethers"; +import { withCache } from "../src/internal/filecache.js"; + +// @ts-ignore +import { Contract } from "sevm"; + +import type { ABI, ABIFunction, ABIEvent } from "../src/abi.js"; + + +const { INFURA_API_KEY } = process.env; +const provider = INFURA_API_KEY ? (new ethers.InfuraProvider("homestead", INFURA_API_KEY)) : ethers.getDefaultProvider("homestead"); + + +export function abiFromBytecode(bytecode: string): ABI { + const abi : ABI = []; + + const c = new Contract(bytecode); + for (const [selector, fn] of Object.entries(c.functions)) { + // let mutability = fn.payable ? "payable" : "nonpayable"; + // TODO: Can we get view or pure? + // TODO: Can we get inputs/outputs? + const a = { + selector, + } as ABIFunction; + if (fn.payable) a.payable = true; + abi.push(a); + } + + for (const [topic, _] of Object.entries(c.getEvents())) { + abi.push({ + hash: topic, + } as ABIEvent); + } + + return abi; +} + + +async function main() { + const address = process.env["ADDRESS"] || process.argv[2]; + + let code : string; + if (!address) { + console.error("Invalid address: " + address); + process.exit(1); + } else if (address === "-") { + // Read contract code from stdin + code = readFileSync(0, 'utf8').trim(); + } else { + console.debug("Loading code for address:", address); + code = await withCache( + `${address}_abi`, + async () => { + return await provider.getCode(address) + }, + ); + } + + //const c = new Contract(code); + //console.log(c); + //console.log(c.functions); /* Get functions */ + //console.log(c.getEvents()); /* Get events */ + + const abi = abiFromBytecode(code); + console.log(abi); +}; + +main().then().catch(err => { + console.error("Failed:", err) + process.exit(2); +}) diff --git a/package.json b/package.json index 8aa2ed1..84dc8a1 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,12 @@ "dependencies": { "ethers": "^6.8.0" }, - "peerDependencies": { - "@noble/hashes": "^1" - }, "devDependencies": { "@size-limit/esbuild-why": "^8.2.6", "@size-limit/preset-small-lib": "^8.2.6", "size-limit": "^8.2.6", "ts-node": "^10.9.1", + "sevm": "^0.5.3", "viem": "^1.16.4", "vitest": "^0.34.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11fef6d..7912fd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: ethers: specifier: ^6.8.0 version: 6.8.0 + sevm: + specifier: ^0.5.3 + version: 0.5.3 devDependencies: '@size-limit/esbuild-why': @@ -998,6 +1001,10 @@ packages: lru-cache: 6.0.0 dev: true + /sevm@0.5.3: + resolution: {integrity: sha512-KA9kmu70EHaWCyZBTTEuCWJsAC1kuJb1BfpP6dWYgcYzc1wOK/IdW/t1M2dFxFPbeqj2hcGUFS/2Z7qIlbAIKg==} + dev: false + /siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true diff --git a/src/__tests__/env.ts b/src/__tests__/env.ts index ce1716d..ff0c8d0 100644 --- a/src/__tests__/env.ts +++ b/src/__tests__/env.ts @@ -1,4 +1,4 @@ -import { test } from 'vitest'; +import { test, describe } from 'vitest'; import { ethers } from "ethers"; import { createPublicClient, http } from 'viem'; @@ -29,10 +29,15 @@ type TestWithContext = ( timeout?: number ) => void; +// TODO: Switch to https://vitest.dev/api/#test-extend function testerWithContext(tester: ItConcurrent, context: any): TestWithContext { return (name, fn, timeout) => tester(name, () => fn(context), timeout); } +export function describe_cached(d: string, fn: (context: any) => void) { + return describe(d, () => fn({ provider, env, withCache })); +} + // TODO: Port this to context-aware wrapper export const online_test = testerWithContext(process.env["ONLINE"] ? test : test.skip, { provider, env }); export const cached_test = testerWithContext(!process.env["SKIP_CACHED"] ? test : test.skip, { provider, env, withCache }); @@ -40,3 +45,10 @@ export const cached_test = testerWithContext(!process.env["SKIP_CACHED"] ? test if (process.env["ONLINE"] === undefined) { console.log("Skipping online tests. Set ONLINE env to run them."); } + +export const KNOWN_ADDRESSES = [ + {address: "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", label: "Uniswap v2"}, + {address: "0x00000000006c3852cbEf3e08E8dF289169EdE581", label: "Seaport v1.1"}, + {address: "0x4A137FD5e7a256eF08A7De531A17D0BE0cc7B6b6", label: "Random unverified"}, + {address: "0x000000000000Df8c944e775BDe7Af50300999283", label: "Has 0x0 selector"}, +]; diff --git a/src/__tests__/sevm.bench.ts b/src/__tests__/sevm.bench.ts new file mode 100644 index 0000000..f82886d --- /dev/null +++ b/src/__tests__/sevm.bench.ts @@ -0,0 +1,20 @@ +import { describe, bench } from 'vitest'; + +import { describe_cached, KNOWN_ADDRESSES } from "./env"; + +import { Contract } from "sevm"; +import { disasm } from "../disasm.js"; + +describe_cached("bench: whatsabi vs sevm", async ({ provider, withCache}) => { + describe.each(KNOWN_ADDRESSES)("decompile $address ($label)", async ({address}) => { + const code = await withCache(`${address}_code`, provider.getCode.bind(provider, address)) + + bench('disassemble with whatsabi', () => { + disasm(code); + }) + + bench('disassemble with sevm', () => { + new Contract(code); + }) + }); +}); diff --git a/src/__tests__/sevm.test.ts b/src/__tests__/sevm.test.ts new file mode 100644 index 0000000..7e20997 --- /dev/null +++ b/src/__tests__/sevm.test.ts @@ -0,0 +1,63 @@ +import { expect, test, describe } from 'vitest'; + +// @ts-ignore +import { Contract } from "sevm"; + +import type { ABI, ABIFunction, ABIEvent } from "../abi.js"; + +import { whatsabi } from "../index.js"; +import { describe_cached, KNOWN_ADDRESSES } from "./env"; + +type sevmPublicFunction = { + readonly payable: boolean; + readonly visibility: string; + readonly constant: boolean; + readonly returns: string[]; +}; + +function abiFromBytecode(bytecode: string): ABI { + const abi : ABI = []; + + const c = new Contract(bytecode); + for (const [selector, fn] of Object.entries(c.functions as Record)) { + // let mutability = fn.payable ? "payable" : "nonpayable"; + // TODO: Can we get view or pure? + // TODO: Can we get inputs/outputs? + // TODO: Look at returns + const a = { + type: "function", + selector: "0x" + selector, // Add 0x + } as ABIFunction; + if (fn.payable) a.payable = true; + abi.push(a); + } + + for (const [topic, _] of Object.entries(c.getEvents())) { + abi.push({ + type: "event", + hash: topic, + } as ABIEvent); + } + + return abi; +} + +describe_cached("whatsabi vs sevm: abiFromBytecode", async ({ provider, withCache}) => { + + describe.each(KNOWN_ADDRESSES)("decompile $address ($label)", async ({address}) => { + + const code = await withCache(`${address}_code`, provider.getCode.bind(provider, address)) + + test("compare selectors", async () => { + const [a, b] = [abiFromBytecode, whatsabi.abiFromBytecode].map(getABI => { + const abi = getABI(code); + const functions = abi.filter(a => a.type === "function") as ABIFunction[]; + const selectors = functions.map(abi => abi.selector); + selectors.sort(); + return selectors; + }); + + expect(a).toStrictEqual(b); + }); + }); +}); diff --git a/src/auto.ts b/src/auto.ts index e301417..9cfff41 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -34,6 +34,7 @@ export type AutoloadResult = { export type AutoloadConfig = { provider: AnyProvider; + abiFromBytecode?: (bytecode: string) => Promise; abiLoader?: ABILoader|false; signatureLookup?: SignatureLookup|false; @@ -118,11 +119,16 @@ export async function autoload(address: string, config: AutoloadConfig): Promise } // Load from code - onProgress("getCode", {address}); - result.abi = abiFromBytecode(program); - - if (!config.enableExperimentalMetadata) { - result.abi = stripUnreliableABI(result.abi); + onProgress("abiFromBytecode", {address}); + if (config.abiFromBytecode) { + result.abi = await config.abiFromBytecode(bytecode); + } else { + result.abi = abiFromBytecode(program); + + // We only strip if our default abiFromBytecode function is used. + if (!config.enableExperimentalMetadata) { + result.abi = stripUnreliableABI(result.abi); + } } let signatureLookup = config.signatureLookup;