This tutorial contains a solidity contract (HelloWormhole.sol
) that can be deployed onto many EVM chains to form a fully functioning cross-chain application.
Specifically, we will write and deploy a contract onto many chains that allows users to request, from one contract, that a GreetingReceived
event be emitted from a contracts on a different chain.
This also allows users to pay for their custom greeting to be emitted on a chain that they do not have any gas funds for!
Included in the repository is:
- Example Solidity Code
- Example Forge local testing setup
- Testnet Deploy Scripts
- Example Testnet testing setup
- Node 16.14.1 or later, npm 8.5.0 or later: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
- forge 0.2.0 or later: https://book.getfoundry.sh/getting-started/installation
Pull the code down from github and cd into the directory, then build and test it.
git clone https://github.com/wormhole-foundation/hello-wormhole.git
cd hello-wormhole
npm run build
forge test
Expected output is
Running 1 test for test/HelloWormhole.t.sol:HelloWormholeTest
[PASS] testGreeting() (gas: 777229)
Test result: ok. 1 passed; 0 failed; finished in 3.98s
You will need a wallet with at least 0.05 Testnet AVAX and 0.01 Testnet CELO.
EVM_PRIVATE_KEY=your_wallet_private_key npm run deploy
You will need a wallet with at least 0.02 Testnet AVAX. Obtain testnet AVAX here
You must have also deployed contracts onto testnet (as described in the above section).
To test sending and receiving a message on testnet, execute the test as such:
EVM_PRIVATE_KEY=your_wallet_private_key npm run test
Let’s take a simple HelloWorld solidity application, and take it cross-chain!
This single-chain HelloWorld smart contract allows users to send greetings. In other words, it allows them to cause an event GreetingReceived
to be emitted with their greeting!
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract HelloWorld {
event GreetingReceived(string greeting, address sender);
string[] public greetings;
/**
* @notice Returns the cost (in wei) of a greeting
*/
function quoteGreeting() public view returns (uint256 cost) {
return 0;
}
/**
* @notice Updates the list of 'greetings'
* and emits a 'GreetingReceived' event with 'greeting'
*/
function sendGreeting(
string memory greeting
) public payable {
uint256 cost = quoteGreeting();
require(msg.value == cost);
emit GreetingReceived(greeting, msg.sender);
greetings.push(greeting);
}
}
Suppose we want users to be able to request, through their Ethereum wallet, that a greeting be sent to Avalanche, and vice versa.
Let us begin writing a contract that we can deploy onto Ethereum, Avalanche, or any number of other chains, to enable greetings be sent freely between each contract, irrespective of chain.
We'll want to implement the following function:
/**
* @notice Updates the list of 'greetings'
* and emits a 'GreetingReceived' event with 'greeting'
* on the HelloWormhole contract at
* chain 'targetChain' and address 'targetAddress'
*/
function sendCrossChainGreeting(
uint16 targetChain,
address targetAddress,
string memory greeting
) public payable;
The Wormhole Relayer contract lets us do exactly this! Let’s take a look at the Wormhole Relayer contract interface.
/**
* @notice Publishes an instruction for the default delivery provider
* to relay a payload to the address `targetAddress` on chain `targetChain`
* with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
*
* `targetAddress` must implement the IWormholeReceiver interface
*
* This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
*
* Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function
* with `refundChain` and `refundAddress` as parameters
*
* @param targetChain in Wormhole Chain ID format
* @param targetAddress address to call on targetChain (that implements IWormholeReceiver)
* @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
* @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
* @param gasLimit gas limit with which to call `targetAddress`.
* @return sequence sequence number of published VAA containing delivery instructions
*/
function sendPayloadToEvm(
uint16 targetChain,
address targetAddress,
bytes memory payload,
uint256 receiverValue,
uint256 gasLimit
) external payable returns (uint64 sequence);
The Wormhole Relayer network is powered by Delivery Providers, who perform the service of watching for Wormhole Relayer delivery requests and performing the delivery to the intended target chain as instructed.
In exchange for calling your contract at targetAddress
on targetChain
and paying the gas fees that your contract consumes, they charge a source chain fee. The fee charged will depend on the conditions of the target network and the fee can be requested from the delivery provider:
(deliveryPrice,) = quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)
So, following this interface, we can implement sendCrossChainGreeting
by simply calling sendPayloadToEvm with the payload being some information we'd like to send, such as the greeting and the sender of the greeting.
uint256 constant GAS_LIMIT = 50_000;
IWormholeRelayer public immutable wormholeRelayer;
/**
* @notice Returns the cost (in wei) of a greeting
*/
function quoteCrossChainGreeting(
uint16 targetChain
) public view returns (uint256 cost) {
// Cost of requesting a message to be sent to
// chain 'targetChain' with a gasLimit of 'GAS_LIMIT'
(cost, ) = wormholeRelayer.quoteEVMDeliveryPrice(
targetChain,
0,
GAS_LIMIT
);
}
/**
* @notice Updates the list of 'greetings'
* and emits a 'GreetingReceived' event with 'greeting'
* on the HelloWormhole contract at
* chain 'targetChain' and address 'targetAddress'
*/
function sendCrossChainGreeting(
uint16 targetChain,
address targetAddress,
string memory greeting
) public payable {
bytes memory payload = abi.encode(greeting, msg.sender);
uint256 cost = quoteCrossChainGreeting(targetChain);
require(msg.value == cost, "Incorrect payment");
wormholeRelayer.sendPayloadToEvm{value: cost}(
targetChain,
targetAddress,
payload,
0, // no receiver value needed
GAS_LIMIT
);
}
A key part of this system, though, is that the contract at the targetAddress
must implement the IWormholeReceiver
interface.
Since we want to allow sending and receiving messages by the HelloWormhole
contract, we must implement this interface.
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
/**
* @notice Interface for a contract which can receive Wormhole messages.
*/
interface IWormholeReceiver {
/**
* @notice When a `send` is performed with this contract as the target, this function will be
* invoked by the WormholeRelayer contract
*
* NOTE: This function should be restricted such that only the Wormhole Relayer contract can call it.
*
* We also recommend that this function checks that `sourceChain` and `sourceAddress` are indeed who
* you expect to have requested the calling of `send` on the source chain
*
* The invocation of this function corresponding to the `send` request will have msg.value equal
* to the receiverValue specified in the send request.
*
* If the invocation of this function reverts or exceeds the gas limit
* specified by the send requester, this delivery will result in a `ReceiverFailure`.
*
* @param payload - an arbitrary message which was included in the delivery by the
* requester.
* @param additionalVaas - Additional VAAs which were requested to be included in this delivery.
* They are guaranteed to all be included and in the same order as was specified in the
* delivery request.
* @param sourceAddress - the (wormhole format) address on the sending chain which requested
* this delivery.
* @param sourceChain - the wormhole chain ID where this delivery was requested.
* @param deliveryHash - the VAA hash of the deliveryVAA.
*
* NOTE: These signedVaas are NOT verified by the Wormhole core contract prior to being provided
* to this call. Always make sure `parseAndVerify()` is called on the Wormhole core contract
* before trusting the content of a raw VAA, otherwise the VAA may be invalid or malicious.
*/
function receiveWormholeMessages(
bytes memory payload,
bytes[] memory additionalVaas,
bytes32 sourceAddress,
uint16 sourceChain,
bytes32 deliveryHash
) external payable;
}
After sendPayloadToEvm
is called on the source chain, the off-chain Delivery Provider will pick up the VAA corresponding to the message. It will then call the receiveWormholeMessages
method on the targetChain
and targetAddress
specified.
So, in receiveWormholeMessages, we want to:
- Update the latest greeting
- Emit a 'GreetingReceived' event with the 'greeting' and sender of the greeting
Note: It is crucial that only the Wormhole Relayer contract can call receiveWormholeMessages
To provide certainty about the validity of the payload, we must restrict the msg.sender of this function to only be the Wormhole Relayer contract. Otherwise, anyone could call this receiveWormholeMessages endpoint with fake greetings, source chains, and source senders.
And voila, we have a full contract that can be deployed to many EVM chains, and in totality would form a full cross-chain application powered by Wormhole!
Users with any wallet can request greetings to be emitted on any chain that is part of the system.
Check out Part 2 for an in-depth explanation of how Wormhole Relayer causes contracts on other blockchains to be called with the appropriate inputs!
See the full implementation of the HelloWormhole.sol contract and the full Github repository with testing infrastructure