How-to guide

Deploy testnet contracts

The testnet deployer installs the complete deterministic Zoltar and Statoblast infrastructure in one run. It is safe to rerun: a contract already at its target address is skipped only when its runtime bytecode matches, and an unexpected contract at a target address stops the deployment.

Before you deploy

  • Complete the repository setup in the README.
  • Use a dedicated testnet account. To size its balance without deploying, run the deployer once with --max-total-cost-eth=0.000001: its preflight aborts and the abort message reports the upper-bound cost of the missing steps. A normal run prints the same figure in its Estimated maximum cost log line.
  • Use an HTTPS RPC endpoint. Loopback HTTP endpoints are accepted for local test networks.
  • Make sure the deployer account has no pending transactions.
  • If the testnet rejects the fixed raw legacy transactions that install the canonical CREATE2 deployer and the proxy deployer, make sure both are already present as predeploys.

Sepolia is the default target (chain ID 11155111). A different testnet must support EIP-1559, the Cancun opcodes used by Zoltar and Uniswap V4, and the Osaka CLZ opcode used by the compiled contracts. The deployer rejects Ethereum mainnet (chain ID 1).

Deploy with GitHub Actions

Use the Deploy Testnet Contracts workflow for a deployment from main.

  1. Create a protected GitHub environment named testnet-deployment.
  2. Add the deployer's private key as the environment secret TESTNET_DEPLOYER_PRIVATE_KEY. Optionally add ETHERSCAN_API_KEY as well; without it post-deploy verification skips Etherscan.
  3. Open Actions → Deploy Testnet Contracts → Run workflow.
  4. Select main, complete the inputs, and enter DEPLOY as the confirmation.
  5. Review the job summary for each planned deployment's result and address. It includes transaction hashes for contracts deployed during the run.

The job runs only when the workflow is started from main and the confirmation is exactly DEPLOY. Otherwise GitHub skips it without an error and no summary is produced; if the run shows a skipped job, check the branch and confirmation and start it again.

Use a public RPC URL without credentials. Workflow inputs are stored in GitHub metadata and are not secret.

Deploy locally

Load PRIVATE_KEY from a secret manager or a hidden prompt. Never paste the key into a command, because the command may be saved in shell history. In Bash:

read -rsp 'Testnet deployer private key: ' PRIVATE_KEY \
  && echo && export PRIVATE_KEY

Run the deployer with an explicit RPC endpoint and spending limits:

bun run deploy:testnet -- \
  --rpc-url=https://rpc.example \
  --chain-id=11155111 \
  --max-fee-per-gas-nanoeth=100 \
  --max-total-cost-eth=20

Remove the key from the shell when the command finishes:

unset PRIVATE_KEY

The deployer reads the exported PRIVATE_KEY automatically. You can instead pass --private-key=0x... directly, but then the complete command, and therefore the key, may be saved in shell history. Run bun run deploy:testnet -- --help for all options. Options other than --private-key also accept uppercase arguments after -- or environment variables.

Input Default Purpose
RPC_URL / --rpc-url Required RPC endpoint for the target network
CHAIN_ID / --chain-id 11155111 Expected decimal chain ID
MAX_FEE_PER_GAS_NANO_ETH / --max-fee-per-gas-nanoeth 100 Rejects higher RPC fee suggestions. If a canonical deployer is missing, the run aborts when this is below 100, the fixed gas price of the deployers' raw transactions.
MAX_TOTAL_COST_ETH / --max-total-cost-eth 20 Caps the conservative preflight estimate and transaction budget
PRIVATE_KEY / --private-key Required 0x-prefixed 32-byte deployer key

The defaults are authorization limits, not a spend forecast or a required balance. Before sending a transaction, the command checks the RPC chain ID, EVM features, EIP-1559 support, canonical deployer compatibility, expected bytecode, and fee limits. It then estimates only the missing deployment steps. If the conservative estimate exceeds MAX_TOTAL_COST_ETH, it exits; on a live network nothing has been funded or deployed at that point, while an Anvil development node may already have received the replayed Uniswap contracts. Per-transaction checks enforce the same budget while the deployment runs.

The run logs each planned contract with Status: deployed, Status: already deployed, or Status: ready (installed without a submitted transaction), then verifies the bootstrap support contracts. A step that fails logs Status: failed and stops the run.

When deployment succeeds, the command runs source verification for the target chain. Only Sepolia (11155111) is verified; any other chain logs that it has no known explorer support and succeeds. Verification submits the addresses in the tracked Yes and No manifests, docs/sepolia-deployment-addresses.json and docs/sepolia-no-deployment-addresses.json, not addresses discovered during the run. Blockscout and Sourcify run without a key; Etherscan runs only when ETHERSCAN_API_KEY is set and is otherwise skipped with a message. A verification failure makes the command exit nonzero even though the contracts are deployed, and prints the command to retry on its own: bun run verify:contracts -- --chain-id=11155111. Treat a nonzero exit as "check which stage failed" rather than "nothing was deployed".

Recover an interrupted run

Wait for all pending transactions to settle, then rerun the same command. The deployer revalidates completed contracts and resumes with the first missing step.

What gets deployed

  • separate Yes and No genesis REP tokens, each with the reviewed allocations
  • the canonical CREATE2 deployer, the proxy deployer, and Permit2
  • a deterministic Uniswap V3 SwapRouter
  • on chains other than Sepolia, deterministic WETH and a complete Uniswap deployment (V3 factory, QuoterV2, V4 PoolManager, and V4 Quoter)
  • Zoltar and Statoblast deployments and their bootstrap support contracts for both genesis outcomes
  • a Trading factory and router for each genesis outcome, at the canonical 0.30% fee

The command does not create Uniswap pools or add liquidity; protocol factories create market-specific contracts later, when those features are used. Uniswap sourcing per chain is listed in the contract inventory, and the deployment status reference explains how clients read which steps are installed.

The bots use the Yes deployment in the tracked mainnet and Sepolia manifests, so on testnets they work only against Sepolia and Sepolia replays such as a local Anvil node with chain ID 11155111. A deployment to another testnet is not usable by the bots until that chain has a generated manifest.