UNPKG

@rsksmart/rsk-contract-parser

Version:

A tool to parse/interact with contracts and decode events from the Rootstock blockchain.

668 lines (580 loc) 26 kB
"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.default = exports.ContractParser = void 0;var _interfacesIds = _interopRequireDefault(require("./interfacesIds")); var _rskUtils = require("@rsksmart/rsk-utils"); var _NativeContractsDecoder = _interopRequireDefault(require("./nativeContracts/NativeContractsDecoder")); var _NativeContracts = _interopRequireDefault(require("./nativeContracts/NativeContracts")); var _Contract = _interopRequireDefault(require("./Contract")); var _EventDecoder = _interopRequireDefault(require("./EventDecoder")); var _Abi = _interopRequireDefault(require("./Abi")); var _ERC1967Beacon = _interopRequireDefault(require("./jsonAbis/ERC1967Beacon.json")); var _types = require("./types"); var _utils = require("./utils"); var _addresses2 = require("@rsksmart/rsk-utils/dist/addresses");function _interopRequireDefault(e) {return e && e.__esModule ? e : { default: e };} /** * The ContractParser class handles the analysis and interpretation of Ethereum smart contracts. * * This class provides comprehensive functionality for working with contract ABIs, * transaction data, and event logs in blockchain networks. It enables: * * - Decoding of contract method calls and event signatures * - Identification of implemented interfaces (ERC standards) * - Analysis of proxy patterns with implementation resolution * - Processing of native contract events specific to RSK networks */ class ContractParser { /** * Creates a new ContractParser instance. * @param {Object} options - Configuration options * @param {Array} [options.abi] - The Application Binary Interface (ABI) used for decoding. If not provided, a default ABI is used, however its strongly recommended to provide the full ABI. * @param {Object} [options.log] - Logging mechanism to use for error and debug messages * @param {Object} [options.initConfig] - Initial configuration object. Optional. * @example * const initConfig = { * nativeContracts: { * bridge: '0x0000000000000000000000000000000001000006', // Bridge contract address * remasc: '0x0000000000000000000000000000000001000008' // Remasc contract address * }, * net: { * id: '30', // 30: RSK Mainnet, 31: RSK Testnet * } * } * @param {Nod3} options.nod3 - Nod3 instance for making blockchain calls. Required for most functionality including contract analysis, proxy detection, and event decoding. * @param {number | string} [options.txBlockNumber] - Transaction's block number for accurate event decoding. Can be a block number or a tag. Defaults to tag 'latest'. */ constructor({ abi, log, initConfig = {}, nod3, txBlockNumber = 'latest' } = {}) { const { net } = initConfig; this.netId = net ? net.id : undefined; this.abi = (0, _utils.setAbi)(abi ?? _Abi.default); this.log = log || console; if (!nod3) throw new Error('Nod3 instance is required for ContractParser initialization'); this.nod3 = nod3; this.nativeContracts = (0, _NativeContracts.default)(initConfig); if (this.netId) { const bitcoinNetwork = _types.bitcoinRskNetWorks[this.netId]; this.nativeContractsEvents = (0, _NativeContractsDecoder.default)({ bitcoinNetwork, txBlockNumber }); } } /** * Retrieves the methods from the ABI. * @param {Array} abi - The ABI to use for decoding * @param {boolean} [addAbiSignatureData=false] - Whether to add the ABI signature data to the methods (default: false) * @returns {Array} The methods */ static getMethodsFromAbi(abi, addAbiSignatureData = false) { const methods = abi. filter((fragment) => fragment.type === 'function'); if (addAbiSignatureData) { return methods.map((method) => { const sig = method[_types.ABI_SIGNATURE] || (0, _utils.abiSignatureData)(method); sig.name = method.name; return sig; }); } return methods; } /** * Sets the Nod3 instance for making blockchain calls. * @param {Nod3} nod3 - Nod3 instance for making blockchain calls */ setNod3(nod3) { this.nod3 = nod3; } /** * Retrieves the address of a native contract * @param {string} name - The name of the native contract * @returns {string} The address of the native contract */ getNativeContractAddress(name) { const { nativeContracts } = this; if (nativeContracts) { return nativeContracts.getNativeContractAddress(name); } } /** * Retrieves the current ABI being used by the ContractParser instance. * @returns {Array} The ABI */ getAbi() { return this.abi; } /** * Sets the ABI for the ContractParser instance. * @param {Array} abi - The Application Binary Interface (ABI). If no ABI is provided, a default ABI will be used. */ setAbi(abi) { try { if (!abi) { // If no ABI is provided, use the default ABI this.abi = (0, _utils.setAbi)(_Abi.default); return; } this.abi = (0, _utils.setAbi)(abi); } catch (error) { throw new Error(`Error setting ABI: ${error}`); } } /** * Retrieves the methods and their selectors from the ABI. */ getMethodsSelectors() { const selectors = {}; const methods = this.getAbiMethods(); for (const m in methods) { const method = methods[m]; const signature = method.signature || (0, _utils.soliditySignature)(m); selectors[m] = (0, _utils.soliditySelector)(signature); } return selectors; } /** * Retrieves the methods and their signatures from the ABI. */ getAbiMethods() { const methods = {}; this.abi. filter((def) => def.type === 'function'). forEach((m) => { const sig = m[_types.ABI_SIGNATURE] || (0, _utils.abiSignatureData)(m); sig.name = m.name; methods[sig.method] = sig; }); return methods; } /** * Parses transaction logs and returns decoded events. Also handles native contract events. * @param {Array} logs - The transaction logs to parse * @returns {Array} An array of decoded events */ parseTxLogs(logs) { return this.decodeLogs(logs).map((event) => { this.addEventAddresses(event); event.abi = (0, _utils.removeAbiSignatureData)(event.abi); return event; }); } /** * Adds event addresses to the event object. * @param {Object} event - The event object to add addresses to */ addEventAddresses(event) { const { abi, args } = event; const _addresses = event._addresses || []; if (abi && args) { const inputs = abi.inputs || []; inputs.forEach((v, i) => { if (v.type === 'address') { _addresses.push(args[i]); } if (v.type === 'address[]') { const value = args[i] || []; if (Array.isArray(value)) {// temp fix to undecoded events value.forEach((v) => _addresses.push(v)); } else { let i = 0; while (2 + (i + 1) * 40 <= value.length) { _addresses.push('0x' + value.slice(2 + i * 40, 2 + (i + 1) * 40)); i++; } } } }); event._addresses = [...new Set(_addresses)]; } return event; } /** * Decodes transaction logs and returns decoded events. Also handles native contract events. * @param {Array} logs - The transaction logs to decode * @returns {Array} An array of decoded events */ decodeLogs(logs) { const eventDecoder = (0, _EventDecoder.default)(this.abi, this.log); if (!this.nativeContracts || !this.nativeContractsEvents) { throw new Error(`Native contracts decoder is missing, check the value of netId:${this.netId}`); } const { isNativeContract } = this.nativeContracts; const { nativeContractsEvents } = this; return logs.map((log) => { const { address } = log; const decoder = isNativeContract(address) ? nativeContractsEvents.getEventDecoder(log) : eventDecoder; return decoder.decodeLog(log); }); } /** * Creates a contract instance, useful for calling methods on the contract * @param {string} address - The address of the contract * @returns {Contract} A contract instance */ makeContract(address) { const { nod3 } = this; return new _Contract.default(this.abi, { address, nod3 }); } /** * Calls a method on a specific contract * @param {Contract} contract - The contract object * @param {FunctionFragment | string} method - The method to call * @param {Array} [params] - The parameters to pass to the method * @param {Object} [options] - The options for the call * @param {Object} [options.txData] - The transaction data for the call * @param {number | string} [options.blockNumber] - The specific block number to use for the call. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<* | null>} The result of the call */ async call(contract, method, params = [], options = { txData: {}, blockNumber: 'latest' }) { try { const res = await contract.call(method, params, options); return res; } catch (err) { // avoid spamming the console with errors // this.log.debug(`Error calling contract ${contract.getAddress()}: ${err}`) // this.log.debug(err) return null; } } /** * Retrieves token data from a contract * @param {Contract} contract - The contract object * @param {number | string} [blockNumber] - The specific block number to use for the call. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<Object>} The token data */ async getDefaultTokenData(contract, blockNumber = 'latest') { const defaultTokenMethods = [ 'name', 'symbol', 'decimals', 'totalSupply']; const result = await Promise.all( defaultTokenMethods.map((method) => this.call(contract, method, [], { blockNumber })) ); return result.reduce((v, a, i) => { const name = defaultTokenMethods[i]; v[name] = a; return v; }, {}); } /** * Maps interfaces to ERCs. * @param {Object} interfaces - The interfaces to map * @returns {Array} The mapped interfaces */ mapInterfacesToERCs(interfaces) { return Object.keys(interfaces). filter((k) => interfaces[k] === true). map((t) => _types.contractsInterfaces[t] || t); } /** * Checks if a contract bytecode contains a method selector. * @param {string} contractByteCode - The bytecode of the contract * @param {string} selector - The selector to check for * @returns {boolean} True if the selector is found in the contract bytecode, false otherwise */ hasMethodSelector(contractByteCode, selector) { return selector && contractByteCode && contractByteCode.includes(selector); } /** * Retrieves the methods from the contract bytecode. * * This bytecode is also the txInputData on contract creation transactions. * Note that using the default ABI for methods validation may not be 100% precise. Therefore, it is recommended to set a verified contract ABI and use the `getAbiMethods` method. * * @param {string} contractByteCode - The contract bytecode to analyze. */ getMethodsFromContractByteCode(contractByteCode) { const methods = this.getMethodsSelectors(); return Object.keys(methods). filter((method) => this.hasMethodSelector(contractByteCode, methods[method]) === true); } /** * Retrieves the contract methods and ERC interfaces * Uses the current set ABI to inspect the contract bytecode and validate methods and interfaces. * If a block number is provided, bytecode used to validate methods and interfaces will be retrieved from the node at the given block number. * @param {string} address - The contract address * @param {number | string} [blockNumber] - Optional. Retrieve methods and interfaces at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<{ * methods: string[], * interfaces: string[] * }>} The contract methods and ERC interfaces */ async getContractMethodsAndERCInterfaces(address, blockNumber = 'latest') { const contractByteCode = await this.getContractCodeFromNode(address, blockNumber); const methods = this.getMethodsFromContractByteCode(contractByteCode); const interfaces = this.getInterfacesByMethods(methods); return { methods, interfaces }; } /** * Retrieves contract details using the current set ABI. * This method also detects if the contract is a proxy. * It is recommended to set the verified ABI of the contract so all methods and interfaces are detected. * * @param {string} contractAddress - The address of the contract * @param {number | string} [blockNumber] - Optional. Retrieve contract details at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * * @returns {Promise<{ * address: string, * isProxy: boolean, * implementationAddress: string | null, * beaconAddress: string | null, * proxyType: string | null, * methods: string[], * interfaces: string[] * }>} The contract details */ async getContractDetails(contractAddress, blockNumber = 'latest') { const contractDetails = { address: contractAddress, isProxy: false, implementationAddress: null, beaconAddress: null, proxyType: null, methods: [], interfaces: [] }; // Native contracts check - Bridge if (this.nativeContracts.isNativeContract(contractAddress)) { contractDetails.methods = (0, _utils.getLatestBridgeMethods)(); return contractDetails; } try { // ERC1822 Proxy check const ERC1822ProxyDetails = await this.isERC1822Proxy(contractAddress, blockNumber); if (ERC1822ProxyDetails.isProxy) { contractDetails.isProxy = true; contractDetails.implementationAddress = ERC1822ProxyDetails.implementationAddress; contractDetails.proxyType = ERC1822ProxyDetails.proxyType; if ((0, _addresses2.isAddress)(contractDetails.implementationAddress)) { // Use implementation methods and interfaces. Append proxy standard interfaces const { methods, interfaces } = await this.getContractMethodsAndERCInterfaces(contractDetails.implementationAddress, blockNumber); contractDetails.methods = methods; contractDetails.interfaces = [_types.contractsInterfaces.ERC1822, ...interfaces]; } return contractDetails; } // ERC1967 Proxy check const ERC1967ProxyDetails = await this.isERC1967Proxy(contractAddress, blockNumber); if (ERC1967ProxyDetails.isProxy) { contractDetails.isProxy = true; contractDetails.implementationAddress = ERC1967ProxyDetails.implementationAddress; contractDetails.beaconAddress = ERC1967ProxyDetails.beaconAddress; contractDetails.proxyType = ERC1967ProxyDetails.proxyType; if ((0, _addresses2.isAddress)(contractDetails.implementationAddress)) { // Use implementation methods and interfaces. Append proxy standard interfaces const { methods, interfaces } = await this.getContractMethodsAndERCInterfaces(contractDetails.implementationAddress, blockNumber); contractDetails.methods = methods; contractDetails.interfaces = [_types.contractsInterfaces.ERC1967, ...interfaces]; } return contractDetails; } // Open Zeppelin Unstructured Storage Proxy check const OZUnstructuredStorageProxyDetails = await this.isOZUnstructuredStorageProxy(contractAddress, blockNumber); if (OZUnstructuredStorageProxyDetails.isProxy) { contractDetails.isProxy = true; contractDetails.implementationAddress = OZUnstructuredStorageProxyDetails.implementationAddress; contractDetails.proxyType = OZUnstructuredStorageProxyDetails.proxyType; if ((0, _addresses2.isAddress)(contractDetails.implementationAddress)) { // Use implementation methods and interfaces const { methods, interfaces } = await this.getContractMethodsAndERCInterfaces(contractDetails.implementationAddress, blockNumber); contractDetails.methods = methods; contractDetails.interfaces = interfaces; } return contractDetails; } // Normal contracts const { methods, interfaces } = await this.getContractMethodsAndERCInterfaces(contractAddress, blockNumber); contractDetails.methods = methods; contractDetails.interfaces = interfaces; return contractDetails; } catch (error) { this.log.error(`[${contractAddress}] Error getting contract details: ${error}`); return Promise.reject(error); } } /** * Checks if the contract is a proxy contract using the ERC1822 Universal Upgradeable Proxy Standard (UUPS). * @param {string} contractAddress - The address of the contract * @param {number | string} [blockNumber] - Optional. Retrieve proxy details at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<{ * address: string, * isProxy: boolean, * implementationAddress: string | null, * proxyType: string | null * }>} The proxy details * @see https://eips.ethereum.org/EIPS/eip-1822 */ async isERC1822Proxy(contractAddress, blockNumber = 'latest') { const result = { address: contractAddress, isProxy: false, implementationAddress: null, proxyType: null }; // ERC1822 uses keccak256("PROXIABLE") as storage slot // keccak256("PROXIABLE") = 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7 const implementationSlot = '0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7'; let implementationSlotValue; try { implementationSlotValue = await this.getStorageSlotValueFromNode(contractAddress, implementationSlot, blockNumber); } catch (err) { this.log.warn(`[${contractAddress}] Error checking implementation slot for ${_types.PROXY_TYPES.ERC1822}: ${err}`); return Promise.reject(err); } if ((0, _utils.notZero)(implementationSlotValue)) { result.proxyType = _types.PROXY_TYPES.ERC1822; result.isProxy = true; result.implementationAddress = (0, _utils.formatAddressFromSlot)(implementationSlotValue); return result; } // Not a proxy contract return result; } /** * Checks if the contract is a proxy contract using the ERC1967 standard. * @param {string} contractAddress - The address of the contract * @param {number | string} [blockNumber] - Optional. Retrieve proxy details at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<{ * address: string, * isProxy: boolean, * implementationAddress: string | null, * beaconAddress: string | null, * proxyType: string | null * }>} The proxy details * @see https://eips.ethereum.org/EIPS/eip-1967 */ async isERC1967Proxy(contractAddress, blockNumber = 'latest') { const result = { address: contractAddress, isProxy: false, implementationAddress: null, beaconAddress: null, proxyType: null }; // Normal Proxies const implementationSlot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; let implementationSlotValue; try { implementationSlotValue = await this.getStorageSlotValueFromNode(contractAddress, implementationSlot, blockNumber); } catch (err) { this.log.warn(`[${contractAddress}] Error checking implementation slot for ${_types.PROXY_TYPES.ERC1967.Normal}: ${err}`); return Promise.reject(err); } if ((0, _utils.notZero)(implementationSlotValue)) { result.proxyType = _types.PROXY_TYPES.ERC1967.Normal; result.isProxy = true; result.implementationAddress = (0, _utils.formatAddressFromSlot)(implementationSlotValue); return result; } // Beacon Proxies const beaconSlot = '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50'; let beaconSlotValue; try { beaconSlotValue = await this.getStorageSlotValueFromNode(contractAddress, beaconSlot, blockNumber); } catch (err) { this.log.warn(`[${contractAddress}] Error checking implementation slot for ${_types.PROXY_TYPES.ERC1967.Beacon}: ${err}`); return Promise.reject(err); } if ((0, _utils.notZero)(beaconSlotValue)) { result.proxyType = _types.PROXY_TYPES.ERC1967.Beacon; result.isProxy = true; try { // Get beacon contract address const beaconContractAddress = (0, _utils.formatAddressFromSlot)(beaconSlotValue); if (!(0, _addresses2.isAddress)(beaconContractAddress)) { throw new Error('Invalid beacon contract address'); } result.beaconAddress = beaconContractAddress; // Create contract instance for the beacon const beaconContract = new _Contract.default(_ERC1967Beacon.default, { address: beaconContractAddress, nod3: this.nod3 }); // Get implementation contract address from beacon contract const implementationAddress = await beaconContract.call('implementation', [], { blockNumber }); if (!(0, _addresses2.isAddress)(implementationAddress)) { throw new Error('Beacon returns an invalid implementation address'); } result.implementationAddress = implementationAddress; return result; } catch (err) { this.log.warn(`[${contractAddress}] Error fetching implementation from beacon proxy: ${err}`); return Promise.reject(err); } } // Not a proxy contract return result; } /** * Checks if the contract is a proxy contract using the Open Zeppelin Unstructured Storage Pattern (not an official standard) * @param {string} contractAddress - The address of the contract * @param {number | string} [blockNumber] - Optional. Retrieve proxy details at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<{ * address: string, * isProxy: boolean, * implementationAddress: string | null, * proxyType: string | null * }>} The proxy details * @see https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code (USDC token - also mentioned in EIP1967) * @see https://ethereum.stackexchange.com/questions/99812/finding-the-address-of-the-proxied-to-address-of-a-proxy * @see https://blog.openzeppelin.com/proxy-patterns * @see https://github.com/OpenZeppelin/openzeppelin-labs/tree/master/upgradeability_using_unstructured_storage * @see https://github.com/OpenZeppelin/openzeppelin-labs/blob/master/upgradeability_using_unstructured_storage/contracts/UpgradeabilityProxy.sol */ async isOZUnstructuredStorageProxy(contractAddress, blockNumber = 'latest') { const result = { address: contractAddress, isProxy: false, implementationAddress: null, proxyType: null }; const implementationSlot = '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3'; let implementationSlotValue; try { implementationSlotValue = await this.getStorageSlotValueFromNode(contractAddress, implementationSlot, blockNumber); } catch (err) { this.log.warn(`[${contractAddress}] Error checking implementation slot for ${_types.PROXY_TYPES.OZUnstructuredStorage}: ${err}`); return Promise.reject(err); } if ((0, _utils.notZero)(implementationSlotValue)) { result.proxyType = _types.PROXY_TYPES.OZUnstructuredStorage; result.isProxy = true; result.implementationAddress = (0, _utils.formatAddressFromSlot)(implementationSlotValue); return result; } // Not a proxy contract return result; } /** * Retrieves the value of a storage slot for a specific contract from the node. * @param {string} contractAddress - The address of the contract * @param {string} slot - The slot to retrieve the value from * @param {number | string} [blockNumber] - Optional. Retrieve storage slot value at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<string>} The value of the storage slot */ async getStorageSlotValueFromNode(contractAddress, slot, blockNumber = 'latest') { if (typeof blockNumber === 'number') { // Convert to hex blockNumber = (0, _utils.toHex)(blockNumber); } return this.nod3.eth.getStorageAt(contractAddress, slot, blockNumber); } /** * Retrieves the code of a contract from the node. * @param {string} contractAddress - The address of the contract * @param {number | string} [blockNumber] - Optional. Retrieve contract code at the given block number. Can be a block number or a tag. Defaults to tag 'latest'. * @returns {Promise<string>} The contract code */ async getContractCodeFromNode(contractAddress, blockNumber = 'latest') { if (typeof blockNumber === 'number') { // Convert to hex blockNumber = (0, _utils.toHex)(blockNumber); } return this.nod3.eth.getContractCodeAt(contractAddress, blockNumber); } /** * Retrieves the interfaces of the contract based on the methods. * @param {Array} methods - The methods of the contract */ getInterfacesByMethods(methods) { const interfaces = Object.keys(_interfacesIds.default); const mappedInterfaces = interfaces.map((i) => [i, (0, _rskUtils.includesAll)(methods, _interfacesIds.default[i].methods)]); const reducedInterfaces = mappedInterfaces.reduce((obj, value) => { obj[value[0]] = value[1]; return obj; }, {}); return this.mapInterfacesToERCs(reducedInterfaces); } }exports.ContractParser = ContractParser;var _default = exports.default = ContractParser;