diff --git a/packages/discovery/.gitignore b/packages/discovery/.gitignore index af70545a..352b0217 100644 --- a/packages/discovery/.gitignore +++ b/packages/discovery/.gitignore @@ -2,3 +2,4 @@ node_modules build .env cache +layout.json \ No newline at end of file diff --git a/packages/discovery/CHANGELOG.md b/packages/discovery/CHANGELOG.md index cee42de4..71cbe5ea 100644 --- a/packages/discovery/CHANGELOG.md +++ b/packages/discovery/CHANGELOG.md @@ -1,5 +1,11 @@ # @l2beat/discovery +## 0.26.0 + +### Minor Changes + +- Add an experimental layout command + ## 0.25.0 ### Minor Changes diff --git a/packages/discovery/package.json b/packages/discovery/package.json index fd19209e..a43b0a49 100644 --- a/packages/discovery/package.json +++ b/packages/discovery/package.json @@ -1,7 +1,7 @@ { "name": "@l2beat/discovery", "description": "L2Beat discovery - engine & tooling utilized for keeping an eye on L2s", - "version": "0.25.0", + "version": "0.26.0", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { @@ -30,6 +30,7 @@ "node-fetch": "^2.6.7", "prettier": "^3.0.3", "rimraf": "^5.0.0", + "solc": "^0.8.23-fixed", "sqlite3": "^5.1.6", "zod": "^3.22.2" }, diff --git a/packages/discovery/src/cli.ts b/packages/discovery/src/cli.ts index d9fb86a9..65e514e2 100644 --- a/packages/discovery/src/cli.ts +++ b/packages/discovery/src/cli.ts @@ -3,6 +3,7 @@ import { Logger } from '@l2beat/backend-tools' import { discoverCommand } from './cli/discoverCommand' import { handleCli } from './cli/handleCli' import { invertCommand } from './cli/invertCommand' +import { layoutCommand } from './cli/layoutCommand' import { singleDiscoveryCommand } from './cli/singleDiscoveryCommand' import { getDiscoveryCliConfig } from './config/config.discovery' @@ -19,4 +20,5 @@ async function main(): Promise { await discoverCommand(config, logger) await invertCommand(config, logger) await singleDiscoveryCommand(config, logger) + await layoutCommand(config, logger) } diff --git a/packages/discovery/src/cli/getCliParameters.ts b/packages/discovery/src/cli/getCliParameters.ts index bdd3ff62..8ca0b5f0 100644 --- a/packages/discovery/src/cli/getCliParameters.ts +++ b/packages/discovery/src/cli/getCliParameters.ts @@ -7,8 +7,9 @@ export type CliParameters = | ServerCliParameters | DiscoverCliParameters | InvertCliParameters - | HelpCliParameters | SingleDiscoveryCliParameters + | LayoutCliParameters + | HelpCliParameters export interface ServerCliParameters { mode: 'server' @@ -31,12 +32,19 @@ export interface InvertCliParameters { chain: ChainId useMermaidMarkup: boolean } + export interface SingleDiscoveryCliParameters { mode: 'single-discovery' address: EthereumAddress chain: ChainId } +export interface LayoutCliParameters { + mode: 'layout' + addresses: EthereumAddress[] + chain: ChainId +} + export interface HelpCliParameters { mode: 'help' error?: string @@ -197,6 +205,24 @@ export function getCliParameters(args = process.argv.slice(2)): CliParameters { return result } + if (args[0] === 'layout') { + const remaining = args.slice(1) + const [chainName, ...addresses] = remaining + if (remaining.length < 2 || !chainName) { + return { mode: 'help', error: 'Not enough arguments' } + } + + const chain = getChainIdSafe(chainName) + if (!chain) return createWrongChainNameHelpCli(chainName) + + const result: LayoutCliParameters = { + mode: 'layout', + addresses: addresses.map((a) => EthereumAddress(a)), + chain, + } + return result + } + const mode = args[0] ?? '' return { mode: 'help', error: `Unknown mode: ${mode}` } diff --git a/packages/discovery/src/cli/layoutCommand.ts b/packages/discovery/src/cli/layoutCommand.ts new file mode 100644 index 00000000..82599652 --- /dev/null +++ b/packages/discovery/src/cli/layoutCommand.ts @@ -0,0 +1,45 @@ +import { Logger } from '@l2beat/backend-tools' +import { writeFileSync } from 'fs' + +import { DiscoveryCliConfig } from '../config/config.discovery' +import { flattenLayout } from '../layout/flattenLayout' +import { mergeFlatLayouts } from '../layout/mergeFlatLayouts' +import { parseAndGetLayout } from '../layout/parseAndGetLayout' +import { EthereumAddress } from '../utils/EthereumAddress' +import { EtherscanLikeClient } from '../utils/EtherscanLikeClient' +import { HttpClient } from '../utils/HttpClient' + +export async function layoutCommand( + config: DiscoveryCliConfig, + logger: Logger, +): Promise { + if (!config.layout) { + return + } + const http = new HttpClient() + const etherscanClient = EtherscanLikeClient.createForDiscovery( + http, + config.chain.etherscanUrl, + config.chain.etherscanApiKey, + config.chain.etherscanUnsupported, + ) + await runLayout(etherscanClient, config.layout.addresses, logger) +} + +async function runLayout( + etherscanClient: EtherscanLikeClient, + addresses: EthereumAddress[], + logger: Logger, +): Promise { + const sources = await Promise.all( + addresses.map((address) => etherscanClient.getContractSource(address)), + ) + logger.info('Got sources', { + lengths: sources.map((source) => source.SourceCode.length), + }) + const layout = mergeFlatLayouts( + sources.map((s) => flattenLayout(parseAndGetLayout(s))), + ) + writeFileSync('layout.json', JSON.stringify(layout, null, 2)) + logger.info('Saved layout', { filename: 'layout.json', items: layout.length }) +} diff --git a/packages/discovery/src/cli/usage.ts b/packages/discovery/src/cli/usage.ts index 7e894892..6e837c2d 100644 --- a/packages/discovery/src/cli/usage.ts +++ b/packages/discovery/src/cli/usage.ts @@ -8,6 +8,7 @@ const usage = `Usage: yarn invert [chain] [project] ................... print addresses and their functions yarn invert [chain] [project] --mermaid ......... print mermaid graph markup yarn discover:single [chain] [address] .......... run a discovery on the address (no config needed, useful for experimenting) + yarn layout [chain] [address...] ................ (experimental) print storage layout for the address(es) yarn --help .................... display this message supported chains: checkout config.discovery.ts diff --git a/packages/discovery/src/config/config.discovery.ts b/packages/discovery/src/config/config.discovery.ts index 33b6afa9..a5f08ef6 100644 --- a/packages/discovery/src/config/config.discovery.ts +++ b/packages/discovery/src/config/config.discovery.ts @@ -14,7 +14,8 @@ export function getDiscoveryCliConfig(cli: CliParameters): DiscoveryCliConfig { if ( cli.mode !== 'invert' && cli.mode !== 'discover' && - cli.mode !== 'single-discovery' + cli.mode !== 'single-discovery' && + cli.mode !== 'layout' ) { throw new Error(`No local config for mode: ${cli.mode}`) } @@ -22,6 +23,7 @@ export function getDiscoveryCliConfig(cli: CliParameters): DiscoveryCliConfig { const discoveryEnabled = cli.mode === 'discover' const singleDiscoveryEnabled = cli.mode === 'single-discovery' const invertEnabled = cli.mode === 'invert' + const layoutEnabled = cli.mode === 'layout' const chain = getChainConfig(cli.chain) return { @@ -44,6 +46,10 @@ export function getDiscoveryCliConfig(cli: CliParameters): DiscoveryCliConfig { address: cli.address, chainId: cli.chain, }, + layout: layoutEnabled && { + addresses: cli.addresses, + chainId: cli.chain, + }, chain, } } @@ -190,6 +196,7 @@ export interface DiscoveryCliConfig { singleDiscovery: SingleDiscoveryModuleConfig | false chain: DiscoveryChainConfig invert: InversionConfig | false + layout: LayoutConfig | false } export interface DiscoveryModuleConfig { @@ -223,3 +230,8 @@ export interface InversionConfig { readonly useMermaidMarkup: boolean readonly chainId: ChainId } + +export interface LayoutConfig { + readonly addresses: EthereumAddress[] + readonly chainId: ChainId +} diff --git a/packages/discovery/src/layout/LayoutItem.ts b/packages/discovery/src/layout/LayoutItem.ts new file mode 100644 index 00000000..2be2283d --- /dev/null +++ b/packages/discovery/src/layout/LayoutItem.ts @@ -0,0 +1,77 @@ +export type LayoutItem = + | StaticItem + | StructItem + | StaticArrayItem + | DynamicArrayItem + | MappingItem + | DynamicBytesItem + +export type AnonymousItem = + | Anonymize + | Anonymize + | Anonymize + | Anonymize + | Anonymize + | Anonymize + +type Anonymize = Omit + +export interface StaticItem { + name: string + kind: 'static' + type: string + slot: number + offset: number + size: number +} + +export interface StructItem { + name: string + kind: 'struct' + type: string + slot: number + offset: number + size: number + children: LayoutItem[] +} + +export interface StaticArrayItem { + name: string + kind: 'static array' + type: string + slot: number + offset: number + size: number + length: number + item: AnonymousItem +} + +export interface DynamicArrayItem { + name: string + kind: 'dynamic array' + type: string + slot: number + offset: number + size: number + item: AnonymousItem +} + +export interface MappingItem { + name: string + kind: 'mapping' + type: string + slot: number + offset: number + size: number + key: AnonymousItem + value: AnonymousItem +} + +export interface DynamicBytesItem { + name: string + kind: 'dynamic bytes' + type: string + slot: number + offset: number + size: number +} diff --git a/packages/discovery/src/layout/SlotView.ts b/packages/discovery/src/layout/SlotView.ts new file mode 100644 index 00000000..3b38da00 --- /dev/null +++ b/packages/discovery/src/layout/SlotView.ts @@ -0,0 +1,52 @@ +export type SlotView = + | SingleSlotView + | CompositeSlotView + | BytesSlotView + | MappingSlotView + | ArraySlotView + +export interface SingleSlotView { + kind: 'static single' + path: string[] + slot: number + variable: SlotVariable +} + +export interface CompositeSlotView { + kind: 'static composite' + path: string[] + slot: number + variables: SlotVariable[] +} + +export interface BytesSlotView { + kind: 'dynamic bytes' + path: string[] + slot: number + variable: SlotVariable +} + +export interface MappingSlotView { + kind: 'dynamic mapping' + path: string[] + slot: number + variable: SlotVariable + keyType: string + valueView: SlotView[] +} + +export interface ArraySlotView { + kind: 'dynamic array' + path: string[] + slot: number + variable: SlotVariable + itemView: SlotView[] +} + +export interface SlotVariable { + name: string + aliases: string[] + type: string + offset: number + size: number +} diff --git a/packages/discovery/src/layout/SolidityStorageLayout.ts b/packages/discovery/src/layout/SolidityStorageLayout.ts new file mode 100644 index 00000000..28127edc --- /dev/null +++ b/packages/discovery/src/layout/SolidityStorageLayout.ts @@ -0,0 +1,28 @@ +import { z } from 'zod' + +export type SolidityStorageEntry = z.infer +export const SolidityStorageEntry = z.strictObject({ + astId: z.number(), + contract: z.string(), + label: z.string(), + offset: z.number(), + slot: z.string(), + type: z.string(), +}) + +export type SolidityTypeEntry = z.infer +export const SolidityTypeEntry = z.strictObject({ + encoding: z.enum(['inplace', 'mapping', 'dynamic_array', 'bytes']), + label: z.string(), + numberOfBytes: z.string(), + key: z.string().optional(), + value: z.string().optional(), + base: z.string().optional(), + members: z.array(SolidityStorageEntry).optional(), +}) + +export type SolidityStorageLayout = z.infer +export const SolidityStorageLayout = z.strictObject({ + storage: z.array(SolidityStorageEntry), + types: z.record(SolidityTypeEntry).nullable(), +}) diff --git a/packages/discovery/src/layout/flattenLayout.test.ts b/packages/discovery/src/layout/flattenLayout.test.ts new file mode 100644 index 00000000..1629e67b --- /dev/null +++ b/packages/discovery/src/layout/flattenLayout.test.ts @@ -0,0 +1,346 @@ +import { expect } from 'earl' + +import { flattenLayout } from './flattenLayout' + +describe(flattenLayout.name, () => { + it('works for the example from solidity docs', () => { + const result = flattenLayout([ + { + name: 'x', + kind: 'static', + type: 'uint256', + slot: 0, + offset: 0, + size: 32, + }, + { + name: 'y', + kind: 'static', + type: 'uint256', + slot: 1, + offset: 0, + size: 32, + }, + { + name: 's', + kind: 'struct', + type: 'struct A.S', + slot: 2, + offset: 0, + size: 32 * 4, + children: [ + { + name: 'a', + kind: 'static', + type: 'uint128', + slot: 0, + offset: 0, + size: 16, + }, + { + name: 'b', + kind: 'static', + type: 'uint128', + slot: 0, + offset: 16, + size: 16, + }, + { + name: 'staticArray', + kind: 'static array', + type: 'uint256[2]', + slot: 1, + offset: 0, + size: 32 * 2, + length: 2, + item: { + kind: 'static', + type: 'uint256', + size: 32, + }, + }, + { + name: 'dynArray', + kind: 'dynamic array', + type: 'uint256[]', + slot: 3, + offset: 0, + size: 32, + item: { + kind: 'static', + type: 'uint256', + size: 32, + }, + }, + ], + }, + { + name: 'addr', + kind: 'static', + type: 'address', + slot: 6, + offset: 0, + size: 20, + }, + { + name: 'map', + kind: 'mapping', + type: 'mapping(uint256 => mapping(address => bool))', + slot: 7, + offset: 0, + size: 32, + key: { + kind: 'static', + type: 'uint256', + size: 32, + }, + value: { + kind: 'mapping', + type: 'mapping(address => bool)', + size: 32, + key: { + kind: 'static', + type: 'address', + size: 20, + }, + value: { + kind: 'static', + type: 'bool', + size: 1, + }, + }, + }, + { + name: 'array', + kind: 'dynamic array', + type: 'uint256[]', + slot: 8, + offset: 0, + size: 32, + item: { + kind: 'static', + type: 'uint256', + size: 32, + }, + }, + { + name: 's1', + kind: 'dynamic bytes', + type: 'string', + slot: 9, + offset: 0, + size: 32, + }, + { + name: 'b1', + kind: 'dynamic bytes', + type: 'bytes', + slot: 10, + offset: 0, + size: 32, + }, + ]) + + expect(result).toEqual([ + { + kind: 'static single', + slot: 0, + path: [], + variable: { + name: 'x', + aliases: [], + type: 'uint256', + offset: 0, + size: 32, + }, + }, + { + kind: 'static single', + path: [], + slot: 1, + variable: { + name: 'y', + aliases: [], + type: 'uint256', + offset: 0, + size: 32, + }, + }, + { + kind: 'static composite', + path: ['s'], + slot: 2, + variables: [ + { + name: 'a', + aliases: [], + type: 'uint128', + offset: 0, + size: 16, + }, + { + name: 'b', + aliases: [], + type: 'uint128', + offset: 16, + size: 16, + }, + ], + }, + { + kind: 'static single', + path: ['s', 'staticArray'], + slot: 3, + variable: { + name: '0', + aliases: [], + type: 'uint256', + offset: 0, + size: 32, + }, + }, + { + kind: 'static single', + path: ['s', 'staticArray'], + slot: 4, + variable: { + name: '1', + aliases: [], + type: 'uint256', + offset: 0, + size: 32, + }, + }, + { + kind: 'dynamic array', + path: ['s'], + slot: 5, + variable: { + name: 'dynArray', + aliases: [], + type: 'uint256[]', + offset: 0, + size: 32, + }, + itemView: [ + { + kind: 'static single', + path: ['s', 'dynArray'], + slot: 0, + variable: { + name: '#', + aliases: [], + type: 'uint256', + offset: 0, + size: 32, + }, + }, + ], + }, + { + kind: 'static single', + path: [], + slot: 6, + variable: { + name: 'addr', + aliases: [], + type: 'address', + offset: 0, + size: 20, + }, + }, + { + kind: 'dynamic mapping', + path: [], + slot: 7, + variable: { + name: 'map', + aliases: [], + type: 'mapping(uint256 => mapping(address => bool))', + offset: 0, + size: 32, + }, + keyType: 'uint256', + valueView: [ + { + kind: 'dynamic mapping', + path: ['map'], + slot: 0, + variable: { + name: '*', + aliases: [], + type: 'mapping(address => bool)', + offset: 0, + size: 32, + }, + keyType: 'address', + valueView: [ + { + kind: 'static single', + path: ['map', '*'], + slot: 0, + variable: { + name: '*', + aliases: [], + type: 'bool', + offset: 0, + size: 1, + }, + }, + ], + }, + ], + }, + { + kind: 'dynamic array', + path: [], + slot: 8, + variable: { + name: 'array', + aliases: [], + type: 'uint256[]', + offset: 0, + size: 32, + }, + itemView: [ + { + kind: 'static single', + path: ['array'], + slot: 0, + variable: { + name: '#', + aliases: [], + type: 'uint256', + offset: 0, + size: 32, + }, + }, + ], + }, + { + kind: 'dynamic bytes', + path: [], + slot: 9, + variable: { + name: 's1', + aliases: [], + type: 'string', + offset: 0, + size: 32, + }, + }, + { + kind: 'dynamic bytes', + path: [], + slot: 10, + variable: { + aliases: [], + name: 'b1', + type: 'bytes', + offset: 0, + size: 32, + }, + }, + ]) + }) +}) diff --git a/packages/discovery/src/layout/flattenLayout.ts b/packages/discovery/src/layout/flattenLayout.ts new file mode 100644 index 00000000..6022aa73 --- /dev/null +++ b/packages/discovery/src/layout/flattenLayout.ts @@ -0,0 +1,114 @@ +import { LayoutItem } from './LayoutItem' +import { SlotView } from './SlotView' + +export function flattenLayout( + layout: LayoutItem[], + slotOffset = 0, + path: string[] = [], +): SlotView[] { + const slots: SlotView[] = [] + for (let i = 0; i < layout.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const items: LayoutItem[] = [layout[i]!] + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + while (i < layout.length - 1 && layout[i]!.slot === layout[i + 1]!.slot) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + items.push(layout[i + 1]!) + i++ + } + const item = items[0] + if (items.length === 1 && item) { + if (item.kind === 'struct') { + slots.push( + ...flattenLayout( + item.children, + slotOffset + item.slot, + path.concat(item.name), + ), + ) + } else if (item.kind === 'static array') { + const items = Array.from({ length: item.length }).map( + (_, i): LayoutItem => ({ + ...item.item, + name: i.toString(), + offset: 0, + slot: i * Math.ceil(item.item.size / 32), + }), + ) + slots.push( + ...flattenLayout( + items, + slotOffset + item.slot, + path.concat(item.name), + ), + ) + } else if (item.kind === 'dynamic array') { + slots.push({ + kind: 'dynamic array', + path, + slot: slotOffset + item.slot, + variable: { + name: item.name, + aliases: [], + offset: 0, + size: 32, + type: item.type, + }, + itemView: flattenLayout( + [{ ...item.item, name: '#', offset: 0, slot: 0 }], + 0, + path.concat(item.name), + ), + }) + } else if (item.kind === 'mapping') { + slots.push({ + kind: 'dynamic mapping', + path, + slot: slotOffset + item.slot, + variable: { + name: item.name, + aliases: [], + offset: 0, + size: 32, + type: item.type, + }, + keyType: item.key.type, + valueView: flattenLayout( + [{ ...item.value, name: '*', offset: 0, slot: 0 }], + 0, + path.concat(item.name), + ), + }) + } else { + slots.push({ + kind: + item.kind === 'dynamic bytes' ? 'dynamic bytes' : 'static single', + path, + slot: slotOffset + item.slot, + variable: { + name: item.name, + aliases: [], + offset: item.offset, + size: item.size, + type: item.type, + }, + }) + } + } else { + slots.push({ + kind: 'static composite', + path, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + slot: slotOffset + item!.slot, + variables: items.map((item) => ({ + name: item.name, + aliases: [], + offset: item.offset, + size: item.size, + type: item.type, + })), + }) + } + } + return slots +} diff --git a/packages/discovery/src/layout/getLayout.test.ts b/packages/discovery/src/layout/getLayout.test.ts new file mode 100644 index 00000000..15dd38ac --- /dev/null +++ b/packages/discovery/src/layout/getLayout.test.ts @@ -0,0 +1,312 @@ +import { expect } from 'earl' + +import { getLayout } from './getLayout' +import { SolidityStorageLayout } from './SolidityStorageLayout' + +describe(getLayout.name, () => { + it('processes the example from solidity docs', () => { + const example: SolidityStorageLayout = { + storage: [ + { + astId: 15, + contract: 'fileA:A', + label: 'x', + offset: 0, + slot: '0', + type: 't_uint256', + }, + { + astId: 17, + contract: 'fileA:A', + label: 'y', + offset: 0, + slot: '1', + type: 't_uint256', + }, + { + astId: 20, + contract: 'fileA:A', + label: 's', + offset: 0, + slot: '2', + type: 't_struct(S)13_storage', + }, + { + astId: 22, + contract: 'fileA:A', + label: 'addr', + offset: 0, + slot: '6', + type: 't_address', + }, + { + astId: 28, + contract: 'fileA:A', + label: 'map', + offset: 0, + slot: '7', + type: 't_mapping(t_uint256,t_mapping(t_address,t_bool))', + }, + { + astId: 31, + contract: 'fileA:A', + label: 'array', + offset: 0, + slot: '8', + type: 't_array(t_uint256)dyn_storage', + }, + { + astId: 33, + contract: 'fileA:A', + label: 's1', + offset: 0, + slot: '9', + type: 't_string_storage', + }, + { + astId: 35, + contract: 'fileA:A', + label: 'b1', + offset: 0, + slot: '10', + type: 't_bytes_storage', + }, + ], + types: { + t_address: { + encoding: 'inplace', + label: 'address', + numberOfBytes: '20', + }, + 't_array(t_uint256)2_storage': { + base: 't_uint256', + encoding: 'inplace', + label: 'uint256[2]', + numberOfBytes: '64', + }, + 't_array(t_uint256)dyn_storage': { + base: 't_uint256', + encoding: 'dynamic_array', + label: 'uint256[]', + numberOfBytes: '32', + }, + t_bool: { + encoding: 'inplace', + label: 'bool', + numberOfBytes: '1', + }, + t_bytes_storage: { + encoding: 'bytes', + label: 'bytes', + numberOfBytes: '32', + }, + 't_mapping(t_address,t_bool)': { + encoding: 'mapping', + key: 't_address', + label: 'mapping(address => bool)', + numberOfBytes: '32', + value: 't_bool', + }, + 't_mapping(t_uint256,t_mapping(t_address,t_bool))': { + encoding: 'mapping', + key: 't_uint256', + label: 'mapping(uint256 => mapping(address => bool))', + numberOfBytes: '32', + value: 't_mapping(t_address,t_bool)', + }, + t_string_storage: { + encoding: 'bytes', + label: 'string', + numberOfBytes: '32', + }, + 't_struct(S)13_storage': { + encoding: 'inplace', + label: 'struct A.S', + members: [ + { + astId: 3, + contract: 'fileA:A', + label: 'a', + offset: 0, + slot: '0', + type: 't_uint128', + }, + { + astId: 5, + contract: 'fileA:A', + label: 'b', + offset: 16, + slot: '0', + type: 't_uint128', + }, + { + astId: 9, + contract: 'fileA:A', + label: 'staticArray', + offset: 0, + slot: '1', + type: 't_array(t_uint256)2_storage', + }, + { + astId: 12, + contract: 'fileA:A', + label: 'dynArray', + offset: 0, + slot: '3', + type: 't_array(t_uint256)dyn_storage', + }, + ], + numberOfBytes: '128', + }, + t_uint128: { + encoding: 'inplace', + label: 'uint128', + numberOfBytes: '16', + }, + t_uint256: { + encoding: 'inplace', + label: 'uint256', + numberOfBytes: '32', + }, + }, + } + + const processed = getLayout(example) + expect(processed).toEqual([ + { + name: 'x', + kind: 'static', + type: 'uint256', + slot: 0, + offset: 0, + size: 32, + }, + { + name: 'y', + kind: 'static', + type: 'uint256', + slot: 1, + offset: 0, + size: 32, + }, + { + name: 's', + kind: 'struct', + type: 'struct A.S', + slot: 2, + offset: 0, + size: 32 * 4, + children: [ + { + name: 'a', + kind: 'static', + type: 'uint128', + slot: 0, + offset: 0, + size: 16, + }, + { + name: 'b', + kind: 'static', + type: 'uint128', + slot: 0, + offset: 16, + size: 16, + }, + { + name: 'staticArray', + kind: 'static array', + type: 'uint256[2]', + slot: 1, + offset: 0, + size: 32 * 2, + length: 2, + item: { + kind: 'static', + type: 'uint256', + size: 32, + }, + }, + { + name: 'dynArray', + kind: 'dynamic array', + type: 'uint256[]', + slot: 3, + offset: 0, + size: 32, + item: { + kind: 'static', + type: 'uint256', + size: 32, + }, + }, + ], + }, + { + name: 'addr', + kind: 'static', + type: 'address', + slot: 6, + offset: 0, + size: 20, + }, + { + name: 'map', + kind: 'mapping', + type: 'mapping(uint256 => mapping(address => bool))', + slot: 7, + offset: 0, + size: 32, + key: { + kind: 'static', + type: 'uint256', + size: 32, + }, + value: { + kind: 'mapping', + type: 'mapping(address => bool)', + size: 32, + key: { + kind: 'static', + type: 'address', + size: 20, + }, + value: { + kind: 'static', + type: 'bool', + size: 1, + }, + }, + }, + { + name: 'array', + kind: 'dynamic array', + type: 'uint256[]', + slot: 8, + offset: 0, + size: 32, + item: { + kind: 'static', + type: 'uint256', + size: 32, + }, + }, + { + name: 's1', + kind: 'dynamic bytes', + type: 'string', + slot: 9, + offset: 0, + size: 32, + }, + { + name: 'b1', + kind: 'dynamic bytes', + type: 'bytes', + slot: 10, + offset: 0, + size: 32, + }, + ]) + }) +}) diff --git a/packages/discovery/src/layout/getLayout.ts b/packages/discovery/src/layout/getLayout.ts new file mode 100644 index 00000000..483b156f --- /dev/null +++ b/packages/discovery/src/layout/getLayout.ts @@ -0,0 +1,99 @@ +import { AnonymousItem, LayoutItem } from './LayoutItem' +import { + SolidityStorageEntry, + SolidityStorageLayout, + SolidityTypeEntry, +} from './SolidityStorageLayout' + +const MAX_DEPTH = 10 + +export function getLayout(output: SolidityStorageLayout): LayoutItem[] { + return output.storage.map((item) => parseEntry(item, output.types ?? {}, 0)) +} + +function parseEntry( + item: SolidityStorageEntry, + types: Record, + depth: number, +): LayoutItem { + return { + name: item.label, + slot: parseInt(item.slot, 10), + offset: item.offset, + ...parseType(item.type, types, depth), + } +} + +function parseType( + typeKey: string, + types: Record, + depth: number, +): AnonymousItem { + const type = types[typeKey] + if (!type) { + throw new Error(`Unknown type: ${typeKey}`) + } + + const typeBase = { + type: type.label, + size: parseInt(type.numberOfBytes, 10), + } + + if (type.encoding === 'inplace' && !type.base && !type.members) { + return { kind: 'static', ...typeBase } + } + + if (type.encoding === 'bytes') { + return { + kind: 'dynamic bytes', + ...typeBase, + } + } + + if (depth >= MAX_DEPTH) { + return { + kind: 'static', + ...typeBase, + type: 'Error: Maximum depth exceeded', + } + } + + if (type.encoding === 'inplace' && type.members) { + return { + kind: 'struct', + ...typeBase, + children: type.members.map((member) => + parseEntry(member, types, depth + 1), + ), + } + } + + if (type.encoding === 'inplace' && type.base) { + const item = parseType(type.base, types, depth + 1) + return { + kind: 'static array', + ...typeBase, + length: parseInt(type.numberOfBytes, 10) / item.size, + item, + } + } + + if (type.encoding === 'dynamic_array' && type.base) { + return { + kind: 'dynamic array', + ...typeBase, + item: parseType(type.base, types, depth + 1), + } + } + + if (type.encoding === 'mapping' && type.key && type.value) { + return { + kind: 'mapping', + ...typeBase, + key: parseType(type.key, types, depth + 1), + value: parseType(type.value, types, depth + 1), + } + } + + throw new Error('Invalid type') +} diff --git a/packages/discovery/src/layout/mergeFlatLayouts.test.ts b/packages/discovery/src/layout/mergeFlatLayouts.test.ts new file mode 100644 index 00000000..694bf04d --- /dev/null +++ b/packages/discovery/src/layout/mergeFlatLayouts.test.ts @@ -0,0 +1,578 @@ +import { expect } from 'earl' + +import { mergeFlatLayouts } from './mergeFlatLayouts' +import { SlotView } from './SlotView' + +describe(mergeFlatLayouts.name, () => { + it('identical item', () => { + const a: SlotView[] = [ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'a', + aliases: ['c'], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'b', + aliases: ['b1'], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + ] + const b: SlotView[] = [ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'a', + aliases: ['c'], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'b', + aliases: ['b1'], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + ] + expect(mergeFlatLayouts([a, b])).toEqual(a) + }) + + it('renamed item', () => { + const a: SlotView[] = [ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'a', + aliases: ['c'], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'b', + aliases: ['b1'], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + ] + const b: SlotView[] = [ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'y', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'c', + aliases: [], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'd', + aliases: [], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + ] + expect(mergeFlatLayouts([a, b])).toEqual([ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'x', + aliases: ['y'], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'a', + aliases: ['c'], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'b', + aliases: ['b1', 'd'], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + ]) + }) + + it('different item', () => { + const a: SlotView[] = [ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'a', + aliases: ['c'], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'b', + aliases: ['b1'], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + ] + const b: SlotView[] = [ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'y', + aliases: [], + offset: 0, + size: 20, + type: 'address', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'c', + aliases: [], + offset: 0, + size: 20, + type: 'address', + }, + { + name: 'd', + aliases: [], + offset: 20, + size: 1, + type: 'bool', + }, + ], + }, + ] + expect(mergeFlatLayouts([a, b])).toEqual([ + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: [], + slot: 0, + variable: { + name: 'y', + aliases: [], + offset: 0, + size: 20, + type: 'address', + }, + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'a', + aliases: ['c'], + offset: 0, + size: 16, + type: 'uint128', + }, + { + name: 'b', + aliases: ['b1'], + offset: 16, + size: 16, + type: 'uint128', + }, + ], + }, + { + kind: 'static composite', + path: [], + slot: 1, + variables: [ + { + name: 'c', + aliases: [], + offset: 0, + size: 20, + type: 'address', + }, + { + name: 'd', + aliases: [], + offset: 20, + size: 1, + type: 'bool', + }, + ], + }, + ]) + }) + + it('recursive array', () => { + const a: SlotView[] = [ + { + kind: 'dynamic array', + path: [], + slot: 0, + variable: { + name: 'array', + aliases: [], + offset: 0, + size: 32, + type: 'struct S[]', + }, + itemView: [ + { + kind: 'static single', + path: ['array', '#'], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: ['array', '#'], + slot: 1, + variable: { + name: 'y', + aliases: [], + offset: 32, + size: 32, + type: 'uint256', + }, + }, + ], + }, + ] + const b: SlotView[] = [ + { + kind: 'dynamic array', + path: [], + slot: 0, + variable: { + name: 'array', + aliases: [], + offset: 0, + size: 32, + type: 'struct S[]', + }, + itemView: [ + { + kind: 'static single', + path: ['array', '#'], + slot: 0, + variable: { + name: 'a', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: ['array', '#'], + slot: 1, + variable: { + name: 'b', + aliases: [], + offset: 32, + size: 32, + type: 'uint256', + }, + }, + ], + }, + ] + expect(mergeFlatLayouts([a, b])).toEqual([ + { + kind: 'dynamic array', + path: [], + slot: 0, + variable: { + name: 'array', + aliases: [], + offset: 0, + size: 32, + type: 'struct S[]', + }, + itemView: [ + { + kind: 'static single', + path: ['array', '#'], + slot: 0, + variable: { + name: 'x', + aliases: ['a'], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: ['array', '#'], + slot: 1, + variable: { + name: 'y', + aliases: ['b'], + offset: 32, + size: 32, + type: 'uint256', + }, + }, + ], + }, + ]) + }) + + it('recursive mapping', () => { + const a: SlotView[] = [ + { + kind: 'dynamic mapping', + path: [], + slot: 0, + variable: { + name: 'map', + aliases: [], + offset: 0, + size: 32, + type: 'mapping(uint256 => struct S)', + }, + keyType: 'uint256', + valueView: [ + { + kind: 'static single', + path: ['array', '#'], + slot: 0, + variable: { + name: 'x', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: ['array', '#'], + slot: 1, + variable: { + name: 'y', + aliases: [], + offset: 32, + size: 32, + type: 'uint256', + }, + }, + ], + }, + ] + const b: SlotView[] = [ + { + kind: 'dynamic mapping', + path: [], + slot: 0, + variable: { + name: 'map', + aliases: [], + offset: 0, + size: 32, + type: 'mapping(uint256 => struct S)', + }, + keyType: 'uint256', + valueView: [ + { + kind: 'static single', + path: ['array', '#'], + slot: 0, + variable: { + name: 'a', + aliases: [], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: ['array', '#'], + slot: 1, + variable: { + name: 'b', + aliases: [], + offset: 32, + size: 32, + type: 'uint256', + }, + }, + ], + }, + ] + expect(mergeFlatLayouts([a, b])).toEqual([ + { + kind: 'dynamic mapping', + path: [], + slot: 0, + variable: { + name: 'map', + aliases: [], + offset: 0, + size: 32, + type: 'mapping(uint256 => struct S)', + }, + keyType: 'uint256', + valueView: [ + { + kind: 'static single', + path: ['array', '#'], + slot: 0, + variable: { + name: 'x', + aliases: ['a'], + offset: 0, + size: 32, + type: 'uint256', + }, + }, + { + kind: 'static single', + path: ['array', '#'], + slot: 1, + variable: { + name: 'y', + aliases: ['b'], + offset: 32, + size: 32, + type: 'uint256', + }, + }, + ], + }, + ]) + }) +}) diff --git a/packages/discovery/src/layout/mergeFlatLayouts.ts b/packages/discovery/src/layout/mergeFlatLayouts.ts new file mode 100644 index 00000000..b650ea80 --- /dev/null +++ b/packages/discovery/src/layout/mergeFlatLayouts.ts @@ -0,0 +1,83 @@ +import { cloneDeep, zip } from 'lodash' + +import { SlotVariable, SlotView } from './SlotView' + +export function mergeFlatLayouts(layouts: SlotView[][]): SlotView[] { + return layouts.reduce((a, b) => mergeTwoLayouts(cloneDeep(a), b)) +} + +function mergeTwoLayouts(a: SlotView[], b: SlotView[]): SlotView[] { + for (const item of b) { + const existing = a.find((x) => isEqual(x, item)) + if (!existing) { + a.push(item) + } else { + if ( + existing.kind !== 'static composite' && + item.kind !== 'static composite' + ) { + addAlias(existing.variable, item.variable.name) + } + if ( + existing.kind === 'static composite' && + item.kind === 'static composite' + ) { + for (const [i, variable] of existing.variables.entries()) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + addAlias(variable, item.variables[i]!.name) + } + } + if (existing.kind === 'dynamic array' && item.kind === 'dynamic array') { + existing.itemView = mergeTwoLayouts(existing.itemView, item.itemView) + } + if ( + existing.kind === 'dynamic mapping' && + item.kind === 'dynamic mapping' + ) { + existing.valueView = mergeTwoLayouts(existing.valueView, item.valueView) + } + } + } + return a.sort((a, b) => a.slot - b.slot) +} + +function isEqual(a: SlotView, b: SlotView): boolean { + if ( + a.kind !== b.kind || + a.slot !== b.slot || + a.path.join('.') !== b.path.join('.') + ) { + return false + } + if ( + (a.kind === 'static single' && b.kind === 'static single') || + (a.kind === 'dynamic bytes' && b.kind === 'dynamic bytes') + ) { + return variableEquals(a.variable, b.variable) + } + if (a.kind === 'static composite' && b.kind === 'static composite') { + return zip(a.variables, b.variables).every( + ([x, y]) => x && y && variableEquals(x, y), + ) + } + if (a.kind === 'dynamic array' && b.kind === 'dynamic array') { + return ( + variableEquals(a.variable, b.variable) && + a.itemView.length === b.itemView.length + ) + } + if (a.kind === 'dynamic mapping' && b.kind === 'dynamic mapping') { + return variableEquals(a.variable, b.variable) && a.keyType === b.keyType + } + return false +} + +function variableEquals(a: SlotVariable, b: SlotVariable): boolean { + return a.offset === b.offset && a.size === b.size && a.type === b.type +} + +function addAlias(variable: SlotVariable, name: string): void { + if (variable.name !== name && !variable.aliases.includes(name)) { + variable.aliases.push(name) + } +} diff --git a/packages/discovery/src/layout/parseAndGetLayout.ts b/packages/discovery/src/layout/parseAndGetLayout.ts new file mode 100644 index 00000000..3d6f4314 --- /dev/null +++ b/packages/discovery/src/layout/parseAndGetLayout.ts @@ -0,0 +1,38 @@ +import * as solc from 'solc' +import { z } from 'zod' + +import { ContractSource } from '../utils/EtherscanLikeClient' +import { getLayout } from './getLayout' +import { LayoutItem } from './LayoutItem' +import { parseContractSource, SolcInput } from './parseContractSource' +import { SolidityStorageLayout } from './SolidityStorageLayout' + +export function parseAndGetLayout(source: ContractSource): LayoutItem[] { + const input = parseContractSource(source.SourceCode) + if (!input.settings) { + input.settings = {} + } + input.settings.outputSelection = { '*': { '*': ['storageLayout'] } } + + const out = compile(input) + const mainContract = Object.values(out.contracts).find( + (c) => c[source.ContractName], + )?.[source.ContractName] + if (!mainContract) { + throw new Error(`Cannot find main contract: ${source.ContractName}`) + } + return getLayout(mainContract.storageLayout) +} + +function compile(input: SolcInput): SpecificSolcOutput { + return SpecificSolcOutput.parse( + JSON.parse(solc.compile(JSON.stringify(input))), + ) +} + +type SpecificSolcOutput = z.infer +const SpecificSolcOutput = z.object({ + contracts: z.record( + z.record(z.object({ storageLayout: SolidityStorageLayout })), + ), +}) diff --git a/packages/discovery/src/layout/parseContractSource.ts b/packages/discovery/src/layout/parseContractSource.ts new file mode 100644 index 00000000..cdd59e4e --- /dev/null +++ b/packages/discovery/src/layout/parseContractSource.ts @@ -0,0 +1,84 @@ +import { z } from 'zod' + +export function parseContractSource(source: string): SolcInput { + if (source.startsWith('{{') && source.endsWith('}}')) { + source = source.slice(1, -1) + } + return SolcInput.parse(JSON.parse(source)) +} + +export type SolcInput = z.infer +export const SolcInput = z.object({ + language: z.string(), + sources: z.record( + z.object({ + keccak256: z.string().optional(), + urls: z.array(z.string()).optional(), + ast: z.unknown().optional(), + content: z.string().optional(), + }), + ), + settings: z + .object({ + stopAfter: z.string().optional(), + remappings: z.array(z.string()).optional(), + optimizer: z + .object({ + enabled: z.boolean().optional(), + runs: z.number().optional(), + details: z + .object({ + peephole: z.boolean().optional(), + inliner: z.boolean().optional(), + jumpdestRemover: z.boolean().optional(), + orderLiterals: z.boolean().optional(), + deduplicate: z.boolean().optional(), + cse: z.boolean().optional(), + constantOptimizer: z.boolean().optional(), + simpleCounterForLoopUncheckedIncrement: z.boolean().optional(), + yul: z.boolean().optional(), + yulDetails: z + .object({ + stackAllocation: z.boolean().optional(), + optimizerSteps: z.string().optional(), + }) + .optional(), + }) + .optional(), + }) + .optional(), + evmVersion: z.string().optional(), + viaIR: z.boolean().optional(), + debug: z + .object({ + revertStrings: z.string().optional(), + debugInfo: z.array(z.string()).optional(), + }) + .optional(), + metadata: z + .object({ + appendCBOR: z.boolean().optional(), + useLiteralContent: z.boolean().optional(), + bytecodeHash: z.string().optional(), + }) + .optional(), + libraries: z.record(z.record(z.string())).optional(), + outputSelection: z.record(z.record(z.array(z.string()))).optional(), + modelChecker: z + .object({ + contracts: z.record(z.array(z.string())).optional(), + divModNoSlacks: z.boolean().optional(), + engine: z.string().optional(), + extCalls: z.string().optional(), + invariants: z.array(z.string()).optional(), + showProved: z.boolean().optional(), + showUnproved: z.boolean().optional(), + showUnsupported: z.boolean().optional(), + solvers: z.array(z.string()).optional(), + targets: z.array(z.string()).optional(), + timeout: z.number().optional(), + }) + .optional(), + }) + .optional(), +}) diff --git a/packages/discovery/src/layout/solc.d.ts b/packages/discovery/src/layout/solc.d.ts new file mode 100644 index 00000000..c4fcf6cf --- /dev/null +++ b/packages/discovery/src/layout/solc.d.ts @@ -0,0 +1,3 @@ +declare module 'solc' { + export function compile(input: string): string +} diff --git a/packages/discovery/tsconfig.json b/packages/discovery/tsconfig.json index ea383eb8..67346bae 100644 --- a/packages/discovery/tsconfig.json +++ b/packages/discovery/tsconfig.json @@ -4,5 +4,8 @@ "outDir": "dist", "incremental": true }, - "include": ["src"] + "include": ["src", "src/layout/solc.d.ts"], + "ts-node": { + "files": true + } } diff --git a/yarn.lock b/yarn.lock index fc062f85..7dc8f17d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1571,6 +1571,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +command-exists@^1.2.8: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + +commander@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -2301,6 +2311,11 @@ flatted@^3.1.0: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +follow-redirects@^1.12.1: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" @@ -3150,6 +3165,11 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + meow@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -4065,6 +4085,19 @@ socks@^2.6.2: ip "^2.0.0" smart-buffer "^4.2.0" +solc@^0.8.23-fixed: + version "0.8.23-fixed" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.23-fixed.tgz#336e36986faaf7e45804b9b136845b6d5c242700" + integrity sha512-XMp8jbXl29nlD0losEG+9nAdH5bibQPELI0jqOpyqCT7DKo7MbIdWPMwiCtK/QKe0CCvCvKbHswBflZmcmXIYA== + dependencies: + command-exists "^1.2.8" + commander "^8.1.0" + follow-redirects "^1.12.1" + js-sha3 "0.8.0" + memorystream "^0.3.1" + semver "^5.5.0" + tmp "0.0.33" + spawndamnit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/spawndamnit/-/spawndamnit-2.0.0.tgz#9f762ac5c3476abb994b42ad592b5ad22bb4b0ad" @@ -4333,7 +4366,7 @@ through@2: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tmp@^0.0.33: +tmp@0.0.33, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==