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.
- 🔄 Declarative Deployment Jobs: Define complex deployment workflows using YAML configuration files
- 📋 Template System: Create reusable deployment templates that can be shared across projects
- 🔗 Dependency Management: Automatic resolution of deployment dependencies and execution ordering
- 🌐 Multi-Network Support: Deploy to multiple blockchain networks simultaneously
- ⚡ Built-in Actions: Comprehensive set of built-in actions for common deployment tasks
- 🧮 Value Resolvers: Powerful system for computing values, encoding data, and performing calculations
- ✅ Skip Conditions: Smart conditional logic to avoid redundant deployments
- 🔍 Validation & Dry Run: Validate configurations and preview deployment plans without execution
- 📊 Event System: Rich event system for monitoring deployment progress and debugging
- 🧾 Multi-platform Verification: Verify on Etherscan v2 and Sourcify (tries all configured platforms by default)
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.
- Top-level constants are discovered anywhere under your project root by adding YAML files with
type: "constants"
. - Keys must be unique across all constants files; duplicates will fail the load.
- Within jobs/templates, reference constants using bare placeholders like
{{MY_CONSTANT}}
. - Job-level constants override top-level constants when names collide.
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:
- If an
{{RPC_*}}
placeholder is present and the corresponding environment variable is not set, it now defaults to an empty string. This allows templates likehttps://node.url/{{RPC_TOKEN}}
to collapse gracefully tohttps://node.url/
without failing the load. - Multiple RPC tokens in one URL are supported, and whitespace inside the token delimiters is ignored (e.g.,
{{ 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:
- If
only_networks
is set and non-empty, the job runs only on those chain IDs. - Else, if
skip_networks
is set and non-empty, the job is skipped on those chain IDs. - Otherwise, the job runs on all networks selected for the run (via
networks.yaml
or--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:
- Deprecated jobs are skipped by default when running without specifying job names.
- Explicitly targeting a deprecated job on the CLI will run it even without extra flags:
catapult run legacy-seed -k $PRIVATE_KEY
. - To include all deprecated jobs in a normal run, pass
--run-deprecated
:catapult run --run-deprecated -k $PRIVATE_KEY
. - If a non-deprecated job depends on a deprecated job, that deprecated dependency is ALWAYS included automatically to satisfy dependencies (even without
--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:
- The
type: "template"
discriminator is optional but recommended for clarity. If provided, it must be exactlytemplate
. - Templates are auto-discovered from your project
templates/
folder and anytemplates/
subfolders underjobs/
.
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 (no networks.yaml required)-k, --private-key <key>
: EOA private key (or setPRIVATE_KEY
)--etherscan-api-key <key>
: Etherscan API key (or setETHERSCAN_API_KEY
)--fail-early
: Stop as soon as any job fails--no-post-check-conditions
: Skip post-execution evaluation of skip conditions--flat-output
: Write outputs in a single flatoutput/
directory (do not mirrorjobs/
structure)--no-summary
: Hide the end-of-run summary--run-deprecated
: Allow running jobs markeddeprecated: 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:
- Using a custom RPC (no networks.yaml needed):
catapult run --rpc-url http://127.0.0.1:8545 -k $PRIVATE_KEY
- Write outputs flat instead of mirroring
jobs/
folders:
catapult run --flat-output -k $PRIVATE_KEY
- Run a deprecated job explicitly:
- Without flag (explicit targeting runs it):
catapult run legacy-job -k $PRIVATE_KEY
- Or include all deprecated jobs in the plan:
catapult run --run-deprecated -k $PRIVATE_KEY
- Without flag (explicit targeting runs it):
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
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
Catapult provides several built-in primitive actions:
Send 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:
- If a network gas limit is configured, it will be multiplied by this factor
- If no network gas limit is set, gas will be estimated first, then multiplied by this factor
- Must be a positive number (e.g., 1.5 for 50% more gas, 0.8 for 20% less gas)
Broadcast a pre-signed transaction:
- type: "send-signed-transaction"
arguments:
transaction: "0x..." # Raw signed transaction
Sets 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.
Create a contract by sending its creation bytecode (and optional value):
- type: "create-contract"
name: "deploy-foo"
arguments:
data: "{{Contract(Foo).creationCode}}"
gasMultiplier: 1.2
Make an HTTP JSON request and use the result downstream:
- type: "json-request"
name: "get-config"
arguments:
url: "https://example.com/config.json"
method: "GET"
Catapult includes powerful value resolvers for computing complex values:
ABI-encode function call data:
data:
type: "abi-encode"
arguments:
signature: "transfer(address,uint256)"
values:
- "0x742..."
- "1000000000000000000"
Encode constructor parameters with bytecode:
creationCode:
type: "constructor-encode"
arguments:
creationCode: "{{Contract(MyContract).creationCode}}"
types: ["address", "uint256"]
values: ["{{factory.address}}", "100"]
Pack values per ABI types into bytes:
payload:
type: "abi-pack"
arguments:
types: ["address", "uint256"]
values: ["{{recipient}}", "{{amount}}"]
Compute CREATE2 addresses:
address:
type: "compute-create2"
arguments:
deployerAddress: "{{factory.address}}"
salt: "{{salt}}"
initCode: "{{creationCode}}"
Perform mathematical operations:
amount:
type: "basic-arithmetic"
arguments:
operation: "add"
values: ["{{current_balance}}", "1000000000000000000"]
Read account balance:
balance:
type: "read-balance"
arguments:
address: "{{deployer_address}}"
Make view/pure function calls:
result:
type: "call"
arguments:
to: "{{contract.address}}"
signature: "getName()"
values: []
Read a value from a JSON object at a given path:
tokenAddress:
type: "read-json"
arguments:
json: "{{get-config.response}}"
path: "tokens.usdc.address"
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)
Avoid redundant operations with skip conditions:
Skip if contract exists at address:
skip_condition:
- type: "contract-exists"
arguments:
address: "{{computed_address}}"
Skip if another job is completed:
skip_condition:
- type: "job-completed"
arguments:
job: "prerequisite-job"
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 Deployererc-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 address- Raw building blocks:
raw-sequence-universal-deployer-2
,raw-nano-universal-deployer
,raw-erc-2470
Catapult automatically discovers and indexes contract artifacts in your project. It supports:
- JSON artifacts (Hardhat, Truffle, Foundry)
- Nested directory structures
- Hash-based contract references
- Path-based contract references
- Name-based contract references
Reference contracts in your YAML using the new unified Contract() syntax:
creationCode: "{{Contract(path/to/MyContract).creationCode}}"
# or
creationCode: "{{Contract(0x1234...hash).creationCode}}"
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:
- Minimal repetition: Successful deployments with identical outputs across multiple networks are grouped together
- Clear error tracking: Individual network failures are clearly documented
- Scalability: The format remains readable even with deployments across dozens of networks
Output layout and selection:
- By default, output files mirror the structure under
jobs/
(e.g.,jobs/core/job.yaml
->output/core/job.json
). Use--flat-output
to write all job JSON files directly underoutput/
. - You can control which action outputs are persisted per job using the
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
)
You can load environment variables from a file using --dotenv <path>
on the run
command (defaults to .env
in the current directory when provided).
- Node.js >= 16.0.0
- npm or yarn
# 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 changes