12 KiB
cUSDT / cUSDC Multi-Chain Deployment and Liquidity Runbook
Purpose: Deploy cUSDT and cUSDC to other blockchain networks, create Dodo PMM and Uniswap liquidity pools, and add cUSDT/cUSDC to Balancer, Curve, and other protocols.
Status: Active runbook
Last updated: 2026-02-20
Overview
| Phase | Description |
|---|---|
| 1. Deploy cUSDT/cUSDC to other chains | Deploy CompliantUSDT and CompliantUSDC on each target chain (BSC, Polygon, Base, etc.) using existing Forge scripts. |
| 2. Dodo PMM | Create PMM pools: Chain 138 (existing) + L2s via deploy-pmm-all-l2s.sh; optionally use cUSDT/cUSDC on L2s once deployed. |
| 3. Uniswap | Create Uniswap V2/V3 pools for cUSDT/cUSDC on each chain (Uniswap factory + add liquidity). |
| 4. Balancer | Create Balancer pools for cUSDT/cUSDC (Balancer UI or factory); on Ethereum mainnet, register pool IDs in EnhancedSwapRouter. |
| 5. Curve | Add cUSDT/cUSDC to Curve pools (Curve UI or factory; mainnet-focused). |
| 6. Other protocols | Document 1inch (aggregator), and other DEXes as needed. |
Prerequisites
- .env in
smom-dbis-138/with:PRIVATE_KEY— deployer (must hold native gas token on each target chain).- Per-chain RPC:
BSC_RPC_URL,POLYGON_MAINNET_RPC,BASE_MAINNET_RPC,ARBITRUM_MAINNET_RPC,OPTIMISM_MAINNET_RPC,AVALANCHE_RPC_URL,CRONOS_RPC_URL,GNOSIS_MAINNET_RPC,ETHEREUM_MAINNET_RPC.
- Gas: Deployer funded with ETH (mainnet), BNB (BSC), MATIC (Polygon), etc.
- Compliance: Same compliance/owner setup as Chain 138 if required (e.g.
COMPLIANCE_ADMIN,USDT_OWNER,USDC_OWNER).
Phase 1: Deploy cUSDT and cUSDC to Other Chains
cUSDT/cUSDC are deployed per chain using the same contracts; only the RPC and chain ID change.
Chains to target
- Ethereum (1) — mainnet
- BSC (56)
- Polygon (137)
- Base (8453)
- Arbitrum (42161)
- Optimism (10)
- Avalanche (43114)
- Cronos (25)
- Gnosis (100)
Deploy commands (per chain)
From smom-dbis-138/:
# Set RPC and chain for the target network
export RPC_URL="$POLYGON_MAINNET_RPC" # or BSC_RPC_URL, BASE_MAINNET_RPC, etc.
export CHAIN_ID=137 # 56=BSC, 137=Polygon, 8453=Base, etc.
# Deploy CompliantUSDT
forge script script/DeployCompliantUSDT.s.sol:DeployCompliantUSDT \
--rpc-url "$RPC_URL" \
--chain-id "$CHAIN_ID" \
--broadcast \
--private-key "$PRIVATE_KEY" \
-vv
# Deploy CompliantUSDC
forge script script/DeployCompliantUSDC.s.sol:DeployCompliantUSDC \
--rpc-url "$RPC_URL" \
--chain-id "$CHAIN_ID" \
--broadcast \
--private-key "$PRIVATE_KEY" \
-vv
Record the deployed addresses and set them in .env (see Env vars below).
Optional: batch deploy script
Use scripts/deployment/deploy-cusdt-cusdc-all-chains.sh (see below) to loop over chains and deploy both tokens, then record addresses.
Env vars (per-chain cUSDT/cUSDC)
After deployment, set in .env for token-aggregation and PMM:
# Example for Polygon (137)
CUSDT_ADDRESS_137=0x...
CUSDC_ADDRESS_137=0x...
# Example for BSC (56)
CUSDT_ADDRESS_56=0x...
CUSDC_ADDRESS_56=0x...
Use the same pattern for other chain IDs: CUSDT_ADDRESS_<chainId>, CUSDC_ADDRESS_<chainId>. Token-aggregation reads these when provided (see canonical-tokens.ts and .env.example).
Phase 2: Dodo PMM Liquidity Pools
Chain 138 (existing)
- DODOPMMIntegration and mock DVM are already deployed.
- Create cUSDT/cUSDC pools:
createCUSDTUSDTPool,createCUSDCUSDCPool,createCUSDTCUSDCPoolviascripts/setup-dodo-pools.shor cast (see DODO_PMM_INTEGRATION.md). - Add liquidity:
DODOPMMIntegration.addLiquidity(pool, baseAmount, quoteAmount).
L2s (BSC, Polygon, Base, etc.)
Option A — Official USDT/USDC on L2 (current script behavior)
Pools are USDT/USDC on each L2 (no cUSDT/cUSDC on L2 yet):
cd smom-dbis-138
# Set in .env: BSC_RPC_URL, BSC_DODO_VENDING_MACHINE_ADDRESS, BSC_OFFICIAL_USDT_ADDRESS, BSC_OFFICIAL_USDC_ADDRESS
# (and same for POLYGON_*, BASE_*, etc.). DODO DVM addresses: https://docs.dodoex.io/developer/contracts/dodo-v1-v2/contracts-address
./scripts/deployment/deploy-pmm-all-l2s.sh
# Or one chain: ./scripts/deployment/deploy-pmm-all-l2s.sh --chain polygon
Option B — cUSDT/cUSDC on L2
After deploying cUSDT/cUSDC to an L2 (Phase 1), set that chain’s compliant addresses and DVM, then run the same script with overrides so the integration uses cUSDT/cUSDC for that chain (e.g. POLYGON_COMPLIANT_USDT_ADDRESS, POLYGON_COMPLIANT_USDC_ADDRESS). The script currently uses COMPLIANT_USDT_ADDRESS / COMPLIANT_USDC_ADDRESS as fallback; for L2 cUSDT/cUSDC you would set per-chain env (e.g. POLYGON_CUSDT_ADDRESS_137) and, if the script supports it, pass them as compliant tokens for that chain. If not yet supported, deploy DODOPMMIntegration for that L2 manually with COMPLIANT_USDT_ADDRESS / COMPLIANT_USDC_ADDRESS set to the L2 cUSDT/cUSDC addresses.
Record DODOPMM_INTEGRATION_<CHAIN> (or per-chain integration address) in .env.
Phase 3: Uniswap Liquidity Pools
Uniswap does not have a single “create pool” script in this repo. Create pools on each chain as follows.
Uniswap V3 (recommended for mainnet and L2s)
- Factory: Same address on mainnet, Polygon, BSC, Base, Arbitrum, Optimism:
0x1F98431c8aD98523631AE4a59f267346ea31F984(Uniswap V3 Deployments). - Create pool: Call
UniswapV3Factory.createPool(tokenA, tokenB, fee)(e.g. fee 500 = 0.05% for stables). - Initialize: Call
UniswapV3Pool.initialize(sqrtPriceX96). - Add liquidity: Use Uniswap’s NonfungiblePositionManager or Uniswap UI.
Helper script (from smom-dbis-138/):
# Create cUSDT/cUSDC pool on Polygon (set CUSDT_ADDRESS_137, CUSDC_ADDRESS_137 and POLYGON_MAINNET_RPC in .env)
RPC_URL=$POLYGON_MAINNET_RPC ./scripts/deployment/create-uniswap-v3-pool-cusdt-cusdc.sh
# Or pass tokens and RPC explicitly
TOKEN_A=0x... TOKEN_B=0x... FEE=500 RPC_URL=$ETHEREUM_MAINNET_RPC ./scripts/deployment/create-uniswap-v3-pool-cusdt-cusdc.sh
Example (manual cast, Ethereum mainnet):
FACTORY=0x1F98431c8aD98523631AE4a59f267346ea31F984
cast send $FACTORY "createPool(address,address,uint24)" $CUSDT_ADDRESS_1 $CUSDC_ADDRESS_1 500 \
--rpc-url "$ETHEREUM_MAINNET_RPC" --private-key "$PRIVATE_KEY"
# Then initialize and add liquidity via Uniswap UI or position manager.
Uniswap V2
- Use the chain’s Uniswap V2 (or SushiSwap) factory:
createPair(tokenA, tokenB). - Add liquidity via router
addLiquidityor the pair’smint.
Documentation
- Uniswap V3: https://docs.uniswap.org/contracts/v3/reference/core/UniswapV3Factory
- Uniswap V2: https://docs.uniswap.org/contracts/v2/reference/smart-contracts/factory
After creating pools, the token-aggregation service can index them if it is configured to index Uniswap V2/V3 on that chain (see pool-indexer and config).
Phase 4: Balancer
- Create pools: Use Balancer (or the chain’s Balancer app) to create a pool containing cUSDT and cUSDC (and optionally WETH or other assets). Balancer uses pool IDs (bytes32).
- Ethereum mainnet + EnhancedSwapRouter: After creating a pool, set the pool ID in EnhancedSwapRouter for the pairs you route (e.g. WETH–cUSDT, WETH–cUSDC):
cast send $ENHANCED_SWAP_ROUTER \
"setBalancerPoolId(address,address,bytes32)" \
<tokenA> <tokenB> <poolId> \
--rpc-url "$ETHEREUM_MAINNET_RPC" --private-key "$PRIVATE_KEY"
See ENHANCED_SWAP_ROUTER_UPGRADE.md.
Phase 5: Curve
- Curve is used mainly on Ethereum (and a few L2s). Add cUSDT/cUSDC to an existing Curve pool or create a new pool via Curve’s factory/UI.
- Resources: Curve Docs, Curve Factory.
- EnhancedSwapRouter: Uses Curve for large swaps; ensure the router is configured with the correct Curve pool (or pool getter) for cUSDT/cUSDC if you add a dedicated pool.
Phase 6: Other Protocols
- 1inch: Aggregator only; it discovers existing pools. Once cUSDT/cUSDC have liquidity on Uniswap, Balancer, Curve, DODO, etc., 1inch will quote them if the pool is indexed.
- Other DEXes: Per chain (e.g. SushiSwap, Camelot, etc.): create pairs/pools via their factory or UI, then add liquidity. Token-aggregation can index additional DEXes if the pool indexer is extended (see
services/token-aggregation/src/indexer/pool-indexer.ts).
Checklist Summary
| Step | Action | Script / reference |
|---|---|---|
| 1a | Deploy cUSDT to each target chain | forge script script/DeployCompliantUSDT.s.sol with chain RPC |
| 1b | Deploy cUSDC to each target chain | forge script script/DeployCompliantUSDC.s.sol with chain RPC |
| 1c | Set CUSDT_ADDRESS_<chainId>, CUSDC_ADDRESS_<chainId> in .env |
— |
| 2a | Dodo PMM on Chain 138 | scripts/setup-dodo-pools.sh, DODO_PMM_INTEGRATION.md |
| 2b | Dodo PMM on L2s | ./scripts/deployment/deploy-pmm-all-l2s.sh |
| 3 | Uniswap V2/V3 pools for cUSDT/cUSDC | scripts/deployment/create-uniswap-v3-pool-cusdt-cusdc.sh or Uniswap factory + init + add liquidity (per chain) |
| 4 | Balancer pools + EnhancedSwapRouter pool IDs | Balancer UI/factory; setBalancerPoolId on router |
| 5 | Curve pools | Curve UI/factory |
| 6 | Token-aggregation / CoinGecko | Set canonical addresses; report API and CoinGecko submission |
Verification
- Token on chain:
cast call <CUSDT_ADDRESS> "totalSupply()(uint256)" --rpc-url $RPC_URL(and same for cUSDC). Expect 6 decimals (e.g. 1e6 = 1 token). - PMM integration (L2): After
deploy-pmm-all-l2s.sh, setDODOPMM_INTEGRATION_<CHAIN>in .env; verify withcast call $INTEGRATION "..." --rpc-url $RPC. - Uniswap pool:
cast call $UNISWAP_V3_FACTORY "getPool(address,address,uint24)(address)" $TOKEN_A $TOKEN_B 500 --rpc-url $RPC_URL; non-zero address = pool exists. - Token-aggregation:
GET /api/v1/report/token-list?chainId=<id>andGET /api/v1/report/coingecko?chainId=<id>should include cUSDT/cUSDC whenCUSDT_ADDRESS_<id>andCUSDC_ADDRESS_<id>are set in the service env.
Troubleshooting
| Issue | Check | Fix |
|---|---|---|
| Deploy fails "insufficient funds" | Deployer balance on that chain | Fund with native gas (ETH, BNB, MATIC, etc.) |
| Deploy fails "nonce" / "replacement" | Pending or stuck tx on that chain | Clear mempool or use next nonce; see RPC_NODES_BLOCK_PRODUCTION_FIX for Chain 138 |
| deploy-pmm-all-l2s skips chain | Missing RPC or DVM/token env for that chain | Set BSC_RPC_URL, BSC_DODO_VENDING_MACHINE_ADDRESS, BSC_OFFICIAL_USDT_ADDRESS, BSC_OFFICIAL_USDC_ADDRESS (and equivalent for other chains) |
| Uniswap createPool reverts | Token order or fee; pool may already exist | Use factory getPool(tokenA, tokenB, fee) first; 0x0 = create, else pool exists |
| Token-aggregation report missing token | Canonical list only has 138/651940 by default; L2 needs env | Set CUSDT_ADDRESS_56, CUSDC_ADDRESS_56, etc. in env used by token-aggregation (see canonical-tokens.ts) |
Related docs
- ALL_MAINNETS_DEPLOYMENT_RUNBOOK.md — CCIP, Trustless, Oracle, PMM overview
- DVM_DEPLOYMENT_CHECK.md — DODO DVM on Chain 138 vs L2s
- DODO_PMM_INTEGRATION.md — Pool creation and swap methods
- ENHANCED_SWAP_ROUTER_UPGRADE.md — Balancer/Curve/Uniswap on mainnet
- OPERATOR_NEXT_STEPS_RUNBOOK.md — G1 PMM on L2s, G2/G3 Trustless