1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import fs from 'fs'
import path from 'path'
/**
* Directory path to the artifacts.
* Can be configured as the first argument to the script or
* defaults to the forge-artifacts directory.
*/
const directoryPath =
process.argv[2] || path.join(__dirname, '..', 'forge-artifacts')
/**
* Returns true if the contract should be skipped when inspecting its storage layout.
* This is useful for abstract contracts that are meant to be inherited.
* The two particular targets are:
* - CrossDomainMessengerLegacySpacer0
* - CrossDomainMessengerLegacySpacer1
*/
const skipped = (contractName: string): boolean => {
return contractName.includes('CrossDomainMessengerLegacySpacer')
}
/**
* Parses out variable info from the variable structure in standard compiler json output.
*
* @param variable Variable structure from standard compiler json output.
* @returns Parsed variable info.
*/
const parseVariableInfo = (
variable: any
): {
name: string
slot: number
offset: number
length: number
} => {
// Figure out the length of the variable.
let variableLength: number
if (variable.type.startsWith('t_mapping')) {
variableLength = 32
} else if (variable.type.startsWith('t_uint')) {
variableLength = variable.type.match(/uint([0-9]+)/)?.[1] / 8
} else if (variable.type.startsWith('t_bytes_')) {
variableLength = 32
} else if (variable.type.startsWith('t_bytes')) {
variableLength = variable.type.match(/uint([0-9]+)/)?.[1]
} else if (variable.type.startsWith('t_address')) {
variableLength = 20
} else if (variable.type.startsWith('t_bool')) {
variableLength = 1
} else if (variable.type.startsWith('t_array')) {
// Figure out the size of the type inside of the array
// and then multiply that by the length of the array.
// This does not support recursion multiple times for simplicity
const type = variable.type.match(/^t_array\((\w+)\)/)?.[1]
const info = parseVariableInfo({
label: variable.label,
offset: variable.offset,
slot: variable.slot,
type,
})
const size = variable.type.match(/^t_array\(\w+\)([0-9]+)/)?.[1]
variableLength = info.length * parseInt(size, 10)
} else {
throw new Error(
`${variable.label}: unsupported type ${variable.type}, add it to the script`
)
}
return {
name: variable.label,
slot: parseInt(variable.slot, 10),
offset: variable.offset,
length: variableLength,
}
}
/**
* Main logic of the script
* - Ensures that all of the spacer variables are named correctly
*/
const main = async () => {
const paths = []
const readFilesRecursively = (dir: string) => {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const fileStat = fs.statSync(filePath)
if (fileStat.isDirectory()) {
readFilesRecursively(filePath)
} else {
paths.push(filePath)
}
}
}
readFilesRecursively(directoryPath)
for (const filePath of paths) {
if (filePath.includes('t.sol')) {
continue
}
const raw = fs.readFileSync(filePath, 'utf8').toString()
const artifact = JSON.parse(raw)
// Handle contracts without storage
const storageLayout = artifact.storageLayout || {}
if (storageLayout.storage) {
for (const variable of storageLayout.storage) {
const fqn = variable.contract
// Skip some abstract contracts
if (skipped(fqn)) {
continue
}
// Check that the spacers are all named correctly
if (variable.label.startsWith('spacer_')) {
const [, slot, offset, length] = variable.label.split('_')
const variableInfo = parseVariableInfo(variable)
// Check that the slot is correct.
if (parseInt(slot, 10) !== variableInfo.slot) {
throw new Error(
`${fqn} ${variable.label} is in slot ${variable.slot} but should be in ${slot}`
)
}
// Check that the offset is correct.
if (parseInt(offset, 10) !== variableInfo.offset) {
throw new Error(
`${fqn} ${variable.label} is at offset ${variable.offset} but should be at ${offset}`
)
}
// Check that the length is correct.
if (parseInt(length, 10) !== variableInfo.length) {
throw new Error(
`${fqn} ${variable.label} is ${variableInfo.length} bytes long but should be ${length}`
)
}
console.log(`${fqn}.${variable.label} is valid`)
}
}
}
}
}
main()