Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone getting started guide #155

Merged
merged 13 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ contract ERC20Bridge is
ITeleporterMessenger teleporterMessenger = teleporterRegistry
.getLatestTeleporter();

// For non-zero fee amounts, transfer the fee into the control of this contract first, and then
// For non-zero fee amounts, first transfer the fee to this contract, and then
// allow the Teleporter contract to spend it.
uint256 adjustedFeeAmount = 0;
if (messageFeeAmount > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ contract ExampleCrossChainMessenger is
) external nonReentrant returns (uint256) {
ITeleporterMessenger teleporterMessenger = teleporterRegistry
.getLatestTeleporter();
// For non-zero fee amounts, transfer the fee into the control of this contract first, and then
// For non-zero fee amounts, first transfer the fee to this contract, and then
// allow the Teleporter contract to spend it.
uint256 adjustedFeeAmount = 0;
if (feeAmount > 0) {
Expand Down
214 changes: 214 additions & 0 deletions contracts/src/CrossChainApplications/GETTING_STARTED.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Getting Started with an Example Teleporter Application

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 the directory that will hold the application.

At the top of the file define the Solidity version to work with, and import the necessary types and interfaces.

```solidity
pragma solidity 0.8.18;

import {ITeleporterMessenger, TeleporterMessageInput, TeleporterFeeInfo} from "../../Teleporter/ITeleporterMessenger.sol";
import {ITeleporterReceiver} from "../../Teleporter/ITeleporterReceiver.sol";
```

Next, define the initial empty contract.

```solidity
contract MyExampleCrossChainMessenger {}
```

## Step 2: Integrating Teleporter Messenger

Now that the initial empty `MyExampleCrossChainMessenger` is defined, it's time to integrate the `ITeleporterMessenger` that will provide the functionality to deliver cross chain messages.

Create a state variable of `ITeleporterMessenger` type called `teleporterMessenger`. Then create a constructor for our contract that takes in an address where the Teleporter Messenger would be deployed on this chain, and set our state variable with it.

```solidity
contract ExampleCrossChainMessenger {
ITeleporterMessenger public immutable teleporterMessenger;

constructor(address teleporterMessengerAddress) {
teleporterMessenger = ITeleporterMessenger(teleporterMessengerAddress);
}
}
```

## Step 3: Send and Receive

Now that `MyExampleCrossChainMessenger` has an instantiation of `ITeleporterMessenger`, the next step is to add in functionality of sending and receiving arbitrary string data between chains.

To start, create the function declarations 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' `receiveTeleporterMessage` function, and the actual message data.

```solidity
// Send a new message to another chain.
function sendMessage(
bytes32 destinationBlockchainID,
address destinationAddress,
address feeTokenAddress,
uint256 feeAmount,
uint256 requiredGasLimit,
string calldata message
) external returns (uint256 messageID) {}
```

`MyExampleCrossChainMessenger` also needs to implement `ITeleporterReceiver` by adding the method `receiveTeleporterMessage` that receives the cross-chain messages from Teleporter.

```solidity
// Receive a new message from another chain.
function receiveTeleporterMessage(
bytes32 originBlockchainID,
address originSenderAddress,
bytes calldata message
) external {}
```

Now it's time to implement the methods, starting with `sendMessage`. First, add the import for OpenZeppelin's `IERC20` contract to the top of your contract.

```solidity
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
```

Then in `sendMessage` check 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.

```solidity
function sendMessage(
bytes32 destinationBlockchainID,
address destinationAddress,
address feeTokenAddress,
uint256 feeAmount,
uint256 requiredGasLimit,
string calldata message
) external returns (uint256 messageID) {
// For non-zero fee amounts, first transfer the fee to this contract, and then
// allow the Teleporter contract to spend it.
if (feeAmount > 0) {
IERC20 feeToken = IERC20(feeTokenAddress);
require(
feeToken.transferFrom(msg.sender, address(this), feeAmount),
"Failed to transfer fee amount"
);
require(
feeToken.approve(address(teleporterMessenger), feeAmount),
"Failed to approve fee amount"
);
}
}
```

Note: Relayer fees are an optional way to incentive 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, add the call to the `TeleporterMessenger` contract with the message data to be executed when delivered to the destination address. In `sendMessage`, form a `TeleporterMessageInput` and call `sendCrossChainMessage` on the `TeleporterMessenger` instance to start the cross chain messaging process.

> `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.
> The `message` must be ABI encoded so that it can be properly decoded on the receiving end.

```solidity
return
teleporterMessenger.sendCrossChainMessage(
TeleporterMessageInput({
destinationBlockchainID: destinationBlockchainID,
destinationAddress: destinationAddress,
feeInfo: TeleporterFeeInfo({
feeTokenAddress: feeTokenAddress,
amount: feeAmount
}),
requiredGasLimit: requiredGasLimit,
allowedRelayerAddresses: new address[](0),
message: abi.encode(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.

```solidity
// Receive a new message from another chain.
function receiveTeleporterMessage(
bytes32 originBlockchainID,
address originSenderAddress,
bytes calldata message
) external {
// Only the Teleporter receiver can deliver a message.
require(msg.sender == address(teleporterMessenger), "Unauthorized.");

// do something with message.
return true;
}
```

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 defining the `struct` for how to save our messages. It saves the string message itself and the address of the sender.

A map will also be added where the key is the `originBlockchainID`, and the value is the latest `message` sent from that chain.

```solidity
// Messages sent to this contract.
struct Message {
address sender;
string message;
}

mapping(bytes32 originBlockchainID => Message message) private _messages;
```

Next, update `receiveTeleporterMessage` to save the message into our mapping after we receive and verify that it's sent from Teleporter. ABI decode the `message` bytes into a string.

```solidity
// Receive a new message from another chain.
function receiveTeleporterMessage(
bytes32 originBlockchainID,
address originSenderAddress,
bytes calldata message
) external {
// Only the Teleporter receiver can deliver a message.
require(msg.sender == address(teleporterMessenger), "Unauthorized.");

// Store the message.
messages[originBlockchainID] = Message(originSenderAddress, abi.decode(message, (string)));
}
```

Next, add a function called `getCurrentMessage` that allows users or contracts to easily query our contract for the latest message sent by a specified chain.

```solidity
// Check the current message from another chain.
function getCurrentMessage(
bytes32 originBlockchainID
) external view returns (address, string memory) {
Message memory messageInfo = messages[originBlockchainID];
return (messageInfo.sender, messageInfo.message);
}
```

There we have it, a simple cross chain messenger built on top of Teleporter! Full example [here](./ExampleMessenger/ExampleCrossChainMessenger.sol).

## Step 5: Testing

For testing, `scripts/local/test.sh` sets up a local Avalanche network with three subnets deployed with Teleporter, and a relayer to deliver Teleporter messages. To add an integration test simply add a new test script under `integration-tests`. An integration test for `ExampleCrossChainMessenger` is already included (`scripts/local/integration_tests/example_messenger.sh`), which performs the following steps:
Copy link
Contributor

@meaghanfitzgerald meaghanfitzgerald Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran scripts/local/test.sh in teleporter base directory and it failed.
I believe it is because while running /code/docker/run_setup.sh, subnet-evm is not cloned to the docker container before sourcing the latest avalanchego version from ./scripts/versions.sh in subnet-evm.

ARCH set to arm64
Using published awm-relayer image
Waiting for containers to start...
WARN[0000] The "TEST_TARGET" variable is not set. Defaulting to a blank string. 
[+] Building 0.8s (22/22) FINISHED                                                                                        docker:desktop-linux
 => [test_runner internal] load build definition from Dockerfile                                                                          0.0s
 => => transferring dockerfile: 2.22kB                                                                                                    0.0s
 => [test_runner internal] load .dockerignore                                                                                             0.0s
 => => transferring context: 2B                                                                                                           0.0s
 => [test_runner internal] load metadata for docker.io/library/ubuntu:20.04                                                               0.7s
 => [local_network internal] load .dockerignore                                                                                           0.0s
 => => transferring context: 2B                                                                                                           0.0s
 => [local_network internal] load build definition from Dockerfile                                                                        0.0s
 => => transferring dockerfile: 2.22kB                                                                                                    0.0s
 => [test_runner auth] library/ubuntu:pull token for registry-1.docker.io                                                                 0.0s
 => [local_network  1/14] FROM docker.io/library/ubuntu:20.04@sha256:ed4a42283d9943135ed87d4ee34e542f7f5ad9ecf2f244870e23122f703f91c2     0.0s
 => CACHED [test_runner  2/14] RUN apt-get update &&     apt-get upgrade -y &&     apt-get clean                                          0.0s
 => CACHED [test_runner  3/14] RUN apt-get install -y wget curl git python3 make gcc build-essential                                      0.0s
 => CACHED [test_runner  4/14] RUN apt-get install -y bison                                                                               0.0s
 => CACHED [test_runner  5/14] RUN wget https://go.dev/dl/go1.20.10.linux-arm64.tar.gz                                                    0.0s
 => CACHED [test_runner  6/14] RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.10.linux-arm64.tar.gz                            0.0s
 => CACHED [test_runner  7/14] RUN go version                                                                                             0.0s
 => CACHED [test_runner  8/14] RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/*     && localedef -i en_US  0.0s
 => CACHED [test_runner  9/14] RUN curl -L https://raw.githubusercontent.com/ava-labs/foundry/v0.1.0/foundryup/install > /tmp/foundry-in  0.0s
 => CACHED [test_runner 10/14] RUN apt-get update &&     apt-get install -y python3-pip                                                   0.0s
 => CACHED [test_runner 11/14] RUN pip3 install base58                                                                                    0.0s
 => CACHED [test_runner 12/14] RUN curl -sSfL https://raw.githubusercontent.com/ava-labs/avalanche-cli/main/scripts/install.sh | sh -s -  0.0s
 => CACHED [test_runner 13/14] RUN avalanche --version                                                                                    0.0s
 => CACHED [test_runner 14/14] RUN echo '{"MetricsEnabled}":false}' > ~/.avalanche-cli/config                                             0.0s
 => [test_runner] exporting to image                                                                                                      0.0s
 => => exporting layers                                                                                                                   0.0s
 => => writing image sha256:cd2514843237abc94311537a801c1aa85072f4f2993e865baea3213adcaef35e                                              0.0s
 => => naming to docker.io/library/test-runner-image                                                                                      0.0s
 => [local_network] exporting to image                                                                                                    0.0s
 => => exporting layers                                                                                                                   0.0s
 => => writing image sha256:164396b1c78dee645d78e82b1fea8512637af7fd14e1f6fd2b3c9f2bad4d4339                                              0.0s
 => => naming to docker.io/library/local-network-image                                                                                    0.0s
[+] Running 3/0
 ✔ Container teleporter-relayer-1  Created                                                                                                0.0s 
 ✔ Container local_network_test    Created                                                                                                0.0s 
 ✔ Container test_runner           Created                                                                                                0.0s 
Attaching to local_network_test, teleporter-relayer-1, test_runner
teleporter-relayer-1  | Waiting for subnets to start up. Retry count: 
test_runner           | Waiting for subnets to start up. Retry count: 
local_network_test    | /code/docker/run_setup.sh: line 34: ./scripts/versions.sh: No such file or directory
local_network_test exited with code 1
Aborting on container exit...
[+] Stopping 3/0
 ✔ Container test_runner           Stopped                                                                                                0.0s 
 ✔ Container teleporter-relayer-1  Stopped                                                                                                0.0s 
 ✔ Container local_network_test    Stopped                                                                                                0.0s 
Waiting for containers to start...
Docker process is dead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might conflict with #152

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is teleporter/subnet-evm populated? After cloning teleporter, run git submodule update --init --recursive to do so.

That being said, we're planning on removing the Docker-based integration tests, as well as reworking our subnet-evm dependency, so this part of the README will be removed soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is teleporter/subnet-evm populated? After cloning teleporter, run git submodule update --init --recursive to do so.

Did this and it ran, but seems that once the subnets were up and running the script exited due to foundry not being installed.

teleporter-relayer-1  | Waiting for subnets to start up. Retry count: 160
local_network_test    |   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
local_network_test    |                                  Dload  Upload   Total   Spent    
local_network_test    | Left  Speed
  0     0    0     0    0     0      0      0 
local_network_test    | --:--:-- --:--:-- 
local_network_test    | --:--:-- 
local_network_test    |     0
100  1097  100  1022  100    75   998k  75000 --:--:-- --:--:-- --:--:-- 1071k
local_network_test    | Subnet A chain ID: aQP7s5jeKjXBuGfEiLTxb2jViWPiqeygfD5pfUxmPjUAG37CT
local_network_test    | Subnet B chain ID: J3i5NRXusfHDEAHWPa1fm5P9LKdCLqn8sZQXLFByXGLtkd4cY
local_network_test    | Subnet C chain ID: wuU5JChqjAaUDsct3tTSgQxxsyL3rhAVjoBzuyqsU24S1gtek
local_network_test    | C-Chain chain ID: 2JeJDKL9Bvn1vLuuPL1DpUccBCVUh7iRnkv3a5pV9kJW5HbuQz
local_network_test    | /code/docker/run_setup.sh: line 101: forge: command not found
local_network_test exited with code 127
Aborting on container exit...

That being said, we're planning on removing the Docker-based integration tests, as well as reworking our subnet-evm dependency, so this part of the README will be removed soon.

Might not be worth troubleshooting this further if this testing portion is going to be rewritten. If this test is removed, this tutorial should definitely still include a test script or equivalent instructions for beginners.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was an issue with how our foundry fork was setup. If you were running this on Mac M1, then this should be resolved now. In any case, agreed not worth debugging here.


1. Deploys the [ExampleERC20](../Mocks/ExampleERC20.sol) token to subnet A.
2. Deploys `ExampleCrossChainMessenger` to both subnets A and B.
3. Approves the cross-chain messenger on subnet A to spend ERC20 tokens from the default address.
4. Sends `"hello world"` from subnet A to subnet B's cross-chain messenger to receive.
5. Calls `getCurrentMessage` on subnet B to make sure the right message and sender are received.

Running `./test.sh example_messenger` at the root of the repo should yield at the end of the test:

```bash
test_runner | Raw result is: "0x5DB9A7629912EBF95876228C24A848de0bfB43A9
test_runner | hello world!"
test_runner | The latest message from chain ID GoTmTVw77eGauaL17e1xPrtWEa72SQnvf9G8ackU6KZVGVYpz was:
test_runner | Message Sender: 0x5DB9A7629912EBF95876228C24A848de0bfB43A9
test_runner | Message: "hello world!"
test_runner | Successfully called getCurrentMessage.
test_runner | Done running test example_messenger.
test_runner |
test_runner exited with code 0
```
Loading