UNPKG

@nomiclabs/buidler

Version:

Buidler is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

544 lines (453 loc) 13.1 kB
import abi from "ethereumjs-abi"; import { CompilerInput, CompilerOutput, CompilerOutputBytecode, } from "./compiler-types"; import { getLibraryAddressPositions, normalizeCompilerOutputBytecode, } from "./library-utils"; import { Bytecode, Contract, ContractFunction, ContractFunctionType, ContractFunctionVisibility, ContractType, SourceFile, SourceLocation, } from "./model"; import { decodeInstructions } from "./source-maps"; export function createModelsAndDecodeBytecodes( solcVersion: string, compilerInput: CompilerInput, compilerOutput: CompilerOutput ): Bytecode[] { const fileIdToSourceFile = new Map<number, SourceFile>(); const contractIdToContract = new Map<number, Contract>(); createSourcesModelFromAst( compilerOutput, compilerInput, fileIdToSourceFile, contractIdToContract ); const bytecodes = decodeBytecodes( solcVersion, compilerOutput, fileIdToSourceFile, contractIdToContract ); correctSelectors(bytecodes, compilerOutput); return bytecodes; } function createSourcesModelFromAst( compilerOutput: CompilerOutput, compilerInput: CompilerInput, fileIdToSourceFile: Map<number, SourceFile>, contractIdToContract: Map<number, Contract> ) { const contractIdToLinearizedBaseContractIds = new Map<number, number[]>(); for (const [globalName, source] of Object.entries(compilerOutput.sources)) { const file = new SourceFile( globalName, compilerInput.sources[globalName].content ); fileIdToSourceFile.set(source.id, file); for (const contractNode of source.ast.nodes) { if (contractNode.nodeType !== "ContractDefinition") { continue; } const contractType = contractKindToContractType( contractNode.contractKind ); if (contractType === undefined) { continue; } processContractAstNode( file, contractNode, fileIdToSourceFile, contractType, contractIdToContract, contractIdToLinearizedBaseContractIds ); } } applyContractsInheritance( contractIdToContract, contractIdToLinearizedBaseContractIds ); } function processContractAstNode( file: SourceFile, contractNode: any, fileIdToSourceFile: Map<number, SourceFile>, contractType: ContractType, contractIdToContract: Map<number, Contract>, contractIdToLinearizedBaseContractIds: Map<number, number[]> ) { const contractLocation = astSrcToSourceLocation( contractNode.src, fileIdToSourceFile )!; const contract = new Contract( contractNode.name, contractType, contractLocation ); contractIdToContract.set(contractNode.id, contract); contractIdToLinearizedBaseContractIds.set( contractNode.id, contractNode.linearizedBaseContracts ); file.addContract(contract); for (const node of contractNode.nodes) { if (node.nodeType === "FunctionDefinition") { processFunctionDefinitionAstNode( node, fileIdToSourceFile, contract, file ); } else if (node.nodeType === "ModifierDefinition") { processModifierDefinitionAstNode( node, fileIdToSourceFile, contract, file ); } else if (node.nodeType === "VariableDeclaration") { processVariableDeclarationAstNode( node, fileIdToSourceFile, contract, file ); } } } function processFunctionDefinitionAstNode( functionDefinitionNode: any, fileIdToSourceFile: Map<number, SourceFile>, contract: Contract, file: SourceFile ) { if (functionDefinitionNode.implemented === false) { return; } const functionType = functionDefinitionKindToFunctionType( functionDefinitionNode.kind ); const functionLocation = astSrcToSourceLocation( functionDefinitionNode.src, fileIdToSourceFile )!; const visibility = astVisibilityToVisibility( functionDefinitionNode.visibility ); const cf = new ContractFunction( functionDefinitionNode.name, functionType, functionLocation, contract, visibility, functionDefinitionNode.stateMutability === "payable", functionType === ContractFunctionType.FUNCTION ? astFunctionDefinitionToSelector(functionDefinitionNode) : undefined ); contract.addLocalFunction(cf); file.addFunction(cf); } function processModifierDefinitionAstNode( modifierDefinitionNode: any, fileIdToSourceFile: Map<number, SourceFile>, contract: Contract, file: SourceFile ) { const functionLocation = astSrcToSourceLocation( modifierDefinitionNode.src, fileIdToSourceFile )!; const cf = new ContractFunction( modifierDefinitionNode.name, ContractFunctionType.MODIFIER, functionLocation, contract ); contract.addLocalFunction(cf); file.addFunction(cf); } function getPublicVariableSelectorFromDeclarationAstNode( variableDeclaration: any ) { const paramTypes: string[] = []; let nextType = variableDeclaration.typeName; while (true) { if (nextType.nodeType === "Mapping") { paramTypes.push(toCanonicalAbiType(nextType.keyType.name)); nextType = nextType.valueType; } else { if (nextType.nodeType === "ArrayTypeName") { paramTypes.push("uint256"); } break; } } return abi.methodID(variableDeclaration.name, paramTypes); } function processVariableDeclarationAstNode( variableDeclarationNode: any, fileIdToSourceFile: Map<number, SourceFile>, contract: Contract, file: SourceFile ) { const visibility = astVisibilityToVisibility( variableDeclarationNode.visibility ); // Variables can't be external if (visibility !== ContractFunctionVisibility.PUBLIC) { return; } const functionLocation = astSrcToSourceLocation( variableDeclarationNode.src, fileIdToSourceFile )!; const cf = new ContractFunction( variableDeclarationNode.name, ContractFunctionType.GETTER, functionLocation, contract, visibility, false, // Getters aren't payable getPublicVariableSelectorFromDeclarationAstNode(variableDeclarationNode) ); contract.addLocalFunction(cf); file.addFunction(cf); } function applyContractsInheritance( contractIdToContract: Map<number, Contract>, contractIdToLinearizedBaseContractIds: Map<number, number[]> ) { for (const [cid, contract] of contractIdToContract.entries()) { const inheritanceIds = contractIdToLinearizedBaseContractIds.get(cid)!; for (const baseId of inheritanceIds) { const baseContract = contractIdToContract.get(baseId); if (baseContract === undefined) { // This list includes interface, which we don't model continue; } contract.addNextLinearizedBaseContract(baseContract); } } } function decodeBytecodes( solcVersion: string, compilerOutput: CompilerOutput, fileIdToSourceFile: Map<number, SourceFile>, contractIdToContract: Map<number, Contract> ): Bytecode[] { const bytecodes: Bytecode[] = []; for (const contract of contractIdToContract.values()) { const contractFile = contract.location.file.globalName; const contractEvmOutput = compilerOutput.contracts[contractFile][contract.name].evm; // This is an abstract contract if (contractEvmOutput.bytecode.object === "") { continue; } const deploymentBytecode = decodeEvmBytecode( contract, solcVersion, true, contractEvmOutput.bytecode, fileIdToSourceFile ); const runtimeBytecode = decodeEvmBytecode( contract, solcVersion, false, contractEvmOutput.deployedBytecode, fileIdToSourceFile ); bytecodes.push(deploymentBytecode); bytecodes.push(runtimeBytecode); } return bytecodes; } function decodeEvmBytecode( contract: Contract, solcVersion: string, isDeployment: boolean, compilerBytecode: CompilerOutputBytecode, fileIdToSourceFile: Map<number, SourceFile> ): Bytecode { const libraryAddressPositions = getLibraryAddressPositions(compilerBytecode); const immutableReferences = compilerBytecode.immutableReferences !== undefined ? Object.values(compilerBytecode.immutableReferences).reduce( (previousValue, currentValue) => [...previousValue, ...currentValue], [] ) : []; const normalizedCode = normalizeCompilerOutputBytecode( compilerBytecode.object, libraryAddressPositions ); const instructions = decodeInstructions( normalizedCode, compilerBytecode.sourceMap, fileIdToSourceFile, isDeployment ); return new Bytecode( contract, isDeployment, normalizedCode, instructions, libraryAddressPositions, immutableReferences, solcVersion ); } function astSrcToSourceLocation( src: string, fileIdToSourceFile: Map<number, SourceFile> ): SourceLocation | undefined { const [offset, length, fileId] = src.split(":").map((p) => +p); const file = fileIdToSourceFile.get(fileId); if (file === undefined) { return undefined; } return new SourceLocation(file, offset, length); } function contractKindToContractType( contractKind?: string ): ContractType | undefined { if (contractKind === "library") { return ContractType.LIBRARY; } if (contractKind === "contract") { return ContractType.CONTRACT; } return undefined; } function astVisibilityToVisibility( visibility: string ): ContractFunctionVisibility { if (visibility === "private") { return ContractFunctionVisibility.PRIVATE; } if (visibility === "internal") { return ContractFunctionVisibility.INTERNAL; } if (visibility === "public") { return ContractFunctionVisibility.PUBLIC; } return ContractFunctionVisibility.EXTERNAL; } function functionDefinitionKindToFunctionType( kind: string ): ContractFunctionType { if (kind === "constructor") { return ContractFunctionType.CONSTRUCTOR; } if (kind === "fallback") { return ContractFunctionType.FALLBACK; } if (kind === "receive") { return ContractFunctionType.RECEIVE; } return ContractFunctionType.FUNCTION; } function astFunctionDefinitionToSelector(functionDefinition: any): Buffer { const paramTypes: string[] = []; for (const param of functionDefinition.parameters.parameters) { if (isContractType(param)) { paramTypes.push("address"); continue; } if (isEnumType(param)) { // TODO: If the enum has >= 256 elements this will fail. It should be a uint16. This is // complicated, as enums can be inherited. Fortunately, if multiple parent contracts // define the same enum, solc fails to compile. paramTypes.push("uint8"); continue; } if (param.typeName.nodeType === "ArrayTypeName") { paramTypes.push(`${toCanonicalAbiType(param.typeName.baseType.name)}[]`); continue; } paramTypes.push(toCanonicalAbiType(param.typeName.name)); } return abi.methodID(functionDefinition.name, paramTypes); } function isContractType(param: any) { return ( param.typeName.nodeType === "UserDefinedTypeName" && param.typeDescriptions.typeString.startsWith("contract ") ); } function isEnumType(param: any) { return ( param.typeName.nodeType === "UserDefinedTypeName" && param.typeDescriptions.typeString.startsWith("enum ") ); } function toCanonicalAbiType(type: string): string { if (type.startsWith("int[")) { return `int256${type.slice(3)}`; } if (type === "int") { return "int256"; } if (type.startsWith("uint[")) { return `uint256${type.slice(4)}`; } if (type === "uint") { return "uint256"; } if (type.startsWith("fixed[")) { return `fixed128x128${type.slice(5)}`; } if (type === "fixed") { return "fixed128x128"; } if (type.startsWith("ufixed[")) { return `ufixed128x128${type.slice(6)}`; } if (type === "ufixed") { return "ufixed128x128"; } return type; } function correctSelectors( bytecodes: Bytecode[], compilerOutput: CompilerOutput ) { for (const bytecode of bytecodes) { if (bytecode.isDeployment) { continue; } const contract = bytecode.contract; const methodIdentifiers = compilerOutput.contracts[contract.location.file.globalName][contract.name] .evm.methodIdentifiers; for (const [signature, hexSelector] of Object.entries(methodIdentifiers)) { const functionName = signature.slice(0, signature.indexOf("(")); const selector = Buffer.from(hexSelector, "hex"); const contractFunction = contract.getFunctionFromSelector(selector); if (contractFunction !== undefined) { continue; } const fixedSelector = contract.correctSelector(functionName, selector); if (!fixedSelector) { // tslint:disable-next-line only-buidler-error throw new Error( `Failed to compute the selector one or more implementations of ${contract.name}#${functionName}. BuidlerEVM can automatically fix this problem if you don't use function overloading.` ); } } } }