sevm
Version:
A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI
337 lines (307 loc) • 12.2 kB
JavaScript
/* eslint-env node */
import c from 'ansi-colors';
import assert from 'assert';
import envPaths from 'env-paths';
import { EtherscanProvider } from 'ethers';
import { existsSync, mkdirSync, promises, readFileSync, writeFileSync } from 'fs';
import path from 'path';
import yargs from 'yargs';
import { Contract, sol, yul } from 'sevm';
import 'sevm/4bytedb';
const paths = envPaths('sevm');
const { underline, blue, dim, magenta, red, cyan: info, yellow: warn } = c;
/**
* @typedef {import('sevm').State<import('sevm/ast').Inst, import('sevm/ast').Expr>} EVMState
*/
/**
* @param {Contract} contract
* @param {import('yargs').ArgumentsCamelCase} argv
*/
function dis(contract, argv) {
const MAX_STACK = 10;
console.info(`${dim('pc'.padStart(5))} ${magenta('opcode')} ${'push data (PUSHx)'}`);
for (const chunk of contract.chunks()) {
console.info(c.blue(chunk.pcbegin.toString()), ':', chunk.states === undefined ? red('unreachable') : '');
if (chunk.content instanceof Uint8Array) {
console.info(Buffer.from(chunk.content).toString('hex'));
} else {
const block = contract.blocks.get(chunk.pcbegin);
for (const { opcode, stack } of block?.opcodes ?? []) {
const pc = opcode.pc.toString().padStart(5);
const pushData = opcode.data
? (opcode.mnemonic.length === 5 ? ' ' : '') + `0x${opcode.hexData()}`
: '';
let values;
if (argv['with-stack']) {
values = info('〒 ');
values += stack === undefined
? warn('<no stack>')
: stack.values.slice(0, MAX_STACK).map(e => yul`${e}`).join(dim('|')) +
(stack.values.length > MAX_STACK ? dim(`| ..${stack.values.length - MAX_STACK} more items`) : '');
} else {
values = '';
}
console.info(`${dim(pc)} ${magenta(opcode.mnemonic)} ${pushData} ${values}`);
}
}
if (argv['with-trace'] && chunk.states !== undefined) {
for (const state of chunk.states) {
console.info('state', '〒 ', state.stack.values.map(e => yul`${e}`).join(' | '));
state.stmts.forEach(stmt => console.info(' ', yul`${stmt}`));
}
}
}
}
/**
* @param {string} pathOrAddress
* @returns {Promise<string | null>}
*/
async function getBytecode(pathOrAddress) {
const cacheFolder = path.join(paths.cache, 'mainnet');
const cachePath = path.join(cacheFolder, `${pathOrAddress}.bytecode`);
const readInputFile = async () => {
if (pathOrAddress === '') {
const buffer = readFileSync(process.stdin.fd, 'utf-8').trim();
if (buffer !== '') return Promise.resolve(buffer);
throw new Error('No input from stdin');
}
return await promises.readFile(pathOrAddress, 'utf8');
};
/** @param {unknown} field */
const fromJSONField = field =>
field !== null && typeof field === 'object' && 'object' in field && typeof field['object'] === 'string'
? field['object']
: typeof field === 'string'
? field
: null;
const tries = [
async () => {
const text = await readInputFile();
let json;
try {
json = JSON.parse(text);
} catch (e) {
return text;
}
const { deployedBytecode, bytecode } = json;
const value = fromJSONField(deployedBytecode) ?? fromJSONField(bytecode);
if (value !== null)
return value;
throw new Error('Cannot find `deployedBytecode`|`bytecode` in json file');
},
() => promises.readFile(cachePath, 'utf8'),
async () => {
const provider = new EtherscanProvider();
const bytecode = await provider.getCode(pathOrAddress);
if (!existsSync(cacheFolder)) {
mkdirSync(cacheFolder, { recursive: true });
}
writeFileSync(cachePath, bytecode, 'utf8');
return bytecode;
},
];
for (const fn of tries) {
try {
return await fn();
} catch (_err) {
// console.log(_err);
}
}
return null;
}
/** @param {(contract: Contract, argv: import('yargs').ArgumentsCamelCase) => void} handler */
function make(handler) {
/** @param {import('yargs').ArgumentsCamelCase} argv */
return async argv => {
const pathOrAddress = /** @type {string} */ (argv['contract']);
const bytecode = await getBytecode(pathOrAddress);
if (bytecode !== null) {
const contract = new Contract(bytecode).patchdb();
handler(contract, argv);
} else {
console.info(warn(`Cannot find bytecode for ${info(pathOrAddress)}`));
process.exit(1);
}
};
}
/** @param {import('yargs').Argv} argv */
const pos = argv => argv.positional('contract', {
type: 'string',
describe: 'path or address where to locate the bytecode of the contract',
});
void yargs(process.argv.slice(2))
.scriptName('sevm')
.usage('$0 <cmd> <contract>')
.command('metadata <contract>', 'Shows the Metadata of the contract[1]', pos, make(contract => {
console.info(underline('Contract Metadata'));
if (contract.metadata) {
console.info(blue('protocol'), contract.metadata.protocol);
console.info(blue('hash'), contract.metadata.hash);
console.info(blue('solc'), contract.metadata.solc);
console.info(blue('url'), contract.metadata.url);
} else {
console.info(warn('No metadata'));
}
}))
.command('abi <contract>', 'Shows the ABI of the contract[2]', pos, make(contract => {
console.info(underline('Function Selectors'));
contract.getFunctions().forEach(sig => console.info(' ', blue(sig)));
console.info();
console.info(underline('Events'));
contract.getEvents().forEach(sig => console.info(' ', magenta(sig)));
}))
.command('selectors <contract>', 'Shows the function selectors of the contract[3]', pos, make(contract => {
for (const [selector, fn] of Object.entries(contract.functions)) {
console.info(
c.cyan('0x' + selector),
fn.label === undefined ? warn('<no signature>') : fn.label
);
}
}))
.command('dis <contract>', 'Disassemble the bytecode into Opcodes', argv => pos(argv)
.option('with-stack', {
description: 'Include the current stack next to each decoded opcode',
})
.option('with-trace', {
description: 'Include the trace of staments at the end of each basic block',
}), make(dis))
.command(
'cfg <contract>',
'Writes the cfg of the selected function in `dot` format into standard output',
pos,
make(cfg)
)
.command('sol <contract>', "Decompile the contract into Solidity-like source", pos, make(contract => {
console.info(contract.solidify());
}))
.command('yul <contract>', "Decompile the contract into Yul-like source[4]", pos, make(contract => {
console.info(contract.yul());
}))
.command('config', 'Shows cache path used to store downloaded bytecode', {}, () =>
console.info(paths.cache)
)
.middleware(argv => {
if (!argv['color']) {
c.enabled = false;
}
})
.option('color', {
type: 'boolean',
description: 'Display with colors, use `--no-color` to deactivate colors',
default: true,
})
// .option('selector', {
// alias: 's',
// type: 'string',
// description:
// 'Function signature, e.g., `balanceOf(address)` or selector hash to choose a specific function',
// })
.demandCommand(1, 'At least one command must be specified')
.recommendCommands()
.example(
'$0 abi 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
'shows the ABI of the ENS registry contract'
)
.example(
'$0 decompile 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
'decompiles the ENS registry contract'
)
.epilog(
`[1] See https://docs.soliditylang.org/en/latest/metadata.html for more information regarding Metadata generated by the Solidity compiler.
[2] See https://docs.soliditylang.org/en/latest/abi-spec.html#abi-json for more information regarding the ABI specification.
[3] See https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector for more information regarding Function Selectors
[4] See https://docs.soliditylang.org/en/latest/yul.html for more information regarding Yul.`
)
.help().argv;
/** @param {Contract} contract */
function cfg(contract) {
/** @type {WeakMap<EVMState, string>} */
const ids = new WeakMap();
let id = 0;
for (const block of contract.blocks.values()) {
for (const state of block.states) {
assert(!ids.has(state));
if (!ids.has(state)) {
ids.set(state, `id-${id}`);
id++;
}
}
}
const write = console.log;
write(`digraph G {
color="#efefef";
#rankdir = LR;
#graph[fontsize=6];
node[shape=box style=filled fontsize=12 fontname="Verdana" fillcolor="#efefef"];
`);
let edges = '';
for (const [pc, block] of contract.blocks) {
write(`subgraph cluster_${pc} {`);
write(` style=filled;`);
let label = `pc ${pc}\\l`;
// for (let i = pc; i < chunk.pcend; i++) {
// const opcode = evm.opcodes[i];
// label += opcode.formatOpcode() + '\\l';
// }
write(` label = "${label}";`);
for (const state of block.states) {
writeNode(pc, state);
switch (state.last?.name) {
case 'Jumpi':
writeEdge(state, state.last.destBranch);
writeEdge(state, state.last.fallBranch);
break;
case 'SigCase':
// writeEdge(pc, state.last.condition.hash);
writeEdge(state, state.last.fallBranch);
break;
case 'Jump':
writeEdge(state, state.last.destBranch);
break;
case 'JumpDest':
writeEdge(state, state.last.fallBranch);
break;
default:
}
}
write('}\n');
}
write(edges);
write('}');
/**
* @param {number} pc
* @param {EVMState} state
*/
function writeNode(pc, state) {
const id = ids.get(state);
let label = `key:${pc} ${id}`;
label += '\\l';
// label += 'doms: ' + [...doms].join(', ');
// label += '\\l';
// if (tree) {
// label += 'tree: ' + [...tree].join(', ');
// label += '\\l';
// }
// label += block.entry.state.stack.values.map(elem => `=| ${elem.toString()}`).join('');
// label += '\\l';
// label += block.opcodes.map(op => formatOpcode(op)).join('\\l');
// label += '\\l';
label += state.stack.values.map(elem => sol`=| ${elem.eval()}`).join('');
label += '\\l';
// label += inspect(state.memory);
// label += '\\l';
Object.entries(state.memory).forEach(([k, v]) => (label += sol`${k}: ${v}\\l`));
label += state.stmts.map(stmt => sol`${stmt}`).join('\\l');
label += '\\l';
write(`"${id}" [label="${label}" fillcolor="${'#ffa500'}"];`);
}
/**
* @param {EVMState} src
* @param {import('sevm/ast').Branch} branch
*/
function writeEdge(src, branch) {
const id = ids.get(src);
edges += `"${id}" -> "${ids.get(branch.state)}";\n`;
}
}