Getting Started
Dive deeper into Teleporter and kickstart your journey in building cross-chain dApps by enrolling in our Teleporter course.
Note: All example applications in the examples directory are meant for education purposes only and are not audited. The example contracts are not intended for use in production environments.
This section walks through how to build an example cross-chain application on top of the Teleporter protocol, recreating the ExampleCrossChainMessenger
contract that sends arbitrary string data from one chain to another. Note that this tutorial is meant for education purposes only. The resulting code is not intended for use in production environments.
Step 1: Create Initial Contract
Create a new file called MyExampleCrossChainMessenger.sol
in a new directory:
At the top of the file define the Solidity version to work with, and import the necessary types and interfaces.
Next, define the initial empty contract. The contract inherits from ReentrancyGuard
to prevent reentrancy attacks.
Finally, add the following struct and event declarations into the body of the contract, which will be integrated in later:
Step 2: Integrating Teleporter Messenger
Now that the initial empty MyExampleCrossChainMessenger
is defined, it's time to integrate with ITeleporterMessenger
, which will provide the functionality to deliver cross chain messages.
Create a state variable of ITeleporterMessenger
type called teleporterMessenger
. Then create a constructor that takes in an address where the Teleporter Messenger would be deployed on this chain, and set the corresponding state variable.
Step 3: Send and Receive
Now that MyExampleCrossChainMessenger
has an instantiation of ITeleporterMessenger
, the next step is to add in the functionality of sending and receiving arbitrary string data between chains.
To start, create the function declaration for sendMessage
, which will send string data cross-chain to the specified destination address' receiver. This function allows callers to specify the destination chain ID, destination address to send to, relayer fees, required gas limit for message execution at the destination address.
MyExampleCrossChainMessenger
also needs to implement ITeleporterReceiver
. First, add the import of this interface:
Then declare that the contract will implement it:
And then finally add the method receiveTeleporterMessage
that receives the cross-chain messages from Teleporter.
Now it's time to implement the methods, starting with sendMessage
. First, add the necessary imports.
Next, add a using
directive to the top of the contract body specifying SafeERC20
as the IERC20
implementation to use:
Then add a check to the sendMessage
function for whether feeAmount
is greater than zero. If it is, transfer and approve the amount of IERC20 asset at feeTokenAddress
to the Teleporter Messenger saved as a state variable.
Note: Relayer fees are an optional way to incentivize relayers to deliver a Teleporter message to its destination. They are not strictly necessary, and may be omitted if a relayer is willing to relay messages with no fee, such as with a self-hosted relayer.
Next, to the end of the sendMessage
function, add the event to emit, as well as the call to the TeleporterMessenger
contract with the message data to be executed when delivered to the destination address. Form a TeleporterMessageInput
and call sendCrossChainMessage
on the TeleporterMessenger
instance to start the cross chain messaging process. The message
must be ABI encoded so that it can be properly decoded on the receiving end.
Note: allowedRelayerAddresses
is empty in this example, meaning any relayer can try to deliver this cross chain message. Specific relayer addresses can be specified to ensure only those relayers can deliver the message.
With the sending side complete, the next step is to implement ITeleporterReceiver.receiveTeleporterMessage
. The receiver in this example will just receive the arbitrary string data, and check that the message is sent through Teleporter. To the receiveTeleporterMessage
function, add:
The base of sending and receiving messages cross chain is complete. MyExampleCrossChainMessenger
can now be expanded with functionality that saves the received messages, and allows users to query for the latest message received from a specified chain.
Step 4: Storing the Message
Start by adding a map to the body of the contract, in which the key is the sourceBlockchainID
and the value is the latest message
sent from that chain. The message
is of type Message
, which is already declared in the contract.
Next, update receiveTeleporterMessage
to save the message into the mapping after it is received and verified that it's sent from Teleporter. At the end of that function, ABI decode the message
bytes into a string, and emit the ReceiveMessage
event.
Next, add a function to the contract called getCurrentMessage
that allows users or contracts to easily query the contract for the latest message sent by a specified chain.
Step 5: Upgrade Support
At this point, the contract is now fully usable, and can be used to send arbitrary string data between chains. However, there are a few more modifications that need to be made to support upgrades to TeleporterMessenger
. For a more in-depth explanation of how to support upgrades, see the Upgrades README here.
The first change to make is to inherit from TeleporterOwnerUpgradeable
instead of ITeleporterReceiver
. TeleporterOwnerUpgradeable
integrates with the TeleporterRegistry
via TeleporterUpgradeable
to easily utilize the latest TeleporterMessenger
implementation. TeleporterOwnerUpgradeable
also ensures that only an admin address for managing Teleporter versions, specified by the constructor argument teleporterManager
, is able to upgrade the TeleporterMessenger
implementation used by the contract.
To start, replace the import for ITeleporterReceiver
with TeleporterOwnerUpgradeable
:
Also, replace the contract declaration to inherit from TeleporterOwnerUpgradeable
instead of ITeleporterReceiver
:
Next, update the constructor to invoke the TeleporterOwnerUpgradeable
constructor.
Then, remove the teleporterMessenger
state variable:
And at the beginning of sendMessage()
add a call to get the latest ITeleporterMessenger
implementation from TeleporterRegistry
:
And finally, change receiveTeleporterMessage
to _receiveTeleporterMessage
, mark it as internal override
, and change the data location of its message
parameter to memory
. It's also safe to remove the check against teleporterMessenger
in _receiveTeleporterMessage
, since that same check is handled in TeleporterOwnerUpgradeable
's receiveTeleporterMessage
function.
MyExampleCrossChainMessenger
is now a working cross-chain dApp built on top of Teleporter! Full example here.
Step 6: Testing
For testing, scripts/local/e2e_test.sh
sets up a local test environment consisting of three subnets deployed with Teleporter, and a lightweight inline relayer implementation to facilitate cross chain message delivery. An end-to-end test for ExampleCrossChainMessenger
is included in tests/flows/example_messenger.go
, which performs the following:
- Deploys the ExampleERC20 token to subnet A.
- Deploys
ExampleCrossChainMessenger
to both subnets A and B. - Approves the cross-chain messenger on subnet A to spend ERC20 tokens from the default address.
- Sends
"Hello, world!"
from subnet A to subnet B's cross-chain messenger to receive. - Calls
getCurrentMessage
on subnet B to make sure the right message and sender are received.
To run this test against the newly created MyExampleCrossChainMessenger
, first generate the ABI Go bindings by running ./scripts/abi_bindings.sh --contract MyExampleCrossChainMessenger
from the root of this repository. Then, add to the generated Go package the SendMessageRequiredGas
constant, which is required by the tests, in a new file abi-bindings/go/CrossChainApplications/MyExampleCrossChainMessenger/MyExampleCrossChainMessenger/constants.go
:
Next, modify tests/utils/utils.go
, which is used by tests/flows/example_messenger.go
, to use the ABI bindings for MyExampleCrossChainMessenger
instead of ExampleCrossChainMessenger
. First replace the import:
Then, in that same utils.go
, replace all instances of to examplecrosschainmessenger
with myexamplecrosschainmessenger
and all instances of ExampleCrossChainMessenger
with MyExampleCrossChainMessenger
.
Finally, from the root of the repository, invoke the tests with an extra bit of configuration that tells the Ginkgo test framework to focus only on the tests of this example contract (excluding all of the broader tests of Teleporter):
Last updated on