Bridge overview
ChainHop is a permissionless lock-and-mint bridge for ERC20s and ERC721s, built on Chainlink CCIP as an owner-routed mesh of one vault per chain.
On this page
What ChainHop is
ChainHop moves ERC20 tokens and ERC721 NFTs between chains. It is not a liquidity network and it holds no pools. It is a mesh of vault contracts, one per chain per asset class, that talk to each other over Chainlink CCIP.
There are two independent meshes:
ChainHopTokenVault, the ERC20 mesh. One instance per chain.ChainHopNFTVault, the ERC721 mesh. One instance per chain, with its own peer registry and its own routing table.
The two meshes share a base contract and therefore share a read surface
(homeSelectorOf, nextHopOf, bridgingPaused), but they are separate
contracts with separate state. Calling one with the other's addresses is always
a mistake.
Both are permissionless in the asset dimension: the owner wires chains, never tokens. Any ERC20 and any ERC721 can be bridged without registration, allowlisting, or a pool being seeded first.
No CCIP token pools
ChainHop uses CCIP for arbitrary messaging only. Every message it sends carries
an empty tokenAmounts array. The asset movement is ChainHop's own lock and
mint accounting, not CCIP's token-transfer path. That is why an arbitrary ERC20
works without a CCIP token pool existing for it.
The CCIP concepts you need
You do not need to know CCIP to use ChainHop, but five terms appear in the ABI and in the events.
Chain selector. CCIP identifies a chain by a uint64 selector, which is not
the EVM chain id and has no relationship to it. Every ChainHop entry point takes
a selector, never a chain id. Selectors exceed JavaScript's safe integer range:
handle them as bigint or as strings. Number(selector) silently corrupts the
value.
Router. Each chain has a CCIP Router contract. A vault calls
router.ccipSend(destinationSelector, message) to send and
router.getFee(destinationSelector, message) to price. Inbound, the router is
the only address allowed to deliver a message into the vault.
Fee token. CCIP lets a sender pay in LINK or in the source chain's native
coin. ChainHop always pays in native coin: its outbound message is built with
feeToken: address(0). So the fee you attach as msg.value on ApeChain is APE,
on BNB Chain it is BNB, on HyperEVM it is HYPE, and on Base, Ethereum, Robinhood
Chain and Arbitrum it is ETH.
extraArgs. Per-lane send options. ChainHop sets two: a destination
gasLimit (how much gas the receiving vault gets when the message executes) and
allowOutOfOrderExecution. It builds GenericExtraArgsV2 by default and can
fall back to EVMExtraArgsV1 per lane if a lane ever rejects the V2 tag.
Message id and execution. ccipSend returns a bytes32 message id. CCIP
then waits for source-chain finality, commits the message, and executes it on
the destination by calling the receiver. Execution can fail. A failed CCIP
message is retryable by manual execution and is never dropped.
Finality is the reason a hop takes minutes rather than seconds. Each leg waits for source-chain finality before it can execute on the next chain.
Lock and mint, burn and unlock
Every asset has exactly one home chain: the chain whose vault holds the canonical asset in escrow. Which model applies to a given transfer follows from where the asset is home relative to where it is going.
So it is both: lock and mint outbound from home, burn and unlock inbound to home, and burn and mint between two non-home chains. Escrow never leaves the home chain.
The ordering invariant is the safety property. The lock or the burn happens in the source transaction, strictly before any release message exists. Home-chain escrow therefore always covers total wrapped supply everywhere else.
A vault decides which case it is in by a single read. homeSelectorOf(asset)
returns 0 for anything the vault did not itself deploy: that is a canonical
asset, and this chain is (or becomes) its home. A non-zero value means the
address is a ChainHop wrapper, and gives the home chain's selector.
Home is per-deployment, not per-token
The mesh has no notion of "the same token on two chains". If a project deploys
its ERC20 independently on ApeChain and on Base, bridging each one creates two
unrelated mesh assets with two different homes and two different wrappers.
Identity in the mesh is the pair (homeSelector, homeAsset), and the first
bridge out of a chain fixes that pairing permanently. Nothing warns you.
The chains
ChainHop spans ApeChain, Base, Ethereum, Robinhood Chain, Arbitrum One, BNB Chain and HyperEVM. The vault addresses are identical on every chain: one token-vault address and one NFT-vault address, mesh-wide. Every wrapped asset also resolves to one address across destination chains because its CREATE2 inputs are the same.
Chain ids and selectors
Bridge entry points take CCIP selectors, not EVM chain ids.
The chain id and the selector are unrelated numbers. Never derive one from the
other, and never round-trip a selector through a JavaScript number.
Read each vault's CCIP router with getRouter() and its wrapper implementation
with wrappedImplementation().
Routing: the mesh is a distance-vector table
A vault does not know a path. It knows one thing per destination: the next hop.
nextHopOf(finalSelector) returns the selector of the chain to send to next.
When that equals the final selector, the hop is direct. When it differs, the
message is relayed: the next vault receives it, reads its own table, and
forwards.
Routes can change behind the configuration timelock. Read
nextHopOf(dest.selector) on the source vault and continue the lookup on each
transit vault until the next hop equals the destination. See
How a hop works for multi-hop tracing.
Envelopes carry a TTL. The vault stamps maxHops() into an envelope at
origination and decrements it on each forward. A routing loop ends with
HopLimitReached instead of draining node balances.
Trust and security model
Who can execute a message
Two gates, both mandatory:
- The CCIP router is the only caller that can deliver into the vault. This is
enforced by the
CCIPReceiverBasebase contract. - The vault then checks the CCIP-authenticated source. It decodes the message
sender and requires it to equal
peers[message.sourceChainSelector]. Anything else reverts withUntrustedSource(selector, sender).
Peers are instances of the same contract, so a node either originates an envelope truthfully or forwards it byte for byte. That is what makes envelope contents trustworthy across a multi-hop path: no intermediate node can rewrite the order.
What the owner can do
The vault is Ownable. Trust-critical wiring is protected by an on-chain
timelock, while operational settings remain immediate.
Timelocked. After finalizeSetup(), peers, routes and the router can no
longer change in one transaction. Each change must be queued, emits a public
event and waits TIMELOCK_DELAY before execution. Read setupFinalized() and
configExecutableAt(id) for the current state.
The residual trust surface is the owner: a malicious peer or router can redirect traffic or mint unbacked wrappers after the queued change matures. Monitor the queue events if your integration depends on the bridge's trust configuration.
withdrawNative is not timelocked because a node's native balance is
owner-supplied fee float used to pay forward hops, never user escrow.
Pause semantics
bridgingPaused gates the whenNotPaused modifier on bridgeTokens and
bridgeNFTs only. Inbound delivery, minting, unlocking and claiming all keep
working while paused, so a pause can never wedge messages that are already in
flight.
Where to go next
- How a hop works for the lifecycle, multi-hop tracing and failure recourse.
- Token vault and NFT vault for the two ABI surfaces.
- Wrapped assets for how a destination asset comes into existence and what it can do.
- Integration recipes for working code.
- Subgraph reference for the indexed history.