validate-spacers.ts 5.6 KB
import fs from 'fs'
import path from 'path'

import layoutLock from '../layout-lock.json'

/**
 * 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 the fully qualified name of a contract into the name of the contract.
 * For example `contracts/Foo.sol:Foo` becomes `Foo`.
 */
const parseFqn = (name: string): string => {
  const parts = name.split(':')
  return parts[parts.length - 1]
}

/**
 * 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
 * - Ensures that storage slots in the layout lock file do not change
 */
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
        }

        const contractName = parseFqn(fqn)

        // Check that the layout lock has not changed
        const lock = layoutLock[contractName] || {}
        if (lock[variable.label]) {
          const variableInfo = parseVariableInfo(variable)
          const expectedInfo = lock[variable.label]
          if (variableInfo.slot !== expectedInfo.slot) {
            throw new Error(`${fqn}.${variable.label} slot has changed`)
          }
          if (variableInfo.offset !== expectedInfo.offset) {
            throw new Error(`${fqn}.${variable.label} offset has changed`)
          }
          if (variableInfo.length !== expectedInfo.length) {
            throw new Error(`${fqn}.${variable.label} length has changed`)
          }
        }

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