sol2uml
Version:
Solidity contract visualisation tool.
315 lines • 14.9 kB
JavaScript
;
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