# {{PROJECT_NAME}}

Minimal Hardhat Counter project with a task-based deployment workflow.

## Install

```bash
npm install
```

`npm install` runs `patch-package` and applies the bundled
`@openzeppelin/hardhat-upgrades` patch. Proxy deploy steps that use
`create2Factory` depend on this patch for CREATE2, Multicall, and optional
ownership transfer support.

The patch does not write `contractInfo.json`. It only changes how OpenZeppelin
deploys proxies and implementations. The workflow runtime writes the
business-facing address to a pending registry candidate after a deploy step
succeeds. `workflow:commit-contract-info` promotes verified candidates in one
atomic registry commit; CheckCode is read-only. Implementation, transaction,
CREATE2, and gate-tool evidence stays in the execution record and
`deploymentInfo.json`.

## Hardhat And Foundry

The template supports both toolchains after `npm install`:

```bash
npm run compile
npm run compile:forge
npm test
npm run test:forge
npm run test:all
```

`foundry.toml` uses Solidity 0.8.28, Cancun EVM, IR compilation, and optimizer
settings aligned with `hardhat.config.ts`. Normal project initialization installs
`forge-std` v1.16.2 into `lib/forge-std` without creating a Git submodule. If dependency
installation was skipped with `--no-install`, run `npm run install:foundry`.

Compiler, contract, and build dependencies use exact versions. The
`gate-evm-tools` workflow utility uses the compatible `^1.0.23` release range.
Keep the lockfile generated by
the selected package manager so each initialized project remains reproducible.
The bundled contracts and Foundry tests pin `pragma solidity 0.8.28`.

## Wallet Credential

The project uses one wallet credential for both native private keys and custody
wallets. Keep it only in the untracked project `.env` or process environment:

```dotenv
# A normal private key remains supported without script changes.
PRIVATE_KEY=0x...

# Or use the pseudo-private-key form copied from the custody service.
PRIVATE_KEY=custody:<ProjectSecret>:<WalletAddress>
CUSTODY_API_BASE_URL=https://custody.example.com
```

`CUSTODY_API_BASE_URL` must be an HTTP(S) base URL without embedded
credentials, query parameters, or fragments.

`WALLET_CREDENTIAL` is an optional alias for `PRIVATE_KEY`. If both variables
are configured, they must contain the same value. `hardhat.config.ts` loads the
custody plugin after the ethers and OpenZeppelin plugins. A real private key is
passed through native `networks.accounts`; a custody credential leaves native
accounts empty and supplies the remote signer through Hardhat and the workflow
signer factory. Existing deploy scripts that use `ethers.getSigners()`, contract
factories, or `upgrades.deployProxy()` therefore do not need custody-specific
branches. The bundled OpenZeppelin patch remains required for its independent
CREATE2, Multicall, and proxy deployment behavior.

SmartFlow derives the public signer address from the selected `.env` credential
when it creates a task and writes that public address to
`task.yaml.deploymentSignerAddress`. That task value is a frozen expected
identity for simulation, audit, and execution locking; it is not a second
secret or a dynamically selected account. Execute restores the signer from the
current `.env` credential and rejects it before broadcast unless its address
matches the frozen `deploymentSignerAddress`. Project Secret and pseudo-private
key values must never be written to task YAML, result evidence, logs, or Git.

## Workflow Model

SmartFlow should create real deployment tasks under `scripts/tasks/<taskId>/`.
This template only ships an example task at `scripts/tasks/_examples/counter-rollout`.

SmartFlow's developer lane selects one `developmentNetwork` and generates:

- `manifest.yaml`
- `parameters.schema.json`
- `parameters.yaml`
- `release.json`
- optional task-local runnable scripts only for `kind: custom`
- `docs/developer-handoff.md`

The development deploy lane executes the complete manifest on that network in
one process, verifies the business `view`/`check` suffix with the same execution
ID, and commits the verified candidate before CheckCode. Release verification
then adds `releaseTargetNetworks`, derives each target's parameters, and runs one
complete fork simulation per target. The administrator selects only a merged,
release-qualified task and creates an immutable `parameters.lock.json` plus a
per-transaction authority plan during final simulation. Execute must reuse that
exact lock and plan.

## Run The Example

The example task is a workflow shape reference. Infrastructure addresses are project config:
runtime deployment, simulation, and execute-time locking read the CREATE2
factory, blank UUPS placeholder implementation, and controlled multicall from
the selected target in `config.yaml`.
SmartFlow does not ship an infrastructure address registry.
Before running against a target, configure that target's deployment
infrastructure in `config.yaml`:

```yaml
gtDev:
  environment: development
  admin: ""
  create2:
    factory: "0x2370B48f20FFd75A46B14A9888B3E79677eFC28c"
    leadingZeroBytes: 4
    caller: "0x0000000000000000000000000000000000000000"
  uups:
    placeholderImplementation: "0x000000009F21E08bE874b78F40A6d038587E0C54"
  multicall:
    controlledMulticall: "0x00000000bD2f006A24b6d99baaD1323D5809948f"
```

```bash
npm run workflow:example
```

For a SmartFlow-generated task, development simulation creates the immutable
execution plan without creating `parameters.lock.json`:

```bash
npm run workflow -- \
  --task 20260608-counter-rollout \
  --network hardhat \
  --mode simulate \
  --target gtDev \
  --releaseKind development \
  --executionId development-gtDev-1
```

`npm run workflow` is a TypeScript wrapper around `hardhat run`; it forwards
`--network` to Hardhat and passes workflow arguments such as `--task`, `--mode`,
`--target`, `--step`, `--from`, and `--simulationSigner` to the runtime.
For `--mode execute` on real configured networks, the runtime fails early when
neither `WALLET_CREDENTIAL` nor `PRIVATE_KEY` is present in the project `.env`
or process environment.

Development and Admin `--useLock` simulation each run as one selected public EVM
address. A normal development simulation may read `deploymentSignerAddress`
from the task's `task.yaml`; an explicit `--simulationSigner` must match it. An
Admin final simulation using `--useLock` never falls back to task metadata and
requires the argument explicitly. Both modes ignore `simulation-callers.yaml`.
No private key is required to impersonate a public address on the Hardhat fork.

The first real execution is limited to the task's development network. It must
reuse the simulation execution ID and must not use `--useLock`:

```bash
npm run workflow -- \
  --task 20260608-counter-rollout \
  --network gtDev \
  --mode execute \
  --releaseKind development \
  --executionId development-gtDev-1
```

Run the business verification suffix with that same execution ID, then commit
the verified development candidate before CheckCode:

```bash
npm run workflow -- \
  --task 20260608-counter-rollout \
  --network gtDev \
  --mode verify \
  --executionId development-gtDev-1 \
  --from <first-view-or-check-step>

npm run workflow:commit-contract-info -- \
  --task 20260608-counter-rollout \
  --commitId development-gtDev-1 \
  --selection gtDev=development-gtDev-1
```

After development verify, commit the development registry and run CheckCode
against the formal registry. The live `ReleaseSimulationCallersInput` node then
creates `scripts/tasks/<deployment-id>/simulation-callers.yaml`, after the
release target set and parameters are known and immediately before target
qualification. It contains one required default caller per target and optional
per-Manifest-step overrides; it contains public addresses only.

Each full release target qualification runs without a release-target simulation
signer argument and automatically loads that file. It must execute the complete
Manifest in order, on one shared fork per target:

```bash
npm run workflow -- \
  --task 20260608-counter-rollout \
  --network hardhat \
  --mode simulate \
  --target gtProd \
  --executionId qualification-gtProd-1
```

There is no `--simulationSigner` compatibility fallback for release-target
qualification. Development simulation and the existing Admin workflow ignore
`simulation-callers.yaml` and retain their signer, lock, plan, and calldata
behavior.

After qualification, the Admin final simulation still writes and consumes the
immutable execution lock, so provide its operator, execution ID, source
SuperTask, and merged commit:

```bash
npm run workflow -- \
  --task 20260608-counter-rollout \
  --network hardhat \
  --mode simulate \
  --target gtProd \
  --simulationSigner <operator-address> \
  --releaseKind admin_release \
  --useLock \
  --executionId release-gtProd-1 \
  --sourceSuperTaskId <supertask-id> \
  --mergeCommit <40-char-commit>
```

Simulation mode uses the selected target's RPC URL from `hardhat.config.ts`,
runs the task on a Hardhat fork, and exclusively claims
`results/<target>/<executionId>/simulation/` before config, manifest, or step
preflight. It immediately creates `execution.json`, `view.json`, and
`verification.json` in that directory with empty `steps`, so an early failure
cannot expose evidence from an older run. Target and execution ID are validated
as individual safe path segments; dots inside one segment cannot alias another
target or execution.

Every successful simulation appends a terminal
`workflow-simulation-completed` step with the exact ordered step IDs that ran,
plus the normalized selected `signerAddress`,
to `results/<target>/<executionId>/simulation/execution.json`. There is no
mutable latest result file. The claimed `(target, executionId, mode)` namespace
cannot be reused or overwritten, whether the run succeeds or fails; retries must
use a new execution ID. Distinct execution IDs therefore write independent files.

`status.json` records the successful simulation's confined relative path as
`simulationResult`. Release Verify must copy the exact returned status evidence
path into `release.json` under the target's `simulationResult`; it must not derive
or rename the path. A failed current run clears any older top-level
`simulationResult` from global status. Admin simulation with `--useLock` records
the same scoped path in `parameters.lock.json.finalSimulation.result`, with the
lock-success update as the final fallible side effect. Failed or partial
simulations retain their scoped partial evidence but do not append the terminal
marker and cannot make a lock executable. Simulation never writes `contractInfo.json`.

The shared execution-scoped naming scheme is
`results/<target>/<executionId>/<mode>/<result>.json`, where simulate uses the
`simulation` mode directory. Separate mode claims allow Admin simulation,
execute, and verify to reuse one locked `executionId` without sharing result
files. Examples include `results/gtProd/release-gtProd-1/execute/execution.json`
and `results/gtProd/release-gtProd-1/verify/verification.json`.
A normal development simulation does not write `parameters.lock.json`; the final
release simulation shown above deliberately creates and marks the immutable lock
as simulated while retaining its release fingerprint evidence.
The lock binds `operator`, `target.signer`, and terminal simulation
`signerAddress` to the same identity. Final simulation resolves authority for
every state-changing transaction. It executes an operator route with the
selected signer and impersonates only that transaction's required external
executor on the fork. A development plan rejects every external route. An Admin
plan may interleave contiguous operator and external segments in manifest order;
the immutable transaction order and executor of every route remain fixed.

The administrator executes the locked plan with the same `executionId`:

```bash
npm run workflow -- --task 20260608-counter-rollout --network gtProd --mode execute --useLock --executionId release-gtProd-1
```

Execute replays the completed plan prefix without rebroadcasting it, sends the
next contiguous operator segment, and stops at the following external segment.
It cumulatively appends operator receipts and external segments to the canonical
`calldata/<network>/<executionId>/external-execution.json` package, leaving the
record in `waiting_external`. It never emits the old per-step or batch format.
After the external executor completes the current segment, reconcile finalized
chain state in bounded batches:

```bash
npm run workflow:reconcile-external -- --task 20260608-counter-rollout --network gtProd --executionId release-gtProd-1
```

Repeat reconciliation while it reports `waiting_external` or
`waiting_finality`. When it reports `waiting_operator`, rerun the same locked
execute command to advance the next operator segment, then reconcile again.
Only `not_required` or `reconciled` permits business verify.
SmartFlow's Admin confirmation action only records the human confirmation and
advances the workflow. The following deterministic network runner performs
reconciliation and then the manifest's read-only `view`/`check` suffix for each
network, with bounded concurrency across networks. It cannot force completion
or bypass a reverted, missing, unfinalized, or reorged transaction. A release
may contain multiple transactions, so verification is bound to the execution ID
rather than one transaction hash.

```bash
npm run workflow -- --task 20260608-counter-rollout --network gtProd --mode verify --executionId <same-executionId> --useLock --from <first-view-or-check-step>
```

`<same-executionId>` is the immutable lock ID used by final simulation and
execute. Verify uses its locked resolved parameters and the same pending
candidate addresses. A successful view/check verification updates
`verifyStatus` without changing the network's deployment status. After every
selected network is reconciled and verified, commit all selections atomically:

```bash
npm run workflow:commit-contract-info -- \
  --task 20260608-counter-rollout \
  --commitId admin-release-1 \
  --selection gtProd=<same-executionId>
```

The commit writes `deployed_pending_code_audit` before CheckCode. SmartFlow runs
this commit automatically after its Admin verify node; there is no independent
registry-write approval button.

## CheckCode

CheckCode is a signer-free project operation. It compiles the current sources,
checks the formal `contractInfo.json` with gate-tool, reads proxy classification
from `deploymentInfo.json`, and conditionally syncs only stale UUPS target
manifests. It never reads, promotes, or writes a registry candidate.

For a SmartFlow SuperTask, create immutable discovery evidence before the
runner starts:

```bash
npm run workflow:checkcode-plan -- \
  --runId <checkcode-run-id> \
  --artifactHash <release-artifact-hash> \
  --deploymentId <deployment-id> \
  --sourceSuperTaskId <supertask-id> \
  --developmentNetwork <development-network> \
  --executionId <registry-execution-id> \
  --manifestDir <configured-manifest-dir> \
  --contract <saveAs-or-contract>
```

The four SuperTask identity arguments are all-or-nothing, and the plan may
target only `developmentNetwork`. This writes the canonical plan to
`scripts/tasks/<deployment-id>/docs/checkcode-plan.md`; do not hand-edit it.

```bash
npm run workflow:checkcode -- \
  --runId <checkcode-run-id> \
  --artifactHash <release-artifact-hash> \
  --network gtDev \
  --contract Counter
```

Repeat `--network <network>` or `--contract <saveAs-or-contract>` to narrow the
scope. `--runId <id>` selects the report directory under
`scripts/checkcode/runs/<id>/`; otherwise the runner creates one. The same entry
is available through `npm run workflow -- --checkcode ...`. The canonical report
is `version: 1`; each entry records `manifestDir`, before/after hashes, and one of
`not_required`, `already_current`, `synced`, `proxy_kind_unknown`, or `failed`.
CheckCode does not read `PRIVATE_KEY` and is independent of a single deployment
transaction hash. CheckCode proves runtime bytecode and proxy implementation
consistency. A business check in the task manifest separately proves
initialization, ownership, roles, limits, and other configured state; neither
result substitutes for the other.

After the selected canonical report succeeds, finalize a development task with:

```bash
npm run workflow:checkcode-finalize -- \
  --deploymentId <deployment-id> \
  --sourceSuperTaskId <supertask-id> \
  --developmentNetwork <development-network> \
  --executionId <registry-execution-id> \
  --runId <checkcode-run-id>
```

Finalize validates the report and current task identity before updating
`status.json`, `handoff.json`, and `docs/checkcode-report.md`. It keeps
`releaseTargetNetworks` empty during development; Admin Verify selects release
targets later. The formal `contractInfo.json` is byte-for-byte read-only during
CheckCode.

## Developer Static Validation

Generate the Developer gate evidence with the template runtime instead of
assembling the report manually:

```bash
npm run workflow:static-validate -- --task <deployment-id>
```

The command runs `npm run compile`, `npx tsc --noEmit`, the project test command,
and parses every YAML/JSON document in the task directory. It writes
`scripts/tasks/<deployment-id>/docs/static-validation.md` and reports `failed`
if any required check fails.

Workflow subprocesses prefer the checked-out project's executable under
`node_modules/.bin`; when it is absent they use `npx --no-install`, so task runs
never install a different Hardhat, ts-node, or gate-tool version implicitly.
During execute, the signer restored from the selected wallet credential must
match the optional `task.yaml.deploymentSignerAddress` before any workflow step
or broadcast runs.

## Release Verification Finalize

After every selected release target has complete parameters and successful
canonical simulation evidence, finalize the Verifier handoff with one runtime
command:

```bash
npm run workflow:release-finalize -- \
  --task <deployment-id> \
  --network <release-target-network>
```

Repeat `--network` for every release target. The command requires the exact
target set from `task.yaml`, `release.json`, and `simulation-callers.yaml`. It
re-reads the exact Manifest bytes, recomputes each caller network hash, and
validates the ordered terminal caller map plus every step annotation across the
execution, view, and verification results. Old signer-only target evidence has
no compatibility fallback and is stale. Only after all targets pass does it
write canonical release-ready state to
`release.json`, `status.json`, `handoff.json`, and
`docs/release-verification.md`. If any target is invalid, those formal artifacts
remain unchanged. This step does not create an Admin parameter lock, deploy to a
release network, or modify `contractInfo.json`.

For each selected target, CheckCode resolves one exact OpenZeppelin manifest
file from the configured `manifestDir` and the RPC chain ID. It never combines
proxy, implementation, or storage-layout evidence from sibling JSON files. The
canonical report records that relative file as `manifestFile`.

SmartFlow may select multiple simulation target networks, but it should call the
runtime once per target. If a simulation fails, `status.json` includes
`errorType` and `retryable`. Retry only when `retryable: true`, which is reserved
for transient RPC or network failures. Do not retry deterministic failures such
as `check_failed`, `parameter_error`, `config_error`, `contract_error`, or
`execution_reverted`; those should be sent back to the developer worktree for a
script or parameter fix.

## Import Legacy Deployment State

Use `workflow:import-state` after migrating legacy contracts, artifacts, and the
two OpenZeppelin state directories. It builds canonical `contractInfo.json` and
`deploymentInfo.json` records from a reviewed plan and writes immutable evidence
under `scripts/migrations/<migrationId>/`.

Development and production are release stages, not different chains. For
example, `bscDev` and `bscProd` may use the same chainId (`56`), the same mainnet
chain, and the same RPC route while identifying two independent contract address
sets. Confirm them separately: `bscDev` uses `.openzeppelin.dev`, while
`bscProd` uses `.openzeppelin`. Never merge those manifests or infer the stage
from the chain ID.

Create a project-local plan such as
`scripts/migrations/legacy-bsc-state.plan.json`:

```json
{
  "version": 1,
  "migrationId": "legacy-bsc-state",
  "sourceProject": "legacy-vault",
  "networks": {
    "bscDev": {
      "sourceNetwork": "development",
      "releaseStage": "development",
      "chainId": 56,
      "manifestDir": ".openzeppelin.dev",
      "contracts": [
        {
          "instanceId": "vault",
          "contractInfoKey": "vault:Vault",
          "address": "0x1111111111111111111111111111111111111111",
          "actualContract": "Vault",
          "proxyKind": "uups",
          "implementationAddress": "0x2222222222222222222222222222222222222222"
        }
      ]
    },
    "bscProd": {
      "sourceNetwork": "production",
      "releaseStage": "production",
      "chainId": 56,
      "manifestDir": ".openzeppelin",
      "contracts": [
        {
          "instanceId": "vault",
          "contractInfoKey": "vault:Vault",
          "address": "0x3333333333333333333333333333333333333333",
          "actualContract": "Vault",
          "proxyKind": "uups",
          "implementationAddress": "0x4444444444444444444444444444444444444444"
        }
      ]
    }
  }
}
```

First print the plan-bound confirmation tokens. Review the network, release
stage, manifest directory, proxy address, implementation address, and chain ID
before copying each token:

```bash
npm run workflow:import-state -- \
  --plan scripts/migrations/legacy-bsc-state.plan.json \
  --print-confirmations
```

Then provide every token independently:

```bash
npm run workflow:import-state -- \
  --plan scripts/migrations/legacy-bsc-state.plan.json \
  --confirm 'migration-v1:legacy-bsc-state:bscDev:development:.openzeppelin.dev:<plan-bound-hash>' \
  --confirm 'migration-v1:legacy-bsc-state:bscProd:production:.openzeppelin:<plan-bound-hash>'
```

The importer verifies target configuration, chain ID, deployed code, artifact,
ERC-1967 proxy kind and implementation, then runs gate-tool bytecode checking
before committing any formal state. A zero gate-tool exit is insufficient by
itself: the bytecode report must exist as a regular file, contain valid JSON,
and retain the hash recorded in the evidence.

Every output is durably staged before a transaction journal is written.
`contractInfo.json`, `deploymentInfo.json`, and the bytecode report are renamed
first; immutable `state-import.json` evidence is renamed last as the commit
marker. Its `integrityHash` binds the complete evidence document. On the next
run, crash recovery reads the journal and either restores every exact pre-import
byte stream or confirms the fully committed hashes before deleting staged
files. Retrying the same completed migration is therefore idempotent, while a
missing/tampered report, evidence field, symlinked path component, or mixed
formal state fails closed.

This is a signer-free, non-broadcast migration operation. Confirmation printing
finishes before Hardhat loads; an actual import sets `GATE_MIGRATION_READ_ONLY`
so Hardhat config exposes no signer accounts. It never reads a private key and
must not broadcast a transaction to mainnet. Acceptance uses
compiler/tests, this import audit, `workflow:checkcode`, and dry-run or fork
simulation for legacy script entrypoints. A real mainnet transaction is never an
acceptance requirement.

## Configuration

`hardhat.config.ts` owns RPC route selection, public fallbacks, chain ids, and
signer setup. The tracked `rpc.env` file provides the shared `BASE_RPC_URI` for
the project; SmartFlow may populate its initially empty value once when it
creates a new internal project. `.env` remains untracked and only provides
`WALLET_CREDENTIAL`/`PRIVATE_KEY`, `CUSTODY_API_BASE_URL`, or other
developer-specific secrets.

Each remote network declares `route`, `publicFallback`, and `useBaseRpc`
explicitly. When `useBaseRpc` is `true` and `BASE_RPC_URI` is non-empty, the
resolved URL is `<BASE_RPC_URI>/<route>/` with exactly one separator and a
trailing slash. Otherwise Hardhat uses that network's public fallback. A
`BASE_RPC_URI` already present in the process environment overrides
`rpc.env`.

The internal Base RPC routes enabled by the template are `eth`, `bsc`,
`optimism`, `arbitrum`, `zksync`, `base`, `avaxc`, `blast`, `polygon`,
`world`, `gatelayer`, `eni`, `bera`, `sonic`, and `linea`. Networks without an
internal route, such as Fantom and Sepolia, explicitly disable Base RPC and
always use their public fallback.

`config.yaml` owns long-lived business parameters and deployment
infrastructure addresses. Top-level keys starting with `_` are shared profiles,
not Hardhat networks; concrete network keys inherit those profiles:

```yaml
_profiles:
  create2Base: &create2Base
    leadingZeroBytes: 4
    caller: "0x0000000000000000000000000000000000000000"

  create2Dev: &create2Dev
    <<: *create2Base
    factory: "0xYourDevCreate2Factory"

  create2Prod: &create2Prod
    <<: *create2Base
    factory: "0xYourProdCreate2Factory"

  uups: &defaultUups
    placeholderImplementation: "0xYourBlankUUPSPlaceholderImplementation"

  multicall: &defaultMulticall
    controlledMulticall: "0xYourControlledMulticall"

  dev: &devProfile
    environment: development
    admin: ""
    create2: *create2Dev
    uups: *defaultUups
    multicall: *defaultMulticall

  prod: &prodProfile
    environment: production
    admin: "0xYourSafeOrAdminAddress"
    create2: *create2Prod
    uups: *defaultUups
    multicall: *defaultMulticall

ethDev:
  <<: *devProfile

ethProd:
  <<: *prodProfile
```

Every selectable target has an explicit `environment` inherited from its
profile: `local`, `development`, or `production`. Consumers should classify a
parsed target from this field. The workflow does not infer the environment from
the network name, alias, RPC URL, or chain ID.

Every target also declares an explicit `openzeppelin.manifestDir`. Local tooling
uses `.openzeppelin.local`, development networks use `.openzeppelin.dev`,
Sepolia uses `.openzeppelin.sepolia`, and production networks use
`.openzeppelin`. Non-production targets must never fall back to the production
directory. The CLI exports `MANIFEST_DEFAULT_DIR` before Hardhat loads the
OpenZeppelin plugin. Multi-network CheckCode starts one isolated sync child per
target so one target cannot mutate another target's manifest.

`admin: ""` or `admin: "deployer"` resolves to the current signer. The workflow
does not use a default fallback; if `--network ethProd` is selected, `config.yaml`
must contain an `ethProd` entry.

For setup or upgrade calls, final simulation checks the contract's `owner()` or
`admin()` and controlled-multicall permissions. It records the exact operator or
external executor per transaction. Real execute enforces every locked
`to/value/data/operation/executor` tuple, advances one ordered operator segment,
and appends the following external segment without changing its executor. A
`view`/`check` step runs only after external reconciliation completes.

## Workflow Steps

Use standard workflow steps whenever the action can be described declaratively:

- `deploy`: deploy a contract and stage its address in a pending registry candidate.
- `call`: plan and execute state-changing contract calls under per-transaction authority.
- `view`: read contract state and record the result.
- `check`: read contract state and fail when it does not match the expected value.
- `upgrade`: upgrade execution slot for proxy projects.

For `call`, `view`, and `check`, `contract` is the ABI or fully qualified
contract name. If the deployed instance is recorded under a different `saveAs`,
set `deployment` to that instance ID. The runtime resolves both an exact
contract key and a unique `<saveAs>:<actualContract>` alias:

```yaml
- id: call-core-vault-cap
  kind: call
  contract: contracts/core/Vault.sol:Vault
  deployment: CoreVault
  calls:
    - method: setCap
      args:
        - $param.cap
```

Every `deploy` step must use `strategy: create2`, define a `saveAs` value, and
`saveAs` must be unique within the task. It is the deployment instance id used
by `contractInfo.json` and by network-scoped salts. It must not contain `:` because
that separator belongs to the gate-tool alias format. If `saveAs` equals the
actual Solidity contract name, the registry key is just the contract name.
Otherwise the key is `<saveAs>:<actualContract>`. Fully qualified artifact names
are reduced to their final Solidity contract name for this key.

If two Solidity contracts have the same name, use a fully qualified contract
name and different `saveAs` values:

```yaml
- id: deploy-core-vault
  kind: deploy
  contract: contracts/core/Vault.sol:Vault
  saveAs: CoreVault

- id: deploy-reward-vault
  kind: deploy
  contract: contracts/reward/Vault.sol:Vault
  saveAs: RewardVault
```

Those steps produce `CoreVault:Vault` and `RewardVault:Vault`. Later steps use
`deployment: CoreVault` or `deployment: RewardVault`; they do not need to repeat
the suffix.

CREATE2 deploys keep salt values in each network's task parameters under
`params.<network>.salts.<saveAs>`:

- A development salt is reused only when the target factory, initCodeHash, and leading-zero policy still produce the recorded expected address. Otherwise SmartFlow mines a target-specific salt.
- different `saveAs` values must not share one salt value; each contract or deployment instance gets its own salt group.
- For UUPS contracts, the canonical business address is the proxy. The business task only needs a network-scoped proxy salt. Later business implementation addresses are ordinary upgrade outputs and do not use vanity salts.

SmartFlow is the CREATE2 salt source. The developer lane should use the built-in
CREATE2 resolver/miner, write the requested artifacts to
`scripts/tasks/<taskId>/create2-requests.json`, and write confirmed results to
`scripts/tasks/<taskId>/create2-results.json`. Runtime execution must only
consume the locked salt values already written to `parameters.yaml`; it must not
mine new salts, call an external salt service, or silently replace a salt during
simulation or execution.

For non-upgradeable contracts, SmartFlow mines the salt from the standard
CREATE2 inputs:

```text
predicted = last20bytes(keccak256(0xff ++ factory ++ salt ++ initCodeHash))
initCodeHash = keccak256(contract bytecode ++ abi.encode(constructorArgs))
```

`leadingZeroBytes: 4` means the mined address must start with
`0x00000000`. The `caller` recorded in SmartFlow miner requests is metadata for
the mining session/result; it is not part of the CREATE2 address formula.

```yaml
- id: deploy-counter
  kind: deploy
  contract: Counter
  saveAs: Counter
  strategy: create2
  create2:
    factory: $target.create2.factory
    salt: $param.salts.Counter.deploySalt
    expectedAddress: $param.salts.Counter.expectedAddress
    leadingZeroBytes: $target.create2.leadingZeroBytes
```

Non-upgradeable CREATE2 deployments default through the configured
`target.multicall.controlledMulticall`.
The runtime sends the factory `deploy(...)` call through
that contract instead of calling the factory directly, and any future
post-deploy calls can be included in the same batch.

Upgradeable deployments should use the bundled blank `UUPSPlaceholder`
implementation as the initial UUPS implementation. Treat `UUPSPlaceholder` as
chain-level infrastructure, like the CREATE2 factory. Before generating or
executing a proxySalt, every target network must declare the exact factory and
configured placeholder implementation address it will use. The placeholder
implementation is read from `target.uups.placeholderImplementation`. The proxy
CREATE2 initCode is `ERC1967Proxy(placeholderImplementation, 0x)`; if the
factory or placeholder implementation address differs by chain, the same proxy
salt will not produce the same proxy address.

Production rollouts should pre-deploy and verify this `UUPSPlaceholder`
infrastructure on every target network before the business deployment task.
The business deployment workflow does not mine or deploy a placeholder
implementation; it consumes `target.uups.placeholderImplementation` and
only deploys the CREATE2 proxy.

For `upgradeableStrategy: uups-placeholder-proxy`, SmartFlow mines the proxy
salt from the proxy init code hash:

```text
initCodeHash = keccak256(ERC1967Proxy bytecode ++ abi.encode(placeholderImplementation, 0x))
```

The business implementation bytecode hash is not used for proxy salt mining.

The placeholder has no owner, admin, or business state. It only exists so the
proxy initCode is stable. The deployment workflow must run an atomic first upgrade
in the same transaction that creates the proxy. If the first upgrade
fails, the whole transaction reverts and no placeholder proxy is left behind.

The real business implementation owns its own `initialize(...)` parameters,
owner/admin setup, and future `_authorizeUpgrade` policy. Add a final ownership handoff
step after deployment-time setup to transfer control to `$target.admin`
or the production Safe.

`UUPSPlaceholder` does not touch OpenZeppelin Initializable state. The real
business implementation runs its own `initialize(...)` during the atomic first
`upgradeToAndCall` call.

Use `upgradeableStrategy: uups-placeholder-proxy` in task metadata/docs. The
`saveAs` value identifies the business proxy. The `firstUpgrade.contract` value
is the actual business contract used for the `contractInfo.json` key; the
placeholder contract is never used as that alias suffix. Implementation and
placeholder addresses are retained only in the per-execution deployment record.

```yaml
- id: deploy-counter-placeholder-proxy
  kind: deploy
  contract: contracts/workflow/UUPSPlaceholder.sol:UUPSPlaceholder
  saveAs: Counter
  strategy: create2
  upgradeableStrategy: uups-placeholder-proxy
  proxy:
    kind: uups
    initializer: false
  create2:
    factory: $target.create2.factory
    salt: $param.salts.Counter.proxySalt
    leadingZeroBytes: $target.create2.leadingZeroBytes
    expectedAddress: $param.salts.Counter.proxyAddress
  firstUpgrade:
    contract: CounterUUPS
    call:
      method: initialize
      args:
        - $target.admin
        - $param.initialValue

- id: call-transfer-counter-owner
  kind: call
  contract: CounterUUPS
  deployment: Counter
  calls:
    - method: transferOwnership
      args:
        - $target.admin
```

Task parameters should group salts by deployment instance id:

```yaml
params:
  gtDev:
    salts:
      Counter:
        proxySalt: "0x..."
        proxyAddress: "0x00000000..."
      CoreVault:
        deploySalt: "0x..."
      RewardVault:
        deploySalt: "0x..."
  gtProd:
    salts:
      Counter:
        proxySalt: "0x..."
        proxyAddress: "0x00000000..."
```

Real proxy upgrades run `gate-tool validate` first. That command compiles,
validates upgrade safety, deploys the new implementation, and writes the
canonical `upgradeToAndCall` calldata. Fork simulation performs the equivalent
OpenZeppelin validation and implementation deployment inside the active Hardhat
process so the selected simulation signer and fork state cannot diverge. The
workflow uses the same locked per-transaction authority plan as `call`.

```yaml
- id: upgrade-counter
  kind: upgrade
  proxy: Counter
  old: CounterUUPS
  contract: CounterUUPSV2
  proxyKind: uups
```

`custom` is the special-case escape hatch for logic that cannot be represented
by standard steps:

```yaml
- id: custom-init
  kind: custom
  script: scripts/tasks/20260611-counter/scripts/02_custom_init.ts
```

Deployer review should treat `kind: custom` as arbitrary project code. Custom
scripts should still reuse workflow helpers for deployment records, authority
checks, and reports. Admin release plans reject a custom step after the external
suffix begins.

## Files

- `config.yaml`: long-lived business config by Hardhat network key.
- `contractInfo.json`: current business-facing contract or proxy addresses by network key.
- `deploymentInfo.json`: proxy kind, implementation, source record, and other deployment metadata.
- `scripts/workflow/run.ts`: workflow entrypoint used by deployers.
- `scripts/workflow/steps`: standard step executors for deploy, call, view, check, upgrade, and custom.
- `scripts/workflow/lib`: shared workflow runtime helpers for config, parameters, CREATE2, calldata, authority, reporting, and deployment records.
- `scripts/tasks/<taskId>/manifest.yaml`: task workflow execution plan.
- `scripts/tasks/<taskId>/parameters.yaml`: per-network task parameters and explicitly shared business parameters.
- `scripts/tasks/<taskId>/release.json`: source SuperTask, development network, and release-qualified network fingerprints.
- `scripts/tasks/<taskId>/create2-requests.json`: SmartFlow built-in CREATE2 resolver requests for deploy steps.
- `scripts/tasks/<taskId>/create2-results.json`: resolved CREATE2 salts, expected addresses, and resolver metadata.
- `scripts/tasks/<taskId>/scripts`: optional SmartFlow-generated custom scripts.
- `scripts/tasks/<taskId>/results/<target>/<executionId>/<mode>/<result>.json`: single-use execution, view, and verification envelopes for one workflow mode.
- `scripts/tasks/<taskId>/results/<network>.<executionId>.deployment.json`: deploy and upgrade evidence for one execute run.
- `scripts/tasks/<taskId>/results/<network>/<executionId>/execution-plan.json`: immutable per-transaction authority plan.
- `scripts/tasks/<taskId>/calldata/<network>/<executionId>/external-execution.json`: canonical cumulative external segment package.
- `scripts/tasks/<taskId>/results/<network>/<executionId>/external-reconciliation.json`: finalized chain reconciliation evidence.
- `scripts/tasks/<taskId>/results/registry/<commitId>.json`: all-network registry commit evidence.
- `contractInfo.pending.<executionId>.json`: complete pending registry candidate awaiting verified commit.
- `scripts/checkcode/runs/<runId>`: effective registry, raw gate-tool report, sync evidence, and final CheckCode report.

`contractInfo.json` uses the same string-only shape as gate-tool. Values are
always address strings; nested address or deployment objects are rejected:

```json
{
  "gtDev": {
    "Vault": "0xVaultProxy",
    "USDCVault:Vault": "0xUsdcVaultProxy",
    "USDTVault:Vault": "0xUsdtVaultProxy"
  }
}
```

Every execute run containing a deploy or upgrade step writes
`scripts/tasks/<taskId>/results/<network>.<executionId>.deployment.json`. The
record starts as `prepared`, captures the complete `contractInfo.json` before
snapshot, and records each step's instance ID, actual contract, registry key,
factory, multicall, salt, expected and actual addresses, implementation,
transaction hash, and CREATE2 reuse state. A successful deployment writes an
immutable before backup plus `contractInfo.pending.<executionId>.json`, then
waits in `waiting_code_check`. It does not change the formal registry.
`workflow:commit-contract-info` requires finalized execution and same-execution
business verification, promotes all selected candidates in one atomic registry
write, updates `deploymentInfo.json`, and records `deployed_pending_code_audit`.
CheckCode then audits the formal registry without changing it.

Upgrade records include the gate-tool output path and parsed output but do not
rewrite `contractInfo.json`, because the business proxy address is unchanged.
An external-admin run is marked `waiting_external`; a failed run remains
`partial`, preserving prepared changes and the before and planned snapshots for
recovery.

When a normal deployed contract already exists, the administrator must choose
`reuse` or `redeploy`. Reuse requires passed CheckCode evidence for the same
address and release artifact hash. Redeploy requires a new salt, a different
expected address, and a reason; the old registry is backed up at registry commit.
ERC1967/UUPS proxies cannot use redeploy and must continue through an upgrade
step.

The runtime serializes `contractInfo.json` read-modify-write operations across
workflow processes, so parallel network executions cannot overwrite one
another. A concurrent candidate for the same network and deployment instance is
rejected. Generated execution IDs include
millisecond precision and a random suffix, and an existing deployment-record
path is never overwritten. Locks left by a terminated workflow process are
detected and recovered before the next registry update.
