-
Notifications
You must be signed in to change notification settings - Fork 27
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
Update getting started guide #202
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d771181
update getting started guide
cam-schultz ed15dd3
remove possessive language
cam-schultz 73717b2
Update contracts/src/CrossChainApplications/GETTING_STARTED.md
cam-schultz 016c0f3
Merge branch 'main' into update-getting-started
cam-schultz e9a0755
add example commands
cam-schultz dcde08c
Merge branch 'update-getting-started' of github.com:ava-labs/teleport…
cam-schultz f0ed9e2
Merge branch 'main' into update-getting-started
cam-schultz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
|
@@ -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. | ||
|
@@ -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), | ||
|
@@ -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. | ||
|
@@ -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. | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't know you could put diffs in READMEs like this. Very nice! |
||
- 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`. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gonna keep this as is to match the flow of the rest of this step.