sevm
Version:
A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI
234 lines (218 loc) • 9.14 kB
text/typescript
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);
};