From e0fd74a71e8ca5246f5b881f87e02f2dcc09e3c5 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 30 Apr 2021 18:26:46 -0400 Subject: [PATCH 1/8] wip: start storage slot parsing lib for chugsplash --- .../test-helpers/Helper_StorageHelper.sol | 39 +++ packages/contracts/src/chugsplash/index.ts | 1 + packages/contracts/src/chugsplash/storage.ts | 279 ++++++++++++++++++ packages/contracts/src/index.ts | 1 + .../contracts/test/chugsplash/storage.spec.ts | 218 ++++++++++++++ 5 files changed, 538 insertions(+) create mode 100644 packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol create mode 100644 packages/contracts/src/chugsplash/index.ts create mode 100644 packages/contracts/src/chugsplash/storage.ts create mode 100644 packages/contracts/test/chugsplash/storage.spec.ts diff --git a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol new file mode 100644 index 0000000000000..192d76c91e59f --- /dev/null +++ b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; + +contract Helper_StorageHelper { + struct BasicStruct { + uint256 _structUint256; + address _structAddress; + bytes32 _structBytes32; + } + + uint8 _uint8; // slot 0 + bytes32 _spacer1; // to avoid slot packing, unused + + uint64 _uint64; // slot 2 + bytes32 _spacer2; // to avoid slot packing, unused + + uint256 _uint256; // slot 4 + bytes32 _spacer3; // to avoid slot packing, unused + + bytes1 _bytes1; // slot 6 + bytes32 _spacer4; // to avoid slot packing, unused + + bytes8 _bytes8; // slot 8 + bytes32 _spacer5; // to avoid slot packing, unused + + bytes32 _bytes32; // slot 10 + bytes32 _spacer6; // to avoid slot packing, unused + + bool _bool; // slot 12 + bytes32 _spacer7; // to avoid slot packing, unused + + address _address; // slot 14 + bytes32 _spacer8; // to avoid slot packing, unused + + bytes _bytes; // slot 16 + string _string; // slot 17 + + BasicStruct _struct; // slot 18,19,20 +} diff --git a/packages/contracts/src/chugsplash/index.ts b/packages/contracts/src/chugsplash/index.ts new file mode 100644 index 0000000000000..69b61ecc471a0 --- /dev/null +++ b/packages/contracts/src/chugsplash/index.ts @@ -0,0 +1 @@ +export * from './storage' diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts new file mode 100644 index 0000000000000..de0bdf101f3e9 --- /dev/null +++ b/packages/contracts/src/chugsplash/storage.ts @@ -0,0 +1,279 @@ +/* External Imports */ +import { fromHexString, remove0x } from '@eth-optimism/core-utils' +import { BigNumber, ethers } from 'ethers' + +// Represents the JSON objects outputted by the Solidity compiler that describe the structure of +// state within the contract. See +// https://docs.soliditylang.org/en/v0.8.3/internals/layout_in_storage.html for more information. +interface SolidityStorageObj { + astId: number + contract: string + label: string + offset: number + slot: number + type: string +} + +// Represents the JSON objects outputted by the Solidity compiler that describe the types used for +// the various pieces of state in the contract. See +// https://docs.soliditylang.org/en/v0.8.3/internals/layout_in_storage.html for more information. +interface SolidityStorageType { + encoding: 'inplace' | 'mapping' | 'dynamic_array' | 'bytes' + label: string + numberOfBytes: number + key?: string + value?: string + base?: string + members?: SolidityStorageObj[] +} + +// Container object returned by the Solidity compiler. See +// https://docs.soliditylang.org/en/v0.8.3/internals/layout_in_storage.html for more information. +export interface SolidityStorageLayout { + storage: SolidityStorageObj[] + types: { + [name: string]: SolidityStorageType + } +} + +interface StorageSlotPair { + key: string + val: string +} + +export const getStorageLayout = async ( + hre: any, //HardhatRuntimeEnvironment, + name: string +): Promise => { + const { sourceName, contractName } = hre.artifacts.readArtifactSync(name) + const buildInfo = await hre.artifacts.getBuildInfo( + `${sourceName}:${contractName}` + ) + const output = buildInfo.output.contracts[sourceName][contractName] + + if (!('storageLayout' in output)) { + throw new Error( + `Storage layout for ${name} not found. Did you forget to set the storage layout compiler option in your hardhat config? Read more: https://github.com/ethereum-optimism/smock#note-on-using-smoddit` + ) + } + + return (output as any).storageLayout +} + +/** + * Encodes a single variable as a series of key/value storage slot pairs using some storage layout + * as instructions for how to perform this encoding. Works recursively with struct types. + * @param variable Variable to encode as key/value slot pairs. + * @param storageObj Solidity compiler JSON output describing the layout for this + * @param storageTypes Full list of storage types allowed for this encoding. + * @param nestedSlotOffset For nested data structures, keeps track of a value to be added onto the + * keys for nested values. + * @returns Variable encoded as a series of key/value slot pairs. + */ +const encodeVariable = ( + variable: any, + storageObj: SolidityStorageObj, + storageTypes: { + [name: string]: SolidityStorageType + }, + nestedSlotOffset = 0 +): Array => { + const variableType = storageTypes[storageObj.type] + + // Slot key will be the same no matter what so we can just compute it here. + const slotKey = + '0x' + + remove0x( + BigNumber.from( + parseInt(storageObj.slot as any, 10) + nestedSlotOffset + ).toHexString() + ) + .padStart(64 - storageObj.offset * 2, '0') + .padEnd(64, '0') + + if (variableType.encoding === 'inplace') { + if ( + variableType.label === 'address' || + variableType.label.startsWith('contract') + ) { + if (!ethers.utils.isAddress(variable)) { + throw new Error(`invalid address type: ${variable}`) + } + + // Addresses are right-aligned. + const slotVal = + '0x' + + remove0x(variable) + .padStart(64, '0') + .toLowerCase() + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label === 'bool') { + // Do some light parsing here to make sure "true" and "false" are recognized. + if (typeof variable === 'string') { + if (variable === 'false') { + variable = false + } + if (variable === 'true') { + variable = true + } + } + + if (typeof variable !== 'boolean') { + throw new Error(`invalid bool type: ${variable}`) + } + + // Booleans are right-aligned and represented as 0 or 1. + const slotVal = '0x' + (variable ? '1' : '0').padStart(64, '0') + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label.startsWith('bytes')) { + if (!ethers.utils.isHexString(variable, variableType.numberOfBytes)) { + throw new Error(`invalid bytesN type`) + } + + // BytesN are **left** aligned (eyeroll). + const slotVal = + '0x' + + remove0x(variable) + .padEnd(64, '0') + .toLowerCase() + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label.startsWith('uint')) { + if ( + remove0x(BigNumber.from(variable).toHexString()).length / 2 > + variableType.numberOfBytes + ) { + throw new Error( + `provided ${variableType.label} is too big: ${variable}` + ) + } + + // Uints are right aligned. + const slotVal = + '0x' + + remove0x(BigNumber.from(variable).toHexString()) + .padStart(64, '0') + .toLowerCase() + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label.startsWith('struct')) { + // Structs are encoded recursively, as defined by their `members` field. + let slots = [] + for (const [varName, varVal] of Object.entries(variable)) { + slots = slots.concat( + encodeVariable( + varVal, + variableType.members.find((member) => { + return member.label === varName + }), + storageTypes, + nestedSlotOffset + parseInt(storageObj.slot as any, 10) + ) + ) + } + return slots + } + } else if (variableType.encoding === 'bytes') { + if (storageObj.offset !== 0) { + throw new Error(`offset not supported for string/bytes types`) + } + + // ref: https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string + const bytes = + storageObj.type === 'string' + ? ethers.utils.toUtf8Bytes(variable) + : fromHexString(variable) + if (bytes.length < 32) { + const slotVal = ethers.utils.hexlify( + ethers.utils.concat([ + ethers.utils.concat([bytes, ethers.constants.HashZero]).slice(0, 31), + ethers.BigNumber.from(bytes.length * 2).toHexString(), + ]) + ) + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else { + throw new Error('large strings (>31 bytes) not supported') + } + } else if (variableType.encoding === 'mapping') { + throw new Error('mapping types not yet supported') + } else if (variableType.encoding === 'dynamic_array') { + throw new Error('array types not yet supported') + } else { + throw new Error( + `unknown unsupported type ${variableType.encoding} ${variableType.label}` + ) + } +} + +/** + * Computes the key/value storage slot pairs that would be used if a given set of variable values + * were applied to a given contract. + * @param storageLayout Solidity storage layout to use as a template for determining storage slots. + * @param variables Variable values to apply against the given storage layout. + * @returns An array of key/value storage slot pairs that would result in the desired state. + */ +export const computeStorageSlots = ( + storageLayout: SolidityStorageLayout, + variables: any = {} +): Array => { + let slots: StorageSlotPair[] = [] + for (const [variableName, variableValue] of Object.entries(variables)) { + // Find the entry in the storage layout that corresponds to this variable name. + const storageObj = storageLayout.storage.find((entry) => { + return entry.label === variableName + }) + + // Complain very loudly if attempting to set a variable that doesn't exist within this layout. + if (!storageObj) { + throw new Error( + `variable name not found in storage layout: ${variableName}` + ) + } + + // Encode this variable as series of storage slot key/value pairs and save it. + slots = slots.concat( + encodeVariable(variableValue, storageObj, storageLayout.types) + ) + } + + // TODO: Deal with packed storage slots. + + const seen = {} + for (const slot of slots) { + if (seen[slot.key]) { + throw new Error(`packed storage slots not supported`) + } else { + seen[slot.key] = true + } + } + + return slots +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index ba9a1959a5686..71c73b7258268 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -2,3 +2,4 @@ export * from './contract-defs' export { getLatestStateDump, StateDump } from './contract-dumps' export * from './contract-deployment' export * from './predeploys' +export * from './chugsplash' diff --git a/packages/contracts/test/chugsplash/storage.spec.ts b/packages/contracts/test/chugsplash/storage.spec.ts new file mode 100644 index 0000000000000..e93af048be26a --- /dev/null +++ b/packages/contracts/test/chugsplash/storage.spec.ts @@ -0,0 +1,218 @@ +import { expect } from '../setup' + +import hre from 'hardhat' +import { + computeStorageSlots, + getStorageLayout, + SolidityStorageLayout, +} from '../../src' + +describe('ChugSplash storage layout parsing', () => { + let layout: SolidityStorageLayout + before(async () => { + layout = await getStorageLayout(hre, 'Helper_StorageHelper') + }) + + describe('computeStorageSlots', () => { + it('compute slots for uint8', () => { + expect( + computeStorageSlots(layout, { + _uint8: 123, + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000000', + val: + '0x000000000000000000000000000000000000000000000000000000000000007b', + }, + ]) + }) + + it('compute slots for uint64', () => { + expect( + computeStorageSlots(layout, { + _uint64: 1234, + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000002', + val: + '0x00000000000000000000000000000000000000000000000000000000000004d2', + }, + ]) + }) + + it('compute slots for uint256', () => { + expect( + computeStorageSlots(layout, { + _uint256: 12345, + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000004', + val: + '0x0000000000000000000000000000000000000000000000000000000000003039', + }, + ]) + }) + + it('compute slots for bytes1', () => { + expect( + computeStorageSlots(layout, { + _bytes1: '0x11', + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000006', + val: + '0x1100000000000000000000000000000000000000000000000000000000000000', + }, + ]) + }) + + it('compute slots for bytes8', () => { + expect( + computeStorageSlots(layout, { + _bytes8: '0x1212121212121212', + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000008', + val: + '0x1212121212121212000000000000000000000000000000000000000000000000', + }, + ]) + }) + + it('compute slots for bytes32', () => { + expect( + computeStorageSlots(layout, { + _bytes32: + '0x2222222222222222222222222222222222222222222222222222222222222222', + }) + ).to.deep.equal([ + { + key: + '0x000000000000000000000000000000000000000000000000000000000000000a', + val: + '0x2222222222222222222222222222222222222222222222222222222222222222', + }, + ]) + }) + + it('compute slots for bool', () => { + expect( + computeStorageSlots(layout, { + _bool: true, + }) + ).to.deep.equal([ + { + key: + '0x000000000000000000000000000000000000000000000000000000000000000c', + val: + '0x0000000000000000000000000000000000000000000000000000000000000001', + }, + ]) + }) + + it('compute slots for address', () => { + expect( + computeStorageSlots(layout, { + _address: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + }) + ).to.deep.equal([ + { + key: + '0x000000000000000000000000000000000000000000000000000000000000000e', + val: + '0x0000000000000000000000005a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', + }, + ]) + }) + + it('compute slots for bytes (<32 bytes long)', () => { + expect( + computeStorageSlots(layout, { + _bytes: + '0x12121212121212121212121212121212121212121212121212121212121212', // only 31 bytes + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000010', + val: + '0x121212121212121212121212121212121212121212121212121212121212123e', // last byte contains byte length * 2 + }, + ]) + }) + + it('compute slots for string (<32 bytes long)', () => { + expect( + computeStorageSlots(layout, { + _string: 'hello i am a string', // 19 bytes + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000011', + val: + '0x68656c6c6f206920616d206120737472696e6700000000000000000000000026', // 19 * 2 = 38 = 0x26 + }, + ]) + }) + + it('compute slots for a simple (complete) struct', () => { + expect( + computeStorageSlots(layout, { + _struct: { + _structUint256: 1234, + _structAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + _structBytes32: + '0x1212121212121212121212121212121212121212121212121212121212121212', + }, + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000012', + val: + '0x00000000000000000000000000000000000000000000000000000000000004d2', + }, + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000013', + val: + '0x0000000000000000000000005a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', + }, + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000014', + val: + '0x1212121212121212121212121212121212121212121212121212121212121212', + }, + ]) + }) + + it('compute slots for a simple (partial) struct', () => { + expect( + computeStorageSlots(layout, { + _struct: { + _structAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + }, + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000013', + val: + '0x0000000000000000000000005a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', + }, + ]) + }) + }) +}) From f9298525fd572dd6023fbf5679410d65b2324bc7 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 30 Apr 2021 19:18:54 -0400 Subject: [PATCH 2/8] add support for packed storage slots and do bytesN correctly --- .../test-helpers/Helper_StorageHelper.sol | 10 +++ packages/contracts/src/chugsplash/storage.ts | 64 +++++++++++++++---- .../contracts/test/chugsplash/storage.spec.ts | 38 ++++++++++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol index 192d76c91e59f..63c7c4ea4ac97 100644 --- a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol +++ b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol @@ -36,4 +36,14 @@ contract Helper_StorageHelper { string _string; // slot 17 BasicStruct _struct; // slot 18,19,20 + + // Pack into (bytes11,bool,address) + address _packedAddress; // slot 21 + bool _packedBool; // slot 21 + bytes11 _packedBytes11; // slot 21 + + // Pack into (address,bool,bytes11) + bytes11 _otherPackedBytes11; // slot 22 + bool _otherPackedBool; // slot 22 + address _otherPackedAddress; // slot 22 } diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts index de0bdf101f3e9..f9d6f6de790da 100644 --- a/packages/contracts/src/chugsplash/storage.ts +++ b/packages/contracts/src/chugsplash/storage.ts @@ -87,9 +87,7 @@ const encodeVariable = ( BigNumber.from( parseInt(storageObj.slot as any, 10) + nestedSlotOffset ).toHexString() - ) - .padStart(64 - storageObj.offset * 2, '0') - .padEnd(64, '0') + ).padStart(64, '0') if (variableType.encoding === 'inplace') { if ( @@ -104,7 +102,8 @@ const encodeVariable = ( const slotVal = '0x' + remove0x(variable) - .padStart(64, '0') + .padStart(64 - storageObj.offset * 2, '0') + .padEnd(64, '0') .toLowerCase() return [ @@ -129,7 +128,11 @@ const encodeVariable = ( } // Booleans are right-aligned and represented as 0 or 1. - const slotVal = '0x' + (variable ? '1' : '0').padStart(64, '0') + const slotVal = + '0x' + + (variable ? '1' : '0') + .padStart(64 - storageObj.offset * 2, '0') + .padEnd(64, '0') return [ { @@ -142,10 +145,11 @@ const encodeVariable = ( throw new Error(`invalid bytesN type`) } - // BytesN are **left** aligned (eyeroll). const slotVal = '0x' + remove0x(variable) + .padEnd(variableType.numberOfBytes * 2, '0') + .padStart(64 - storageObj.offset * 2, '0') .padEnd(64, '0') .toLowerCase() @@ -169,7 +173,8 @@ const encodeVariable = ( const slotVal = '0x' + remove0x(BigNumber.from(variable).toHexString()) - .padStart(64, '0') + .padStart(64 - storageObj.offset * 2, '0') + .padEnd(64, '0') .toLowerCase() return [ @@ -264,16 +269,47 @@ export const computeStorageSlots = ( ) } - // TODO: Deal with packed storage slots. + slots = slots.reduce((slots, slot) => { + const prevSlot = slots.find((prevSlot) => { + return prevSlot.key === slot.key + }) + + if (prevSlot !== undefined) { + slots = slots.filter((slot) => { + return slot.key !== prevSlot.key + }) - const seen = {} - for (const slot of slots) { - if (seen[slot.key]) { - throw new Error(`packed storage slots not supported`) + const valA = remove0x(slot.val) + const valB = remove0x(prevSlot.val) + + let val = '0x' + for (let i = 0; i < 64; i += 2) { + const byteA = valA.slice(i, i + 2) + const byteB = valB.slice(i, i + 2) + + if (byteA === '00' && byteB === '00') { + val += '00' + } else if (byteA === '00' && byteB !== '00') { + val += byteB + } else if (byteA !== '00' && byteB === '00') { + val += byteA + } else { + throw new Error( + 'detected badly encoded packed string, should not happen' + ) + } + } + + slots.push({ + key: slot.key, + val: val, + }) } else { - seen[slot.key] = true + slots.push(slot) } - } + + return slots + }, []) return slots } diff --git a/packages/contracts/test/chugsplash/storage.spec.ts b/packages/contracts/test/chugsplash/storage.spec.ts index e93af048be26a..fdf32e255b9da 100644 --- a/packages/contracts/test/chugsplash/storage.spec.ts +++ b/packages/contracts/test/chugsplash/storage.spec.ts @@ -69,7 +69,7 @@ describe('ChugSplash storage layout parsing', () => { key: '0x0000000000000000000000000000000000000000000000000000000000000006', val: - '0x1100000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000011', }, ]) }) @@ -84,7 +84,7 @@ describe('ChugSplash storage layout parsing', () => { key: '0x0000000000000000000000000000000000000000000000000000000000000008', val: - '0x1212121212121212000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000001212121212121212', }, ]) }) @@ -214,5 +214,39 @@ describe('ChugSplash storage layout parsing', () => { }, ]) }) + + it('compute slots for packed variables', () => { + expect( + computeStorageSlots(layout, { + _packedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + _packedBool: true, + _packedBytes11: '0x1212121212121212121212', + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000015', + val: + '0x1212121212121212121212015a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', + }, + ]) + }) + + it('compute slots for packed variables (2)', () => { + expect( + computeStorageSlots(layout, { + _otherPackedBytes11: '0x1212121212121212121212', + _otherPackedBool: true, + _otherPackedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + }) + ).to.deep.equal([ + { + key: + '0x0000000000000000000000000000000000000000000000000000000000000016', + val: + '0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c011212121212121212121212', + }, + ]) + }) }) }) From 09add4420797d0672011acf300b7e8431cf7b106 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 30 Apr 2021 19:20:25 -0400 Subject: [PATCH 3/8] fix lint errors --- packages/contracts/src/chugsplash/storage.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts index f9d6f6de790da..1497f270a5a06 100644 --- a/packages/contracts/src/chugsplash/storage.ts +++ b/packages/contracts/src/chugsplash/storage.ts @@ -269,14 +269,14 @@ export const computeStorageSlots = ( ) } - slots = slots.reduce((slots, slot) => { - const prevSlot = slots.find((prevSlot) => { - return prevSlot.key === slot.key + slots = slots.reduce((prevSlots, slot) => { + const prevSlot = prevSlots.find((otherSlot) => { + return otherSlot.key === slot.key }) if (prevSlot !== undefined) { - slots = slots.filter((slot) => { - return slot.key !== prevSlot.key + prevSlots = prevSlots.filter((otherSlot) => { + return otherSlot.key !== prevSlot.key }) const valA = remove0x(slot.val) @@ -300,15 +300,15 @@ export const computeStorageSlots = ( } } - slots.push({ + prevSlots.push({ key: slot.key, - val: val, + val, }) } else { - slots.push(slot) + prevSlots.push(slot) } - return slots + return prevSlots }, []) return slots From d3fe5128552469ba346d8ebc082b3f74b9f9364e Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 30 Apr 2021 19:32:15 -0400 Subject: [PATCH 4/8] Simplify some repeated logic --- packages/contracts/src/chugsplash/storage.ts | 61 +++++++++----------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts index 1497f270a5a06..194422797191c 100644 --- a/packages/contracts/src/chugsplash/storage.ts +++ b/packages/contracts/src/chugsplash/storage.ts @@ -41,6 +41,22 @@ interface StorageSlotPair { val: string } +/** + * Takes a slot value (in hex), left-pads it with zeros, and displaces it by a given offset. + * @param val Hex string value to pad. + * @param offset Number of bytes to offset from the right. + * @return Padded hex string. + */ +const padHexSlotValue = (val: string, offset: number): string => { + return ( + '0x' + + remove0x(val) + .padStart(64 - offset * 2, '0') // Pad the start with 64 - offset zero bytes. + .padEnd(64, '0') // Pad the end (up to 64 bytes) with zero bytes. + .toLowerCase() // Making this lower case makes assertions more consistent later. + ) +} + export const getStorageLayout = async ( hre: any, //HardhatRuntimeEnvironment, name: string @@ -98,18 +114,10 @@ const encodeVariable = ( throw new Error(`invalid address type: ${variable}`) } - // Addresses are right-aligned. - const slotVal = - '0x' + - remove0x(variable) - .padStart(64 - storageObj.offset * 2, '0') - .padEnd(64, '0') - .toLowerCase() - return [ { key: slotKey, - val: slotVal, + val: padHexSlotValue(variable, storageObj.offset), }, ] } else if (variableType.label === 'bool') { @@ -127,17 +135,10 @@ const encodeVariable = ( throw new Error(`invalid bool type: ${variable}`) } - // Booleans are right-aligned and represented as 0 or 1. - const slotVal = - '0x' + - (variable ? '1' : '0') - .padStart(64 - storageObj.offset * 2, '0') - .padEnd(64, '0') - return [ { key: slotKey, - val: slotVal, + val: padHexSlotValue(variable ? '1' : '0', storageObj.offset), }, ] } else if (variableType.label.startsWith('bytes')) { @@ -145,18 +146,13 @@ const encodeVariable = ( throw new Error(`invalid bytesN type`) } - const slotVal = - '0x' + - remove0x(variable) - .padEnd(variableType.numberOfBytes * 2, '0') - .padStart(64 - storageObj.offset * 2, '0') - .padEnd(64, '0') - .toLowerCase() - return [ { key: slotKey, - val: slotVal, + val: padHexSlotValue( + remove0x(variable).padEnd(variableType.numberOfBytes * 2, '0'), + storageObj.offset + ), }, ] } else if (variableType.label.startsWith('uint')) { @@ -169,18 +165,13 @@ const encodeVariable = ( ) } - // Uints are right aligned. - const slotVal = - '0x' + - remove0x(BigNumber.from(variable).toHexString()) - .padStart(64 - storageObj.offset * 2, '0') - .padEnd(64, '0') - .toLowerCase() - return [ { key: slotKey, - val: slotVal, + val: padHexSlotValue( + BigNumber.from(variable).toHexString(), + storageObj.offset + ), }, ] } else if (variableType.label.startsWith('struct')) { From ea95c3ad4eebb2dc8283f809219e05267dd7b898 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 30 Apr 2021 19:48:27 -0400 Subject: [PATCH 5/8] style: small improvements and comments --- packages/contracts/src/chugsplash/storage.ts | 64 ++++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts index 194422797191c..37551a8dde6fd 100644 --- a/packages/contracts/src/chugsplash/storage.ts +++ b/packages/contracts/src/chugsplash/storage.ts @@ -57,6 +57,13 @@ const padHexSlotValue = (val: string, offset: number): string => { ) } +/** + * Retrieves the storageLayout portion of the compiler artifact for a given contract by name. This + * function is hardhat specific. + * @param hre HardhatRuntimeEnvironment, required for the readArtifactSync function. + * @param name Name of the contract to retrieve the storage layout for. + * @return Storage layout object from the compiler output. + */ export const getStorageLayout = async ( hre: any, //HardhatRuntimeEnvironment, name: string @@ -79,6 +86,7 @@ export const getStorageLayout = async ( /** * Encodes a single variable as a series of key/value storage slot pairs using some storage layout * as instructions for how to perform this encoding. Works recursively with struct types. + * ref: https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#layout-of-state-variables-in-storage * @param variable Variable to encode as key/value slot pairs. * @param storageObj Solidity compiler JSON output describing the layout for this * @param storageTypes Full list of storage types allowed for this encoding. @@ -193,26 +201,32 @@ const encodeVariable = ( } } else if (variableType.encoding === 'bytes') { if (storageObj.offset !== 0) { - throw new Error(`offset not supported for string/bytes types`) + // string/bytes types are *not* packed by Solidity. + throw new Error(`got offset for string/bytes type, should never happen`) } - // ref: https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string + // `string` types are converted to utf8 bytes, `bytes` are left as-is (assuming 0x prefixed). const bytes = storageObj.type === 'string' ? ethers.utils.toUtf8Bytes(variable) : fromHexString(variable) - if (bytes.length < 32) { - const slotVal = ethers.utils.hexlify( - ethers.utils.concat([ - ethers.utils.concat([bytes, ethers.constants.HashZero]).slice(0, 31), - ethers.BigNumber.from(bytes.length * 2).toHexString(), - ]) - ) + // ref: https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string + if (bytes.length < 32) { + // NOTE: Solidity docs (see above) specifies that strings or bytes with a length of 31 bytes + // should be placed into a storage slot where the last byte of the storage slot is the length + // of the variable in bytes * 2. return [ { key: slotKey, - val: slotVal, + val: ethers.utils.hexlify( + ethers.utils.concat([ + ethers.utils + .concat([bytes, ethers.constants.HashZero]) + .slice(0, 31), + ethers.BigNumber.from(bytes.length * 2).toHexString(), + ]) + ), }, ] } else { @@ -260,43 +274,53 @@ export const computeStorageSlots = ( ) } + // Dealing with packed storage slots now. We know that a storage slot is packed when two storage + // slots produced by the above encoding have the same key. In this case, we want to merge the two + // values into a single bytes32 value. We'll throw an error if the two values overlap (have some + // byte where both values are non-zero). slots = slots.reduce((prevSlots, slot) => { + // Find some previous slot where we have the same key. const prevSlot = prevSlots.find((otherSlot) => { return otherSlot.key === slot.key }) - if (prevSlot !== undefined) { + if (prevSlot === undefined) { + // Slot doesn't share a key with any other slot so we can just push it and continue. + prevSlots.push(slot) + } else { + // Slot shares a key with some previous slot. + // First, we remove the previous slot from the list of slots since we'll be modifying it. prevSlots = prevSlots.filter((otherSlot) => { return otherSlot.key !== prevSlot.key }) + // Now we'll generate a merged value by taking the non-zero bytes from both values. There's + // probably a more efficient way to do this, but this is relatively easy and straightforward. + let mergedVal = '0x' const valA = remove0x(slot.val) const valB = remove0x(prevSlot.val) - - let val = '0x' for (let i = 0; i < 64; i += 2) { const byteA = valA.slice(i, i + 2) const byteB = valB.slice(i, i + 2) if (byteA === '00' && byteB === '00') { - val += '00' + mergedVal += '00' } else if (byteA === '00' && byteB !== '00') { - val += byteB + mergedVal += byteB } else if (byteA !== '00' && byteB === '00') { - val += byteA + mergedVal += byteA } else { + // Should never happen, means our encoding is broken. Values should *never* overlap. throw new Error( - 'detected badly encoded packed string, should not happen' + 'detected badly encoded packed value, should not happen' ) } } prevSlots.push({ key: slot.key, - val, + val: mergedVal, }) - } else { - prevSlots.push(slot) } return prevSlots From 98f83f90692ef864ea319ce89fb42ec78932de07 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 3 May 2021 17:27:41 -0400 Subject: [PATCH 6/8] Update testing for storage library --- .../test-helpers/Helper_StorageHelper.sol | 61 ++-- .../contracts/test/chugsplash/storage.spec.ts | 299 ++++++------------ 2 files changed, 127 insertions(+), 233 deletions(-) diff --git a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol index 63c7c4ea4ac97..fd5541ab5ee50 100644 --- a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol +++ b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol @@ -2,48 +2,59 @@ pragma solidity >0.5.0 <0.8.0; contract Helper_StorageHelper { + function setStorage( + bytes32 _key, + bytes32 _val + ) + public + { + assembly { + sstore(_key, _val) + } + } + struct BasicStruct { uint256 _structUint256; address _structAddress; bytes32 _structBytes32; } - uint8 _uint8; // slot 0 - bytes32 _spacer1; // to avoid slot packing, unused + uint8 public _uint8; // slot 0 + bytes32 _spacer1; // to avoid slot packing, unused - uint64 _uint64; // slot 2 - bytes32 _spacer2; // to avoid slot packing, unused + uint64 public _uint64; // slot 2 + bytes32 _spacer2; // to avoid slot packing, unused - uint256 _uint256; // slot 4 - bytes32 _spacer3; // to avoid slot packing, unused + uint256 public _uint256; // slot 4 + bytes32 _spacer3; // to avoid slot packing, unused - bytes1 _bytes1; // slot 6 - bytes32 _spacer4; // to avoid slot packing, unused + bytes1 public _bytes1; // slot 6 + bytes32 _spacer4; // to avoid slot packing, unused - bytes8 _bytes8; // slot 8 - bytes32 _spacer5; // to avoid slot packing, unused + bytes8 public _bytes8; // slot 8 + bytes32 _spacer5; // to avoid slot packing, unused - bytes32 _bytes32; // slot 10 - bytes32 _spacer6; // to avoid slot packing, unused + bytes32 public _bytes32; // slot 10 + bytes32 _spacer6; // to avoid slot packing, unused - bool _bool; // slot 12 - bytes32 _spacer7; // to avoid slot packing, unused + bool public _bool; // slot 12 + bytes32 _spacer7; // to avoid slot packing, unused - address _address; // slot 14 - bytes32 _spacer8; // to avoid slot packing, unused + address public _address; // slot 14 + bytes32 _spacer8; // to avoid slot packing, unused - bytes _bytes; // slot 16 - string _string; // slot 17 + bytes public _bytes; // slot 16 + string public _string; // slot 17 - BasicStruct _struct; // slot 18,19,20 + BasicStruct public _struct; // slot 18,19,20 // Pack into (bytes11,bool,address) - address _packedAddress; // slot 21 - bool _packedBool; // slot 21 - bytes11 _packedBytes11; // slot 21 + address public _packedAddress; // slot 21 + bool public _packedBool; // slot 21 + bytes11 public _packedBytes11; // slot 21 // Pack into (address,bool,bytes11) - bytes11 _otherPackedBytes11; // slot 22 - bool _otherPackedBool; // slot 22 - address _otherPackedAddress; // slot 22 + bytes11 public _otherPackedBytes11; // slot 22 + bool public _otherPackedBool; // slot 22 + address public _otherPackedAddress; // slot 22 } diff --git a/packages/contracts/test/chugsplash/storage.spec.ts b/packages/contracts/test/chugsplash/storage.spec.ts index fdf32e255b9da..445c3a8b22edb 100644 --- a/packages/contracts/test/chugsplash/storage.spec.ts +++ b/packages/contracts/test/chugsplash/storage.spec.ts @@ -1,11 +1,14 @@ import { expect } from '../setup' -import hre from 'hardhat' +import hre, { ethers } from 'hardhat' +import { Contract } from 'ethers' + import { computeStorageSlots, getStorageLayout, SolidityStorageLayout, } from '../../src' +import { isObject, toPlainObject } from 'lodash' describe('ChugSplash storage layout parsing', () => { let layout: SolidityStorageLayout @@ -13,240 +16,120 @@ describe('ChugSplash storage layout parsing', () => { layout = await getStorageLayout(hre, 'Helper_StorageHelper') }) + let Helper_StorageHelper: Contract + beforeEach(async () => { + Helper_StorageHelper = await ( + await hre.ethers.getContractFactory('Helper_StorageHelper') + ).deploy() + }) + + const computeAndVerifyStorageSlots = async ( + variables: any + ): Promise => { + const slots = computeStorageSlots(layout, variables) + for (const slot of slots) { + await Helper_StorageHelper.setStorage(slot.key, slot.val) + } + + for (const variable of Object.keys(variables)) { + const valA = await Helper_StorageHelper[variable]() + const valB = variables[variable] + if (Array.isArray(valA) && isObject(valB)) { + expect(toPlainObject(valA)).to.deep.include(valB) + } else { + expect(valA).to.equal(valB) + } + } + } + describe('computeStorageSlots', () => { - it('compute slots for uint8', () => { - expect( - computeStorageSlots(layout, { - _uint8: 123, - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000000', - val: - '0x000000000000000000000000000000000000000000000000000000000000007b', - }, - ]) + it('compute slots for uint8', async () => { + await computeAndVerifyStorageSlots({ + _uint8: 123, + }) }) - it('compute slots for uint64', () => { - expect( - computeStorageSlots(layout, { - _uint64: 1234, - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000002', - val: - '0x00000000000000000000000000000000000000000000000000000000000004d2', - }, - ]) + it('compute slots for uint64', async () => { + await computeAndVerifyStorageSlots({ + _uint64: 1234, + }) }) - it('compute slots for uint256', () => { - expect( - computeStorageSlots(layout, { - _uint256: 12345, - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000004', - val: - '0x0000000000000000000000000000000000000000000000000000000000003039', - }, - ]) + it('compute slots for uint256', async () => { + await computeAndVerifyStorageSlots({ + _uint256: 12345, + }) }) - it('compute slots for bytes1', () => { - expect( - computeStorageSlots(layout, { - _bytes1: '0x11', - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000006', - val: - '0x0000000000000000000000000000000000000000000000000000000000000011', - }, - ]) + it('compute slots for bytes1', async () => { + await computeAndVerifyStorageSlots({ + _bytes1: '0x11', + }) }) - it('compute slots for bytes8', () => { - expect( - computeStorageSlots(layout, { - _bytes8: '0x1212121212121212', - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000008', - val: - '0x0000000000000000000000000000000000000000000000001212121212121212', - }, - ]) + it('compute slots for bytes8', async () => { + await computeAndVerifyStorageSlots({ + _bytes8: '0x1212121212121212', + }) }) - it('compute slots for bytes32', () => { - expect( - computeStorageSlots(layout, { - _bytes32: - '0x2222222222222222222222222222222222222222222222222222222222222222', - }) - ).to.deep.equal([ - { - key: - '0x000000000000000000000000000000000000000000000000000000000000000a', - val: - '0x2222222222222222222222222222222222222222222222222222222222222222', - }, - ]) + it('compute slots for bytes32', async () => { + await computeAndVerifyStorageSlots({ + _bytes32: + '0x2222222222222222222222222222222222222222222222222222222222222222', + }) }) - it('compute slots for bool', () => { - expect( - computeStorageSlots(layout, { - _bool: true, - }) - ).to.deep.equal([ - { - key: - '0x000000000000000000000000000000000000000000000000000000000000000c', - val: - '0x0000000000000000000000000000000000000000000000000000000000000001', - }, - ]) + it('compute slots for bool', async () => { + await computeAndVerifyStorageSlots({ + _bool: true, + }) }) - it('compute slots for address', () => { - expect( - computeStorageSlots(layout, { - _address: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', - }) - ).to.deep.equal([ - { - key: - '0x000000000000000000000000000000000000000000000000000000000000000e', - val: - '0x0000000000000000000000005a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', - }, - ]) + it('compute slots for address', async () => { + await computeAndVerifyStorageSlots({ + _address: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + }) }) - it('compute slots for bytes (<32 bytes long)', () => { - expect( - computeStorageSlots(layout, { - _bytes: - '0x12121212121212121212121212121212121212121212121212121212121212', // only 31 bytes - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000010', - val: - '0x121212121212121212121212121212121212121212121212121212121212123e', // last byte contains byte length * 2 - }, - ]) + it('compute slots for bytes (<32 bytes long)', async () => { + await computeAndVerifyStorageSlots({ + _bytes: + '0x12121212121212121212121212121212121212121212121212121212121212', // only 31 bytes + }) }) - it('compute slots for string (<32 bytes long)', () => { - expect( - computeStorageSlots(layout, { - _string: 'hello i am a string', // 19 bytes - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000011', - val: - '0x68656c6c6f206920616d206120737472696e6700000000000000000000000026', // 19 * 2 = 38 = 0x26 - }, - ]) + it('compute slots for string (<32 bytes long)', async () => { + await computeAndVerifyStorageSlots({ + _string: 'hello i am a string', // 19 bytes + }) }) - it('compute slots for a simple (complete) struct', () => { - expect( - computeStorageSlots(layout, { - _struct: { - _structUint256: 1234, - _structAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', - _structBytes32: - '0x1212121212121212121212121212121212121212121212121212121212121212', - }, - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000012', - val: - '0x00000000000000000000000000000000000000000000000000000000000004d2', - }, - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000013', - val: - '0x0000000000000000000000005a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', - }, - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000014', - val: + it('compute slots for a simple (complete) struct', async () => { + await computeAndVerifyStorageSlots({ + _struct: { + _structUint256: ethers.BigNumber.from(1234), + _structAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + _structBytes32: '0x1212121212121212121212121212121212121212121212121212121212121212', }, - ]) + }) }) - it('compute slots for a simple (partial) struct', () => { - expect( - computeStorageSlots(layout, { - _struct: { - _structAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', - }, - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000013', - val: - '0x0000000000000000000000005a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', - }, - ]) + it('compute slots for packed variables', async () => { + await computeAndVerifyStorageSlots({ + _packedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + _packedBool: true, + _packedBytes11: '0x1212121212121212121212', + }) }) - it('compute slots for packed variables', () => { - expect( - computeStorageSlots(layout, { - _packedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', - _packedBool: true, - _packedBytes11: '0x1212121212121212121212', - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000015', - val: - '0x1212121212121212121212015a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c', - }, - ]) - }) - - it('compute slots for packed variables (2)', () => { - expect( - computeStorageSlots(layout, { - _otherPackedBytes11: '0x1212121212121212121212', - _otherPackedBool: true, - _otherPackedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', - }) - ).to.deep.equal([ - { - key: - '0x0000000000000000000000000000000000000000000000000000000000000016', - val: - '0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c011212121212121212121212', - }, - ]) + it('compute slots for packed variables (2)', async () => { + await computeAndVerifyStorageSlots({ + _otherPackedBytes11: '0x1212121212121212121212', + _otherPackedBool: true, + _otherPackedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', + }) }) }) }) From 5d7a90d75d7879792a6d19ffea7e5605dd45f5de Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 3 May 2021 17:36:12 -0400 Subject: [PATCH 7/8] test: Add some tests for unsupported types --- .../test-helpers/Helper_StorageHelper.sol | 4 ++ .../contracts/test/chugsplash/storage.spec.ts | 40 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol index fd5541ab5ee50..1de01f7d65125 100644 --- a/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol +++ b/packages/contracts/contracts/test-helpers/Helper_StorageHelper.sol @@ -57,4 +57,8 @@ contract Helper_StorageHelper { bytes11 public _otherPackedBytes11; // slot 22 bool public _otherPackedBool; // slot 22 address public _otherPackedAddress; // slot 22 + + // Unsupported types. + mapping (uint256 => uint256) _uint256ToUint256Map; + uint256[] _uint256Array; } diff --git a/packages/contracts/test/chugsplash/storage.spec.ts b/packages/contracts/test/chugsplash/storage.spec.ts index 445c3a8b22edb..7453c6030f8b6 100644 --- a/packages/contracts/test/chugsplash/storage.spec.ts +++ b/packages/contracts/test/chugsplash/storage.spec.ts @@ -1,14 +1,16 @@ import { expect } from '../setup' +/* Imports: External */ import hre, { ethers } from 'hardhat' import { Contract } from 'ethers' +import { isObject, toPlainObject } from 'lodash' +/* Imports: Internal */ import { computeStorageSlots, getStorageLayout, SolidityStorageLayout, } from '../../src' -import { isObject, toPlainObject } from 'lodash' describe('ChugSplash storage layout parsing', () => { let layout: SolidityStorageLayout @@ -131,5 +133,41 @@ describe('ChugSplash storage layout parsing', () => { _otherPackedAddress: '0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c', }) }) + + describe('unsupported types', () => { + it('should not support mappings', () => { + expect(() => { + computeStorageSlots(layout, { + _uint256ToUint256Map: { + 1234: 5678, + }, + }) + }).to.throw('mapping types not yet supported') + }) + + it('should not support arrays', () => { + expect(() => { + computeStorageSlots(layout, { + _uint256Array: [1234, 5678], + }) + }).to.throw('array types not yet supported') + }) + + it('should not support bytes > 31 bytes long', () => { + expect(() => { + computeStorageSlots(layout, { + _bytes: '0x' + '22'.repeat(64), + }) + }).to.throw('large strings (>31 bytes) not supported') + }) + + it('should not support strings > 31 bytes long', () => { + expect(() => { + computeStorageSlots(layout, { + _string: 'hello'.repeat(32), + }) + }).to.throw('large strings (>31 bytes) not supported') + }) + }) }) }) From 06879fb66852844e4392b67565c5bee483be6568 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 3 May 2021 19:12:42 -0400 Subject: [PATCH 8/8] fix: use semver to block unsupported solidity versions --- packages/contracts/package.json | 3 ++- packages/contracts/src/chugsplash/storage.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 18d155a11567b..648d40a5a5188 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -51,7 +51,8 @@ "@openzeppelin/contracts-upgradeable": "^3.3.0", "@typechain/hardhat": "^1.0.1", "ganache-core": "^2.13.2", - "glob": "^7.1.6" + "glob": "^7.1.6", + "semver": "^7.3.5" }, "devDependencies": { "@eth-optimism/hardhat-ovm": "^0.1.0", diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts index 37551a8dde6fd..5774f2e2e2ac9 100644 --- a/packages/contracts/src/chugsplash/storage.ts +++ b/packages/contracts/src/chugsplash/storage.ts @@ -1,6 +1,7 @@ /* External Imports */ import { fromHexString, remove0x } from '@eth-optimism/core-utils' import { BigNumber, ethers } from 'ethers' +import semver from 'semver' // Represents the JSON objects outputted by the Solidity compiler that describe the structure of // state within the contract. See @@ -74,6 +75,12 @@ export const getStorageLayout = async ( ) const output = buildInfo.output.contracts[sourceName][contractName] + if (!semver.satisfies(buildInfo.solcVersion, '>=0.4.x <0.9.x')) { + throw new Error( + `Storage layout for Solidity version ${buildInfo.solcVersion} not yet supported. Sorry!` + ) + } + if (!('storageLayout' in output)) { throw new Error( `Storage layout for ${name} not found. Did you forget to set the storage layout compiler option in your hardhat config? Read more: https://github.com/ethereum-optimism/smock#note-on-using-smoddit`