sevm
Version:
A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI
435 lines (381 loc) • 14.5 kB
text/typescript
import { CallSite, If, Require, Throw, type Expr, type Inst, type Stmt, type Val, reduce, MStore } from './ast';
import { IsZero } from './ast/alu';
import type { IEvents } from './ast/log';
import { Variable, type IStore, type MappingLoad, type SLoad } from './ast/storage';
import type { IReverts, Return, Revert } from './ast/system';
import { arrayify } from './.bytes';
import ERCs from './ercs';
import { EVM } from './evm';
import { splitMetadataHash, type Metadata } from './metadata';
import { State } from './state';
import { Shanghai, type Members, type Opcode } from './step';
import type { Type } from './abi';
/**
*
*/
export const ERCIds = Object.keys(ERCs);
/**
*
*/
export class Contract {
/**
* The `bytecode` used to create this `Contract`.
*/
readonly bytecode: Uint8Array;
/**
* The `metadataHash` part from the `bytecode`.
* That is, if present, the `bytecode` without its `code`.
*/
readonly metadata: Metadata | undefined;
/**
*
*/
readonly main: Stmt[];
readonly events: IEvents = {};
readonly variables: IStore['variables'] = new Map();
readonly mappings: IStore['mappings'] = {};
readonly functionBranches: Members['functionBranches'] = new Map();
readonly reverts: IReverts = {};
/**
* Symbolic execution `errors` found during interpretation of `this.bytecode`.
*/
readonly errors: EVM<string>['errors'];
readonly blocks: EVM<string>['blocks'];
readonly chunks: EVM<string>['chunks'];
/**
* Returns the `opcode`s present in the **reacheable blocks** of `this` Contract's `bytecode`.
*/
readonly opcodes: () => Opcode<string>[];
/**
*
*/
readonly functions: { [selector: string]: PublicFunction } = {};
readonly payable: boolean;
/**
*
* @param bytecode the bytecode to analyze in hexadecimal format.
*/
constructor(bytecode: Parameters<typeof arrayify>[0], step = new Shanghai(), main = new State<Inst, Expr>()) {
this.bytecode = arrayify(bytecode);
const evm = new EVM(this.bytecode, step);
evm.run(0, main);
this.main = build(main);
this.payable = !requiresNoValue(this.main, true);
for (const [selector, branch] of evm.step.functionBranches) {
evm.run(branch.pc, branch.state);
this.functions[selector] = new PublicFunction(this, build(branch.state), selector);
}
this.events = evm.step.events;
this.variables = evm.step.variables;
this.mappings = evm.step.mappings;
this.functionBranches = evm.step.functionBranches;
this.reverts = evm.step.reverts;
this.metadata = splitMetadataHash(this.bytecode).metadata;
this.errors = evm.errors;
this.blocks = evm.blocks;
this.chunks = () => evm.chunks();
this.opcodes = () => evm.opcodes();
}
reduce(): Contract {
const obj = Object.create(Contract.prototype) as object;
const contract = Object.assign(obj, this);
(contract as { main: unknown }).main = reduce(this.main);
(contract as { payable: unknown }).payable = !requiresNoValue(contract.main);
(contract as { functions: unknown }).functions = {};
for (const [selector, fn] of Object.entries(this.functions)) {
const newfn = new PublicFunction(contract, reduce(fn.stmts), selector as string, fn.payable);
newfn.label = fn.label;
contract.functions[selector] = newfn;
}
return contract;
}
/**
*
* @returns
*/
getFunctions(): string[] {
return Object.values(this.functions).flatMap(fn =>
fn.label === undefined ? [] : [fn.label]
);
}
/**
*
* @returns
*/
getEvents(): string[] {
return Object.values(this.events).flatMap(event =>
event.sig === undefined ? [] : [event.sig]
);
}
// getABI() {
// return Object.values(this.contract).map(fn => {
// return {
// type: 'function',
// name: fn.label.split('(')[0],
// payable: fn.payable,
// constant: fn.constant,
// };
// });
// }
/**
* https://eips.ethereum.org/EIPS/eip-165
* https://eips.ethereum.org/EIPS/eip-20
* https://eips.ethereum.org/EIPS/eip-20
* https://eips.ethereum.org/EIPS/eip-721
*
* @param ercid
* @returns
*/
isERC(ercid: (typeof ERCIds)[number], checkEvents = true): boolean {
return (
ERCs[ercid].selectors.every(s => this.functionBranches.has(s)) &&
(!checkEvents || ERCs[ercid].topics.every(t => t in this.events))
);
}
}
export function isRevertBlock(falseBlock: Stmt[]): falseBlock is [...Inst[], Revert] {
return (
falseBlock.length >= 1 &&
falseBlock.slice(0, -1).every(stmt => stmt.name === 'Local' || stmt.name === 'MStore') &&
falseBlock.at(-1)!.name === 'Revert' &&
(falseBlock.at(-1) as Revert).isRequireOrAssert()
);
}
export class PublicFunction {
/**
*
*/
private _label: string | undefined = undefined;
readonly payable: boolean;
readonly visibility: string;
readonly constant: boolean;
readonly returns: string[] = [];
constructor(
readonly contract: Contract,
readonly stmts: Stmt[],
readonly selector: string,
payable?: boolean,
) {
this.visibility = 'public';
if (payable === undefined) {
if (requiresNoValue(this.stmts)) {
this.payable = false;
// this.stmts.shift();
} else if (!contract.payable) {
this.payable = false;
} else {
this.payable = true;
}
} else {
this.payable = payable;
}
this.constant = this.stmts.length === 1 && this.stmts[0].name === 'Return';
const returns: Expr[][] = [];
PublicFunction.findReturns(stmts, returns);
if (
returns.length > 0 &&
returns.every(
args =>
args.length === returns[0].length &&
args.map(arg => arg.type).join('') === returns[0].map(arg => arg.type).join('')
)
) {
returns[0].forEach(arg => {
if (arg.isVal()) {
this.returns.push('uint256');
} else if (arg.type) {
this.returns.push(arg.type);
} else {
this.returns.push('unknown');
}
});
} else if (returns.length > 0) {
this.returns.push('<unknown>');
}
}
get label(): string | undefined {
return this._label;
}
set label(value: string | undefined) {
this._label = value;
if (value !== undefined) {
const functionName = value.split('(')[0];
if (this.isGetter()) {
const ret = this.stmts.at(-1) as Return & { args: [SLoad & { slot: Val }] };
const location = ret.args[0].slot.val;
const variable = this.contract.variables.get(location);
if (variable !== undefined) {
variable.label = functionName;
} else {
this.contract.variables.set(location, new Variable(functionName, [], this.contract.variables.size + 1));
}
// this.contract.variables[this.selector] = new Variable(
// functionName,
// variable ? variable.types : []
// );
}
if (this.isMappingGetter()) {
const ret = this.stmts.at(-1) as Return & { args: MappingLoad[] };
const location = ret.args[0].location;
this.contract.mappings[location].name = functionName;
}
const paramTypes = value.replace(functionName, '').slice(1, -1).split(',');
if (paramTypes.length > 1 || (paramTypes.length === 1 && paramTypes[0] !== '')) {
this.stmts.forEach(stmt =>
PublicFunction.patchCallDataLoad(
stmt as unknown as Record<string, Expr>,
paramTypes as Type[]
)
);
}
}
}
private isGetter(): this is { stmts: [...Stmt[], Return & { args: [SLoad & { slot: Val }] }] } {
const exit = this.stmts.at(-1)!;
return (
this.stmts.length >= 1 &&
this.stmts.slice(0, -1).every(stmt => stmt.name === 'Local' || stmt.name === 'MStore' || stmt.name === 'Require') &&
exit.name === 'Return' &&
exit.args !== undefined &&
exit.args.length === 1 &&
exit.args[0].tag === 'SLoad' &&
exit.args[0].slot.isVal()
);
}
private isMappingGetter(): this is { stmts: [...Stmt[], Return & { args: MappingLoad[] }] } {
const exit = this.stmts.find(stmt => stmt.name !== 'Local' && stmt.name !== 'MStore' && stmt.name !== 'Require');
return (
exit !== undefined &&
this.stmts.at(-1) === exit &&
exit.name === 'Return' &&
exit.args !== undefined &&
exit.args.every(arg => arg.tag === 'MappingLoad')
);
}
private static findReturns(stmts: Stmt[], returns: Expr[][]) {
for (const stmt of stmts) {
if (stmt.name === 'Return' && stmt.args && stmt.args.length > 0) {
returns.push(stmt.args);
} else if (stmt.name === 'If') {
[stmt.trueBlock, stmt.falseBlock].forEach(stmts => {
if (stmts !== undefined) {
PublicFunction.findReturns(stmts, returns);
}
});
}
}
}
private static patchCallDataLoad(stmtOrExpr: Record<string, Expr>, paramTypes: Type[], visited = new Set()) {
if (stmtOrExpr === null || visited.has(stmtOrExpr)) return;
visited.add(stmtOrExpr);
for (const propKey in stmtOrExpr) {
if (propKey === 'mappings') continue;
if (Object.prototype.hasOwnProperty.call(stmtOrExpr, propKey)) {
const expr = stmtOrExpr[propKey];
if (expr && expr.tag === 'CallDataLoad' && expr.location.isVal()) {
const argNumber = Number((expr.location.val - 4n) / 32n);
expr.type = paramTypes[argNumber];
}
if (
typeof expr === 'object' &&
!(expr instanceof Variable) &&
!(expr instanceof Throw)
) {
PublicFunction.patchCallDataLoad(
expr as unknown as Record<string, Expr>,
paramTypes,
visited
);
}
}
}
}
}
export function build(state: State<Inst, Expr>): Stmt[] {
const visited = new WeakSet();
const res = buildState(state);
// mem(res);
return res;
function buildState(state: State<Inst, Expr>): Stmt[] {
if (visited.has(state)) {
return [];
}
visited.add(state);
const last = state.last!;
if (last === undefined) return [];
for (let i = 0; i < state.stmts.length; i++) {
// state.stmts[i] = state.stmts[i].eval();
}
switch (last.name) {
case 'Jumpi': {
if (last.evalCond.isVal()) {
if (last.evalCond.val === 0n) {
return [...state.stmts.slice(0, -1), ...buildState(last.fallBranch.state)];
} else {
return [...state.stmts.slice(0, -1), ...buildState(last.destBranch.state)];
}
}
const trueBlock = buildState(last.destBranch.state);
const falseBlock = buildState(last.fallBranch.state);
return [
...state.stmts.slice(0, -1),
...(isRevertBlock(falseBlock)
? [
new Require(
// last.cond.eval(),
last.cond,
// ((falseBlock.at(-1) as Revert).args ?? []).map(e => e.eval())
(falseBlock.at(-1) as Revert).selector,
(falseBlock.at(-1) as Revert).args ?? []
),
...trueBlock,
]
: [new If(new IsZero(last.cond), falseBlock), ...trueBlock]),
];
}
case 'SigCase': {
const falseBlock = buildState(last.fallBranch.state);
return [
...state.stmts.slice(0, -1),
new If(last.condition, [new CallSite(last.condition.selector)], falseBlock),
];
}
case 'Jump':
return [...state.stmts.slice(0, -1), ...buildState(last.destBranch.state)];
case 'JumpDest':
return [...state.stmts.slice(0, -1), ...buildState(last.fallBranch.state)];
default:
return state.stmts;
}
}
}
export function reduce0(stmts: Inst[]): Stmt[] {
const result = [];
for (const stmt of stmts) {
if (stmt.name !== 'Local' || stmt.local.nrefs > 0) {
result.push(stmt);
}
}
return result;
}
/**
*
* @param stmts
* @param allowMStoreInit
* @returns
*/
function requiresNoValue(stmts: Stmt[], allowMStoreInit = false): boolean {
stmts = allowMStoreInit && stmts[0] instanceof MStore ? stmts.slice(1) : stmts;
const first = stmts.find(stmt => stmt.name !== 'Local');
return first instanceof Require && (first =>
first.condition.tag === 'IsZero' &&
first.condition.value.tag === 'CallValue'
)(first.eval());
}
export * from './abi';
export * from './evm';
export * from './metadata';
export * from './sol';
export * from './state';
export * from './step';
export * from './yul';