From d771181ce01f28527f455087d1dafcc80da0440f Mon Sep 17 00:00:00 2001 From: Cameron Schultz Date: Thu, 14 Dec 2023 13:24:01 -0600 Subject: [PATCH 1/4] update getting started guide --- .../CrossChainApplications/GETTING_STARTED.md | 175 +++++++++++++++--- 1 file changed, 147 insertions(+), 28 deletions(-) diff --git a/contracts/src/CrossChainApplications/GETTING_STARTED.md b/contracts/src/CrossChainApplications/GETTING_STARTED.md index 6f2b56558..ae428a64c 100644 --- a/contracts/src/CrossChainApplications/GETTING_STARTED.md +++ b/contracts/src/CrossChainApplications/GETTING_STARTED.md @@ -4,7 +4,7 @@ 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 the `teleporter/contracts/src/CrossChainApplications/MyExampleCrossChainMessenger/` At the top of the file define the Solidity version to work with, and import the necessary types and interfaces. @@ -12,13 +12,49 @@ At the top of the file define the Solidity version to work with, and import the 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. We inherit from `ReentrancyGuard` to prevent reentrancy attacks. We inherit from `ITeleporterReceiver` to allow our 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 @@ -28,7 +64,10 @@ Now that the initial empty `MyExampleCrossChainMessenger` is defined, it's time 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 { +contract ExampleCrossChainMessenger is + ReentrancyGuard, + ITeleporterReceiver +{ ITeleporterMessenger public immutable teleporterMessenger; constructor(address teleporterMessengerAddress) { @@ -66,10 +105,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 your 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. @@ -85,15 +137,15 @@ 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 ); } } @@ -101,12 +153,20 @@ function sendMessage( 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({ @@ -114,7 +174,7 @@ return destinationAddress: destinationAddress, feeInfo: TeleporterFeeInfo({ feeTokenAddress: feeTokenAddress, - amount: feeAmount + amount: adjustedFeeAmount }), requiredGasLimit: requiredGasLimit, allowedRelayerAddresses: new address[](0), @@ -136,7 +196,6 @@ function receiveTeleporterMessage( require(msg.sender == address(teleporterMessenger), "Unauthorized."); // do something with message. - return true; } ``` @@ -144,21 +203,15 @@ The base of sending and receiving messages cross chain is complete. `MyExampleCr ## 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 we declared earlier. ```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 our mapping after we receive and verify 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. @@ -171,7 +224,16 @@ 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 + ); } ``` @@ -187,14 +249,71 @@ function getCurrentMessage( } ``` +# 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) {} +``` + +Then, remove the `teleporterMessenger` state variable, and add a call to get the latest `ITeleporterMessenger` implementation from `TeleporterRegistry` in `sendMessage`. + +```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."); +``` + + There we have it, a simple cross chain messenger built on top of Teleporter! Full example [here](./ExampleMessenger/ExampleCrossChainMessenger.sol). -## Step 5: Testing +## 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/example_messenger.go`, which performs the following: +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`. From ed15dd3ce256b988c1ea0351578a833daf1c086b Mon Sep 17 00:00:00 2001 From: Cameron Schultz Date: Tue, 19 Dec 2023 15:05:28 -0600 Subject: [PATCH 2/4] remove possessive language --- .../src/CrossChainApplications/GETTING_STARTED.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/src/CrossChainApplications/GETTING_STARTED.md b/contracts/src/CrossChainApplications/GETTING_STARTED.md index ae428a64c..33596ad9a 100644 --- a/contracts/src/CrossChainApplications/GETTING_STARTED.md +++ b/contracts/src/CrossChainApplications/GETTING_STARTED.md @@ -16,7 +16,7 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard. import {ITeleporterReceiver} from "../../Teleporter/ITeleporterReceiver.sol"; ``` -Next, define the initial empty contract. We inherit from `ReentrancyGuard` to prevent reentrancy attacks. We inherit from `ITeleporterReceiver` to allow our contract to receive messages from Teleporter. +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 is @@ -61,7 +61,7 @@ event ReceiveMessage( 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 is @@ -105,7 +105,7 @@ 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, as well as the import for the `SafeERC20` library. +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"; @@ -203,13 +203,13 @@ The base of sending and receiving messages cross chain is complete. `MyExampleCr ## Step 4: Storing the Message -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 we declared earlier. +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 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. Also, emit the `ReceiveMessage` event. +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 @@ -237,7 +237,7 @@ function receiveTeleporterMessage( } ``` -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. @@ -304,7 +304,7 @@ And finally, change `receiveTeleporterMessage` to `_receiveTeleporterMessage`, a ``` -There we have it, a simple cross chain messenger built on top of Teleporter! Full example [here](./ExampleMessenger/ExampleCrossChainMessenger.sol). +`MyExampleCrossChainMessenger` is now a working cross-chain dApp built on top of Teleporter! Full example [here](./ExampleMessenger/ExampleCrossChainMessenger.sol). ## Step 6: Testing From 73717b22ebe2ed62d9c1bff516aefb86db779d0e Mon Sep 17 00:00:00 2001 From: cam-schultz <78878559+cam-schultz@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:06:54 -0600 Subject: [PATCH 3/4] Update contracts/src/CrossChainApplications/GETTING_STARTED.md Co-authored-by: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> Signed-off-by: cam-schultz <78878559+cam-schultz@users.noreply.github.com> --- contracts/src/CrossChainApplications/GETTING_STARTED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/CrossChainApplications/GETTING_STARTED.md b/contracts/src/CrossChainApplications/GETTING_STARTED.md index 33596ad9a..936f005d7 100644 --- a/contracts/src/CrossChainApplications/GETTING_STARTED.md +++ b/contracts/src/CrossChainApplications/GETTING_STARTED.md @@ -4,7 +4,7 @@ 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 `teleporter/contracts/src/CrossChainApplications/MyExampleCrossChainMessenger/` +Create a new file called `MyExampleCrossChainMessenger.sol` in a `teleporter/contracts/src/CrossChainApplications/MyExampleCrossChainMessenger/` directory. At the top of the file define the Solidity version to work with, and import the necessary types and interfaces. From e9a07551fc5b81d0186cda9e5cf08a73e2023ab5 Mon Sep 17 00:00:00 2001 From: Cameron Schultz Date: Wed, 20 Dec 2023 10:09:32 -0600 Subject: [PATCH 4/4] add example commands --- contracts/src/CrossChainApplications/GETTING_STARTED.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/src/CrossChainApplications/GETTING_STARTED.md b/contracts/src/CrossChainApplications/GETTING_STARTED.md index 33596ad9a..e68a76370 100644 --- a/contracts/src/CrossChainApplications/GETTING_STARTED.md +++ b/contracts/src/CrossChainApplications/GETTING_STARTED.md @@ -4,7 +4,12 @@ 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 `teleporter/contracts/src/CrossChainApplications/MyExampleCrossChainMessenger/` +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.