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()