Guides › Bridge funds
End users should use the UI at btcw.tech/swap/#bridge. This page is for bots, wallets and apps that call the contracts directly. How it works: Bridge.
Pre-send checks
paused()on the source router must befalse(otherwise: revertPaused()).allowlistEnabled(): iftrue,isAllowed(wallet)must also betrue(otherwise:NotAllowed(address)). Currently disabled.- Destination liquidity: the destination router must hold enough tokens. Withdrawing to Arbitrum: the USDC/USD₮0 balance of that route's Arbitrum router. Depositing to 482120: the USDw balance of the 482120 router. If it is short, the message arrives but cannot be delivered.
- From 482120: the amount must be divisible by
granularity()= 1012.
# destination liquidity when withdrawing USD₮0 to Arbitrum
cast call 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9 "balanceOf(address)(uint256)" 0x984fDC24D1A2Be538C7290487d0fc212635D98A5 --rpc-url https://arb1.arbitrum.io/rpc
# destination liquidity when depositing USD₮0 to 482120
cast call 0x3b5BD179CA6Aab4DAF0f842829776843ABe6e727 "balanceOf(address)(uint256)" 0xD6d8452FdCC26B155BEc4fc878635c05CaC95FB1 --rpc-url https://rpc.btcw.tech
Steps
approve(router, amount)on the source-chain token — the exact amount.transferRemote(uint32 destination, bytes32 recipient, uint256 amount)on the source-chain router,msg.value = 0.destination:482120to deposit,42161to withdraw.recipient: destination wallet address left-padded with zeros to 32 bytes (Hyperlane standard).- Wait 10–30 seconds and watch for
ReceivedTransferRemoteon the destination router.
Pad on the left: cast abi-encode "f(address)" 0xABC… produces the right format. Do not use cast to-bytes32 — it pads on the right,
and the funds go to an address nobody holds the key for.
Withdraw: 482120 → Arbitrum
# withdraw 1 USDw as USD₮0 to wallet 0xABC… on Arbitrum
RECIPIENT=$(cast abi-encode "f(address)" 0xABC...)
cast send 0x3b5BD179CA6Aab4DAF0f842829776843ABe6e727 "approve(address,uint256)" \
0xD6d8452FdCC26B155BEc4fc878635c05CaC95FB1 1000000000000000000 --rpc-url https://rpc.btcw.tech --private-key $PK
cast send 0xD6d8452FdCC26B155BEc4fc878635c05CaC95FB1 "transferRemote(uint32,bytes32,uint256)" \
42161 $RECIPIENT 1000000000000000000 --rpc-url https://rpc.btcw.tech --private-key $PK
1 USDw (18 decimals) → 1 USD₮0 (6 decimals) received on Arbitrum.
Deposit: Arbitrum → 482120
# deposit 5 USDC (6 decimals) → 5 USDw to wallet 0xABC… on 482120
RECIPIENT=$(cast abi-encode "f(address)" 0xABC...)
cast send 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 "approve(address,uint256)" \
0x10B5757aC9a40E3467846e35749ae30b00ef0182 5000000 --rpc-url https://arb1.arbitrum.io/rpc --private-key $PK
cast send 0x10B5757aC9a40E3467846e35749ae30b00ef0182 "transferRemote(uint32,bytes32,uint256)" \
482120 $RECIPIENT 5000000 --rpc-url https://arb1.arbitrum.io/rpc --private-key $PK
The receiving wallet needs a little BTCw to do anything further on 482120 — the bridge only moves USDw.
With ethers v6
import { Contract, AbiCoder, parseUnits } from "ethers";
const router = new Contract("0x10B5757aC9a40E3467846e35749ae30b00ef0182", [
"function transferRemote(uint32,bytes32,uint256) payable returns (bytes32)",
"function paused() view returns (bool)",
], signer);
const usdc = new Contract("0xaf88d065e77c8cC2239327C5EDb3A432268e5831", ["function approve(address,uint256) returns (bool)"], signer);
const amount = parseUnits("5", 6);
const recipient = AbiCoder.defaultAbiCoder().encode(["address"], [await signer.getAddress()]); // left-padded to 32 bytes
if (await router.paused()) throw new Error("bridge is paused");
await (await usdc.approve(router.target, amount)).wait();
const tx = await router.transferRemote(482120, recipient, amount, { value: 0 });
const receipt = await tx.wait(); // watch ReceivedTransferRemote on the 482120 router to know the funds arrived
Warp route config (Hyperlane registry format)
tokens:
- chainName: arbitrum # domain 42161
standard: EvmHypCollateral
addressOrDenom: "0x984fDC24D1A2Be538C7290487d0fc212635D98A5"
collateralAddressOrDenom: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" # USD₮0
decimals: 6
connections: [{ token: "ethereum|bitcoinswap|0xD6d8452FdCC26B155BEc4fc878635c05CaC95FB1" }]
- chainName: bitcoinswap # domain 482120
standard: EvmHypCollateral
addressOrDenom: "0xD6d8452FdCC26B155BEc4fc878635c05CaC95FB1"
collateralAddressOrDenom: "0x3b5BD179CA6Aab4DAF0f842829776843ABe6e727" # USDw
decimals: 18
connections: [{ token: "ethereum|arbitrum|0x984fDC24D1A2Be538C7290487d0fc212635D98A5" }]
# the USDC route is identical: Arbitrum 0x10B5757a…0182 (USDC 0xaf88…5831) ⇄ 482120 0x10EDFF80…8Fd9b (USDw)
The Mailbox and ISM are a dedicated deployment (see Bridge), so this config is not in Hyperlane's public registry. Source code: 7 contracts on the 482120 side are verified on explorer.btcw.tech; 9 contracts on the Arbitrum side are exact matches on Sourcify / arbitrum.blockscout.com.