Observed since 21 Oct, 10:03 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Ethereum contract deployment CLI tool
Catapult is a powerful Ethereum contract deployment and management framework designed to simplify the orchestration of complex contract deployments across multiple blockchain networks. Built with TypeScript and Node.js, it provides a declarative YAML-based approach to defining deployment jobs, templates, and dependencies.
Catapult addresses the challenge of managing complex contract deployment scenarios where multiple contracts need to be deployed in a specific order, with dependencies between them, across multiple blockchain networks. Instead of writing custom deployment scripts for each scenario, you define your deployment logic declaratively using YAML files.
Available on npm as @0xsequence/catapult.
Global install (provides the catapult CLI on your PATH):
npm install -g @0xsequence/catapult
# or
yarn global add @0xsequence/catapult
# or
pnpm add -g @0xsequence/catapult
Project-local install (use via npx or package.json scripts):
npm install -D @0xsequence/catapult
# or
yarn add -D @0xsequence/catapult
# or
pnpm add -D @0xsequence/catapult
# then run
npx catapult --help
git clone <repository-url>
cd catapult
npm install
npm run build
npm link
npm install -g @0xsequence/catapult
A Catapult project follows this structure:
my-deployment-project/
├── networks.yaml # Network configurations
├── jobs/ # Deployment job definitions
│ ├── core-contracts.yaml
│ ├── factory-setup.yaml
│ └── token-deployment.yaml
├── templates/ # Custom template definitions
│ ├── erc20-factory.yaml
│ └── proxy-deployment.yaml
├── artifacts/ # Contract build artifacts
│ ├── MyContract.json
│ └── Factory.json
└── output/ # Generated deployment results
Create a networks.yaml file in your project root to define target networks:
- name: "Ethereum Mainnet"
chainId: 1
rpcUrl: "https://mainnet.infura.io/v3/YOUR_PROJECT_ID"
- name: "Arbitrum One"
chainId: 42161
rpcUrl: "https://arb1.arbitrum.io/rpc"
- name: "Polygon"
chainId: 137
rpcUrl: "https://polygon-rpc.com"
supports: ["etherscan_v2"] # Optional: verification platforms supported
gasLimit: 500000 # Optional: gas limit for all transactions on this network
testnet: true # Optional: mark as test network
evmVersion: "cancun" # Optional: network EVM hardfork (e.g., london, paris, shanghai, cancun)
The supports field is optional and specifies which verification platforms are available for the network. Currently supported platforms:
etherscan_v2: Etherscan v2 verification API (supports Ethereum, Polygon, Arbitrum, BSC, etc.)sourcify: Sourcify verification (no API key required)If supports is omitted, all built-in platforms are allowed for that network. Etherscan requires an API key to be considered “configured”; Sourcify requires no configuration. The gasLimit field is optional and specifies a fixed gas limit to use for all transactions on this network. If not specified, the system will use ethers.js default gas estimation.
You can define reusable values in constants files or directly within a job.
type: "constants".{{MY_CONSTANT}}.Example top-level constants file (can be placed anywhere, e.g., constants.yaml):
type: "constants"
constants:
address-zero: "0x0000000000000000000000000000000000000000"
salt-zero: "0x0000000000000000000000000000000000000000000000000000000000000000"
developer-multisig-01: "0x007a47e6BF40C1e0ed5c01aE42fDC75879140bc4"
entrypoint-4337-07: "0x0000000071727de22e5e9d8baf0edac6f37da032"
Job-level constants example (defined at the top of a job):
name: "job-with-constants"
version: "1"
constants:
FEE: "1000"
ADMIN: "0x0000000000000000000000000000000000000001"
actions:
- name: "example"
template: "some-template"
arguments:
admin: "{{ADMIN}}" # resolves to job-level constant
defaultSalt: "{{salt-zero}}" # resolves to top-level constant
Tip: Use catapult list constants to see discovered top-level constants and any job-level constants.
You can inject secrets (like access tokens) into rpcUrl using placeholders of the form {{RPC_...}}. At load time, any placeholder whose name starts with RPC will be replaced with the value of the corresponding environment variable. Placeholders not starting with RPC are left as-is.
Example networks.yaml:
- name: "MyNet"
chainId: 999
rpcUrl: "https://node.url/something/{{RPC_URL_TOKEN}}"
With an environment variable:
export RPC_URL_TOKEN="my-secret-token"
Resulting rpcUrl at runtime:
https://node.url/something/my-secret-token
Notes:
{{RPC_*}} placeholder is present and the corresponding environment variable is not set, it now defaults to an empty string. This allows templates like https://node.url/{{RPC_TOKEN}} to collapse gracefully to https://node.url/ without failing the load.{{ RPC_TOKEN }}).Jobs are the core deployment units. Create YAML files in the jobs/ directory:
# jobs/core-contracts.yaml
name: "core-contracts"
version: "1.0.0"
description: "Deploy core system contracts"
actions:
- name: "deploy-factory"
template: "sequence-universal-deployer-2"
arguments:
creationCode: "{{Contract(MyFactory).creationCode}}"
salt: "0"
- name: "deploy-implementation"
template: "sequence-universal-deployer-2"
depends_on: ["deploy-factory"]
arguments:
creationCode:
type: "constructor-encode"
arguments:
creationCode: "{{Contract(MyImplementation).creationCode}}"
types: ["address"]
values: ["{{deploy-factory.address}}"]
salt: "0"
Jobs run on all selected networks by default. You can restrict or exclude networks for a specific job by chain ID:
name: "token-deployment"
version: "1.0.0"
# Run only on these networks (takes precedence if present)
only_networks: [1, 42161]
# Or, skip these networks (used only if only_networks is not set)
# skip_networks: [137]
actions:
- name: "deploy"
template: "erc-2470"
arguments: { /* ... */ }
Rules:
only_networks is set and non-empty, the job runs only on those chain IDs.skip_networks is set and non-empty, the job is skipped on those chain IDs.networks.yaml or --network).A network can restrict or exclude jobs by name in networks.yaml. Patterns support * and ? wildcards:
- name: "Monad"
chainId: 143
rpcUrl: "https://rpc.monad.xyz"
# Run only these jobs (plus their dependencies) on this network
onlyJobs: ["trails-*"]
# Skip these jobs on this network, applied after onlyJobs
skipJobs: ["trails-rc-1"]
Rules:
onlyJobs is set, jobs it matches run along with their transitive dependencies; every other job is skipped on that network.skipJobs then skips every job it matches, including dependencies pulled in by onlyJobs.onlyJobs matches, every job is skipped on that network.Jobs can declare a minimum EVM hardfork they require. When a network’s evmVersion is older than the job’s min_evm_version, the job is skipped on that network.
name: "post-shanghai-feature"
version: "1.0.0"
min_evm_version: "shanghai"
actions:
- name: "deploy"
template: "erc-2470"
arguments: { /* ... */ }
Supported identifiers include: frontier, homestead, tangerine, spuriousdragon, byzantium, constantinople, petersburg, istanbul, berlin, london, paris (The Merge), shanghai, cancun, prague.
Mark a job as deprecated to opt it out of normal runs without deleting it:
name: "legacy-seed"
version: "1.2.3"
deprecated: true
actions:
- name: "noop"
type: "static"
arguments: { value: null }
Behavior:
catapult run legacy-seed -k $PRIVATE_KEY.--run-deprecated: catapult run --run-deprecated -k $PRIVATE_KEY.--run-deprecated).Templates are reusable deployment patterns. Create them in the templates/ directory:
# templates/proxy-factory.yaml
name: "proxy-factory"
type: "template"
arguments:
implementation:
type: "address"
salt:
type: "bytes32"
returns:
address:
type: "address"
setup:
- type: "job-completed"
arguments:
job: "core-contracts"
actions:
- type: "send-transaction"
arguments:
to: "{{core-contracts.deploy-factory.address}}"
data:
type: "abi-encode"
arguments:
signature: "createProxy(address,bytes32)"
values:
- "{{implementation}}"
- "{{salt}}"
skip_condition:
- type: "contract-exists"
arguments:
address:
type: "compute-create2"
arguments:
deployerAddress: "{{core-contracts.deploy-factory.address}}"
salt: "{{salt}}"
initCode:
type: "constructor-encode"
arguments:
creationCode: "{{Contract(ProxyBytecode).creationCode}}"
types: ["address"]
values: ["{{implementation}}"]
outputs:
address:
type: "compute-create2"
arguments:
deployerAddress: "{{core-contracts.deploy-factory.address}}"
salt: "{{salt}}"
initCode:
type: "constructor-encode"
arguments:
creationCode: "{{Contract(ProxyBytecode).creationCode}}"
types: ["address"]
values: ["{{implementation}}"]
Notes about template files:
type: "template" discriminator is optional but recommended for clarity. If provided, it must be exactly template.templates/ folder and any templates/ subfolders under jobs/.Deploy all jobs to all configured networks:
catapult run --private-key YOUR_PRIVATE_KEY
Deploy specific jobs:
catapult run core-contracts token-setup --private-key YOUR_PRIVATE_KEY
Deploy multiple jobs using wildcards (matches job names, including nested names like sequence_v3/beta_4):
# Run all jobs whose name starts with "sequence_"
catapult run sequence_* -k $PRIVATE_KEY
# Run all jobs under a namespace/folder-like prefix
catapult run "sequence_v3/*" -k $PRIVATE_KEY
# Combine patterns and exact names; duplicates are de-duplicated
catapult run job1 job? -k $PRIVATE_KEY
Deploy to specific networks:
# Comma-separated, supports chain IDs and network names (name matches include all networks with that name)
catapult run --network 1,42161 --private-key YOUR_PRIVATE_KEY
catapult run --network mainnet --private-key YOUR_PRIVATE_KEY # all networks named "Mainnet"
catapult run --network mainnet,polygon -k $PRIVATE_KEY core-contracts
Common options (run):
-p, --project <path>: Project root directory (defaults to current directory)--dotenv <path>: Load environment variables from a custom .env file (run command only)-n, --network <selectors>: Comma-separated selectors by chain ID or network name--rpc-url <url>: Run against a single custom RPC; chain ID is auto-detected. If networks.yaml defines that chain, Catapult merges yaml settings (name, supports, gasLimit, testnet, evmVersion, params) while using your RPC URL.-k, --private-key <key>: EOA private key (or set PRIVATE_KEY)--etherscan-api-key <key>: Etherscan API key (or set ETHERSCAN_API_KEY)--safe-api-key <key>: Safe API key for actions with propose: true (or set SAFE_API_KEY / SAFE_TRANSACTION_SERVICE_API_KEY)--safe-tx-service-url <url>: Custom Safe Transaction Service URL (or set SAFE_TX_SERVICE_URL)--safe-proposer-private-key <key>: Dedicated Safe owner/delegate key (or set SAFE_PROPOSER_PRIVATE_KEY; defaults to the run signer)--fail-early: Stop as soon as any job fails--ignore-verify-errors: Convert verification errors to warnings and show complete report at end (instead of exiting with error code)--no-post-check-conditions: Skip post-execution evaluation of skip conditions--flat-output: Write outputs in a single flat output/ directory (do not mirror jobs/ structure)--no-summary: Hide the end-of-run summary--run-deprecated: Allow running jobs marked deprecated: true (otherwise skipped unless explicitly targeted)--no-std: Do not load built-in standard templates-v, --verbose (repeatable): Increase logging verbosity (-v, -vv, -vvv)Examples:
catapult run --rpc-url http://127.0.0.1:8545 -k $PRIVATE_KEY
jobs/ folders:catapult run --flat-output -k $PRIVATE_KEY
catapult run legacy-job -k $PRIVATE_KEY
catapult run --run-deprecated -k $PRIVATE_KEY
Validate your configuration without executing transactions:
catapult dry-run
Validate specific jobs:
catapult dry-run core-contracts --network 1
catapult dry-run core-contracts --network polygon
catapult dry-run core-contracts --network mainnet,42161
List available jobs:
catapult list jobs
List detected contracts:
catapult list contracts
List available templates:
catapult list templates
List configured networks:
catapult list networks
List only test networks:
catapult list networks --only-testnets
List only non-test networks:
catapult list networks --only-non-testnets
List constants (top-level and per-job):
catapult list constants
Simple outputs for scripting:
# Names only, one per line
catapult list networks --simple
# Chain IDs only, one per line
catapult list networks --simple-chain-ids
Utilities:
# Convert chain ID to network name
catapult utils chain-id-to-name 42161 -p ./my-project
# Export a Safe transaction artifact for Safe Transaction Builder
catapult utils safe-batch ./output --chain-id 1 --output safe-batch.json
# Multiple transactions require an explicit order
catapult utils safe-batch ./output --chain-id 1 \
--transaction upgrade-job/deploy upgrade-job/upgrade \
--output safe-batch.json
Etherscan helpers:
# Fetch ABI from Etherscan v2
catapult etherscan abi -n 1 -a 0xdAC17F958D2ee523a2206206994597C13D831ec7 --etherscan-api-key $ETHERSCAN_API_KEY
catapult etherscan abi -n mainnet -a 0xdAC17F... --etherscan-api-key $ETHERSCAN_API_KEY
# Fetch source (standard-json or flattened) from Etherscan v2
catapult etherscan source -n 1 -a 0xdAC17F958D2ee523a2206206994597C13D831ec7 --etherscan-api-key $ETHERSCAN_API_KEY
catapult etherscan source -n mainnet -a 0xdAC17F... --etherscan-api-key $ETHERSCAN_API_KEY
Source provenance helpers:
# Verify every source.yaml provenance entry in the project
catapult provenance verify
# Verify only one job's provenance entries
catapult provenance verify my-job
# Verify a job and the jobs it depends on
catapult provenance verify my-job --include-dependencies
# Generate missing build-info JSON files from source.yaml provenance
catapult provenance generate
Catapult provides several built-in primitive actions:
send-transactionSend a transaction to the blockchain:
- type: "send-transaction"
arguments:
to: "0x742..."
value: "1000000000000000000" # 1 ETH in wei
data: "0x..."
gasMultiplier: 1.5 # Optional: multiply gas limit by this factor
The gasMultiplier parameter is optional and allows you to tune the gas limit before sending the transaction:
send-signed-transactionBroadcast a pre-signed transaction:
- type: "send-signed-transaction"
arguments:
transaction: "0x..." # Raw signed transaction
staticSets a static value that can be referenced in subsequent steps. Useful for defining constants or passing data between actions.
- type: "static"
name: "my-value"
arguments:
value: "hello world"
The name field is optional. When provided, the value is stored under name.value in the context. If omitted, the value is computed but not stored. Supports all JSON data types including strings, numbers, booleans, objects, and arrays.
Example with complex data:
- type: "static"
name: "config"
arguments:
value:
endpoint: "https://api.example.com"
timeout: 5000
enabled: true
This makes config.value.endpoint, config.value.timeout, and config.value.enabled available for use in subsequent actions.
safe-transactionBuild a Safe transaction artifact without signing, proposing, or broadcasting it by default:
- name: "upgrade"
type: "safe-transaction"
arguments:
safe: "{{admin-safe}}"
to: "{{proxy-admin}}"
value: "0"
data:
type: "abi-encode"
arguments:
signature: "upgradeAndCall(address,address,bytes)"
values:
- "{{proxy}}"
- "{{deployment.implementation.address}}"
- "0x"
operation: 0
simulate: true
propose: false
output: true
safe, to, value, and data support normal Catapult values and resolvers. value defaults to 0, data defaults to 0x, operation defaults to CALL (0), simulate defaults to true, and propose defaults to false.
For CALL, simulation performs a read-only eth_call from the Safe address directly to the target. This checks the inner call's calldata, target-contract authorization, value, and current-state behavior. It does not run Safe.execTransaction, so it does not validate Safe signatures, threshold, nonce, guards, or refund settings. DELEGATECALL (1) artifacts require simulate: false because an inner delegatecall cannot be reproduced with a direct eth_call.
The action writes a versioned name.safeTransaction object to Catapult output. It also emits name.safeTxTo, name.safeTxValue, name.safeTxData, name.safeTxOperation, and name.executorMultisig for compatibility with existing payload consumers. Use catapult utils safe-batch to turn one or more CALL artifacts into checksummed Safe Transaction Builder JSON. When multiple artifacts exist, pass --transaction job/action ... in the required batch order.
Set propose: true to sign the Safe transaction hash with Catapult's existing run signer and submit it to the Safe Transaction Service:
- name: "upgrade"
type: "safe-transaction"
arguments:
safe: "{{admin-safe}}"
to: "{{proxy-admin}}"
data: "{{upgrade-calldata}}"
propose: true
origin: "Release automation"
# safeNonce: 42 # Optional. Defaults to the next nonce after pending proposals.
output: true
For Safe's hosted service, provide SAFE_API_KEY (or --safe-api-key). For a self-hosted service, provide SAFE_TX_SERVICE_URL (or --safe-tx-service-url); an API key remains optional unless that service requires one. The run signer must be a Safe owner or a registered Safe Transaction Service delegate. To keep deployment and proposal authority separate, set SAFE_PROPOSER_PRIVATE_KEY to a dedicated registered delegate key; otherwise Catapult uses the normal run signer.
Proposal is off-chain: it makes the transaction visible to Safe owners but does not execute it. Catapult asks the Transaction Service to estimate safeTxGas, uses zero refund parameters, reads the Safe's canonical transaction hash on-chain, signs that hash, and submits it with the selected nonce. A successful proposal emits name.safeTransactionProposal, name.safeTxHash, name.safeTxNonce, and name.safeTxProposer. The existing unsigned artifact is still emitted.
create-contractCreate a contract by sending its creation bytecode (and optional value):
- type: "create-contract"
name: "deploy-foo"
arguments:
data: "{{Contract(Foo).creationCode}}"
gasMultiplier: 1.2
json-requestMake an HTTP JSON request and use the result downstream:
- type: "json-request"
name: "get-config"
arguments:
url: "https://example.com/config.json"
method: "GET"
assertAssert an on-chain invariant or value comparison. Evaluates a condition and throws a clear error if it doesn't hold (no broadcast, ever).
The action provides two ways to obtain the ACTUAL value:
to + signature — performs an eth_call (like the call value resolver)actual — resolves any Value resolver (e.g. read-balance, static, etc.)Then provide exactly one comparator key (eq, neq, gt, lt, gte, lte) whose value is the EXPECTED result. If the comparison is false, the action throws an error that fails the run.
Example — call a view function and compare:
- type: assert
name: check-deposit-manager
arguments:
to: "{{some-proxy}}"
signature: "depositManager() returns (address)"
eq: "{{expected-deposit-manager}}"
Example — resolve a value and compare:
- type: assert
name: check-balance
arguments:
actual: { type: read-balance, arguments: { address: "{{deployer}}" } }
gte: "1000000000000000000"
message: "deployer underfunded"
An optional message field is included in the error output for clarity.
Catapult includes powerful value resolvers for computing complex values:
abi-encodeABI-encode function call data:
data:
type: "abi-encode"
arguments:
signature: "transfer(address,uint256)"
values:
- "0x742..."
- "1000000000000000000"
constructor-encodeEncode constructor parameters with bytecode:
creationCode:
type: "constructor-encode"
arguments:
creationCode: "{{Contract(MyContract).creationCode}}"
types: ["address", "uint256"]
values: ["{{factory.address}}", "100"]
abi-packPack values per ABI types into bytes:
payload:
type: "abi-pack"
arguments:
types: ["address", "uint256"]
values: ["{{recipient}}", "{{amount}}"]
compute-create2Compute CREATE2 addresses:
address:
type: "compute-create2"
arguments:
deployerAddress: "{{factory.address}}"
salt: "{{salt}}"
initCode: "{{creationCode}}"
basic-arithmeticPerform mathematical operations:
amount:
type: "basic-arithmetic"
arguments:
operation: "add"
values: ["{{current_balance}}", "1000000000000000000"]
read-balanceRead account balance:
balance:
type: "read-balance"
arguments:
address: "{{deployer_address}}"
callMake view/pure function calls:
result:
type: "call"
arguments:
to: "{{contract.address}}"
signature: "getName()"
values: []
get-storage-atRead a raw EVM storage slot via eth_getStorageAt:
storageValue:
type: "get-storage-at"
arguments:
address: "{{contract.address}}"
slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" # EIP-1967 implementation slot
The slot can be a hex string, a number, or a reference to another value. Returns the 32-byte storage value as a hex string.
compute-slotCompute EVM storage slots for the common Solidity storage layouts. The result is always a 32-byte, 0x-prefixed hex string, so it can be fed directly into get-storage-at or nested as the slot of another compute-slot (e.g. for nested mappings). Select a layout with kind:
mapping — value slot of mapping[key] (keccak256(h(key) . slot)):
slot:
type: "compute-slot"
arguments:
kind: "mapping"
slot: 0 # declaration slot of the mapping
key: "{{owner}}"
keyType: "address" # optional, default "uint256"; "string"/"bytes" keys are packed
Nested mappings (balances[a][b]) are expressed by nesting compute-slot in the slot field:
slot:
type: "compute-slot"
arguments:
kind: "mapping"
key: "{{spender}}"
keyType: "address"
slot:
type: "compute-slot"
arguments:
kind: "mapping"
slot: 1
key: "{{owner}}"
keyType: "address"
dynamic-array — element slot of a dynamic array (keccak256(slot) + index * elementSize). The array length lives at slot itself:
slot:
type: "compute-slot"
arguments:
kind: "dynamic-array"
slot: 3
index: 4 # optional, default 0
elementSize: 1 # optional slots-per-element, default 1
struct-field — a struct field or fixed-array element (slot + offset):
slot:
type: "compute-slot"
arguments:
kind: "struct-field"
slot: "{{structBase}}"
offset: 2
erc7201 — ERC-7201 namespaced storage root (keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~0xff):
slot:
type: "compute-slot"
arguments:
kind: "erc7201"
id: "openzeppelin.storage.Ownable"
eip1967 — well-known EIP-1967 proxy slot (keccak256("eip1967.proxy.<name>") - 1), where name is implementation, admin, or beacon:
implementation:
type: "get-storage-at"
arguments:
address: "{{proxy.address}}"
slot:
type: "compute-slot"
arguments:
kind: "eip1967"
name: "implementation"
verify-contractread-jsonRead a value from a JSON object at a given path:
tokenAddress:
type: "read-json"
arguments:
json: "{{get-config.response}}"
path: "tokens.usdc.address"
slice-bytesSlice hex strings by byte offsets (supports negative indexes and range syntax):
trimmedPatchData:
type: "slice-bytes"
arguments:
value:
type: "read-json"
arguments:
json: "{{get-guard-v1-signature.response}}"
path: "txs.data"
range: ":-1" # Drop the trailing byte from the payload
You can also provide explicit start and end byte positions (end is exclusive). Negative indexes count from the end of the byte array, so end: -1 trims the last byte and start: -32 keeps the final 32 bytes. range accepts either start:end or the bracket form [start:end].
read-fileRead the raw contents of a file as a value. This is the home for large, opaque, per-execution operational blobs (e.g. packed multisig signatures) that don't belong in constants and aren't typed build artifacts. The path is resolved relative to the directory of the job/template that uses it and is confined to the project root (absolute paths and .. escapes are rejected):
# Read packed Safe owner signatures written to a gitignored file next to the job
signatures:
type: "read-file"
arguments:
path: "signatures.hex"
encoding: "hex" # "utf8" (default), "hex", or "json"
encoding: "utf8" returns the text with a single trailing newline trimmed; "hex" validates and normalizes to a 0x-prefixed lowercase hex string; "json" parses the file and returns the resulting value (composes with read-json).
concatJoin resolved parts into a single string. Use this for URL/path templating instead of embedding {{...}} inside a longer literal (which is not interpolated — a {{ref}} is only resolved when it is the entire value):
url:
type: "concat"
arguments:
values:
- "https://safe-transaction-mainnet.safe.global/api/v1/multisig-transactions/"
- "{{safe-tx-hash}}"
- "/"
separator: "" # optional, defaults to "" (direct concatenation)
Verify deployed contracts on block explorers:
- type: "verify-contract"
arguments:
address: "{{deploy-factory.address}}"
contract: "{{Contract(MyContract)}}" # Reference to the contract to verify
constructorArguments: "0x000000000000000000000000..." # Optional hex-encoded args
platform: "etherscan_v2" # Optional, defaults to "all" (tries all configured platforms)
By default, verification failures will cause the deployment to exit with an error code. To continue deployment even when verification fails and receive a comprehensive warning report at the end, use the --ignore-verify-errors flag:
catapult run --ignore-verify-errors
This is useful when:
When enabled, verification errors are converted to warnings and a detailed report is shown at the end of the deployment with all verification failures.
Avoid redundant operations with skip conditions:
contract-existsSkip if contract exists at address:
skip_condition:
- type: "contract-exists"
arguments:
address: "{{computed_address}}"
job-completedSkip if another job is completed:
skip_condition:
- type: "job-completed"
arguments:
job: "prerequisite-job"
skip_if (pure gate, no post-execution check)In addition to skip_condition, jobs support a skip_if field for pure gate semantics:
skip_if is true → skip the whole job (status skipped)skip_condition)Use skip_if for jobs that generate artifacts (e.g., Safe/multisig transaction payloads for human execution out-of-band) and should skip when already in the desired state, without requiring convergence within the run itself.
name: "generate-upgrade-payload"
version: "1.0.0"
skip_if:
- type: "contract-exists"
arguments:
address: "{{computed_upgrade_address}}"
actions:
- name: "generate"
type: "static"
arguments:
value: "upgrade-payload-data"
Combining skip_condition and skip_if:
If both are present, the job is skipped if ANY condition in either array is true at pre-skip time. Only skip_condition is post-execution-checked.
name: "hybrid-job"
version: "1.0.0"
skip_condition:
- type: "contract-exists"
arguments:
address: "{{deployed_address}}"
skip_if:
- type: "job-completed"
arguments:
job: "setup-job"
actions:
- name: "deploy"
template: "erc-2470"
arguments:
creationCode: "{{Contract(MyContract).creationCode}}"
salt: "0"
Catapult includes several standard templates:
sequence-universal-deployer-2: Deploy contracts using Sequence's Universal Deployer v2nano-universal-deployer: Deploy contracts using the Nano Universal Deployerarachnid-deterministic-deployment-proxy: Deploy contracts via Arachnid's CREATE2 proxy at 0x4e59…, with automatic factory bootstrappingerc-2470 and raw variant: CREATE2 Deployer (singleton factory)assured-deployment: Helper to ensure a contract is deployed at a specific addressmin-balance: Ensure minimum balance for any given addresssafe-exec-transaction: Assemble and broadcast a fully-signed Gnosis Safe execTransaction (see below)raw-sequence-universal-deployer-2, raw-nano-universal-deployer, raw-erc-2470safe-exec-transactionBroadcasts a fully-signed Gnosis Safe transaction on-chain ("Shape 1" relay) instead of stopping at calldata for a human to paste into the Safe UI. It packs the owner signatures the Safe already collected into execTransaction and sends the outer transaction with the configured EOA as relayer.
The packed signatures are passed as the signatures argument. Where they come from is the caller's choice — the two common sources:
Offline / air-gapped (signatures collected into a gitignorable file):
- name: "relay"
template: "safe-exec-transaction"
arguments:
safe: "{{safe_address}}"
to: "{{target}}"
data: "{{inner_calldata}}"
operation: "0" # 0 = CALL, 1 = DELEGATECALL
signatures:
type: "read-file"
arguments: { path: "signatures.hex", encoding: "hex" }
Safe Transaction Service (hosted flow — the service returns a top-level pre-packed signatures field):
- name: "fetch-safe-tx"
type: "json-request"
arguments:
url:
type: "concat"
arguments:
values:
- "https://safe-transaction-mainnet.safe.global/api"
- "/v1/multisig-transactions/"
- "{{safe_tx_hash}}"
- "/"
- name: "relay"
template: "safe-exec-transaction"
arguments:
safe: "{{safe_address}}"
to: "{{target}}"
data: "{{inner_calldata}}"
operation: "0"
signatures:
type: "read-json"
arguments: { json: "{{fetch-safe-tx.response}}", path: "signatures" }
The template only assembles and broadcasts; it does not impose a post-execution skip condition, because the desired state of a Safe relay is the effect of the inner call. Wrap the calling job with skip_if observing that on-chain effect (e.g. owner() == newOwner) for idempotent, re-runnable convergence.
Catapult automatically discovers and indexes contract artifacts in your project. It supports:
Reference contracts in your YAML using the new unified Contract() syntax:
creationCode: "{{Contract(path/to/MyContract).creationCode}}"
# or
creationCode: "{{Contract(0x1234...hash).creationCode}}"
Build-info files can carry optional source provenance through a nearby source.yaml
sidecar. Deployment runs still use committed artifacts; Catapult does not rebuild
from the source repository during catapult run.
jobs/my-stack/build-info/rc-5/
├── stage1.json
└── source.yaml
type: source
build_info:
"./stage1.json":
repo: "https://github.com/0xsequence/wallet-contracts-v3"
ref: "v3.0.0-rc.5"
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
build: "forge build --build-info"
Catapult can use the same provenance to rebuild and compare build-info files on demand:
# Rebuild each source provenance entry and compare it with the committed file
catapult provenance verify
# Scope to one job, or include that job's dependencies
catapult provenance verify my-job
catapult provenance verify my-job --include-dependencies
# Clone/build from provenance and write missing build-info files
catapult provenance generate
catapult provenance generate my-job --include-dependencies
provenance generate skips existing build-info files by default; pass --force
to overwrite them. Both commands clone the configured repo, check out ref or
commit, run the build command in that checkout, and look for generated
build-info/*.json files. If the build produces more than one build-info file,
Catapult selects by matching the committed build-info id, then by filename; if
neither is unique, the entry fails with an ambiguity error.
imageBecause provenance compares the entire build-info JSON (including compiler
settings such as evmVersion), the rebuild must use the same toolchain that
produced the committed file. Add an optional image field to run the build
command inside a pinned Docker image instead of on the host:
type: source
build_info:
"./stage1.json":
repo: "https://github.com/0xsequence/wallet-contracts-v3"
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
image: "ghcr.io/foundry-rs/foundry:v1.5.1"
build: "forge build --build-info"
When image is set, Catapult runs docker run <image> with the checkout
bind-mounted at /workspace (also the working directory and $HOME), the build
command executed via sh -c, and — on POSIX hosts — the container running as the
caller's uid:gid so the generated files are owned by you and the temporary
checkout can be cleaned up. This keeps the toolchain pinned per entry (different
build-info files can use different images) and leaves the host/runner untouched;
it requires Docker to be installed and running. The image field is also
supported in per-contract contracts overrides.
If a build-info file needs a per-contract override, key it by fully-qualified contract name:
type: source
build_info:
"./stage1.json":
repo: "https://github.com/0xsequence/wallet-contracts-v3"
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
contracts:
"src/Stage1Module.sol:Stage1Module":
ref: "stage1-special"
After successful deployment, Catapult generates JSON files in the output/ directory for each job. The output format is optimized to reduce repetition:
Networks with identical deployment outputs are grouped together:
{
"jobName": "core-contracts",
"jobVersion": "1.0.0",
"lastRun": "2025-01-15T10:30:45.123Z",
"networks": [
{
"status": "success",
"chainIds": ["1", "42161", "137"],
"outputs": {
"deploy-factory.address": "0x742d35Cc6ab8b3c7B3d4B8b3aB4c8f9e9C8e8aB6",
"deploy-factory.txHash": "0xabc123...",
"deploy-implementation.address": "0x123abc..."
}
}
]
}
When deployments fail on specific networks, each failure is recorded separately:
{
"jobName": "core-contracts",
"jobVersion": "1.0.0",
"lastRun": "2025-01-15T10:30:45.123Z",
"networks": [
{
"status": "success",
"chainIds": ["1", "42161"],
"outputs": {
"deploy-factory.address": "0x742d35Cc6ab8b3c7B3d4B8b3aB4c8f9e9C8e8aB6"
}
},
{
"status": "error",
"chainId": "137",
"error": "Transaction failed: insufficient funds"
}
]
}
This format ensures:
Output layout and selection:
jobs/ (e.g., jobs/core/job.yaml -> output/core/job.json). Use --flat-output to write all job JSON files directly under output/.output flag on actions:
output: true to include all outputs for that actionoutput: false to exclude outputs for that actionoutput: { key1: true, key2: true } to include only specific keys from that action (e.g., txHash, address)PRIVATE_KEY: Signer private key (alternative to --private-key)ETHERSCAN_API_KEY: API key for Etherscan v2 verification (alternative to --etherscan-api-key)SAFE_API_KEY or SAFE_TRANSACTION_SERVICE_API_KEY: Safe API key for optional proposalsSAFE_TX_SERVICE_URL: Custom Safe Transaction Service URLSAFE_PROPOSER_PRIVATE_KEY: Dedicated Safe owner/delegate key used only for proposalsYou can load environment variables from a file using --dotenv <path> on the run command (defaults to .env in the current directory when provided).
# Install dependencies
npm install
# Build the project
npm run build
# Run in development mode
npm run dev
# Watch for changes
npm run watch
npm run build - Compile TypeScript to JavaScriptnpm run dev - Run the CLI in development mode with ts-nodenpm run watch - Watch for changesDownload counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 2.2.0Registry latest | 15 Oct 2026 | 20 | |
| 2.1.0 | 28 Sept 2026 | 20 | |
| 2.0.0 | 28 Sept 2026 | 20 | |
| 1.5.0 | 30 Jun 2026 | 19 | |
| 1.4.0 | 29 Jun 2026 | 19 | |
| 1.3.17 | 17 May 2026 | 19 | |
| 1.3.16 | 17 May 2026 | 19 | |
| 1.3.15 | 31 Mar 2026 | 19 | |
| 1.3.14 | 31 Mar 2026 | 19 | |
| 1.3.13 | 30 Mar 2026 | 19 | |
| 1.3.12 | 30 Mar 2026 | 19 | |
| 1.3.11 | 25 Nov 2025 | 19 | |
| 1.3.10 | 13 Nov 2025 | 19 | |
| 1.3.9 | 13 Nov 2025 | 19 | |
| 1.3.8 | 30 Sept 2025 | 19 | |
| 1.3.7 | 29 Sept 2025 | 19 | |
| 1.3.6 | 11 Sept 2025 | 19 | |
| 1.3.5 | 11 Sept 2025 | 19 | |
| 1.3.4 | 9 Sept 2025 | 19 | |
| 1.3.3 | 4 Sept 2025 | 19 |
This describes our collected data; it is not a security assessment.
Observed since 21 Oct, 10:03 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:c5fef47c404495e9a2cb0cd150c169e95dcb4bb4f09d735046f022758f15ae23