Warp Token Deployment
This guide walks you through deploying a cross-chain warp route between BitSong (crescendo-1) and Base Sepolia. A warp route enables native token transfers across chains — tokens are locked as collateral on BitSong while a synthetic HypERC20 representation is minted on the EVM side.
MAILBOX_ID from Step 3.Overview
The warp route consists of three components deployed across both chains:
Collateral Token
HypERC20
Router Enrollment
Prerequisites
Before starting, ensure you have the following ready:
- Completed Hyperlane Setup — you need
MAILBOX_IDfrom Step 3 - The
bitsongdbinary installed and available in your PATH jqinstalled (sudo apt install jq)- Node.js v22+ installed (required by the Hyperlane CLI):
Terminal
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs node --version # should print v22.x.x - Hyperlane CLI installed:
Terminal
sudo npm install -g @hyperlane-xyz/cli - Foundry (
cast) installed:Terminalcurl -L https://foundry.paradigm.xyz | bash && foundryup - An EVM private key funded with Base Sepolia ETH (for deploying the HypERC20 contract)
Configuration Reference
These parameters are used throughout the deployment. Set them as environment variables:
0x). Must be funded with Base Sepolia ETH.0x6966b0E55883d49BFB24539356a2f8A673E02039.https://sepolia.base.org.84532.7171.export CHAIN_ID="crescendo-1"
export DOMAIN_ID="7171"
export REMOTE_DOMAIN="84532"
export DENOM="utbtsg"
export KEY_NAME="<your-key-name>"
export NODE="tcp://localhost:26657"
# From the Hyperlane Setup (Step 3)
export MAILBOX_ID="<your-mailbox-id>"
# EVM deployer
export EVM_KEY="0x<your-evm-private-key>"
export EVM_RPC="https://sepolia.base.org"
export BASESEPOLIA_MAILBOX="0x6966b0E55883d49BFB24539356a2f8A673E02039"
# Validator address (from agent keys — needed for ISM deployment)
export VALIDATOR_ADDR="<your-validator-evm-address>"
Deployment Steps
Register Chain Metadata
The Hyperlane CLI needs to know about the BitSong chain. Create the metadata and addresses files in the CLI's chain registry:
mkdir -p ~/.hyperlane/chains/bitsong
Create the chain metadata file:
chainId: crescendo-1
domainId: 7171
name: bitsong
protocol: cosmos
bech32Prefix: bitsong
slip44: 639
rpcUrls:
- http: http://localhost:26657
restUrls:
- http: http://localhost:1317
grpcUrls:
- http: http://localhost:9090
nativeToken:
name: BitSong
symbol: BTSG
decimals: 6
denom: utbtsg
blocks:
confirmations: 1
estimateBlockTime: 6
isTestnet: true
Create the addresses file referencing your Mailbox:
mailbox: "<your-mailbox-id>"
Replace <your-mailbox-id> with the MAILBOX_ID value from your Hyperlane Setup.
Create the Warp Route Config
Use the Hyperlane CLI interactive wizard to generate the warp route config:
hyperlane warp init
The wizard will prompt you for a series of choices. Select the following:
| Prompt | Value |
|---|---|
| Network type | testnet |
| Chain | basesepolia |
| Token type | synthetic |
| Token name | BitSong |
| Token symbol | BTSG |
| Token decimals | 6 |
This generates a config file at ./configs/warp-route-deployment.yaml.
After generation, open the file and set interchainSecurityModule to the zero address (0x0000000000000000000000000000000000000000). This is required as a placeholder for the initial deployment.
Your generated config should look similar to this:
basesepolia:
type: synthetic
name: "BitSong"
symbol: "BTSG"
decimals: 6
mailbox: "0x..."
owner: "0x..."
interchainSecurityModule: "0x0000000000000000000000000000000000000000"
The mailbox and owner fields are filled in automatically by the CLI based on the chain registry and your deployer wallet.
Deploy HypERC20 on Base Sepolia
Export the deployer key for the Hyperlane CLI and run the deployment:
export HYP_KEY="$EVM_KEY"
hyperlane warp deploy --config ./configs/warp-route-deployment.yaml --yes
The CLI deploys the HypERC20 contract and prints the deployment artifacts at the end. Look for the addressOrDenom field in the output:
tokens:
- chainName: basesepolia
standard: EvmHypSynthetic
decimals: 6
symbol: TBTSG
name: Test BitSong
addressOrDenom: "0x7b2BD29..."
Copy the address and export it:
export HYP_ERC20_ADDR="<addressOrDenom-from-deploy-output>"
echo "HypERC20 Address: $HYP_ERC20_ADDR"
Convert it to bytes32 format (needed for Cosmos-side enrollment):
HYP_ERC20_BYTES32=$(printf "0x%064s" "${HYP_ERC20_ADDR#0x}" | tr ' ' '0')
echo "HypERC20 Bytes32: $HYP_ERC20_BYTES32"
cast call "$HYP_ERC20_ADDR" "decimals()(uint8)" --rpc-url "$EVM_RPC"
6 (matching BTSG's 6 decimal places).Deploy Custom ISM on Base Sepolia
The default Mailbox ISM on Base Sepolia does not know about BitSong (domain 7171), so you must deploy a custom StaticMessageIdMultisigISM that recognizes your validator's signatures. This ISM verifies checkpoints from your BitSong validator when processing BitSong → Base Sepolia messages.
The ISM is deployed via Hyperlane's official ISM factory on Base Sepolia, which uses CREATE2 for deterministic addressing:
# ISM factory on Base Sepolia (official Hyperlane contract)
BASESEPOLIA_ISM_FACTORY="0xfc6e546510dC9d76057F1f76633FCFfC188CB213"
# 1. Predict the ISM address (read-only call)
CUSTOM_ISM=$(cast call "$BASESEPOLIA_ISM_FACTORY" \
"deploy(address[],uint8)(address)" "[$VALIDATOR_ADDR]" 1 \
--rpc-url "$EVM_RPC")
echo "Custom ISM address: $CUSTOM_ISM"
# 2. Deploy the ISM (state-changing transaction)
cast send "$BASESEPOLIA_ISM_FACTORY" \
"deploy(address[],uint8)" "[$VALIDATOR_ADDR]" 1 \
--private-key "$EVM_KEY" \
--rpc-url "$EVM_RPC"
# 3. Set the ISM on the HypERC20 contract
cast send "$HYP_ERC20_ADDR" \
"setInterchainSecurityModule(address)" "$CUSTOM_ISM" \
--private-key "$EVM_KEY" \
--rpc-url "$EVM_RPC"
deploy() function on the ISM factory creates a StaticMessageIdMultisigISM with your validator address and a threshold of 1 (single-validator setup). The factory uses CREATE2, so the ISM address is deterministic — calling cast call (read-only) before cast send lets you predict the address without spending gas.cast call "$HYP_ERC20_ADDR" "interchainSecurityModule()(address)" --rpc-url "$EVM_RPC"
$CUSTOM_ISM address, not 0x0000000000000000000000000000000000000000.Create the Cosmos Collateral Token
The Collateral Warp Token locks native TBTSG on BitSong when tokens are transferred to the EVM chain. A synthetic representation is minted on the destination chain via the HypERC20 contract.
bitsongd query warp tokens --output json --node $NODE | jq '.'
token_type: "HYP_TOKEN_TYPE_COLLATERAL" and origin_denom: "utbtsg", export its id and skip to the next step:export TOKEN_ID="<existing-token-id>"
TX_HASH=$(bitsongd tx warp create-collateral-token \
$MAILBOX_ID $DENOM \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
Extract the Token ID:
TOKEN_ID=$(bitsongd query tx $TX_HASH --output json | \
jq -r 'first(.events[] | select(.type | test("warp")) | .attributes[] | select(.key == "token_id") | .value | fromjson)')
echo "Token ID: $TOKEN_ID"
Enroll the EVM Router on Cosmos
Register the HypERC20 contract address as the remote router on the Cosmos side. This tells the collateral token where to send messages on the EVM chain.
bitsongd tx warp enroll-remote-router \
$TOKEN_ID $REMOTE_DOMAIN $HYP_ERC20_BYTES32 300000 \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y
0x...) to the 32-byte format, left-pad it with zeros:EVM_ADDR="0x<your-evm-contract-address>"
EVM_BYTES32=$(printf "0x%064s" "${EVM_ADDR#0x}" | tr ' ' '0')
echo "Bytes32: $EVM_BYTES32"
0x + 64 hex character string.Enroll the Cosmos Router on EVM
Register the Cosmos collateral token ID as the remote router on the EVM side. This completes the bidirectional link so the HypERC20 contract knows where to route messages back to BitSong.
cast send "$HYP_ERC20_ADDR" \
"enrollRemoteRouter(uint32,bytes32)" "$DOMAIN_ID" "$TOKEN_ID" \
--private-key "$EVM_KEY" \
--rpc-url "$EVM_RPC"
TOKEN_ID from Step 4 is already in bytes32 format (it's a numeric ID that Hyperlane stores as a 32-byte value). No conversion is needed.Verify the Deployment
After completing all steps, verify both sides of the warp route.
EVM Side
Query the HypERC20 contract to confirm its configuration:
echo "=== HypERC20 ($HYP_ERC20_ADDR) ==="
echo "decimals: $(cast call "$HYP_ERC20_ADDR" "decimals()(uint8)" --rpc-url "$EVM_RPC")"
echo "mailbox: $(cast call "$HYP_ERC20_ADDR" "mailbox()(address)" --rpc-url "$EVM_RPC")"
echo "ism: $(cast call "$HYP_ERC20_ADDR" "interchainSecurityModule()(address)" --rpc-url "$EVM_RPC")"
echo "totalSupply: $(cast call "$HYP_ERC20_ADDR" "totalSupply()(uint256)" --rpc-url "$EVM_RPC")"
echo "router(${DOMAIN_ID}): $(cast call "$HYP_ERC20_ADDR" "routers(uint32)(bytes32)" "$DOMAIN_ID" --rpc-url "$EVM_RPC")"
Expected values:
- decimals:
6 - mailbox: the Base Sepolia Hyperlane Mailbox address
- ism: your
$CUSTOM_ISMaddress (not0x0000...0000— see Deploy Custom ISM) - totalSupply:
0(no tokens minted yet) - router: your
TOKEN_IDvalue (non-zero confirms enrollment)
Cosmos Side
Query the warp token and its remote routers:
bitsongd query warp tokens --output json | jq '.'
bitsongd query warp remote-routers $TOKEN_ID --output json | jq '.'
The remote routers query should show the HypERC20 address enrolled for domain 84532.
Update Remote Router Enrollment
If you need to update the enrolled EVM address after the initial setup (for example, after redeploying the HypERC20 contract), re-enroll with the new address on both sides.
bitsongd query warp remote-routers $TOKEN_ID --output json | jq '.'
receiver_contract matches your current $HYP_ERC20_ADDR, skip this section and proceed to Testing Warp Route.Update the Cosmos side
Convert the new EVM address and submit the enrollment:
NEW_EVM_ADDR="0xYourNewContractAddress"
NEW_EVM_BYTES32=$(printf "0x%064s" "${NEW_EVM_ADDR#0x}" | tr ' ' '0')
echo "Bytes32: $NEW_EVM_BYTES32"
bitsongd tx warp enroll-remote-router \
$TOKEN_ID $REMOTE_DOMAIN $NEW_EVM_BYTES32 300000 \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y
Update the EVM side
Enroll the Cosmos token on the new HypERC20 contract:
cast send "$NEW_EVM_ADDR" \
"enrollRemoteRouter(uint32,bytes32)" "$DOMAIN_ID" "$TOKEN_ID" \
--private-key "$EVM_KEY" \
--rpc-url "$EVM_RPC"
Verify both sides
# Cosmos side
bitsongd query warp remote-routers $TOKEN_ID --output json | jq '.'
# EVM side
cast call "$NEW_EVM_ADDR" "routers(uint32)(bytes32)" "$DOMAIN_ID" --rpc-url "$EVM_RPC"
Troubleshooting
Common causes:
- Insufficient ETH: Ensure your deployer address has enough Base Sepolia ETH for gas
- HYP_KEY not set: The CLI reads the deployer key from
HYP_KEY, notEVM_KEY - Chain metadata missing: Verify
~/.hyperlane/chains/bitsong/metadata.yamlexists - RPC issues: Ensure your BitSong node's REST API (port 1317) and gRPC (port 9090) are accessible
Check the CLI version: hyperlane --version
Verify the following:
- Contract address: Ensure
$HYP_ERC20_ADDRis the correct deployed address - Private key: The key must be the contract owner (the address that deployed it)
- RPC URL: Test connectivity with
cast chain-id --rpc-url "$EVM_RPC" - Already enrolled: If the router is already enrolled for this domain, the call may revert. Check current enrollment:
Terminal
cast call "$HYP_ERC20_ADDR" "routers(uint32)(bytes32)" "$DOMAIN_ID" --rpc-url "$EVM_RPC"
- Mailbox ID: Ensure the
MAILBOX_IDmatches what was created in the Hyperlane Setup - Denomination: The
$DENOMmust be a valid token denomination on the chain (e.g.utbtsg) - Funds: Your account must have sufficient funds for gas and fees
Verify the mailbox exists:
bitsongd query hyperlane mailboxes --output json | jq '.'