'use strict' import { hash } from '@remix-project/remix-lib' import { bufferToHex, setLengthLeft, toBuffer, addHexPrefix } from '@ethereumjs/util' import stringSimilarity from 'string-similarity' /* contains misc util: @TODO should be split - hex conversion - binary search - CALL related look up - sha3 calculation - swarm hash extraction - bytecode comparison */ /* ints: IntArray */ /** * Converts a hex string to an array of integers. */ export function hexToIntArray (hexString) { if (hexString.slice(0, 2) === '0x') { hexString = hexString.slice(2) } const integers = [] for (let i = 0; i < hexString.length; i += 2) { integers.push(parseInt(hexString.slice(i, i + 2), 16)) } return integers } /* ints: list of BNs */ export function hexListFromBNs (bnList) { const ret = [] for (const k in bnList) { const v = bnList[k].toString(16) ret.push('0x' + v.padStart(64, '0')) } return ret } export function toHexPaddedString(v: bigint | string): string { if (v) { if (typeof v === 'string') { return v.startsWith('0x') ? v : '0x' + v } else { return '0x' + v.toString(16).padStart(64, '0') } } else return '0x' + '0'.padStart(64, '0') } /* ints: ints: IntArray */ export function formatMemory (mem) { const hexMem = bufferToHex(mem).substr(2) const ret = [] for (let k = 0; k < hexMem.length; k += 32) { const row = hexMem.substr(k, 32) ret.push(row) } return ret } /* Binary Search: Assumes that @arg array is sorted increasingly return largest i such that array[i] <= target; return -1 if array[0] > target || array is empty */ export function findLowerBound (target, array) { let start = 0 let length = array.length while (length > 0) { const half = length >> 1 const middle = start + half if (array[middle] <= target) { length = length - 1 - half start = middle + 1 } else { length = half } } return start - 1 } /* Binary Search: Assumes that @arg array is sorted increasingly return largest array[i] such that array[i] <= target; return null if array[0] > target || array is empty */ export function findLowerBoundValue (target, array) { const index = findLowerBound(target, array) return index >= 0 ? array[index] : null } /* Binary Search: Assumes that @arg array is sorted increasingly return Return i such that |array[i] - target| is smallest among all i and -1 for an empty array. Returns the smallest i for multiple candidates. */ export function findClosestIndex (target, array): number { if (array.length === 0) { return -1 } const index = findLowerBound(target, array) if (index < 0) { return 0 } else if (index >= array.length - 1) { return array.length - 1 } else { const middle = (array[index] + array[index + 1]) / 2 return target <= middle ? index : index + 1 } } /** * Find the call from @args rootCall which contains @args index (recursive) * * @param {Int} index - index of the vmtrace * @param {Object} rootCall - call tree, built by the trace analyser * @return {Object} - return the call which include the @args index */ export function findCall (index, rootCall) { const ret = buildCallPath(index, rootCall) return ret[ret.length - 1] } /** * Find calls path from @args rootCall which leads to @args index (recursive) * * @param {Int} index - index of the vmtrace * @param {Object} rootCall - call tree, built by the trace analyser * @return {Array} - return the calls path to @args index */ export function buildCallPath (index, rootCall) { const ret = [] findCallInternal(index, rootCall, ret) return ret } /** * sha3 the given @arg value (left pad to 32 bytes) * * @param {String} value - value to sha3 * @return {Object} - return sha3ied value */ // eslint-disable-next-line camelcase export function sha3_256 (value) { value = toBuffer(addHexPrefix(value)) const retInBuffer: Buffer = hash.keccak(setLengthLeft(value, 32)) return bufferToHex(retInBuffer) } /** * return a regex which extract the swarmhash from the bytecode. * * @return {RegEx} */ export function swarmHashExtraction () { return /a165627a7a72305820([0-9a-f]{64})0029$/ } /** * return a regex which extract the swarmhash from the bytecode, from POC 0.3 * * @return {RegEx} */ export function swarmHashExtractionPOC31 () { return /a265627a7a72315820([0-9a-f]{64})64736f6c6343([0-9a-f]{6})0032$/ } /** * return a regex which extract the swarmhash from the bytecode, from POC 0.3 * * @return {RegEx} */ export function swarmHashExtractionPOC32 () { return /a265627a7a72305820([0-9a-f]{64})64736f6c6343([0-9a-f]{6})0032$/ } /** * return a regex which extract the cbor encoded metadata : {"ipfs": , "solc": } from the bytecode. * ref https://solidity.readthedocs.io/en/v0.6.6/metadata.html?highlight=ipfs#encoding-of-the-metadata-hash-in-the-bytecode * @return {RegEx} */ export function cborEncodedValueExtraction () { return /64697066735822([0-9a-f]{68})64736f6c6343([0-9a-f]{6})0033$/ } /** * return a regex which extract the input parameters from the bytecode * * @return {RegEx} */ export function inputParametersExtraction () { return /64697066735822[0-9a-f]{68}64736f6c6343[0-9a-f]{6}0033(.*)$/ } export function extractcborMetadata (value) { const cbor = value.match(cborEncodedValueExtraction()) if (cbor && cbor[0]) value = value.replace(cbor[0], '') return value } export function extractSwarmHash (value) { value = value.replace(swarmHashExtraction(), '') value = value.replace(swarmHashExtractionPOC31(), '') value = value.replace(swarmHashExtractionPOC32(), '') return value } export function extractinputParameters (value) { const inputsParam = getinputParameters(value) if (inputsParam) value = value.replace(inputsParam, '') return value } export function getinputParameters (value) { const regex = value.match(inputParametersExtraction()) if (regex && regex[1]) { return regex[1] } else return '' } /** * Compare bytecode. return true if the code is equal (handle swarm hash and library references) * @param {String} code1 - the bytecode that is actually deployed (contains resolved library reference and a potentially different swarmhash) * @param {String} code2 - the bytecode generated by the compiler (contains unresolved library reference and a potentially different swarmhash) this will return false if the generated bytecode is empty (abstract contract cannot be deployed) * * @return {bool} */ export function compareByteCode (code1, code2) { if (code1 === code2) return true if (code2 === '0x') return false // abstract contract. see comment if (code1 === '0x00' || code2 === '0x00' && code1 !== code2) return false // // This can be removed some time once YUL returns correct bytecode if (code2.substr(2, 46) === '7300000000000000000000000000000000000000003014') { // testing the following signature: PUSH20 00..00 ADDRESS EQ // in the context of a library, that slot contains the address of the library (pushed by the compiler to avoid calling library other than with a DELEGATECALL) // if code2 is not a library, well we still suppose that the comparison remain relevant even if we remove some information from `code1` code1 = replaceLibReference(code1, 4) } let pos = -1 while ((pos = code2.search(/__(.*)__/)) !== -1) { code2 = replaceLibReference(code2, pos) code1 = replaceLibReference(code1, pos) } code1 = removeImmutableReference(code1, code2) code1 = extractinputParameters(code1) code1 = extractSwarmHash(code1) code1 = extractcborMetadata(code1) code2 = extractinputParameters(code2) code2 = extractSwarmHash(code2) code2 = extractcborMetadata(code2) if (code1 && code2) { if (code1.length !== code2.length) { // if the length isn't the same, we have an issue with extracting the metadata hash. const minLength = code1.length > code2.length ? code2.length: code1.length code1 = code1.substr(0, minLength - 10) code2 = code2.substr(0, minLength - 10) } const compare = stringSimilarity.compareTwoStrings(code1, code2) return compare == 1 } return false } /* util extracted out from remix-ide. @TODO split this file, cause it mix real util fn with solidity related stuff ... */ export function groupBy (arr, key) { return arr.reduce((sum, item) => { const groupByVal = item[key] const groupedItems = sum[groupByVal] || [] groupedItems.push(item) sum[groupByVal] = groupedItems return sum }, {}) } export function concatWithSeparator (list, separator) { return list.reduce((sum, item) => sum + item + separator, '').slice(0, -separator.length) } export function escapeRegExp (str) { return str.replace(/[-[\]/{}()+?.\\^$|]/g, '\\$&') } function replaceLibReference (code, pos) { return code.substring(0, pos) + '0000000000000000000000000000000000000000' + code.substring(pos + 40) } function removeByIndex (code, index, length, emptyRef) { if (!code) return code return code.slice(0, index) + emptyRef + code.slice(index + length) } function removeImmutableReference (code1, code2) { try { const refOccurrence = code2.match(/7f0000000000000000000000000000000000000000000000000000000000000000/g) if (!refOccurrence) return code1 let offset = 0 refOccurrence.map((value) => { offset = code2.indexOf(value, offset) code1 = removeByIndex(code1, offset, value.length, '7f0000000000000000000000000000000000000000000000000000000000000000') offset = offset + 1 }) } catch (e) { console.log('error removeImmutableReference', e) } return code1 } function findCallInternal (index, rootCall, callsPath) { const calls = Object.keys(rootCall.calls) const ret = rootCall callsPath.push(rootCall) for (const k in calls) { const subCall = rootCall.calls[calls[k]] if (index >= subCall.start && index <= subCall.return) { findCallInternal(index, subCall, callsPath) break } } return ret }