Skip to content

Commit

Permalink
Merge pull request #202 from ava-labs/update-getting-started
Browse files Browse the repository at this point in the history
Update getting started guide
  • Loading branch information
cam-schultz authored Dec 20, 2023
2 parents 1a7b124 + f0ed9e2 commit 83ee0af
Showing 1 changed file with 155 additions and 31 deletions.
186 changes: 155 additions & 31 deletions contracts/src/CrossChainApplications/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,75 @@ This section walks through how to build an example cross-chain application on to

## Step 1: Create Initial Contract

Create a new file called `MyExampleCrossChainMessenger.sol` in the directory that will hold the application.
Create a new file called `MyExampleCrossChainMessenger.sol` in a new directory:

```bash
mkdir teleporter/contracts/src/CrossChainApplications/MyExampleCrossChainMessenger/
touch teleporter/contracts/src/CrossChainApplications/MyExampleCrossChainMessenger/MyExampleCrossChainMessenger.sol
```

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 {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {ITeleporterReceiver} from "@teleporter/ITeleporterReceiver.sol";
```

Next, define the initial empty contract.
Next, define the initial empty contract. The contract inherits from `ReentrancyGuard` to prevent reentrancy attacks, and inherits from `ITeleporterReceiver` to allow the contract to receive messages from Teleporter.

```solidity
contract MyExampleCrossChainMessenger {}
contract MyExampleCrossChainMessenger is
ReentrancyGuard,
ITeleporterReceiver
{}
```

Finally, add the following struct and event declarations to the contract, which will be integrated in later:

```solidity
// Messages sent to this contract.
struct Message {
address sender;
string message;
}
/**
* @dev Emitted when a message is submited to be sent.
*/
event SendMessage(
bytes32 indexed destinationBlockchainID,
address indexed destinationAddress,
address feeTokenAddress,
uint256 feeAmount,
uint256 requiredGasLimit,
string message
);
/**
* @dev Emitted when a new message is received from a given chain ID.
*/
event ReceiveMessage(
bytes32 indexed originBlockchainID,
address indexed originSenderAddress,
string message
);
```

## 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.
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.

```solidity
contract ExampleCrossChainMessenger {
contract ExampleCrossChainMessenger is
ReentrancyGuard,
ITeleporterReceiver
{
ITeleporterMessenger public immutable teleporterMessenger;
constructor(address teleporterMessengerAddress) {
Expand Down Expand Up @@ -68,10 +112,23 @@ function receiveTeleporterMessage(
) 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.
Now it's time to implement the methods, starting with `sendMessage`. First, add the import for OpenZeppelin's `IERC20` contract to the top of the contract, as well as the import for the `SafeERC20` library.

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

Next, add a `using` directive in the contract declaration to specify `SafeERC20` as the `IERC20` implementation to use:

```solidity
contract ExampleCrossChainMessenger is
ReentrancyGuard,
TeleporterOwnerUpgradeable
{
using SafeERC20 for IERC20;
...
}
```

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.
Expand All @@ -87,36 +144,44 @@ function sendMessage(
) 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.
uint256 adjustedFeeAmount;
if (feeAmount > 0) {
IERC20 feeToken = IERC20(feeTokenAddress);
require(
feeToken.transferFrom(msg.sender, address(this), feeAmount),
"Failed to transfer fee amount"
adjustedFeeAmount = SafeERC20TransferFrom.safeTransferFrom(
IERC20(feeTokenAddress),
feeAmount
);
require(
feeToken.approve(address(teleporterMessenger), feeAmount),
"Failed to approve fee amount"
IERC20(feeTokenAddress).safeIncreaseAllowance(
address(teleporterMessenger),
adjustedFeeAmount
);
}
}
```

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.
Next, 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. 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
emit SendMessage({
destinationBlockchainID: destinationBlockchainID,
destinationAddress: destinationAddress,
feeTokenAddress: feeTokenAddress,
feeAmount: adjustedFeeAmount,
requiredGasLimit: requiredGasLimit,
message: message
});
return
teleporterMessenger.sendCrossChainMessage(
TeleporterMessageInput({
destinationBlockchainID: destinationBlockchainID,
destinationAddress: destinationAddress,
feeInfo: TeleporterFeeInfo({
feeTokenAddress: feeTokenAddress,
amount: feeAmount
amount: adjustedFeeAmount
}),
requiredGasLimit: requiredGasLimit,
allowedRelayerAddresses: new address[](0),
Expand All @@ -138,29 +203,22 @@ function receiveTeleporterMessage(
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.
Start by adding a map where the key is the `originBlockchainID`, and the value is the latest `message` sent from that chain. The `message` is of type `Message`, which is already declared in the contract.

```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.
Next, update `receiveTeleporterMessage` to save the message into the mapping after it is received and verified that it's sent from Teleporter. ABI decode the `message` bytes into a string. Also, emit the `ReceiveMessage` event.

```solidity
```solidity
// Receive a new message from another chain.
Expand All @@ -173,11 +231,20 @@ function receiveTeleporterMessage(
require(msg.sender == address(teleporterMessenger), "Unauthorized.");
// Store the message.
messages[originBlockchainID] = Message(originSenderAddress, abi.decode(message, (string)));
string memory messageString = abi.decode(message, (string));
_messages[originBlockchainID] = Message(
originSenderAddress,
messageString
);
emit ReceiveMessage(
originBlockchainID,
originSenderAddress,
messageString
);
}
```

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.
Next, add a function called `getCurrentMessage` that allows users or contracts to easily query the contract for the latest message sent by a specified chain.

```solidity
// Check the current message from another chain.
Expand All @@ -189,14 +256,71 @@ function getCurrentMessage(
}
```

There we have it, a simple cross chain messenger built on top of Teleporter! Full example [here](./ExampleMessenger/ExampleCrossChainMessenger.sol).
# 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](../Teleporter/Upgrades/README.md).

The first change to make is to inherit from `TeleporterOwnerUpgradeable` instead of `ITeleporterReceiver`. `TeleporterOwnerUpgradeable` integrates with `TeleporterRegistry` via `TeleporterUpgradeable` to easily utilize the latest `TeleporterMessenger` implementation. `TeleporterOwnerUpgradeable` also ensures that only the contract owner is able to upgrade the `TeleporterMessenger` implementation used by the contract.

To start, replace the import for `ITeleporterReceiver` with `TeleporterOwnerUpgradeable`:

```diff
- import {ITeleporterReceiver} from "@teleporter/ITeleporterReceiver.sol";
+ import {TeleporterOwnerUpgradeable} from "@teleporter/upgrades/TeleporterOwnerUpgradeable.sol";
```

Also, replace the contract declaration to inherit from `TeleporterOwnerUpgradeable` instead of `ITeleporterReceiver`:

```diff
contract ExampleCrossChainMessenger is
ReentrancyGuard,
- ITeleporterReceiver
+ TeleporterOwnerUpgradeable
{}
```

Next, update the constructor to invoke the `TeleporterOwnerUpgradeable` constructor.

```diff
- constructor(address teleporterMessengerAddress) {
- teleporterMessenger = ITeleporterMessenger(teleporterMessengerAddress);
- }
+ constructor(
+ address teleporterRegistryAddress
+ ) TeleporterOwnerUpgradeable(teleporterRegistryAddress) {}
```

## Step 5: Testing
Then, remove the `teleporterMessenger` state variable, and add a call to get the latest `ITeleporterMessenger` implementation from `TeleporterRegistry` in `sendMessage`.

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/example_messenger.go`, which performs the following:
```diff
- ITeleporterMessenger public immutable teleporterMessenger;
```

And finally, change `receiveTeleporterMessage` to `_receiveTeleporterMessage`, and mark it as `internal override`. It's also safe to remove the check against `teleporterMessenger` in `_receiveTeleporterMessage`, since that same check is handled in `TeleporterOwnerUpgradeable`'s `receiveTeleporterMessage` function.

```diff
- function receiveTeleporterMessage(
+ function _receiveTeleporterMessage(
bytes32 originBlockchainID,
address originSenderAddress,
bytes memory message
- external {
+ internal override {
- // Only the Teleporter receiver can deliver a message.
- require(msg.sender == address(teleporterMessenger), "Unauthorized.");
```


`MyExampleCrossChainMessenger` is now a working cross-chain dApp built on top of Teleporter! Full example [here](./ExampleMessenger/ExampleCrossChainMessenger.sol).

## 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:

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.

To run this test against the newly created `MyExampleCrossChainMessenger`, first generate the ABI Go bindings by running `./scripts/abi_bindings.sh` from the root of this repository. Then, modify `example_messenger.go` to use the ABI bindings for `MyExampleCrossChainMessenger` instead of `ExampleCrossChainMessenger`.

0 comments on commit 83ee0af

Please sign in to comment.