diff --git a/.gitignore b/.gitignore index 85198aa..5841fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/ # Dotenv file .env + +# IDE files +.vscode/ diff --git a/.gitmodules b/.gitmodules index 6b9ffa6..4b5107b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/README.md b/README.md index b86ff5e..7d67ac1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -18,7 +61,7 @@ $ forge build ### Test ```shell -$ forge test +$ forge test -vvv ``` ### Format @@ -27,12 +70,6 @@ $ forge test $ forge fmt ``` -### Gas Snapshots - -```shell -$ forge snapshot -``` - ## License Analog's Contracts is released under the [MIT License](LICENSE). diff --git a/examples/simple/README.md b/examples/simple/README.md index 77e060a..0a89c92 100644 --- a/examples/simple/README.md +++ b/examples/simple/README.md @@ -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). diff --git a/examples/teleport-tokens/BasicERC20.sol b/examples/teleport-tokens/BasicERC20.sol new file mode 100644 index 0000000..0a41c44 --- /dev/null +++ b/examples/teleport-tokens/BasicERC20.sol @@ -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; + } +} diff --git a/examples/teleport-tokens/BasicERC20.t.sol b/examples/teleport-tokens/BasicERC20.t.sol new file mode 100644 index 0000000..31ccf96 --- /dev/null +++ b/examples/teleport-tokens/BasicERC20.t.sol @@ -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"); + } +} diff --git a/examples/teleport-tokens/README.md b/examples/teleport-tokens/README.md new file mode 100644 index 0000000..13a6525 --- /dev/null +++ b/examples/teleport-tokens/README.md @@ -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). diff --git a/lib/analog-gmp b/lib/analog-gmp new file mode 160000 index 0000000..42a7223 --- /dev/null +++ b/lib/analog-gmp @@ -0,0 +1 @@ +Subproject commit 42a7223b44141a9028f39c7aff9f6cd9c75c1196 diff --git a/lib/contracts b/lib/contracts deleted file mode 160000 index edacd42..0000000 --- a/lib/contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit edacd4279bcc3378a200caae921d1d4e96a9b0b0 diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index e8343c2..0000000 --- a/remappings.txt +++ /dev/null @@ -1,3 +0,0 @@ -@analog-gmp/=lib/contracts/src/ -@analog-gmp-testing/=lib/contracts/test/ -forge-std/=lib/forge-std/src/