diff --git a/abi.ts b/abi.ts index ebe6a8ce..a8196b48 100644 --- a/abi.ts +++ b/abi.ts @@ -1,6 +1,7 @@ import * as semver from 'semver'; +import { ABIDescription, isError, isEvent } from './common/compiler-output'; -function update (compilerVersion, abi) { +function update (compilerVersion, abi: ABIDescription[]) { let hasConstructor = false; let hasFallback = false; @@ -18,7 +19,7 @@ function update (compilerVersion, abi) { hasFallback = true; } - if (item.type !== 'event') { + if (!isEvent(item) && !isError(item)) { // add 'payable' to everything, except constant functions if (!item.constant && semver.lt(compilerVersion, '0.4.0')) { item.payable = true; diff --git a/common/compiler-input.ts b/common/compiler-input.ts new file mode 100644 index 00000000..d6cbab4f --- /dev/null +++ b/common/compiler-input.ts @@ -0,0 +1,261 @@ +/***********/ +// SOURCES // +/***********/ + +export interface SourceInputUrls { + /** Hash of the source file. It is used to verify the retrieved content imported via URLs */ + keccak256?: string + /** + * URL(s) to the source file. + * URL(s) should be imported in this order and the result checked against the + * keccak256 hash (if available). If the hash doesn't match or none of the + * URL(s) result in success, an error should be raised. + */ + urls: string[] +} +export interface SourceInputContent { + /** Hash of the source file. */ + keccak256?: string + /** Literal contents of the source file */ + content: string +} + +export interface SourcesInput { + [contractName: string]: SourceInputContent | SourceInputUrls +} + +/********************/ +// OUTPUT SELECTION // +/********************/ +export type OutputType = + | 'abi' + | 'devdoc' + | 'userdoc' + | 'metadata' + | 'ir' + | 'irOptimized' + | 'storageLayout' + | 'evm.assembly' + | 'evm.legacyAssembly' + | 'evm.bytecode.functionDebugData' + | 'evm.bytecode.object' + | 'evm.bytecode.opcodes' + | 'evm.bytecode.sourceMap' + | 'evm.bytecode.linkReferences' + | 'evm.bytecode.generatedSources' + | 'evm.deployedBytecode' + | 'evm.deployedBytecode.immutableReferences' + | 'evm.methodIdentifiers' + | 'evm.gasEstimates' + | 'ewasm.wast' + | 'ewasm.wasm' + +export interface CompilerOutputSelection { + [file: string]: { + [contract: string]: OutputType[] + } +} + +export interface CompilerModelChecker { + /** + * Chose which contracts should be analyzed as the deployed one. + * - key is the contract path + * - value is an array of contract's name in the file + */ + contracts: { + [contractPath: string]: string[]; + } + /** + * Choose whether division and modulo operations should be replaced by multiplication with slack variables. + * Default is `true`. + * Using `false` here is recommended if you are using the CHC engine and not using Spacer as the Horn solver (using Eldarica, for example). + * See the Formal Verification section for a more detailed explanation of this option. + */ + divModWithSlacks?: boolean + /** Choose which model checker engine to use: all (default), bmc, chc, none. */ + engine?: 'all' | 'bmc' | 'chc' | 'none' + /** Choose which types of invariants should be reported to the user: contract, reentrancy. */ + invariants: ('contract' | 'reentrancy')[] + /** Choose whether to output all unproved targets. The default is `false`. */ + showUnproved?: boolean + /** Choose which solvers should be used, if available. See the Formal Verification section for the solvers description. */ + solvers: ('cvc4' | 'smtlib2' | 'z3')[] + /** + * Choose which targets should be checked: constantCondition, underflow, overflow, divByZero, balance, assert, popEmptyArray, outOfBounds. + * If the option is not given all targets are checked by default, except underflow/overflow for Solidity >=0.8.7. + * See the Formal Verification section for the targets description. + */ + targets?: ('constantCondition' | 'underflow' | 'overflow' | 'divByZero' | 'balance' | 'assert' | 'popEmptyArray' | 'outOfBounds')[] + /** + * Timeout for each SMT query in milliseconds. + * If this option is not given, the SMTChecker will use a deterministic resource limit by default. + * A given timeout of 0 means no resource/time restrictions for any query. + */ + timeout?: number +} + +/************/ +// SETTINGS // +/************/ +export type EvmVersion = 'london' | 'berlin' | 'istanbul' | 'petersburg' | 'constantinople' | 'byzantium' | 'spuriousDragon' | 'tangerineWhistle' | 'homestead'; + +export interface CompilerOptimizerDetails { + /** The peephole optimizer is always on if no details are given, use details to switch it off. */ + peephole: boolean; + /** The inliner is always on if no details are given, use details to switch it off. */ + inliner: boolean; + /** The unused jumpdest remover is always on if no details are given, use details to switch it off. */ + jumpdestRemover: boolean; + /** Sometimes re-orders literals in commutative operations. */ + orderLiterals: boolean; + /** Removes duplicate code blocks */ + deduplicate: boolean; + /** Common subexpression elimination, this is the most complicated step but can also provide the largest gain. */ + cse: boolean; + /** Optimize representation of literal numbers and strings in code. */ + constantOptimizer: boolean; + /** + * The new Yul optimizer. Mostly operates on the code of ABI coder v2 and inline assembly. + * It is activated together with the global optimizer setting and can be deactivated here. + * Before Solidity 0.6.0 it had to be activated through this switch. + */ + yul: boolean; + /** Tuning options for the Yul optimizer. */ + yulDetails: { + /** + * Improve allocation of stack slots for variables, can free up stack slots early. + * Activated by default if the Yul optimizer is activated. + */ + stackAllocation: boolean; + /** Select optimization steps to be applied. The optimizer will use the default sequence if omitted. */ + optimizerSteps?: string; + } +} + +export interface CompileDebug { +/** + * How to treat revert (and require) reason strings. Settings are "default", "strip", "debug" and "verboseDebug". + * - `"default"` does not inject compiler-generated revert strings and keeps user-supplied ones. + * - `"strip"` removes all revert strings (if possible, i.e. if literals are used) keeping side-effects + * - `"debug"` injects strings for compiler-generated internal reverts, implemented for ABI encoders V1 and V2 for now. + * - `"verboseDebug"` even appends further information to user-supplied revert strings (not yet implemented) + */ +revertStrings?: 'default' | 'strip' | 'debug' | 'verboseDebug' +/** + * How much extra debug information to include in comments in the produced EVM assembly and Yul code. Available components are: + * - `location`: Annotations of the form `@src ::` indicating the location of the corresponding element in the original Solidity file, where: + * 1. `` is the file index matching the `@use-src` annotation, + * 2. `` is the index of the first byte at that location, + * 3. `` is the index of the first byte after that location. + * - `snippet`: A single-line code snippet from the location indicated by `@src`. The snippet is quoted and follows the corresponding `@src` annotation. + * - `*`: Wildcard value that can be used to request everything. + */ +debugInfo?: ('location' | 'snippet' | '*')[] +} + +export interface CompilerMetadata { +/** Use only literal content and not URLs (false by default) */ +useLiteralContent: boolean +/** + * Use the given hash method for the metadata hash that is appended to the bytecode. + * The metadata hash can be removed from the bytecode via option "none". + * If the option is omitted, "ipfs" is used by default.. + */ +bytecodeHash?: 'ipfs' | 'bzzr1' | 'none'; +} + +/** +* The top level key is the the name of the source file where the library is used. +* If remappings are used, this source file should match the global path after remappings were applied. +* If this key is an empty string, that refers to a global level. +*/ +export interface CompilerLibraries { +[contractName: string]: { + [libName: string]: string +} +} + +export interface CompilerOptimizer { + /** disabled by default */ + enable: boolean + /** + * Optimize for how many times you intend to run the code. + * Lower values will optimize more for initial deployment cost, higher values will optimize more for high-frequency usage. + */ + runs: number + /** Switch optimizer components on or off in detail. + * The "enabled" switch above provides two defaults which can be + * tweaked here. If "details" is given, "enabled" can be omitted. + */ + details: Partial +} + +export interface CompilerSettings { + /** Stop compilation after the given stage. Currently only "parsing" is valid here */ + stopAfter?: 'parsing' + /** Sorted list of remappings */ + remappings?: string[] + /** Optimizer settings */ + optimizer?: Partial + /** Version of the EVM to compile for. Affects type checking and code generation */ + evmVersion: EvmVersion; + /** + * Change compilation pipeline to go through the Yul intermediate representation. + * This is a highly EXPERIMENTAL feature, not to be used for production. This is false by default. + */ + viaIR?: boolean + /** Debugging settings */ + debug?: CompileDebug + /** Metadata settings */ + metadata?: CompilerMetadata + /** Addresses of the libraries. If not all libraries are given here, it can result in unlinked objects whose output data is different. */ + libraries: CompilerLibraries + /** + * The following can be used to select desired outputs. + * If this field is omitted, then the compiler loads and does type checking, but will not generate any outputs apart from errors. + * The first level key is the file name and the second is the contract name, where empty contract name refers to the file itself, + * while the star refers to all of the contracts. + * Note that using a using `evm`, `evm.bytecode`, `ewasm`, etc. will select every + * target part of that output. Additionally, `*` can be used as a wildcard to request everything. + * Contract level (needs the contract name or "*"): + * - abi - ABI + * - devdoc - Developer documentation (natspec) + * - userdoc - User documentation (natspec) + * - metadata - Metadata + * - ir - Yul intermediate representation of the code before optimization + * - irOptimized - Intermediate representation after optimization + * - storageLayout - Slots, offsets and types of the contract's state variables. + * - evm.assembly - New assembly format + * - evm.legacyAssembly - Old-style assembly format in JSON + * - evm.bytecode.functionDebugData - Debugging information at function level + * - evm.bytecode.object - Bytecode object + * - evm.bytecode.opcodes - Opcodes list + * - evm.bytecode.sourceMap - Source mapping (useful for debugging) + * - evm.bytecode.linkReferences - Link references (if unlinked object) + * - evm.bytecode.generatedSources - Sources generated by the compiler + * - evm.deployedBytecode* - Deployed bytecode (has all the options that evm.bytecode has) + * - evm.deployedBytecode.immutableReferences - Map from AST ids to bytecode ranges that reference immutables + * - evm.methodIdentifiers - The list of function hashes + * - evm.gasEstimates - Function gas estimates + * - ewasm.wast - Ewasm in WebAssembly S-expressions format + * - ewasm.wasm - Ewasm in WebAssembly binary format + */ + outputSelection?: CompilerOutputSelection + /** The modelChecker object is experimental and subject to changes. */ + modelChecker?: CompilerModelChecker +} + +export interface CompilationInput { + /** Source code language */ + language: 'Solidity' | 'yul' + sources: SourcesInput + settings?: CompilerSettings +} + +export interface CondensedCompilationInput { + language: 'Solidity' | 'Vyper' | 'lll' | 'assembly' | 'yul' + optimize: boolean + /** e.g: 0.6.8+commit.0bbfe453 */ + version: string + evmVersion?: EvmVersion +} diff --git a/common/compiler-output.ts b/common/compiler-output.ts new file mode 100644 index 00000000..d5e68af5 --- /dev/null +++ b/common/compiler-output.ts @@ -0,0 +1,372 @@ +/*********/ +// ERROR // +/*********/ + +export interface SourceLocation { + file: string + start: number + end: number + message?: string +} + +export type CompilationErrorType = +| 'JSONError' +| 'IOError' +| 'ParserError' +| 'DocstringParsingError' +| 'SyntaxError' +| 'DeclarationError' +| 'TypeError' +| 'UnimplementedFeatureError' +| 'InternalCompilerError' +| 'Exception' +| 'CompilerError' +| 'FatalError' +| 'Warning'; + +export interface CompilationError { + /** Location within the source file */ + sourceLocation?: SourceLocation + /** Further locations (e.g. places of conflicting declarations) */ + secondarySourceLocations?: SourceLocation[], + /** Error type */ + type: CompilationErrorType + /** Component where the error originated, such as "general", "ewasm", etc. */ + component: 'general' | 'ewasm' | string + severity: 'error' | 'warning' | 'info' + /** unique code for the cause of the error */ + errorCode?: string + message: string + /** the message formatted with source location */ + formattedMessage?: string +} + +/*******/ +// AST // +/*******/ +export interface AstNode { + absolutePath?: string + exportedSymbols?: Record + id: number + nodeType: string + nodes?: Array + src: string + literals?: Array + file?: string + scope?: number + sourceUnit?: number + symbolAliases?: Array + [x: string]: any +} + +export interface AstNodeAtt { + operator?: string + string?: null + type?: string + value?: string + constant?: boolean + name?: string + public?: boolean + exportedSymbols?: Record + argumentTypes?: null + absolutePath?: string + [x: string]: any +} + +export interface AstNodeLegacy { + id: number + name: string + src: string + children?: Array + attributes?: AstNodeAtt +} + +/**********/ +// SOURCE // +/**********/ +export interface CompilationSource { + /** Identifier of the source (used in source maps) */ + id: number + /** The AST object */ + ast: AstNode + /** The legacy AST object */ + legacyAST: AstNodeLegacy +} + +/*******/ +// ABI // +/*******/ +type bytes = '8' | '16' | '32' | '64' | '128' | '256'; + +type ABISingleTypeParameter = +| 'string' +| 'uint' +| 'int' +| 'address' +| 'bool' +| 'fixed' +| `fixed${bytes}` +| 'ufixed' +| `ufixed${bytes}` +| 'bytes' +| `bytes${bytes}` +| 'function' +| 'tuple'; + +type ABIArrayType = `${T}[]` | `${T}[${number}]`; + +export type ABITypeParameter = ABISingleTypeParameter | ABIArrayType; + +export interface ABIParameter { + /** The name of the parameter */ + name: string; + /** The canonical type of the parameter */ + type: ABITypeParameter; + /** Used for tuple types */ + components?: ABIParameter[]; + /** + * @example "struct StructName" + * @example "struct Contract.StructName" + */ + internalType?: string; +} + +interface ABIEventParameter extends ABIParameter { + /** true if the field is part of the log’s topics, false if it one of the log’s data segment. */ + indexed: boolean; +} + +export interface FunctionDescription { + /** Type of the method. default is 'function' */ + type?: 'function' | 'constructor' | 'fallback' | 'receive'; + /** The name of the function. Constructor and fallback functions never have a name */ + name?: string; + /** List of parameters of the method. Fallback functions don’t have inputs. */ + inputs?: ABIParameter[]; + /** List of the output parameters for the method, if any */ + outputs?: ABIParameter[]; + /** State mutability of the method */ + stateMutability?: 'pure' | 'view' | 'nonpayable' | 'payable'; + /** true if function accepts Ether, false otherwise. Default is false */ + payable?: boolean; + /** true if function is either pure or view, false otherwise. Default is false */ + constant?: boolean; +} + +export interface EventDescription { + type: 'event'; + name: string; + inputs: ABIEventParameter[]; + /** true if the event was declared as anonymous. */ + anonymous: boolean; +} + +export interface ErrorDescription { + type: 'error'; + name: string; + inputs: ABIEventParameter[]; +} + +export type ABIDescription = FunctionDescription | EventDescription | ErrorDescription; + +/*************************/ +// NATURAL SPECIFICATION // +/*************************/ +export interface UserMethodDoc { + notice: string +} + +export type UserMethodList = { + [functionIdentifier: string]: UserMethodDoc +} & { + 'constructor'?: string +} + +export interface DevMethodDoc { + author: string + details: string + return: string + returns: { + [param: string]: string + } + params: { + [param: string]: string + } +} + +export interface DevMethodList { + [functionIdentifier: string]: DevMethodDoc +} + +// Devdoc +export interface DeveloperDocumentation { + author: string + title: string + details: string + methods: DevMethodList +} + +// Userdoc +export interface UserDocumentation { + methods: UserMethodList + notice: string +} + +/**************/ +// EVM OUTPUT // +/**************/ + +export interface FunctionDebugData { + /** Byte offset into the bytecode where the function starts */ + entryPoint: number; + /** AST ID of the function definition or null for compiler-internal functions */ + id: number; + /** Number of EVM stack slots for the function parameters */ + parameterSlots: number; + /** Number of EVM stack slots for the return values */ + returnSlots: number; +} + +export interface GeneratedSources { + /** Yul AST */ + ast: Record // TODO: Find Yul AST type + /** Source file in its text form (may contain comments) */ + contents: string + /** Source file ID, used for source references, same namespace as the Solidity source files */ + id: number + language: 'Yul' + name: string +} + +export interface GasEstimates { + creation: { + codeDepositCost: string + executionCost: 'infinite' | string + totalCost: 'infinite' | string + } + external: { + [functionIdentifier: string]: string + } + internal: { + [functionIdentifier: string]: 'infinite' | string + } +} + +export interface BytecodeObject { + /** + * Debugging data at the level of functions. + * Set of functions including compiler-internal and user-defined function. + */ + functionDebugData: { + [internalName: string]: Partial + } + /** The bytecode as a hex string. */ + object: string + /** Opcodes list */ + opcodes: string + /** The source mapping as a string. See the source mapping definition. */ + sourceMap: string + /** Array of sources generated by the compiler. Currently only contains a single Yul file. */ + generatedSources: GeneratedSources[], + /** If given, this is an unlinked object. */ + linkReferences?: { + [contractName: string]: { + /** Byte offsets into the bytecode. */ + [library: string]: { start: number; length: number }[] + } + } +} + +export interface EvmOutputs { + assembly: string + legacyAssembly: Record + /** Bytecode and related details. */ + bytecode: BytecodeObject + deployedBytecode: BytecodeObject + /** The list of function hashes */ + methodIdentifiers: { + [functionIdentifier: string]: string + } + /** Function gas estimates */ + gasEstimates: GasEstimates +} + +/***********/ +// SOURCES // +/***********/ +export interface CompilationFileSources { + [fileName: string]: { + // Optional: keccak256 hash of the source file + keccak256?: string, + // Required (unless "urls" is used): literal contents of the source file + content: string, + urls?: string[] + } +} + +export interface SourceWithTarget { + sources?: CompilationFileSources, + target?: string | null | undefined +} + +/************/ +// CONTRACT // +/************/ +export interface CompiledContract { + /** The Ethereum Contract ABI. If empty, it is represented as an empty array. */ + abi: ABIDescription[] + // See the Metadata Output documentation (serialised JSON string) + metadata: string + /** User documentation (natural specification) */ + userdoc: UserDocumentation + /** Developer documentation (natural specification) */ + devdoc: DeveloperDocumentation + /** Intermediate representation (string) */ + ir: string + /** EVM-related outputs */ + evm: EvmOutputs + /** eWASM related outputs */ + ewasm: { + /** S-expressions format */ + wast: string + /** Binary format (hex string) */ + wasm: string + } +} + +/**********/ +// RESULT // +/**********/ + +export interface CompilationResult { + /** not present if no errors/warnings were encountered */ + errors?: CompilationError[] + /** This contains the file-level outputs. In can be limited/filtered by the outputSelection settings */ + sources: { + [contractName: string]: CompilationSource + } + /** This contains the contract-level outputs. It can be limited/filtered by the outputSelection settings */ + contracts: { + /** If the language used has no contract names, this field should equal to an empty string. */ + [fileName: string]: { + [contract: string]: CompiledContract + } + } +} + +export interface lastCompilationResult { + data: CompilationResult | null + source: SourceWithTarget | null | undefined +} + +/***************/ +// TYPE GUARDS // +/***************/ + +export function isEvent (description: ABIDescription): description is EventDescription { + return description.type === 'event'; +} + +export function isError (description: ABIDescription): description is ErrorDescription { + return description.type === 'error'; +} diff --git a/test/abi.ts b/test/abi.ts index 6328c904..9610db66 100644 --- a/test/abi.ts +++ b/test/abi.ts @@ -11,62 +11,65 @@ tape('ABI translator', function (t) { st.end(); }); t.test('0.3.6 (constructor)', function (st) { - const input = [{ inputs: [], type: 'constructor' }]; + const input = [{ inputs: [], type: 'constructor' as const }]; st.deepEqual(abi.update('0.3.6', input), [{ inputs: [], payable: true, stateMutability: 'payable', type: 'constructor' }, { payable: true, stateMutability: 'payable', type: 'fallback' }]); st.end(); }); t.test('0.3.6 (non-constant function)', function (st) { - const input = [{ inputs: [], type: 'function' }]; + const input = [{ inputs: [], type: 'function' as const }]; st.deepEqual(abi.update('0.3.6', input), [{ inputs: [], payable: true, stateMutability: 'payable', type: 'function' }, { payable: true, stateMutability: 'payable', type: 'fallback' }]); st.end(); }); t.test('0.3.6 (constant function)', function (st) { - const input = [{ inputs: [], type: 'function', constant: true }]; + const input = [{ inputs: [], type: 'function' as const, constant: true }]; st.deepEqual(abi.update('0.3.6', input), [{ inputs: [], constant: true, stateMutability: 'view', type: 'function' }, { payable: true, stateMutability: 'payable', type: 'fallback' }]); st.end(); }); t.test('0.3.6 (event)', function (st) { - const input = [{ inputs: [], type: 'event' }]; - st.deepEqual(abi.update('0.3.6', input), [{ inputs: [], type: 'event' }, { payable: true, stateMutability: 'payable', type: 'fallback' }]); + const input = [{ name: 'eventName', inputs: [], type: 'event' as const, anonymous: false }]; + st.deepEqual(abi.update('0.3.6', input), [ + { name: 'eventName', inputs: [], type: 'event', anonymous: false }, + { payable: true, stateMutability: 'payable', type: 'fallback' } + ]); st.end(); }); t.test('0.3.6 (has no fallback)', function (st) { - const input = [{ inputs: [], type: 'constructor' }]; + const input = [{ inputs: [], type: 'constructor' as const }]; st.deepEqual(abi.update('0.3.6', input), [{ inputs: [], type: 'constructor', payable: true, stateMutability: 'payable' }, { type: 'fallback', payable: true, stateMutability: 'payable' }]); st.end(); }); t.test('0.4.0 (has fallback)', function (st) { - const input = [{ inputs: [], type: 'constructor' }, { type: 'fallback' }]; + const input = [{ inputs: [], type: 'constructor' as const }, { type: 'fallback' as const }]; st.deepEqual(abi.update('0.4.0', input), [{ inputs: [], type: 'constructor', payable: true, stateMutability: 'payable' }, { type: 'fallback', stateMutability: 'nonpayable' }]); st.end(); }); t.test('0.4.0 (non-constant function)', function (st) { - const input = [{ inputs: [], type: 'function' }]; + const input = [{ inputs: [], type: 'function' as const }]; st.deepEqual(abi.update('0.4.0', input), [{ inputs: [], stateMutability: 'nonpayable', type: 'function' }]); st.end(); }); t.test('0.4.0 (constant function)', function (st) { - const input = [{ inputs: [], type: 'function', constant: true }]; + const input = [{ inputs: [], type: 'function' as const, constant: true }]; st.deepEqual(abi.update('0.4.0', input), [{ inputs: [], constant: true, stateMutability: 'view', type: 'function' }]); st.end(); }); t.test('0.4.0 (payable function)', function (st) { - const input = [{ inputs: [], payable: true, type: 'function' }]; + const input = [{ inputs: [], payable: true, type: 'function' as const }]; st.deepEqual(abi.update('0.4.0', input), [{ inputs: [], payable: true, stateMutability: 'payable', type: 'function' }]); st.end(); }); t.test('0.4.1 (constructor not payable)', function (st) { - const input = [{ inputs: [], payable: false, type: 'constructor' }]; + const input = [{ inputs: [], payable: false, type: 'constructor' as const }]; st.deepEqual(abi.update('0.4.1', input), [{ inputs: [], payable: true, stateMutability: 'payable', type: 'constructor' }]); st.end(); }); t.test('0.4.5 (constructor payable)', function (st) { - const input = [{ inputs: [], payable: false, type: 'constructor' }]; + const input = [{ inputs: [], payable: false, type: 'constructor' as const }]; st.deepEqual(abi.update('0.4.5', input), [{ inputs: [], payable: false, stateMutability: 'nonpayable', type: 'constructor' }]); st.end(); }); t.test('0.4.16 (statemutability)', function (st) { - const input = [{ inputs: [], payable: false, stateMutability: 'pure', type: 'function' }]; + const input = [{ inputs: [], payable: false, stateMutability: 'pure' as const, type: 'function' as const }]; st.deepEqual(abi.update('0.4.16', input), [{ inputs: [], payable: false, stateMutability: 'pure', type: 'function' }]); st.end(); });