sevm
Version:
A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI
577 lines • 21.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.solMappings = exports.solStructs = exports.solVars = exports.solEvents = exports.solStmts = exports.sol = void 0;
const _1 = require(".");
const abi_1 = require("./abi");
const ast_1 = require("./ast");
const special_1 = require("./ast/special");
/**
*
* @param strings
* @param nodes
* @returns
*/
function sol(strings, ...nodes) {
const result = [strings[0]];
nodes.forEach((node, i) => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
const str = (0, ast_1.isExpr)(node) ? solExpr(node) : (0, ast_1.isInst)(node) ? solStmt(node) : `${node}`;
result.push(str, strings[i + 1]);
});
return result.join('');
}
exports.sol = sol;
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],
};
const prec = (expr) => {
const tag = expr.tag;
if (tag in OPS) {
return OPS[tag][1];
}
else {
return 16;
}
};
function paren(expr, exprc) {
return prec(expr) < prec(exprc) ? sol `(${expr})` : sol `${expr}`;
}
function solExpr(expr) {
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 special_1.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) {
if (sig === undefined)
return undefined;
try {
return (0, abi_1.parseSig)(sig).name;
}
catch {
return sig;
}
}
function solInst(inst) {
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) => 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) {
return args?.length === 3 && args.every(arg => arg.isVal());
}
function hex2a(hexstr) {
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 }, selector) {
args = args?.map(ast_1.evalE);
return selector === ast_1.Revert.ERROR && isStringReturn(args) && args[0].val === 32n
? `"${hex2a(args[2].val.toString(16))}"`
: undefined;
}
function solStmt(stmt) {
switch (stmt.name) {
case 'If':
return sol `(${stmt.condition})`;
case 'CallSite':
return sol `$${stmt.selector}();`;
case 'Require': {
const fnname = stmt.selector === ast_1.Revert.PANIC ? 'assert' : 'require';
const args = stmt.selector === undefined || stmt.selector === ast_1.Revert.PANIC
? stmt.args
: [new ast_1.Val(BigInt('0x' + stmt.selector)), ...stmt.args];
const revertMsg = getRevertMsg(stmt, ast_1.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
*/
function solStmts(stmts, spaces = 0) {
let text = '';
for (const stmt of stmts) {
if (stmt instanceof ast_1.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;
}
exports.solStmts = solStmts;
/**
*
* @param events
* @returns
*/
function solEvents(events, 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;
}
exports.solEvents = solEvents;
/**
*
* @param variables
* @returns
*/
function solVars(variables) {
let output = '';
[...variables.entries()].forEach(([hash, variable], index) => {
const types = 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;
}
exports.solVars = solVars;
/**
*
* @param mappings
* @returns
*/
function solStructs(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;
}
exports.solStructs = solStructs;
/**
*
* @param mappings
* @returns
*/
function solMappings(mappings) {
let output = '';
Object.keys(mappings).forEach((key, index) => {
const mapping = mappings[key];
const label = mapping.name ? `public ${mapping.name}` : `mapping${index + 1}`;
output += ` ${solMapping(mapping)} ${label};\n`;
});
return output;
function solMapping(mapping) {
const mappingKey = [];
const mappingValue = [];
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('|') + ')';
}
}
exports.solMappings = solMappings;
/**
*
* @returns the decompiled text for `this` function.
*/
function solPublicFunction(self, tab = ' ') {
let output = tab + 'function ' + (self.label !== undefined
? (0, abi_1.fnsig)((0, abi_1.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) {
let output = '';
for (const [selector, decl] of Object.entries(reverts)) {
if (!ast_1.Revert.isRequireOrAssert(selector)) {
if (decl.sig === undefined)
output += ` // error ${selector}\n`;
else
output += ` error ${decl.sig}; // ${selector}\n`;
}
}
return output;
}
function solContract(options = {}) {
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) => 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;
}
_1.Contract.prototype.solidify = solContract;
ast_1.Tag.prototype.sol = function () {
return solExpr(this);
};
//# sourceMappingURL=sol.js.map