sevm
Version:
A Symbolic Ethereum Virtual Machine (EVM) bytecode decompiler & analyzer library & CLI
144 lines (127 loc) • 4.8 kB
text/typescript
import CBOR from 'cbor-js';
import { arrayify, hexlify } from './.bytes';
/**
* https://docs.soliditylang.org/en/v0.5.8/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
*
* https://docs.soliditylang.org/en/v0.5.9/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
* https://blog.soliditylang.org/2019/05/28/solidity-0.5.9-release-announcement/
*
* v0.6.2 ends with `0x00 0x33` but v0.6.1 ends with `0x00 0x32`
* https://blog.soliditylang.org/2019/12/17/solidity-0.6.0-release-announcement/
* https://docs.soliditylang.org/en/v0.6.2/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
*/
/**
* Represents the metadata hash protocols embedded in bytecode by `solc`.
*/
export class Metadata {
[key: string]: string | Uint8Array | undefined | boolean | number;
protocol: 'bzzr0' | 'bzzr1' | 'ipfs' | '' = '';
hash = '';
solc = '';
experimental?: boolean;
get url(): string {
return `${this.protocol}://${this.hash}`;
}
get minor(): number | undefined {
const field = /^0\.(\d+)\./.exec(this.solc)?.[1];
return field ? parseInt(field) : undefined;
}
}
/**
* Splits the `bytecode` into executable code and embedded metadata hash as
* placed by the Solidity compiler, if present in the `bytecode`.
*
* If `metadata` contains an IPFS hash, it is encoded using base 58.[^1]
*
* @param bytecode the contract or library `bytecode` to test for metadata hash.
* @returns An object where the `bytecode` is the executable code and
* `metadata` is the metadata hash when the metadata is present.
*
* [^1]: https://github.com/pur3miish/base58-js
*/
export function splitMetadataHash(buffer: Parameters<typeof arrayify>[0]): {
/**
* The executable code without metadata when it is present.
* Otherwise, the original `bytecode`.
*/
bytecode: Uint8Array,
/**
* The metadata if present. Otherwise `undefined`.
*/
metadata: Metadata | undefined
} {
const bytecode = arrayify(buffer);
if (buffer.length <= 2) return { bytecode, metadata: undefined };
const dataLen = (bytecode.at(-2)! << 8) + bytecode.at(-1)!;
const data = new Uint8Array(bytecode.subarray(bytecode.length - 2 - dataLen, bytecode.length - 2));
if (data.length !== dataLen) return { bytecode, metadata: undefined };
try {
const obj = CBOR.decode(data.buffer);
const metadata = new Metadata();
if ('ipfs' in obj && obj['ipfs'] instanceof Uint8Array) {
metadata.protocol = 'ipfs';
metadata.hash = bs58(obj['ipfs']);
delete obj['ipfs'];
} else if ('bzzr0' in obj && obj['bzzr0'] instanceof Uint8Array) {
metadata.protocol = 'bzzr0';
metadata.hash = hexlify(obj['bzzr0']);
delete obj['bzzr0'];
} else if ('bzzr1' in obj && obj['bzzr1'] instanceof Uint8Array) {
metadata.protocol = 'bzzr1';
metadata.hash = hexlify(obj['bzzr1']);
delete obj['bzzr1'];
}
if ('solc' in obj && obj['solc'] instanceof Uint8Array) {
metadata.solc = obj['solc'].join('.');
delete obj['solc'];
}
return {
bytecode: bytecode.subarray(0, bytecode.length - 2 - dataLen),
metadata: Object.assign(metadata, obj)
};
} catch {
return { bytecode, metadata: undefined };
}
}
/**
* Converts a Uint8Array into a base58 string.
*
* @param buffer Unsigned integer array to encode.
* @returns base58 string representation of the binary array.
* @example <caption>Usage.</caption>
*
* ```js
* const str = bs58([15, 239, 64])
* console.log(str)
* ```
*
* Logged output will be 6MRy.
*/
function bs58(buffer: Uint8Array): string {
/** Base58 characters include numbers `123456789`, uppercase `ABCDEFGHJKLMNPQRSTUVWXYZ` and lowercase `abcdefghijkmnopqrstuvwxyz` */
const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
/** Mapping between base58 and ASCII */
const base58Map = Array(256).fill(-1) as number[];
for (let i = 0; i < chars.length; ++i) {
base58Map[chars.charCodeAt(i)] = i;
}
const result = [];
for (const byte of buffer) {
let carry = byte;
for (let j = 0; j < result.length; ++j) {
const x: number = (base58Map[result[j]] << 8) + carry;
result[j] = chars.charCodeAt(x % 58);
carry = (x / 58) | 0;
}
while (carry) {
result.push(chars.charCodeAt(carry % 58));
carry = (carry / 58) | 0;
}
}
for (const byte of buffer) {
if (byte) break;
else result.push('1'.charCodeAt(0));
}
result.reverse();
return String.fromCharCode(...result);
}