import type { WorkflowContext } from "../run"; import { requireExistingDeploymentPolicy, resolveExistingDeploymentPolicyAction, resolveValue, WorkflowStep, } from "../lib/parameters"; import { actualContractName, contractInfoKey, findDeploymentAddress, } from "../lib/deployments"; import type { DeploymentChange } from "../lib/deploymentRecords"; import { Create2Options, deployCreate2Contract, prepareControlledMulticallTransaction, prepareCreate2Call, predictCreate2ContractAddress, resolveCreate2Options, sendPreparedControlledMulticallTransaction, } from "../lib/create2"; import { assertReusableDeployment } from "../lib/checkCodeReport"; import { getERC1967Implementation } from "../lib/erc1967"; import { ExternalExecutionRequired } from "../lib/external"; import { recordParameterLockImplementationSalts } from "../lib/lock"; import { ethers } from "hardhat"; import { deployUUPSPlaceholderProxyContract, FirstUpgradeOptions, prepareUUPSCalldataDeployment, prepareFirstUpgrade, ProxyOptions, } from "../lib/proxy"; import { decideControlledMulticallAuthority } from "../lib/authority"; import { impersonateWorkflowSimulationSigner } from "../lib/signer"; const resolveInContext = ( context: WorkflowContext, value: unknown, ): unknown => { return resolveValue(value, { targetId: context.targetId, target: context.target, params: context.targetParams, state: context.state, }); }; const resolveArray = ( context: WorkflowContext, value: unknown, label: string, ): unknown[] => { const resolved = resolveInContext(context, value || []); if (!Array.isArray(resolved)) { throw new Error(`${label} must resolve to an array`); } return resolved; }; const resolveObject = ( context: WorkflowContext, value: unknown, ): Record => { const resolved = resolveInContext(context, value || {}); if (!resolved || typeof resolved !== "object" || Array.isArray(resolved)) { return {}; } return resolved as Record; }; const resolveProxyOptions = ( context: WorkflowContext, step: WorkflowStep, ): { options: ProxyOptions } | undefined => { if (step.proxy === undefined) return undefined; const proxy = resolveObject(context, step.proxy); const initializer = proxy.initializer === false ? false : typeof proxy.initializer === "string" ? proxy.initializer : undefined; const constructorArgs = proxy.constructorArgs === undefined ? undefined : resolveArray(context, proxy.constructorArgs, `Step ${step.id} proxy.constructorArgs`); return { options: { kind: typeof proxy.kind === "string" ? proxy.kind : undefined, initializer, unsafeAllow: Array.isArray(proxy.unsafeAllow) ? proxy.unsafeAllow.map((item) => String(item)) : undefined, constructorArgs, }, }; }; const assertCreate2Options = ( step: WorkflowStep, options: Create2Options | undefined, ): Create2Options => { if (!options) { throw new Error(`Deploy step ${step.id} requires strategy: create2`); } return options; }; const applyExistingDeploymentPolicy = async ( context: WorkflowContext, step: WorkflowStep, saveAs: string, preparedChange: DeploymentChange, create2Options: Create2Options, declaredUpgradeable: boolean, ): Promise<{ completed: boolean; create2Options: Create2Options; existingAddress?: string; redeployReason?: string; }> => { const existing = findDeploymentAddress(context.root, context.config, context.targetId, saveAs); if (!existing) return { completed: false, create2Options }; const policy = requireExistingDeploymentPolicy( context.existingDeploymentPolicies, saveAs, ); const implementationAddress = await getERC1967Implementation(ethers.provider, existing); const action = resolveExistingDeploymentPolicyAction(policy, { instanceId: saveAs, existingAddress: existing, currentSalt: create2Options.salt, upgradeable: declaredUpgradeable || implementationAddress !== undefined, }); if (action.mode === "reuse") { if (!context.releaseArtifactHash) { throw new Error( `Deploy step ${step.id} cannot reuse ${context.targetId}.${saveAs} without an execution artifactHash`, ); } const evidence = assertReusableDeployment(context.root, { network: context.targetId, instanceId: saveAs, address: existing, artifactHash: context.releaseArtifactHash, }); context.deploymentRecorder?.prepare(preparedChange); context.deploymentRecorder?.apply({ ...preparedChange, address: existing, proxyAddress: implementationAddress ? existing : undefined, implementationAddress, reused: true, alreadyDeployed: true, gateTool: { outputPath: "scripts/checkcode/index.json", data: { runId: evidence.runId, artifactHash: evidence.artifactHash }, }, }); setDeploymentState(context, [saveAs], { address: existing, implementationAddress, reused: true, alreadyDeployed: true, }); context.reporter.result("execution", { stepId: step.id, kind: step.kind, contract: preparedChange.actualContract, saveAs, strategy: "reuse", address: existing, implementationAddress, checkCodeRunId: evidence.runId, reused: true, alreadyDeployed: true, }); return { completed: true, create2Options, existingAddress: existing }; } return { completed: false, existingAddress: existing, redeployReason: action.reason, create2Options: { ...create2Options, salt: action.salt, expectedAddress: action.expectedAddress, }, }; }; const stringObjectField = ( source: Record, key: string, ): string | undefined => { const value = source[key]; return typeof value === "string" && value.trim() ? value.trim() : undefined; }; const resolveFirstUpgradeOptions = ( context: WorkflowContext, step: WorkflowStep, ): FirstUpgradeOptions | undefined => { if (step.firstUpgrade === undefined) return undefined; const firstUpgrade = resolveObject(context, step.firstUpgrade); const contractName = stringObjectField(firstUpgrade, "contract"); if (!contractName) { throw new Error(`Deploy step ${step.id} firstUpgrade requires contract`); } const callObject = resolveObject(context, firstUpgrade.call); const fn = stringObjectField(callObject, "fn") || stringObjectField(callObject, "method"); const call = fn ? { fn, args: resolveArray(context, callObject.args || [], `Step ${step.id} firstUpgrade.call.args`), } : undefined; return { contractName, call, constructorArgs: resolveArray( context, firstUpgrade.constructorArgs || [], `Step ${step.id} firstUpgrade.constructorArgs`, ), unsafeAllow: Array.isArray(firstUpgrade.unsafeAllow) ? firstUpgrade.unsafeAllow.map((item) => String(item)) : undefined, }; }; const setDeploymentState = ( context: WorkflowContext, names: string[], value: Record, ): void => { for (const name of Array.from(new Set(names))) { context.state.set(name, value); } }; const runPlannedControlledMulticall = async ( context: WorkflowContext, step: WorkflowStep, factory: string, controlledMulticall: string, calls: Array<{ to: string; value: string; data: string }>, ): Promise<{ external: boolean; transactionId: string; txHash?: string }> => { if (!context.executionPlanner || !context.signer || !context.signerAddress) { throw new Error(`Deploy step ${step.id} requires a planned signer context`); } const prepared = prepareControlledMulticallTransaction( controlledMulticall, calls.map((call) => ({ target: call.to, value: call.value, data: call.data })), ); const multicall = await ethers.getContractAt( "Multicall3", controlledMulticall, context.signer, ); const authority = await decideControlledMulticallAuthority( multicall as unknown as { callers: (caller: string) => Promise; factoryCallers: (factoryAddress: string, caller: string) => Promise; owner?: () => Promise; }, factory, context.target, context.signerAddress, ); const executor = authority.mode === "execute" ? context.signerAddress : authority.executor; if (!executor) throw new Error(`Deploy step ${step.id} cannot resolve its required executor`); const planned = context.executionPlanner.add({ stepId: step.id, executor, to: prepared.target, value: prepared.value, data: prepared.data, operation: 0, }); if (planned.executionAction === "replay") { return { external: planned.route === "external", transactionId: planned.transactionId, txHash: planned.transactionHash, }; } if (planned.route === "external" && context.mode !== "simulate") { context.externalCalls.push({ transactionId: planned.transactionId, stepId: planned.stepId, to: planned.to, value: planned.value, data: planned.data, operation: planned.operation, method: prepared.method, args: prepared.calls, executor: planned.executor, reason: authority.reason, }); return { external: true, transactionId: planned.transactionId }; } const selectedSigner = planned.route === "external" ? await impersonateWorkflowSimulationSigner(planned.executor) : context.signer; const transaction = await sendPreparedControlledMulticallTransaction(prepared, selectedSigner); if (context.mode === "execute" && planned.route === "operator") { context.operatorReceipts.push({ transactionId: planned.transactionId, transactionHash: transaction.hash, }); } return { external: false, transactionId: planned.transactionId, txHash: transaction.hash, }; }; export const runDeployStep = async ( context: WorkflowContext, step: WorkflowStep, ): Promise => { const contractName = String(step.contract); const saveAs = typeof step.saveAs === "string" ? step.saveAs.trim() : ""; if (!saveAs) { throw new Error(`Deploy step ${step.id} requires saveAs`); } const create2Options = assertCreate2Options(step, resolveCreate2Options(context, step)); const proxy = resolveProxyOptions(context, step); const recordsDeployment = context.mode === "execute"; const recorder = recordsDeployment ? context.deploymentRecorder : undefined; if (recordsDeployment && !recorder) { throw new Error(`Deploy step ${step.id} requires an execution deployment recorder`); } if (proxy) { const firstUpgrade = resolveFirstUpgradeOptions(context, step); if (step.upgradeableStrategy !== "uups-placeholder-proxy" || !firstUpgrade) { throw new Error(`Deploy step ${step.id} requires firstUpgrade for uups-placeholder-proxy`); } if (proxy.options.initializer !== false) { throw new Error(`Deploy step ${step.id} must use proxy.initializer: false for uups-placeholder-proxy`); } if (!create2Options.controlledMulticall) { throw new Error(`Deploy step ${step.id} requires controlledMulticall for atomic firstUpgrade`); } const actualContract = actualContractName(firstUpgrade.contractName); const registryKey = contractInfoKey(saveAs, firstUpgrade.contractName); const preparedChange: DeploymentChange = { stepId: step.id, kind: "deploy", instanceId: saveAs, actualContract, contractInfoKey: registryKey, strategy: "create2", proxyKind: proxy.options.kind || "uups", factory: create2Options.factory, multicall: create2Options.controlledMulticall, salt: create2Options.salt, expectedAddress: create2Options.expectedAddress, leadingZeroBytes: create2Options.leadingZeroBytes, placeholderImplementationAddress: create2Options.uupsPlaceholderImplementation, }; const existingResolution = await applyExistingDeploymentPolicy( context, step, saveAs, preparedChange, create2Options, true, ); if (existingResolution.completed) return; recorder?.prepare(preparedChange); if (context.executionPlanner) { const prepared = await prepareUUPSCalldataDeployment({ executionId: context.executionId, stepId: step.id, contractName: firstUpgrade.contractName, firstUpgrade, proxy: proxy.options, create2: create2Options, }); const plannedChange: DeploymentChange = { ...preparedChange, implementationAddress: prepared.implementationAddress, implementationSalt: prepared.implementationSalt, predictedAddress: prepared.proxyAddress, }; if (context.parameterLockExecutionId) { recordParameterLockImplementationSalts( context.taskDir, context.parameterLockExecutionId, { [step.id + ".implementation"]: prepared.implementationSalt }, ); } recorder?.prepare(plannedChange); const execution = await runPlannedControlledMulticall( context, step, create2Options.factory, create2Options.controlledMulticall, prepared.calls, ); if (context.mode === "execute") { if (execution.external) { recorder?.stageAddressCandidate(plannedChange, prepared.proxyAddress); } else { recorder?.applyAddress({ ...plannedChange, address: prepared.proxyAddress, proxyAddress: prepared.proxyAddress, txHash: execution.txHash, }, prepared.proxyAddress); } } setDeploymentState(context, [saveAs], { address: prepared.proxyAddress, proxyAddress: prepared.proxyAddress, implementationAddress: prepared.implementationAddress, implementationSalt: prepared.implementationSalt, txHash: execution.txHash, external: execution.external, }); context.reporter.result("execution", { stepId: step.id, kind: step.kind, mode: execution.external ? "external" : context.mode, transactionId: execution.transactionId, contract: contractName, actualContract, saveAs, contractInfoKey: registryKey, strategy: "create2", proxy: proxy.options.kind || "uups", address: prepared.proxyAddress, implementationAddress: prepared.implementationAddress, firstUpgradeImplementationAddress: prepared.implementationAddress, placeholderImplementationAddress: create2Options.uupsPlaceholderImplementation, predictedAddress: prepared.proxyAddress, reused: false, alreadyDeployed: false, txHash: execution.txHash, }); return; } if (!context.signer) { throw new Error(`Deploy step ${step.id} requires a signer`); } const firstUpgradePrepared = await prepareFirstUpgrade( firstUpgrade, proxy.options, context.signer, ); const implementationPreparedChange: DeploymentChange = { ...preparedChange, implementationAddress: firstUpgradePrepared.implementationAddress, preparedImplementationAddress: firstUpgradePrepared.implementationAddress, implementationTxHash: firstUpgradePrepared.implementationTxHash, }; recorder?.prepare(implementationPreparedChange); const proxyCreate2Options = { ...create2Options, postDeployCalls: [ ...(create2Options.postDeployCalls || []), ...firstUpgradePrepared.postDeployCalls, ], }; const deployment = await deployUUPSPlaceholderProxyContract( proxyCreate2Options, context.signer, ); const activeImplementationAddress = deployment.alreadyDeployed ? deployment.implementationAddress : firstUpgradePrepared.implementationAddress; if (context.mode === "execute") { recorder?.applyAddress( { ...implementationPreparedChange, address: deployment.proxyAddress, proxyAddress: deployment.proxyAddress, implementationAddress: activeImplementationAddress, predictedAddress: deployment.predictedAddress, txHash: deployment.txHash, reused: deployment.alreadyDeployed === true, alreadyDeployed: deployment.alreadyDeployed === true, }, deployment.proxyAddress, ); } setDeploymentState(context, [saveAs], { address: deployment.proxyAddress, implementationAddress: activeImplementationAddress, firstUpgradeImplementationAddress: firstUpgradePrepared.implementationAddress, placeholderImplementationAddress: create2Options.uupsPlaceholderImplementation, txHash: deployment.txHash, }); context.reporter.result("execution", { stepId: step.id, kind: step.kind, contract: contractName, actualContract, saveAs, contractInfoKey: registryKey, strategy: "create2", proxy: proxy.options.kind || "uups", address: deployment.proxyAddress, implementationAddress: activeImplementationAddress, firstUpgradeImplementationAddress: firstUpgradePrepared.implementationAddress, placeholderImplementationAddress: create2Options.uupsPlaceholderImplementation, predictedAddress: deployment.predictedAddress, reused: deployment.alreadyDeployed === true, alreadyDeployed: deployment.alreadyDeployed === true, txHash: deployment.txHash, }); return; } const args = resolveArray(context, step.args || [], `Step ${step.id} args`); const actualContract = actualContractName(contractName); const registryKey = contractInfoKey(saveAs, contractName); let preparedChange: DeploymentChange = { stepId: step.id, kind: "deploy", instanceId: saveAs, actualContract, contractInfoKey: registryKey, strategy: "create2", factory: create2Options.factory, multicall: create2Options.controlledMulticall, salt: create2Options.salt, expectedAddress: create2Options.expectedAddress, leadingZeroBytes: create2Options.leadingZeroBytes, }; const existingResolution = await applyExistingDeploymentPolicy( context, step, saveAs, preparedChange, create2Options, false, ); if (existingResolution.completed) return; const effectiveCreate2Options = existingResolution.create2Options; preparedChange = { ...preparedChange, salt: effectiveCreate2Options.salt, expectedAddress: effectiveCreate2Options.expectedAddress, previousAddress: existingResolution.existingAddress, redeployReason: existingResolution.redeployReason, }; recorder?.prepare(preparedChange); if (context.executionPlanner) { const predicted = await predictCreate2ContractAddress( contractName, args, effectiveCreate2Options, ); const prepared = prepareCreate2Call( contractName, predicted.initCode, effectiveCreate2Options, ); if (!effectiveCreate2Options.controlledMulticall) { throw new Error(`Deploy step ${step.id} requires controlledMulticall`); } const plannedChange: DeploymentChange = { ...preparedChange, predictedAddress: predicted.address, }; const execution = await runPlannedControlledMulticall( context, step, effectiveCreate2Options.factory, effectiveCreate2Options.controlledMulticall, [{ to: prepared.target, value: prepared.value, data: prepared.data }], ); if (context.mode === "execute") { if (execution.external) { recorder?.stageAddressCandidate(plannedChange, predicted.address); } else { recorder?.applyAddress({ ...plannedChange, address: predicted.address, txHash: execution.txHash, }, predicted.address); } } context.state.set(saveAs, { address: predicted.address, predictedAddress: predicted.address, txHash: execution.txHash, external: execution.external, }); context.reporter.result("execution", { stepId: step.id, kind: step.kind, mode: execution.external ? "external" : context.mode, contract: contractName, actualContract, saveAs, contractInfoKey: registryKey, strategy: "create2", address: predicted.address, predictedAddress: predicted.address, reused: false, alreadyDeployed: false, txHash: execution.txHash, }); return; } if (existingResolution.existingAddress) { const predicted = await predictCreate2ContractAddress( contractName, args, effectiveCreate2Options, ); if ( existingResolution.existingAddress && predicted.address.toLowerCase() === existingResolution.existingAddress.toLowerCase() ) { throw new Error(`Deploy step ${step.id} redeploy predicted the existing address`); } recorder?.stageAddressCandidate( { ...preparedChange, predictedAddress: predicted.address, }, predicted.address, ); } if (!context.signer) { throw new Error(`Deploy step ${step.id} requires a signer`); } const deployment = await deployCreate2Contract( contractName, args, effectiveCreate2Options, context.signer, ); if (context.mode === "execute") { recorder?.applyAddress( { ...preparedChange, address: deployment.address, predictedAddress: deployment.predictedAddress, txHash: deployment.txHash, reused: deployment.alreadyDeployed, alreadyDeployed: deployment.alreadyDeployed, }, deployment.address, ); } context.state.set(saveAs, { address: deployment.address, txHash: deployment.txHash, predictedAddress: deployment.predictedAddress, alreadyDeployed: deployment.alreadyDeployed, }); context.reporter.result("execution", { stepId: step.id, kind: step.kind, contract: contractName, actualContract, saveAs, contractInfoKey: registryKey, strategy: "create2", address: deployment.address, predictedAddress: deployment.predictedAddress, alreadyDeployed: deployment.alreadyDeployed, txHash: deployment.txHash, }); };