Skip to content

Get Started with Core Contracts

Introduction

Wormhole's Core Contracts, deployed on each supported blockchain network, enable the fundamental operations of sending and receiving cross-chain messages.

While the implementation details of the Core Contracts varies by network, the core functionality remains consistent across chains. Each version of the Core Contract facilitates secure and reliable cross-chain communication, ensuring that developers can effectively publish and verify messages.

This guide will walk you through the variations and key methods of the Core Contracts, providing you with the knowledge needed to integrate them into your cross-chain contracts. To learn more about Core Contracts' features and how it works, please refer to the Core Contracts page in the Learn section.

Prerequisites

To interact with the Wormhole Core Contract, you'll need the following:

How to Interact with Core Contracts

Before writing your own contracts, it's essential to understand the key functions and events of the Wormhole Core Contracts. The primary functionality revolves around:

  • Sending messages - submitting messages to the Wormhole network for cross-chain communication
  • Receiving and verifying messages - validating messages received from other chains via the Wormhole network

While the implementation details of the Core Contracts vary by network, the core functionality remains consistent across chains.

Sending Messages

To send a message, regardless of the environment or chain, the Core Contract is invoked with a message argument from an emitter. This emitter might be your contract or an existing application such as the Token Bridge or NFT Bridge.

The IWormhole.sol interface provides the publishMessage function, which can be used to publish a message directly to the Core Contract:

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

nonce uint32

A free integer field that can be used however you like. Note that changing the nonce will result in a different digest.


payload bytes memory

The content of the emitted message. Due to the constraints of individual blockchains, it may be capped to a certain maximum length.


consistencyLevel uint8

A value that defines the required level of finality that must be reached before the Guardians will observe and attest to emitted events.

Returns

sequence uint64

A unique number that increments for every message for a given emitter (and implicitly chain). This, combined with the emitter address and emitter chain ID, allows the VAA for this message to be queried from the Wormholescan API.

Example
IWormhole wormhole = IWormhole(wormholeAddr);

// Get the fee for publishing a message
uint256 wormholeFee = wormhole.messageFee();

// Check fee and send parameters

// Create the HelloWorldMessage struct
HelloWorldMessage memory parsedMessage = HelloWorldMessage({
    payloadID: uint8(1),
    message: helloWorldMessage
});

// Encode the HelloWorldMessage struct into bytes
bytes memory encodedMessage = encodeMessage(parsedMessage);

// Send the HelloWorld message by calling publishMessage on the
// wormhole core contract and paying the Wormhole protocol fee.
messageSequence = wormhole.publishMessage{value: wormholeFee}(
    0, // batchID
    encodedMessage,
    wormholeFinality()
);

View the complete Hello World example in the Wormhole Scaffolding repository on GitHub.

The wormhole_anchor_sdk::wormhole module and the Wormhole program account can be used to pass a message directly to the Core Contract via the wormhole::post_message function:

pub fn post_message<'info>(
    ctx: CpiContext<'_, '_, '_, 'info, PostMessage<'info>>,
    batch_id: u32,
    payload: Vec<u8>,
    finality: Finality
) -> Result<()>
Parameters

ctx CpiContext<'_, '_, '_, 'info, PostMessage<'info>>

Provides the necessary context for executing the function, including the accounts and program information required for the Cross-Program Invocation (CPI).

Type pub struct CpiContext<'a, 'b, 'c, 'info, T>
pub struct CpiContext<'a, 'b, 'c, 'info, T>
where
    T: ToAccountMetas + ToAccountInfos<'info>,
{
    pub accounts: T,
    pub remaining_accounts: Vec<AccountInfo<'info>>,
    pub program: AccountInfo<'info>,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
}

For more information, please refer to the wormhole_anchor_sdk Rust docs.

Type PostMessage<'info>
pub struct PostMessage<'info> {
    pub config: AccountInfo<'info>,
    pub message: AccountInfo<'info>,
    pub emitter: AccountInfo<'info>,
    pub sequence: AccountInfo<'info>,
    pub payer: AccountInfo<'info>,
    pub fee_collector: AccountInfo<'info>,
    pub clock: AccountInfo<'info>,
    pub rent: AccountInfo<'info>,
    pub system_program: AccountInfo<'info>,
}

For more information, please refer to the wormhole_anchor_sdk Rust docs.


batch_id u32

An identifier for the message batch.


payload Vec<u8>

The data being sent in the message. This is a variable-length byte array that contains the actual content or information being transmitted. To learn about the different types of payloads, check out the VAAs page.


finality Finality

Specifies the level of finality or confirmation required for the message.

Type Finality
pub enum Finality {
    Confirmed,
    Finalized,
}
Returns

Result<()>

The result of the function’s execution. If the function completes successfully, it returns Ok(()), otherwise it returns Err(E), indicating that an error occurred along with the details about the error

Example
let fee = ctx.accounts.wormhole_bridge.fee();
// ... Check fee and send parameters

let config = &ctx.accounts.config;
let payload: Vec<u8> = HelloWorldMessage::Hello { message }.try_to_vec()?;

// Invoke `wormhole::post_message`.
wormhole::post_message(
    CpiContext::new_with_signer(
        ctx.accounts.wormhole_program.to_account_info(),
        wormhole::PostMessage {
            // ... Set fields
        },
        &[
            // ... Set seeds
        ],
    ),
    config.batch_id,
    payload,
    config.finality.into(),
)?;

View the complete Hello World example in the Wormhole Scaffolding repository on GitHub.

Once the message is emitted from the Core Contract, the Guardian Network will observe the message and sign the digest of an Attestation VAA. On EVM chains, the body of the VAA is hashed twice with keccak256 to produce the signed digest message. On Solana, the Solana secp256k1 program will hash the message passed. In this case, the argument for the message should be a single hash of the body, not the twice-hashed body.

VAAs are multicast by default. This means there is no default target chain for a given message. The application developer decides on the format of the message and its treatment upon receipt.

Receiving Messages

The way a message is received and handled depends on the environment.

On EVM chains, the message passed is the raw VAA encoded as binary. The IWormhole.sol interface provides the parseAndVerifyVM function, which can be used to parse and verify the received message.

function parseAndVerifyVM(
    bytes calldata encodedVM
) external view returns (VM memory vm, bool valid, string memory reason);
Parameters

encodedVM bytes calldata

The encoded message as a Verified Action Approval (VAA), which contains all necessary information for verification and processing.

Returns

vm VM memory

The valid parsed VAA, which will include the original emitterAddress, sequenceNumber, and consistencyLevel, among other fields outlined on the VAAs page.

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

For more information, refer to the IWormhole.sol interface.


valid bool

A boolean indicating whether the VAA is valid or not.


reason string

If the VAA is not valid, a reason will be provided

Example
function receiveMessage(bytes memory encodedMessage) public {
    // Call the Wormhole core contract to parse and verify the encodedMessage
    (
        IWormhole.VM memory wormholeMessage,
        bool valid,
        string memory reason
    ) = wormhole().parseAndVerifyVM(encodedMessage);

    // Perform safety checks here

    // Decode the message payload into the HelloWorldMessage struct
    HelloWorldMessage memory parsedMessage = decodeMessage(
        wormholeMessage.payload
    );

    // Your custom application logic here
}

View the complete Hello World example in the Wormhole Scaffolding repository on GitHub.

On Solana, the VAA is first posted and verified by the Core Contract, after which it can be read by the receiving contract and action taken.

Retrieve the raw message data:

let posted_message = &ctx.accounts.posted;
posted_message.data()
Example
pub fn receive_message(ctx: Context<ReceiveMessage>, vaa_hash: [u8; 32]) -> Result<()> {
    let posted_message = &ctx.accounts.posted;

    if let HelloWorldMessage::Hello { message } = posted_message.data() {
        // Check message
        // Your custom application logic here
        Ok(())
    } else {
        Err(HelloWorldError::InvalidMessage.into())
    }
}

View the complete Hello World example in the Wormhole Scaffolding repository on GitHub.

Validating the Emitter

When processing cross-chain messages, it's critical to ensure that the message originates from a trusted sender (emitter). This can be done by verifying the emitter address and chain ID in the parsed VAA.

Typically, contracts should provide a method to register trusted emitters and check incoming messages against this list before processing them. For example, the following check ensures that the emitter is registered and authorized:

require(isRegisteredSender(emitterChainId, emitterAddress), "Invalid emitter");

This check can be applied after the VAA is parsed, ensuring only authorized senders can interact with the receiving contract. Trusted emitters can be registered using a method like setRegisteredSender during contract deployment or initialization.

const tx = await receiverContract.setRegisteredSender(
  sourceChain.chainId,
  ethers.zeroPadValue(senderAddress as BytesLike, 32)
);

await tx.wait();

Additional Checks

In addition to environment-specific checks that should be performed, a contract should take care to check other fields in the body, including:

  • Sequence - is this the expected sequence number? How should out-of-order deliveries be handled?
  • Consistency level - for the chain this message came from, is the consistency level enough to guarantee the transaction won't be reverted after taking some action?

The VAA digest is separate from the VAA body but is also relevant. It can be used for replay protection by checking if the digest has already been seen. Since the payload itself is application-specific, there may be other elements to check to ensure safety.

Source Code References

For a deeper understanding of the Core Contract implementation for a specific blockchain environment and to review the actual source code, please refer to the following links:

Got any questions?

Find out more