Skip to content

Commit

Permalink
Token Teleport Example (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lohann authored Apr 19, 2024
1 parent 88b89c3 commit 83c3934
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 34 deletions.
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.

0 comments on commit 83c3934

Please sign in to comment.