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