validate-spacers.ts 5.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
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.
 */
11 12
const directoryPath =
  process.argv[2] || path.join(__dirname, '..', 'forge-artifacts')
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

/**
 * 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) => {
98
    const files = fs.readdirSync(dir)
99 100 101 102 103 104 105 106 107 108 109 110 111

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

112
  readFilesRecursively(directoryPath)
113 114 115 116 117

  for (const filePath of paths) {
    if (filePath.includes('t.sol')) {
      continue
    }
118 119
    const raw = fs.readFileSync(filePath, 'utf8').toString()
    const artifact = JSON.parse(raw)
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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182

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