contract-storage.ts 9.32 KB
Newer Older
Kelvin Fichter's avatar
Kelvin Fichter committed
1
import bre, { ethers } from '@nomiclabs/buidler'
Kelvin Fichter's avatar
Kelvin Fichter committed
2 3
import { Contract, BigNumber, ContractFactory } from 'ethers'
import { keccak256, defaultAbiCoder } from 'ethers/lib/utils'
Kelvin Fichter's avatar
Kelvin Fichter committed
4 5

import { remove0x } from '../byte-utils'
Kelvin Fichter's avatar
Kelvin Fichter committed
6
import { readArtifact } from '@nomiclabs/buidler/internal/artifacts'
Kelvin Fichter's avatar
Kelvin Fichter committed
7

Kelvin Fichter's avatar
Kelvin Fichter committed
8
const getFlattenedKeys = (depth: number, value: any): string[] => {
Kelvin Fichter's avatar
Kelvin Fichter committed
9 10 11 12 13 14
  if (depth === 0) {
    return []
  }

  let keys = Object.keys(value)
  if (depth > 1) {
Kelvin Fichter's avatar
Kelvin Fichter committed
15
    keys = keys.concat(getFlattenedKeys(depth - 1, Object.values(value)[0]))
Kelvin Fichter's avatar
Kelvin Fichter committed
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
  }

  return keys
}

const toHexString32 = (
  value: string | number | BigNumber | boolean
): string => {
  if (typeof value === 'string') {
    return '0x' + remove0x(value).padStart(64, '0').toLowerCase()
  } else if (typeof value === 'boolean') {
    return toHexString32(value ? 1 : 0)
  } else {
    return toHexString32(BigNumber.from(value).toHexString())
  }
}

Kelvin Fichter's avatar
Kelvin Fichter committed
33
const getFlattenedValues = (depth: number, value: any): any[] => {
Kelvin Fichter's avatar
Kelvin Fichter committed
34
  if (depth > 0) {
Kelvin Fichter's avatar
Kelvin Fichter committed
35
    return getFlattenedValues(depth - 1, Object.values(value)[0])
Kelvin Fichter's avatar
Kelvin Fichter committed
36 37 38 39 40 41
  }

  if (typeof value === 'object' && value !== null) {
    return Object.keys(value).map((key) => {
      return {
        label: key,
Kelvin Fichter's avatar
Kelvin Fichter committed
42
        value: toHexString32(value[key]),
Kelvin Fichter's avatar
Kelvin Fichter committed
43 44 45
      }
    })
  } else {
Kelvin Fichter's avatar
Kelvin Fichter committed
46 47 48 49 50 51
    return [
      {
        label: 'default',
        value: toHexString32(value),
      },
    ]
Kelvin Fichter's avatar
Kelvin Fichter committed
52 53 54 55 56 57 58
  }
}

const getStorageSlotHash = (
  slot: number,
  depth: number,
  value: any
Kelvin Fichter's avatar
Kelvin Fichter committed
59
): string => {
Kelvin Fichter's avatar
Kelvin Fichter committed
60 61 62 63 64 65
  let keys = []
  if (typeof value === 'object' && value !== null) {
    keys = getFlattenedKeys(depth, value)
  }

  if (keys.length === 0) {
Kelvin Fichter's avatar
Kelvin Fichter committed
66
    return defaultAbiCoder.encode(['uint256'], [slot])
Kelvin Fichter's avatar
Kelvin Fichter committed
67 68 69
  } else {
    let slotHash = toHexString32(slot)
    for (const key of keys) {
Kelvin Fichter's avatar
Kelvin Fichter committed
70
      slotHash = keccak256(toHexString32(key) + remove0x(slotHash))
Kelvin Fichter's avatar
Kelvin Fichter committed
71 72 73 74 75
    }
    return slotHash
  }
}

Kelvin Fichter's avatar
Kelvin Fichter committed
76
const parseInputSlots = (layout: any, inputTypeName: string): any[] => {
Kelvin Fichter's avatar
Kelvin Fichter committed
77 78 79 80 81 82 83 84 85
  const inputType = layout.types[inputTypeName]

  if (inputType.encoding === 'mapping') {
    return parseInputSlots(layout, inputType.value)
  } else if (inputType.encoding === 'inplace') {
    if (inputType.members) {
      return inputType.members.map((member: any) => {
        return {
          label: member.label,
Kelvin Fichter's avatar
Kelvin Fichter committed
86
          slot: member.slot,
Kelvin Fichter's avatar
Kelvin Fichter committed
87 88 89
        }
      })
    } else {
Kelvin Fichter's avatar
Kelvin Fichter committed
90 91 92 93 94 95
      return [
        {
          label: 'default',
          slot: 0,
        },
      ]
Kelvin Fichter's avatar
Kelvin Fichter committed
96 97 98 99 100 101 102 103 104 105
    }
  } else {
    throw new Error('Encoding type not supported.')
  }
}

export const getModifiableStorageFactory = async (
  name: string
): Promise<ContractFactory> => {
  const contractFactory = await ethers.getContractFactory(name)
Kelvin Fichter's avatar
Kelvin Fichter committed
106 107 108
  const proxyFactory = await ethers.getContractFactory(
    'Helper_ModifiableStorage'
  )
Kelvin Fichter's avatar
Kelvin Fichter committed
109 110 111 112

  const originalDeploy = contractFactory.deploy.bind(contractFactory)
  contractFactory.deploy = async (...args: any[]): Promise<Contract> => {
    const originalDefinePropertyFn = Object.defineProperty
Kelvin Fichter's avatar
Kelvin Fichter committed
113 114 115 116 117
    Object.defineProperty = (
      object: any,
      propName: string,
      props: any
    ): void => {
Kelvin Fichter's avatar
Kelvin Fichter committed
118 119 120
      if (props.writable === false) {
        props.writable = true
      }
Kelvin Fichter's avatar
Kelvin Fichter committed
121 122

      originalDefinePropertyFn(object, propName, props)
Kelvin Fichter's avatar
Kelvin Fichter committed
123 124 125 126 127 128 129 130 131 132 133
    }

    const contract = await originalDeploy(...args)
    const proxy = await proxyFactory.deploy(contract.address)
    ;(contract as any).address = proxy.address
    ;(contract as any).resolvedAddress = proxy.address
    ;(contract as any).__setStorageSlot = proxy.__setStorageSlot.bind(proxy)
    ;(contract as any).__getStorageSlot = proxy.__getStorageSlot.bind(proxy)
    ;(contract as any).__setContractStorage = async (value: any) => {
      await setContractStorage(
        contract,
Kelvin Fichter's avatar
Kelvin Fichter committed
134 135
        ((await readArtifact(bre.config.paths.artifacts, name)) as any)
          .storageLayout,
Kelvin Fichter's avatar
Kelvin Fichter committed
136 137 138 139 140 141
        value
      )
    }
    ;(contract as any).__checkContractStorage = async (value: any) => {
      await checkContractStorage(
        contract,
Kelvin Fichter's avatar
Kelvin Fichter committed
142 143
        ((await readArtifact(bre.config.paths.artifacts, name)) as any)
          .storageLayout,
Kelvin Fichter's avatar
Kelvin Fichter committed
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        value
      )
    }

    Object.defineProperty = originalDefinePropertyFn
    return contract
  }

  return contractFactory
}

export const setContractStorage = async (
  contract: Contract,
  layout: any,
  storage: any
): Promise<void> => {
  storage = storage || {}

  for (const [key, value] of Object.entries(storage)) {
Kelvin Fichter's avatar
Kelvin Fichter committed
163 164
    const layoutMap = layout.storage.find((lmap: any) => {
      return lmap.label === key
Kelvin Fichter's avatar
Kelvin Fichter committed
165 166 167
    })
    const inputSlots = parseInputSlots(layout, layoutMap.type)

Kelvin Fichter's avatar
Kelvin Fichter committed
168
    const slot = parseInt(layoutMap.slot, 10)
169
    let depth = (layoutMap.type.match(/t_mapping/g) || []).length
Kelvin Fichter's avatar
Kelvin Fichter committed
170 171 172

    if (typeof value !== 'object') {
      const slotHash = getStorageSlotHash(slot, depth, value)
Kelvin Fichter's avatar
Kelvin Fichter committed
173
      await contract.__setStorageSlot(slotHash, toHexString32(value as string))
Kelvin Fichter's avatar
Kelvin Fichter committed
174
    } else {
175 176 177 178 179 180
      if (key === 'contractStorage' || key === 'verifiedContractStorage') {
        for (const [subKey1, subValue1] of Object.entries(value)) {
          for (const [subKey, subValue] of Object.entries(subValue1)) {
            const baseSlotHash = getStorageSlotHash(slot, depth, {
              [subKey1]: {
                [subKey]: subValue,
Kelvin Fichter's avatar
Kelvin Fichter committed
181
              },
182 183 184 185
            })
            const slotValues = getFlattenedValues(depth, {
              [subKey1]: {
                [subKey]: subValue,
Kelvin Fichter's avatar
Kelvin Fichter committed
186
              },
187
            })
Kelvin Fichter's avatar
Kelvin Fichter committed
188

189 190 191 192 193 194 195
            for (const slotValue of slotValues) {
              const slotIndex = inputSlots.find((inputSlot) => {
                return inputSlot.label === slotValue.label
              }).slot
              const slotHash = toHexString32(
                BigNumber.from(baseSlotHash).add(slotIndex)
              )
Kelvin Fichter's avatar
Kelvin Fichter committed
196

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
              await contract.__setStorageSlot(slotHash, slotValue.value)
            }
          }
        }
      } else {
        for (const [subKey, subValue] of Object.entries(value)) {
          const baseSlotHash = getStorageSlotHash(slot, depth, {
            [subKey]: subValue,
          })
          const slotValues = getFlattenedValues(depth, {
            [subKey]: subValue,
          })

          for (const slotValue of slotValues) {
            const slotIndex = inputSlots.find((inputSlot) => {
              return inputSlot.label === slotValue.label
            }).slot
            const slotHash = toHexString32(
              BigNumber.from(baseSlotHash).add(slotIndex)
            )

            await contract.__setStorageSlot(slotHash, slotValue.value)
          }
Kelvin Fichter's avatar
Kelvin Fichter committed
220 221 222 223 224 225 226 227 228 229 230 231 232 233
        }
      }
    }
  }
}

export const checkContractStorage = async (
  contract: Contract,
  layout: any,
  storage: any
): Promise<void> => {
  storage = storage || {}

  for (const [key, value] of Object.entries(storage)) {
Kelvin Fichter's avatar
Kelvin Fichter committed
234 235
    const layoutMap = layout.storage.find((lmap: any) => {
      return lmap.label === key
Kelvin Fichter's avatar
Kelvin Fichter committed
236 237 238
    })
    const inputSlots = parseInputSlots(layout, layoutMap.type)

Kelvin Fichter's avatar
Kelvin Fichter committed
239
    const slot = parseInt(layoutMap.slot, 10)
Kelvin Fichter's avatar
Kelvin Fichter committed
240 241 242 243
    const depth = (layoutMap.type.match(/t_mapping/g) || []).length

    if (typeof value !== 'object') {
      const slotHash = getStorageSlotHash(slot, depth, value)
Kelvin Fichter's avatar
Kelvin Fichter committed
244
      const retSlotValue = await contract.__getStorageSlot(slotHash)
Kelvin Fichter's avatar
Kelvin Fichter committed
245 246

      if (retSlotValue !== toHexString32(value as string)) {
Kelvin Fichter's avatar
Kelvin Fichter committed
247 248 249 250 251
        throw new Error(
          `Resulting state of ${key} (${retSlotValue}) did not match expected state (${toHexString32(
            value as string
          )})`
        )
Kelvin Fichter's avatar
Kelvin Fichter committed
252 253
      }
    } else {
254 255 256 257 258 259
      if (key === 'contractStorage' || key === 'verifiedContractStorage') {
        for (const [subKey1, subValue1] of Object.entries(value)) {
          for (const [subKey, subValue] of Object.entries(subValue1)) {
            const baseSlotHash = getStorageSlotHash(slot, depth, {
              [subKey1]: {
                [subKey]: subValue,
Kelvin Fichter's avatar
Kelvin Fichter committed
260
              },
261 262 263 264
            })
            const slotValues = getFlattenedValues(depth, {
              [subKey1]: {
                [subKey]: subValue,
Kelvin Fichter's avatar
Kelvin Fichter committed
265
              },
266
            })
Kelvin Fichter's avatar
Kelvin Fichter committed
267

268 269 270 271 272 273 274 275 276
            for (const slotValue of slotValues) {
              const slotIndex = inputSlots.find((inputSlot) => {
                return inputSlot.label === slotValue.label
              }).slot
              const slotHash = toHexString32(
                BigNumber.from(baseSlotHash).add(slotIndex)
              )

              const retSlotValue = await contract.__getStorageSlot(slotHash)
Kelvin Fichter's avatar
Kelvin Fichter committed
277

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
              if (retSlotValue !== slotValue.value) {
                throw new Error(
                  `Resulting state of ${slotValue.label} (${retSlotValue}) did not match expected state (${slotValue.value}).`
                )
              }
            }
          }
        }
      } else {
        for (const [subKey, subValue] of Object.entries(value)) {
          const baseSlotHash = getStorageSlotHash(slot, depth, {
            [subKey]: subValue,
          })
          const slotValues = getFlattenedValues(depth, {
            [subKey]: subValue,
          })

          for (const slotValue of slotValues) {
            const slotIndex = inputSlots.find((inputSlot) => {
              return inputSlot.label === slotValue.label
            }).slot
            const slotHash = toHexString32(
              BigNumber.from(baseSlotHash).add(slotIndex)
Kelvin Fichter's avatar
Kelvin Fichter committed
301
            )
302 303 304 305 306 307 308 309

            const retSlotValue = await contract.__getStorageSlot(slotHash)

            if (retSlotValue !== slotValue.value) {
              throw new Error(
                `Resulting state of ${slotValue.label} (${retSlotValue}) did not match expected state (${slotValue.value}).`
              )
            }
Kelvin Fichter's avatar
Kelvin Fichter committed
310 311 312 313 314 315
          }
        }
      }
    }
  }
}