Files
proxmox/docs/07-ccip/CW_DEPLOY_AND_WIRE_RUNBOOK.md
defiQUG dbd517b279 Sync workspace: config, docs, scripts, CI, operator rules, and submodule pointers.
- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains
- Omit embedded publish git dirs and empty placeholders from index

Made-with: Cursor
2026-04-12 06:12:20 -07:00

20 KiB
Raw Blame History

Runbook: Deploy cW* and Wire Config

Created: 2026-02-27
Purpose: Steps to deploy generic cW* mirrors on a chain, set CW_BRIDGE_<CHAIN>, update token-mapping, and verify roles. Covers Phase D and E from CW_BRIDGE_TASK_LIST.md.

For the single-runbook Proxmox operator flow, including generic lane wiring and generic outbound sends, see PROXMOX_CSTAR_TO_CW_BRIDGE_RUNBOOK.md.


Prerequisites

  • smom-dbis-138/.env has CW_BRIDGE_<CHAIN> set (already done from deployed bridge suite for Mainnet, Cronos, BSC, Polygon, Gnosis, Avalanche, Base, Arbitrum, Optimism).
  • For cross-chain mint to work, the bridge at that address must either be extended to mint cW* in ccipReceive or you must deploy a dedicated cW* receiver (e.g. TwoWayTokenBridgeL2) and point CW_BRIDGE_<CHAIN> to it; see CW_BRIDGE_APPROACH.md.
  • RPC URL and PRIVATE_KEY for the target chain(s).

Optional hard-peg deployment knobs

DeployCWTokens.s.sol now supports:

  • CW_STRICT_MODE=1 — revoke deployer MINTER_ROLE / BURNER_ROLE after granting the bridge
  • CW_GOVERNANCE_ADMIN=0x... — grant DEFAULT_ADMIN_ROLE to governance; in strict mode the deployer admin role is revoked when this is set
  • CW_FREEZE_OPERATIONAL_ROLES=1 — freeze future MINTER_ROLE / BURNER_ROLE changes on the token after setup

For production hard-peg rollouts, use at least CW_STRICT_MODE=1.

Strict bridge hard-peg requirements

If you are using CWMultiTokenBridgeL1.sol and CWMultiTokenBridgeL2.sol for cWUSDC / cWUSDT, strict mode now also means:

  • L1 must explicitly allowlist the canonical token with configureSupportedCanonicalToken(token, true)
  • L1 should set a per-destination ceiling with setMaxOutstanding(token, chainSelector, amount) unless you intentionally want unlimited capacity
  • L1 should attach CWReserveVerifier.sol so new outbound wraps are blocked when canonical backing is unsafe
  • L2 token pairs and destination peers should be frozen after wiring with freezeTokenPair(canonicalToken) and freezeDestination(chainSelector)
  • Admin withdrawal of supported canonical escrow is blocked while funds are locked, so “rescue” flows must use the bridge or pause process instead of withdrawToken

Operational note: the verifier gates new lockAndSend mints. Return ccipReceive releases on Chain 138 are intentionally left live so users are not trapped in wrapped positions during a reserve incident.


Phase D: Deploy cW* and wire config

One-command helper (from repo root):
./scripts/deployment/run-cw-remaining-steps.sh runs a dry-run and --update-mapping by default. Use --deploy to broadcast, then set CWUSDT_/CWUSDC_ in .env from output and run again with --update-mapping (or run --update-mapping after editing .env). Use --verify to check MINTER/BURNER roles per chain and --verify-hard-peg to inspect the Avalanche hard-peg bridge state (supportedCanonicalToken, maxOutstanding, verifier attachment/config, and L2 freeze flags).

Generic lane wiring helper (from repo root):
./scripts/deployment/configure-cstar-cw-bridge-pair.sh --chain <CHAIN> --asset <c*> wires one canonical-to-mirrored lane on CWMultiTokenBridgeL1 / CWMultiTokenBridgeL2. Add --execute to broadcast and --freeze for strict-mode production locks.

Generic outbound send helper (from repo root):
./scripts/bridge/bridge-cstar-to-cw.sh --asset <c*> --chain <CHAIN> --amount <human> preflights allowance plus calculateFee(address,uint64,address,uint256) and prints or submits the exact lockAndSend(...) call. Add --approve --execute to broadcast.

D1. Run cW* deploy

All supported chains:

cd smom-dbis-138
./scripts/deployment/deploy-tokens-and-weth-all-chains-skip-canonical.sh --deploy-cw

Or from repo root (runs same deploy, then can run update-mapping/verify):

./scripts/deployment/run-cw-remaining-steps.sh --deploy

Single chain (e.g. BSC 56):

cd smom-dbis-138
source .env
# Use the per-chain bridge; script will pick CW_BRIDGE_BSC when running for chain 56
CW_BRIDGE_ADDRESS="$CW_BRIDGE_BSC" forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens \
  --rpc-url "$BSC_RPC_URL" --chain-id 56 --broadcast --private-key "$PRIVATE_KEY" --legacy

Strict production example:

cd smom-dbis-138
source .env
CW_STRICT_MODE=1 \
CW_GOVERNANCE_ADMIN=0xYourMultisig \
CW_BRIDGE_ADDRESS="$CW_BRIDGE_BSC" forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens \
  --rpc-url "$BSC_RPC_URL" --chain-id 56 --broadcast --private-key "$PRIVATE_KEY" --legacy

Or with the wrapper (target one chain only if the script supports --chain 56):

./scripts/deployment/deploy-tokens-and-weth-all-chains-skip-canonical.sh --deploy-cw --chain 56

Record the printed addresses for cWUSDT and cWUSDC per chain.

D2. Set CWUSDT_ and CWUSDC_ in .env

Append or update in smom-dbis-138/.env (replace with actual addresses from D1 output):

# Example for BSC (56)
CWUSDT_BSC=0x...
CWUSDC_BSC=0x...

# Example for Polygon (137)
CWUSDT_POLYGON=0x...
CWUSDC_POLYGON=0x...

Use the chain suffix that matches the deploy script: MAINNET, CRONOS, BSC, POLYGON, GNOSIS, AVALANCHE, BASE, ARBITRUM, OPTIMISM.

D3. Update token-mapping-multichain.json

Automated: After setting CWUSDT_<CHAIN> and CWUSDC_<CHAIN> in smom-dbis-138/.env, run from repo root:

./scripts/deployment/run-cw-remaining-steps.sh --update-mapping

This updates config/token-mapping-multichain.json for all chains that have CWUSDT_*/CWUSDC_* in .env (138→chain pairs; Compliant_USDT_cW, Compliant_USDC_cW, and Compliant_EURC_cW if CWEURC_* is set).

Manual: For each chain where cW* was deployed, set addressTo for the _cW entries (replace the 0x0 placeholder) in config/token-mapping-multichain.json: Compliant_USDT_cW → CWUSDT_, Compliant_USDC_cW → CWUSDC_, Compliant_EURC_cW if cWEURC deployed.

D3b. Update the active GRU Transport overlay

After the mapping is correct, confirm or update config/gru-transport-active.json:

  • ensure the destination chain is enabled
  • ensure the transportPairs entry points at the correct peerKey
  • ensure maxOutstanding policy is set for the pair
  • ensure the reserve-verifier reference is correct for hard-peg pairs
  • leave public pools inactive until they are actually deployed and recorded in deployment-status.json

D4. Verify on-chain

Confirm the bridge/receiver has MINTER_ROLE and BURNER_ROLE on the cW* token:

# MINTER_ROLE = keccak256("MINTER_ROLE")
cast keccak "MINTER_ROLE"
# Example: 0x...

# Check cWUSDT on BSC (replace addresses and rpc)
cast call <CWUSDT_BSC> "hasRole(bytes32,address)(bool)" $(cast keccak "MINTER_ROLE") $CW_BRIDGE_BSC --rpc-url $BSC_RPC_URL
cast call <CWUSDT_BSC> "hasRole(bytes32,address)(bool)" $(cast keccak "BURNER_ROLE") $CW_BRIDGE_BSC --rpc-url $BSC_RPC_URL

Both should return true.

D5. Configure strict escrow bridge state

For hard-peg deployments using CWMultiTokenBridgeL1 / CWMultiTokenBridgeL2, wire the bridge state after roles are verified.

Example on Chain 138 for cUSDC -> cWUSDC on Avalanche:

cd smom-dbis-138 && source .env

# Allowlist the canonical token on the 138-side escrow bridge
cast send "$CHAIN138_L1_BRIDGE" "configureSupportedCanonicalToken(address,bool)" "$CUSDC_138" true \
  --rpc-url "$RPC_URL_138" --private-key "$PRIVATE_KEY" --legacy

# Optional but recommended: cap how much this destination can keep outstanding
cast send "$CHAIN138_L1_BRIDGE" "setMaxOutstanding(address,uint64,uint256)" "$CUSDC_138" 6433500567565415381 1000000000000 \
  --rpc-url "$RPC_URL_138" --private-key "$PRIVATE_KEY" --legacy

Example on the destination chain:

cd smom-dbis-138 && source .env

# Freeze the token pair once canonical -> wrapped mapping is correct
cast send "$AVAX_CW_BRIDGE" "freezeTokenPair(address)" "$CUSDC_138" \
  --rpc-url "$AVALANCHE_RPC_URL" --private-key "$PRIVATE_KEY" --legacy

# Freeze the Chain 138 peer once the bridge address is confirmed
cast send "$AVAX_CW_BRIDGE" "freezeDestination(uint64)" 138 \
  --rpc-url "$AVALANCHE_RPC_URL" --private-key "$PRIVATE_KEY" --legacy

Recommended verification calls:

cast call "$CHAIN138_L1_BRIDGE" "supportedCanonicalToken(address)(bool)" "$CUSDC_138" --rpc-url "$RPC_URL_138"
cast call "$CHAIN138_L1_BRIDGE" "maxOutstanding(address,uint64)(uint256)" "$CUSDC_138" 6433500567565415381 --rpc-url "$RPC_URL_138"
cast call "$AVAX_CW_BRIDGE" "tokenPairFrozen(address)(bool)" "$CUSDC_138" --rpc-url "$AVALANCHE_RPC_URL"
cast call "$AVAX_CW_BRIDGE" "destinationFrozen(uint64)(bool)" 138 --rpc-url "$AVALANCHE_RPC_URL"

For the production Avalanche route, smom-dbis-138/scripts/deployment/complete-nonprefunded-avax-cutover.sh now applies these controls directly from env:

  • CW_MAX_OUTSTANDING_USDT_AVALANCHE
  • CW_MAX_OUTSTANDING_USDC_AVALANCHE
  • CW_FREEZE_AVAX_L2_CONFIG

D6. Deploy and attach the canonical reserve verifier

Use the helper script in smom-dbis-138/script/DeployCWReserveVerifier.s.sol to deploy the verifier and optionally attach it to CWMultiTokenBridgeL1.

Example:

cd smom-dbis-138 && source .env

CW_L1_BRIDGE="$CHAIN138_L1_BRIDGE" \
CW_STABLECOIN_RESERVE_VAULT="$STABLECOIN_RESERVE_VAULT" \
CW_RESERVE_SYSTEM="$RESERVE_SYSTEM" \
CW_CANONICAL_USDT="$CUSDT_138" \
CW_CANONICAL_USDC="$CUSDC_138" \
CW_USDT_RESERVE_ASSET=0xOfficialUSDTReserveAsset \
CW_USDC_RESERVE_ASSET=0xOfficialUSDCReserveAsset \
forge script script/DeployCWReserveVerifier.s.sol:DeployCWReserveVerifier \
  --rpc-url "$RPC_URL_138" --broadcast --private-key "$PRIVATE_KEY" --legacy

The script defaults to:

  • attaching the verifier to the L1 bridge
  • requiring vault backing when CW_STABLECOIN_RESERVE_VAULT is set
  • requiring reserve-system balance checks when CW_RESERVE_SYSTEM is set
  • requiring canonical token ownership to match the reserve vault when a vault is set

Recommended post-deploy verification:

cast call "$CHAIN138_L1_BRIDGE" "reserveVerifier()(address)" --rpc-url "$RPC_URL_138"
cast call <CW_RESERVE_VERIFIER> "verifyLock(address,uint64,uint256)(bool)" "$CUSDC_138" 6433500567565415381 1 --rpc-url "$RPC_URL_138"
cast call <CW_RESERVE_VERIFIER> "getVerificationStatus(address,uint64)((bool,bool,bool,bool,bool,bool,bool,bool,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))" "$CUSDC_138" 6433500567565415381 --rpc-url "$RPC_URL_138"

For the production Avalanche route, smom-dbis-138/scripts/deployment/complete-nonprefunded-avax-cutover.sh now also reads and converges:

  • CW_RESERVE_VERIFIER_CHAIN138
  • CW_STABLECOIN_RESERVE_VAULT
  • CW_RESERVE_SYSTEM
  • CW_ATTACH_VERIFIER_TO_L1
  • CW_REQUIRE_VAULT_BACKING
  • CW_REQUIRE_RESERVE_SYSTEM_BALANCE
  • CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT
  • CW_CANONICAL_USDT
  • CW_CANONICAL_USDC
  • CW_USDT_RESERVE_ASSET
  • CW_USDC_RESERVE_ASSET

Phase E: Relay and send path (138 → other chains)

E1. Relay service (138 → Mainnet)

Mainnet now has a relay-compatible cW receiver for the generic c* -> cW* path:

  • Mainnet relay router: 0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA
  • Mainnet cW bridge (CW_BRIDGE_MAINNET): 0x2bF74583206A49Be07E0E8A94197C12987AbD7B5
  • Chain 138 sender bridge (CW_L1_BRIDGE_CHAIN138): 0x152ed3e9912161b76bdfd368d0c84b7c31c10de7

The legacy Mainnet CCIPRelayBridge at 0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939 remains the WETH-only release bridge. Do not point cW mint flows at that contract.

The current proven Mainnet mint-on-receive corridor is:

  • 138 cUSDC -> Mainnet cWUSDC
  • Source send tx: 0x1228c6aa540e59055b0c548390272554135ef6611bec7f5424da6da0c85944af
  • Message id: 0x1fc04053a22f9aaa51a43d883a7d154d952a523873995e106cc2c90889376952
  • Mainnet relay tx: 0xa67a85dc0fd8b20f6f3516c5db68e75084ff10adb40cc7edc9fb00f54ff2ad47

That canary completed with processed(messageId)=true on the Mainnet cW bridge and increased the deployer's Mainnet cWUSDC balance by 1 token.

E2. Direct CCIP (138 → chain)

If Chain 138 uses UniversalCCIPBridge or a dedicated sender to send c* to a destination chain:

  • Add destination config for the c* token with receiver = the cW* receiver on the destination (e.g. TwoWayTokenBridgeL2 address).
  • Ensure the receiver on the destination has MINTER_ROLE on the cW* token and implements ccipReceivecW*.mint(recipient, amount) (see CW_BRIDGE_APPROACH.md).

E3. Test E2E

  1. On Chain 138: Lock or transfer cUSDT to the sender/bridge and trigger a send to the target chain (recipient = test address).
  2. Wait for CCIP execution on the destination chain.
  3. On the destination chain: Verify the recipients cWUSDT balance increased (e.g. cast call <CWUSDT_<CHAIN>> "balanceOf(address)(uint256)" <recipient> --rpc-url <RPC>).

New chain checklist (summary)

Step Action
1 Set CW_BRIDGE_<CHAIN> in .env (or use existing from bridge suite).
2 Run DeployCWTokens for that chain (D1).
3 Set CWUSDT_<CHAIN>, CWUSDC_<CHAIN> in .env (D2).
4 Update config/token-mapping-multichain.json addressTo for _cW entries (D3).
4a Confirm config/gru-transport-active.json activation and policy refs for the new chain (D3b).
5 Verify MINTER_ROLE and BURNER_ROLE on cW* for the bridge (D4).
6 In hard-peg mode, allowlist canonical tokens and set maxOutstanding on CWMultiTokenBridgeL1, then freeze token pair and destination on CWMultiTokenBridgeL2 (D5).
7 Deploy and attach CWReserveVerifier, then configure canonical cUSDT / cUSDC backing requirements (D6).
8 If cross-chain mint is required, ensure the bridge/receiver code mints cW* in ccipReceive (Phase B or C); then wire relay/direct CCIP (E1, E2) and run E2E test (E3). Mainnet cUSDC -> cWUSDC is now the reference completed relay-backed example.

Retry or finalize cW* deploys (Mainnet focus; Cronos verifier; Arbitrum replay only if drift appears)

As of 2026-04-03, the public EVM mesh is no longer blocked on token deployment:

  • Cronos now has the full 12-token cW* suite live and bridge-role verified.
  • Arbitrum has the full address set recorded in .env; only rerun deployment there if a verifier shows bytecode or role drift.
  • Mainnet recovery is complete: the full 12-token cW* suite is now live there alongside the earlier cWUSDT and cWUSDC.

Use the steps below only for replay or disaster recovery on a chain that drifts from the recorded mesh.

Mainnet (1) — replay only if drift reappears

The Mainnet nonce-recovery flow below is now historical recovery guidance. Use it only if a fresh verifier shows missing bytecode or a stuck deployment nonce again.

  1. Check nonce and balance:
    ./scripts/deployment/check-deployer-nonce-and-balance.sh

  2. Options:

    • Wait for pending transactions to confirm, then re-run the deploy.
    • Replace/cancel pending txs: send a 0-value transaction from the deployer to itself with the next expected nonce (e.g. 253) and higher gas so it replaces the stuck one; repeat until the queue is cleared.
    • Use the same wallet in another tool (e.g. MetaMask) and ensure no other process is sending from this account.
  3. Run deploy once nonce is aligned:

cd smom-dbis-138 && source .env
DEPLOY_CWUSDT=0 DEPLOY_CWUSDC=0 CW_BRIDGE_ADDRESS="$CW_BRIDGE_MAINNET" forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens \
  --rpc-url "${ETHEREUM_MAINNET_RPC}" --chain-id 1 --broadcast --private-key "$PRIVATE_KEY" --legacy -vvv

Add the printed CWEURC_MAINNET … CWXAUT_MAINNET to .env, then run ./scripts/deployment/run-cw-remaining-steps.sh --update-mapping.

Cronos (25) — complete and verified; keep these steps only for replay / disaster recovery

If you ever need to replay Cronos because of a fresh RPC or nonce issue, use the flow below. The current expected post-recovery result is:

CRONOS_CW_VERIFY_RPC_URL=https://cronos-evm-rpc.publicnode.com \
  bash ../scripts/verify/check-cw-cronos-wave1.sh

That verifier should report missing_code=0 missing_roles=0.

cd smom-dbis-138
source scripts/lib/deployment/dotenv.sh
load_deployment_env --repo-root "$PWD"
DEPLOY_CWUSDT=0 DEPLOY_CWUSDC=0 CW_BRIDGE_ADDRESS="$CW_BRIDGE_CRONOS" forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens \
  --rpc-url "${CRONOS_RPC_URL}" --chain-id 25 --broadcast --private-key "$PRIVATE_KEY" --legacy -vvv

If evm.cronos.org starts rate-limiting (Cloudflare 1015), retry against https://cronos-evm-rpc.publicnode.com and reduce the batch size:

cd smom-dbis-138
source scripts/lib/deployment/dotenv.sh
load_deployment_env --repo-root "$PWD"
DEPLOY_CWUSDT=0 DEPLOY_CWUSDC=0 DEPLOY_CWAUSDT=0 DEPLOY_CWUSDW=0 DEPLOY_CWEURC=0 CW_BRIDGE_ADDRESS="$CW_BRIDGE_CRONOS" forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens \
  --rpc-url "https://cronos-evm-rpc.publicnode.com" --chain-id 25 --broadcast --private-key "$PRIVATE_KEY" --legacy -vvv

Then set CWEURC_CRONOSCWXAUT_CRONOS in .env, run ./scripts/deployment/run-cw-remaining-steps.sh --update-mapping, and confirm with:

CRONOS_CW_VERIFY_RPC_URL=https://cronos-evm-rpc.publicnode.com \
  bash ../scripts/verify/check-cw-cronos-wave1.sh

Arbitrum (42161) — rerun only if verifier shows drift

Arbitrum One gas is often ~0.02 Gwei; the 10-contract deploy then costs <0.001 ETH. Only rerun this section if the mesh verifier or a direct bytecode check shows an actual gap. If you do need to replay, set ARBITRUM_GAS_PRICE slightly above base (e.g. 25000000 = 0.025 gwei). Only use 35 gwei if the network is congested. Run the deploy:

cd smom-dbis-138 && source .env
# Only if you see "max fee per gas less than block base fee" (check https://arbiscan.io/gastracker)
export ARBITRUM_GAS_PRICE=25000000
./scripts/deployment/deploy-tokens-and-weth-all-chains-skip-canonical.sh --deploy-cw --chain 42161

Or single-run without the script (use 25000000 = 0.025 gwei when gas is ~0.02 Gwei):

DEPLOY_CWUSDT=0 DEPLOY_CWUSDC=0 CW_BRIDGE_ADDRESS="$CW_BRIDGE_ARBITRUM" forge script script/deploy/DeployCWTokens.s.sol:DeployCWTokens \
  --rpc-url "${ARBITRUM_MAINNET_RPC}" --chain-id 42161 --broadcast --private-key "$PRIVATE_KEY" --legacy --with-gas-price 25000000 -vvv

Then set in .env (expected addresses from script when it runs):

  • CWEURC_ARBITRUM=0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4
  • CWEURT_ARBITRUM=0x3CD9ee18db7ad13616FCC1c83bC6098e03968E66
  • CWGBPC_ARBITRUM=0xBeF5A0Bcc0E77740c910f197138cdD90F98d2427
  • CWGBPT_ARBITRUM=0x948690147D2e50ffe50C5d38C14125aD6a9FA036
  • CWAUDC_ARBITRUM=0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd
  • CWJPYC_ARBITRUM=0xFb4B6Cc81211F7d886950158294A44C312abCA29
  • CWCHFC_ARBITRUM=0xf9f5D0ACD71C76F9476F10B3F3d3E201F0883C68
  • CWCADC_ARBITRUM=0xeE17bB0322383fecCA2784fbE2d4CD7d02b1905B
  • CWXAUC_ARBITRUM=0xc9750828124D4c10e7a6f4B655cA8487bD3842EB
  • CWXAUT_ARBITRUM=0x328Cd365Bb35524297E68ED28c6fF2C9557d1363

Then run ./scripts/deployment/run-cw-remaining-steps.sh --update-mapping.


References