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

/**
 * 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) => {
86
    const files = fs.readdirSync(dir)
87 88 89 90 91 92 93 94 95 96 97 98 99

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

100
  readFilesRecursively(directoryPath)
101 102 103 104 105

  for (const filePath of paths) {
    if (filePath.includes('t.sol')) {
      continue
    }
106 107
    const raw = fs.readFileSync(filePath, 'utf8').toString()
    const artifact = JSON.parse(raw)
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

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