validate-spacers.ts 4.64 KB
Newer Older
1 2
import { task } from 'hardhat/config'

3 4 5 6 7 8 9 10 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
import layoutLock from '../layout-lock.json'

/**
 * 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 {
    throw new Error('unsupported type, add it to the script')
  }

  return {
    name: variable.label,
    slot: parseInt(variable.slot, 10),
    offset: variable.offset,
    length: variableLength,
  }
}

43 44 45 46
task(
  'validate-spacers',
  'validates that spacer variables are in the correct positions'
).setAction(async (args, hre) => {
47 48
  const accounted: string[] = []

49 50 51 52 53 54 55 56
  const names = await hre.artifacts.getAllFullyQualifiedNames()
  for (const name of names) {
    // Script is remarkably slow because of getBuildInfo, so better to skip test files since they
    // don't matter for this check.
    if (name.includes('.t.sol')) {
      continue
    }

57 58
    // Some files may not have buildInfo (certain libraries). We can safely skip these because we
    // make sure that everything is accounted for anyway.
59
    const buildInfo = await hre.artifacts.getBuildInfo(name)
60 61 62 63 64
    if (buildInfo === undefined) {
      console.log(`Skipping ${name} because it has no buildInfo`)
      continue
    }

65 66 67
    for (const source of Object.values(buildInfo.output.contracts)) {
      for (const [contractName, contract] of Object.entries(source)) {
        const storageLayout = (contract as any).storageLayout
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 98 99 100 101 102

        // Check that the layout lock is respected.
        if (layoutLock[contractName]) {
          const removed = Object.entries(layoutLock[contractName]).filter(
            ([key, val]: any) => {
              const storage = storageLayout?.storage || []
              return !storage.some((item: any) => {
                // Skip anything that doesn't clearly match the key because otherwise we'll get an
                // error while parsing the variable info for unsupported variable types.
                if (!item.label.includes(key)) {
                  return false
                }

                // Make sure the variable matches **exactly**.
                const variableInfo = parseVariableInfo(item)
                return (
                  variableInfo.name === key &&
                  variableInfo.offset === val.offset &&
                  variableInfo.slot === val.slot &&
                  variableInfo.length === val.length
                )
              })
            }
          )

          if (removed.length > 0) {
            throw new Error(
              `variable(s) removed from ${contractName}: ${removed.join(', ')}`
            )
          }

          accounted.push(contractName)
        }

        // Check that the positions have not changed.
103 104 105
        for (const variable of storageLayout?.storage || []) {
          if (variable.label.startsWith('spacer_')) {
            const [, slot, offset, length] = variable.label.split('_')
106
            const variableInfo = parseVariableInfo(variable)
107 108

            // Check that the slot is correct.
109
            if (parseInt(slot, 10) !== variableInfo.slot) {
110 111 112 113 114 115
              throw new Error(
                `${contractName} ${variable.label} is in slot ${variable.slot} but should be in ${slot}`
              )
            }

            // Check that the offset is correct.
116
            if (parseInt(offset, 10) !== variableInfo.offset) {
117 118 119 120 121 122
              throw new Error(
                `${contractName} ${variable.label} is at offset ${variable.offset} but should be at ${offset}`
              )
            }

            // Check that the length is correct.
123
            if (parseInt(length, 10) !== variableInfo.length) {
124
              throw new Error(
125
                `${contractName} ${variable.label} is ${variableInfo.length} bytes long but should be ${length}`
126 127 128 129 130 131 132
              )
            }
          }
        }
      }
    }
  }
133 134 135 136 137 138

  for (const name of Object.keys(layoutLock)) {
    if (!accounted.includes(name)) {
      throw new Error(`contract ${name} is not accounted for`)
    }
  }
139
})