How to integrate Wormhole in your smart contracts
Entering a New Era of Blockchain Interoperability

Wormhole began as a token bridge between Ethereum and Solana and has since expanded into a decentralized interoperability protocol for multiple blockchain ecosystems. Wormhole now supports many chains like Ethereum, Cosmos, Polkadot, Injective and many more.
It makes cross-chain communication easy and efficient. Let's explore how it works and then let's send some cross-chain messages between EVM chains in Solidity!
How does Wormhole work?
The fundamental architecture is straight-forward. It consists of:
- Wormchain: A Proof of Authority chain controlled by the Guardians.
- VAAs: The Verified Action Approvals are the signed messages from the Guardians.
- Relayers: The entities actually submitting the signed entities to various chains, you can use your own here as well.
- Blockchains: Any blockchain connected to Wormhole.

Why not Proof of Stake (PoS) for the Wormchain?
While you might think for decentralization concerns, PoS would be a better candidate for the Wormchain, it comes with issues for such a protocol.
Network security: PoS systems are designed primarily for achieving consensus within a single blockchain environment. Interoperability protocols like Wormhole are responsible for verifying transactions and information across multiple blockchains. The security assumptions and requirements may be different in a multi-chain environment and PoS may not be best suited to address these complexities.
Unclear security properties: When a PoS system is applied to a decentralized oracle network, it is not immediately clear how the network's security would be affected. As PoS relies on validators who stake their tokens to participate in the consensus process, there may be concerns about the overall security if a majority of validators collude or if the amount staked is insufficient to provide adequate economic security. This contrasts with the security guarantees provided by the underlying blockchains, which may have different consensus mechanisms and security assumptions.
Challenges in achieving outlined goals: A PoS system may introduce complexities as it would require the integration of a native token and a staking mechanism that needs to be compatible with multiple blockchains. This added complexity may make it more difficult to achieve the desired level of decentralization and seamless interoperability.
The Guardian Network
All these things considered, a Proof of Authority (PoA) system was chosen for the Wormchain. It's a purpose-built Cosmos blockchain run by 19 validators, called guardians. 2/3 of the signatures are needed for consensus in the guardian network, meaning 13 signatures need to be verified on-chain, which remains reasonable from a gas-cost and security perspective.
This group of validators is responsible for creating the Verifiable Action Assertions (VAAs). Guardians observe on-chain events, process them, and create a VAA that gets signed by a threshold of Guardians. The VAA then serves as a cryptographically verifiable proof that can be relayed to other chains and executed accordingly.
Rather than securing the network with tokenomics, it was deemed better to initially secure the network by 'involving robust companies which are heavily invested in the success of De-Fi as a whole'. The 19 Guardians are not anonymous or small--they are many of the largest and most widely-known validator companies in cryptocurrency. The current list of Guardians can be viewed here and includes big names like 01 Node, Chorus One, ChainLayer, Figment and Jump.

The Relayers
Relayers are vital components in Wormhole's cross-chain processes, enabling the delivery of VAAs to their destination chains. Unlike other interoperability protocols, Wormhole does not rely on a specific relaying methodology, giving developers the freedom to choose the most suitable strategy for their needs. There are three common relaying strategies: client-side relaying, specialized relayers, and generic relayers. Each strategy has its own set of advantages and drawbacks, catering to different application requirements and user experiences.
Let's send a message from Goerli -> Sepolia

Now let's try and use Wormhole. We can now even send messages from one Ethereum testnet to another Ethereum testnet.
The general flow will be:
- Emit message on Goerli using already existing Wormhole contracts.
- Guardians are monitoring emitted messages. Once they detect a new one, they will create and sign a VAA.
- We can retrieve the VAA and submit to our receiving Sepolia contract. (this step can be abstracted away via generic relayers, we'll use those afterwards)
- The Sepolia contract verifies the signature, chain id and emitter address. If all is correct, the message can be stored/processed.
Note that in all examples I'm using the current testnet versions. As Wormhole is in active development, changes to the interfaces may happen.
1. Sending the Message from Goerli
Let's start by sending the message on the Goerli testnet. We'll deploy the GoerliMessageSender
contract first. We can import the Wormhole interface from the Wormhole Repository: https://github.com/wormhole-foundation/wormhole. All deployed contracts can be found here.
The interface has a publishMessage
and a messageFee
function we'll need.
function publishMessage(
uint32 nonce,
bytes memory payload,
uint8 consistencyLevel
) external payable returns (uint64 sequence);
It takes in the following arguments:
nonce
: A number assigned to each message. You can use the same nonce, if you want to batch submit multiple messages in one. We'll only submit a single message, so we can just pass a nonce of 1.Consistency
: The level of finality the guardians will reach before signing the message. For our test case purposes, 200 is fine which means instant. More information about finality can be found here.Payload
: The actual message to send as raw bytes.
The return value will be a sequence identifier number that we can use to retrieve the signed VAA.
function messageFee() external view returns (uint256);
The messageFee
function gives us the cost for sending such a message. On testnet this is currently zero, so you won't need to pass any ETH, but if your message publishing fails for unknown reasons, it may be because you forgot to send ETH along.
As for the getMessageForAddress
, we'll get to that in a moment.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/wormhole-foundation/wormhole/blob/1c0a1d7b63fc61dc751537c6c2c4d153725d1dc0/ethereum/relayers/contracts/interfaces/IWormhole.sol";
contract GoerliMessageSender {
struct MyMessage {
address recipient;
string message;
}
address private whAddr = 0x706abc4E45D419950511e474C7B9Ed348A4a716c;
IWormhole public immutable wormhole = IWormhole(whAddr);
uint256 public lastMessageSequence;
function getMessageForAddress(address recipient, string calldata message) external pure returns (bytes memory) {
return abi.encode(MyMessage(recipient, message));
}
function sendMessage(
bytes memory fullMessage
) public payable {
lastMessageSequence = wormhole.publishMessage{
value: wormhole.messageFee()
}(1, fullMessage, 200);
}
function emitterAddress() public view returns (bytes32) {
return bytes32(uint256(uint160(address(this))));
}
}
And we'll need the emitterAddress
view function. This is just for our own convenience and converts our contract address into bytes32. We'll need that to retrieve the VAA as well as double checking the emitter address on the receiver side.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/wormhole-foundation/wormhole/blob/1c0a1d7b63fc61dc751537c6c2c4d153725d1dc0/ethereum/relayers/contracts/interfaces/IWormhole.sol";
import "https://github.com/wormhole-foundation/wormhole/blob/1c0a1d7b63fc61dc751537c6c2c4d153725d1dc0/ethereum/relayers/contracts/interfaces/IWormholeReceiver.sol";
contract SepoliaMessageReceiver is IWormholeReceiver {
address private whAddr = 0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78;
IWormhole public immutable wormhole = IWormhole(whAddr);
struct MyMessage {
address recipient;
string message;
}
mapping(uint16 => bytes32) public registeredContracts;
mapping(bytes32 => bool) public hasProcessedMessage;
string[] public messageHistory;
function registerEmitter(uint16 chainId, bytes32 emitterAddress) public {
// require(msg.sender == owner);
registeredContracts[chainId] = emitterAddress;
}
function receiveWormholeMessages(
bytes[] memory signedVaas,
bytes[] memory
) public payable override {
(IWormhole.VM memory parsed, bool valid, string memory reason)
= wormhole.parseAndVerifyVM(signedVaas[0]);
require(valid, reason);
require(
registeredContracts[parsed.emitterChainId] == parsed.emitterAddress,
"Emitter address not valid"
);
require(!hasProcessedMessage[parsed.hash]);
MyMessage memory message = abi.decode(parsed.payload, (MyMessage));
require(message.recipient == address(this));
hasProcessedMessage[parsed.hash] = true;
messageHistory.push(message.message);
}
function getFullMessageHistory() public view returns (string[] memory) {
return messageHistory;
}
}
2. Receiving the Message on Sepolia
Now we can create a contract to receive the message. We'll implement the IWormholeReceiver
interface for that.
Technically we could define our own interface, since we are just submitting the signed VAA ourselves, but it's always good to follow some standards. And the interface when using generic relayers is the receiveWormholeMessages
function that takes in the signed VAAs as array. So let's use that!
In our case we only send a single message, so we can just take the first element of the signedVAAs. Using wormhole.parseAndVerifyVM
we will receive the results.
struct VM {
uint8 version;
uint32 timestamp;
uint32 nonce;
uint16 emitterChainId;
bytes32 emitterAddress;
uint64 sequence;
uint8 consistencyLevel;
bytes payload;
uint32 guardianSetIndex;
Signature[] signatures;
bytes32 hash;
}
- The payload will be the actual message.
- The hash is an identifier that can be used to avoid replaying the message more than once, see our
hasProcessedMessage
mapping.
- The emitter chain id and address can be used to make sure the message comes from the contract we care about. You will have to register the expected emitter addresses and chain ids first, see our
registeredContracts
mapping. - It's also best practice to encode the supposed recipient inside the payload message. You can do that for example using a struct and using
abi.decode
to get the struct back in Solidity. And then ensure that the recipient is actually our current contract.
Once all checks are passed, you can store the hash as replay protection and process the message.
3. Let's try on Remix!

We can just use Remix to test it. So go to https://remix.ethereum.org/ and add both our contracts.
A. Let's deploy the sender and receiver contract
Let's the deploy the contracts:
- First deploy
GoerliMessageSender
to, you guessed it, the Goerli testnet. - Take the wormhole address for Goerli, select
IWormhole
from the contracts to deploy in Remix and choose 'At Address'. - Then switch your wallet to the Sepolia testnet and deploy the
SepoliaMessageReceiver
.
You should now have three contracts in your list as shown on the right. Of course you can use other testnets, but make sure to change the addresses according to https://book.wormhole.com/reference/contracts.html.

B. Let's register the sender with the receiver
Now let's register the sender with the receiver!
- Switch back to Goerli and retrieve the emitter address by calling
emitterAddress()
. We'll need this helper to convert the address into bytes32 which is the expected emitter address type. - With that result, switch back to Sepolia and call
registerEmitter(2, emitterAddress)
where 2 stands for the chain id of Goerli.
So that means now this specific contract on the Goerli testnet is registered to send a message to us.
C. Let's send the message
Now if we want to send a message, make sure you switch back to Goerli testnet.
- Copy the
SepoliaMessageReceiver
address and callgetMessageForAddress(sepoliaMessageReceiverAddress, "Hello World!")
which will encode the message for us in a way that let's us verify the intended recipient on the Sepolia side. - Now call messageFee on the IWormhole contract.
- Lastly emit the message by calling
sendMessage
with the result fromgetMessageForAddress
. Also make sure to pass the fee inside the 'VALUE' field. Most likely, especially on the testnets, this will actually be zero and you don't have to worry about it. But for mainnet this might be different.
D. Let's send retrieve the VAA
Great, so now the message was emitted. Unless you use generic relayers (we'll look at those next), you have to relay the message yourself. So let's first retrieve it by going to https://wormhole-v2-testnet-api.certus.one/v1/signed_vaa/2/<emitterAddress>/1
. You can find the details for this over at the proto file. This file shows you the publicly available RPC messages for the Wormchain from which we have to retrieve the VAA. For mainnet the available RPCs can be found here.
Wait a few moments for the VAA to appear. Sometimes testnet, unlike mainnet, is also not working perfectly, so you might have to emit the message multiple times for it be picked up. The result should look something like:
{"vaaBytes":"AQAAAAABAK/jB/sgQgOhZXnHlytNy/piP9dWizgbPP9rTpXgS/SOHFq+iW5PzP4j6IyB1UWx/Hos6JW3Bje0jrJgqPljuPsBZFZrWAAAAAEAAgAAAAAAAAAAAAAAACyICfoR/iqwChsK5RTo2NO0QUOGAAAAAAAAAAHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhvbGE="}
You can verify the VAA on https://vaa.dev/#/parse.
E. Let's submit the VAA
Now to submit the VAA, we have to convert these base64-encoded bytes into raw bytes. You can go to https://cryptii.com/pipes/base64-to-binary, enter the vaaBytes
. And on the right choose Bytes with Format Binary and Group By None.

Copy the binary result and call receiveWormholeMessages([copiedBinary],[])
. This should succeed.
If you now call getFullMessageHistory()
you should see our "Hello World!" message.
How this could be done in a Dapp
To use these cross-chain messages in a Dapp, you can automate the process using the wormhole-sdk. It comes with one obvious downside of course, a user has to manually relay the message, meaning signing two transactions in total with some delay. Not a great UX.
Alternatively you relay the message yourself with some backend server. Or you'll use the generic relayers. Let's try that now!

Sending a Message with Generic Relayers
Take a look at the MockRelayerIntegration, it shows how to use these generic relayers.
You once again submit the message like before, but on top you also request relaying the message. In its current version at the time of writing the interface is:
IWormholeRelayer.Send memory request = IWormholeRelayer.Send({
targetChain: targetChainId,
targetAddress: relayer.toWormholeFormat(address(destination)),
refundAddress: relayer.toWormholeFormat(address(refundAddress)), // This will be ignored on the target chain if the intent is to perform a forward
maxTransactionFee: msg.value - 3 * wormhole.messageFee() - receiverValue,
receiverValue: receiverValue,
relayParameters: relayer.getDefaultRelayParams()
});
relayer.send{value: msg.value - 2 * wormhole.messageFee()}(request, nonce, relayer.getDefaultRelayProvider());
You'll need to use the relayer contracts from https://book.wormhole.com/reference/contracts.html#relayer-contracts. At this time they are only available for a few networks, e.g. to send a message from Mumbai testnet (chain id 5) to BSC testnet (chain id 4).
You can test the generic relaying by using the mock relayer:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/wormhole-foundation/trustless-generic-relayer/blob/9e282508f796a76a8aef03ba1911b68e03c8f627/ethereum/contracts/mock/MockRelayerIntegration.sol";
contract MyWormholeBSC is MockRelayerIntegration {
address private wormholeCore = 0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D;
address private coreRelayer = 0xda2592C43f2e10cBBA101464326fb132eFD8cB09;
constructor() MockRelayerIntegration(wormholeCore, coreRelayer) {}
}
contract MyWormholeMumbai is MockRelayerIntegration {
address private wormholeCore = 0x0CBE91CF822c73C2315FB05100C2F714765d5c20;
address private coreRelayer = 0xFAd28FcD3B05B73bBf52A3c4d8b638dFf1c5605c;
constructor() MockRelayerIntegration(wormholeCore, coreRelayer) {}
}
Conclusion
Wormhole emerges as an innovative answer to the longstanding issue of blockchain interoperability, setting the stage for unprecedented possibilities within the decentralized applications sphere. Leveraging a cohesive blend of Guardians, Relayers, and xAssets, Wormhole enables smooth cross-chain communication and interaction.
Our recent exploration of this ecosystem provided us with practical insights into the process of sending messages via self-relaying, as exemplified by the successful transmission from the Goerli to Sepolia testnet. Moreover, we delved into the effective use of generic relayers to transport messages from the Mumbai to the BSC testnet. As the Wormhole ecosystem continues to mature and expand, it holds the promise of a more interconnected and versatile blockchain environment for developers and users alike.
Solidity Developer
More great blog posts from Markus Waas
How to use ChatGPT with Solidity
Using the Solidity Scholar and other GPT tips
How to integrate Uniswap 4 and create custom hooks
Let's dive into Uniswap v4's new features and integration
Solidity Deep Dive: New Opcode 'Prevrandao'
All you need to know about the latest opcode addition
How Ethereum scales with Arbitrum Nitro and how to use it
A blockchain on a blockchain deep dive
The Ultimate Merkle Tree Guide in Solidity
Everything you need to know about Merkle trees and their future
The New Decentralized The Graph Network
What are the new features and how to use it
zkSync Guide - The future of Ethereum scaling
How the zero-knowledge tech works and how to use it
Exploring the Openzeppelin CrossChain Functionality
What is the new CrossChain support and how can you use it.
Deploying Solidity Contracts in Hedera
What is Hedera and how can you use it.
Writing ERC-20 Tests in Solidity with Foundry
Blazing fast tests, no more BigNumber.js, only Solidity
ERC-4626: Extending ERC-20 for Interest Management
How the newly finalized standard works and can help you with Defi
Advancing the NFT standard: ERC721-Permit
And how to avoid the two step approve + transferFrom with ERC721-Permit (EIP-4494)
Moonbeam: The EVM of Polkadot
Deploying and onboarding users to Moonbeam or Moonriver
Advanced MultiSwap: How to better arbitrage with Solidity
Making multiple swaps across different decentralized exchanges in a single transaction
Deploying Solidity Smart Contracts to Solana
What is Solana and how can you deploy Solidity smart contracts to it?
Smock 2: The powerful mocking tool for Hardhat
Features of smock v2 and how to use them with examples
How to deploy on Evmos: The first EVM chain on Cosmos
Deploying and onboarding users to Evmos
EIP-2535: A standard for organizing and upgrading a modular smart contract system.
Multi-Facet Proxies for full control over your upgrades
MultiSwap: How to arbitrage with Solidity
Making multiple swaps across different decentralized exchanges in a single transaction
The latest tech for scaling your contracts: Optimism
How the blockchain on a blockchain works and how to use it
Ultimate Performance: The Aurora Layer2 Network
Deploying and onboarding users to the Aurora Network powered by NEAR Protocol
What is ecrecover in Solidity?
A dive into the waters of signatures for smart contracts
How to use Binance Smart Chain in your Dapp
Deploying and onboarding users to the Binance Smart Chain (BSC)
Using the new Uniswap v3 in your contracts
What's new in Uniswap v3 and how to integrate Uniswap v3
What's coming in the London Hardfork?
Looking at all the details of the upcoming fork
Welcome to the Matrix of blockchain
How to get alerted *before* getting hacked and prevent it
The Ultimate Ethereum Mainnet Deployment Guide
All you need to know to deploy to the Ethereum mainnet
SushiSwap Explained!
Looking at the implementation details of SushiSwap
Solidity Fast Track 2: Continue Learning Solidity Fast
Continuing to learn Solidity fast with the advanced basics
What's coming in the Berlin Hardfork?
Looking at all the details of the upcoming fork
Using 1inch ChiGas tokens to reduce transaction costs
What are gas tokens and example usage for Uniswap v2
Openzeppelin Contracts v4 in Review
Taking a look at the new Openzeppelin v4 Release
EIP-3156: Creating a standard for Flash Loans
A new standard for flash loans unifying the interface + wrappers for existing ecosystems
Tornado.cash: A story of anonymity and zk-SNARKs
What is Tornado.cash, how to use it and the future
High Stakes Roulette on Ethereum
Learn by Example: Building a secure High Stakes Roulette
How to implement generalized meta transactions
We'll explore a powerful design for meta transactions based on 0x
Utilizing Bitmaps to dramatically save on Gas
A simple pattern which can save you a lot of money
Using the new Uniswap v2 as oracle in your contracts
How does the Uniswap v2 oracle function and how to integrate with it
Smock: The powerful mocking tool for Hardhat
Features of smock and how to use them with examples
How to build and use ERC-721 tokens in 2021
An intro for devs to the uniquely identifying token standard and its future
Trustless token management with Set Protocol
How to integrate token sets in your contracts
Exploring the new Solidity 0.8 Release
And how to upgrade your contracts to Solidity 0.8
How to build and use ERC-1155 tokens
An intro to the new standard for having many tokens in one
Leveraging the power of Bitcoins with RSK
Learn how RSK works and how to deploy your smart contracts to it
Solidity Fast Track: Learn Solidity Fast
'Learn X in Y minutes' this time with X = Solidity 0.7 and Y = 20
Sourcify: The future of a Decentralized Etherscan
Learn how to use the new Sourcify infrastructure today
Integrating the 0x API into your contracts
How to automatically get the best prices via 0x
How to build and use ERC-777 tokens
An intro to the new upgraded standard for ERC-20 tokens
COMP Governance Explained
How Compound's Decentralized Governance is working under the hood
How to prevent stuck tokens in contracts
And other use cases for the popular EIP-165
Understanding the World of Automated Smart Contract Analyzers
What are the best tools today and how can you use them?
A Long Way To Go: On Gasless Tokens and ERC20-Permit
And how to avoid the two step approve + transferFrom with ERC20-Permit (EIP-2612)!
Smart Contract Testing with Waffle 3
What are the features of Waffle and how to use them.
How to use xDai in your Dapp
Deploying and onboarding users to xDai to avoid the high gas costs
Stack Too Deep
Three words of horror
Integrating the new Chainlink contracts
How to use the new price feeder oracles
TheGraph: Fixing the Web3 data querying
Why we need TheGraph and how to use it
Adding Typescript to Truffle and Buidler
How to use TypeChain to utilize the powers of Typescript in your project
Integrating Balancer in your contracts
What is Balancer and how to use it
Navigating the pitfalls of securely interacting with ERC20 tokens
Figuring out how to securely interact might be harder than you think
Why you should automatically generate interests from user funds
How to integrate Aave and similar systems in your contracts
How to use Polygon (Matic) in your Dapp
Deploying and onboarding users to Polygon to avoid the high gas costs
Migrating from Truffle to Buidler
And why you should probably keep both.
Contract factories and clones
How to deploy contracts within contracts as easily and gas-efficient as possible
How to use IPFS in your Dapp?
Using the interplanetary file system in your frontend and contracts
Downsizing contracts to fight the contract size limit
What can you do to prevent your contracts from getting too large?
Using EXTCODEHASH to secure your systems
How to safely integrate anyone's smart contract
Using the new Uniswap v2 in your contracts
What's new in Uniswap v2 and how to integrate Uniswap v2
Solidity and Truffle Continuous Integration Setup
How to setup Travis or Circle CI for Truffle testing along with useful plugins.
Upcoming Devcon 2021 and other events
The Ethereum Foundation just announced the next Devcon in 2021 in Colombia
The Year of the 20: Creating an ERC20 in 2020
How to use the latest and best tools to create an ERC-20 token contract
How to get a Solidity developer job?
There are many ways to get a Solidity job and it might be easier than you think!
Design Pattern Solidity: Mock contracts for testing
Why you should make fun of your contracts
Kickstart your Dapp frontend development with create-eth-app
An overview on how to use the app and its features
The big picture of Solidity and Blockchain development in 2020
Overview of the most important technologies, services and tools that you need to know
Design Pattern Solidity: Free up unused storage
Why you should clean up after yourself
How to setup Solidity Developer Environment on Windows
What you need to know about developing on Windows
Avoiding out of gas for Truffle tests
How you do not have to worry about gas in tests anymore
Design Pattern Solidity: Stages
How you can design stages in your contract
Web3 1.2.5: Revert reason strings
How to use the new feature
Gaining back control of the internet
How Ocelot is decentralizing cloud computing
Devcon 5 - Review
Impressions from the conference
Devcon 5 - Information, Events, Links, Telegram
What you need to know
Design Pattern Solidity: Off-chain beats on-chain
Why you should do as much as possible off-chain
Design Pattern Solidity: Initialize Contract after Deployment
How to use the Initializable pattern
Consensys Blockchain Jobs Report
What the current blockchain job market looks like
Provable — Randomness Oracle
How the Oraclize random number generator works
Solidity Design Patterns: Multiply before Dividing
Why the correct order matters!
Devcon 5 Applications closing in one week
Devcon 5 Applications closing
Randomness and the Blockchain
How to achieve secure randomness for Solidity smart contracts?