Skip to main content

Deployment

Deployed on mainnet

zk-pad is deployed on BSC mainnet (chain 56). The admin contracts are owned by a single EOA until the hand-over below.

Mainnet deployment (chain 56)​

The current deployment was broadcast on 2026-10-05 from main at commit ef6614c (PR #3, which also merged PR #2). It includes the round-4 contract fixes (810c61c, 1127365): sells pay the protocol fee at execution value, the same-block conversion guard and protocol top-up (fees), and the quoted whole-balance Consolidate form that keeps the signed rate after a creator front-run (consolidation). It replaces a first deployment of 2026-10-04 that lacked those fixes; see superseded deployment.

Deployed2026-10-05, from main at ef6614c4aad7f1b21139d9dc225ecf38012a80b7 (recorded as gitCommit in deployments/56.json)
Release gatescripts/release-check.sh --ack-unmerged (recorded as releaseAckUnmerged: true); the acknowledged exclusion is listed under branch state
Scriptcontracts/script/Deploy.s.sol (shared logic in DeployBase.sol), broadcast with --verify
Configcontracts/config/56.json
Outputcontracts/deployments/56.json (source of truth); broadcast log in contracts/broadcast/Deploy.s.sol/56/
Start block125812602
VerificationAll 12 contracts (the 11 in deployments/56.json plus the ZkPadDeployer library 0x11d72841feAda2c73CA78a19117E18A5A21e91dD) verified on BscScan
Addresses and rolesAddresses
SupersededThe 2026-10-04 deployment: factory deprecated on-chain, no coins launched, record in contracts/deployments/56-2026-10-04-superseded.json (details)

How a mainnet broadcast is done (the release gate in step 1 was added after the 2026-10-04 deployment, which followed steps 2 to 5; the 2026-10-05 deployment passed it with --ack-unmerged):

  1. Release gate. Run every check in the release gate below and broadcast only from the commit scripts/release-check.sh prints.
  2. Fork tests first. With BSC_RPC_URL set, run the BSC fork suites (forge test --match-test fork), including test_fork_mainnetConfigDeploysAndFeedsAreLive, which deploys config/56.json on a fork and checks every V3 route for liquidity and every configured Chainlink feed (answering, 8 decimals, updated within the registry's 1-hour MAX_ORACLE_AGE), the Railgun proxy and the sanctions oracle. They first passed on 2026-10-04, after which the UNVERIFIED notes on those entries in config/56.json were removed.
  3. Dry run of Deploy.s.sol against the fork (no --broadcast).
  4. Broadcast and verify with --broadcast --verify. The script deploys the ZkPadDeployer library and the 10 contracts in the order below (RailgunShieldAdapter deploys its ShieldSender implementation in its constructor), then runs the wiring calls.
  5. Commit deployments/56.json and the broadcast log, and regenerate the SDK's embedded deployments (pnpm -C packages/sdk gen:deployments). Merge that commit, and the address switch that goes with it, to origin/main before any web or service deploy (gate step 4).

Release gate​

The first mainnet deployment was broadcast from main while 48 commits, including the round-4 contract fixes and most of the newer web, SDK and service work, were still on an unmerged branch, and the deployment record and the off-chain switchover then lived only on local, unpushed branches. Nothing checked either. The gate below applies to every mainnet contract broadcast and every production deploy of the web app, relayer, indexer or attestor.

  1. scripts/release-check.sh (enforced for contract broadcasts). It fetches origin and fails on a merge, rebase or cherry-pick in progress, on a dirty working tree (staged, modified or untracked files, submodules off their recorded commits), when HEAD is not origin/main, and when any remote branch has unmerged commits touching contracts/src, contracts/script or contracts/config (unless --ack-unmerged is passed, which must be explained in the deploy notes). It warns about local branches with unpushed commits. On success it prints export DEPLOY_GIT_COMMIT=<sha>: DeployBase.sol refuses a chain-56 deployment without a well-formed DEPLOY_GIT_COMMIT and writes it as gitCommit into deployments/56.json, so every deployment file names its source commit.
  2. Whole-branch review (manual; the script only blocks contract inputs). For every branch listed by git branch -a --no-merged origin/main (remote and local branches, including worktree branches), git log --oneline origin/main..<branch> with no path filter must be empty, or the branch is merged first, or it is recorded as abandoned in the table below with the reason. Unmerged web, SDK and service work blocks the off-chain deploy just as unmerged contract work blocks the broadcast.
  3. Fork tests, dry run, broadcast and verify (steps 2 to 4 above) from that clean checkout.
  4. Merge the record before the switch. Open a PR with deployments/56.json, the broadcast log, the regenerated SDK deployments and the address swap, and merge it to origin/main. No web or service deploy may point at the new addresses before that merge.
  5. Deploy the web app, docs and services only from origin/main. On Vercel, production deploys come from main (the Git integration's production branch, or vercel --prod only from a clean checkout of origin/main). On Railway, connect each service to the GitHub repository on main, or run railway up only from a clean checkout of an origin/main commit on which scripts/release-check.sh passes. Record the deployed commit in the deploy notes.
  6. Afterwards, check for drift. scripts/preflight.sh (with BSC_RPC_URL set) runs scripts/check-deployed-bytecode.mjs, which fails when the runtime code at an address in deployments/56.json differs from what the checked-out commit compiles to. Run it after every release and whenever main changes contracts/src. There is no GitHub Actions CI; scripts/preflight.sh is the full check suite.

Branch state at the 2026-10-05 broadcast​

scripts/release-check.sh was run with --ack-unmerged, and deployments/56.json records it (releaseAckUnmerged: true, gitCommit ef6614c). The table lists every branch relevant at that commit (merged or not) and the decision taken.

BranchUnmerged workStatus
origin/claude/blissful-lovelace-0vlhfjRound-4 contract fixes, web redesign, claim-link rotation, payout codes, Discord and Farcaster, Railway IaCMerged (PR #2, then PR #3 at ef6614c)
deploy/bsc-mainnetThe 2026-10-04 deployment record and the mainnet switchoverMerged (PR #3 at ef6614c)
feat/bstocks-quotes (local)bStocks quotes: the AddQuotes.s.sol script, config/56.bstocks.json, DeployBase.sol helpers and fork/e2e tests, plus the SDK bStocks data and generator, indexer bStock pricing and the web RWAs Stocks tab; no contracts/src changesDeliberately excluded from the broadcast (the reason for --ack-unmerged). The bStocks quotes will be registered later with AddQuotes against the 2026-10-05 QuoteRegistry. Its SDK, indexer and web work is undecided: merge, or record as abandoned, before the next off-chain deploy
origin/claude/zkpad-track-dappIndexer pricing from QuoteRegistry routes without Chainlink feeds (f99c1fe), report docs; no contract changesUndecided: merge, or record as abandoned, before the next off-chain deploy
origin/claude/zkpad-track-adaptersRelayer on-chain test fixture sets the shield adapter (0cbe770); no contract changesUndecided: merge, or record as abandoned, before the next off-chain deploy

Launch roles in config/56.json (the same for both mainnet deployments): owner and teamFeeRecipient are the deployer EOA, so the script skipped the ownership hand-over (pendingOwner is zero); guardian is a separate EOA; attestors is empty and attestorThreshold is 0, so social-account binds are disabled; bindDelay is 172800 (48 hours).

Planned ownership hand-over​

Not done yet. Until it is, the deployer EOA holds every owner power listed in trust assumptions, with no delay.

  1. Deploy a Safe multisig and a timelock whose delay is longer than bindDelay.
  2. From the deployer EOA, call transferOwnership(safe) on FeeVault, ZkPadFactory, QuoteRegistry and RailgunShieldAdapter (Ownable2Step: ownership stays with the EOA until accepted).
  3. From the Safe, call acceptOwnership() on each of the four contracts, and read back owner() and pendingOwner().
  4. Move the other roles with their setters: factory.setTeamFeeRecipient(treasury), and setGuardian(newGuardian) on both FeeVault and QuoteRegistry if the guardian changes.
  5. When independent attestors are ready: feeVault.setAttestor(a_i, true) for each, then setAttestorThreshold(k). Social-account binds stay impossible until this step.
  6. Update contracts/config/56.json roles, the addresses page and the trust assumptions to match.

Off-chain services​

ServiceHostingURL
Web app (apps/web)Vercelzkpad.family
Docs (apps/docs)Verceldocs.zkpad.family
Relayer (services/relayer)Railwayhttps://relayer-production-76a3.up.railway.app
Indexer (services/indexer) + PostgresRailwayhttps://indexer-production-addc.up.railway.app
Attestor (services/attestor)not deployedsocial-account binds stay disabled

The live Railway services are configured in the Railway dashboard: chain 56, the relayer, the indexer and Postgres only, with no attestor. docs/DEPLOY_RAILWAY.md in the repository is a general Railway guide that differs from that project: it starts on BSC testnet (chain 97) and also deploys an attestor with a volume. .railway/railway.ts is an optional Infrastructure-as-Code version of that guide that the live project does not use. The live services were first uploaded from local snapshots rather than built from a repository commit; from the 2026-10-05 deployment on, they follow release gate step 5. Service images build from the repository root; they declare no Docker VOLUME (Railway rejects it), so attach any volume in the dashboard. The relayer, indexer and attestor resolve the contract addresses and the indexer start block (125812602) from the 2026-10-05 deployments/56.json (through DEPLOYMENT_FILE or the SDK's embedded deployments), so they must run an origin/main commit that contains that record. The relayer discards an account-index snapshot of the 2026-10-04 FeeVault and rescans, and the indexer starts a fresh schema from the new start block.

Superseded deployment (2026-10-04)​

Do not use the 2026-10-04 contracts

The first mainnet deployment is superseded. Its factory is deprecated on-chain. Builds of the app, SDK and services from the commit that merges the 2026-10-05 record on point at the new contracts; older builds, including ef6614c itself, still point at the 2026-10-04 addresses.

The first mainnet deployment (2026-10-04, start block 125685708) was broadcast from main at commit 7c5afec, before the release gate existed and while the round-4 contract fixes were still on an unmerged branch. It was replaced by the 2026-10-05 deployment above.

  • Record kept. Its deployment file is contracts/deployments/56-2026-10-04-superseded.json. Its addresses are listed under superseded (do not use).
  • Factory deprecated. The owner EOA called setDeprecated(true) on the 2026-10-04 ZkPadFactory 0xA91e4A3714b31ce8E4573F79fEDd65053f90b6a2 in transaction 0xbb3764e5…79868e5. Any launch through it, including a direct call or an older SDK build, reverts with Deprecated().
  • Nothing to migrate. No coin was ever launched from that factory (its nonce is still 1), and its FeeVault 0x555767e731A55b46f1FCb344Ac47ef57B7fE5dc6 never held funds. No pool, LP position or beneficiary balance lives on the 2026-10-04 contracts.
  • Legacy handling in the web app. The app keeps that FeeVault in SENTINEL_ONLY_CONSOLIDATE_VAULTS (apps/web/src/components/beneficiaries/requests.ts): it predates the quoted whole-balance Consolidate form and only understands amountIn = 2^256 − 1. The entry is a safeguard only; the vault holds no balances.

Scripts​

ScriptPurpose
contracts/script/Deploy.s.solAny network. Reads contracts/config/<chainId>.json, deploys, wires, starts the ownership hand-over and writes contracts/deployments/<chainId>.json.
contracts/script/LocalDev.s.solAnvil only. Deploys Infinity, mocks and seeded reference pools first, then runs the same DeployBase logic. Used by scripts/dev.sh.
contracts/script/DeployBase.solThe shared deployment and wiring, tested by test/e2e/DeployScript.t.sol.
# mainnet only: the release gate; DeployBase refuses chain 56 without DEPLOY_GIT_COMMIT
eval "$(scripts/release-check.sh | grep '^export DEPLOY_GIT_COMMIT=')"
cd contracts
# dry run against a fork (no --broadcast): every route and feed is checked on-chain
PRIVATE_KEY=0x… forge script script/Deploy.s.sol --rpc-url $BSC_RPC_URL
# mainnet (how chain 56 was deployed)
PRIVATE_KEY=0x… forge script script/Deploy.s.sol --rpc-url $BSC_RPC_URL --broadcast --verify
# testnet
PRIVATE_KEY=0x… forge script script/Deploy.s.sol --rpc-url $BSC_TESTNET_RPC_URL --broadcast --verify

ZkPadDeployer is an external library. forge script deploys and links it automatically. With forge create, pass --libraries.

Order (what the script does)​

  1. FeeVault (deployer, WBNB, guardian, bindDelay in [1, 30] days).
  2. ZkPadFactory (deployer, Infinity CLPoolManager, WBNB, teamFeeRecipient).
  3. ZkPadHook (CLPoolManager, factory). Hook permissions live in each pool's PoolKey.parameters, so no address mining is needed.
  4. ZkPadLpLocker (factory, CLPoolManager, FeeVault), ZkPadMevDescendingFees, ZkPadSwapRouter (CLPoolManager, WBNB).
  5. QuoteRegistry (deployer, USDT, factory, guardian), InfinityCLRouteExecutor (CLPoolManager, WBNB), PancakeV3RouteExecutor (SwapRouter 0x1b81D678ffb9C0263b24A97847620C99d213eB14, factory 0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865).
  6. RailgunShieldAdapter (deployer, Railgun proxy, FeeVault, USDT, maxFeeBps = 25). On testnet and anvil, where Railgun does not exist, the config may ask for the behavioural mock (railgun.deployMock). The script refuses to do that on chain 56.

Wiring (what the script calls)​

CallNotes
factory.setHook(hook, true), setLocker(locker, hook, true), setMevModule(mev, true)
factory.setQuoteToken(q, {true, min, max})every configured quote, tick bounds from quote tokens
factory.setAdmin(quoteRegistry, true)the registry may only call setQuoteToken
registry.setQuote(USDT, {tier 1, no route})first
registry.setQuote(q, {route, executor, feed, maxDeviationBps, maxSwapSize})every routed Tier-1 quote; the registry checks the route on-chain and reads the feed
feeVault.setConsolidationConfig(registry, USDT, factory)USDT and the factory are set once (later calls may only replace the registry); the first call sets minCreatorShield to 1 USDT
feeVault.setAdapter(railgunAdapter, true), setShieldAdapter(railgunAdapter)
railgunAdapter.setSanctionsList(oracle)only when railgun.sanctionsList is set (56.json: the Chainalysis oracle, fork-checked on 2026-10-04)
feeVault.setAttestor(a_i, true), setAttestorThreshold(k)from roles.attestors
transferOwnership(roles.owner) on FeeVault, ZkPadFactory, QuoteRegistry, RailgunShieldAdapterOwnable2Step; the multisig must call acceptOwnership() on each. Skipped when roles.owner is the deployer (as on mainnet at launch).

Configuration files​

contracts/config/<chainId>.json:

KeyMeaning
roles.ownerIntended to be a multisig behind a timelock. Required (non-zero) on chain 56. Zero or the deployer address keeps the deployer as owner (mainnet launch: the deployer EOA).
roles.teamFeeRecipientProtocol treasury (25% share). Required on chain 56; elsewhere zero means the deployer (the factory owner can change it with setTeamFeeRecipient).
roles.guardian, roles.attestors, roles.attestorThreshold, roles.bindDelaySocial-bind security.
infinity.*, tokens.wbnb, tokens.usdtExternal addresses.
railgun.proxy, railgun.maxFeeBps, railgun.deployMock, railgun.sanctionsListsanctionsList (optional) screens each shield's submitter; zero or absent leaves screening off.
pancakeV3.swapRouter, pancakeV3.factoryZero disables the V3 executor.
quotes[]symbol, token, tier, minStartingTick, maxStartingTick, route ({"kind":"none"}, {"kind":"pcsV3","path":[…],"fees":[…]} or {"kind":"infinityCL",…}), priceFeed, maxDeviationBps, maxSwapSize (decimal string).
  • 56.json is the mainnet config. Its feeds, V3 routes and sanctions oracle were checked on a BSC fork on 2026-10-04. Re-run the fork tests before any new broadcast.
  • 97.json uses the testnet Infinity config and PancakeSwap test tokens and deploys as committed. It has no feeds and no verified V3 pools, so testnet quotes are factory-only and consolidation is off until a verified route is registered; the launch wizard still offers every factory-enabled quote. It configures no attestors (attestorThreshold 0), so handle escrows cannot bind until the owner calls FeeVault.setAttestor and setAttestorThreshold.
  • 31337.json holds anvil roles (three attestors, threshold 2). LocalDev fills in the rest.

Output​

contracts/deployments/<chainId>.json (schema zk-pad.deployment.v1) contains the deployer, owner, pendingOwner, startBlock, every zk-pad contract, the external addresses and the quote list with decimals, tick bounds, feed and caps. Consumers:

  • SDK: registerDeployment(json) at runtime, or pnpm -C packages/sdk gen:deployments to embed the 56/97 files into the build;
  • relayer and attestor: DEPLOYMENT_FILE=…;
  • web app: scripts/dev.sh writes it into apps/web/.env.local for local runs.

Infinity addresses​

BSC mainnet (56)BSC testnet (97)
Vault0x238a358808379702088667322f80aC48bAd5e6c40x2CdB3EC82EE13d341Dc6E73637BE0Eab79cb79dD
CLPoolManager0xa0FfB9c1CE1Fe56963B0321B32E7A0302114058b0x36A12c70c9Cf64f24E89ee132BF93Df2DCD199d4
CLProtocolFeeController0x15F6180033aEa66377d2A1778e418591C00dEb4c0x5ab6c844F3c0e818b92932e55b9942B2c8a2D205
WBNB0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd

Mainnet values come from infinity-core/script/config/bsc-mainnet.json and testnet values from bsc-testnet.json in the same directory. Railgun has no BSC testnet deployment, so Railgun claims can only be tested locally (mock) or on a mainnet fork.

After deploying​

StepMainnet status
Commit contracts/deployments/<chainId>.json, run pnpm -C packages/sdk gen:deployments, and update the addresses pageDone for 2026-10-05
Pass the release gate for the contract broadcast and redeploy from the merged code (round-4 fixes)Done: 2026-10-05 from ef6614c, --ack-unmerged for feat/bstocks-quotes (branch state)
Deprecate the 2026-10-04 factory with setDeprecated(true)Done (tx; details)
Merge the 2026-10-05 record and address switch to main, then deploy the web app, docs and services from that commit (gate steps 4 and 5)Remaining until the live services run that commit
Register the bStocks quotes with AddQuotes against the 2026-10-05 QuoteRegistry, and merge or abandon the branch's SDK, indexer and web work before the off-chain deployRemaining (work on feat/bstocks-quotes)
Branch protection on main requiring CI, so release work cannot stay on side branchesRemaining
Verify sources on BscScanDone for 2026-10-05 (12 contracts)
The multisig accepts ownership of the four Ownable2Step contractsRemaining (see the hand-over)
Register attestors and a thresholdRemaining (social-account binds disabled until then)
Apply to PancakeSwap to list the hook, so Infinity routers and the UI route to zk-pad poolsRemaining
External auditRemaining