Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add an experimental layout command #101

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/discovery/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
build
.env
cache
layout.json
6 changes: 6 additions & 0 deletions packages/discovery/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @l2beat/discovery

## 0.26.0

### Minor Changes

- Add an experimental layout command

## 0.25.0

### Minor Changes
Expand Down
3 changes: 2 additions & 1 deletion packages/discovery/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/discovery/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -19,4 +20,5 @@ async function main(): Promise<void> {
await discoverCommand(config, logger)
await invertCommand(config, logger)
await singleDiscoveryCommand(config, logger)
await layoutCommand(config, logger)
}
28 changes: 27 additions & 1 deletion packages/discovery/src/cli/getCliParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export type CliParameters =
| ServerCliParameters
| DiscoverCliParameters
| InvertCliParameters
| HelpCliParameters
| SingleDiscoveryCliParameters
| LayoutCliParameters
| HelpCliParameters

export interface ServerCliParameters {
mode: 'server'
Expand All @@ -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
Expand Down Expand Up @@ -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] ?? '<unknown mode>'

return { mode: 'help', error: `Unknown mode: ${mode}` }
Expand Down
45 changes: 45 additions & 0 deletions packages/discovery/src/cli/layoutCommand.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making this function return the results (the layout variable) and the function above can save them. This would allow us to later export this runLayout and use the results without having to interface with the filesystem

etherscanClient: EtherscanLikeClient,
addresses: EthereumAddress[],
logger: Logger,
): Promise<void> {
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 })
}
1 change: 1 addition & 0 deletions packages/discovery/src/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <start|discover> --help .................... display this message

supported chains: checkout config.discovery.ts
Expand Down
14 changes: 13 additions & 1 deletion packages/discovery/src/config/config.discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ 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}`)
}

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 {
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -190,6 +196,7 @@ export interface DiscoveryCliConfig {
singleDiscovery: SingleDiscoveryModuleConfig | false
chain: DiscoveryChainConfig
invert: InversionConfig | false
layout: LayoutConfig | false
}

export interface DiscoveryModuleConfig {
Expand Down Expand Up @@ -223,3 +230,8 @@ export interface InversionConfig {
readonly useMermaidMarkup: boolean
readonly chainId: ChainId
}

export interface LayoutConfig {
readonly addresses: EthereumAddress[]
readonly chainId: ChainId
}
77 changes: 77 additions & 0 deletions packages/discovery/src/layout/LayoutItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export type LayoutItem =
| StaticItem
| StructItem
| StaticArrayItem
| DynamicArrayItem
| MappingItem
| DynamicBytesItem

export type AnonymousItem =
| Anonymize<StaticItem>
| Anonymize<StructItem>
| Anonymize<StaticArrayItem>
| Anonymize<DynamicArrayItem>
| Anonymize<MappingItem>
| Anonymize<DynamicBytesItem>

type Anonymize<T> = Omit<T, 'name' | 'slot' | 'offset'>

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
}
52 changes: 52 additions & 0 deletions packages/discovery/src/layout/SlotView.ts
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 28 additions & 0 deletions packages/discovery/src/layout/SolidityStorageLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod'

export type SolidityStorageEntry = z.infer<typeof SolidityStorageEntry>
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<typeof SolidityTypeEntry>
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<typeof SolidityStorageLayout>
export const SolidityStorageLayout = z.strictObject({
storage: z.array(SolidityStorageEntry),
types: z.record(SolidityTypeEntry).nullable(),
})
Loading