UNPKG

@truffle/codec

Version:

Library for encoding and decoding smart contract data

424 lines 19 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.storageSize = exports.getStateAllocations = exports.getStorageAllocations = exports.UnknownBaseContractIdError = void 0; const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)("codec:storage:allocate"); const Compiler = __importStar(require("../../compiler")); const Common = __importStar(require("../../common")); const Basic = __importStar(require("../../basic")); const Utils = __importStar(require("../utils")); const Ast = __importStar(require("../../ast")); const Evm = __importStar(require("../../evm")); const Format = __importStar(require("../../format")); const bn_js_1 = __importDefault(require("bn.js")); const partition_1 = __importDefault(require("lodash/partition")); class UnknownBaseContractIdError extends Error { constructor(derivedId, derivedName, derivedKind, baseId) { const message = `Cannot locate base contract ID ${baseId} of ${derivedKind} ${derivedName} (ID ${derivedId})`; super(message); this.name = "UnknownBaseContractIdError"; this.derivedId = derivedId; this.derivedName = derivedName; this.derivedKind = derivedKind; this.baseId = baseId; } } exports.UnknownBaseContractIdError = UnknownBaseContractIdError; //contracts contains only the contracts to be allocated; any base classes not //being allocated should just be in referenceDeclarations function getStorageAllocations(userDefinedTypesByCompilation) { let allocations = {}; for (const compilation of Object.values(userDefinedTypesByCompilation)) { const { compiler, types: userDefinedTypes } = compilation; for (const dataType of Object.values(compilation.types)) { if (dataType.typeClass === "struct") { try { allocations = allocateStruct(dataType, userDefinedTypes, allocations, compiler); } catch (_a) { //if allocation fails... oh well, allocation fails, we do nothing and just move on :P //note: a better way of handling this would probably be to *mark* it //as failed rather than throwing an exception as that would lead to less //recomputation, but this is simpler and I don't think the recomputation //should really be a problem } } } } return allocations; } exports.getStorageAllocations = getStorageAllocations; /** * This function gets allocations for the state variables of the contracts; * this is distinct from getStorageAllocations, which gets allocations for * storage structs. * * While mostly state variables are kept in storage, constant ones are not. * And immutable ones, once those are introduced, will be kept in code! * (But those don't exist yet so this function doesn't handle them yet.) */ function getStateAllocations(contracts, referenceDeclarations, userDefinedTypes, storageAllocations, existingAllocations = {}) { let allocations = existingAllocations; for (const contractInfo of contracts) { let { contractNode: contract, immutableReferences, compiler, compilationId } = contractInfo; try { allocations = allocateContractState(contract, immutableReferences, compilationId, compiler, referenceDeclarations[compilationId], userDefinedTypes, storageAllocations, allocations); } catch (_a) { //we're just going to allow failure here and catch the problem elsewhere } } return allocations; } exports.getStateAllocations = getStateAllocations; function allocateStruct(dataType, userDefinedTypes, existingAllocations, compiler) { //NOTE: dataType here should be a *stored* type! //it is up to the caller to take care of this return allocateMembers(dataType.id, dataType.memberTypes, userDefinedTypes, existingAllocations, compiler); } function allocateMembers(parentId, members, userDefinedTypes, existingAllocations, compiler) { let offset = 0; //will convert to BN when placing in slot let index = Evm.Utils.WORD_SIZE - 1; //don't allocate things that have already been allocated if (parentId in existingAllocations) { return existingAllocations; } let allocations = Object.assign({}, existingAllocations); //otherwise, we'll be adding to this, so we better clone //otherwise, we need to allocate let memberAllocations = []; for (const member of members) { let size; ({ size, allocations } = storageSizeAndAllocate(member.type, userDefinedTypes, allocations, compiler)); //if it's sized in words (and we're not at the start of slot) we need to start on a new slot //if it's sized in bytes but there's not enough room, we also need a new slot if (Utils.isWordsLength(size) ? index < Evm.Utils.WORD_SIZE - 1 : size.bytes > index + 1) { index = Evm.Utils.WORD_SIZE - 1; offset += 1; } //otherwise, we remain in place let range; if (Utils.isWordsLength(size)) { //words case range = { from: { slot: { offset: new bn_js_1.default(offset) //start at the current slot... }, index: 0 //...at the beginning of the word. }, to: { slot: { offset: new bn_js_1.default(offset + size.words - 1) //end at the current slot plus # of words minus 1... }, index: Evm.Utils.WORD_SIZE - 1 //...at the end of the word. } }; } else { //bytes case range = { from: { slot: { offset: new bn_js_1.default(offset) //start at the current slot... }, index: index - (size.bytes - 1) //...early enough to fit what's being allocated. }, to: { slot: { offset: new bn_js_1.default(offset) //end at the current slot... }, index: index //...at the current position. } }; } memberAllocations.push({ name: member.name, type: member.type, pointer: { location: "storage", range } }); //finally, adjust the current position. //if it was sized in words, move down that many slots and reset position w/in slot if (Utils.isWordsLength(size)) { offset += size.words; index = Evm.Utils.WORD_SIZE - 1; } //if it was sized in bytes, move down an appropriate number of bytes. else { index -= size.bytes; //but if this puts us into the next word, move to the next word. if (index < 0) { index = Evm.Utils.WORD_SIZE - 1; offset += 1; } } } //finally, let's determine the overall siz; we're dealing with a struct, so //the size is measured in words //it's one plus the last word used, i.e. one plus the current word... unless the //current word remains entirely unused, then it's just the current word //SPECIAL CASE: if *nothing* has been used, allocate a single word (that's how //empty structs behave in versions where they're legal) let totalSize; if (index === Evm.Utils.WORD_SIZE - 1 && offset !== 0) { totalSize = { words: offset }; } else { totalSize = { words: offset + 1 }; } //having made our allocation, let's add it to allocations! allocations[parentId] = { members: memberAllocations, size: totalSize }; //...and we're done! return allocations; } function getStateVariables(contractNode) { // process for state variables return contractNode.nodes.filter((node) => node.nodeType === "VariableDeclaration" && node.stateVariable); } function allocateContractState(contract, immutableReferences, compilationId, compiler, referenceDeclarations, userDefinedTypes, storageAllocations, existingAllocations = {}) { //we're going to do a 2-deep clone here let allocations = Object.assign({}, ...Object.entries(existingAllocations).map(([compilationId, compilationAllocations]) => ({ [compilationId]: Object.assign({}, compilationAllocations) }))); if (!immutableReferences) { immutableReferences = {}; //also, let's set this up for convenience } //base contracts are listed from most derived to most base, so we //have to reverse before processing, but reverse() is in place, so we //clone with slice first let linearizedBaseContractsFromBase = contract.linearizedBaseContracts.slice().reverse(); //first, let's get all the variables under consideration let variables = [].concat(...linearizedBaseContractsFromBase.map((id) => { let baseNode = referenceDeclarations[id]; if (baseNode === undefined) { throw new UnknownBaseContractIdError(contract.id, contract.name, contract.contractKind, id); } return getStateVariables(baseNode).map(definition => ({ definition, definedIn: baseNode })); })); //just in case the constant field ever gets removed const isConstant = (definition) => definition.constant || definition.mutability === "constant"; //now: we split the variables into storage, constant, and code let [constantVariables, variableVariables] = (0, partition_1.default)(variables, variable => isConstant(variable.definition)); //why use this function instead of just checking //definition.mutability? //because of a bug in Solidity 0.6.5 that causes the mutability field //not to exist. So, we also have to check against immutableReferences. const isImmutable = (definition) => definition.mutability === "immutable" || definition.id.toString() in immutableReferences; let [immutableVariables, storageVariables] = (0, partition_1.default)(variableVariables, variable => isImmutable(variable.definition)); //transform storage variables into data types const storageVariableTypes = storageVariables.map(variable => ({ name: variable.definition.name, type: Ast.Import.definitionToType(variable.definition, compilationId, compiler) })); //let's allocate the storage variables using a fictitious ID const id = "-1"; const storageVariableStorageAllocations = allocateMembers(id, storageVariableTypes, userDefinedTypes, storageAllocations, compiler)[id]; //transform to new format const storageVariableAllocations = storageVariables.map(({ definition, definedIn }, index) => ({ definition, definedIn, compilationId, pointer: storageVariableStorageAllocations.members[index].pointer })); //now let's create allocations for the immutables let immutableVariableAllocations = immutableVariables.map(({ definition, definedIn }) => { let references = immutableReferences[definition.id.toString()] || []; let pointer; if (references.length === 0) { pointer = { location: "nowhere" }; } else { pointer = { location: "code", start: references[0].start, length: references[0].length }; } return { definition, definedIn, compilationId, pointer }; }); //and let's create allocations for the constants let constantVariableAllocations = constantVariables.map(({ definition, definedIn }) => ({ definition, definedIn, compilationId, pointer: { location: "definition", definition: definition.value } })); //now, reweave the three together let contractAllocation = []; for (let variable of variables) { let arrayToGrabFrom = isConstant(variable.definition) ? constantVariableAllocations : isImmutable(variable.definition) ? immutableVariableAllocations : storageVariableAllocations; contractAllocation.push(arrayToGrabFrom.shift()); //note that push and shift both modify! } //finally, set things and return if (!allocations[compilationId]) { allocations[compilationId] = {}; } allocations[compilationId][contract.id] = { members: contractAllocation }; return allocations; } //NOTE: This wrapper function is for use in decoding ONLY, after allocation is done. //The allocator should (and does) instead use a direct call to storageSizeAndAllocate, //not to the wrapper, because it may need the allocations returned. function storageSize(dataType, userDefinedTypes, allocations, compiler) { return storageSizeAndAllocate(dataType, userDefinedTypes, allocations, compiler).size; } exports.storageSize = storageSize; function storageSizeAndAllocate(dataType, userDefinedTypes, existingAllocations, compiler) { //we'll only directly handle reference types here; //direct types will be handled by dispatching to Basic.Allocate.byteLength //in the default case switch (dataType.typeClass) { case "bytes": { switch (dataType.kind) { case "static": //really a basic type :) return { size: { bytes: Basic.Allocate.byteLength(dataType, userDefinedTypes) }, allocations: existingAllocations }; case "dynamic": return { size: { words: 1 }, allocations: existingAllocations }; } } case "string": case "mapping": return { size: { words: 1 }, allocations: existingAllocations }; case "array": { switch (dataType.kind) { case "dynamic": return { size: { words: 1 }, allocations: existingAllocations }; case "static": //static array case const length = dataType.length.toNumber(); //warning! but if it's too big we have a problem if (length === 0) { //in versions of Solidity where it's legal, arrays of length 0 still take up 1 word return { size: { words: 1 }, allocations: existingAllocations }; } let { size: baseSize, allocations } = storageSizeAndAllocate(dataType.baseType, userDefinedTypes, existingAllocations); if (!Utils.isWordsLength(baseSize)) { //bytes case const perWord = Math.floor(Evm.Utils.WORD_SIZE / baseSize.bytes); debug("length %o", length); const numWords = Math.ceil(length / perWord); return { size: { words: numWords }, allocations }; } else { //words case return { size: { words: baseSize.words * length }, allocations }; } } } case "struct": { let allocations = existingAllocations; let allocation = allocations[dataType.id]; //may be undefined! if (allocation === undefined) { //if we don't find an allocation, we'll have to do the allocation ourselves const storedType = (userDefinedTypes[dataType.id]); if (!storedType) { throw new Common.UnknownUserDefinedTypeError(dataType.id, Format.Types.typeString(dataType)); } allocations = allocateStruct(storedType, userDefinedTypes, existingAllocations); allocation = allocations[dataType.id]; } //having found our allocation, we can just look up its size return { size: allocation.size, allocations }; } case "userDefinedValueType": if (Compiler.Utils.solidityFamily(compiler) === "0.8.7+") { //UDVTs were introduced in Solidity 0.8.8. However, in that version, //and that version only, they have a bug where they always take up a //full word in storage regardless of the size of the underlying type. return { size: { words: 1 }, allocations: existingAllocations }; } //otherwise, treat them normally //DELIBERATE FALL-TRHOUGH default: //otherwise, it's a direct type return { size: { bytes: Basic.Allocate.byteLength(dataType, userDefinedTypes) }, allocations: existingAllocations }; } } //# sourceMappingURL=index.js.map