import type { Signer } from "ethers"; import { concat, getAddress, getCreate2Address, Interface, isAddress, keccak256 } from "ethers"; import { artifacts, ethers } from "hardhat"; import { resolveValue, WorkflowStep } from "./parameters"; import type { WorkflowContext } from "../run"; const CREATE2_FACTORY_ABI = [ "function deploy(bytes32 salt, bytes initCode, bytes data, uint256 create2ForwardValue, uint256 callForwardValue) external payable returns (address deployed)", ]; const CREATE2_FACTORY_INTERFACE = new Interface(CREATE2_FACTORY_ABI); const CONTROLLED_MULTICALL_INTERFACE = new Interface([ "function aggregate3(tuple(address target,bool allowFailure,bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)", "function aggregate3Value(tuple(address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)", ]); export interface PostDeployCall { target?: string; data: string; value?: string; } export interface Create2Options { factory: string; salt: string; controlledMulticall?: string; transferOwnership?: boolean; postDeployCalls?: PostDeployCall[]; create2ForwardValue?: string; callForwardValue?: string; expectedAddress?: string; leadingZeroBytes?: number; uupsPlaceholderImplementation?: string; } export interface Create2Deployment { address: string; txHash?: string; alreadyDeployed: boolean; predictedAddress: string; } export interface PredictedCreate2ContractAddress { address: string; initCode: string; initCodeHash: string; } export interface PreparedCreate2Call { kind: "create2-deploy"; target: string; value: string; data: string; predictedAddress: string; salt: string; initCode: string; initCodeHash: string; create2ForwardValue: string; callForwardValue: string; } export interface ControlledMulticallInnerCall { target: string; value: string; data: string; } export interface PreparedControlledMulticallTransaction { target: string; value: string; data: string; operation: 0; method: "aggregate3" | "aggregate3Value"; calls: Array<{ target: string; allowFailure: false; value: string; callData: string; }>; } interface MulticallTx { hash: string; wait: () => Promise; } interface MulticallCall { target: string; allowFailure: boolean; callData: string; } interface MulticallValueCall extends MulticallCall { value: bigint; } interface ControlledMulticall { aggregate3: (calls: MulticallCall[]) => Promise; aggregate3Value: ( calls: MulticallValueCall[], overrides: { value: bigint }, ) => Promise; } const bigintValue = (value: string | undefined): bigint => { if (!value || value === "0") return 0n; return BigInt(value); }; export const prepareControlledMulticallTransaction = ( controlledMulticall: string, inputCalls: ControlledMulticallInnerCall[], ): PreparedControlledMulticallTransaction => { if (!isAddress(controlledMulticall)) { throw new Error("controlledMulticall must be an EVM address"); } if (inputCalls.length === 0) { throw new Error("controlled multicall requires at least one inner call"); } const calls = inputCalls.map((call) => ({ target: getAddress(call.target), allowFailure: false as const, value: bigintValue(call.value).toString(10), callData: call.data, })); const totalValue = calls.reduce((sum, call) => sum + BigInt(call.value), 0n); const method = totalValue === 0n ? "aggregate3" as const : "aggregate3Value" as const; const encodedCalls = method === "aggregate3" ? calls.map(({ value: _value, ...call }) => call) : calls; return { target: getAddress(controlledMulticall), value: totalValue.toString(10), data: CONTROLLED_MULTICALL_INTERFACE.encodeFunctionData(method, [encodedCalls]), operation: 0, method, calls, }; }; export const sendPreparedControlledMulticallTransaction = async ( prepared: PreparedControlledMulticallTransaction, signer: Signer, ): Promise<{ hash: string; wait: () => Promise }> => { const transaction = await signer.sendTransaction({ to: prepared.target, value: prepared.value, data: prepared.data, }); await transaction.wait(); return transaction; }; const resolveObject = ( context: WorkflowContext, value: unknown, ): Record => { const resolved = resolveValue(value || {}, { targetId: context.targetId, target: context.target, params: context.targetParams, state: context.state, }); if (!resolved || typeof resolved !== "object" || Array.isArray(resolved)) { return {}; } return resolved as Record; }; const stringField = ( value: Record, key: string, ): string | undefined => { const field = value[key]; return typeof field === "string" && field.trim() ? field.trim() : undefined; }; const booleanField = ( value: Record, key: string, ): boolean | undefined => { return typeof value[key] === "boolean" ? value[key] as boolean : undefined; }; const numberField = ( value: Record, key: string, ): number | undefined => { const field = value[key]; return typeof field === "number" ? field : undefined; }; const assertLeadingZeroBytes = ( stepId: string, leadingZeroBytes: number | undefined, ): void => { if (leadingZeroBytes === undefined) return; if (!Number.isInteger(leadingZeroBytes) || leadingZeroBytes < 0 || leadingZeroBytes > 20) { throw new Error(`Step ${stepId} create2.leadingZeroBytes must be an integer from 0 to 20`); } }; export const resolveCreate2Options = ( context: WorkflowContext, step: WorkflowStep, ): Create2Options | undefined => { const strategy = typeof step.strategy === "string" ? step.strategy : undefined; const stepCreate2 = resolveObject(context, step.create2); const targetCreate2 = resolveObject(context, context.target.create2); const targetUups = resolveObject(context, context.target.uups); const targetMulticall = resolveObject(context, context.target.multicall); if (strategy !== "create2" && Object.keys(stepCreate2).length === 0) { return undefined; } const factory = stringField(stepCreate2, "factory") || stringField(targetCreate2, "factory"); const salt = stringField(stepCreate2, "salt"); const leadingZeroBytes = numberField(stepCreate2, "leadingZeroBytes") ?? numberField(targetCreate2, "leadingZeroBytes"); const uupsPlaceholderImplementation = stringField(targetUups, "placeholderImplementation"); const controlledMulticall = stringField(targetMulticall, "controlledMulticall"); if (!factory) { throw new Error(`Step ${step.id} uses create2 but no factory address was provided`); } if (!salt) { throw new Error(`Step ${step.id} uses create2 but no salt was provided`); } assertLeadingZeroBytes(step.id, leadingZeroBytes); if (step.upgradeableStrategy === "uups-placeholder-proxy" && !uupsPlaceholderImplementation) { throw new Error( `Step ${step.id} requires target.uups.placeholderImplementation`, ); } return { factory, salt, controlledMulticall, transferOwnership: booleanField(stepCreate2, "transferOwnership"), create2ForwardValue: stringField(stepCreate2, "create2ForwardValue"), callForwardValue: stringField(stepCreate2, "callForwardValue"), expectedAddress: stringField(stepCreate2, "expectedAddress"), leadingZeroBytes, uupsPlaceholderImplementation, }; }; export const toOpenZeppelinCreate2Config = ( options: Create2Options, ): Record => { return { address: options.factory, salt: options.salt, implementationAddress: options.uupsPlaceholderImplementation, multicall: options.controlledMulticall, transferOwnership: options.transferOwnership, postDeployCalls: options.postDeployCalls, }; }; export const hasLeadingZeroBytes = ( address: string, leadingZeroBytes: number, ): boolean => { if (!isAddress(address)) { throw new Error(`Invalid CREATE2 address: ${address}`); } return address.slice(2, 2 + leadingZeroBytes * 2).toLowerCase() === "00".repeat(leadingZeroBytes); }; export const assertCreate2AddressPolicy = ( label: string, predictedAddress: string, options: Pick, ): void => { if (!isAddress(predictedAddress)) { throw new Error(`${label} predicted invalid CREATE2 address: ${predictedAddress}`); } if (options.expectedAddress) { if (!isAddress(options.expectedAddress)) { throw new Error(`${label} expectedAddress is invalid: ${options.expectedAddress}`); } if (getAddress(predictedAddress) !== getAddress(options.expectedAddress)) { throw new Error( `${label} predicted ${predictedAddress} but parameters expected ${options.expectedAddress}`, ); } } if (options.leadingZeroBytes !== undefined && !hasLeadingZeroBytes(predictedAddress, options.leadingZeroBytes)) { throw new Error( `${label} predicted ${predictedAddress} but create2.leadingZeroBytes requires prefix ` + `0x${"00".repeat(options.leadingZeroBytes)}`, ); } }; export const prepareCreate2Call = ( label: string, initCode: string, options: Create2Options, ): PreparedCreate2Call => { const initCodeHash = keccak256(initCode); const predictedAddress = getCreate2Address(options.factory, options.salt, initCodeHash); assertCreate2AddressPolicy(label, predictedAddress, options); const create2ForwardValue = options.create2ForwardValue || "0"; const callForwardValue = options.callForwardValue || "0"; const value = ( bigintValue(create2ForwardValue) + bigintValue(callForwardValue) ).toString(); return { kind: "create2-deploy", target: options.factory, value, data: CREATE2_FACTORY_INTERFACE.encodeFunctionData("deploy", [ options.salt, initCode, "0x", create2ForwardValue, callForwardValue, ]), predictedAddress, salt: options.salt, initCode, initCodeHash, create2ForwardValue, callForwardValue, }; }; const postDeployTarget = ( target: string | undefined, predictedAddress: string, ): string => { if (!target || target === "$proxy" || target === "$deployed") { return predictedAddress; } return target; }; export const deployCreate2InitCode = async ( label: string, initCode: string, options: Create2Options, signer: Signer, ): Promise => { const prepared = prepareCreate2Call(label, initCode, options); const predictedAddress = prepared.predictedAddress; const existingCode = await ethers.provider.getCode(predictedAddress); if (existingCode && existingCode !== "0x") { return { address: predictedAddress, alreadyDeployed: true, predictedAddress, }; } if (!options.controlledMulticall) { throw new Error(`create2 deployment for ${label} requires controlledMulticall`); } const factoryCallValue = BigInt(prepared.value); const postDeployCalls = (options.postDeployCalls || []).map((call) => ({ target: postDeployTarget(call.target, predictedAddress), allowFailure: false, value: bigintValue(call.value), callData: call.data, })); const calls = [ { target: options.factory, allowFailure: false, value: factoryCallValue, callData: prepared.data, }, ...postDeployCalls, ]; const totalValue = calls.reduce((sum, call) => sum + call.value, 0n); const multicall = await ethers.getContractAt( "Multicall3", options.controlledMulticall, signer, ) as unknown as ControlledMulticall; const tx = totalValue === 0n ? await multicall.aggregate3(calls.map(({ value, ...call }) => call)) : await multicall.aggregate3Value(calls, { value: totalValue }); await tx.wait(); return { address: predictedAddress, txHash: tx.hash, alreadyDeployed: false, predictedAddress, }; }; export const deployCreate2Contract = async ( contractName: string, args: unknown[], options: Create2Options, signer: Signer, ): Promise => { const predicted = await predictCreate2ContractAddress( contractName, args, options, signer, ); return deployCreate2InitCode(contractName, predicted.initCode, options, signer); }; export const predictCreate2ContractAddress = async ( contractName: string, args: unknown[], options: Create2Options, _signer?: Signer, ): Promise => { const artifact = await artifacts.readArtifact(contractName); const contractInterface = new Interface(artifact.abi); const encodedArgs = contractInterface.encodeDeploy(args); const initCode = concat([artifact.bytecode, encodedArgs]); const initCodeHash = keccak256(initCode); const address = getCreate2Address(options.factory, options.salt, initCodeHash); assertCreate2AddressPolicy(contractName, address, options); return { address, initCode, initCodeHash }; };