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

Token Teleport Example #4

Merged
merged 9 commits into from
Apr 19, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/

# Dotenv file
.env

# IDE files
.vscode/
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "hello_foundry/lib/forge-std"]
path = hello_foundry/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/contracts"]
path = lib/contracts
url = https://github.com/Analog-Labs/contracts
[submodule "lib/analog-gmp"]
path = lib/analog-gmp
url = https://github.com/Analog-Labs/analog-gmp
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,50 @@
This project uses **Forge** Ethereum testing framework (like Truffle, Hardhat and DappTools).
Install instructions: https://book.getfoundry.sh/

## Usage
## Examples

- [Simple Counter](./examples/teleport-tokens/README.md): Increment a counter in a contract deployed at `Chain A` by sending a message from `Chain B`.
- [Teleport Tokens](./examples/teleport-tokens/README.md): Teleport ERC20 tokens from `Chain A` to `Chain B`.

## Starting a New Project
To start a new project with Foundry, use forge init
```sh
forge init hello_gmp
```
This creates a new directory hello_gmp from the default Foundry template. This also initializes a new git repository.

Install analog-gmp dependencies.
```sh
cd hello_gmp
forge install Analog-Labs/analog-gmp
```

All setup! now just need to import gmp dependencies from `@analog-gmp`:
```solidity
import {IGmpReceiver} from "@analog-gmp/interfaces/IGmpReceiver.sol";
import {IGateway} from "@analog-gmp/interfaces/IGateway.sol";
```

### Writing Tests
You can easily write cross-chain unit tests using analog's testing tools at `@analog-gmp-testing`.
```solidity
import {GmpTestTools} from "@analog-gmp-testing/GmpTestTools.sol";

// Deploy gateway contracts and create forks for all supported networks
GmpTestTools.setup();

// Set `account` balance in all networks
GmpTestTools.deal(address(account), 100 ether);

// Switch to Sepolia network
GmpTestTools.switchNetwork(5);

// Switch to Shibuya network and set `account` as `msg.sender` and `tx.origin`
GmpTestTools.switchNetwork(7, address(account));

// Relay all pending GMP messages.
GmpTestTools.relayMessages();
```

### Build

Expand All @@ -18,7 +61,7 @@ $ forge build
### Test

```shell
$ forge test
$ forge test -vvv
```

### Format
Expand All @@ -27,12 +70,6 @@ $ forge test
$ forge fmt
```

### Gas Snapshots

```shell
$ forge snapshot
```

## License

Analog's Contracts is released under the [MIT License](LICENSE).
19 changes: 0 additions & 19 deletions examples/simple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,6 @@

This example demonstrates how to relay a message from a source-chain to a destination-chain.

### Prerequisite

- [Setup environment variables](/README.md#set-environment-variables)
- [Install Forge](https://book.getfoundry.sh/getting-started/installation)

## Usage

### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

## License

Analog's Examples is released under the [MIT License](../../LICENSE).
96 changes: 96 additions & 0 deletions examples/teleport-tokens/BasicERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0;

import {ERC20} from "@solmate/tokens/ERC20.sol";
import {IGmpReceiver} from "@analog-gmp/interfaces/IGmpReceiver.sol";
import {IGateway} from "@analog-gmp/interfaces/IGateway.sol";
import {GmpSender, PrimitiveUtils} from "@analog-gmp/Primitives.sol";

contract BasicERC20 is ERC20, IGmpReceiver {
using PrimitiveUtils for GmpSender;

IGateway private immutable _trustedGateway;
BasicERC20 private immutable _recipientErc20;
uint16 private immutable _recipientNetwork;

/**
* @dev Emitted when `amount` tokens are teleported from one account (`from`) in this chain to
* another (`to`) in another chain.
*
* Note Is not necessary to emit the destination network, because this is already emitted by the gateway in `GmpCreated` event.
*/
event OutboundTransfer(bytes32 indexed id, address indexed from, address indexed to, uint256 amount);

/**
* @dev @dev Emitted when `amount` tokens are teleported from one account (`from`) in another chain to
* an account (`to`) in this chain.
*
* Note Is not necessary to emit the source network, because this is already emitted by the gateway in `GmpExecuted` event.
*/
event InboundTransfer(bytes32 indexed id, address indexed from, address indexed to, uint256 amount);

/**
* @dev Gas limit used to execute `onGmpReceived` method.
*/
uint256 private constant MSG_GAS_LIMIT = 100_000;

/**
* @dev Command that will be encoded in the `data` field on the `onGmpReceived` method.
*/
struct TeleportCommand {
address from;
address to;
uint256 amount;
}

constructor(
string memory name,
string memory symbol,
IGateway gatewayAddress,
BasicERC20 recipient,
uint16 recipientNetwork,
address holder,
uint256 initialSupply
) ERC20(name, symbol, 10) {
_trustedGateway = gatewayAddress;
_recipientErc20 = recipient;
_recipientNetwork = recipientNetwork;
if (initialSupply > 0) {
_mint(holder, initialSupply);
}
}

/**
* @dev Teleport tokens from `msg.sender` to `recipient` in `_recipientNetwork`
*/
function teleport(address recipient, uint256 amount) external returns (bytes32 messageID) {
_burn(msg.sender, amount);
bytes memory message = abi.encode(TeleportCommand({from: msg.sender, to: recipient, amount: amount}));
messageID = _trustedGateway.submitMessage(address(_recipientErc20), _recipientNetwork, MSG_GAS_LIMIT, message);
emit OutboundTransfer(messageID, msg.sender, recipient, amount);
}

function onGmpReceived(bytes32 id, uint128 network, bytes32 sender, bytes calldata data)
external
payable
returns (bytes32)
{
// Convert bytes32 to address
address senderAddr = GmpSender.wrap(sender).toAddress();

// Validate the message
require(msg.sender == address(_trustedGateway), "Unauthorized: only the gateway can call this method");
require(network == _recipientNetwork, "Unauthorized network");
require(senderAddr == address(_recipientErc20), "Unauthorized sender");

// Decode the command
TeleportCommand memory command = abi.decode(data, (TeleportCommand));

// Mint the tokens to the destination account
_mint(command.to, command.amount);
emit InboundTransfer(id, command.from, command.to, command.amount);

return id;
}
}
123 changes: 123 additions & 0 deletions examples/teleport-tokens/BasicERC20.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0;

import {Test} from "forge-std/Test.sol";
import {BasicERC20} from "./BasicERC20.sol";
import {GmpTestTools} from "@analog-gmp-testing/GmpTestTools.sol";
import {Gateway} from "@analog-gmp/Gateway.sol";
import {IGateway} from "@analog-gmp/interfaces/IGateway.sol";
import {GmpMessage, GmpStatus, GmpSender, PrimitiveUtils} from "@analog-gmp/Primitives.sol";

contract GmpTestToolsTest is Test {
using PrimitiveUtils for GmpSender;
using PrimitiveUtils for address;

address private constant ALICE = address(bytes20(keccak256("Alice")));
address private constant BOB = address(bytes20(keccak256("Bob")));

Gateway private constant SEPOLIA_GATEWAY = Gateway(GmpTestTools.SEPOLIA_GATEWAY);
uint16 private constant SEPOLIA_NETWORK = GmpTestTools.SEPOLIA_NETWORK_ID;

Gateway private constant SHIBUYA_GATEWAY = Gateway(GmpTestTools.SHIBUYA_GATEWAY);
uint16 private constant SHIBUYA_NETWORK = GmpTestTools.SHIBUYA_NETWORK_ID;

/// @dev Test the teleport of tokens from Alice's account in Shibuya to Bob's account in Sepolia
function test_teleportTokens() external {
////////////////////////////////////
// Step 1: Setup test environment //
////////////////////////////////////

// Deploy the gateway contracts at pre-defined addresses
// Also creates one fork for each supported network
GmpTestTools.setup();

// Add funds to Alice and Bob in all networks
GmpTestTools.deal(ALICE, 100 ether);
GmpTestTools.deal(BOB, 100 ether);

///////////////////////////////////////////////////////
// Step 2: Deploy the sender and recipient contracts //
///////////////////////////////////////////////////////

// Pre-compute the contract addresses, because the contracts must know each other addresses.
BasicERC20 shibuyaErc20 = BasicERC20(vm.computeCreateAddress(ALICE, vm.getNonce(ALICE)));
BasicERC20 sepoliaErc20 = BasicERC20(vm.computeCreateAddress(BOB, vm.getNonce(BOB)));

// Switch to Shibuya network and deploy the ERC20 using Alice account
GmpTestTools.switchNetwork(SHIBUYA_NETWORK, ALICE);
shibuyaErc20 = new BasicERC20("Shibuya ", "A", SHIBUYA_GATEWAY, sepoliaErc20, SEPOLIA_NETWORK, ALICE, 1000);
assertEq(shibuyaErc20.balanceOf(ALICE), 1000, "unexpected alice balance in shibuya");
assertEq(shibuyaErc20.balanceOf(BOB), 0, "unexpected bob balance in shibuya");

// Switch to Sepolia network and deploy the ERC20 using Bob account
GmpTestTools.switchNetwork(SEPOLIA_NETWORK, BOB);
sepoliaErc20 = new BasicERC20("Sepolia", "B", SEPOLIA_GATEWAY, shibuyaErc20, SHIBUYA_NETWORK, BOB, 0);
assertEq(sepoliaErc20.balanceOf(ALICE), 0, "unexpected alice balance in sepolia");
assertEq(sepoliaErc20.balanceOf(BOB), 0, "unexpected bob balance in sepolia");

// Check if the computed addresses matches
assertEq(address(shibuyaErc20), vm.computeCreateAddress(ALICE, 0), "unexpected shibuyaErc20 address");
assertEq(address(sepoliaErc20), vm.computeCreateAddress(BOB, 0), "unexpected sepoliaErc20 address");

///////////////////////////////////////////////////////////
// Step 3: Deposit funds to destination Gateway Contract //
///////////////////////////////////////////////////////////

// Switch to Sepolia network and Alice account
GmpTestTools.switchNetwork(SEPOLIA_NETWORK, ALICE);
// If the sender is a contract, it's address must be converted
GmpSender sender = address(shibuyaErc20).toSender(true);
// Alice deposit 1 ether to Sepolia gateway contract
SEPOLIA_GATEWAY.deposit{value: 1 ether}(sender, SHIBUYA_NETWORK);

//////////////////////////////
// Step 4: Send GMP message //
//////////////////////////////

// Switch to Shibuya network and Alice account
GmpTestTools.switchNetwork(SHIBUYA_NETWORK, ALICE);

// Teleport 100 tokens from Alice to to Bob's account in sepolia
// Obs: The `teleport` method internally calls `gateway.submitMessage(...)`
vm.expectEmit(false, true, false, true, address(shibuyaErc20));
emit BasicERC20.OutboundTransfer(bytes32(0), ALICE, BOB, 100);
bytes32 messageID = shibuyaErc20.teleport(BOB, 100);

// Now with the `messageID`, Alice can check the message status in the destination gateway contract
// status 0: means the message is pending
// status 1: means the message was executed successfully
// status 2: means the message was executed but reverted
GmpTestTools.switchNetwork(SEPOLIA_NETWORK, ALICE);
assertTrue(
SEPOLIA_GATEWAY.gmpInfo(messageID).status == GmpStatus.NOT_FOUND,
"unexpected message status, expect 'pending'"
);

///////////////////////////////////////////////////
// Step 5: Wait Chronicles Relay the GMP message //
///////////////////////////////////////////////////

// The GMP hasn't been executed yet...
assertEq(sepoliaErc20.balanceOf(ALICE), 0, "unexpected alice balance in shibuya");

// Note: In a live network, the GMP message will be relayed by Chronicle Nodes after a minimum number of confirmations.
// here we can simulate this behavior by calling `GmpTestTools.relayMessages()`, this will relay all pending messages.
vm.expectEmit(true, true, false, true, address(sepoliaErc20));
emit BasicERC20.InboundTransfer(messageID, ALICE, BOB, 100);
GmpTestTools.relayMessages();

// Success! The GMP message was executed!!!
assertTrue(SEPOLIA_GATEWAY.gmpInfo(messageID).status == GmpStatus.SUCCESS, "failed to execute GMP");

// Check ALICE and BOB balance in shibuya
GmpTestTools.switchNetwork(SHIBUYA_NETWORK);
assertEq(shibuyaErc20.balanceOf(ALICE), 900, "unexpected alice's balance in shibuya");
assertEq(shibuyaErc20.balanceOf(BOB), 0, "unexpected bob's balance in shibuya");

// Check ALICE and BOB balance in sepolia
GmpTestTools.switchNetwork(SEPOLIA_NETWORK);
assertEq(sepoliaErc20.balanceOf(ALICE), 0, "unexpected alice's balance in sepolia");
assertEq(sepoliaErc20.balanceOf(BOB), 100, "unexpected bob's balance in sepolia");
}
}
7 changes: 7 additions & 0 deletions examples/teleport-tokens/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Teleport Tokens

This example demonstrates how teleport an ERC20 from Alice's account in Shibuya to Bob's account in Sepolia.

## License

Analog's Examples is released under the [MIT License](../../LICENSE).
1 change: 1 addition & 0 deletions lib/analog-gmp
Submodule analog-gmp added at 42a722
1 change: 0 additions & 1 deletion lib/contracts
Submodule contracts deleted from edacd4
3 changes: 0 additions & 3 deletions remappings.txt

This file was deleted.