# Runbook: Deploy cW* and Wire Config **Created:** 2026-02-27 **Purpose:** Steps to deploy cWUSDT/cWUSDC on a chain, set `CW_BRIDGE_`, update token-mapping, and verify roles. Covers Phase D and E from [CW_BRIDGE_TASK_LIST.md](../00-meta/CW_BRIDGE_TASK_LIST.md). --- ## Prerequisites - `smom-dbis-138/.env` has `CW_BRIDGE_` 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_` to it; see [CW_BRIDGE_APPROACH.md](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`](../../smom-dbis-138/contracts/bridge/CWMultiTokenBridgeL1.sol) and [`CWMultiTokenBridgeL2.sol`](../../smom-dbis-138/contracts/bridge/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`](../../smom-dbis-138/contracts/bridge/integration/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). ### D1. Run cW* deploy **All supported chains:** ```bash 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): ```bash ./scripts/deployment/run-cw-remaining-steps.sh --deploy ``` **Single chain (e.g. BSC 56):** ```bash 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:** ```bash 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`): ```bash ./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): ```bash # 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_` and `CWUSDC_` in `smom-dbis-138/.env`, run from repo root: ```bash ./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`](../../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: ```bash # MINTER_ROLE = keccak256("MINTER_ROLE") cast keccak "MINTER_ROLE" # Example: 0x... # Check cWUSDT on BSC (replace addresses and rpc) cast call "hasRole(bytes32,address)(bool)" $(cast keccak "MINTER_ROLE") $CW_BRIDGE_BSC --rpc-url $BSC_RPC_URL cast call "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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash cast call "$CHAIN138_L1_BRIDGE" "reserveVerifier()(address)" --rpc-url "$RPC_URL_138" cast call "verifyLock(address,uint64,uint256)(bool)" "$CUSDC_138" 6433500567565415381 1 --rpc-url "$RPC_URL_138" cast call "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) If using the existing CCIPRelayBridge for cW* on Mainnet, that contract is WETH-only and must be **extended** (or a separate cW* receiver deployed and relay logic updated) so that when the relay sends a cUSDT/cUSDC message, the Mainnet receiver mints cWUSDT/cWUSDC. See [RELAY_BRIDGE_ADD_LINK_SUPPORT_RUNBOOK.md](RELAY_BRIDGE_ADD_LINK_SUPPORT_RUNBOOK.md) for the pattern (token whitelist + transfer or mint in `ccipReceive`). Then: - Configure relay to send c* messages to the cW* receiver on Mainnet. - Ensure the receiver has MINTER_ROLE on cWUSDT/cWUSDC and holds or mints as designed. ### 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 `ccipReceive` → `cW*.mint(recipient, amount)` (see [CW_BRIDGE_APPROACH.md](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 recipient’s cWUSDT balance increased (e.g. `cast call > "balanceOf(address)(uint256)" --rpc-url `). --- ## New chain checklist (summary) | Step | Action | |------|--------| | 1 | Set `CW_BRIDGE_` in .env (or use existing from bridge suite). | | 2 | Run DeployCWTokens for that chain (D1). | | 3 | Set `CWUSDT_`, `CWUSDC_` 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). | --- ## Retry failed cW* deploys (Mainnet, Cronos, Arbitrum) If the 10 remaining cW* (cWEURC..cWXAUT) failed on **Mainnet** (nonce), **Cronos** (RPC timeout), or **Arbitrum** (gas), use the steps below. ### Mainnet (1) — fix nonce Deployer nonce on Mainnet must match the RPC (error: `nonce=256 maxNonce=253` means 3 pending txs are ahead of what the RPC has confirmed). 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: ```bash 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) — retry when RPC is stable; fix nonce if needed If you see `invalid nonce; got X, expected Y`, wait for pending txs to confirm or replace/cancel them (same idea as Mainnet). Check: `./scripts/deployment/check-deployer-nonce-and-balance.sh` ```bash cd smom-dbis-138 && source .env 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 ``` Then set CWEURC_CRONOS … CWXAUT_CRONOS in `.env` from the script output and run `--update-mapping`. ### Arbitrum (42161) — use current gas; low gas price is enough [Arbitrum One gas](https://arbiscan.io/gastracker) is often ~0.02 Gwei; the 10-contract deploy then costs <0.001 ETH. If you see "max fee per gas less than block base fee", 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: ```bash 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): ```bash 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 - [CW_BRIDGE_TASK_LIST.md](../00-meta/CW_BRIDGE_TASK_LIST.md) - [CW_BRIDGE_APPROACH.md](CW_BRIDGE_APPROACH.md) - [CW_TOKENS_AND_NETWORKS.md](../11-references/CW_TOKENS_AND_NETWORKS.md) - [C_TO_CW_MAPPER_MAPPING.md](../04-configuration/C_TO_CW_MAPPER_MAPPING.md)