UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

236 lines (197 loc) 7.12 kB
import { bytesToHex } from "@nomicfoundation/ethereumjs-util"; import { normalizeLibraryRuntimeBytecodeIfNecessary, zeroOutAddresses, zeroOutSlices, } from "./library-utils"; import { Bytecode } from "./model"; import { getOpcodeLength, Opcode } from "./opcodes"; /** * This class represent a somewhat special Trie of bytecodes. * * What makes it special is that every node has a set of all of its descendants and its depth. */ class BytecodeTrie { public static isBytecodeTrie(o: any): o is BytecodeTrie { if (o === undefined || o === null) { return false; } return "childNodes" in o; } public readonly childNodes: Map<number, BytecodeTrie> = new Map(); public readonly descendants: Bytecode[] = []; public match?: Bytecode; constructor(public readonly depth: number) {} public add(bytecode: Bytecode) { // eslint-disable-next-line @typescript-eslint/no-this-alias let trieNode: BytecodeTrie = this; for ( let currentCodeByte = 0; currentCodeByte <= bytecode.normalizedCode.length; currentCodeByte += 1 ) { if (currentCodeByte === bytecode.normalizedCode.length) { // If multiple contracts with the exact same bytecode are added we keep the last of them. // Note that this includes the metadata hash, so the chances of happening are pretty remote, // except in super artificial cases that we have in our test suite. trieNode.match = bytecode; return; } const byte = bytecode.normalizedCode[currentCodeByte]; trieNode.descendants.push(bytecode); let childNode = trieNode.childNodes.get(byte); if (childNode === undefined) { childNode = new BytecodeTrie(currentCodeByte); trieNode.childNodes.set(byte, childNode); } trieNode = childNode; } } /** * Searches for a bytecode. If it's an exact match, it is returned. If there's no match, but a * prefix of the code is found in the trie, the node of the longest prefix is returned. If the * entire code is covered by the trie, and there's no match, we return undefined. */ public search( code: Uint8Array, currentCodeByte: number = 0 ): Bytecode | BytecodeTrie | undefined { if (currentCodeByte > code.length) { return undefined; } // eslint-disable-next-line @typescript-eslint/no-this-alias let trieNode: BytecodeTrie = this; for (; currentCodeByte <= code.length; currentCodeByte += 1) { if (currentCodeByte === code.length) { return trieNode.match; } const childNode = trieNode.childNodes.get(code[currentCodeByte]); if (childNode === undefined) { return trieNode; } trieNode = childNode; } } } export class ContractsIdentifier { private _trie = new BytecodeTrie(-1); private _cache: Map<string, Bytecode> = new Map(); constructor(private readonly _enableCache = true) {} public addBytecode(bytecode: Bytecode) { this._trie.add(bytecode); this._cache.clear(); } public getBytecodeForCall( code: Uint8Array, isCreate: boolean ): Bytecode | undefined { const normalizedCode = normalizeLibraryRuntimeBytecodeIfNecessary(code); let normalizedCodeHex: string | undefined; if (this._enableCache) { normalizedCodeHex = bytesToHex(normalizedCode); const cached = this._cache.get(normalizedCodeHex); if (cached !== undefined) { return cached; } } const result = this._searchBytecode(isCreate, normalizedCode); if (this._enableCache) { if (result !== undefined) { this._cache.set(normalizedCodeHex!, result); } } return result; } private _searchBytecode( isCreate: boolean, code: Uint8Array, normalizeLibraries = true, trie = this._trie, firstByteToSearch = 0 ): Bytecode | undefined { const searchResult = trie.search(code, firstByteToSearch); if (searchResult === undefined) { return undefined; } if (!BytecodeTrie.isBytecodeTrie(searchResult)) { return searchResult; } // Deployment messages have their abi-encoded arguments at the end of the bytecode. // // We don't know how long those arguments are, as we don't know which contract is being // deployed, hence we don't know the signature of its constructor. // // To make things even harder, we can't trust that the user actually passed the right // amount of arguments. // // Luckily, the chances of a complete deployment bytecode being the prefix of another one are // remote. For example, most of the time it ends with its metadata hash, which will differ. // // We take advantage of this last observation, and just return the bytecode that exactly // matched the searchResult (sub)trie that we got. if ( isCreate && searchResult.match !== undefined && searchResult.match.isDeployment ) { return searchResult.match; } if (normalizeLibraries) { for (const bytecodeWithLibraries of searchResult.descendants) { if ( bytecodeWithLibraries.libraryAddressPositions.length === 0 && bytecodeWithLibraries.immutableReferences.length === 0 ) { continue; } const normalizedLibrariesCode = zeroOutAddresses( code, bytecodeWithLibraries.libraryAddressPositions ); const normalizedCode = zeroOutSlices( normalizedLibrariesCode, bytecodeWithLibraries.immutableReferences ); const normalizedResult = this._searchBytecode( isCreate, normalizedCode, false, searchResult, searchResult.depth + 1 ); if (normalizedResult !== undefined) { return normalizedResult; } } } // If we got here we may still have the contract, but with a different metadata hash. // // We check if we got to match the entire executable bytecode, and are just stuck because // of the metadata. If that's the case, we can assume that any descendant will be a valid // Bytecode, so we just choose the most recently added one. // // The reason this works is because there's no chance that Solidity includes an entire // bytecode (i.e. with metadata), as a prefix of another one. if ( this._isMatchingMetadata(code, searchResult.depth) && searchResult.descendants.length > 0 ) { return searchResult.descendants[searchResult.descendants.length - 1]; } return undefined; } /** * Returns true if the lastByte is placed right when the metadata starts or after it. */ private _isMatchingMetadata(code: Uint8Array, lastByte: number): boolean { for (let byte = 0; byte < lastByte; ) { const opcode = code[byte]; // Solidity always emits REVERT INVALID right before the metadata if (opcode === Opcode.REVERT && code[byte + 1] === Opcode.INVALID) { return true; } byte += getOpcodeLength(opcode); } return false; } }