diff --git a/.changeset/warm-needles-double.md b/.changeset/warm-needles-double.md new file mode 100644 index 000000000..529aec2f6 --- /dev/null +++ b/.changeset/warm-needles-double.md @@ -0,0 +1,6 @@ +--- +'@chugsplash/plugins': patch +'@chugsplash/core': patch +--- + +Add support for struct constructor args diff --git a/packages/core/src/actions/artifacts.ts b/packages/core/src/actions/artifacts.ts index b61996e8f..0d4ea8180 100644 --- a/packages/core/src/actions/artifacts.ts +++ b/packages/core/src/actions/artifacts.ts @@ -127,7 +127,7 @@ export const writeDeploymentArtifacts = async ( const buildInfo = readBuildInfo( artifactPaths[referenceName].buildInfoPath ) - const { constructorArgValues } = getConstructorArgs( + const constructorArgValues = getConstructorArgs( parsedConfig.contracts[referenceName].constructorArgs, abi ) diff --git a/packages/core/src/config/parse.ts b/packages/core/src/config/parse.ts index dd61a2b6f..1c9291890 100644 --- a/packages/core/src/config/parse.ts +++ b/packages/core/src/config/parse.ts @@ -97,7 +97,9 @@ const logValidationError = ( silent: boolean, stream: NodeJS.WritableStream ) => { - validationErrors = true + if (logLevel === 'error') { + validationErrors = true + } chugsplashLog(logLevel, title, lines, silent, stream) } @@ -681,13 +683,23 @@ const parseUnsignedInteger = ( .pow(8 * numberOfBytes) .sub(1) - if ( - remove0x(BigNumber.from(variable).toHexString()).length / 2 > - numberOfBytes - ) { - throw new Error( - `invalid value for ${label}: ${variable}, outside valid range: [0:${maxValue}]` - ) + try { + if ( + remove0x(BigNumber.from(variable).toHexString()).length / 2 > + numberOfBytes + ) { + throw new Error( + `invalid value for ${label}: ${variable}, outside valid range: [0:${maxValue}]` + ) + } + } catch (e) { + if (e.message.includes('invalid BigNumber string')) { + throw new Error( + `invalid value for ${label}, expected a valid number but got: ${variable}` + ) + } else { + throw e + } } return BigNumber.from(variable).toString() @@ -746,13 +758,23 @@ const parseInteger = ( .pow(8 * numberOfBytes) .div(2) .sub(1) - if ( - BigNumber.from(variable).lt(minValue) || - BigNumber.from(variable).gt(maxValue) - ) { - throw new Error( - `invalid value for ${label}: ${variable}, outside valid range: [${minValue}:${maxValue}]` - ) + try { + if ( + BigNumber.from(variable).lt(minValue) || + BigNumber.from(variable).gt(maxValue) + ) { + throw new Error( + `invalid value for ${label}: ${variable}, outside valid range: [${minValue}:${maxValue}]` + ) + } + } catch (e) { + if (e.message.includes('invalid BigNumber string')) { + throw new Error( + `invalid value for ${label}, expected a valid number but got: ${variable}` + ) + } else { + throw e + } } return BigNumber.from(variable).toString() @@ -802,7 +824,7 @@ export const parseInplaceStruct: VariableHandler< }) if (memberStorageObj === undefined) { throw new InputError( - `User entered incorrect member in ${variableType.label}: ${varName}` + `Extra member(s) detected in ${variableType.label}, ${storageObj.label}: ${varName}` ) } parsedVariable[varName] = parseAndValidateVariable( @@ -814,6 +836,21 @@ export const parseInplaceStruct: VariableHandler< ) } + // Find any members missing from the struct + const missingMembers: string[] = [] + for (const member of variableType.members) { + if (parsedVariable[member.label] === undefined) { + missingMembers.push(member.label) + } + } + + if (missingMembers.length > 0) { + throw new InputError( + `Missing member(s) in struct ${variableType.label}, ${storageObj.label}: ` + + missingMembers.join(', ') + ) + } + return parsedVariable } @@ -1224,7 +1261,9 @@ const parseContractVariables = ( const parseArrayConstructorArg = ( input: ParamType, - constructorArgValue: UserConfigVariable + name: string, + constructorArgValue: UserConfigVariable, + cre: ChugSplashRuntimeEnvironment ): ParsedConfigVariable[] => { if (!Array.isArray(constructorArgValue)) { throw new InputError( @@ -1235,7 +1274,7 @@ const parseArrayConstructorArg = ( if (input.arrayLength !== -1) { if (constructorArgValue.length !== input.arrayLength) { throw new InputError( - `Expected array of length ${input.arrayLength} for ${input.name} but got array of length ${constructorArgValue.length}` + `Expected array of length ${input.arrayLength} for ${name} but got array of length ${constructorArgValue.length}` ) } } @@ -1243,7 +1282,7 @@ const parseArrayConstructorArg = ( const parsedValues: ParsedConfigVariable = [] for (const element of constructorArgValue) { parsedValues.push( - parseAndValidateConstructorArg(input.arrayChildren, element) + parseAndValidateConstructorArg(input.arrayChildren, name, element, cre) ) } @@ -1252,7 +1291,9 @@ const parseArrayConstructorArg = ( export const parseStructConstructorArg = ( paramType: ParamType, - constructorArgValue: UserConfigVariable + name: string, + constructorArgValue: UserConfigVariable, + cre: ChugSplashRuntimeEnvironment ) => { if (typeof constructorArgValue !== 'object') { throw new InputError( @@ -1262,17 +1303,41 @@ export const parseStructConstructorArg = ( ) } + const memberErrors: string[] = [] const parsedValues: ParsedConfigVariable = {} for (const [key, value] of Object.entries(constructorArgValue)) { const inputChild = paramType.components.find((component) => { return component.name === key }) if (inputChild === undefined) { - throw new InputError( - `User entered incorrect member in ${paramType.name}: ${key}` + memberErrors.push(`Extra member(s) in struct ${paramType.name}: ${key}`) + } else { + parsedValues[key] = parseAndValidateConstructorArg( + inputChild, + `${name}.${key}`, + value, + cre ) } - parsedValues[key] = parseAndValidateConstructorArg(inputChild, value) + } + + // Find any members missing from the struct + const missingMembers: string[] = [] + for (const member of paramType.components) { + if (parsedValues[member.name] === undefined) { + missingMembers.push(member.name) + } + } + + if (missingMembers.length > 0) { + memberErrors.push( + `Missing member(s) in struct ${paramType.name}: ` + + missingMembers.join(', ') + ) + } + + if (memberErrors.length > 0) { + throw new InputError(memberErrors.join('\n')) } return parsedValues @@ -1280,7 +1345,9 @@ export const parseStructConstructorArg = ( const parseAndValidateConstructorArg = ( input: ParamType, - constructorArgValue: UserConfigVariable + name: string, + constructorArgValue: UserConfigVariable, + cre: ChugSplashRuntimeEnvironment ): ParsedConfigVariable => { const constructorArgType = input.type // We fetch a new ParamType using the input type even though input is a ParamType object @@ -1288,7 +1355,6 @@ const parseAndValidateConstructorArg = ( // an object with more useful information on it const paramType = input.type === 'tuple' ? input : ethers.utils.ParamType.from(input.type) - const name = input.name if ( paramType.baseType && (paramType.baseType.startsWith('uint') || @@ -1334,7 +1400,9 @@ const parseAndValidateConstructorArg = ( } else if (paramType.baseType === 'string') { return parseBytes(constructorArgValue, name, paramType.type, 0) } else if (paramType.baseType === 'array') { - return parseArrayConstructorArg(paramType, constructorArgValue) + return parseArrayConstructorArg(paramType, name, constructorArgValue, cre) + } else if (paramType.type === 'tuple') { + return parseStructConstructorArg(paramType, name, constructorArgValue, cre) } else { // throw or log error throw new InputError( @@ -1414,7 +1482,9 @@ export const parseContractConstructorArgs = ( try { parsedConstructorArgs[input.name] = parseAndValidateConstructorArg( input, - constructorArgValue + input.name, + constructorArgValue, + cre ) } catch (e) { inputFormatErrors.push((e as Error).message) @@ -1958,7 +2028,10 @@ export const assertValidContracts = ( } } - if (!contractConfig.unsafeAllowEmptyPush) { + if ( + !contractConfig.unsafeAllowEmptyPush && + contractConfig.kind !== 'no-proxy' + ) { for (const memberAccessNode of findAll('MemberAccess', contractDef)) { const typeIdentifier = memberAccessNode.expression.typeDescriptions.typeIdentifier diff --git a/packages/core/src/etherscan.ts b/packages/core/src/etherscan.ts index aef64c0c4..ffeb465fe 100644 --- a/packages/core/src/etherscan.ts +++ b/packages/core/src/etherscan.ts @@ -94,7 +94,7 @@ export const verifyChugSplashConfig = async ( )) { const { artifact, buildInfo } = artifacts[referenceName] const { abi, contractName, sourceName } = artifact - const { constructorArgValues } = getConstructorArgs( + const constructorArgValues = getConstructorArgs( canonicalConfig.contracts[referenceName].constructorArgs, abi ) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 9ae2df642..07a5d83ec 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -831,11 +831,7 @@ export const getContractAddress = ( export const getConstructorArgs = ( constructorArgs: ParsedConfigVariables, abi: Array -): { - constructorArgTypes: Array - constructorArgValues: Array -} => { - const constructorArgTypes: Array = [] +): Array => { const constructorArgValues: Array = [] const constructorFragment = abi.find( @@ -843,15 +839,14 @@ export const getConstructorArgs = ( ) if (constructorFragment === undefined) { - return { constructorArgTypes, constructorArgValues } + return constructorArgValues } constructorFragment.inputs.forEach((input) => { - constructorArgTypes.push(input.type) constructorArgValues.push(constructorArgs[input.name]) }) - return { constructorArgTypes, constructorArgValues } + return constructorArgValues } export const getCreationCodeWithConstructorArgs = ( @@ -859,15 +854,12 @@ export const getCreationCodeWithConstructorArgs = ( constructorArgs: ParsedConfigVariables, abi: ContractArtifact['abi'] ): string => { - const { constructorArgTypes, constructorArgValues } = getConstructorArgs( - constructorArgs, - abi - ) + const constructorArgValues = getConstructorArgs(constructorArgs, abi) + + const iface = new ethers.utils.Interface(abi) const creationCodeWithConstructorArgs = bytecode.concat( - remove0x( - utils.defaultAbiCoder.encode(constructorArgTypes, constructorArgValues) - ) + remove0x(iface.encodeDeploy(constructorArgValues)) ) return creationCodeWithConstructorArgs diff --git a/packages/executor/.env.example b/packages/executor/.env.example index 866d8f7d5..20c0557fe 100644 --- a/packages/executor/.env.example +++ b/packages/executor/.env.example @@ -1,13 +1,15 @@ +# Required for all environments +CHUGSPLASH_EXECUTOR__NETWORK= +CHUGSPLASH_EXECUTOR__PORT=7300 CHUGSPLASH_EXECUTOR__PRIVATE_KEYS= CHUGSPLASH_EXECUTOR__URL= -CHUGSPLASH_EXECUTOR__NETWORK= +IPFS_PROJECT_ID= +IPFS_API_KEY_SECRET= + +# Required in production CHUGSPLASH_EXECUTOR__AMPLITUDE_KEY= CHUGSPLASH_EXECUTOR__LOG_LEVEL= CHUGSPLASH_EXECUTOR__LOOP_INTERVAL_MS= -CHUGSPLASH_EXECUTOR__PORT=7300 CHUGSPLASH_EXECUTOR__MANAGED_API_URL= MANAGED_PUBLIC_KEY= -IPFS_PROJECT_ID= -IPFS_API_KEY_SECRET= ETHERSCAN_API_KEY= -HARDHAT_NETWORK= diff --git a/packages/plugins/chugsplash/ConstructorArgValidation.config.ts b/packages/plugins/chugsplash/ConstructorArgValidation.config.ts index 38bd5ebf7..86e1904eb 100644 --- a/packages/plugins/chugsplash/ConstructorArgValidation.config.ts +++ b/packages/plugins/chugsplash/ConstructorArgValidation.config.ts @@ -2,8 +2,8 @@ import { UserChugSplashConfig } from '@chugsplash/core' import { ethers } from 'ethers' import { - invalidValueTypesPartOne, - invalidValueTypesPartTwo, + invalidConstructorArgsPartOne, + invalidConstructorArgsPartTwo, } from '../test/constants' const projectName = 'Constructor Args Validation' @@ -21,14 +21,14 @@ const config: UserChugSplashConfig = { ConstructorArgsValidationPartOne: { contract: 'ConstructorArgsValidationPartOne', constructorArgs: { - ...invalidValueTypesPartOne, + ...invalidConstructorArgsPartOne, _immutableUint: 1, }, }, ConstructorArgsValidationPartTwo: { contract: 'ConstructorArgsValidationPartTwo', constructorArgs: { - ...invalidValueTypesPartTwo, + ...invalidConstructorArgsPartTwo, }, }, }, diff --git a/packages/plugins/chugsplash/VariableValidation.config.ts b/packages/plugins/chugsplash/VariableValidation.config.ts index 7375108bb..43f19c9ce 100644 --- a/packages/plugins/chugsplash/VariableValidation.config.ts +++ b/packages/plugins/chugsplash/VariableValidation.config.ts @@ -53,6 +53,14 @@ const config: UserChugSplashConfig = { testKey: true, }, }, + extraMemberStruct: { + a: 1, + b: 2, + c: 3, + }, + missingMemberStruct: { + b: 2, + }, // variables that are not in the contract extraVar: 214830928, anotherExtraVar: [], diff --git a/packages/plugins/chugsplash/foundry/deploy.t.js b/packages/plugins/chugsplash/foundry/deploy.t.js index 6e454b883..e4624e9ed 100644 --- a/packages/plugins/chugsplash/foundry/deploy.t.js +++ b/packages/plugins/chugsplash/foundry/deploy.t.js @@ -42,6 +42,16 @@ const complexConstructorArgs = { [16, 17, 18], ], ], + _complexStruct: { + b: 2, + a: '0x' + 'aa'.repeat(32), + c: 3, + d: [1, 2], + e: [ + [1, 2, 3], + [4, 5, 6], + ], + }, } const variables = { diff --git a/packages/plugins/contracts/ComplexConstructorArgs.sol b/packages/plugins/contracts/ComplexConstructorArgs.sol index cdf05f7b1..fa47dcd00 100644 --- a/packages/plugins/contracts/ComplexConstructorArgs.sol +++ b/packages/plugins/contracts/ComplexConstructorArgs.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.9; contract ComplexConstructorArgs { type UserDefinedType is uint256; + struct ComplexStruct { bytes32 a; uint128 b; uint128 c; uint64[2] d; uint[][] e; } + string public str; bytes public dynamicBytes; uint64[5] public uint64FixedArray; int64[] public int64DynamicArray; uint64[5][6] public uint64FixedNestedArray; uint64[][][] public uint64DynamicMultiNestedArray; + ComplexStruct public complexStruct; constructor( string memory _str, @@ -17,7 +20,8 @@ contract ComplexConstructorArgs { uint64[5] memory _uint64FixedArray, int64[] memory _int64DynamicArray, uint64[5][6] memory _uint64FixedNestedArray, - uint64[][][] memory _uint64DynamicMultiNestedArray + uint64[][][] memory _uint64DynamicMultiNestedArray, + ComplexStruct memory _complexStruct ) { str = _str; dynamicBytes = _dynamicBytes; @@ -25,5 +29,10 @@ contract ComplexConstructorArgs { int64DynamicArray = _int64DynamicArray; uint64FixedNestedArray = _uint64FixedNestedArray; uint64DynamicMultiNestedArray = _uint64DynamicMultiNestedArray; + complexStruct = _complexStruct; + } + + function getComplexStruct() external view returns (ComplexStruct memory) { + return complexStruct; } } diff --git a/packages/plugins/contracts/ConstructorArgsValidationPartTwo.sol b/packages/plugins/contracts/ConstructorArgsValidationPartTwo.sol index be0394e29..43e1c79f5 100644 --- a/packages/plugins/contracts/ConstructorArgsValidationPartTwo.sol +++ b/packages/plugins/contracts/ConstructorArgsValidationPartTwo.sol @@ -2,23 +2,40 @@ pragma solidity ^0.8.9; contract ConstructorArgsValidationPartTwo { + struct SimpleStruct { uint a; uint b; } + bytes8 immutable public longBytes8; bytes16 immutable public malformedBytes16; bool immutable public intBoolean; bool immutable public stringBoolean; bool immutable public arrayBoolean; + uint[] public invalidBaseTypeArray; + uint[][] public invalidNestedBaseTypeArray; + uint[2] public incorrectlySizedArray; + uint[2][2] public incorrectlySizedNestedArray; + SimpleStruct public structMissingMembers; constructor( bytes8 _longBytes8, bytes16 _malformedBytes16, bool _intBoolean, bool _stringBoolean, - bool _arrayBoolean + bool _arrayBoolean, + uint[] memory _invalidBaseTypeArray, + uint[][] memory _invalidNestedBaseTypeArray, + uint[2] memory _incorrectlySizedArray, + uint[2][2] memory _incorrectlySizedNestedArray, + SimpleStruct memory _structMissingMembers ) { longBytes8 = _longBytes8; malformedBytes16 = _malformedBytes16; intBoolean = _intBoolean; stringBoolean = _stringBoolean; arrayBoolean = _arrayBoolean; + invalidBaseTypeArray = _invalidBaseTypeArray; + invalidNestedBaseTypeArray = _invalidNestedBaseTypeArray; + incorrectlySizedArray = _incorrectlySizedArray; + incorrectlySizedNestedArray = _incorrectlySizedNestedArray; + structMissingMembers = _structMissingMembers; } } diff --git a/packages/plugins/contracts/VariableValidation.sol b/packages/plugins/contracts/VariableValidation.sol index 41b654e81..40dc70afb 100644 --- a/packages/plugins/contracts/VariableValidation.sol +++ b/packages/plugins/contracts/VariableValidation.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.9; contract VariableValidation { + struct SimpleStruct { uint a; uint b; } + int8 public arrayInt8; int8 public int8OutsideRange; uint8 public uint8OutsideRange; @@ -26,6 +28,8 @@ contract VariableValidation { mapping(string => string) public invalidStringStringMapping; mapping(string => int) public invalidStringIntMapping; mapping(string => mapping(string => int)) public invalidNestedStringIntBoolMapping; + SimpleStruct public extraMemberStruct; + SimpleStruct public missingMemberStruct; // Variables that are not set in the config uint public notSetUint; diff --git a/packages/plugins/test/Storage.spec.ts b/packages/plugins/test/Storage.spec.ts index 829ff25a8..0b7debbff 100644 --- a/packages/plugins/test/Storage.spec.ts +++ b/packages/plugins/test/Storage.spec.ts @@ -550,4 +550,27 @@ describe('Storage', () => { } } }) + + it('does set mutable struct constructor arg', async () => { + const { a, b, c, d, e } = await ComplexConstructorArgs.getComplexStruct() + expect(a).equals(complexConstructorArgs._complexStruct.a) + expect(b).deep.equals( + BigNumber.from(complexConstructorArgs._complexStruct.b) + ) + expect(c).deep.equals( + BigNumber.from(complexConstructorArgs._complexStruct.c) + ) + for (let i = 0; i < d.length; i++) { + expect(d[i]).deep.equals( + BigNumber.from(complexConstructorArgs._complexStruct.d[i]) + ) + } + for (let i = 0; i < e.length; i++) { + for (let j = 0; j < e[i].length; j++) { + expect(e[i][j]).deep.equals( + BigNumber.from(complexConstructorArgs._complexStruct.e[i][j]) + ) + } + } + }) }) diff --git a/packages/plugins/test/Validation.spec.ts b/packages/plugins/test/Validation.spec.ts index f61dae4a7..88390ec41 100644 --- a/packages/plugins/test/Validation.spec.ts +++ b/packages/plugins/test/Validation.spec.ts @@ -317,6 +317,18 @@ describe('Validate', () => { ) }) + it('did catch struct with extra member', async () => { + expect(validationOutput).to.have.string( + 'Extra member(s) detected in struct VariableValidation.SimpleStruct, extraMemberStruct: c' + ) + }) + + it('did catch struct with missing member', async () => { + expect(validationOutput).to.have.string( + 'Missing member(s) in struct struct VariableValidation.SimpleStruct, missingMemberStruct: a' + ) + }) + it('did catch missing variables', async () => { expect(validationOutput).to.have.string( 'were not defined in the ChugSplash config file' @@ -368,4 +380,40 @@ describe('Validate', () => { `Detected value for functionType which is a function. Function variables should be ommitted from your ChugSplash config.` ) }) + + it('did catch invalid array base type in constructor arg', async () => { + expect(validationOutput).to.have.string( + `invalid value for _invalidBaseTypeArray, expected a valid number but got: hello` + ) + }) + + it('did catch invalid nested array base type in constructor arg', async () => { + expect(validationOutput).to.have.string( + `invalid value for _invalidNestedBaseTypeArray, expected a valid number but got: hello` + ) + }) + + it('did catch incorrect array size in constructor arg', async () => { + expect(validationOutput).to.have.string( + `Expected array of length 2 for _incorrectlySizedArray but got array of length 5` + ) + }) + + it('did catch incorrect nested array size in constructor arg', async () => { + expect(validationOutput).to.have.string( + `Expected array of length 2 for _incorrectlySizedNestedArray but got array of length 3` + ) + }) + + it('did catch incorrect member in constructor arg struct', async () => { + expect(validationOutput).to.have.string( + `Extra member(s) in struct _structMissingMembers: z` + ) + }) + + it('did catch struct with missing members in constructor arg', async () => { + expect(validationOutput).to.have.string( + `Missing member(s) in struct _structMissingMembers: b` + ) + }) }) diff --git a/packages/plugins/test/constants.ts b/packages/plugins/test/constants.ts index 1cfce23f3..be551fe0a 100644 --- a/packages/plugins/test/constants.ts +++ b/packages/plugins/test/constants.ts @@ -20,7 +20,7 @@ const enum TestEnum { 'C', } -export const invalidValueTypesPartOne = { +export const invalidConstructorArgsPartOne = { _arrayInt8: [0, 1, 2], _int8OutsideRange: 255, _uint8OutsideRange: 256, @@ -33,12 +33,23 @@ export const invalidValueTypesPartOne = { _oddStaticBytes: '0xabcdefghijklmno', } -export const invalidValueTypesPartTwo = { +export const invalidConstructorArgsPartTwo = { _longBytes8: '0x' + '11'.repeat(32), _malformedBytes16: '11'.repeat(16), _intBoolean: 1, _stringBoolean: 'true', _arrayBoolean: [true, false], + _invalidBaseTypeArray: ['hello', 'world'], + _invalidNestedBaseTypeArray: [['hello', 'world']], + _incorrectlySizedArray: [1, 2, 3, 4, 5], + _incorrectlySizedNestedArray: [ + [1, 2, 3], + [4, 5, 6], + ], + _structMissingMembers: { + a: 1, + z: 2, + }, } export const constructorArgs = { @@ -83,6 +94,16 @@ export const complexConstructorArgs = { [16, 17, 18], ], ], + _complexStruct: { + b: 2, + a: '0x' + 'aa'.repeat(32), + c: 3, + d: [1, 2], + e: [ + [1, 2, 3], + [4, 5, 6], + ], + }, } export const variables = {