How do cross-chain bridges work? A case on Wormhole (Part 1)
It might be a bit surprising to our readers that crypto hacks caused over $3 billion lost of funds in 2022. However, a majority of the lost was on cross-chain bridges (e.g., Ronin Network, Wormhole, Nomad, BNB Token Hub, Horizon, Qubit).
Why are cross-chain bridges so vulnerable? Are they not designed or implemented carefully? On the contrary, due to their high value they are developed with the highest possible standards by some of the best engineers (e.g., leoluk). The real reason is that cross-chain bridges are just too complex to get right, involving too many technical subtleties.
In this article series, we will elaborate the internals of cross-chain bridges, how they are implemented and what their caveats are from the user’s perspective. We will use a state-of-the-art bridge Wormhole as an example.
At a high level, what’s a cross-chain bridge?
A cross-chain bridge is just a (virtual) link between two blockchains for message communication. The message can essentially be any bit sequence (such as transferring a token or an NFT from Chain A to Chain B).
Suppose you want to transfer a bitcoin from Ethereum to Solana, you need to do at least two operations:
Debit a bitcoin from a source account on Ethereum
Credit a bitcoin to a target account on Solana
However, because these two chains do not directly communicate (in a technical term: there is no way to touch both chains in a single transaction), you need a man-in-the-middle who owns bitcoins on both chains and who can help you with two steps:
Receive a bitcoin from your source account to their account on Ethereum
Send a bitcoin (or an equivalent) to your target account from their account on Solana
A cross-chain bridge is such a man-in-the-middle. There are several challenges here:
How does a bridge ensure that these two steps happen atomically (i.e., either they both succeed or they both fail)? If any one of them succeeds but the other fails, then either you or the bridge will lose money.
How does a bridge ensure that the same message is delivered (i.e., only one bitcoin, not one Dogecoin, or any other coin, or two bitcoins, and to your target account, not to anyone else’s account)?
How does a bridge ensure that the same message is delivered exactly once (e.g., their account on Solana will never double-send to your target account)?
Web3 researchers have developed multiple different solutions to address these challenges (with different tradeoffs). For example, multi-sig validators, multi-party computation (MPC), rollup and optimistic bridges, etc. See the Interoperability Trilemma for a nice summary.
We will focus on explaining a representative solution based on guardians (i.e., a type of multi-sig validators) used by Wormhole.
At a high level, how does Wormhole work?
As of Jan 2023, Wormhole bridges 20 different chains including Ethereum, Solana, Binance Smart Chain, Polygon, Aptos, and so on.
It achieves this by operating a network of 19 nodes called guardians (see all the 19 guardian addresses), and a number of smart contracts deployed on each chain (including a core bridge contract, a token bridge contract and an NFT bridge contract). The core bridge contract provides functions for emitting messages, verifying guardian signatures, and so on. The token and NFT bridge contracts are responsible for transferring tokens and NFT, respectively.
The 19 guardians each observe messages emitted by the Wormhole core bridge contracts continuously, and sign on the messages (e.g., a message like “Alice just sent a bitcoin to Wormhole on Ethereum, and she wanted Wormhole to send a bitcoin to Bob on Solana).
The guardians each hold equal weight. When a supermajority (2/3) of them sign a message, the guardian network produces a Verifiable Action Approval (VAA), which serves as a proof for Wormhole to deliver the same message on the target chain (e.g., send a bitcoin to Bob on Solana).
VAA: a core technical component of Wormhole
VAAs are at the heart of all Wormhole technical details. There are a number of questions on VAAs that keen readers may wonder (like what the sec3 core team did):
What information does a VAA contain? What exactly is the format of VAA?
How are the VAAs used? Where are they stored? How to get them?
What’s the end-to-end workflow of a cross-chain message from a user’s perspective? How many transactions are needed to complete a message (i.e., from sending on the source chain to receiving on the target chain)?
How are the guardian signatures verified (to prevent fake VAAs)? What if a (malicious) guardian signs the same message multiple times and produces multiple signatures?
How to ensure the tokens are the same (or equivalent) on the bridged two chains? What if Wormhole does not have the same token (or enough amount) on the target chain as the user sent on the source token?
Who will perform the message delivery steps on the target chain (since it requires paying transaction fees)?
How to prevent the same VAA used twice on the target chain?
Next, we will answer these questions one by one (with code examples whenever necessary).
The VAA format and internals
Each VAA is encoded as a byte array with two parts — a Header and a Body.
The Header contains information about the guardians and their signatures:
byte version (VAA Version)
u32 guardian_set_index (Indicates which guardian set is signing)
u8 len_signatures (Number of signatures stored)
[][66]byte signatures (Collection of ecdsa signatures)
The Body contains detailed information about the message (e.g., timestamp
of the source transaction, emitter_chain
, emitter_address
, sequence
, and the message payload
):
u32 timestamp (Timestamp of the block where the source transaction occurred)
u32 nonce (A grouping number)
u16 emitter_chain (Wormhole ChainId of emitter contract)
[32]byte emitter_address (Emitter contract address, in Wormhole format)
u64 sequence (Strictly increasing sequence, tied to emitter address & chain)
u8 consistency_level (What finality level was reached before emitting this message)
[]byte payload (VAA message content)
In particular, sequence
is an important piece of information to ensure that each VAA is unique for a message (i.e., any two different messages would have different VAAs even if they have the same message content).
The sequence
number is incremented by 1 in the useSequence
function for every publishMessage
call:
This is critical to guarantee the same message will never be delivered twice. We will elaborate this point further in the next section.
The payload
byte array contains the message content. For example, for a token Transfer
, it includes the transfer amount, token address, token chain, the recipient address, the target chain ID, transfer fee, etc:
struct Transfer {
// PayloadID uint8 = 1
uint8 payloadID;
// Amount being transferred (big-endian uint256)
uint256 amount;
// Address of the token. Left-zero-padded if shorter than 32 bytes
bytes32 tokenAddress;
// Chain ID of the token
uint16 tokenChain;
// Address of the recipient. Left-zero-padded if shorter than 32 bytes
bytes32 to;
// Chain ID of the recipient
uint16 toChain;
// Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount.
uint256 fee;
}
How are the VAAs used? Where are they stored?
Once a VAA is produced (i.e., a message that has been signed by 2/3 guardians), it will be stored in the guardian network (likely for a long time or even permanently).
Each VAA is uniquely indexed by its emitterChain
, emittedAddress
and sequence
, and can be obtained by querying a guardian node (via an RPC API) with this information:
// Fetch the signedVAA from the Wormhole Network (this may require retries while you wait for confirmation)
const { signedVAA } = await getSignedVAA(
WORMHOLE_RPC_HOST,
CHAIN_ID_ETH,
emitterAddress,
sequence
);
b, err := s.db.GetSignedVAABytes(db.VAAID{
EmitterChain: vaa.ChainID(req.MessageId.EmitterChain.Number()),
EmitterAddress: addr,
Sequence: req.MessageId.Sequence,
})
For example, an RPC query with
"EmitterChain": "ethereum",
"EmitterAddress": "0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585",
"Sequence": "99139",
returns the following VAA bytes:
{"vaaBytes":"AQAAAAINAAOuz9Ep03HIS1T2ypAoahT/BO2UBHpRWxiA1CfnlUUKVvMnPyR+E4Eusb1JkHivpoi6mBcEUvDkGW/bOThL7hoAArNvvcthq87MTGsQHjReeIIL0o3G0Epg/kT438O3GzDtZki5wlZ5YpRbuISJlT3nB7x8fYsxNUpJFAhO1DOyHj8BA3XfOUc2I0oIbQF8waRjl27BQQJsexBmNvXyT9XWJEwoXqq5hSQ+tf/0AUuGwHQ9ae84ENBpxyQhgfLxjqm3YAoBBAlFk8Rj/3Am5ULZUZGpGuoghigwNpCH8/OQLq9KEpxYXgTVecr9yznjvlw8PeerhjSUvgjjEuoT6bMV9WWHhPYBBaJOC3wrK3Lp6fbTcLlkRxJqjLPWeu6I33a3BKSAbJtcRAdm40CWwX5t0Kgt5EadmrvIRLIiKu0w037ok9nZUdIABocfns7u1MOoYaUdozFdyd2yJauIXNtAHrhHWE7QXq2XCITt6LK3qSGex9kwZhVeQVxfbplphKDS4ecDEo3l7/UBDLx+py+3KJWOByFPE5ZSBvimJ3GL3KOARgXM13+FvZxDL4tbeil+im9nVEb2J0J7dFBSPAnIP6+6QXkGtMK2Tx8BDTItV66QzmT4wQ9Io6Is+X7xjjhWIThkoPkUVHxqcLQjId1fOs69sd1KuXRt7Fi7TTVb7+OcPh15Uwkx1U5gAH8BDkJ4qZNL3y0gd7hFBLs57NxUPMljmdgbADS2uv5M7i0icdW9AUYxkY3mJXhUlKeS40VFMBAONKnwiIxfAgO/MXsAD/U/vytJGBHNrXO/OSC/Kkc8rH9h57pzUD2gBRULRjTUW4hL9iOULd6HbjYTW9F/U+jqh1SLktILvXS70c+gQnUAEB6M8654Oi/bCoChGFc1/vfDed0n5e9geecJvXz9fIHgMQG561i9aYmhoYtaAPrwW0NV0WweUJBZr3sgD9Yxm0YBEekZvilyWm03vf1/1WiOU6HO15FBQGb8xKyUIEp9BtuWTHAEy3rRe7KfC2grX8XvxBiUK8RDw2kc3M3udKt6nv4AEjivx2qzCoyUcvDJ2GaotcyiWRbZSYfd67PdeNDtlQhyOtD3eGxqKDC8UfxdS/LiTpLVY6sTG7P3TmXm6RBzko0BY7o2y1grAQAAAgAAAAAAAAAAAAAAAD7hiyIUr/lwANl0z2R+fDR+j6WFAAAAAAABg0MBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmwGdAAAAAAAAAAAAAAAAwCqqObIj/o0KDlxPJ+rZCDx1bMIAAv3Kt+tcG5vAryvnKh9emBXjIeGPLemctNwQfsx3bVQvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJrHQ=="}
corresponding to a transfer of 0.1 WETH from Ethereum to Solana ($128.15):

The end-to-end workflow of a cross-chain message
Anyone can query a guardian node, and the returned VAA bytes can be submitted by anyone to the target chain to complete the message (e.g., mint a bitcoin to the recipient address encoded in the VAA).
Consider the message in Figure 1 “a token transfer of 0.1 WETH from Ethereum to Solana”. From the user’s perspective, there are actually five on-chain transactions involved (one on Ethereum and four on Solana) in the following order:
Tx1 (0x6539 wrapAndTransferETH
on Ethereum): the user 0x4f98 sends 0.1 WETH to the Wormhole Token Bridge 0x3ee1 by calling wrapAndTransferETH
with parameters specifying the recipient chain (Solana 0x01
), recipient (0xfdca
, base58 encode as 7vfC on Solana), arbiterFee (i.e., the relayer fee if used) and nonce:
Tx1 invokes Wormhole Core Bridge 0x98f3 internally and emits a message containing
sequence
,nonce
,payload
andconsistencyLevel
as below:
The guardians observe the message shown above and produce a VAA (
AQAA…
)Next, the VAA is retrieved from the guardian network and used to call Wormhole core bridge contract on Solana
Tx2 (5CVY VerifySignatures
on Solana): the VerifySignatures
function on the Solana Wormhole core bridge is invoked by a signer HZBb with the VAA to create a SignatureSet 58Ui:
This transaction also invokes the precompiled Secp256k1 SigVerify program to verify the guardian signatures in the VAA
The VAA contains 13 signatures in total. Due to the compute limit on Solana, Wormhole splits verifying these signatures into two transactions. Tx2 (5CVY) verifies seven signatures:
00 ff 01 02 03 04 05 ff ff ff ff ff 06 ff ff ff ff ff ff
Tx3 (46tG VerifySignatures
on Solana): update SignatureSet 58Ui by verifying the other six signatures:
ff ff ff ff ff ff ff ff ff ff ff ff ff 00 01 02 03 04 05
Tx4 (5AoD PostVAA
on Solana): after all signatures in the VAA are verified, the PostVAA
function can be invoked to create a message account 31Np, which uniquely identifies transferred message:
pub message: Mut<PostedVAA<’b, { AccountState::MaybeInitialized }>>,
Two other transactions 3goi and ZpYN also invoke
PostVAA
successfully, however, because the message account is a PDA, it is only initialized once by Tx4 (5AoD)The 2/3 quorum is checked in the
PostVAA
function:
signature_count > 2/3 guardian_set.keys.size
Tx5 (4p4q CompleteWrapped
on Solana ): Finally, the CompleteWrapped
function on the Wormhole Token Bridge (wormDTUJ) is invoked to complete the transfer.
The recipient
0xfdca
to receive the corresponding WETH token (ETH — Ether (Portal) 7vfC) is actually an associated token account (PDA) on Solana owned by4Kt8
. If the recipient does not exist yet then it has to be created because Tx5 can be executed. The account0xfdca
was created in Tx (sSjG on Solana).
How are the guardian signatures verified (to prevent fake VAAs)?
Verifying the guardian signatures is absolutely critical for the security of any cross-chain bridges and not surprisingly it is a complex task. We will elaborate in Part 2.
Who performs the downstream transactions on the target chain?
Anyone can can retrieve VAAs and perform the downstream transactions. It would be straightforward for the message sender to do so (e.g., the users who initiate the token transfer on the source chain). However, what if a user does not have any account on the target chain or a user has insufficient balances to pay for the transaction fees?
To address this issue, Wormhole allows bridge relayers to deliver the messages and earn fees. The fee can be specified in the source transaction, and is encoded into the VAA payload:
How to ensure the bridged token and amount are correct?
The transferred token on the target chain must be the same or equivalent to the token transferred on the source chain. For example, either both are USDC, or one is Wrapped Ether (WETH) on Ethereum and the other is ETH — Ether (Portal) on Solana. But it could not be WETH on Ethereum and USDC on Solana. We will discuss this part in Part 3.
How to prevent double-delivery of the same message (VAA replay)?
This is a subtle point and entails a careful design on the target chain. Essentially, a global state is required to flag each delivered message and reject transactions that attempt to re-deliver the same message. We will explain this part further in Part 4.
About Sec3 (formerly Soteria)
sec3 is a security research firm that prepares Solana projects for millions of users. sec3’s Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with Github to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.
To learn more about sec3, please visit: https://www.sec3.dev.