From 5e74723020877904dfd18c2767cb55249bc23b70 Mon Sep 17 00:00:00 2001 From: RPate97 Date: Sun, 6 Nov 2022 09:32:05 -0800 Subject: [PATCH] Add Support for Mapping Types (#149) * parent 4262614cda500b8e77803826f10aff4efb20520b author Ryan Pate 1667458967 -0700 committer Ryan Pate 1667689541 -0700 Add support for mapping types * add support for 0x prefixed bytes Co-authored-by: sam-goldman <106038229+sam-goldman@users.noreply.github.com> --- .changeset/olive-waves-own.md | 6 + .../core/src/languages/solidity/storage.ts | 86 ++++++++++++-- packages/plugins/.gitignore | 4 + .../chugsplash/SimpleStorage.config.ts | 84 ++++++++++++++ packages/plugins/contracts/SimpleStorage.sol | 103 +++++++++++++++++ packages/plugins/hardhat.config.ts | 56 +++++++++ packages/plugins/package.json | 3 +- packages/plugins/src/hardhat/deployments.ts | 2 +- packages/plugins/test/SimpleStorage.spec.ts | 107 ++++++++++++++++++ 9 files changed, 438 insertions(+), 13 deletions(-) create mode 100644 .changeset/olive-waves-own.md create mode 100644 packages/plugins/chugsplash/SimpleStorage.config.ts create mode 100644 packages/plugins/contracts/SimpleStorage.sol create mode 100644 packages/plugins/hardhat.config.ts create mode 100644 packages/plugins/test/SimpleStorage.spec.ts diff --git a/.changeset/olive-waves-own.md b/.changeset/olive-waves-own.md new file mode 100644 index 000000000..1ef55329a --- /dev/null +++ b/.changeset/olive-waves-own.md @@ -0,0 +1,6 @@ +--- +'@chugsplash/core': patch +'@chugsplash/plugins': patch +--- + +Add support for mappings diff --git a/packages/core/src/languages/solidity/storage.ts b/packages/core/src/languages/solidity/storage.ts index 0a4f37295..3589b8799 100644 --- a/packages/core/src/languages/solidity/storage.ts +++ b/packages/core/src/languages/solidity/storage.ts @@ -1,5 +1,5 @@ import { fromHexString, remove0x } from '@eth-optimism/core-utils' -import { BigNumber, ethers } from 'ethers' +import { BigNumber, ethers, utils } from 'ethers' import { ContractConfig } from '../../config' import { @@ -64,18 +64,18 @@ const encodeVariable = ( 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' + + nestedSlotOffset = 0, + // Slot key will be the same unless we are storing a mapping. + // So default to calculating it here, unless one is passed in. + slotKey = '0x' + remove0x( BigNumber.from( parseInt(storageObj.slot as any, 10) + nestedSlotOffset ).toHexString() - ).padStart(64, '0') + ).padStart(64, '0'), + mappingType: string | undefined = undefined +): Array => { + const variableType = storageTypes[mappingType ?? storageObj.type] if (variableType.encoding === 'inplace') { if (storageObj.type.startsWith('t_array')) { @@ -162,6 +162,7 @@ const encodeVariable = ( ] } else if ( variableType.label.startsWith('uint') || + variableType.label.startsWith('int') || variableType.label.startsWith('enum') ) { if ( @@ -221,12 +222,18 @@ const encodeVariable = ( `incorrect member in ${variableType.label}: ${varName}` ) } + // if this struct is within a mapping, then the key must be calculated + // using the passed in slotkey + const offsetKey = BigNumber.from(slotKey) + .add(parseInt(currMember.slot as any, 10)) + .toHexString() slots = slots.concat( encodeVariable( varVal, currMember, storageTypes, - nestedSlotOffset + parseInt(storageObj.slot as any, 10) + nestedSlotOffset + parseInt(storageObj.slot as any, 10), + mappingType ? offsetKey : undefined ) ) } @@ -266,7 +273,64 @@ const encodeVariable = ( throw new Error('large strings (>31 bytes) not supported') } } else if (variableType.encoding === 'mapping') { - throw new Error('mapping types not yet supported') + let slots = [] + for (const [key, value] of Object.entries(variable)) { + // default pack type for value types + let type = variableType.key.split('_')[1] + // default key encoding for value types + let encodedKey: string | Uint8Array = encodeVariable( + key, + storageObj, + storageTypes, + nestedSlotOffset + parseInt(storageObj.slot as any, 10), + undefined, + variableType.key + )[0].val + + if (variableType.key.startsWith('t_uint')) { + // all uints must be packed with type uint256 + type = 'uint256' + } else if (variableType.key.startsWith('t_int')) { + // all ints must be packed with type int256 + type = 'int256' + } else if (variableType.key.startsWith('t_string')) { + // strings do not need to be encoded + // pack type can be pulled from input type + encodedKey = key + } else if (variableType.key.startsWith('t_bytes')) { + // bytes do not need to be encoded, but must be converted from the input string + // pack type can be pulled straight from input type + encodedKey = fromHexString(key) + } + + // key for nested mappings is computed by packing and hashing the key of the child mapping + let concatenated + if (mappingType) { + concatenated = ethers.utils.solidityPack( + [type, 'uint256'], + [encodedKey, slotKey] + ) + } else { + concatenated = ethers.utils.solidityPack( + [type, 'uint256'], + [encodedKey, BigNumber.from(storageObj.slot).toHexString()] + ) + } + + const mappingKey = utils.keccak256(concatenated) + + slots = slots.concat( + encodeVariable( + value, + storageObj, + storageTypes, + 0, + mappingKey, + variableType.value + ) + ) + } + return slots } else if (variableType.encoding === 'dynamic_array') { throw new Error('array types not yet supported') } else { diff --git a/packages/plugins/.gitignore b/packages/plugins/.gitignore index 51eb69f27..33d75fd5f 100644 --- a/packages/plugins/.gitignore +++ b/packages/plugins/.gitignore @@ -1,2 +1,6 @@ dist/ .env +.deployed +artifacts +cache +deployments diff --git a/packages/plugins/chugsplash/SimpleStorage.config.ts b/packages/plugins/chugsplash/SimpleStorage.config.ts new file mode 100644 index 000000000..d9a691fcf --- /dev/null +++ b/packages/plugins/chugsplash/SimpleStorage.config.ts @@ -0,0 +1,84 @@ +import { ChugSplashConfig } from '@chugsplash/core' + +const config: ChugSplashConfig = { + // Configuration options for the project: + options: { + projectName: 'My First Project', + projectOwner: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + }, + // Below, we define all of the contracts in the deployment along with their state variables. + contracts: { + // First contract config: + FirstSimpleStorage: { + contract: 'SimpleStorage', + variables: { + testInt: 1, + number: 1, + stored: true, + storageName: 'First', + testStruct: { + a: 1, + b: 2, + c: 3, + }, + strTest: { + string: 'test', + }, + uintTest: { + uint: 1234, + }, + boolTest: { + bool: true, + }, + addressTest: { + address: '0x1111111111111111111111111111111111111111', + }, + structTest: { + test: { + a: 1, + b: 2, + c: 3, + }, + }, + uintStrTest: { + 1: 'test', + }, + intStrTest: { + 1: 'test', + }, + int8StrTest: { + 1: 'test', + }, + int128StrTest: { + 1: 'test', + }, + uint8StrTest: { + 1: 'test', + }, + uint128StrTest: { + 1: 'test', + }, + addressStrTest: { + '0x1111111111111111111111111111111111111111': 'test', + }, + bytesStrTest: { + '0xabcd': 'test', + }, + nestedMappingTest: { + test: { + test: 'success', + }, + }, + multiNestedMapping: { + 1: { + test: { + '0x1111111111111111111111111111111111111111': 2, + }, + }, + }, + }, + }, + }, +} + +export default config diff --git a/packages/plugins/contracts/SimpleStorage.sol b/packages/plugins/contracts/SimpleStorage.sol new file mode 100644 index 000000000..7f47473a5 --- /dev/null +++ b/packages/plugins/contracts/SimpleStorage.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +contract SimpleStorage { + struct S { uint16 a; uint16 b; uint256 c; } + int internal testInt; + uint8 internal number; + bool internal stored; + string internal storageName; + S testStruct; + mapping(string => string) public strTest; + mapping(string => uint) public uintTest; + mapping(string => bool) public boolTest; + mapping(string => address) public addressTest; + + mapping(uint => string) public uintStrTest; + mapping(int => string) public intStrTest; + mapping(int8 => string) public int8StrTest; + mapping(int128 => string) public int128StrTest; + mapping(uint8 => string) public uint8StrTest; + mapping(uint128 => string) public uint128StrTest; + mapping(address => string) public addressStrTest; + mapping(bytes => string) public bytesStrTest; + mapping(string => S) structTest; + mapping(string => mapping(string => string)) public nestedMappingTest; + mapping(uint8 => mapping(string => mapping(address => uint))) public multiNestedMapping; + + function getNumber() external view returns (uint8) { + return number; + } + + function isStored() external view returns (bool) { + return stored; + } + + function getStorageName() external view returns (string memory) { + return storageName; + } + + function getStruct() external view returns (S memory) { + return testStruct; + } + + function getStringTestMappingValue(string memory key) external view returns (string memory) { + return strTest[key]; + } + + function getIntTestMappingValue(string memory key) external view returns (uint) { + return uintTest[key]; + } + + function getBoolTestMappingValue(string memory key) external view returns (bool) { + return boolTest[key]; + } + + function getAddressTestMappingValue(string memory key) external view returns (address) { + return addressTest[key]; + } + + function getStructTestMappingValue(string memory key) external view returns (S memory) { + return structTest[key]; + } + + function getUintStringTestMappingValue(uint key) external view returns (string memory) { + return uintStrTest[key]; + } + + function getIntStringTestMappingValue(int key) external view returns (string memory) { + return intStrTest[key]; + } + + function getInt8StringTestMappingValue(int8 key) external view returns (string memory) { + return int8StrTest[key]; + } + + function getInt128StringTestMappingValue(int128 key) external view returns (string memory) { + return int128StrTest[key]; + } + + function getUint8StringTestMappingValue(uint8 key) external view returns (string memory) { + return uint8StrTest[key]; + } + + function getUint128StringTestMappingValue(uint128 key) external view returns (string memory) { + return uint128StrTest[key]; + } + + function getAddressStringTestMappingValue(address key) external view returns (string memory) { + return addressStrTest[key]; + } + + function getBytesStringTestMappingValue(bytes memory key) external view returns (string memory) { + return bytesStrTest[key]; + } + + function getNestedTestMappingValue(string memory keyOne, string memory keyTwo) external view returns (string memory) { + return nestedMappingTest[keyOne][keyTwo]; + } + + function getMultiNestedMappingTestMappingValue(uint8 keyOne, string memory keyTwo, address keyThree) external view returns (uint) { + return multiNestedMapping[keyOne][keyTwo][keyThree]; + } +} diff --git a/packages/plugins/hardhat.config.ts b/packages/plugins/hardhat.config.ts new file mode 100644 index 000000000..e5066865d --- /dev/null +++ b/packages/plugins/hardhat.config.ts @@ -0,0 +1,56 @@ +import { HardhatUserConfig } from 'hardhat/types' +import * as dotenv from 'dotenv' + +// Hardhat plugins +import '@nomiclabs/hardhat-ethers' +import './dist' + +// Load environment variables from .env +dotenv.config() + +const accounts = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.15', + settings: { + outputSelection: { + '*': { + '*': ['storageLayout'], + }, + }, + }, + }, + networks: { + localhost: { + url: 'http://localhost:8545', + }, + goerli: { + chainId: 5, + url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`, + accounts, + }, + 'optimism-goerli': { + chainId: 420, + url: `https://optimism-goerli.infura.io/v3/${process.env.INFURA_API_KEY}`, + accounts, + }, + optimism: { + chainId: 10, + url: `https://optimism-mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, + accounts, + }, + arbitrum: { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + accounts, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + accounts, + }, + }, +} + +export default config diff --git a/packages/plugins/package.json b/packages/plugins/package.json index d5764b03a..350fcc7e2 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -45,7 +45,8 @@ }, "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.2.1", - "hardhat": "^2.10.0" + "hardhat": "^2.10.0", + "chai": "^4.3.6" }, "peerDependencies": { "@nomiclabs/hardhat-ethers": "^2", diff --git a/packages/plugins/src/hardhat/deployments.ts b/packages/plugins/src/hardhat/deployments.ts index 1dcb283ca..43d0adbcb 100644 --- a/packages/plugins/src/hardhat/deployments.ts +++ b/packages/plugins/src/hardhat/deployments.ts @@ -1,7 +1,6 @@ import * as path from 'path' import * as fs from 'fs' -import { getChainId } from '@eth-optimism/core-utils' import '@nomiclabs/hardhat-ethers' import { Contract, ethers } from 'ethers' import { @@ -26,6 +25,7 @@ import { ProxyABI, } from '@chugsplash/contracts' import ora from 'ora' +import { getChainId } from '@eth-optimism/core-utils' import { getContractArtifact } from './artifacts' diff --git a/packages/plugins/test/SimpleStorage.spec.ts b/packages/plugins/test/SimpleStorage.spec.ts new file mode 100644 index 000000000..fb2a12f61 --- /dev/null +++ b/packages/plugins/test/SimpleStorage.spec.ts @@ -0,0 +1,107 @@ +import { expect } from 'chai' +import { BigNumber, Contract } from 'ethers' + +describe('SimpleStorage', () => { + let FirstSimpleStorage: Contract + beforeEach(async () => { + // You must reset your ChugSplash deployments to their initial state here + await chugsplash.reset() + + FirstSimpleStorage = await chugsplash.getContract('FirstSimpleStorage') + }) + + it('does set struct', async () => { + const struct = await FirstSimpleStorage.getStruct() + expect(struct[0]).equals(1) + expect(struct[1]).equals(2) + expect(struct[2]).to.deep.equal(BigNumber.from(3)) + }) + + it('does set string mapping to string, uint, bool, address', async () => { + expect( + await FirstSimpleStorage.getStringTestMappingValue('string') + ).to.equal('test') + expect( + await FirstSimpleStorage.getIntTestMappingValue('uint') + ).to.deep.equal(BigNumber.from(1234)) + expect(await FirstSimpleStorage.getBoolTestMappingValue('bool')).to.equal( + true + ) + expect( + await FirstSimpleStorage.getAddressTestMappingValue('address') + ).equals('0x1111111111111111111111111111111111111111') + }) + + it('does set string mapping to struct', async () => { + const struct = await FirstSimpleStorage.getStructTestMappingValue('test') + expect(struct[0]).equals(1) + expect(struct[1]).equals(2) + expect(struct[2]).to.deep.equal(BigNumber.from(3)) + }) + + it('does set uint mapping to string', async () => { + expect( + await FirstSimpleStorage.getUintStringTestMappingValue(BigNumber.from(1)) + ).to.equal('test') + }) + + it('does set int mapping to string', async () => { + expect( + await FirstSimpleStorage.getIntStringTestMappingValue(BigNumber.from(1)) + ).to.equal('test') + }) + + it('does set int8 mapping to string', async () => { + expect( + await FirstSimpleStorage.getIntStringTestMappingValue(BigNumber.from(1)) + ).to.equal('test') + }) + + it('does set int128 mapping to string', async () => { + expect( + await FirstSimpleStorage.getIntStringTestMappingValue(BigNumber.from(1)) + ).to.equal('test') + }) + + it('does set uint8 mapping to string', async () => { + expect(await FirstSimpleStorage.getUint8StringTestMappingValue(1)).to.equal( + 'test' + ) + }) + + it('does set uint128 mapping to string', async () => { + expect( + await FirstSimpleStorage.getUint128StringTestMappingValue(1) + ).to.equal('test') + }) + + it('does set address mapping to string', async () => { + expect( + await FirstSimpleStorage.getAddressStringTestMappingValue( + '0x1111111111111111111111111111111111111111' + ) + ).to.equal('test') + }) + + it('does set bytes mapping to string', async () => { + expect( + await FirstSimpleStorage.getBytesStringTestMappingValue('0xabcd') + ).to.equal('test') + }) + + it('does set nested string mapping', async () => { + expect( + await FirstSimpleStorage.getNestedTestMappingValue('test', 'test') + ).to.equal('success') + }) + + it('does set multi nested mapping', async () => { + expect( + await FirstSimpleStorage.getMultiNestedMappingTestMappingValue( + 1, + 'test', + '0x1111111111111111111111111111111111111111' + ) + ).to.deep.equal(BigNumber.from(2)) + }) +})