UNPKG

@truffle/codec

Version:

Library for encoding and decoding smart contract data

1,035 lines (1,034 loc) 57.2 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.getEventAllocations = exports.getReturndataAllocations = exports.getCalldataAllocations = exports.abiSizeInfo = exports.getAbiAllocations = exports.FallbackOutputAllocation = exports.Utils = void 0; const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)("codec:abi-data:allocate"); exports.Utils = __importStar(require("./utils")); const Import = __importStar(require("../import")); const AbiDataUtils = __importStar(require("../utils")); const web3_utils_1 = __importDefault(require("web3-utils")); const Evm = __importStar(require("../../evm")); const Common = __importStar(require("../../common")); const Conversion = __importStar(require("../../conversion")); const Ast = __importStar(require("../../ast")); const import_1 = require("../../contexts/import"); const Format = __importStar(require("../../format")); const partition_1 = __importDefault(require("lodash/partition")); exports.FallbackOutputAllocation = { kind: "returnmessage", selector: new Uint8Array(), allocationMode: "full" }; function getAbiAllocations(userDefinedTypes) { let allocations = {}; for (const dataType of Object.values(userDefinedTypes)) { if (dataType.typeClass === "struct") { try { allocations = allocateStruct(dataType, userDefinedTypes, allocations); } catch (_) { //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.getAbiAllocations = getAbiAllocations; function allocateStruct(dataType, userDefinedTypes, existingAllocations) { //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); } //note: we will still allocate circular structs, even though they're not allowed in the abi, because it's //not worth the effort to detect them. However on mappings or internal functions, we'll vomit (allocate null) function allocateMembers(parentId, members, userDefinedTypes, existingAllocations, start = 0) { let dynamic = false; //note that we will mutate the start argument also! //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 let memberAllocations = []; for (const member of members) { let length; let dynamicMember; ({ size: length, dynamic: dynamicMember, allocations } = abiSizeAndAllocate(member.type, userDefinedTypes, allocations)); //vomit on illegal types in calldata -- note the short-circuit! if (length === undefined) { allocations[parentId] = null; return allocations; } let pointer = { location: "abi", start, length }; memberAllocations.push({ name: member.name, type: member.type, pointer }); start += length; dynamic = dynamic || dynamicMember; } allocations[parentId] = { members: memberAllocations, length: dynamic ? Evm.Utils.WORD_SIZE : start, dynamic }; return allocations; } //first return value is the actual size. //second return value is whether the type is dynamic //both will be undefined if type is a mapping or internal function //third return value is resulting allocations, INCLUDING the ones passed in function abiSizeAndAllocate(dataType, userDefinedTypes, existingAllocations) { switch (dataType.typeClass) { case "bool": case "address": case "contract": case "int": case "uint": case "fixed": case "ufixed": case "enum": case "userDefinedValueType": return { size: Evm.Utils.WORD_SIZE, dynamic: false, allocations: existingAllocations }; case "string": return { size: Evm.Utils.WORD_SIZE, dynamic: true, allocations: existingAllocations }; case "bytes": return { size: Evm.Utils.WORD_SIZE, dynamic: dataType.kind === "dynamic", allocations: existingAllocations }; case "mapping": return { allocations: existingAllocations }; case "function": switch (dataType.visibility) { case "external": return { size: Evm.Utils.WORD_SIZE, dynamic: false, allocations: existingAllocations }; case "internal": return { allocations: existingAllocations }; } case "array": { switch (dataType.kind) { case "dynamic": return { size: Evm.Utils.WORD_SIZE, dynamic: true, allocations: existingAllocations }; case "static": if (dataType.length.isZero()) { //arrays of length 0 are static regardless of base type return { size: 0, dynamic: false, allocations: existingAllocations }; } const { size: baseSize, dynamic, allocations } = abiSizeAndAllocate(dataType.baseType, userDefinedTypes, existingAllocations); return { //WARNING! The use of toNumber() here may throw an exception! //I'm judging this OK since if you have arrays that large we have bigger problems :P size: dataType.length.toNumber() * baseSize, dynamic, allocations }; } } case "struct": { let allocations = existingAllocations; let allocation = allocations[dataType.id]; 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[storedType.id]; } //having found our allocation, if it's not null, we can just look up its size and dynamicity if (allocation !== null) { return { size: allocation.length, dynamic: allocation.dynamic, allocations }; } //if it is null, this type doesn't go in the abi else { return { allocations }; } } case "tuple": { //Warning! Yucky wasteful recomputation here! let size = 0; let dynamic = false; //note that we don't just invoke allocateStruct here! //why not? because it has no ID to store the result in! //and we can't use a fake like -1 because there might be a recursive call to it, //and then the results would overwrite each other //I mean, we could do some hashing thing or something, but I think it's easier to just //copy the logic in this one case (sorry) for (let member of dataType.memberTypes) { let { size: memberSize, dynamic: memberDynamic } = abiSizeAndAllocate(member.type, userDefinedTypes, existingAllocations); size += memberSize; dynamic = dynamic || memberDynamic; } return { size, dynamic, allocations: existingAllocations }; } } } //assumes you've already done allocation! don't use if you haven't! /** * @protected */ function abiSizeInfo(dataType, allocations) { let { size, dynamic } = abiSizeAndAllocate(dataType, null, allocations); //the above line should work fine... as long as allocation is already done! //the middle argument, userDefinedTypes, is only needed during allocation //again, this function is only for use if allocation is done, so it's safe to pass null here return { size, dynamic }; } exports.abiSizeInfo = abiSizeInfo; //allocates an external call //NOTE: returns just a single allocation; assumes primary allocation is already complete! //NOTE: returns undefined if attempting to allocate a constructor but we don't have the //bytecode for the constructor function allocateCalldataAndReturndata(abiEntry, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler, constructorContext, deployedContext) { //first: determine the corresponding function node //(simultaneously: determine the offset) let node = undefined; let inputParametersFull; let outputParametersFull; let inputParametersAbi; let outputParametersAbi; let offset; //refers to INPUT offset; output offset is always 0 debug("allocating calldata and returndata"); switch (abiEntry.type) { case "constructor": if (!constructorContext) { return undefined; } let rawLength = constructorContext.binary.length; offset = (rawLength - 2) / 2; //number of bytes in 0x-prefixed bytestring //for a constructor, we only want to search the particular contract if (contractNode) { node = contractNode.nodes.find(functionNode => AbiDataUtils.definitionMatchesAbi( //note this needn't actually be a function node, but then it will //return false (well, unless it's a getter node!) abiEntry, functionNode, referenceDeclarations)); } //if we can't find it, we'll handle this below break; case "function": offset = Evm.Utils.SELECTOR_SIZE; //search through base contracts, from most derived (left) to most base (right) if (contractNode) { const linearizedBaseContracts = contractNode.linearizedBaseContracts; debug("linearized: %O", linearizedBaseContracts); node = findNodeAndContract(linearizedBaseContracts, referenceDeclarations, functionNode => AbiDataUtils.definitionMatchesAbi(abiEntry, functionNode, referenceDeclarations), contractNode).node; //may be undefined! that's OK! debug("found node: %o", Boolean(node)); } break; } //now: get the parameters (both full-mode & ABI) if (node) { switch (node.nodeType) { case "FunctionDefinition": //normal case inputParametersFull = node.parameters.parameters; outputParametersFull = node.returnParameters.parameters; //this exists even for constructors! break; case "VariableDeclaration": //getter case ({ inputs: inputParametersFull, outputs: outputParametersFull } = Ast.Utils.getterParameters(node, referenceDeclarations)); break; } } else { inputParametersFull = undefined; outputParametersFull = undefined; } inputParametersAbi = abiEntry.inputs; switch (abiEntry.type) { case "function": outputParametersAbi = abiEntry.outputs; break; case "constructor": //we just leave this empty for constructors outputParametersAbi = []; break; } //now: do the allocation! let { allocation: abiAllocationInput, mode: inputMode } = allocateDataArguments(inputParametersFull, inputParametersAbi, userDefinedTypes, abiAllocations, compilationId, compiler, offset); let { allocation: abiAllocationOutput, mode: outputMode } = allocateDataArguments(outputParametersFull, outputParametersAbi, userDefinedTypes, abiAllocations, compilationId, compiler //note no offset ); debug("modes: %s in, %s out", inputMode, outputMode); //finally: transform the allocation appropriately let inputArgumentsAllocation = abiAllocationInput.members.map(member => (Object.assign(Object.assign({}, member), { pointer: { location: "calldata", start: member.pointer.start, length: member.pointer.length } }))); let outputArgumentsAllocation = abiAllocationOutput.members.map(member => (Object.assign(Object.assign({}, member), { pointer: { location: "returndata", start: member.pointer.start, length: member.pointer.length } }))); let inputsAllocation = { abi: abiEntry, offset, arguments: inputArgumentsAllocation, allocationMode: inputMode }; let outputsAllocation; switch (abiEntry.type) { case "function": outputsAllocation = { selector: new Uint8Array(), arguments: outputArgumentsAllocation, allocationMode: outputMode, kind: "return" }; break; case "constructor": outputsAllocation = constructorOutputAllocation(deployedContext, contractNode, referenceDeclarations, outputMode); break; } return { input: inputsAllocation, output: outputsAllocation }; //TS chokes on this for some reason } //note: allocateEvent doesn't use this because it needs additional //handling for indexed parameters (maybe these can be unified in //the future though?) function allocateDataArguments(fullModeParameters, abiParameters, userDefinedTypes, abiAllocations, compilationId, compiler, offset = 0) { let allocationMode = fullModeParameters ? "full" : "abi"; //can degrade let parameterTypes; let abiAllocation; if (allocationMode === "full") { let id = "-1"; //fake ID that doesn't matter parameterTypes = fullModeParameters.map(parameter => ({ name: parameter.name, type: Ast.Import.definitionToType(parameter, compilationId, compiler) //if node is defined, compiler had also better be! })); debug("parameterTypes: %O", parameterTypes); //now: perform the allocation! try { abiAllocation = allocateMembers(id, parameterTypes, userDefinedTypes, abiAllocations, offset)[id]; } catch (_a) { //if something goes wrong, switch to ABI mdoe debug("falling back to ABI due to exception!"); allocationMode = "abi"; } } if (allocationMode === "abi") { //THIS IS DELIBERATELY NOT AN ELSE //this is the ABI case. we end up here EITHER //if node doesn't exist, OR if something went wrong //during allocation let id = "-1"; //fake irrelevant ID parameterTypes = abiParameters.map(parameter => ({ name: parameter.name, type: Import.abiParameterToType(parameter) })); abiAllocation = allocateMembers(id, parameterTypes, userDefinedTypes, abiAllocations, offset)[id]; } return { allocation: abiAllocation, mode: allocationMode }; } //allocates an event //NOTE: returns just a single allocation; assumes primary allocation is already complete! function allocateEvent(abiEntry, eventNode, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) { let parameterTypes; let nodeId; let id; //first: determine the corresponding event node //if we're doing inheritance processing, we search through base contracts, //from most derived (right) to most base (left) //if we're not doing inheritance processing (i.e. if eventNode was passed), //we search through *all* contracts, plus the top level! even though we //know the event node already, we still need to know where it's defined let node = undefined; let definedInNode = undefined; let definedIn = undefined; let allocationMode = "full"; //degrade to abi as needed debug("allocating ABI: %O", abiEntry); if (contractNode) { if (eventNode) { node = eventNode; //we already know this one! //note: we don't use findNodeAndContract here because it's meant for searching //through a list of base contracts, that's not really what's going on here //(we don't need all its code here anyway) definedInNode = Object.values(referenceDeclarations).find(possibleContractNode => possibleContractNode.nodeType === "ContractDefinition" && possibleContractNode.nodes.some((possibleEventNode) => possibleEventNode.id === node.id)); if (definedInNode && definedInNode.contractKind === "library" && definedInNode.id !== contractNode.id) { //skip library events! (unless this is the library they're from) //those are always considered in-play no matter what, //so we don't want to handle them here or we'd end up with them appearing twice return undefined; } //if we failed to find what it's in... presumably it was defined at the file level. //leave definedInNode undefined; it'll be handled below. } else { //first: check same contract for the event node = contractNode.nodes.find(eventNode => AbiDataUtils.definitionMatchesAbi( //note this needn't actually be an event node, but then it will //return false abiEntry, eventNode, referenceDeclarations)); //if we found the node, great! If not... if (node) { definedInNode = contractNode; } else { debug("didn't find node in base contract..."); //let's search for the node among the base contracts. //but if we find it... //[note: the following code is overcomplicated; it was used //when we were trying to get the actual node, it's overcomplicated //now that we're just determining its presence. oh well] let linearizedBaseContractsMinusSelf = contractNode.linearizedBaseContracts.slice(); linearizedBaseContractsMinusSelf.shift(); //remove self debug("checking contracts: %o", linearizedBaseContractsMinusSelf); node = findNodeAndContract(linearizedBaseContractsMinusSelf, referenceDeclarations, eventNode => AbiDataUtils.definitionMatchesAbi( //note this needn't actually be a event node, but then it will return false abiEntry, eventNode, referenceDeclarations) //don't pass deriveContractNode here, we're not checking the contract itself ).node; //may be undefined! that's OK! if (node) { //...if we find the node in an ancestor, we //deliberately *don't* allocate! instead such cases //will be handled during a later combination step debug("bailing out for later handling!"); debug("ABI: %O", abiEntry); return undefined; } } } } //otherwise, leave node undefined if (node) { debug("found node"); //if we found the node, let's also turn it into a type if (definedInNode) { definedIn = (Ast.Import.definitionToStoredType(definedInNode, compilationId, compiler)); //can skip reference declarations argument here } else { definedIn = null; //for file-level events, once they exist } //...and set the ID id = (0, import_1.makeTypeId)(node.id, compilationId); } else { //if no node, have to fall back into ABI mode debug("falling back to ABI because no node"); allocationMode = "abi"; } //now: construct the list of parameter types, attaching indexedness info //and overall position (for later reconstruction) let indexed; let nonIndexed; let abiAllocation; //the untransformed allocation for the non-indexed parameters if (allocationMode === "full") { nodeId = node.id.toString(); let parameters = node.parameters.parameters; parameterTypes = parameters.map(definition => ({ //note: if node is defined, compiler had better be defined, too! type: Ast.Import.definitionToType(definition, compilationId, compiler), name: definition.name, indexed: definition.indexed })); //now: split the list of parameters into indexed and non-indexed [indexed, nonIndexed] = (0, partition_1.default)(parameterTypes, (parameter) => parameter.indexed); try { //now: perform the allocation for the non-indexed parameters! abiAllocation = allocateMembers(nodeId, nonIndexed, userDefinedTypes, abiAllocations)[nodeId]; //note the implicit conversion from EventParameterInfo to NameTypePair } catch (_a) { allocationMode = "abi"; } } if (allocationMode === "abi") { //THIS IS DELIBERATELY NOT AN ELSE nodeId = "-1"; //fake irrelevant ID parameterTypes = abiEntry.inputs.map(abiParameter => ({ type: Import.abiParameterToType(abiParameter), name: abiParameter.name, indexed: abiParameter.indexed })); //now: split the list of parameters into indexed and non-indexed [indexed, nonIndexed] = (0, partition_1.default)(parameterTypes, (parameter) => parameter.indexed); //now: perform the allocation for the non-indexed parameters! abiAllocation = allocateMembers(nodeId, nonIndexed, userDefinedTypes, abiAllocations)[nodeId]; //note the implicit conversion from EventParameterInfo to NameTypePair } //now: transform the result appropriately const nonIndexedArgumentsAllocation = abiAllocation.members.map(member => (Object.assign(Object.assign({}, member), { pointer: { location: "eventdata", start: member.pointer.start, length: member.pointer.length } }))); //now: allocate the indexed parameters const startingTopic = abiEntry.anonymous ? 0 : 1; //if not anonymous, selector takes up topic 0 const indexedArgumentsAllocation = indexed.map(({ type, name }, position) => ({ type, name, pointer: { location: "eventtopic", topic: startingTopic + position } })); //finally: weave these back together let argumentsAllocation = []; for (let parameter of parameterTypes) { let arrayToGrabFrom = parameter.indexed ? indexedArgumentsAllocation : nonIndexedArgumentsAllocation; argumentsAllocation.push(arrayToGrabFrom.shift()); //note that push and shift both modify! } //...and return return { abi: abiEntry, contextHash: undefined, definedIn, id, arguments: argumentsAllocation, allocationMode, anonymous: abiEntry.anonymous }; } function allocateError(abiEntry, errorNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) { //first: if we got passed just a node & no abi entry, let id = undefined; let definedIn = undefined; let parametersFull = undefined; const parametersAbi = abiEntry.inputs; if (errorNode) { //first, set parametersFull parametersFull = errorNode.parameters.parameters; //now, set id id = (0, import_1.makeTypeId)(errorNode.id, compilationId); //now, set definedIn let contractNode = null; for (const node of Object.values(referenceDeclarations)) { if (node.nodeType === "ContractDefinition") { if (node.nodes.some((subNode) => subNode.id === errorNode.id)) { contractNode = node; break; } } //if we didn't find it, then contractNode is null //(and thus so will be definedIn) } if (contractNode === null) { definedIn = null; } else { definedIn = (Ast.Import.definitionToStoredType(contractNode, compilationId, compiler)); } } //otherwise, leave parametersFull, id, and definedIn undefined const { allocation: abiAllocation, mode: allocationMode } = allocateDataArguments(parametersFull, parametersAbi, userDefinedTypes, abiAllocations, compilationId, compiler, Evm.Utils.SELECTOR_SIZE //errors use a 4-byte selector ); //finally: transform the allocation appropriately const argumentsAllocation = abiAllocation.members.map(member => (Object.assign(Object.assign({}, member), { pointer: { location: "returndata", start: member.pointer.start, length: member.pointer.length } }))); const selector = Conversion.toBytes(AbiDataUtils.abiSelector(abiEntry)); return { kind: "revert", selector, abi: abiEntry, id, definedIn, arguments: argumentsAllocation, allocationMode }; } function getCalldataAllocationsForContract(abi, contractNode, constructorContext, deployedContext, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) { let allocations = { constructorAllocation: undefined, //(if it doesn't then it will remain as default) functionAllocations: {} }; if (!abi) { //if no ABI, can't do much! allocations.constructorAllocation = defaultConstructorAllocation(constructorContext, contractNode, referenceDeclarations, deployedContext); return allocations; } for (let abiEntry of abi) { if (AbiDataUtils.abiEntryIsObviouslyIllTyped(abiEntry) || AbiDataUtils.abiEntryHasStorageParameters(abiEntry)) { //the first of these conditions is a hack workaround for a Solidity bug. //the second of these is because... seriously? we're not handling these //(at least not for now!) (these only exist prior to Solidity 0.5.6, //thankfully) continue; } switch (abiEntry.type) { case "constructor": allocations.constructorAllocation = allocateCalldataAndReturndata(abiEntry, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler, constructorContext, deployedContext); debug("constructor alloc: %O", allocations.constructorAllocation); break; case "function": allocations.functionAllocations[AbiDataUtils.abiSelector(abiEntry)] = allocateCalldataAndReturndata(abiEntry, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler, constructorContext, deployedContext); break; default: //skip over fallback, error, and event break; } } if (!allocations.constructorAllocation) { //set a default constructor allocation if we haven't allocated one yet allocations.constructorAllocation = defaultConstructorAllocation(constructorContext, contractNode, referenceDeclarations, deployedContext); debug("default constructor alloc: %O", allocations.constructorAllocation); } return allocations; } function defaultConstructorAllocation(constructorContext, contractNode, referenceDeclarations, deployedContext) { if (!constructorContext) { return undefined; } const rawLength = constructorContext.binary.length; const offset = (rawLength - 2) / 2; //number of bytes in 0x-prefixed bytestring const input = { offset, abi: AbiDataUtils.DEFAULT_CONSTRUCTOR_ABI, arguments: [], allocationMode: "full" }; const output = constructorOutputAllocation(deployedContext, contractNode, referenceDeclarations, "full"); //assume full, degrade as necessary return { input, output }; } //note: context should be deployed context! function constructorOutputAllocation(context, contractNode, referenceDeclarations, allocationMode) { if (!context) { //just return a default abi mode result return { selector: new Uint8Array(), allocationMode: "abi", kind: "bytecode", delegatecallGuard: false }; } const { immutableReferences, compilationId, compiler, contractKind, binary } = context; let immutables; if (allocationMode === "full" && immutableReferences) { if (contractNode) { debug("allocating immutables"); immutables = []; for (const [id, references] of Object.entries(immutableReferences)) { if (references.length === 0) { continue; //don't allocate immutables that don't exist } const astId = parseInt(id); //get the corresponding variable node; potentially fail const { node: definition, contract: definedIn } = findNodeAndContract(contractNode.linearizedBaseContracts, referenceDeclarations, node => node.id === astId, contractNode); if (!definition || definition.nodeType !== "VariableDeclaration") { debug("didn't find definition for %d!", astId); allocationMode = "abi"; immutables = undefined; break; } const definedInClass = (Ast.Import.definitionToStoredType(definedIn, compilationId, compiler)); //can skip reference declarations argument here const dataType = Ast.Import.definitionToType(definition, compilationId, compiler); immutables.push({ name: definition.name, definedIn: definedInClass, type: dataType, pointer: { location: "returndata", start: references[0].start, length: references[0].length } }); } } else if (Object.entries(immutableReferences).length > 0) { //if there are immutables, but no contract mode, go to abi mode debug("immutables but no node!"); allocationMode = "abi"; } } else { debug("no immutables"); } //now, is there a delegatecall guard? let delegatecallGuard = false; if (contractKind === "library") { //note: I am relying on this being present! //(also this part is a bit HACKy) const pushAddressInstruction = (0x5f + Evm.Utils.ADDRESS_SIZE).toString(16); //"73" const delegateCallGuardString = "0x" + pushAddressInstruction + "..".repeat(Evm.Utils.ADDRESS_SIZE); if (binary.startsWith(delegateCallGuardString)) { delegatecallGuard = true; } } return { selector: new Uint8Array(), allocationMode, kind: "bytecode", immutables, delegatecallGuard }; } function getCalldataAllocations(contracts, referenceDeclarations, userDefinedTypes, abiAllocations) { let allocations = { constructorAllocations: {}, functionAllocations: {} }; for (let contract of contracts) { const contractAllocations = getCalldataAllocationsForContract(contract.abi, contract.contractNode, contract.constructorContext, contract.deployedContext, referenceDeclarations[contract.compilationId], userDefinedTypes, abiAllocations, contract.compilationId, contract.compiler); if (contract.constructorContext) { allocations.constructorAllocations[contract.constructorContext.context] = contractAllocations.constructorAllocation; } if (contract.deployedContext) { allocations.functionAllocations[contract.deployedContext.context] = contractAllocations.functionAllocations; //set this up under both constructor *and* deployed! this is to handle //constructor returndata decoding allocations.constructorAllocations[contract.deployedContext.context] = contractAllocations.constructorAllocation; } } return allocations; } exports.getCalldataAllocations = getCalldataAllocations; function getReturndataAllocationsForContract(abi, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) { let useAst = Boolean(contractNode && contractNode.usedErrors); if (useAst) { const errorNodes = contractNode.usedErrors.map(errorNodeId => referenceDeclarations[errorNodeId]); let abis; try { abis = errorNodes.map(errorNode => (Ast.Utils.definitionToAbi(errorNode, referenceDeclarations))); } catch (_a) { useAst = false; } if (useAst) { //i.e. if the above operation succeeded return contractNode.usedErrors .map(errorNodeId => referenceDeclarations[errorNodeId]) .map((errorNode, index) => allocateError(abis[index], errorNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler)); } } if (!useAst && abi) { //deliberately *not* an else! return abi .filter((abiEntry) => abiEntry.type === "error") .filter((abiEntry) => !AbiDataUtils.abiEntryIsObviouslyIllTyped(abiEntry)) //hack workaround .map((abiEntry) => allocateError(abiEntry, undefined, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler)); } //otherwise just return nothing return []; } function getReturndataAllocations(contracts, referenceDeclarations, userDefinedTypes, abiAllocations) { let allContexts = [] .concat(...contracts.map(({ deployedContext, constructorContext }) => [ deployedContext, constructorContext ])) .filter(x => x) //filter out nonexistent contexts .map(context => context.context); allContexts.push(""); //HACK: add fictional empty-string context to represent no-context //holds allocations for a given context let selfAllocations = {}; //holds allocations for *other* contexts let additionalAllocations = {}; //now: process the allocations for each contract. we'll add each contract's //allocations to *its* entries in allocations, and to every *other* entry //in additionalAllocations. for (const contract of contracts) { const contractAllocations = getReturndataAllocationsForContract(contract.abi, contract.contractNode, referenceDeclarations[contract.compilationId], userDefinedTypes, abiAllocations, contract.compilationId, contract.compiler); const contexts = [ //contexts for this contract contract.deployedContext, contract.constructorContext ] .filter(x => x) //filter out nonexistent contexts .map(context => context.context); const otherContexts = allContexts.filter( //contexts for all other contracts contextHash => !contexts.includes(contextHash)); //add them to selfAllocations for (const contextHash of contexts) { selfAllocations[contextHash] = contractAllocations; } //add them to additionalAllocations for (const contextHash of otherContexts) { if (additionalAllocations[contextHash] === undefined) { additionalAllocations[contextHash] = []; } additionalAllocations[contextHash] = additionalAllocations[contextHash].concat(contractAllocations); } } let allocations = Object.assign({}, ...allContexts.map(contextHash => ({ [contextHash]: {} }))); //now: perform coalescense! for (const contract of contracts) { //we're setting up contexts again, sorry >_> const contexts = [ //contexts for this contract contract.deployedContext, contract.constructorContext ] .filter(x => x) //filter out nonexistent contexts .map(context => context.context); for (const contextHash of contexts) { allocations[contextHash] = coalesceReturndataAllocations(selfAllocations[contextHash] || [], additionalAllocations[contextHash] || []); debug("allocations: %O", allocations[contextHash]); } } //...also coalesce the fake "" context allocations[""] = coalesceReturndataAllocations([], additionalAllocations[""] || []); debug("error allocations: %O", allocations); return allocations; } exports.getReturndataAllocations = getReturndataAllocations; function coalesceReturndataAllocations(selfAllocations, additionalAllocations) { let bySelector = {}; //start with the additional allocations; we want to process //the self allocations last, due to special handling of no-ID allocations there for (const allocation of additionalAllocations) { const signature = AbiDataUtils.abiSignature(allocation.abi); const selector = web3_utils_1.default.soliditySha3({ type: "string", value: signature }).slice(0, 2 + 2 * Evm.Utils.SELECTOR_SIZE); //arithmetic to account for hex string if (bySelector[selector]) { //note: at this point, for any given signature, there should only be a //no-ID allocation for that signature if it's the only one if (allocation.id !== undefined) { //delete anything with that signature but w/o an ID, or with this same ID bySelector[selector] = bySelector[selector].filter(({ abi, id }) => !(AbiDataUtils.abiSignature(abi) === signature && (id === undefined || id === allocation.id))); //add this allocation bySelector[selector].push(allocation); } else if (!bySelector[selector].some(({ abi }) => AbiDataUtils.abiSignature(abi) === signature)) { //only add ID-less ones if there isn't anything of that signature already bySelector[selector].push(allocation); } } else { //if there's nothing there thus far, add it bySelector[selector] = [allocation]; } } //now we're going to perform a modified version of this procedure for the self allocations: //1. we're going to add to the front, not the back //2. we can add an ID-less one even if there are already ones with IDs there //(sorry for the copypaste) for (const allocation of selfAllocations) { const signature = AbiDataUtils.abiSignature(allocation.abi); const selector = web3_utils_1.default.soliditySha3({ type: "string", value: signature }).slice(0, 2 + 2 * Evm.Utils.SELECTOR_SIZE); //arithmetic to account for hex string if (bySelector[selector]) { //delete anything with that signature but w/o an ID, or with this same ID //(if this alloc has no ID, this will only delete ID-less ones :) ) bySelector[selector] = bySelector[selector].filter(({ abi, id }) => !(AbiDataUtils.abiSignature(abi) === signature && (id === undefined || id === allocation.id))); //add this allocation to front, not back! bySelector[selector].unshift(allocation); } else { //if there's nothing there thus far, add it bySelector[selector] = [allocation]; } } return bySelector; } function getEventAllocationsForContract(abi, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) { let useAst = Boolean(contractNode && contractNode.usedEvents); if (useAst) { const eventNodes = contractNode.usedEvents.map(eventNodeId => referenceDeclarations[eventNodeId]); let abis; try { abis = eventNodes.map(eventNode => (Ast.Utils.definitionToAbi(eventNode, referenceDeclarations))); } catch (_a) { useAst = false; } if (useAst) { //i.e. if the above operation succeeded return contractNode.usedEvents .map(eventNodeId => referenceDeclarations[eventNodeId]) .map((eventNode, index) => ({ selector: AbiDataUtils.abiSelector(abis[index]), anonymous: abis[index].anonymous, topics: AbiDataUtils.topicsCount(abis[index]), allocation: allocateEvent(abis[index], eventNode, contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) })) .filter(allocationTemporary => allocationTemporary.allocation !== undefined); //filter out library events } } if (!useAst && abi) { return abi .filter((abiEntry) => abiEntry.type === "event") .filter((abiEntry) => !AbiDataUtils.abiEntryIsObviouslyIllTyped(abiEntry)) //hack workaround .map((abiEntry) => ({ selector: AbiDataUtils.abiSelector(abiEntry), anonymous: abiEntry.anonymous, topics: AbiDataUtils.topicsCount(abiEntry), allocation: allocateEvent(abiEntry, undefined, //we don't know the event node contractNode, referenceDeclarations, userDefinedTypes, abiAllocations, compilationId, compiler) //note we do *not* filter out undefined allocations; we need these as placeholders })); } //otherwise just return nothing return []; } //WARNING: this function is full of hacks... sorry function getEventAllocations(contracts, referenceDeclarations, userDefinedTypes, abiAllocations, allowConstructorEvents = false) { //first: do allocations for individual contracts let individualAllocations = {}; let groupedAllocations = {}; let contextSwapMap = {}; //maps deployed to constructor & vice versa let allocations = {}; for (const { abi, deployedContext, constructorContext, contractNode, compilationId, compiler } of contracts) { if (!deployedContext && !constructorContext && !contractNode) { //we'll need *one* of these at least continue; } const contractAllocations = getEventAllocationsForContract(abi, contractNode, referenceDeclarations[compilationId], userDefinedTypes, abiAllocations, compilationId, compiler); const key = makeContractKey(deployedContext || constructorContext, contractNode ? contractNode.id : undefined, compilationId); if (individualAllocations[key] === undefined) { individualAllocations[key] = {}; } for (const allocationTemporary of contractAllocations) { //we'll use selector *even for anonymous* here, because it's just //for determining what overrides what at this point individualAllocations[key][allocationTemporary.selector] = { context: deployedContext || constructorContext, contractNode, allocationTemporary, compilationId }; } //also: set up the swap map if (deployedContext && constructorContext) { contextSwapMap[deployedContext.context] = constructorContext.context; contextSwapMap[constructorContext.context] = deployedContext.context; } } //now: put things together for inheritance //note how we always put things in order from most derived to most base for (let contextOrId in individualAllocations) { groupedAllocations[contextOrId] = {}; for (let selector in individualAllocations[contextOrId]) { let { context, contractNode, allocationTemporary, compilationId } = individualAllocations[contextOrId][selector]; debug("allocationTemporary: %O", allocationTemporary); let allocationsTemporary = allocationTemporary.allocation ? [allocationTemporary] : []; //filter out undefined allocations //first, copy from individual allocations groupedAllocations[contextOrId][selector] = { context, contractNode, allocationsTemporary }; //if no contract node, or if we're dealing with a contract node that //lists it's used events for us, that's all. but otherwise... if (contractNode && contractNode.usedEvents === undefined) { //...we have to do inheritance processing debug("contract Id: %d", contractNode.id); debug("base contracts: %o", contractNode.linearizedBaseContracts); let linearizedBaseContractsMinusSelf = contractNode.linearizedBaseContracts.slice(); linearizedBaseContractsMinusSelf.shift(); //remove contract itself; only want ancestors for (let baseId of linearizedBaseContractsMinusSelf) { debug("checking baseId: %d", baseId); let baseNode = referenceDeclarations[compilationId][baseId]; if (!baseNode || baseNode.nodeType !== "ContractDefinition") { debug("failed to find node for baseId: %d", baseId); break; //not a continue! //if we can't find the base node, it's better to stop the loop, //rather than continue to potentially erroneous things } //note: we're not actually going to *use* the baseNode here. //we're just checking for whether we can *find* it //why? because if we couldn't find it, that means that events defined in //base contracts *weren't* skipped earlier, and so we shouldn't now add them in let baseContractInfo = contracts.find(contractAllocationInfo => contractAllocationInfo.compilationId === compilationId && contractAllocationInfo.contractNode && contractAllocationInfo.contractNode.id === baseId); if (!baseContractInfo) { //similar to above... this failure case can happen when there are //two contracts with the same name and you attempt to use the //artifacts; say you have contracts A, B, and B', where A inherits //from B, and B and B' have the same name, and B' is the one that //gets the artifact; B will end up in reference declarations and so //get found above, but it won't appear in contracts, causing the //problem here. Unfortunately I don't know any great way to handle this, //so, uh, we treat it as a failure same as above. debug("failed to find contract info for baseId: %d", baseId); break; } let baseContext = baseContractInfo.deployedContext ||