Bridge Runbook

Runbook Validator, Relayer e Multisig

Runbook operativo (validato end-to-end) per validator e relayer in AWS KMS + S3, rotazione del multisig 2/3 e test di trasferimento warp sul bridge Hyperlane BitSong (crescendo-1) ↔ Base Sepolia.

Runbook operativo, in ordine di esecuzione, per gestire il bridge Hyperlane tra BitSong crescendo-1 e Base Sepolia. Copre sette operazioni:

  1. Validator Hyperlane
  2. Relayer
  3. Creare un nuovo multisig 2/3
  4. Rimuovere l'attuale multisig
  5. Impostare il nuovo multisig
  6. Test transfer BitSong → Base Sepolia
  7. Test transfer Base Sepolia → BitSong
Questa procedura è stata eseguita e verificata sul campo con 3 validator (2/3) + relayer in AWS KMS + S3, sincronizzati solo con i nodi pubblici (nessun nodo locale): announce on-chain su entrambe le chain, rotazione del multisig su entrambi i lati, e round-trip warp BitSong → Base e Base → BitSong completati. I valori e i caveat qui riportati riflettono quel test reale.

Prerequisiti

  • Un VPS (Ubuntu 24.04/26.04, 2-4 vCPU, 4-8 GB RAM) con Docker, Foundry (cast), AWS CLI v2, jq.
  • Un account AWS (per validator/relayer in produzione): utente IAM con access key, chiavi KMS e un bucket S3.
  • Accesso ai nodi pubblici BitSong + un RPC Base Sepolia con buoni limiti (vedi sotto). Non serve un nodo BitSong locale.
  • Account Cosmos finanziato con TBTSG (gas dell'announce) e indirizzi EVM dei validator/relayer finanziati con ETH su Base Sepolia (announce + gas relay).
  • Le operazioni 3-5 (rotazione multisig) richiedono la chiave owner del bridge.
TBTSG: usa il faucet (API: POST https://faucet.testnet.bitsong.io/api/v1/faucet {address} → poll /api/v1/faucet/status/{runId}). ETH Base Sepolia: faucet Coinbase / Alchemy sugli indirizzi EVM derivati da KMS.

Parametri reali del bridge

ID on-chain del bridge su crescendo-1 (verificati). Se cambiano, recuperali coi comandi in coda.

Mailbox / validatorAnnounce (BitSong)
hex
0x68797065726c616e650000000000000000000000000000000000000000000000
MerkleTreeHook (BitSong)
hex
0x726f757465725f706f73745f6469737061746368000000030000000000000000
IGP (BitSong)
hex
0x726f757465725f706f73745f6469737061746368000000040000000000000001
RoutingISM (BitSong)
hex
0x726f757465725f69736d00000000000000000000000000010000000000000001
Domain ID
number
BitSong: 71717171 — Base Sepolia: 84532
Base Sepolia: Mailbox / ISM factory
address
Mailbox 0x6966b0E55883d49BFB24539356a2f8A673E02039 — ISM factory 0xfc6e546510dC9d76057F1f76633FCFfC188CB213
Il dominio di BitSong è 71717171, non 7171. Usare il valore sbagliato produce No router enrolled for domain.
Recupera gli ID dal nodo
bitsongd query hyperlane mailboxes --output json --node $NODE | jq '.mailboxes'
bitsongd query hyperlane hooks merkle-tree-hooks --output json --node $NODE | jq '.merkle_tree_hooks'
bitsongd query hyperlane hooks igps --output json --node $NODE | jq '.igps'
bitsongd query hyperlane ism isms --output json --node $NODE | jq '.isms'

Endpoint dei nodi pubblici (verificati)

  • BitSong: usa l'IP diretto del nodo — RPC http://178.104.0.132:26657, gRPC http://178.104.0.132:9090. Il proxy https://rpc.testnet.bitsong.io può dare 502 sotto carico di indicizzazione e non espone un gRPC pubblico TLS.
  • Base Sepolia: usa un RPC con limiti alti, es. Coinbase CDP https://api.developer.coinbase.com/rpc/v1/base-sepolia/<API_KEY>. Attenzione: il path /base/ è la mainnet (8453), serve /base-sepolia/ (84532). CDP limita getLogs a 1000 blocchiindex.chunk999. L'RPC pubblico https://sepolia.base.org limita a 2000 e va in rate-limit.

Variabili d'ambiente

Terminal
export CHAIN_ID="crescendo-1"
export DOMAIN_ID="71717171"
export REMOTE_DOMAIN="84532"
export DENOM="utbtsg"
export NODE="tcp://178.104.0.132:26657"            # nodo pubblico BitSong (IP diretto)
export EVM_RPC="https://api.developer.coinbase.com/rpc/v1/base-sepolia/<API_KEY>"
export KEY_NAME="<tua-chiave>"

# Bridge (valori reali sopra)
export MAILBOX_ID="0x68797065726c616e650000000000000000000000000000000000000000000000"
export MERKLE_HOOK_ID="0x726f757465725f706f73745f6469737061746368000000030000000000000000"
export IGP_ID="0x726f757465725f706f73745f6469737061746368000000040000000000000001"
export ROUTING_ISM_ID="0x726f757465725f69736d00000000000000000000000000010000000000000001"

# AWS (validator/relayer in produzione)
export AWS_REGION="eu-central-1"
export S3_BUCKET="<tuo-bucket-checkpoint>"
export HYPERLANE_IMAGE="ghcr.io/hyperlane-xyz/hyperlane-agent:agents-v2.2.0"

1. Validator Hyperlane

Il validator firma i checkpoint dei messaggi. Serve una istanza per ciascuna chain di origine: una osserva BitSong, una osserva Base Sepolia. Per 3 validator in un multisig 2/3 ripeti questa sezione per ognuno (3 chiavi KMS distinte → 3 indirizzi EVM distinti). Questo runbook usa la configurazione AWS KMS (firma) + S3 (checkpoint), validata sul campo.

In alternativa: --validator.type hexKey --validator.key 0x… --checkpointSyncer.type localStorage --checkpointSyncer.path /checkpoints (e il relayer con --allowLocalCheckpointSyncers true). Con localStorage l'announce è un percorso file:///… leggibile solo da relayer sullo stesso host. Per un multisig multi-operatore usa S3.

Crea le risorse AWS (IAM, KMS, S3)

Crea un utente IAM con access key e configura aws sul server. Poi una chiave KMS per ogni validator + una per il relayer (asimmetriche, ECC_SECG_P256K1, Sign and verify), e ricava l'indirizzo EVM (è quello che entra nel multisig).

Terminal
# Chiave KMS del validator-N (ripeti per ogni validator + relayer)
KID=$(aws kms create-key --key-spec ECC_SECG_P256K1 --key-usage SIGN_VERIFY \
  --query KeyMetadata.KeyId --output text)
aws kms create-alias --alias-name alias/hl-validator-1 --target-key-id "$KID"

# Indirizzo EVM derivato dalla chiave KMS (da registrare nel multisig)
AWS_KMS_KEY_ID=alias/hl-validator-1 cast wallet address --aws
L'utente IAM deve poter firmare con le chiavi (kms:Sign, kms:GetPublicKey, kms:DescribeKey) e leggere/scrivere S3. Puoi concederlo via IAM policy oppure, alla creazione della chiave, via key policy (statement che dà al tuo utente kms:Sign). Vedi Agent Keys → AWS.

Crea il bucket S3 dei checkpoint (DEVE essere public-read)

I validator leggono i propri checkpoint in modo anonimo (anonymously_read_from_bucket). Con un bucket privato la GET anonima torna 403 e il validator non scrive mai i checkpoint_*.json (loop infinito su s3_storage.rs:101) → il relayer va in Unable to reach quorum. I checkpoint sono firme pubbliche non sensibili: il bucket va reso public-read (setup standard Hyperlane).
Terminal
aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" \
  --create-bucket-configuration LocationConstraint="$AWS_REGION"

# Abilita le ACL (l'agente scrive gli oggetti con ACL public-read)
aws s3api put-bucket-ownership-controls --bucket "$S3_BUCKET" \
  --ownership-controls 'Rules=[{ObjectOwnership=ObjectWriter}]'

# Consenti l'accesso pubblico in lettura
aws s3api put-public-access-block --bucket "$S3_BUCKET" \
  --public-access-block-configuration BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false

# Policy: lettura pubblica (GetObject + ListBucket)
aws s3api put-bucket-policy --bucket "$S3_BUCKET" --policy '{"Version":"2012-10-17","Statement":[{"Sid":"PublicRead","Effect":"Allow","Principal":"*","Action":["s3:GetObject","s3:ListBucket"],"Resource":["arn:aws:s3:::'"$S3_BUCKET"'","arn:aws:s3:::'"$S3_BUCKET"'/*"]}]}'

# Verifica: chiave mancante deve dare 404 (non 403)
curl -s -o /dev/null -w "anon GET -> %{http_code} (atteso 404)\n" \
  "https://$S3_BUCKET.s3.$AWS_REGION.amazonaws.com/bitsong-val1/checkpoint_latest_index.json"

Crea e finanzia la chiave Cosmos di announce

BitSong è una chain Cosmos: anche con la firma KMS, l'announce on-chain è una tx Cosmos e richiede una chiave Cosmos (hex) con saldo TBTSG. Una chiave condivisa tra i validator (e il relayer) è sufficiente.

Terminal
bitsongd keys add hyperlane-signer --keyring-backend test
COSMOS_SIGNER_KEY=0x$(bitsongd keys export hyperlane-signer --unarmored-hex --unsafe --keyring-backend test 2>&1 | tail -1)
export COSMOS_SIGNER_KEY
# Finanzia l'indirizzo bitsong1… via faucet (vedi Prerequisiti)

Crea l'agent-config.json

Terminal
mkdir -p $HOME/hl/config
$HOME/hl/config/agent-config.json
{
  "chains": {
    "bitsong": {
      "name": "bitsong",
      "chainId": "crescendo-1",
      "domainId": 71717171,
      "protocol": "cosmosNative",
      "bech32Prefix": "bitsong",
      "slip44": 639,
      "contractAddressBytes": 32,
      "canonicalAsset": "utbtsg",
      "rpcUrls": [{ "http": "http://178.104.0.132:26657" }],
      "grpcUrls": [{ "http": "http://178.104.0.132:9090" }],
      "nativeToken": { "name": "BitSong", "symbol": "BTSG", "decimals": 6, "denom": "utbtsg" },
      "gasPrice": { "amount": "0.025", "denom": "utbtsg" },
      "gasMultiplier": "1.5",
      "blocks": { "confirmations": 1, "estimateBlockTime": 6, "reorgPeriod": 1 },
      "index": { "from": 3895000, "chunk": 500 },
      "mailbox": "0x68797065726c616e650000000000000000000000000000000000000000000000",
      "validatorAnnounce": "0x68797065726c616e650000000000000000000000000000000000000000000000",
      "merkleTreeHook": "0x726f757465725f706f73745f6469737061746368000000030000000000000000",
      "interchainGasPaymaster": "0x726f757465725f706f73745f6469737061746368000000040000000000000001"
    },
    "basesepolia": {
      "name": "basesepolia",
      "chainId": 84532,
      "domainId": 84532,
      "protocol": "ethereum",
      "rpcUrls": [{ "http": "https://api.developer.coinbase.com/rpc/v1/base-sepolia/<API_KEY>" }],
      "nativeToken": { "name": "Ether", "symbol": "ETH", "decimals": 18 },
      "blocks": { "confirmations": 1, "estimateBlockTime": 2, "reorgPeriod": 1 },
      "index": { "from": 39040037, "chunk": 999 },
      "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039",
      "validatorAnnounce": "0x20c44b1E3BeaDA1e9826CFd48BeEDABeE9871cE9",
      "merkleTreeHook": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD",
      "interchainGasPaymaster": "0x28B02B97a850872C4D33C3E024fab6499ad96564"
    }
  }
}
Il validator Cosmos costruisce il merkle tree dallo stato della chain (gRPC), non rigiocando tutti i blocchi: con bitsong.index.fromrecente (es. altezza attuale − qualche migliaio) l'albero è comunque corretto. Con index.from: 1 su una chain a milioni di blocchi, più agenti scansionano l'intera storia ed esauriscono max_open_connections dell'RPC CometBFT (il nodo smette di rispondere in HTTP). Tieni bitsong.index.chunk ~500 e basesepolia.index.chunk999 (limite CDP).

Avvia i due validator (per ciascun validator del multisig)

Terminal
docker pull --platform linux/amd64 $HYPERLANE_IMAGE
# file ambiente: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, COSMOS_SIGNER_KEY, S3_BUCKET
docker run -d \
  --name hl-val1-bitsong --restart unless-stopped --network host \
  --user 0:0 --ulimit nofile=65536:65536 \
  --env-file $HOME/hl/agent.env \
  -e CONFIG_FILES=/config/agent-config.json \
  -v $HOME/hl/config/agent-config.json:/config/agent-config.json:ro \
  -v $HOME/hl/db/val1-bitsong:/hyperlane_db \
  $HYPERLANE_IMAGE ./validator \
  --db /hyperlane_db --originChainName bitsong --reorgPeriod 1 --interval 10 \
  --validator.type aws --validator.region $AWS_REGION --validator.id alias/hl-validator-1 \
  --chains.bitsong.signer.type cosmosKey --chains.bitsong.signer.key $COSMOS_SIGNER_KEY --chains.bitsong.signer.prefix bitsong \
  --checkpointSyncer.type s3 --checkpointSyncer.bucket $S3_BUCKET --checkpointSyncer.region $AWS_REGION --checkpointSyncer.folder bitsong-val1 \
  --metrics-port 9101 --log.format json --log.level info
  • Il validator BitSong usa sempre cosmosKey per il signer della chain (firma l'announce on-chain); la firma dei checkpoint usa aws (KMS).
  • Il validator Base Sepolia usa aws sia per il checkpoint sia per il signer della chain. Finanzia il suo indirizzo EVM (derivato da KMS) con ETH su Base Sepolia prima di avviarlo.
  • --user 0:0 evita Permission denied sui volumi db; --ulimit nofile evita Too many open files.

Verifica l'announcement e i checkpoint

Terminal
# Announce BitSong (fino a ~2 min) — VALIDATOR_ADDR = indirizzo KMS del validator
bitsongd query hyperlane ism announced-storage-locations \
  $MAILBOX_ID $(echo $VALIDATOR_ADDR | tr '[:upper:]' '[:lower:]') --output json --node $NODE | jq '.storage_locations'

# Announce Base Sepolia
cast call 0x20c44b1E3BeaDA1e9826CFd48BeEDABeE9871cE9 \
  "getAnnouncedStorageLocations(address[])(string[][])" "[$VALIDATOR_ADDR]" --rpc-url $EVM_RPC

# Checkpoint scritti su S3 (compaiono quando passa un nuovo messaggio)
aws s3 ls --recursive s3://$S3_BUCKET/bitsong-val1/ | grep checkpoint

2. Relayer

Il relayer legge i checkpoint firmati (da S3) e consegna i messaggi in entrambe le direzioni. Ne basta uno. Usa una chiave KMS dedicata per Base Sepolia e la stessa chiave Cosmos per BitSong.

Finanzia l'indirizzo EVM del relayer (KMS) con ETH su Base Sepolia (gas di consegna).
Terminal
docker run -d \
  --name hl-relayer --restart unless-stopped --network host \
  --user 0:0 --ulimit nofile=65536:65536 \
  --env-file $HOME/hl/agent.env \
  -e CONFIG_FILES=/config/agent-config.json \
  -v $HOME/hl/config/agent-config.json:/config/agent-config.json:ro \
  -v $HOME/hl/db/relayer:/hyperlane_db \
  $HYPERLANE_IMAGE ./relayer \
  --db /hyperlane_db --relayChains bitsong,basesepolia \
  --gasPaymentEnforcement '[{"type":"none"}]' \
  --chains.bitsong.signer.type cosmosKey --chains.bitsong.signer.key $COSMOS_SIGNER_KEY --chains.bitsong.signer.prefix bitsong \
  --chains.basesepolia.signer.type aws --chains.basesepolia.signer.region $AWS_REGION --chains.basesepolia.signer.id alias/hl-relayer \
  --metrics-port 9301 --log.format json --log.level info
  • Con i checkpoint su S3 il relayer li legge dall'URL annunciato — non serve --allowLocalCheckpointSyncers (è solo per localStorage).
  • --gasPaymentEnforcement '[{"type":"none"}]' disabilita l'enforcement del gas (solo testnet).
Verifica
docker logs -f --tail 100 hl-relayer
Per consegnare solo i messaggi del tuo warp token:
Terminal
EVM_PADDED=$(printf "0x%064s" "${HYP_ERC20_ADDR#0x}" | tr ' ' '0')
--whitelist "[{\"senderAddress\":\"$TOKEN_ID\",\"destinationDomain\":\"$REMOTE_DOMAIN\"},{\"senderAddress\":\"$EVM_PADDED\",\"destinationDomain\":\"$DOMAIN_ID\"}]"

3. Creare un nuovo multisig 2/3

Su Hyperlane il MultisigISM è immutabile: se ne crea uno nuovo con la lista aggiornata di validator e la soglia, poi si ripunta la rotta del RoutingISM (operazioni 4 e 5).

Le operazioni 3-5 vanno firmate con la chiave owner del bridge (--from <owner-key>).

Raccogli i 3 indirizzi EVM dei validator (derivati da KMS in sezione 1) e crea il MultisigISM con soglia 2 su 3.

create-message-id-multisig richiede la lista di validator ordinata in modo crescente per valore di byte (esadecimale, case-insensitive), altrimenti fallisce con validator addresses are not sorted correctly in ascending order. Es.: 0x45ac… < 0x740b… < 0xdb4e….
Owner — Terminal
# Ordina V1,V2,V3 in modo crescente (per byte) prima di creare l'ISM
export V1="0x<addr-piu-basso>"; export V2="0x<addr-medio>"; export V3="0x<addr-piu-alto>"

# Nota: con --gas auto il binario stampa "gas estimate: N" prima del JSON → estrai il txhash così
TX_HASH=$(bitsongd tx hyperlane ism create-message-id-multisig "$V1,$V2,$V3" 2 \
    --from <owner-key> --keyring-backend test --chain-id $CHAIN_ID --node $NODE \
    --gas auto --gas-adjustment 1.5 --fees 10000${DENOM} --output json -y 2>&1 \
    | grep -o '"txhash":"[A-Fa-f0-9]*"' | head -1 | cut -d'"' -f4)
echo "TX: $TX_HASH"

NEW_ISM_ID=$(bitsongd query tx $TX_HASH --output json --node $NODE | \
  jq -r 'first(.events[] | select(.type | test("MultisigIsm")) | .attributes[] | select(.key=="ism_id") | .value | fromjson)')
echo "Nuovo MultisigISM (2/3): $NEW_ISM_ID"
Soglia 2 su 3: servono 2 firme su 3. Il bridge resta operativo anche con un validator offline. Questo ISM verifica la direzione Base Sepolia → BitSong (lato Cosmos); per la direzione opposta aggiorna l'ISM lato EVM (sotto).

4. Rimuovere l'attuale multisig

Verifica quale ISM è attualmente sulla rotta del dominio 84532, poi rimuovi quella mappatura dal RoutingISM.

Terminal
ACTIVE_ISM=$(bitsongd query hyperlane ism isms --output json --node $NODE \
  | jq -r '.isms[] | select(."@type" | test("RoutingISM")) | .routes[] | select(.domain==84532) | .ism')
echo "ISM attuale (da sostituire): $ACTIVE_ISM"
Owner — Terminal
bitsongd tx hyperlane ism remove-routing-ism-domain $ROUTING_ISM_ID $REMOTE_DOMAIN \
    --from <owner-key> --keyring-backend test --chain-id $CHAIN_ID --node $NODE \
    --gas auto --gas-adjustment 1.5 --fees 10000${DENOM} -y
set-routing-ism-domainnon sovrascrive un dominio già mappato (emette l'evento ma lo stato non cambia): bisogna prima rimuovere e poi impostare. Tra i due comandi c'è una breve finestra in cui il dominio 84532 resta senza ISM — pianifica la rotazione a basso traffico. Per il rollback rimetti la rotta sull'ISM precedente (remove + set verso il vecchio ID).

5. Impostare il nuovo multisig

Attendi che la rimozione sia in un blocco, poi ripunta la rotta del dominio Base Sepolia sul nuovo ISM 2/3.

Owner — Terminal
bitsongd tx hyperlane ism set-routing-ism-domain $ROUTING_ISM_ID $REMOTE_DOMAIN $NEW_ISM_ID \
    --from <owner-key> --keyring-backend test --chain-id $CHAIN_ID --node $NODE \
    --gas auto --gas-adjustment 1.5 --fees 10000${DENOM} -y
Verifica
ACTIVE_ISM=$(bitsongd query hyperlane ism isms --output json --node $NODE \
  | jq -r '.isms[] | select(."@type" | test("RoutingISM")) | .routes[] | select(.domain==84532) | .ism')
echo "ISM attivo: $ACTIVE_ISM  (atteso: $NEW_ISM_ID)"
bitsongd query hyperlane ism isms --output json --node $NODE \
  | jq --arg id "$ACTIVE_ISM" '.isms[] | select(.id==$id) | {id, validators, threshold}'

Aggiornare l'ISM lato EVM

La direzione BitSong → Base Sepolia è verificata da un StaticMessageIdMultisigISM su Base Sepolia, agganciato all'HypERC20. Portalo a 2/3: l'ISM puoi deployarlo con qualsiasi chiave finanziata (anche la KMS del relayer via --aws); solo setInterchainSecurityModule richiede la owner key dell'HypERC20.

Terminal
export HYP_ERC20_ADDR="0x27d0F8cBF125995f91407e9E5F70EF52c4412878"   # verifica l'indirizzo corrente
FACTORY="0xfc6e546510dC9d76057F1f76633FCFfC188CB213"
VALS="[$V1,$V2,$V3]"   # stessi 3 indirizzi, ordinati

# 1. Predici e deploya l'ISM 2/3 (qui con la KMS del relayer: AWS_KMS_KEY_ID=alias/hl-relayer ... --aws)
NEW_EVM_ISM=$(cast call $FACTORY "deploy(address[],uint8)(address)" "$VALS" 2 --rpc-url $EVM_RPC)
cast send $FACTORY "deploy(address[],uint8)" "$VALS" 2 --aws --rpc-url $EVM_RPC
echo "Nuovo ISM EVM: $NEW_EVM_ISM"

# 2. Aggancialo all'HypERC20 (OWNER key dell'HypERC20)
cast send $HYP_ERC20_ADDR "setInterchainSecurityModule(address)" $NEW_EVM_ISM \
    --private-key <owner-evm-key> --rpc-url $EVM_RPC

# 3. Verifica
cast call $HYP_ERC20_ADDR "interchainSecurityModule()(address)" --rpc-url $EVM_RPC
Senza questo aggiornamento, BitSong → Base Sepolia resterebbe verificato dal vecchio set di validator. Aggiorna entrambi i lati per completare la rotazione.

6. Test transfer BitSong → Base Sepolia

I token vengono bloccati come collaterale su BitSong e mintati come HypERC20 su Base Sepolia.

Terminal
export TOKEN_ID="0x726f757465725f61707000000000000000000000000000010000000000000000"
export HYP_ERC20_ADDR="0x27d0F8cBF125995f91407e9E5F70EF52c4412878"

RECIPIENT_ADDR="0x<evm-recipient>"
RECIPIENT_BYTES32=$(printf "0x%064s" "${RECIPIENT_ADDR#0x}" | tr ' ' '0')

REQUIRED_FEE=$(bitsongd query hyperlane hooks quote-gas-payment $IGP_ID $REMOTE_DOMAIN 300000 -o json --node $NODE | jq -r '.gas_payment[0].amount')

bitsongd tx warp transfer $TOKEN_ID $REMOTE_DOMAIN $RECIPIENT_BYTES32 7777 \
    --max-hyperlane-fee "${REQUIRED_FEE}utbtsg" \
    --from $KEY_NAME --keyring-backend test --chain-id $CHAIN_ID --node $NODE \
    --gas auto --gas-adjustment 1.5 --fees 10000${DENOM} --output json -y

Dopo la consegna (i validator firmano il checkpoint del nuovo messaggio → S3 → relayer), verifica su Base Sepolia (balanceOf e totalSupply aumentano di 7777):

Terminal
cast call "$HYP_ERC20_ADDR" "totalSupply()(uint256)" --rpc-url "$EVM_RPC"
cast call "$HYP_ERC20_ADDR" "balanceOf(address)(uint256)" "$RECIPIENT_ADDR" --rpc-url "$EVM_RPC"

7. Test transfer Base Sepolia → BitSong

Ritorno: l'HypERC20 viene bruciato su Base Sepolia, il collaterale sbloccato su BitSong.

transferRemote brucia i token del chiamante: devi già possedere HypERC20 (dalla direzione BitSong → Base), altrimenti ERC20: burn amount exceeds balance.
Terminal
# Destinatario Cosmos (bech32) → bytes32
COSMOS_RECIPIENT="bitsong1..."
ADDR_HEX=$(bitsongd keys parse "$COSMOS_RECIPIENT" 2>&1 | grep -i bytes | awk '{print $2}')
RECIPIENT_BYTES32="0x$(printf '%064s' "$ADDR_HEX" | tr ' ' '0')"

# Quota il gas EVM e brucia verso BitSong (dominio 71717171).
# Se il detentore è una chiave KMS, firma con --aws (AWS_KMS_KEY_ID=alias/…); altrimenti --private-key.
VAL=$(cast call "$HYP_ERC20_ADDR" "quoteGasPayment(uint32)(uint256)" $DOMAIN_ID --rpc-url $EVM_RPC)
cast send "$HYP_ERC20_ADDR" "transferRemote(uint32,bytes32,uint256)" \
    "$DOMAIN_ID" "$RECIPIENT_BYTES32" 4444 --value "$VAL" \
    --aws --rpc-url "$EVM_RPC"

Verifica lo sblocco su BitSong (il saldo utbtsg del destinatario aumenta di 4444):

Terminal
bitsongd query bank balances "$COSMOS_RECIPIENT" --node $NODE --output json | jq '.balances'
Usa il dominio 71717171. Su testnet (enforcement off) il --value minimo richiesto è quello di quoteGasPayment (spesso 1 wei).

Operazioni e troubleshooting

docker ps --filter "name=hl-" --format "table {{.Names}}\t{{.Status}}"

Agent Keys (AWS KMS/S3)

Setup di produzione: IAM, KMS e bucket S3.

Warp Token

Deploy del warp route ed enrollment dei router.

Entra nel Multisig (IT)

Aggiungersi come validator a un bridge esistente con AWS.
Copyright © 2026