UNPKG

sevm

Version:

A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI

639 lines (592 loc) 23 kB
import { Contract, type PublicFunction } from '.'; import { fnsig, parseSig } from './abi'; import { If, Revert, Tag, Val, evalE, isExpr, isInst, type Expr, type IReverts, type Inst, type Stmt } from './ast'; import type { IEvents } from './ast/log'; import { FNS } from './ast/special'; import type { IStore } from './ast/storage'; /** * * @param strings * @param nodes * @returns */ export function sol(strings: TemplateStringsArray, ...nodes: unknown[]): string { const result = [strings[0]]; nodes.forEach((node, i) => { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const str = isExpr(node) ? solExpr(node) : isInst(node) ? solStmt(node) : `${node}`; result.push(str, strings[i + 1]); }); return result.join(''); } const OPS = { Add: ['+', 11], Mul: ['*', 12], Sub: ['-', 11], Div: ['/', 12], Mod: ['%', 12], Exp: ['**', 14], Lt: ['<', 9], Gt: ['>', 9], Eq: ['==', 8], And: ['&', 4], Or: ['|', 3], Xor: ['^', 6], Not: ['not', 14], Byte: ['byte', 10], Shl: ['<<', 10], Shr: ['>>>', 10], Sar: ['>>', 10], } as const; const prec = (expr: Expr): number => { const tag = expr.tag; if (tag in OPS) { return OPS[tag as keyof typeof OPS][1]; } else { return 16; } }; function paren(expr: Expr, exprc: Expr): string { return prec(expr) < prec(exprc) ? sol`(${expr})` : sol`${expr}`; } function solExpr(expr: Expr): string { switch (expr.tag) { case 'Val': return `${expr.isJumpDest() ? '[J]' : ''}0x${expr.val.toString(16)}`; case 'Local': return expr.nrefs > 0 ? `local${expr.index}` : expr.count >= 1000 ? `/*expression too big to decompile: ${expr.count} AST nodes*/local${expr.index}` : sol`${expr.value}`; case 'Add': case 'Mul': case 'Sub': case 'Div': case 'Mod': case 'Exp': case 'Eq': case 'And': case 'Or': case 'Xor': return `${paren(expr.left, expr)} ${OPS[expr.tag][0]} ${paren(expr.right, expr)}`; case 'Lt': case 'Gt': return `${paren(expr.left, expr)} ${OPS[expr.tag][0]}${expr.equal ? '=' : ''} ${paren( expr.right, expr )}`; case 'IsZero': return expr.value.tag === 'Eq' ? paren(expr.value.left, expr) + ' != ' + paren(expr.value.right, expr) : paren(expr.value, expr) + ' == 0'; case 'Not': return `~${paren(expr.value, expr)}`; case 'Byte': return `(${paren(expr.data, expr)} >> ${paren(expr.pos, expr)}) & 1`; case 'Shl': case 'Shr': case 'Sar': return `${paren(expr.value, expr)} ${OPS[expr.tag][0]} ${paren(expr.shift, expr)}`; case 'Sig': return `msg.sig == ${expr.selector}`; case 'CallValue': return 'msg.value'; case 'CallDataLoad': { const location = expr.location; return location.isVal() && location.val === 0n ? 'msg.data' : location.isVal() && (location.val - 4n) % 32n === 0n ? `_arg${(location.val - 4n) / 32n}` : sol`msg.data[${location}]`; } case 'Prop': return expr.symbol; case 'Fn': return FNS[expr.mnemonic][0](solExpr(expr.value)); case 'DataCopy': switch (expr.kind) { case 'calldatacopy': return sol`msg.data[${expr.offset}:(${expr.offset}+${expr.size})]`; case 'codecopy': return sol`this.code[${expr.offset}:(${expr.offset}+${expr.size})]`; case 'extcodecopy': return sol`address(${expr.address}).code[${expr.offset}:(${expr.offset}+${expr.size})]`; case 'returndatacopy': return sol`output[${expr.offset}:(${expr.offset}+${expr.size})]`; } case 'MLoad': return sol`memory[${expr.location}]`; case 'Sha3': return expr.args === undefined ? sol`keccak256(memory[${expr.offset}:(${expr.offset}+${expr.size})])` : `keccak256(${expr.args.map(solExpr).join(', ')})`; case 'Create': return sol`new Contract(memory[${expr.offset}..${expr.offset}+${expr.size}]).value(${expr.value}).address`; case 'Call': return expr.argsLen.isZero() && expr.retLen.isZero() ? expr.gas.tag === 'Mul' && expr.gas.left.isZero() && expr.gas.right.isVal() && expr.gas.right.val === 2300n ? expr.throwOnFail ? sol`address(${expr.address}).transfer(${expr.value})` : sol`address(${expr.address}).send(${expr.value})` : sol`address(${expr.address}).call.gas(${expr.gas}).value(${expr.value})` : sol`call(${expr.gas},${expr.address},${expr.value},${expr.argsStart},${expr.argsLen},${expr.retStart},${expr.retLen})`; case 'ReturnData': return sol`output:ReturnData:${expr.retOffset}:${expr.retSize}`; case 'CallCode': return sol`callcode(${expr.gas},${expr.address},${expr.value},${expr.memoryStart},${expr.memoryLength},${expr.outputStart},${expr.outputLength})`; case 'Create2': return sol`new Contract(memory[${expr.offset}:(${expr.offset}+${expr.size})]).value(${expr.value}).address`; case 'StaticCall': return sol`staticcall(${expr.gas},${expr.address},${expr.memoryStart},${expr.memoryLength},${expr.outputStart},${expr.outputLength})`; case 'DelegateCall': return sol`delegatecall(${expr.gas},${expr.address},${expr.memoryStart},${expr.memoryLength},${expr.outputStart},${expr.outputLength})`; case 'SLoad': { // const slot = expr.slot.eval(); // if (slot.isVal() && expr.variable !== undefined) { if (expr.variable !== undefined) { // const loc = slot.val; const label = expr.variable.label; if (label) { return label; } else { // return `var${Object.keys(expr.variables).indexOf(loc) + 1}`; return `var_${expr.variable.index}`; } } else { return sol`storage[${expr.slot}]`; } } case 'MappingLoad': { let mappingName = `mapping${expr.location + 1}`; const maybeName = expr.mappings[expr.location].name; if (expr.location in expr.mappings && maybeName) { mappingName = maybeName; } if (expr.structlocation) { return ( mappingName + expr.items.map(item => sol`[${item}]`).join('') + '[' + expr.structlocation.toString() + ']' ); } else { return mappingName + expr.items.map(item => '[' + solExpr(item) + ']').join(''); } } } } function sigName(sig: string | undefined): string | undefined { if (sig === undefined) return undefined; try { return parseSig(sig).name; } catch { return sig; } } function solInst(inst: Inst): string { switch (inst.name) { case 'Local': return sol`${inst.local.value.type} local${inst.local.index} = ${inst.local.value}; // #refs ${inst.local.nrefs}`; case 'MStore': return sol`memory[${inst.location}] = ${inst.data};`; case 'Stop': return 'return;'; case 'Return': return inst.args === undefined ? sol`return memory[${inst.offset}:(${inst.offset}+${inst.size})];` : inst.args.length === 0 ? 'return;' : isStringReturn(inst.args) && inst.args[0].val === 32n ? `return '${hex2a(inst.args[2].val.toString(16))}';` : inst.args.length === 1 ? sol`return ${inst.args[0]};` : `return (${inst.args.map(solExpr).join(', ')});`; case 'Revert': { const revertMsg = inst.selector !== undefined ? getRevertMsg(inst, inst.selector) : undefined; return revertMsg !== undefined ? `revert(${revertMsg});` : inst.args === undefined ? sol`revert(memory[${inst.offset}:(${inst.offset}+${inst.size})]);` : inst.selector !== undefined ? `revert ${sigName(inst.sig?.sig) ?? inst.selector}(${inst.args.map(solExpr).join(', ')});` : `revert(${inst.args.map(solExpr).join(', ')});`; } case 'SelfDestruct': return sol`selfdestruct(${inst.address});`; case 'Invalid': return `revert('Invalid instruction (0x${inst.opcode.toString(16)})');`; case 'Log': return inst.eventName ? `emit ${inst.eventName}(${[...inst.topics.slice(1), ...(inst.args ?? [])] .map(solExpr) .join(', ')});` : 'log(' + (inst.args === undefined ? [...inst.topics, sol`memory[${inst.offset}:${inst.size} ]`].join(', ') + 'ii' : [...inst.topics, ...inst.args].map(solExpr).join(', ')) + ');'; case 'Jump': return sol`goto :${inst.offset} branch:${inst.destBranch.pc}`; case 'Jumpi': return sol`when ${inst.cond} goto ${inst.destBranch.pc} or fall ${inst.fallBranch.pc}`; case 'JumpDest': return `fall: ${inst.fallBranch.pc}:`; case 'SigCase': return sol`case when ${inst.condition} goto ${inst.offset} or fall ${inst.fallBranch.pc}`; case 'SStore': { const slot = inst.slot.eval(); const isLoad = (value: Expr) => value.tag === 'SLoad' && solExpr(value.slot.eval()) === solExpr(slot); let varName = sol`storage[${slot}]`; if (slot.isVal() && inst.variable !== undefined) { const label = inst.variable.label; if (label) { varName = label; } else { // varName = `var${[...inst.variables.keys()].indexOf(loc) + 1}__${inst.variables.get(loc)!.index}`; varName = `var_${inst.variable.index}`; } } const data = inst.data.eval(); if (data.tag === 'Add' && isLoad(data.left)) { return sol`${varName} += ${data.right};`; } else if (data.tag === 'Add' && isLoad(data.right)) { return sol`${varName} += ${data.left};`; } else if (data.tag === 'Sub' && isLoad(data.left)) { return sol`${varName} -= ${data.right};`; } else { return sol`${varName} = ${inst.data};`; } } case 'MappingStore': { let mappingName = `mapping${inst.location + 1}`; if (inst.location in inst.mappings && inst.mappings[inst.location].name) { mappingName = inst.mappings[inst.location].name!; } if ( inst.data.tag === 'Add' && inst.data.right.tag === 'MappingLoad' && inst.data.right.location === inst.location ) { return ( mappingName + inst.items.map(item => '[' + solExpr(item) + ']').join('') + ' += ' + solExpr(inst.data.left) + ';' ); } else if ( inst.data.tag === 'Add' && inst.data.left.tag === 'MappingLoad' && inst.data.left.location === inst.location ) { return ( mappingName + inst.items.map(item => sol`[${item}]`).join('') + ' += ' + solExpr(inst.data.right) + ';' ); } else if ( inst.data.tag === 'Sub' && inst.data.left.tag === 'MappingLoad' && inst.data.left.location === inst.location ) { return ( mappingName + inst.items.map(item => sol`[${item}]`).join('') + ' -= ' + solExpr(inst.data.right) + ';' ); } else { return ( mappingName + inst.items.map(item => sol`[${item}]`).join('') + ' = ' + solExpr(inst.data) + ';' ); } } case 'Throw': return `throw('${inst.reason}');`; } } function isStringReturn(args: Expr[] | undefined): args is [Val, Val, Val] { return args?.length === 3 && args.every(arg => arg.isVal()); } function hex2a(hexstr: string) { let str = ''; for (let i = 0; i < hexstr.length && hexstr.slice(i, i + 2) !== '00'; i += 2) { str += String.fromCharCode(parseInt(hexstr.substring(i, i + 2), 16)); } return str; } function getRevertMsg({ args }: { args?: Expr[] | undefined }, selector: string): string | undefined { args = args?.map(evalE); return selector === Revert.ERROR && isStringReturn(args) && args[0].val === 32n ? `"${hex2a(args[2].val.toString(16))}"` : undefined; } function solStmt(stmt: Stmt): string { switch (stmt.name) { case 'If': return sol`(${stmt.condition})`; case 'CallSite': return sol`$${stmt.selector}();`; case 'Require': { const fnname = stmt.selector === Revert.PANIC ? 'assert' : 'require'; const args = stmt.selector === undefined || stmt.selector === Revert.PANIC ? stmt.args : [new Val(BigInt('0x' + stmt.selector)), ...stmt.args]; const revertMsg = getRevertMsg(stmt, Revert.ERROR); return revertMsg !== undefined ? sol`${fnname}(${stmt.condition}, ${revertMsg});` : `${fnname}(${[stmt.condition, ...args].map(solExpr).join(', ')});`; } default: return solInst(stmt); } } /** * * @param stmts * @param spaces * @returns */ export function solStmts(stmts: Stmt[], spaces = 0): string { let text = ''; for (const stmt of stmts) { if (stmt instanceof If) { const condition = solStmt(stmt); text += ' '.repeat(spaces) + 'if ' + condition + ' {\n'; text += solStmts(stmt.trueBlock!, spaces + 4); if (stmt.falseBlock) { text += ' '.repeat(spaces) + '} else {\n'; text += solStmts(stmt.falseBlock, spaces + 4); } text += ' '.repeat(spaces) + '}\n'; } else { if (stmt.name === 'Local' && stmt.local.nrefs <= 0) { continue; } if (stmt.name === 'MStore') { continue; } text += ' '.repeat(spaces) + solStmt(stmt) + '\n'; } } return text; } /** * * @param events * @returns */ export function solEvents(events: IEvents, spaces = 0) { let text = ''; for (const [topic, event] of Object.entries(events)) { text += ' '.repeat(spaces) + 'event '; if (event.sig === undefined) { text += topic; } else { const eventName = event.sig.split('(')[0]; const params = event.sig.replace(eventName, '').substring(1).slice(0, -1); if (params) { text += eventName + '('; text += params.split(',').map((param, i) => i < event.indexedCount ? `${param} indexed _arg${i}` : `${param} _arg${i}` ) .join(', '); text += ')'; } else { text += event.sig; } } text += ';\n'; } return text; } /** * * @param variables * @returns */ export function solVars(variables: IStore['variables']) { let output = ''; [...variables.entries()].forEach(([hash, variable], index) => { const types: string[] = variable.types .map(expr => (expr.isVal() ? 'uint256' : expr.type ?? '')) .filter(t => t.trim() !== ''); if (types.length === 0) { types.push('unknown'); } const name = variable.label ? ` public ${variable.label}` : ` var${index + 1}__${variable.index}`; output += ` ${[...new Set(types)].join('|') + name}; // Slot #${hash}\n`; }); return output; } /** * * @param mappings * @returns */ export function solStructs(mappings: IStore['mappings']) { let text = ''; Object.keys(mappings) .filter(key => mappings[key].structs.length > 0) .forEach(key => { const mapping = mappings[key]; text += `struct ${mapping.name}Struct {\n`; mapping.structs.forEach(struct => { text += ` ${struct.toString()};\n`; }); text += '}\n\n'; }); return text; } /** * * @param mappings * @returns */ export function solMappings(mappings: IStore['mappings']) { let output = ''; Object.keys(mappings).forEach((key: string, index: number) => { const mapping = mappings[key]; const label = mapping.name ? `public ${mapping.name}` : `mapping${index + 1}`; output += ` ${solMapping(mapping)} ${label};\n`; }); return output; function solMapping(mapping: IStore['mappings'][keyof IStore['mappings']]) { const mappingKey: string[] = []; const mappingValue: string[] = []; let deepMapping = false; mapping.keys .filter(mappingChild => mappingChild.length > 0) .forEach(mappingChild => { const mappingChild0 = mappingChild[0]; if ( mappingChild.length > 0 && mappingChild0.type && !mappingKey.includes(mappingChild0.type) ) { mappingKey.push(mappingChild0.type); } if (mappingChild.length > 1 && !deepMapping) { deepMapping = true; mappingValue.push( solMapping({ name: mapping.name, structs: mapping.structs, keys: mapping.keys.map(items => { return items.slice(1); }), values: mapping.values, }) ); } else if (mappingChild.length === 1 && !deepMapping) { mapping.values.forEach(mappingChild2 => { if (mappingChild2.type && !mappingValue.includes(mappingChild2.type)) { mappingValue.push(mappingChild2.type); } }); } }); if (mappingKey.length === 0) { mappingKey.push('unknown'); } if (mapping.structs.length > 0 && mappingValue.length === 0) { mappingValue.push(`${mapping.name}Struct`); } else if (mappingValue.length === 0) { mappingValue.push('unknown'); } return 'mapping (' + mappingKey.join('|') + ' => ' + mappingValue.join('|') + ')'; } } /** * * @returns the decompiled text for `this` function. */ function solPublicFunction(self: PublicFunction, tab = ' '): string { let output = tab + 'function ' + (self.label !== undefined ? fnsig(parseSig(self.label)) : `${self.selector}(/*no signature*/)`) + ' ' + self.visibility; if (self.constant) output += ' view'; if (self.payable) output += ' payable'; if (self.returns.length > 0) output += ` returns (${self.returns.join(', ')})`; output += ' {\n'; output += solStmts(self.stmts, 8); output += tab + '}\n\n'; return output; } function solReverts(reverts: IReverts): string { let output = ''; for (const [selector, decl] of Object.entries(reverts) as [string, IReverts[string]][]) { if (!Revert.isRequireOrAssert(selector)) { if (decl.sig === undefined) output += ` // error ${selector}\n`; else output += ` error ${decl.sig}; // ${selector}\n`; } } return output; } function solContract( this: Contract, options: { license?: string | null; pragma?: boolean; contractName?: string } = {} ): string { const { license = 'UNLICENSED', pragma = true, contractName = 'Contract' } = options; let text = ''; if (license) { text += `// SPDX-License-Identifier: ${license}\n`; } if (pragma && this.metadata) { text += `// Metadata ${this.metadata.url}\n`; text += `pragma solidity ${this.metadata.solc};\n`; text += '\n'; } text += `contract ${contractName} {\n\n`; const member = (text: string) => text === '' ? '' : text + '\n'; text += member(solEvents(this.events, 4)); text += solStructs(this.mappings); text += member(solMappings(this.mappings)); text += member(solVars(this.variables)); text += member(solReverts(this.reverts)); // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain const fallback = this.metadata?.minor! >= 6 ? 'fallback' : 'function'; text += ' '.repeat(4) + `${fallback}() external payable {\n`; text += solStmts(this.main, 8); text += ' '.repeat(4) + '}\n\n'; for (const [, fn] of Object.entries(this.functions)) { text += solPublicFunction(fn); } text += '}\n'; return text; } declare module '.' { interface Contract { /** * Decompiles the `Contract` into Solidity-like source code. */ solidify(...args: Parameters<typeof solContract>): string; } } declare module './ast' { interface Tag { /** */ sol(): string; } } Contract.prototype.solidify = solContract; Tag.prototype.sol = function (this: Expr) { return solExpr(this); };