UNPKG

sevm

Version:

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

997 lines (888 loc) 37.5 kB
import { arrayify, hexlify } from './.bytes'; import { ExecError, type Operand, type Ram, type State } from './state'; import type { DataCopy, Expr, IReverts, IEvents, IStore, Inst, Jumpi, Props } from './ast'; import * as ast from './ast'; import { Sha3, Val, Sig } from './ast'; /** * Represents an opcode found in the bytecode augmented with * offset and operand information as defined by the EVM. * * It can be either a unary opcode, _which does not take any operand data_, * or either a `PUSHn` mnemonic augmented with its push `data`. * That is, all but `PUSHn` `n >= 1` opcodes are unary opcodes. * * `PUSHn` `n >= 1` opcodes takes an `n`-byte argument from the bytecode. * Note that `PUSH0`[^1] does not take any data argument from the bytecode (just pushes `0` onto the `Stack`). * Thus it can be considered as an unary opcode. * * [^1]: https://eips.ethereum.org/EIPS/eip-3855 */ export class Opcode<M extends string = string> { constructor( /** * This is the offset in the bytecode where this `Opcode` was found. * Both jump instructions, _i.e._, `JUMP` and `JUMPI`, * expects a stack operand referencing this `offset` in the bytecode. */ /** * The Program Counter of this `Opcode`. * The index in the `Opcode[]` where this `Opcode` is inserted. */ readonly pc: number, /** * Any byte number, _i.e._, between 0 and 255 representing the opcode byte. * The `opcode` may not be a valid opcode. */ readonly opcode: number, /** * Represents a valid opcode. * * In https://www.evm.codes/ you can find an overview of each EVM opcode. * * If the `opcode` given is not a valid opcode, * you can provide `INVALID` as `mnemonic`. * * A `PUSHn` opcode only permits a `PUSHn` opcode. */ readonly mnemonic: M, /** * A `Unary` opcode does not include any `data`. For these opcodes `data` is `null`. * * If this `Opcode` is a `PUSHn` instruction or contains any operand data, * then it contains the data attached to this instruction. */ readonly data: null | Uint8Array = null ) { } /** * Where the next opcode should be located at. */ get nextpc(): number { return this.pc + (this.data?.length ?? 0) + 1; } /** * Returns the hexadecimal representation of `this.data`. */ hexData(): string | undefined { return this.data === null ? undefined : hexlify(this.data); } /** * Returns a `string` representation of `this` `Opcode`. * Usually used for debugging purposes. * * @param includeDataAsNumeric whether to include `data` as numeric. * @returns the `string` representation of `this` `Opcode`. */ format(includeDataAsNumeric = true): string { const pushData = this.data ? ` 0x${this.hexData()}` + (includeDataAsNumeric ? ` (${parseInt(this.hexData()!, 16)})` : '') : ''; return `${this.mnemonic}(0x${this.opcode.toString(16)})@${this.pc}${pushData}`; } } /** * */ export class Members { readonly events: IEvents = {}; readonly variables: IStore['variables'] = new Map(); readonly mappings: IStore['mappings'] = {}; /** * Store selectors', _i.e._, public and external `function`s program counter entry. */ readonly functionBranches: Map<string, { pc: number, state: State<Inst, Expr> }> = new Map(); /** * */ readonly reverts: IReverts = {}; getError(selector: string): IReverts[string] { let error = this.reverts[selector]; if (error === undefined) { error = {}; this.reverts[selector] = error; } return error; } } /** * This module is used to `decode` bytecode into `Opcode`. * * Maps numeric opcodes (byte between `0` and `255`) to decode configuration and string mnemonic. * That is, for the given `opcode`, * `size` indicates the size of the `opcode`'s operand to consume from bytecode in bytes. * `halts` indicates where the step associated with this `opcode` should `halt` the EVM `State`. * `mnemonic` indicates the step the `EVM` should execute. */ export class Undef<M extends string> extends Members { [opcode: number]: [size: number, halts: boolean, mnemonic: M]; constructor() { super(); Object.assign(this, Object.fromEntries([...Array(256).keys()].map(k => [k, [0, true, 'UNDEF']])) ); } UNDEF = (state: State<Inst, Expr>, op: Opcode): void => state.halt(new ast.Invalid(op.opcode)); /** * Retrieves the `mnemonic` of the steps which `halts` the EVM `State`. */ haltingSteps(): M[] { return [...Array(256).keys()] .map(o => (this as unknown as [unknown, boolean, never][])[o]) .filter(([, halts, mnemonic]) => halts && mnemonic !== 'UNDEF') .map(([, , mnemonic]) => mnemonic); } /** * Retrieves the opcodes by mnemonic. */ opcodes(): { readonly [m in M]: number } { return Object.fromEntries([...Array(256).keys()] .map(o => [this[o][2], o] as const) .filter(([mnemonic,]) => mnemonic !== 'UNDEF') ); } /** * Decodes the input `bytecode` into `Opcode`s. * `bytecode` may be a hexadecimal string, * which may or may not begin with the hex prefix `0x`. * * ### Example * * ```typescript * const opcodes = [...this.decode('0x6003600501')]; * ``` * * @param bytecode hexadecimal string or array of numbers containing the bytecode to decode. * @param begin the byte position where to start decoding the `input` bytecode, * defaults to `0` if not provided. * @returns a generator of the decoded `Opcode`s found in `bytecode`. */ *decode(bytecode: Parameters<typeof arrayify>[0], begin = 0) { const buffer = arrayify(bytecode); for (let pc = begin; pc < buffer.length; pc++) { const opcode = buffer[pc]; const [size, , mnemonic] = this[opcode]; yield new Opcode( pc, opcode, mnemonic, size === 0 ? null : function () { const data = buffer.subarray(pc + 1, pc + size + 1); if (data.length !== size) { const op = new Opcode(pc, opcode, mnemonic, data).format(false); throw new Error(`Trying to get \`${size}\` bytes but got only \`${data.length}\` while decoding \`${op}\` before reaching the end of bytecode`); } pc += size; return data; }(), ); } } } /** * The step transition function. * * The `EVM` executes a `StepFn` transition for each `opcode` found in the `evm.bytecode`. * It should change the `state` accordingly to the `opcode` found. */ export type StepFn = (state: State<Inst, Expr>, opcode: Opcode, evm: { bytecode: Uint8Array }) => void; /** * A collection of `StepFn` transition functions augmented with decoding information. * It is indexed by the `mnemonic` of the `opcode` it should decode. */ type Step = { readonly [mnemonic in string]: readonly [ number | { opcode: number, size?: number, halts?: true }, step: StepFn, ] }; const ALU = { ADDMOD: [0x08, ({ stack }) => { const left = stack.pop(); const right = stack.pop(); const mod = stack.pop(); stack.push(left.isVal() && right.isVal() && mod.isVal() ? new Val((left.val + right.val) % mod.val) : left.isVal() && right.isVal() ? new ast.Mod(new Val(left.val + right.val), mod) : new ast.Mod(new ast.Add(left, right), mod) ); }], MULMOD: [0x09, ({ stack }) => { const left = stack.pop(); const right = stack.pop(); const mod = stack.pop(); stack.push(left.isVal() && right.isVal() && mod.isVal() ? new Val((left.val * right.val) % mod.val) : left.isVal() && right.isVal() ? new ast.Mod(new Val(left.val * right.val), mod) : new ast.Mod(new ast.Mul(left, right), mod) ); }], SIGNEXTEND: [0x0b, ({ stack }) => { const left = stack.pop(); const right = stack.pop(); stack.push(left.isVal() && right.isVal() ? new ast.Val((right.val << (32n - left.val)) >> (32n - left.val)) : left.isVal() ? new ast.Sar(new ast.Shl(right, new ast.Val(32n - left.val)), new ast.Val(32n - left.val)) : new ast.Sar(new ast.Shl(right, new ast.Sub(new Val(32n), left)), new ast.Sub(new Val(32n), left)) ); }], EQ: [0x14, ({ stack }) => { const DIVEXPsig = (left: Expr, right: Expr): Sig | undefined => { left = left.eval(); right = right.eval(); if (left.isVal() && right.tag === 'Div' && right.right.isVal()) { const selector = left.val * right.right.val; right = right.left; if (selector % (1n << 0xe0n) === 0n && right.tag === 'CallDataLoad' && right.location.isZero()) { return new ast.Sig(selector .toString(16) .substring(0, 8 - (64 - selector.toString(16).length)) .padStart(8, '0') ); } } return undefined; }; const left = stack.pop(); const right = stack.pop(); stack.push(left.isVal() && right.isVal() ? left.val === right.val ? new Val(1n) : new Val(0n) : DIVEXPsig(left, right) ?? DIVEXPsig(right, left) ?? new ast.Eq(left, right) ); }], ISZERO: [0x15, ({ stack }) => { const value = stack.pop(); stack.push(new ast.IsZero(value)); }], NOT: [0x19, ({ stack }) => { const value = stack.pop(); stack.push(new ast.Not(value)); }], BYTE: [0x1a, ({ stack }) => { const position = stack.pop(); const data = stack.pop(); stack.push(new ast.Byte(position, data)); }], } satisfies { [m: string]: [opcode: number, (state: Operand<Expr>) => void] }; const prop = (symbol: keyof typeof Props) => ({ stack }: Operand<Expr>) => stack.push(ast.Props[symbol]); const SPECIAL = { COINBASE: [0x41, prop('block.coinbase')], TIMESTAMP: [0x42, prop('block.timestamp')], NUMBER: [0x43, prop('block.number')], DIFFICULTY: [0x44, prop('block.difficulty')], GASLIMIT: [0x45, prop('block.gaslimit')], CALLER: [0x33, prop('msg.sender')], CALLDATASIZE: [0x36, prop('msg.data.length')], ORIGIN: [0x32, prop('tx.origin')], GASPRICE: [0x3a, prop('tx.gasprice')], ADDRESS: [0x30, prop('address(this)')], CODESIZE: [0x38, prop('codesize()')], RETURNDATASIZE: [0x3d, prop('returndatasize()')], GAS: [0x5a, prop('gasleft()')], CALLVALUE: [0x34, ({ stack }) => stack.push(new ast.CallValue())], CALLDATALOAD: [0x35, ({ stack }) => stack.push(new ast.CallDataLoad(stack.pop()))], } satisfies { [m: string]: readonly [opcode: number, (state: Operand<Expr>) => void] }; const datacopy = (kind: DataCopy['kind']) => function datacopy({ stack, memory }: Ram<Expr>, address?: Expr) { const dest = stack.pop(); const offset = stack.pop(); const size = stack.pop(); if (!dest.isVal()) { // throw new Error('expected number in returndatacopy'); } else { memory.set(dest.val, new ast.DataCopy(kind, offset, size, address)); } // stmts.push(new MStore(location, data)); }; const DATACOPY = { CALLDATACOPY: [0x37, state => datacopy('calldatacopy')(state)], CODECOPY: [0x39, function codecopy({ stack, memory }, _opcode, { bytecode }) { const dest = stack.pop().eval(); const offset = stack.pop(); const size = stack.pop(); if (!dest.isVal() || dest.val >= 1024 * 32) { throw new ExecError('Memory destination for CODECOPY is not reducible to Val'); } else { memory.set(dest.val, new ast.DataCopy('codecopy', offset, size, undefined, ((offset, size) => offset.isVal() && size.isVal() ? bytecode.subarray(Number(offset.val), Number(offset.val + size.val)) : undefined )(offset.eval(), size.eval()) )); } }], EXTCODECOPY: [0x3c, state => { const address = state.stack.pop(); datacopy('extcodecopy')(state, address); }], RETURNDATACOPY: [0x3e, state => datacopy('returndatacopy')(state)], } satisfies Step; const MAXSIZE = 32 * 1024; function memArgs<T>( { stack, memory }: Ram<Expr>, Klass: new (offset: Expr, size: Expr, args?: Expr[]) => T ): T { const offset_ = stack.pop(); const size_ = stack.pop(); return new Klass(offset_, size_, ((offset, size) => { if (offset.isVal() && size.isVal() && size.val <= MAXSIZE) { return new ArgsFetcher(memory).range(offset, size).args; } else { if (size.isVal() && size.val > MAXSIZE) { throw new ExecError(`Memory size too large creating ${Klass.name}: ${size.val} in \`${size_.yul()}\``); } return undefined; } })(offset_.eval(), size_.eval())); } const getVar = (slot: Expr, values: Expr[], self: IStore) => { let variable; if (slot.isVal()) { variable = self.variables.get(slot.val); if (variable === undefined) { variable = new ast.Variable(null, values, self.variables.size + 1); self.variables.set(slot.val, variable); } else { variable.types.push(...values); } } return variable; }; const STORAGE = { SLOAD: [0x54, function sload(this: IStore, { stack }) { const slot = stack.pop(); let base: number | undefined, parts: Expr[]; const check = (slot: Sha3): boolean => ([base, parts] = parseSha3(slot), base !== undefined && parts.length > 0); if (slot.tag === 'Sha3' && check(slot)) { stack.push(new ast.MappingLoad(slot, this.mappings, base!, parts!)); } else if (slot.tag === 'Add' && slot.left.tag === 'Sha3' && slot.right.isVal() && check(slot.left)) { stack.push(new ast.MappingLoad(slot, this.mappings, base!, parts!, slot.right.val)); } else if (slot.tag === 'Add' && slot.left.isVal() && slot.right.tag === 'Sha3' && check(slot.right)) { stack.push(new ast.MappingLoad(slot, this.mappings, base!, parts!, slot.left.val)); } else { stack.push(new ast.SLoad(slot, getVar(slot.eval(), [], this))); } }], SSTORE: [0x55, function sstore(this: IStore, { stack, stmts }) { const slot = stack.pop(); const value = stack.pop(); if (slot.tag === 'Local') slot.nrefs--; let base: number | undefined, parts: Expr[]; const check = (slot: Sha3): boolean => ([base, parts] = parseSha3(slot), base !== undefined && parts.length > 0); if (slot.tag === 'Sha3' && check(slot)) { stmts.push(new ast.MappingStore(slot, this.mappings, base!, parts!, value)); } else if (slot.tag === 'Add' && slot.left.tag === 'Sha3' && slot.right.isVal() && check(slot.left)) { stmts.push(new ast.MappingStore(slot, this.mappings, base!, parts!, value, slot.right.val)); } else if (slot.tag === 'Add' && slot.left.isVal() && slot.right.tag === 'Sha3' && check(slot.right)) { stmts.push(new ast.MappingStore(slot, this.mappings, base!, parts!, value, slot.left.val)); } else { stmts.push(new ast.SStore(slot, value, getVar(slot.eval(), [value], this))); } }], } satisfies { [m: string]: readonly [opcode: number, (state: State<Inst, Expr>) => void] }; function parseSha3(sha: Sha3): [number | undefined, Expr[]] { const shas = [sha]; const mappings = []; let base = undefined; while (shas.length > 0) { const sha = shas.shift()!; for (const arg of sha.args ?? []) { if (arg.tag === 'Sha3' && arg.args) { shas.unshift(arg); } else if (base === undefined && arg.tag === 'Val') { base = Number(arg.val); } else { mappings.unshift(arg); } } } return [base, mappings]; } export const JUMPDEST = 0x5b; function getJumpDest(offset: Expr, opcode: Opcode, bytecode: Uint8Array) { const unwrapVal = (expr: Expr): Val | undefined => expr.isVal() ? expr : expr.tag === 'Local' && expr.value.isVal() ? expr.value : undefined; const unwrapMask = (lhs: Expr, rhs: Expr): Val | undefined => lhs.tag === 'Val' && lhs.val === 0xffffffffn ? unwrapVal(rhs) : undefined; const offsetVal = offset.tag === 'Local' ? offset.value : offset.tag === 'And' ? unwrapMask(offset.left, offset.right) ?? unwrapMask(offset.right, offset.left) ?? offset : offset; if (!offsetVal.isVal()) { throw new ExecError(`${opcode.format()} offset should be numeric but found \`${offset.yul()}\``); } const destpc = Number(offsetVal.val); if (bytecode[destpc] === JUMPDEST) { // Checks whether `offsetVal` has actually been pushed onto the stack // by a `PUSHn` instruction instead of having been `eval`uated. if (offsetVal.pushStateId === undefined) throw new Error('offset `Val` should be a PUSHn'); offsetVal.jumpDest = destpc; return { destpc, pushStateId: offsetVal.pushStateId }; } else { throw new ExecError(`${opcode.format()} destination should be JUMPDEST@${destpc} but ${bytecode[destpc] === undefined ? `'${destpc}' is out-of-bounds` : `found '0x${bytecode[destpc].toString(16)}'` }`); } } class ArgsFetcher { readonly args: Expr[] = []; constructor(readonly memory: Ram<Expr>['memory']) { } fetch(i: bigint) { this.args.push(this.memory.get(i)?.eval() ?? new ast.MLoad(new Val(i))); } range(offset: Val, size: Val) { for (let i = offset.val; i < offset.val + size.val; i += 32n) { this.fetch(i); } return this; } } const zip = Object.fromEntries; const FrontierStep = { /* Stack operations */ POP: [0x50, ({ stack }: Operand<Expr>) => stack.pop()], ...zip([...Array(32).keys()].map(size => [`PUSH${(size + 1 as Size<32>)}`, [ { opcode: 0x60 + size, size: size + 1 }, (state: State<Inst, Expr>, opcode: Opcode) => state.stack.push(new Val(BigInt('0x' + opcode.hexData()), state.id)) ]] as const)), ...zip([...Array(16).keys()].map(position => [`DUP${(position + 1 as Size<16>)}`, [ 0x80 + position, (state: State<Inst, Expr>) => { if (position >= state.stack.values.length) { throw new ExecError('Invalid duplication operation, position was not found'); } const expr = state.stack.values[position]; if (expr.tag !== 'Local') { const local = new ast.Local(state.nlocals++, expr); state.stack.values[position] = local; state.stmts.push(new ast.Locali(local)); } else { expr.nrefs++; } state.stack.push(state.stack.values[position]); } ]] as const)), ...zip([...Array(16).keys()].map(position => [`SWAP${(position + 1 as Size<16>)}`, [ 0x90 + position, ({ stack }: Operand<Expr>) => stack.swap(position + 1)] ] as const)), /* ALU operations */ ...zip([ ['ADD', 0x01, ast.Add] as const, ['MUL', 0x02, ast.Mul] as const, ['SUB', 0x03, ast.Sub] as const, ['DIV', 0x04, ast.Div] as const, ['SDIV', 0x05, ast.Div] as const, ['MOD', 0x06, ast.Mod] as const, ['SMOD', 0x07, ast.Mod] as const, ['EXP', 0x0a, ast.Exp] as const, ['LT', 0x10, ast.Lt] as const, ['GT', 0x11, ast.Gt] as const, ['SLT', 0x12, ast.Lt] as const, ['SGT', 0x13, ast.Gt] as const, ['AND', 0x16, ast.And] as const, ['OR', 0x17, ast.Or] as const, ['XOR', 0x18, ast.Xor] as const, ].map(([mnemonic, opcode, Klass]) => [mnemonic, [opcode, ({ stack }: Operand<Expr>) => { const lhs = stack.pop(); const rhs = stack.pop(); stack.push(new Klass(lhs, rhs)); }]])), ...ALU, ...SPECIAL, PC: [0x58, ({ stack }, op) => stack.push(new Val(BigInt(op.pc)))], ...zip(Object.entries(ast.FNS).map(([m, [, , o]]) => [m, [ o, ({ stack }) => stack.push(new ast.Fn(m, stack.pop())) ]])), ...DATACOPY, /* Memory operations */ MLOAD: [0x51, ({ stack, memory }: Ram<Expr>) => { const location = stack.pop(); stack.push(new ast.MLoad(location, (location => location.isVal() ? memory.get(location.val) : undefined )(location.eval()))); }], ...zip([ ['MSTORE', 0x52] as const, ['MSTORE8', 0x53] as const, ].map(([m, o]) => [m, [o, ({ stack, memory, stmts }: State<Inst, Expr>) => { let location = stack.pop(); const data = stack.pop(); if (location.tag === 'Local') location.nrefs--; if (data.tag === 'Local') data.nrefs--; stmts.push(new ast.MStore(location, data)); location = location.eval(); if (location.isVal()) { memory.set(location.val, data); } }]])), MSIZE: [0x59, ({ stack }: Operand<Expr>) => stack.push(new ast.Prop('msize()', 'uint'))], /* System operations */ SHA3: [0x20, (state: Ram<Expr>) => state.stack.push(memArgs(state, Sha3))], STOP: [{ opcode: 0x00, halts: true }, state => state.halt(new ast.Stop())], CREATE: [0xf0, function create({ stack, memory }) { const value = stack.pop(); const offset = stack.pop(); const size = stack.pop(); stack.push(new ast.Create(value, offset, size, ((offset, size) => { if (offset.isVal() && size.isVal() && memory.has(offset.val)) { const data = memory.get(offset.val)!; if (data.tag === 'DataCopy' && data.bytecode !== undefined && data.bytecode.length === Number(size.val)) { return data.bytecode; } } return null; })(offset.eval(), size.eval()))); }], CALL: [0xf1, function call({ stack, memory }) { const gas = stack.pop(); const address = stack.pop(); const value = stack.pop(); const argsStart = stack.pop(); const argsLen = stack.pop(); const retStart = stack.pop(); const retLen = stack.pop(); stack.push(new ast.Call(gas, address, value, argsStart, argsLen, retStart, retLen)); memory.invalidateRange(retStart, retLen); if (retStart.isVal()) { memory.set(retStart.val, new ast.ReturnData(retStart, retLen)); } }], CALLCODE: [0xf2, function callcode({ stack, memory }) { const gas = stack.pop(); const address = stack.pop(); const value = stack.pop(); const argsStart = stack.pop(); const argsLen = stack.pop(); const retStart = stack.pop(); const retLen = stack.pop(); stack.push(new ast.CallCode(gas, address, value, argsStart, argsLen, retStart, retLen)); memory.invalidateRange(retStart, retLen); }], RETURN: [{ opcode: 0xf3, halts: true }, state => state.halt(memArgs(state, ast.Return))], DELEGATECALL: [0xf4, function delegatecall({ stack, memory }) { const gas = stack.pop(); const address = stack.pop(); const argsStart = stack.pop(); const argsLen = stack.pop(); const retStart = stack.pop(); const retLen = stack.pop(); stack.push(new ast.DelegateCall(gas, address, argsStart, argsLen, retStart, retLen)); memory.invalidateRange(retStart, retLen); }], STATICCALL: [0xfa, function staticcall({ stack, memory }) { const gas = stack.pop(); const address = stack.pop(); const argsStart = stack.pop(); const argsLen = stack.pop(); const retStart = stack.pop(); const retLen = stack.pop(); stack.push(new ast.StaticCall(gas, address, argsStart, argsLen, retStart, retLen)); memory.invalidateRange(retStart, retLen); }], REVERT: [{ opcode: 0xfd, halts: true }, function (this: Members, state) { const offset_ = state.stack.pop(); const size_ = state.stack.pop(); state.halt(new ast.Revert(offset_, size_, ...(({ memory }: Ram<Expr>, offset, size): [string | undefined, IReverts[string] | undefined, Expr[] | undefined] => { if (offset.isVal() && size.isVal() && size.val <= MAXSIZE) { const fetcher = new ArgsFetcher(memory); // https://docs.soliditylang.org/en/latest/control-structures.html#revert if (size.val % 32n === 4n) { let selector: string | undefined; const selectorVal = memory.get(offset.val)?.eval(); if (selectorVal?.isVal() && selectorVal.val % (1n << 224n) === 0n) { selector = (selectorVal.val >> 224n).toString(16).padStart(8, '0'); } else { const selectorVal = memory.get(offset.val - 28n)?.eval(); if (selectorVal?.isVal() && selectorVal.val <= 0xffffffffn) { selector = selectorVal.val.toString(16).padStart(8, '0'); } } if (selector !== undefined) { for (let i = offset.val + 4n; i < offset.val + size.val; i += 32n) { fetcher.fetch(i); } return [selector, this.getError(selector), fetcher.args]; } } return [undefined, undefined, fetcher.range(offset, size).args]; } else { if (size.isVal() && size.val > MAXSIZE) { throw new ExecError(`Memory size too large creating Revert: ${size.val} in \`${size_.yul()}\``); } return [undefined, undefined, undefined]; } })(state, offset_.eval(), size_.eval()))); }], SELFDESTRUCT: [{ opcode: 0xff, halts: true }, function selfdestruct(state) { const address = state.stack.pop(); state.halt(new ast.SelfDestruct(address)); }], INVALID: [{ opcode: 0xfe, halts: true }, (state, op) => state.halt(new ast.Invalid(op.opcode))], /* Log operations */ ...zip(([0, 1, 2, 3, 4] as const).map(ntopics => [`LOG${ntopics}` as const, [ 0xa0 + ntopics, function log(this: Members, { stack, memory, stmts }: State<Inst, Expr>) { const offset_ = stack.pop(); const size_ = stack.pop(); const topics = []; for (let i = 0; i < ntopics; i++) { topics.push(stack.pop()); } let event: IEvents[string] | undefined = undefined; if (topics.length > 0 && topics[0].isVal()) { const eventTopic = topics[0].val.toString(16).padStart(64, '0'); event = this.events[eventTopic]; if (event === undefined) { event = { indexedCount: topics.length - 1 }; this.events[eventTopic] = event; } } stmts.push(new ast.Log(event, offset_, size_, topics, function (offset, size) { if (offset.isVal() && size.isVal() && size.val <= MAXSIZE) { return memory.range(offset.val, size.val, i => new ast.MLoad(new Val(i))); } else { if (size.isVal() && size.val > MAXSIZE) { throw new ExecError(`Memory size too large creating Log: ${size.val} in \`${size_.yul()}\``); } return undefined; } }(offset_.eval(), size_.eval()))); }] ])), ...STORAGE, /* Flow operations */ JUMPDEST: [JUMPDEST, _state => { }], JUMP: [{ opcode: 0x56, halts: true }, function (state, opcode, { bytecode }) { const offset = state.stack.pop(); const { destpc, pushStateId } = getJumpDest(offset, opcode, bytecode); const destBranch = ast.Branch.make(destpc, state); state.halt(new ast.Jump(offset, destBranch, pushStateId)); }], JUMPI: [{ opcode: 0x57, halts: true }, function (this: Members, state, opcode, { bytecode }) { const offset = state.stack.pop(); const cond = state.stack.pop(); const { destpc, pushStateId } = getJumpDest(offset, opcode, bytecode); const fallBranch = ast.Branch.make(opcode.pc + 1, state); const destBranch = ast.Branch.make(destpc, state); let last: ast.SigCase | Jumpi; if (cond.tag === 'Sig') { const [pc, contBranch] = cond.positive ? [destpc, fallBranch] : [opcode.pc + 1, destBranch]; this.functionBranches.set(cond.selector, { pc, state: state.clone(), }); last = new ast.SigCase(cond, offset, contBranch); } else { last = new ast.Jumpi(cond, offset, fallBranch, destBranch, pushStateId); } state.halt(last); }], } satisfies Step; const isSelectorCallData = (expr: Expr) => expr.tag === 'Shr' && expr.shift.isVal() && expr.shift.val === 0xe0n && expr.value.tag === 'CallDataLoad' && expr.value.location.isZero(); /** * `EXTCODEHASH` implemented in `FNS`. * * https://eips.ethereum.org/EIPS/eip-1052 */ const ConstantinopleStep = { ...FrontierStep, EQ: [FrontierStep.EQ[0], ({ stack }: Operand<Expr>) => { FrontierStep.EQ[1]({ stack }); if (stack.top?.tag === 'Eq') { const SHRsig = (left: Expr, right: Expr): Sig | undefined => { left = left.eval(); right = right.eval(); return left.isVal() && isSelectorCallData(right) ? new Sig(left.val.toString(16).padStart(8, '0')) : undefined; }; const { left, right } = stack.top; const sig = SHRsig(left, right) ?? SHRsig(right, left); if (sig !== undefined) { stack.pop(); stack.push(sig); } } }], ISZERO: [FrontierStep.ISZERO[0], ({ stack }: Operand<Expr>) => { if (stack.top !== undefined && isSelectorCallData(stack.top.eval())) { void stack.pop(); stack.push(new Sig('00000000')); } else { FrontierStep.ISZERO[1]({ stack }); } }], ...zip([ ['SHL', 0x1b, ast.Shl] as const, ['SHR', 0x1c, ast.Shr] as const, ['SAR', 0x1d, ast.Sar] as const, ].map(([mnemonic, opcode, Cons]) => [mnemonic, [opcode, ({ stack }: Operand<Expr>) => { const shift = stack.pop(); const value = stack.pop(); stack.push(new Cons(value, shift)); }]] as const)), CREATE2: [0xf5, function create2({ stack }) { const value = stack.pop(); const memoryStart = stack.pop(); const memoryLength = stack.pop(); stack.push(new ast.Create2(memoryStart, memoryLength, value)); }] as const, } satisfies Step; const IstanbulStep = { ...ConstantinopleStep, CHAINID: [0x46, prop('block.chainid')] as const, SELFBALANCE: [0x47, prop('address(this).balance')] as const, } satisfies Step; const LondonStep = { ...IstanbulStep, BASEFEE: [0x48, prop('block.basefee')] as const, }; const ParisStep = { ...LondonStep, PREVRANDAO: [0x44, prop('block.prevrandao')], } satisfies Step; const ShanghaiStep = { ...ParisStep, PUSH0: [0x5f, (state: State<Inst, Expr>) => state.stack.push(new Val(0n, state.id))], } satisfies Step; // https://github.com/vyperlang/vyper/issues/1603#issuecomment-529238275 const isSelectorMLoadCallData = (expr: Expr, memory: Ram<Expr>['memory']) => expr.tag === 'MLoad' && expr.location.isZero() && !memory.has(0x0n) && (value => ( value?.tag === 'CallDataLoad' && value.location.isZero() ) || ( value?.tag === 'DataCopy' && value.kind === 'calldatacopy' && value.offset.isZero() && value.size.isVal() && value.size.val === 0x4n ) )(memory.get(0x1cn)); const VyperFunctionSelector = { ISZERO: [ConstantinopleStep.ISZERO[0], ({ stack, memory }: Ram<Expr>) => { ConstantinopleStep.ISZERO[1]({ stack }); const top = stack.top; if (top?.tag === 'IsZero' && top.value.tag === 'Eq') { const sel = (left: Expr, right: Expr): Sig | undefined => { return left.isVal() && isSelectorMLoadCallData(right.unwrap(), memory) ? new Sig(left.val.toString(16).padStart(8, '0'), false) : undefined; }; const { left, right } = top.value; const sig = sel(left, right) ?? sel(right, left); if (sig !== undefined) { stack.pop(); stack.push(sig); } } }], XOR: [FrontierStep.XOR[0], ({ stack, memory }: Ram<Expr>) => { FrontierStep.XOR[1]({ stack }); const top = stack.top; if (top?.tag !== 'Xor') throw new Error('expected Xor'); const XORsig = (left: Expr, right: Expr): Sig | undefined => { right = right.unwrap(); return left.isVal() && (isSelectorMLoadCallData(right, memory) || isSelectorCallData(right)) ? new Sig(left.val.toString(16).padStart(8, '0'), false) : undefined; }; const { left, right } = top; const sig = XORsig(left, right) ?? XORsig(right, left); if (sig !== undefined) { stack.pop(); stack.push(sig); } }], } satisfies Step; const SubFunctionSelector = { SUB: [FrontierStep.SUB[0], ({ stack }: Operand<Expr>) => { FrontierStep.SUB[1]({ stack }); const top = stack.top; if (top?.tag !== 'Sub') throw new Error('expected Sub'); const SUBsig = (left: Expr, right: Expr): Sig | undefined => { return left.isVal() && isSelectorCallData(right.eval()) ? new Sig(left.val.toString(16).padStart(8, '0'), false) : undefined; }; const { left, right } = top; const sig = SUBsig(left, right) ?? SUBsig(right, left); if (sig !== undefined) { stack.pop(); stack.push(sig); } }], } satisfies Step; /** * */ function ForkFactory<U extends Step>(steps: U) { const lookup = Object.fromEntries( Object.entries(steps).map(([m, [o,]]) => typeof o === 'number' ? [o, [0, false, m]] as const : [o.opcode, [o.size ?? 0, !!o.halts, m]] as const )); class Fork extends Undef<keyof U & string> { constructor() { super(); Object.assign(this, lookup); } } const props = Object.fromEntries(Object.entries(steps).map(([m, value]) => [m, value[1]])); Object.assign(Fork.prototype, props); return Fork as unknown as new () => Undef<keyof U & string> & { readonly [m in keyof U]: U[m][1] }; } /** * https://eips.ethereum.org/EIPS/eip-3198 * * Keep track of https://eips.ethereum.org/EIPS/eip-4200 */ export const London = ForkFactory(LondonStep); /** * Defines the `Paris` hardfork. * It includes the `PREVRANDAO` instruction. * * Solidity `0.8.18` includes _Support for Paris Hardfork_, * which introduces the global `block.prevrandao` built-in in Solidity and `prevrandao()` * instruction in inline assembly and Yul for EVM versions >= Paris. * * @see https://ethereum.github.io/execution-specs/diffs/gray_glacier_paris.html * @see https://eips.ethereum.org/EIPS/eip-4399 * @see https://soliditylang.org/blog/2023/02/01/solidity-0.8.18-release-announcement */ export const Paris = ForkFactory(ParisStep); /** * Defines the `Shanghai` hardfork. * It includes the `PUSH0` instruction. * * Solidity `0.8.20` uses `push0` for placing `0` on the Stack. * This decreases the deployment and runtime costs. * * Keep track of https://eips.ethereum.org/EIPS/eip-6780 * * @see https://ethereum.github.io/execution-specs/diffs/paris_shanghai.html * @see https://eips.ethereum.org/EIPS/eip-3855 * @see https://soliditylang.org/blog/2023/05/10/solidity-0.8.20-release-announcement/ */ export const Shanghai = ForkFactory({ ...ShanghaiStep, ...VyperFunctionSelector, ...SubFunctionSelector });