UNPKG

sevm

Version:

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

234 lines (218 loc) 9.14 kB
import { Contract } from '.'; import { fnsig, parseSig } from './abi'; import type { Expr, Inst, MappingLoad, MappingStore, Stmt } from './ast'; import { FNS, Tag, Val, isExpr, isInst } from './ast'; /** * Returns the Yul `string` representation of `nodes` that are either * `Expr`essions or `Inst`ructions. * * https://docs.soliditylang.org/en/v0.8.21/yul.html */ export function yul(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) ? yulExpr(node) : isInst(node) ? yulInst(node) : `${node}`; result.push(str, strings[i + 1]); }); return result.join(''); } function yulExpr(expr: Expr): string { switch (expr.tag) { case 'Val': return `${expr.isJumpDest() ? '[J]' : ''}0x${expr.val.toString(16)}`; case 'Local': return `local${expr.index}`; case 'Add': case 'Mul': case 'Sub': case 'Div': case 'Mod': case 'Exp': case 'Lt': case 'Gt': case 'Eq': case 'And': case 'Or': case 'Xor': return yul`${expr.tag.toLowerCase()}(${expr.left}, ${expr.right})`; case 'IsZero': case 'Not': return yul`${expr.tag.toLowerCase()}(${expr.value})`; case 'Byte': return yul`byte(${expr.pos}, ${expr.data})`; case 'Shl': case 'Shr': case 'Sar': return yul`${expr.tag.toLowerCase()}(${expr.value}, ${expr.shift})`; case 'Sig': return `eq(msg.sig, ${expr.selector})`; case 'CallValue': return 'callvalue()'; case 'CallDataLoad': return yul`calldataload(${expr.location})`; case 'Prop': switch (expr.symbol) { case 'msg.sender': return 'caller()'; case 'msg.data.length': return 'calldatasize()'; case 'msize()': return expr.symbol; default: { const i = expr.symbol.indexOf('.'); return (i >= -1 ? expr.symbol.substring(i + 1) : expr.symbol) + '()'; } } case 'Fn': return FNS[expr.mnemonic][0](yulExpr(expr.value)); case 'DataCopy': switch (expr.kind) { case 'calldatacopy': return yul`calldatacopy(${expr.offset}, ${expr.size})`; case 'codecopy': return yul`codecopy(${expr.offset}, ${expr.size})`; case 'extcodecopy': return yul`extcodecopy(${expr.address}, ${expr.offset}, ${expr.size})`; case 'returndatacopy': return yul`returndatacopy(${expr.offset}, ${expr.size})`; } case 'MLoad': return yul`mload(${expr.location})`; case 'Sha3': return yul`keccak256(${expr.offset}, ${expr.size} /*${expr.args?.map(yulExpr).join('.') ?? 'no args'}*/)`; case 'Create': // create(v, p, n) | F | create new contract with code mem[p…(p+n)) and send v wei and return the new address; returns 0 on error return yul`create(${expr.value}, ${expr.offset}, ${expr.size})`; case 'Call': // call(g, a, v, in, insize, out, outsize) | F | call contract at address a with input mem[in…(in+insize)) providing g gas and v wei and output area mem[out…(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success See more return yul`call(${expr.gas},${expr.address},${expr.value},${expr.argsStart},${expr.argsLen},${expr.retStart},${expr.retLen})`; case 'ReturnData': throw new Error('Not implemented yet: "ReturnData" case'); case 'CallCode': throw new Error('Not implemented yet: "CallCode" case'); case 'Create2': // create2(v, p, n, s) | C | create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and // send v wei and return the new address, where 0xff is a 1 byte value, // this is the current contract’s address as a 20 byte value and s is a big-endian 256-bit value; returns 0 on error return yul`create2(${expr.value}, ${expr.offset}, ${expr.size})`; case 'StaticCall': // staticcall(g, a, in, insize, out, outsize) return yul`staticcall(${expr.gas}, ${expr.address}, ${expr.memoryStart}, ${expr.memoryLength}, ${expr.outputStart}, ${expr.outputLength})`; case 'DelegateCall': return yul`delegatecall(${expr.gas},${expr.address},${expr.memoryStart},${expr.memoryLength},${expr.outputStart},${expr.outputLength})`; case 'SLoad': return yul`sload(${expr.slot})`; case 'MappingLoad': return yul`sload(${expr.slot}/*base${expr.location}${yulMapArgs(expr)}*/)`; } } function yulInst(inst: Inst): string { switch (inst.name) { case 'Local': return yul`let local${inst.local.index} := ${inst.local.value} // #refs ${inst.local.nrefs}`; case 'Log': return yul`log${inst.topics.length}(${[ inst.offset, inst.size, ...inst.topics ].map(yulExpr).join(', ')})`; case 'MStore': { const location = inst.location.isVal() ? '' : yul`/*=${inst.location.eval()}*/`; return yul`mstore(${inst.location}${location}, ${inst.data})`; } case 'Stop': return 'stop()'; case 'Return': return yul`return(${inst.offset}, ${inst.size}) // ${inst.args?.map(yulExpr).join('.')}`; case 'Revert': return yul`revert(${inst.offset}, ${inst.size})`; case 'SelfDestruct': return yul`selfdestruct(${inst.address})`; case 'Invalid': return 'invalid()'; case 'Jump': return yul`jump(${inst.offset}, ${inst.destBranch.pc})`; case 'Jumpi': return yul`jumpi(${inst.cond}, ${inst.offset}})`; case 'JumpDest': return `jumpdesp(${inst.fallBranch.pc})`; case 'SigCase': return yul`case when ${inst.condition} goto ${inst.offset} or fall ${inst.fallBranch.pc}`; case 'SStore': return yul`sstore(${inst.slot}, ${inst.data})`; case 'MappingStore': return yul`sstore(${inst.slot}, ${inst.data}) /*${inst.location}${yulMapArgs(inst)}*/`; case 'Throw': return `throw('${inst.reason}');`; } } function yulStmt(stmt: Stmt): string { switch (stmt.name) { case 'If': return yul`(${stmt.condition})`; case 'CallSite': return yul`$${stmt.selector}();`; case 'Require': { const args = stmt.selector !== undefined ? [new Val(BigInt('0x' + stmt.selector)), ...stmt.args] : stmt.args; return `require(${[stmt.condition, ...args].map(yulExpr).join(', ')})`; } default: return yulInst(stmt); } } function yulMapArgs(mapping: MappingLoad | MappingStore): string { return mapping.items.map(e => yul`[${e}]`).join(''); } export function yulStmts(stmts: Stmt[], spaces = 0): string { let text = ''; for (const stmt of stmts) { if (stmt.name === 'If') { const condition = yulStmt(stmt); text += ' '.repeat(spaces) + 'if ' + condition + ' {\n'; text += yulStmts(stmt.trueBlock!, spaces + 4); if (stmt.falseBlock) { text += ' '.repeat(spaces) + '} else {\n'; text += yulStmts(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) + yulStmt(stmt) + '\n'; } } return text; } declare module '.' { interface Contract { /** * @returns */ yul(): string; } } Contract.prototype.yul = function (this: Contract) { let text = ''; text += 'object "runtime" {\n'; text += ' code {\n'; text += yulStmts(this.main, 8); text += '\n'; for (const [selector, fn] of Object.entries(this.functions)) { const name = fn.label !== undefined ? fnsig(parseSig(fn.label)) : `__$${selector}(/*unknown*/)`; const view = fn.constant ? ' view' : ''; const payable = fn.payable ? ' payable' : ''; text += ' '.repeat(8) + `function ${name} { // public${view}${payable}\n`; text += yulStmts(fn.stmts, 12); text += ' '.repeat(8) + '}\n'; text += '\n'; } text += ' }\n'; text += '}\n'; return text; }; declare module './ast' { interface Tag { /** */ yul(): string; } } Tag.prototype.yul = function (this: Expr) { return yulExpr(this); };