UNPKG

sol2uml

Version:

Solidity contract visualisation tool.

315 lines 14.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.escapeString = exports.convert2String = exports.dynamicSlotSize = exports.getSlotValue = exports.getSlotValues = exports.parseValue = exports.addSlotValues = void 0; const axios_1 = __importDefault(require("axios")); const umlClass_1 = require("./umlClass"); const ethers_1 = require("ethers"); const SlotValueCache_1 = require("./SlotValueCache"); const debug = require('debug')('sol2uml'); const commify = (value) => { const match = value.match(/^(-?)(\d+)(\.(.*))?$/); if (!match) return value; const neg = match[1]; const whole = match[2]; const frac = match[4] ? '.' + match[4] : ''; return neg + whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + frac; }; /** * Adds the slot values to the variables in the storage section. * This can be rerun for a section as it will only get if the slot value * does not exist. * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy * @param contractAddress Contract address to get the storage slot values from. * If contract is proxied, use proxy and not the implementation contract. * @param storageSection is mutated with the slot values added to the variables * @param arrayItems the number of items to display at the start and end of an array * @param blockTag block number or `latest` */ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, blockTag) => { const valueVariables = storageSection.variables.filter((variable) => variable.getValue && !variable.slotValue); if (valueVariables.length === 0) return; // for each variable, add all the slots used by the variable. const slots = []; valueVariables.forEach((variable) => { if (variable.offset) { slots.push(BigInt(variable.offset)); } else { for (let i = 0; variable.fromSlot + i <= variable.toSlot; i++) { if (variable.attributeType === umlClass_1.AttributeType.Array && i >= arrayItems && i < variable.toSlot - arrayItems) { continue; } slots.push(variable.fromSlot + i); } } }); // remove duplicate slot numbers const uniqueFromSlots = [...new Set(slots)]; // Convert slot numbers to BigInts and offset dynamic arrays const slotKeys = uniqueFromSlots.map((fromSlot) => { if (storageSection.offset) { return BigInt(storageSection.offset) + BigInt(fromSlot); } return BigInt(fromSlot); }); // Get the contract slot values from the node provider const values = await (0, exports.getSlotValues)(url, contractAddress, slotKeys, blockTag); // For each slot value retrieved values.forEach((value, i) => { // Get the corresponding slot number for the slot value const fromSlot = uniqueFromSlots[i]; // For each variable in the storage section for (const variable of storageSection.variables) { if (variable.getValue && variable.offset && BigInt(variable.offset) === BigInt(fromSlot)) { debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.offset}`); variable.slotValue = value; // parse variable value from slot data if (variable.displayValue) { variable.parsedValue = (0, exports.parseValue)(variable); } } else if (variable.getValue && variable.fromSlot === fromSlot) { debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.fromSlot} offset ${storageSection.offset}`); variable.slotValue = value; // parse variable value from slot data if (variable.displayValue) { variable.parsedValue = (0, exports.parseValue)(variable); } } // if variable is past the slot that has the value else if (variable.toSlot && BigInt(variable.toSlot) > BigInt(fromSlot)) { break; } } }); }; exports.addSlotValues = addSlotValues; const parseValue = (variable) => { if (!variable.slotValue) return undefined; const start = 66 - (variable.byteOffset + variable.byteSize) * 2; const end = 66 - variable.byteOffset * 2; const variableValue = variable.slotValue.substring(start, end); try { // Contracts, structs and enums if (variable.attributeType === umlClass_1.AttributeType.UserDefined) { return parseUserDefinedValue(variable, variableValue); } if (variable.attributeType === umlClass_1.AttributeType.Elementary) return parseElementaryValue(variable, variableValue); // dynamic arrays if (variable.attributeType === umlClass_1.AttributeType.Array && variable.dynamic) { return (0, ethers_1.formatUnits)('0x' + variableValue, 0); } return undefined; } catch (err) { throw Error(`Failed to parse variable ${variable.name} of type ${variable.type}, value "${variableValue}"`, { cause: err }); } }; exports.parseValue = parseValue; const parseUserDefinedValue = (variable, variableValue) => { // TODO need to handle User Defined Value Types introduced in Solidity // https://docs.soliditylang.org/en/v0.8.18/types.html#user-defined-value-types // https://blog.soliditylang.org/2021/09/27/user-defined-value-types/ // using byteSize is crude and will be incorrect for aliases types like int160 or uint160 if (variable.byteSize === 20) { return (0, ethers_1.getAddress)('0x' + variableValue); } // this will also be wrong if the alias is to a 1 byte type. eg bytes1, int8 or uint8 if (variable.byteSize === 1) { // assume 1 byte is an enum so convert value to enum index number const index = Number(BigInt('0x' + variableValue)); // lookup enum value if its available return variable?.enumValues ? variable?.enumValues[index] : undefined; } // we don't parse if a struct which has a size of 32 bytes return undefined; }; const parseElementaryValue = (variable, variableValue) => { // Elementary types if (variable.type === 'bool') { if (variableValue === '00') return 'false'; if (variableValue === '01') return 'true'; throw Error(`Failed to parse bool variable "${variable.name}" in slot ${variable.fromSlot}, offset ${variable.byteOffset} and slot value "${variableValue}"`); } if (variable.type === 'string' || variable.type === 'bytes') { if (variable.dynamic) { const lastByte = variable.slotValue.slice(-2); const size = BigInt('0x' + lastByte); // Check if the last bit is set by AND the size with 0x01 if ((size & 1n) === 1n) { // Return the number of chars or bytes return ((BigInt(variable.slotValue) - 1n) / 2n).toString(); } // The last byte holds the length of the string or bytes in the slot const valueHex = '0x' + variableValue.slice(0, Number(size)); if (variable.type === 'bytes') return valueHex; return `\\"${(0, exports.convert2String)(valueHex)}\\"`; } if (variable.type === 'bytes') return '0x' + variableValue; return `\\"${(0, exports.convert2String)('0x' + variableValue)}\\"`; } if (variable.type === 'address') { return (0, ethers_1.getAddress)('0x' + variableValue); } if (variable.type.match(/^uint([0-9]*)$/)) { const parsedValue = (0, ethers_1.formatUnits)('0x' + variableValue, 0); return commify(parsedValue); } if (variable.type.match(/^bytes([0-9]+)$/)) { return '0x' + variableValue; } if (variable.type.match(/^int([0-9]*)/)) { // parse variable value as an unsigned number let rawValue = BigInt('0x' + variableValue); // parse the number of bits const result = variable.type.match(/^int([0-9]*$)/); const bitSize = result[1] ? Number(result[1]) : 256; // Convert the number of bits to the number of hex characters const hexSize = bitSize / 4; // bit mask has a leading 1 and the rest 0. 0x8 = 1000 binary const mask = BigInt('0x80' + '0'.repeat(hexSize - 2)); // is the first bit a 1? const negative = rawValue & mask; if (negative > 0n) { // Convert unsigned number to a signed negative const negativeOne = BigInt('0xFF' + 'F'.repeat(hexSize - 2)); rawValue = (negativeOne - rawValue + 1n) * -1n; } const parsedValue = (0, ethers_1.formatUnits)(rawValue, 0); return commify(parsedValue); } // add fixed point numbers when they are supported by Solidity return undefined; }; let jsonRpcId = 0; /** * Get storage slot values from JSON-RPC API provider. * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy * @param contractAddress Contract address to get the storage slot values from. * If proxied, use proxy and not the implementation contract. * @param slotKeys array of 32 byte slot keys as BigNumbers. * @param blockTag block number or `latest` * @return slotValues array of 32 byte slot values as hexadecimal strings */ const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest') => { try { if (slotKeys.length === 0) { return []; } const block = blockTag === 'latest' ? blockTag : (0, ethers_1.toQuantity)(BigInt(blockTag)); // get cached values and missing slot keys from the cache const { cachedValues, missingKeys } = SlotValueCache_1.SlotValueCache.readSlotValues(slotKeys); // If all values are in the cache then just return the cached values if (missingKeys.length === 0) { return cachedValues; } // Check we are pointing to the correct chain by checking the contract has code const provider = new ethers_1.JsonRpcProvider(url); const code = await provider.getCode(contractAddress, block); if (!code || code === '0x') { const msg = `Address ${contractAddress} has no code. Check your "-u, --url" option or "NODE_URL" environment variable is pointing to the correct node.\nurl: ${url}`; console.error(msg); throw Error(msg); } debug(`About to get ${slotKeys.length} storage values for ${contractAddress} at block ${blockTag} from slot ${missingKeys[0].toString()}`); // Get the values for the missing slot keys const payload = missingKeys.map((key) => ({ id: (jsonRpcId++).toString(), jsonrpc: '2.0', method: 'eth_getStorageAt', params: [contractAddress, key, block], })); const response = await axios_1.default.post(url, payload); if (response.data?.error?.message) { throw Error(response.data.error.message); } if (response.data.length !== missingKeys.length) { throw Error(`Requested ${missingKeys.length} storage slot values but only got ${response.data.length}`); } const responseData = response.data; const sortedResponses = responseData.sort((a, b) => Number(a.id) - Number(b.id)); const missingValues = sortedResponses.map((data) => { if (data.error) { throw Error(`json rpc call with id ${data.id} failed to get storage values: ${data.error?.message}`); } return '0x' + data.result.toUpperCase().slice(2); }); // add new values to the cache and return the merged slot values return SlotValueCache_1.SlotValueCache.addSlotValues(slotKeys, missingKeys, missingValues); } catch (err) { throw Error(`Failed to get ${slotKeys.length} storage values for contract ${contractAddress} from ${url}`, { cause: err }); } }; exports.getSlotValues = getSlotValues; /** * Get storage slot values from JSON-RPC API provider. * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy * @param contractAddress Contract address to get the storage slot values from. * If proxied, use proxy and not the implementation contract. * @param slotKey 32 byte slot key as a BigNumber. * @param blockTag block number or `latest` * @return slotValue 32 byte slot value as hexadecimal string */ const getSlotValue = async (url, contractAddress, slotKey, blockTag) => { debug(`About to get storage slot ${slotKey} value for ${contractAddress}`); const values = await (0, exports.getSlotValues)(url, contractAddress, [slotKey], blockTag); return values[0]; }; exports.getSlotValue = getSlotValue; /** * Calculates the number of string characters or bytes of a string or bytes type. * See the following for how string and bytes are stored in storage slots * https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html#bytes-and-string * @param variable the variable with the slotValue that is being sized * @return bytes the number of bytes of the dynamic slot. If static, zero is return. */ const dynamicSlotSize = (variable) => { try { if (!variable?.slotValue) throw Error(`Missing slot value.`); const last4bits = '0x' + variable.slotValue.slice(-1); const last4bitsNum = Number(BigInt(last4bits)); // If the last 4 bits is an even number then it's not a dynamic slot if (last4bitsNum % 2 === 0) return 0; const sizeRaw = Number(BigInt(variable.slotValue)); // Adjust the size to bytes return (sizeRaw - 1) / 2; } catch (err) { throw Error(`Failed to calculate dynamic slot size for variable "${variable?.name}" of type "${variable?.type}" with slot value ${variable?.slotValue}`, { cause: err }); } }; exports.dynamicSlotSize = dynamicSlotSize; const convert2String = (bytes) => { if (bytes === '0x0000000000000000000000000000000000000000000000000000000000000000') { return ''; } const rawString = (0, ethers_1.toUtf8String)(bytes); return (0, exports.escapeString)(rawString); }; exports.convert2String = convert2String; const escapeString = (text) => { return text.replace(/(?=[<>&"])/g, '\\'); }; exports.escapeString = escapeString; //# sourceMappingURL=slotValues.js.map