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