Skip to main content

Overview

CCTP V2 contracts use a CREATE2-based deployment strategy to ensure deterministic addresses across chains. The deployment process involves:
  1. Create2Factory: Deploy factory for deterministic deployments
  2. Implementation Contracts: Deploy logic contracts
  3. Proxy Contracts: Deploy proxies pointing to implementations
  4. Setup Remote Resources: Configure cross-chain connections
  5. Key Rotation (Mainnet only): Rotate administrative keys
  6. AddressUtilsExternal: Deploy helper library
All deployment scripts use Forge Scripts and are located in scripts/v2/ directory.

Prerequisites

  • Foundry CLI installed (forge 0.2.0)
  • Git submodules initialized: git submodule update --init --recursive
  • Node.js and Yarn installed: yarn install
  • Sufficient funds in deployer accounts for gas

Deployment Steps

Step 1: Deploy Create2Factory

The Create2Factory enables deterministic address deployment across chains.
1

Configure Environment

Add to your .env file:
CREATE2_FACTORY_OWNER=<address>
Parameters:
  • CREATE2_FACTORY_OWNER: Address that will own the Create2Factory
2

Simulate Deployment

Perform a dry run:
make simulate-deploy-create2-factory \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<PRIVATE_KEY>
SENDER should match the address derived from CREATE2_FACTORY_OWNER_KEY.
3

Deploy Factory

Deploy the Create2Factory:
make deploy-create2-factory \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<PRIVATE_KEY>
Record the deployed factory address for subsequent steps.
Reference: scripts/v2/DeployCreate2Factory.s.sol

Step 2: Deploy V2 Implementation Contracts

Deploy the logic contracts that proxies will delegate to.
1

Configure Environment

Add to your .env file:
CREATE2_FACTORY_CONTRACT_ADDRESS=<address>
TOKEN_MINTER_V2_OWNER_KEY=<private_key>
TOKEN_CONTROLLER_ADDRESS=<address>
DOMAIN=<uint32>
MESSAGE_BODY_VERSION=<uint32>
VERSION=<uint32>
Parameters:
  • CREATE2_FACTORY_CONTRACT_ADDRESS: Address from Step 1
  • TOKEN_MINTER_V2_OWNER_KEY: Private key for TokenMinter owner
  • TOKEN_CONTROLLER_ADDRESS: Token controller address
  • DOMAIN: Local domain ID (e.g., 0 for Ethereum, 1 for Avalanche)
  • MESSAGE_BODY_VERSION: Message format version (typically 1)
  • VERSION: Protocol version (typically 2)
2

Simulate Deployment

make simulate-deploy-implementations-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<CREATE2_FACTORY_OWNER_KEY>
3

Deploy Implementations

make deploy-implementations-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<CREATE2_FACTORY_OWNER_KEY>
This deploys:
  • MessageTransmitterV2 implementation
  • TokenMessengerV2 implementation
  • TokenMinterV2 implementation
Record all implementation addresses.
Reference: scripts/v2/DeployImplementationsV2.s.sol

Step 3: Deploy V2 Proxies

Deploy proxy contracts with full initialization.
1

Configure Environment

Replace environment variables in .env:
# Token Configuration
USDC_CONTRACT_ADDRESS=<address>
TOKEN_CONTROLLER_ADDRESS=<address>

# Remote Configuration (1:1:1 correspondence)
REMOTE_DOMAINS=<uint32[]>                    # e.g., "1,2,3"
REMOTE_USDC_CONTRACT_ADDRESSES=<address[]>   # e.g., "0x123...,0x456...,0x789..."
REMOTE_TOKEN_MESSENGER_V2_ADDRESSES=<bytes32[]>

# Factory
CREATE2_FACTORY_CONTRACT_ADDRESS=<address>

# MessageTransmitterV2 Roles
MESSAGE_TRANSMITTER_V2_OWNER_ADDRESS=<address>
MESSAGE_TRANSMITTER_V2_PAUSER_ADDRESS=<address>
MESSAGE_TRANSMITTER_V2_RESCUER_ADDRESS=<address>
MESSAGE_TRANSMITTER_V2_ATTESTER_MANAGER_ADDRESS=<address>
MESSAGE_TRANSMITTER_V2_ATTESTER_1_ADDRESS=<address>
MESSAGE_TRANSMITTER_V2_ATTESTER_2_ADDRESS=<address>
MESSAGE_TRANSMITTER_V2_PROXY_ADMIN_ADDRESS=<address>

# TokenMinterV2 Roles
TOKEN_MINTER_V2_PAUSER_ADDRESS=<address>
TOKEN_MINTER_V2_RESCUER_ADDRESS=<address>

# TokenMessengerV2 Roles (NEW in V2)
TOKEN_MESSENGER_V2_OWNER_ADDRESS=<address>
TOKEN_MESSENGER_V2_RESCUER_ADDRESS=<address>
TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS=<address>
TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS=<address>
TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS=<address>
TOKEN_MESSENGER_V2_MIN_FEE_CONTROLLER_ADDRESS=<address>
TOKEN_MESSENGER_V2_MIN_FEE=<uint256>         # e.g., 5000 for 0.05%

# Domain Configuration
DOMAIN=<uint32>
BURN_LIMIT_PER_MESSAGE=<uint256>

# Deployer Keys
TOKEN_CONTROLLER_KEY=<private_key>
TOKEN_MINTER_V2_OWNER_KEY=<private_key>
Important: REMOTE_DOMAINS, REMOTE_USDC_CONTRACT_ADDRESSES, and REMOTE_TOKEN_MESSENGER_V2_ADDRESSES must correspond 1:1:1 in order.
New V2 Role Addresses:
  • TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS: Receives collected fees
  • TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS: Manages protocol denylist
  • TOKEN_MESSENGER_V2_MIN_FEE_CONTROLLER_ADDRESS: Sets minimum fees
  • TOKEN_MESSENGER_V2_MIN_FEE: Initial minimum fee in 1/1000 basis points
2

Simulate Deployment

make simulate-deploy-proxies-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<CREATE2_FACTORY_OWNER_KEY>
3

Deploy Proxies

make deploy-proxies-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<CREATE2_FACTORY_OWNER_KEY>
This deploys and initializes:
  • MessageTransmitterV2 proxy
  • TokenMessengerV2 proxy
  • TokenMinterV2 proxy
All contracts are fully initialized with roles and remote configurations.
Reference: scripts/v2/DeployProxiesV2.s.sol

Step 4: Setup Additional Remote Resources

Only perform steps 4-7 for additional remote resources NOT already configured in Step 3.
If you need to add more remote chains after initial deployment:
1

Configure Environment

Update .env with additional remote configuration:
TOKEN_MESSENGER_V2_OWNER_KEY=<private_key>
TOKEN_CONTROLLER_KEY=<private_key>
TOKEN_MESSENGER_V2_CONTRACT_ADDRESS=<address>
TOKEN_MINTER_V2_CONTRACT_ADDRESS=<address>
USDC_CONTRACT_ADDRESS=<address>
REMOTE_USDC_CONTRACT_ADDRESS=<address>
REMOTE_DOMAIN=<uint32>
Add one remote resource at a time, repeating these steps for each remote chain.
2

Simulate Setup

make simulate-setup-remote-resources-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER>
3

Setup Remote Resources

make setup-remote-resources-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER>
This:
  • Links remote TokenMessenger
  • Configures remote USDC token pair
4

Repeat for Each Remote Chain

Repeat steps 1-3 for each additional remote chain you want to support.
Reference: scripts/v2/SetupRemoteResourcesV2.s.sol

Step 5: Key Rotation (Mainnet Only)

Mainnet Only: This step should only be performed on mainnet deployments. Skip for testnets.
1

Configure Environment

Add to your .env file:
# Current Contract Addresses
MESSAGE_TRANSMITTER_V2_CONTRACT_ADDRESS=<address>
TOKEN_MESSENGER_V2_CONTRACT_ADDRESS=<address>
TOKEN_MINTER_V2_CONTRACT_ADDRESS=<address>

# Current Owner Keys
MESSAGE_TRANSMITTER_V2_OWNER_KEY=<private_key>
TOKEN_MESSENGER_V2_OWNER_KEY=<private_key>
TOKEN_MINTER_V2_OWNER_KEY=<private_key>

# New Owner Addresses
MESSAGE_TRANSMITTER_V2_NEW_OWNER_ADDRESS=<address>
TOKEN_MESSENGER_V2_NEW_OWNER_ADDRESS=<address>
TOKEN_MINTER_V2_NEW_OWNER_ADDRESS=<address>
NEW_TOKEN_CONTROLLER_ADDRESS=<address>
2

Simulate Key Rotation

make simulate-rotate-keys-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER>
3

Rotate Keys

make rotate-keys-v2 \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER>
This transfers ownership of all V2 contracts to new owner addresses.
Reference: scripts/v2/RotateKeysV2.s.sol

Step 6: Deploy AddressUtilsExternal

Deploy the helper library for easy integration.
1

Configure Environment

Ensure CREATE2_FACTORY_CONTRACT_ADDRESS is set in .env:
CREATE2_FACTORY_CONTRACT_ADDRESS=<address>
2

Simulate Deployment

make simulate-deploy-address-utils-external \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<CREATE2_FACTORY_OWNER_KEY>
3

Deploy AddressUtilsExternal

make deploy-address-utils-external \
  RPC_URL=<RPC_URL> \
  SENDER=<SENDER> \
  CREATE2_FACTORY_OWNER_KEY=<CREATE2_FACTORY_OWNER_KEY>
This deploys AddressUtilsExternal to a deterministic address for easy integration.
Reference: scripts/v2/DeployAddressUtilsExternal.s.sol

Predicting CREATE2 Addresses

You can predict deployment addresses before deploying:
# Predict MessageTransmitterV2 Implementation
forge script scripts/v2/PredictCreate2Deployments.s.sol \
  --sig "messageTransmitterV2Impl(address,uint32,uint32)" \
  <create2FactoryAddress> \
  <domain> \
  <messageVersion>

# Predict TokenMessengerV2 Implementation
forge script scripts/v2/PredictCreate2Deployments.s.sol \
  --sig "tokenMessengerV2Impl(address,uint32)" \
  <create2FactoryAddress> \
  <messageBodyVersion>

# Predict MessageTransmitterV2 Proxy
forge script scripts/v2/PredictCreate2Deployments.s.sol \
  --sig "messageTransmitterV2Proxy(address)" \
  <create2FactoryAddress>

# Predict TokenMessengerV2 Proxy
forge script scripts/v2/PredictCreate2Deployments.s.sol \
  --sig "tokenMessengerV2Proxy(address)" \
  <create2FactoryAddress>

# Predict TokenMinterV2
forge script scripts/v2/PredictCreate2Deployments.s.sol \
  --sig "tokenMinterV2(address)" \
  <create2FactoryAddress>

# Predict AddressUtilsExternal
forge script scripts/v2/PredictCreate2Deployments.s.sol \
  --sig "addressUtilsExternal(address)" \
  <create2FactoryAddress>
Reference: scripts/v2/PredictCreate2Deployments.s.sol

Role Configuration Summary

MessageTransmitterV2 Roles

RoleResponsibilities
ownerGeneral administration, role updates
pauserEmergency pause functionality
rescuerEmergency token recovery
attesterManagerManages attester set
attesterSigns attestations (multiple attesters)
proxyAdminProxy upgrade authority

TokenMessengerV2 Roles

RoleResponsibilitiesNew in V2
ownerGeneral administration
rescuerEmergency token recovery
feeRecipientReceives collected fees
denylisterManages protocol denylist
minFeeControllerSets minimum fee requirements
proxyAdminProxy upgrade authority

TokenMinterV2 Roles

RoleResponsibilities
ownerGeneral administration
pauserEmergency pause functionality
rescuerEmergency token recovery
tokenControllerManages token pairs and burn limits

Environment Variables Checklist

 CREATE2_FACTORY_OWNER
 CREATE2_FACTORY_CONTRACT_ADDRESS
 TOKEN_MINTER_V2_OWNER_KEY
 TOKEN_CONTROLLER_ADDRESS
 DOMAIN
 MESSAGE_BODY_VERSION
 VERSION
# Token & Factory
 USDC_CONTRACT_ADDRESS
 TOKEN_CONTROLLER_ADDRESS
 CREATE2_FACTORY_CONTRACT_ADDRESS

# Remote Configuration
 REMOTE_DOMAINS
 REMOTE_USDC_CONTRACT_ADDRESSES
 REMOTE_TOKEN_MESSENGER_V2_ADDRESSES

# MessageTransmitter Roles
 MESSAGE_TRANSMITTER_V2_OWNER_ADDRESS
 MESSAGE_TRANSMITTER_V2_PAUSER_ADDRESS
 MESSAGE_TRANSMITTER_V2_RESCUER_ADDRESS
 MESSAGE_TRANSMITTER_V2_ATTESTER_MANAGER_ADDRESS
 MESSAGE_TRANSMITTER_V2_ATTESTER_1_ADDRESS
 MESSAGE_TRANSMITTER_V2_ATTESTER_2_ADDRESS
 MESSAGE_TRANSMITTER_V2_PROXY_ADMIN_ADDRESS

# TokenMinter Roles
 TOKEN_MINTER_V2_PAUSER_ADDRESS
 TOKEN_MINTER_V2_RESCUER_ADDRESS

# TokenMessenger Roles (NEW)
 TOKEN_MESSENGER_V2_OWNER_ADDRESS
 TOKEN_MESSENGER_V2_RESCUER_ADDRESS
 TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS
 TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS
 TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS
 TOKEN_MESSENGER_V2_MIN_FEE_CONTROLLER_ADDRESS
 TOKEN_MESSENGER_V2_MIN_FEE

# Domain
 DOMAIN
 BURN_LIMIT_PER_MESSAGE

# Keys
 TOKEN_CONTROLLER_KEY
 TOKEN_MINTER_V2_OWNER_KEY
 TOKEN_MESSENGER_V2_OWNER_KEY
 TOKEN_CONTROLLER_KEY
 TOKEN_MESSENGER_V2_CONTRACT_ADDRESS
 TOKEN_MINTER_V2_CONTRACT_ADDRESS
 USDC_CONTRACT_ADDRESS
 REMOTE_USDC_CONTRACT_ADDRESS
 REMOTE_DOMAIN
# Current
 MESSAGE_TRANSMITTER_V2_CONTRACT_ADDRESS
 TOKEN_MESSENGER_V2_CONTRACT_ADDRESS
 TOKEN_MINTER_V2_CONTRACT_ADDRESS
 MESSAGE_TRANSMITTER_V2_OWNER_KEY
 TOKEN_MESSENGER_V2_OWNER_KEY
 TOKEN_MINTER_V2_OWNER_KEY

# New
 MESSAGE_TRANSMITTER_V2_NEW_OWNER_ADDRESS
 TOKEN_MESSENGER_V2_NEW_OWNER_ADDRESS
 TOKEN_MINTER_V2_NEW_OWNER_ADDRESS
 NEW_TOKEN_CONTROLLER_ADDRESS
 CREATE2_FACTORY_CONTRACT_ADDRESS

Post-Deployment Verification

After deployment, verify:
1

Contract Addresses

Record all deployed addresses:
  • Create2Factory
  • MessageTransmitterV2 (implementation & proxy)
  • TokenMessengerV2 (implementation & proxy)
  • TokenMinterV2 (implementation & proxy)
  • AddressUtilsExternal
2

Role Configuration

Verify all roles are set correctly:
# Check owner
cast call $TOKEN_MESSENGER_V2 "owner()" --rpc-url $RPC_URL

# Check fee recipient
cast call $TOKEN_MESSENGER_V2 "feeRecipient()" --rpc-url $RPC_URL

# Check denylister
cast call $TOKEN_MESSENGER_V2 "denylister()" --rpc-url $RPC_URL
3

Remote Configuration

Verify remote token messengers are linked:
# Check remote token messenger
cast call $TOKEN_MESSENGER_V2 \
  "remoteTokenMessengers(uint32)" \
  $REMOTE_DOMAIN \
  --rpc-url $RPC_URL
4

Fee Configuration

Verify fee settings:
# Check minimum fee
cast call $TOKEN_MESSENGER_V2 "minFee()" --rpc-url $RPC_URL

# Check min fee controller
cast call $TOKEN_MESSENGER_V2 "minFeeController()" --rpc-url $RPC_URL

Troubleshooting

Simulation Fails

  • Verify all environment variables are set correctly
  • Check that SENDER has sufficient funds for gas
  • Ensure RPC_URL is accessible and correct
  • Verify private keys match expected addresses

CREATE2 Address Mismatch

  • Ensure Create2Factory address is consistent across chains
  • Verify constructor parameters match exactly
  • Check that salt values are identical

Initialization Fails

  • Verify all role addresses are non-zero
  • Check that remote arrays have equal length
  • Ensure owner address is set correctly

Remote Resources Not Linking

  • Verify remote domain IDs are correct
  • Check that remote TokenMessenger addresses are bytes32 format
  • Ensure TokenMinter has correct token pairs configured

Next Steps

Test Deployment

Test your V2 deployment

Integration Guide

Integrate V2 into your application

Migration Guide

Migrate from V1 to V2

API Reference

Complete V2 API documentation

Build docs developers (and LLMs) love