# How to integrate Wormhole in your smart contracts

Author: Markus Waas

Published: 2023-05-15T14:15:10.000Z

Updated: 2026-09-13T14:25:30.000Z

Source: [https://soliditydeveloper.com/wormhole](<https://soliditydeveloper.com/wormhole>)

## Compatibility and review

Before you start

Originally published in May 2023. Goerli, Mumbai, the CertusOne SDK and generic-relayer examples are historical. The Guardian/VAA explanation and defensive message checks were corrected against official sources, but the Solidity examples were not compiled, deployed or rerun. Use the current Wormhole SDK and Executor documentation for a new integration.

[Official reference](<https://wormhole.com/docs/products/messaging/guides/core-contracts/>)

![Wormhole Meme](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/f83b383ca7267b71/da1bb8ec1ef2/v/5e0cbdf79ca9/wormhole-meme.jpeg>)

**Wormhole** carries messages between blockchain ecosystems. Let’s look at the parts involved, then follow the Solidity message flow from the original May 2023 example.

**Historical walkthrough:** Goerli and Mumbai are retired, and the SDK and generic-relayer interfaces below belong to that earlier version. The examples have not been rerun in this review. For a new integration, start with the [current SDK](<https://wormhole.com/docs/tools/typescript-sdk/get-started/>) and [supported-network documentation](<https://wormhole.com/docs/reference/supported-networks/>).

## How does Wormhole ​work?

The messaging flow has four useful pieces:

1. **Core Contracts** publish messages on the source chain and verify VAAs on the receiving chain.
2. **Guardians** attest to observed messages.
3. **VAAs**, or Verified Action Approvals, carry those messages and signatures.
4. **Relayers** deliver VAAs to the destination application, which applies its own checks.

![Wormhole Architecture](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/9a56b83d9048f1c2/aae21187ac3a/v/faba30b60d22/Wormhole.png>)

### What role does Wormchain play?

Wormchain and the Guardian network are related, but they are not interchangeable names. [Wormhole Gateway](<https://wormhole.com/blog/wormhole-101-gateway>) uses a Cosmos SDK/CosmWasm chain to connect the Wormhole stack with the IBC ecosystem.

The EVM example here publishes through a Core Contract, obtains a Guardian-signed VAA and verifies it on another chain. It does not submit the message to Wormchain. You do not need a general claim that Proof of Stake is unsuitable for interoperability to understand that flow.

## The Guardian Network

The [Guardian network](<https://wormhole.com/docs/protocol/infrastructure/guardians/>) has 19 canonical Guardians. A standard VAA requires 13 signatures. Depending on the chain, observations are made directly by the full set or through a configured delegated subset; the final attestation still uses the 13-of-19 format.

That authenticates the message under Wormhole’s Guardian model. It does not decide whether your application should accept the source contract, recipient or requested action. Those checks belong in the receiving application.

![Guardians Wormhole](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/55451513230a7c5a/6bdc6255722d/v/7336800e4dce/guardians.jpeg>)

## The Relayers

Relayers deliver signed VAAs to the destination chain. A client can submit the destination transaction, or a service can automate delivery. The receiver must still validate the message.

The original article called the automated option “generic relayers”. Wormhole’s current documentation says the [Standard Relayer is being deprecated in favor of Executor](<https://wormhole.com/docs/protocol/infrastructure/relayers/relayer/>). The final section preserves the old interface for historical context.

## Historical example: Goerli -&gt; Sepolia

![Sending a message Meme](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/4316b43c347283f5/f81376b93d36/v/248268c23174/send-message.jpg>)

The original example sent a message from Goerli to Sepolia. Its general flow remains useful to study:

1. The sender publishes an encoded payload through the source Core Contract.
2. Guardians attest after the configured observation/finality requirements.
3. A client retrieves that message’s VAA and submits it to the receiver.
4. The receiver verifies the VAA, source identity, destination and replay state before storing the message.

The code below retains its 2023 imports and network addresses, with explicit application checks added. It is not a tested setup for today’s networks.

### 1. Sending the Message from Goerli

The original `GoerliMessageSender` imports a pinned `IWormhole` interface from the [Wormhole repository](<https://github.com/wormhole-foundation/wormhole>). Its `publishMessage` and `messageFee` functions are the parts we use here.

For a current integration, check the exact network in the [official Core Contract address table](<https://wormhole.com/docs/reference/contract-addresses/>). The address embedded in this historical Goerli example is not a current deployment recommendation.

```solidity
function publishMessage(
    uint32 nonce,
    bytes memory payload,
    uint8 consistencyLevel
) external payable returns (uint64 sequence);
```

The arguments are:

1. `nonce`: An application-chosen integer. It is not the sequence number, and reusing it does not combine several calls into one message.
2. `payload`: The bytes the application encodes.
3. `consistencyLevel`: The observation/finality requirement. The old example’s `200` is retained as historical code; do not copy it as a universal “instant” setting. Check the [current finality guidance](<https://wormhole.com/docs/reference/consistency-levels/>) for the chosen chain and application.

`publishMessage` returns the emitted message’s sequence. Keep that value for VAA retrieval.

```solidity
function messageFee() external view returns (uint256);
```

`messageFee()` returns the Core Contract’s publishing fee in the source chain’s native currency. Query it instead of assuming testnet fees are zero. The sender below requires exactly that amount; destination delivery has separate transaction costs.

`getMessageForAddress` encodes our destination chain, receiving contract and message together.

```solidity
// 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 {
    // Historical 2023 network/imports; this revision has not been compiled or deployed.
    address public immutable owner = msg.sender;
    struct MyMessage {
        uint16 targetChain; // Wormhole chain ID, not EVM chain ID
        address recipient;
        string message;
    }

    address private whAddr = 0x706abc4E45D419950511e474C7B9Ed348A4a716c;
    IWormhole public immutable wormhole = IWormhole(whAddr);

    uint256 public lastMessageSequence;

    function getMessageForAddress(uint16 targetChain, address recipient, string calldata message) external pure returns (bytes memory) {
        return abi.encode(MyMessage(targetChain, recipient, message));
    }

    function sendMessage(
        bytes memory fullMessage
    ) public payable {
        require(msg.sender == owner, "Only owner");
        uint256 fee = wormhole.messageFee();
        require(msg.value == fee, "Incorrect message fee");
        lastMessageSequence = wormhole.publishMessage{
            value: fee
        }(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.

```solidity
// 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 {
    // Historical 2023 interface; this revision has not been compiled or deployed.
    address public immutable owner = msg.sender;
    address private whAddr = 0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78;
    IWormhole public immutable wormhole = IWormhole(whAddr);

    struct MyMessage {
        uint16 targetChain; // Wormhole chain ID, not EVM chain ID
        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, "Only owner");
        require(chainId != 0 && emitterAddress != bytes32(0), "Invalid emitter configuration");
        registeredContracts[chainId] = emitterAddress;
    }

    function receiveWormholeMessages(
        bytes[] memory signedVaas,
        bytes[] memory
    ) public payable override {
        require(signedVaas.length == 1, "Expected one VAA");
        (IWormhole.VM memory parsed, bool valid, string memory reason)
            = wormhole.parseAndVerifyVM(signedVaas[0]);

        require(valid, reason);
        bytes32 expectedEmitter = registeredContracts[parsed.emitterChainId];
        require(expectedEmitter != bytes32(0), "Source chain not registered");
        require(
            expectedEmitter == parsed.emitterAddress,
            "Emitter address not valid"
        );

        require(!hasProcessedMessage[parsed.hash], "Message already processed");

        MyMessage memory message = abi.decode(parsed.payload, (MyMessage));
        require(message.targetChain == wormhole.chainId(), "Wrong destination chain");
        require(message.recipient == address(this), "Wrong recipient");

        hasProcessedMessage[parsed.hash] = true;
        messageHistory.push(message.message);
    }

    function getFullMessageHistory() public view returns (string[] memory) {
        return messageHistory;
    }
}
```

### 2. Receiving the Message on Sepolia

The receiver uses the article’s pinned 2023 `IWormholeReceiver` interface. Here we submit the VAA ourselves, so this function is simply our application’s entry point.

Do not treat its `receiveWormholeMessages(bytes[],bytes[])` signature as the current standard for every relaying integration. The [Executor migration guide](<https://wormhole.com/docs/protocol/infrastructure/relayers/executor-vs-sr/>) explains the current division of verification and delivery responsibilities.

This receiver expects exactly one VAA and checks the array length before accessing `signedVaas[0]`. `parseAndVerifyVM` returns the parsed envelope, validity flag and reason. A valid Guardian attestation is only the first check.

```solidity
struct VM {
    uint8 version;
    uint32 timestamp;
    uint32 nonce;
    uint16 emitterChainId;
    bytes32 emitterAddress;
    uint64 sequence;
    uint8 consistencyLevel;
    bytes payload;
    uint32 guardianSetIndex;
    Signature[] signatures;
    bytes32 hash;
}
```

- Check the signed **source chain and emitter address** against an explicitly configured whitelist. Only the receiver’s administrator may change it.
- Decode the payload using the same schema as the sender. The revised struct includes the **destination Wormhole chain ID and recipient**; check both.
- Record the VAA hash before processing so the same accepted message cannot be consumed twice by this receiver.
- Review who can publish through the trusted source contract. The revised sender permits only its deploying administrator. Authenticating a source contract would not, by itself, authorize every caller of a public sender.

This is a small message-history demonstration, not a bridge or governance implementation. Its single immutable administrator and payload schema are deliberate teaching choices.

Once all checks are passed, you can store the hash as replay protection and process the message.

### 3. The historical Remix walkthrough

![Let's go](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/13b66b0ffd121219/a707098d2f8d/lest-go.gif>)

The screenshots below show the original [Remix](<https://remix.ethereum.org/>) walkthrough. They help explain the two-chain workflow, but Goerli is retired and the revised payload now has a destination-chain field. A fresh example needs current networks, matching interfaces and tests before deployment.

#### A. Let's deploy the sender and receiver contract

The original walkthrough deployed the sender on Goerli and the receiver on Sepolia, then attached Remix to the source Core Contract with “At Address”. The screenshot shows those three entries.

Do not deploy against these historical Goerli instructions today. Select current networks and their [official Core Contract addresses](<https://wormhole.com/docs/reference/contract-addresses/>) when preparing a tested replacement.

![Remix Wormhole Contracts](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/2f53cadc21582964/5a4501226209/v/576d96b5829d/Remix-Wormhole-Contracts.png>)

#### B. Let's register the sender with the receiver

`emitterAddress()` converts the sender’s address to Wormhole’s 32-byte representation. The receiver’s administrator registers that value for the expected source chain.

The old `registerEmitter(2, emitterAddress)` call used Wormhole’s historical Ethereum/Goerli identifier. [Wormhole chain IDs differ from EVM chain IDs](<https://wormhole.com/docs/reference/supported-networks/#wormhole-chain-ids>), and replacement testnets have distinct entries in the [official registry](<https://github.com/wormhole-foundation/wormhole/blob/main/sdk/vaa/structs.go>). Use the source network’s actual Wormhole ID, not your wallet’s network number.

#### C. Let's send the message

For the revised payload, `getMessageForAddress(targetWormholeChainId, receiverAddress, "Hello World!")` encodes the target chain, recipient and message. Obtain the destination’s Wormhole ID from its Core Contract or the official registry.

The sender’s administrator calls `sendMessage` with those bytes and exactly the current `messageFee()`. The old two-argument helper and screenshots predate the target-chain field. This review did not publish a message.

#### D. Retrieve the VAA

Retrieve the VAA for the **actual emitted sequence**, together with its source Wormhole chain ID and emitter address. Read the sequence from that transaction’s `LogMessagePublished` event; the demo also stores its latest sequence in `lastMessageSequence`. Do not substitute the nonce or assume it is `1`.

The old CertusOne URL is historical. Use the current [SDK](<https://wormhole.com/docs/tools/typescript-sdk/get-started/>) or API referenced in the [VAA documentation](<https://wormhole.com/docs/protocol/infrastructure/vaas/>). If observation is pending, retry retrieval for the same message. Publishing again creates another message.

The JSON below is the original response-format illustration, not a VAA for your contracts or the revised payload.

```json
{"vaaBytes":"AQAAAAABAK/jB/sgQgOhZXnHlytNy/piP9dWizgbPP9rTpXgS/SOHFq+iW5PzP4j6IyB1UWx/Hos6JW3Bje0jrJgqPljuPsBZFZrWAAAAAEAAgAAAAAAAAAAAAAAACyICfoR/iqwChsK5RTo2NO0QUOGAAAAAAAAAAHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhvbGE="}
```

A [VAA decoder](<https://wormhole.com/docs/tools/typescript-sdk/guides/vaas-protocols/>) can help inspect the envelope and payload. Decoding fields is not the same as verifying Guardian signatures or passing the receiver’s application checks.

#### E. Let's submit the VAA

Remix expects a Solidity `bytes` value as **`0x`-prefixed hexadecimal**, not a string of binary digits. Decode the base64 response into bytes, then format those bytes as hex.

For example, with Node.js: `const vaaHex = "0x" + Buffer.from(vaaBytes, "base64").toString("hex");` See the [Buffer encoding documentation](<https://nodejs.org/api/buffer.html#buffers-and-character-encodings>). The old converter screenshot shows the wrong output format for this input.

![Base64 Binary Conversion](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/7acb2dee12bc6d33/35f2c80328fe/v/514ad4802a1f/Base64-Binary-Conversion.png>)

The historical receiver call has the shape `receiveWormholeMessages(["0x..."], [])`, using the complete VAA bytes for your own message.

Acceptance depends on every signature, source, destination and replay check. The original JSON sample will not encode the revised payload for your contracts. After an accepted message, `getFullMessageHistory()` returns the stored text; no successful submission was rerun during this review.

### How this could be done in a Dapp

For a new Dapp, use the [current Wormhole TypeScript SDK](<https://wormhole.com/docs/tools/typescript-sdk/get-started/>), documented as `@wormhole-foundation/sdk`. The original `@certusone/wormhole-sdk` reference belongs to the older API.

Manual delivery asks the user to send the destination transaction as well. A backend or an automated delivery service can take that step instead, with its own fees and availability considerations. See the [current relaying overview](<https://wormhole.com/docs/protocol/infrastructure/relayers/relayer/>).

![Generic Relayer Meme](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/62783841caec07a4/126077bd7e08/v/d4a7be5a72a9/relayer-brain-meme.jpeg>)

## Historical generic-relayer example

The snippets below preserve the article’s 2023 `MockRelayerIntegration` example and `IWormholeRelayer.Send` API. They are historical reference code.

Wormhole is deprecating the Standard Relayer in favor of Executor. That migration changes the quoting and execution-request interfaces; these snippets are not a current implementation guide. Use the [official migration guide](<https://wormhole.com/docs/protocol/infrastructure/relayers/executor-vs-sr/>) when designing a replacement.

```solidity
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());
```

The original mock contracts below show Mumbai and BSC testnet addresses. [Mumbai was retired](<https://forum.polygon.technology/t/pos-tooling-after-mumbai-deprecation-no-action-required/13740>); these old mock deployments and relayer addresses have not been revalidated.

For a replacement integration, select supported networks and the current Core/Executor contracts. Do not mix the old `IWormholeRelayer.Send` request with a new Executor address.

```solidity
// 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

The useful idea is straightforward: publish a payload on one chain, carry its Guardian-signed VAA to another, and let the destination application decide whether to accept it.

The checks matter as much as the delivery. Know who can send, which source you trust, where the message is intended to go and whether it has already been processed.

The Goerli/Sepolia and Mumbai/BSC walkthroughs above belong to the original 2023 article. For a new Dapp, use current network and SDK documentation and test the complete flow. The protocol has moved on; the old snippets need more than a new RPC URL. ;)
