UNPKG

0xweb

Version:

Contract package manager and other web3 tools

299 lines (262 loc) 9.8 kB
import alot from 'alot'; import type { TAbiItem } from '@dequanto/types/TAbi'; import { OpcodesInfo } from './OpcodesInfo'; import { JsonObjectStore } from '@dequanto/json/JsonObjectStore'; import { $path } from '@dequanto/utils/$path'; import { $abiParser } from '@dequanto/utils/$abiParser'; import Opcode from './interfaces/IOpcode'; import IMemory from './interfaces/IMemory'; import IJumps from './interfaces/IJumps'; import Stack from './Stack'; import IStorage from './interfaces/IStorage'; import { OpcodesWalker } from './OpcodesWalker'; import { $logger } from '@dequanto/utils/$logger'; import IOpcode from './interfaces/IOpcode'; import { $abiUtils } from '@dequanto/utils/$abiUtils'; import { $bytecode } from './utils/$bytecode'; import { $buffer } from '@dequanto/utils/$buffer'; import { $array } from '@dequanto/utils/$array'; import { TEth } from '@dequanto/models/TEth'; import opcodeFunctions from './utils/opcodes'; /** * Functions to SKIP * 08c379a0 - Error(string) * 4e487b71 - Panic(uint256) * ffffffff - */ class Stores { functions = new JsonObjectStore<Record<string, string>>({ path: $path.resolve(`/data/evm/functionHashes.json`) }) functionHashes: Record<string, string>; events = new JsonObjectStore<Record<string, string>>({ path: $path.resolve(`/data/evm/eventHashes.json`) }) eventHashes: Record<string, string>; async readAll () { let [ functions, events ] = await Promise.all([ this.functions.get(), this.events.get(), ]); this.functionHashes = functions; this.eventHashes = events; } } export class EvmBytecode { pc: number = 0; stack = new Stack(); memory: IMemory = {} instructions: any = []; storage: IStorage = {}; jumps: IJumps = {}; mappings: any = {}; layer: number = 0 halted: boolean = false; functions: any = {}; variables: any = {}; events: any = {}; gasUsed: number = 0; private opcodes: Opcode[] = []; private code: Uint8Array; public store = new Stores(); constructor(code: TEth.Hex | Uint8Array, opts?: { withConstructorCode?: boolean }) { if (code instanceof Uint8Array) { this.code = code; } else { if (opts?.withConstructorCode !== true) { // remove the constructor code, leave the runtime bytecode only; code = $bytecode.trimConstructorCode(code); } this.code = $buffer.fromHex(code.replace('0x', '')); } } public getOpcodes(): Opcode[] { if (this.opcodes.length === 0) { for (let index = 0; index < this.code.length; index++) { const currentOp = OpcodesInfo.get(this.code[index], true); currentOp.pc = index; this.opcodes.push(currentOp); if (currentOp.name.startsWith('PUSH')) { const pushDataLength = this.code[index] - 0x5f; const pushData = this.code.slice(index + 1, index + pushDataLength + 1); currentOp.pushData = pushData; index += pushDataLength; } } } return this.opcodes; } public getInstructions () { if (this.instructions.length === 0) { const opcodes = this.getOpcodes(); for (this.pc; this.pc < opcodes.length && !this.halted; this.pc++) { const opcode = opcodes[this.pc]; this.gasUsed += opcode.fee; if (opcode.name in opcodeFunctions === false) { throw new Error('Unknown OPCODE: ' + opcode.name); } opcodeFunctions[opcode.name](opcode, this); } } return this.instructions; } async getAbi(opts?: { parseStateMutability?: boolean }): Promise<TAbiItem[]> { await this.prepare(); let [functions, events] = await Promise.all([ this.getFunctions(), this.getEvents(), ]); let fnsAbi = await alot(functions).mapAsync(async entry => { let str = entry.name ?? `_${entry.signature}()`; let abi = $abiParser.parseMethod(str); try { if (opts?.parseStateMutability !== false) { let { opcodes } = this.getMethodOpcodes({ sig: entry.signature }) let isReadOnly = $opcodes.isReadOnly(opcodes); if (isReadOnly) { abi.stateMutability = 'view'; } } } catch (error) { $logger.error(`Getting method ${str} opcodes failed: ${error.message}`); } return abi; }).toArrayAsync(); let eventsAbi = events.map(entry => { if (entry.name == null) { return null; } let str = `event ${entry.name}`; return $abiParser.parseMethod(str); }).filter(Boolean); return [ ...fnsAbi, ...eventsAbi, ]; } async getFunctions(): Promise<{ signature, name }[]> { let SKIP = [ null, '08c379a0', // Error(string) '4e487b71', // Panic(uint256) 'ffffffff' ]; let opcodes = this.getOpcodes(); /** Select PUSH4 opcodes in first calldataload block to filter any other method calls within the bytecode */ let opcodeCalldataLoadIdx = $array.findIndex(opcodes, x => x.name === 'CALLDATALOAD'); let opcodeCalldataLoad = opcodes[opcodeCalldataLoadIdx]; let jumpDestIdx = $array.findIndex(opcodes, x => x?.name === 'JUMPDEST', opcodeCalldataLoadIdx); let jumpDest = opcodes[jumpDestIdx]; let rangeStart = opcodeCalldataLoadIdx === -1 ? 0 : opcodeCalldataLoad.pc; let rangeEnd = opcodeCalldataLoadIdx === -1 ? Infinity : (jumpDest?.pc ?? Infinity); let hashes = opcodes .filter(opcode => opcode.name === 'PUSH4') .map((opcode, i) => { if (opcode.pc < rangeStart || opcode.pc > rangeEnd) { return null; } return opcode.pushData?.toString('hex') ?? null }) .filter(x => SKIP.includes(x) === false); let fns = await this.resolveFunctions(hashes); return fns; } async checkInterfaceOf (iface: (TAbiItem | string)[]): Promise<{ ok: boolean, missing?: string }> { if (iface == null || iface.length === 0) { return { ok: false }; } let methods = await this.getFunctions(); for (let item of iface) { if (typeof item === 'string') { item = $abiParser.parseMethod(item); } if (item.type !== 'function') { continue; } let inAbi = methods.some(x => x.signature === $abiUtils.getMethodSignature(item)); if (inAbi === false) { return { ok: false, missing: item.name }; } } return { ok: true }; } private async resolveFunctions(hashes: string[]) { let fns = await this.store.functions.get(); return alot(hashes) .distinct() .map(hash => { return { signature: `0x` + hash, name: fns[hash] ?? null }; }) .toArray(); } async getEvents(): Promise<{ signature, name }[]> { let hashes = this.getOpcodes() .filter(opcode => opcode.name === 'PUSH32') .map(opcode => opcode.pushData?.toString('hex') ?? null) .filter(x => x != null); let events = await this.resolveEvents(hashes); return events; } clone(): EvmBytecode { const clone = new EvmBytecode(this.code); clone.pc = this.pc; clone.opcodes = this.opcodes; clone.stack = this.stack.clone(); clone.memory = { ...this.memory }; clone.storage = this.storage; clone.jumps = { ...this.jumps }; clone.mappings = this.mappings; clone.layer = this.layer + 1; clone.functions = this.functions; clone.variables = this.variables; clone.events = this.events; clone.gasUsed = this.gasUsed; clone.store = this.store; return clone; } async prepare (): Promise<this> { await this.store.readAll(); return this; } getMethodOpcodes (abi: string) getMethodOpcodes (opts: { sig: string }) getMethodOpcodes (mix: string | { sig: string }): { opcodes: IOpcode[], exit } { this.stack.reset(); let opcodes = this.getOpcodes(); let walker = new OpcodesWalker(this, opcodes); return walker.getMethodOpcodes(mix); } private async resolveEvents(hashes: string[]) { let events = await this.store.events.get(); return alot(hashes) .distinct() .map(hash => { return { signature: `0x` + hash, name: events[hash] ?? null }; }) .toArray(); } } namespace $opcodes { export function isReadOnly(opcodes: IOpcode[]) { let writes = [ 'SSTORE', 'LOG' ]; let hasWrite = opcodes.some(opcode => writes.includes(opcode.name)); if (hasWrite) { return false; } // Read methods usually have RETURN code let hasStop = opcodes.some(opcode => 'STOP' === opcode.name); if (hasStop) { return false; } return true; } }