Deployment
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.
| Deployed | 2026-10-05, from main at ef6614c4aad7f1b21139d9dc225ecf38012a80b7 (recorded as gitCommit in deployments/56.json) |
| Release gate | scripts/release-check.sh --ack-unmerged (recorded as releaseAckUnmerged: true); the acknowledged exclusion is listed under branch state |
| Script | contracts/script/Deploy.s.sol (shared logic in DeployBase.sol), broadcast with --verify |
| Config | contracts/config/56.json |
| Output | contracts/deployments/56.json (source of truth); broadcast log in contracts/broadcast/Deploy.s.sol/56/ |
| Start block | 125812602 |
| Verification | All 12 contracts (the 11 in deployments/56.json plus the ZkPadDeployer library 0x11d72841feAda2c73CA78a19117E18A5A21e91dD) verified on BscScan |
| Addresses and roles | Addresses |
| Superseded | The 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):
- Release gate. Run every check in the release gate below and broadcast
only from the commit
scripts/release-check.shprints. - Fork tests first. With
BSC_RPC_URLset, run the BSC fork suites (forge test --match-test fork), includingtest_fork_mainnetConfigDeploysAndFeedsAreLive, which deploysconfig/56.jsonon a fork and checks every V3 route for liquidity and every configured Chainlink feed (answering, 8 decimals, updated within the registry's 1-hourMAX_ORACLE_AGE), the Railgun proxy and the sanctions oracle. They first passed on 2026-10-04, after which theUNVERIFIEDnotes on those entries inconfig/56.jsonwere removed. - Dry run of
Deploy.s.solagainst the fork (no--broadcast). - Broadcast and verify with
--broadcast --verify. The script deploys theZkPadDeployerlibrary and the 10 contracts in the order below (RailgunShieldAdapter deploys itsShieldSenderimplementation in its constructor), then runs the wiring calls. - Commit
deployments/56.jsonand 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, toorigin/mainbefore 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.
scripts/release-check.sh(enforced for contract broadcasts). It fetchesoriginand 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), whenHEADis notorigin/main, and when any remote branch has unmerged commits touchingcontracts/src,contracts/scriptorcontracts/config(unless--ack-unmergedis passed, which must be explained in the deploy notes). It warns about local branches with unpushed commits. On success it printsexport DEPLOY_GIT_COMMIT=<sha>:DeployBase.solrefuses a chain-56 deployment without a well-formedDEPLOY_GIT_COMMITand writes it asgitCommitintodeployments/56.json, so every deployment file names its source commit.- 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. - Fork tests, dry run, broadcast and verify (steps 2 to 4 above) from that clean checkout.
- 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 toorigin/main. No web or service deploy may point at the new addresses before that merge. - Deploy the web app, docs and services only from
origin/main. On Vercel, production deploys come frommain(the Git integration's production branch, orvercel --prodonly from a clean checkout oforigin/main). On Railway, connect each service to the GitHub repository onmain, or runrailway uponly from a clean checkout of anorigin/maincommit on whichscripts/release-check.shpasses. Record the deployed commit in the deploy notes. - Afterwards, check for drift.
scripts/preflight.sh(withBSC_RPC_URLset) runsscripts/check-deployed-bytecode.mjs, which fails when the runtime code at an address indeployments/56.jsondiffers from what the checked-out commit compiles to. Run it after every release and whenevermainchangescontracts/src. There is no GitHub Actions CI;scripts/preflight.shis 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.
| Branch | Unmerged work | Status |
|---|---|---|
origin/claude/blissful-lovelace-0vlhfj | Round-4 contract fixes, web redesign, claim-link rotation, payout codes, Discord and Farcaster, Railway IaC | Merged (PR #2, then PR #3 at ef6614c) |
deploy/bsc-mainnet | The 2026-10-04 deployment record and the mainnet switchover | Merged (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 changes | Deliberately 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-dapp | Indexer pricing from QuoteRegistry routes without Chainlink feeds (f99c1fe), report docs; no contract changes | Undecided: merge, or record as abandoned, before the next off-chain deploy |
origin/claude/zkpad-track-adapters | Relayer on-chain test fixture sets the shield adapter (0cbe770); no contract changes | Undecided: 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.
- Deploy a Safe multisig and a timelock whose delay is longer than
bindDelay. - From the deployer EOA, call
transferOwnership(safe)on FeeVault, ZkPadFactory, QuoteRegistry and RailgunShieldAdapter (Ownable2Step: ownership stays with the EOA until accepted). - From the Safe, call
acceptOwnership()on each of the four contracts, and read backowner()andpendingOwner(). - Move the other roles with their setters:
factory.setTeamFeeRecipient(treasury), andsetGuardian(newGuardian)on both FeeVault and QuoteRegistry if the guardian changes. - When independent attestors are ready:
feeVault.setAttestor(a_i, true)for each, thensetAttestorThreshold(k). Social-account binds stay impossible until this step. - Update
contracts/config/56.jsonroles, the addresses page and the trust assumptions to match.
Off-chain services
| Service | Hosting | URL |
|---|---|---|
Web app (apps/web) | Vercel | zkpad.family |
Docs (apps/docs) | Vercel | docs.zkpad.family |
Relayer (services/relayer) | Railway | https://relayer-production-76a3.up.railway.app |
Indexer (services/indexer) + Postgres | Railway | https://indexer-production-addc.up.railway.app |
Attestor (services/attestor) | not deployed | social-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)
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-04ZkPadFactory0xA91e4A3714b31ce8E4573F79fEDd65053f90b6a2in transaction0xbb3764e5…79868e5. Any launch through it, including a direct call or an older SDK build, reverts withDeprecated(). - Nothing to migrate. No coin was ever launched from that factory (its nonce is still 1), and
its FeeVault
0x555767e731A55b46f1FCb344Ac47ef57B7fE5dc6never 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-balanceConsolidateform and only understandsamountIn = 2^256 − 1. The entry is a safeguard only; the vault holds no balances.
Scripts
| Script | Purpose |
|---|---|
contracts/script/Deploy.s.sol | Any network. Reads contracts/config/<chainId>.json, deploys, wires, starts the ownership hand-over and writes contracts/deployments/<chainId>.json. |
contracts/script/LocalDev.s.sol | Anvil only. Deploys Infinity, mocks and seeded reference pools first, then runs the same DeployBase logic. Used by scripts/dev.sh. |
contracts/script/DeployBase.sol | The 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)
- FeeVault (
deployer, WBNB,guardian,bindDelayin [1, 30] days). - ZkPadFactory (
deployer, InfinityCLPoolManager, WBNB,teamFeeRecipient). - ZkPadHook (
CLPoolManager, factory). Hook permissions live in each pool'sPoolKey.parameters, so no address mining is needed. - ZkPadLpLocker (factory,
CLPoolManager, FeeVault), ZkPadMevDescendingFees, ZkPadSwapRouter (CLPoolManager, WBNB). - QuoteRegistry (
deployer, USDT, factory,guardian), InfinityCLRouteExecutor (CLPoolManager, WBNB), PancakeV3RouteExecutor (SwapRouter0x1b81D678ffb9C0263b24A97847620C99d213eB14, factory0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865). - 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)
| Call | Notes |
|---|---|
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, RailgunShieldAdapter | Ownable2Step; 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:
| Key | Meaning |
|---|---|
roles.owner | Intended 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.teamFeeRecipient | Protocol 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.bindDelay | Social-bind security. |
infinity.*, tokens.wbnb, tokens.usdt | External addresses. |
railgun.proxy, railgun.maxFeeBps, railgun.deployMock, railgun.sanctionsList | sanctionsList (optional) screens each shield's submitter; zero or absent leaves screening off. |
pancakeV3.swapRouter, pancakeV3.factory | Zero 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.jsonis 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.jsonuses 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 (attestorThreshold0), so handle escrows cannot bind until the owner callsFeeVault.setAttestorandsetAttestorThreshold.31337.jsonholds 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, orpnpm -C packages/sdk gen:deploymentsto embed the 56/97 files into the build; - relayer and attestor:
DEPLOYMENT_FILE=…; - web app:
scripts/dev.shwrites it intoapps/web/.env.localfor local runs.
Infinity addresses
| BSC mainnet (56) | BSC testnet (97) | |
|---|---|---|
| Vault | 0x238a358808379702088667322f80aC48bAd5e6c4 | 0x2CdB3EC82EE13d341Dc6E73637BE0Eab79cb79dD |
| CLPoolManager | 0xa0FfB9c1CE1Fe56963B0321B32E7A0302114058b | 0x36A12c70c9Cf64f24E89ee132BF93Df2DCD199d4 |
| CLProtocolFeeController | 0x15F6180033aEa66377d2A1778e418591C00dEb4c | 0x5ab6c844F3c0e818b92932e55b9942B2c8a2D205 |
| WBNB | 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c | 0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd |
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
| Step | Mainnet status |
|---|---|
Commit contracts/deployments/<chainId>.json, run pnpm -C packages/sdk gen:deployments, and update the addresses page | Done 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 deploy | Remaining (work on feat/bstocks-quotes) |
Branch protection on main requiring CI, so release work cannot stay on side branches | Remaining |
| Verify sources on BscScan | Done for 2026-10-05 (12 contracts) |
| The multisig accepts ownership of the four Ownable2Step contracts | Remaining (see the hand-over) |
| Register attestors and a threshold | Remaining (social-account binds disabled until then) |
| Apply to PancakeSwap to list the hook, so Infinity routers and the UI route to zk-pad pools | Remaining |
| External audit | Remaining |