sevm
Version:
A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI
243 lines (214 loc) • 8.02 kB
text/typescript
/**
* The following elementary types exist[1]:
*
* - `uint<M>`: unsigned integer type of M bits, 0 < M <= 256, M % 8 == 0. e.g. uint32, uint8, uint256.
* - `int<M>: two’s complement signed integer type of M bits, 0 < M <= 256, M % 8 == 0.
* - `address`: equivalent to uint160, except for the assumed interpretation and language typing. For computing the function selector, address is used.
* - `uint`, `int`: synonyms for uint256, int256 respectively. For computing the function selector, uint256 and int256 have to be used.
* - `bool`: equivalent to uint8 restricted to the values 0 and 1. For computing the function selector, bool is used.
* - `fixed<M>x<N>`: signed fixed-point decimal number of M bits, 8 <= M <= 256, M % 8 == 0, and 0 < N <= 80, which denotes the value v as v / (10 ** N).
* - `ufixed<M>x<N>`: unsigned variant of fixed<M>x<N>.
* - `fixed`, `ufixed`: synonyms for fixed128x18, ufixed128x18 respectively. For computing the function selector, fixed128x18 and ufixed128x18 have to be used.
* - `bytes<M>`: binary type of M bytes, 0 < M <= 32.
* - `function`: an address (20 bytes) followed by a function selector (4 bytes). Encoded identical to bytes24.
*
* See also [2] for more information on Types.
*
* - [1] https://docs.soliditylang.org/en/latest/abi-spec.html#types
* - [2] https://docs.soliditylang.org/en/latest/types.html
*/
export type Type = (typeof ELEM_TYPES)[number];
/**
* Determine whether the given `type` is a valid elementary Solidity type.
*
* @see {@link Type} definition for more info on `Type`.
*
* @param type value to check if it is a valid elementary type.
* @returns
*/
export function isElemType(type: string): type is Type {
return (ELEM_TYPES as readonly string[]).includes(type);
}
function getCanonicalType(type: string): string {
switch (type) {
case 'uint': return 'uint256';
case 'int': return 'int256';
case 'fixed': return 'fixed128x18';
case 'ufixed': return 'ufixed128x18';
default: return type;
}
}
/**
*
*/
const BITS = [...Array(32).keys()].map(n => (n + 1) * 8) as SizeN<32, [8]>;
/**
*
*/
const BYTES = [...Array(32).keys()].map(n => n + 1) as SizeN<32, [1]>;
/**
*
*/
const ELEM_TYPES = [
'address',
'address payable',
'bool',
'uint',
...BITS.map(n => `uint${n}` as const),
'int',
...BITS.map(n => `int${n}` as const),
'bytes',
...BYTES.map(n => `bytes${n}` as const),
'string',
'function',
] as const;
class Tokenizer {
readonly ID_REGEX = /^\w+\b/;
position = 0;
constructor(readonly input: string) { }
next(): [pos: number, kind: 'ID' | 'LIT' | 'TYPE' | 'OP' | null, token: string | null] {
while (this.input[this.position] === ' ') this.position++;
const i = this.position;
const matchType = this.input.slice(i).match(this.ID_REGEX);
if (matchType) {
this.position += matchType[0].length;
const kind = isElemType(matchType[0])
? 'TYPE'
: !Number.isNaN(Number(matchType[0]))
? 'LIT'
: 'ID';
return [i, kind, matchType[0]];
}
switch (this.input[i]) {
case '(':
case ')':
case ',':
case '[':
case ']':
this.position++;
return [i, 'OP', this.input[i]];
}
return [i, null, null];
}
*[Symbol.iterator](): Generator<[pos: number, kind: 'ID' | 'LIT' | 'TYPE' | 'OP', token: string]> {
for (let [pos, kind, token] = this.next(); token !== null; [pos, kind, token] = this.next()) {
yield [pos + 1, kind!, token];
}
}
}
class Tokens {
readonly #tokens: [pos: number, kind: 'ID' | 'LIT' | 'TYPE' | 'OP', token: string][] = [];
constructor(readonly sig: string) {
this.#tokens = [...new Tokenizer(sig)];
}
peek() {
if (this.#tokens.length === 0) throw new Error('peek: reached end of input');
return this.#tokens[0];
}
next() {
if (this.#tokens.length === 0) throw new Error('next: reached end of input');
return this.#tokens.shift()!;
}
consume(value: string) {
const [, , next] = this.next();
if (next !== value) throw new Error(`Expected '${value}' but got '${next}'`);
}
}
export type Ty = { type: string, components?: Ty[], arrayLength?: number | null, arrayType?: Ty };
export type SigMember = {
name: string,
inputs: ({ name?: string; } & Ty)[],
}
export function parseSig(sig: string): SigMember {
const tokens = new Tokens(sig);
let [pos, kind, name] = tokens.next();
if (name === 'function') [pos, kind, name] = tokens.next();
if (kind !== 'ID') throw new Error(`Expected function name, found '${name}':${pos}`);
const inputs = [];
tokens.consume('(');
let [, , n] = tokens.peek();
while (n !== ')') {
inputs.push(parseParam());
[, , n] = tokens.peek();
if (n === ',') tokens.consume(',');
}
tokens.consume(')');
return { name, inputs };
function parseParam() {
const ty = parseType();
const [, kind, name] = tokens.peek();
if (kind !== 'ID') return { ...ty } as const;
tokens.consume(name);
return { ...ty, name } as const;
}
function parseType(): Ty {
const baseType = function () {
// eslint-disable-next-line prefer-const
let [pos, , ty] = tokens.peek();
if (ty === '(') {
const components = [];
tokens.consume('(');
let tupTy;
[, , ty] = tokens.peek();
while (ty !== ')') {
tupTy = parseType();
components.push(tupTy);
[, , ty] = tokens.peek();
if (ty === ',') tokens.consume(',');
}
tokens.consume(')');
return { type: 'tuple', components } as const;
} else if (isElemType(ty)) {
tokens.next();
return { type: getCanonicalType(ty) } as const;
} else {
throw new Error(`Invalid elementary type found: \`${ty}\` at position ${pos} in \`${sig}\``);
}
}();
const dims: (number | null)[] = [];
let [, , array] = tokens.peek();
while (array === '[') {
tokens.consume('[');
const [, kind, size] = tokens.peek();
if (kind === 'LIT') {
tokens.consume(size);
tokens.consume(']');
dims.push(Number(size));
} else {
dims.push(null);
tokens.consume(']');
}
[, , array] = tokens.peek();
}
return dims.reduce<Ty>((ty, size, i) => {
return {
type: `${baseType.type}${dims.slice(0, i + 1).map(size => `[${size === null ? '' : size}]`).join('')}`,
// baseType: 'array',
arrayType: ty,
arrayLength: size
};
}, baseType);
}
}
/**
*
* https://docs.soliditylang.org/en/latest/abi-spec.html#handling-tuple-types
*
* @param member
*/
export function sighash(member: ReturnType<typeof parseSig>): string {
return `${member.name}(${member.inputs.map(sighashType).join(',')})`;
}
function sighashType(ty: Ty): string {
if (ty.arrayType !== undefined) {
const len = ty.arrayLength === null ? '' : ty.arrayLength;
return `${sighashType(ty.arrayType)}[${len}]`;
} else if (ty.type === 'tuple') {
return `(${ty.components!.map(sighashType).join(',')})`;
} else {
return ty.type;
}
}
export function fnsig(member: ReturnType<typeof parseSig>): string {
return `${member.name}(${member.inputs.map((param, i) => `${sighashType(param)} ${param.name ?? '_arg' + i}`).join(', ')})`;
}