From fcda6da38605dfaf2abf27f0edb88d5e3cc4b03b Mon Sep 17 00:00:00 2001 From: James Kim Date: Tue, 17 Sep 2024 11:05:47 -0400 Subject: [PATCH] example: clean up code for CrossChainPingPong and add walkthrough --- README.md | 18 +-- contracts/src/CrossChainPingPong.sol | 153 +++++++++++++----- contracts/test/CrossChainPingPong.t.sol | 31 ++-- docs/README.md | 28 ++++ docs/src/SUMMARY.md | 2 +- .../interop/writing-contract-using-l2cdm.md | 1 + 6 files changed, 175 insertions(+), 58 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/src/guides/interop/writing-contract-using-l2cdm.md diff --git a/README.md b/README.md index 1bed5cfd0..b83aeff68 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # 🛠️ Supersim -Supersim is a lightweight tool to simulate the Superchain locally (with a single L1 and multiple OP-Stack L2s). Run multiple local nodes with one command, and coordinate message passing between them. +**Supersim is a lightweight tool to simulate the Superchain locally** (with a single L1 and multiple OP-Stack L2s). +### ✨ Features +- spin up multiple anvil nodes +- predeployed OP Stack contracts and useful mock contracts (ERC20) +- fork multiple remote chains (fork the entire Superchain) +- simulate L1 <> L2 message passing (deposits) and L2 <> L2 message passing (interoperability) + + +For **detailed instructions** and **usage guides**, refer to the [**📚 Supersim docs**](https://supersim.pages.dev). ## ❓ Why Supersim? @@ -15,14 +23,6 @@ Multichain development offers unique challenges: Supersim enables fast, local iteration on cross-chain features, with a simple developer interface. -## ✨ Features - -- spin up multiple anvil nodes -- predeployed OP Stack contracts and useful mock contracts (ERC20) -- fork multiple remote chains (fork the entire Superchain) -- simulate L1 <> L2 message passing (deposits) -- simulate L2 <> L2 message passing (interoperability) and auto-relayer - ## 🚀 Getting Started ### 1. Install prerequisites: `foundry` diff --git a/contracts/src/CrossChainPingPong.sol b/contracts/src/CrossChainPingPong.sol index 5ec10d030..3ab656516 100644 --- a/contracts/src/CrossChainPingPong.sol +++ b/contracts/src/CrossChainPingPong.sol @@ -4,67 +4,143 @@ pragma solidity 0.8.25; import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol"; +/// @notice Thrown when a function is called by an address other than the L2ToL2CrossDomainMessenger. error CallerNotL2ToL2CrossDomainMessenger(); +/// @notice Thrown when the cross-domain sender is not this contract's address on another chain. error InvalidCrossDomainSender(); +/// @notice Thrown when attempting to serve the ball from a non-server chain. +error UnauthorizedServer(uint256 callerChainId, uint256 serverChainId); + +/// @notice Thrown when attempting to serve the ball more than once. +error BallAlreadyServed(); + +/// @notice Thrown when attempting to hit the ball to an invalid destination chain. +error InvalidDestinationChain(uint256 toChainId); + +/// @notice Thrown when attempting to hit the ball when it's not on the current chain. +error BallNotPresent(); + +/// @notice Represents the state of the ping-pong ball. +/// @param rallyCount The number of times the ball has been hit. +/// @param lastHitterChainId The chain ID of the last chain to hit the ball. +/// @param lastHitterAddress The address of the last player to hit the ball. struct PingPongBall { uint256 rallyCount; uint256 lastHitterChainId; address lastHitterAddress; } +/** + * @title CrossChainPingPong + * @notice This contract implements a cross-chain ping-pong game using the L2ToL2CrossDomainMessenger. + * Players hit a virtual *ball* back and forth between allowed L2 chains. The game starts with a serve + * from a designated server chain, and each hit increases the rally count. The contract tracks the + * last hitter's address, chain ID, and the current rally count. + * @dev This contract relies on the L2ToL2CrossDomainMessenger for cross-chain communication. + */ contract CrossChainPingPong { - event BallSent(uint256 indexed toChainId, PingPongBall ball); + /// @notice Emitted when a ball is sent from one chain to another. + /// @param fromChainId The chain ID from which the ball is sent. + /// @param toChainId The chain ID to which the ball is sent. + /// @param ball The PingPongBall data structure containing rally count and hitter information. + event BallSent(uint256 indexed fromChainId, uint256 indexed toChainId, PingPongBall ball); + + /// @notice Emitted when a ball is received on a chain. + /// @param fromChainId The chain ID from which the ball was sent. + /// @param toChainId The chain ID on which the ball was received. + /// @param ball The PingPongBall data structure containing rally count and hitter information. + event BallReceived(uint256 indexed fromChainId, uint256 indexed toChainId, PingPongBall ball); + + /// @dev Address of the L2 to L2 cross-domain messenger predeploy. + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - event BallReceived(uint256 indexed fromChainId, PingPongBall ball); + /// @notice Chain ID of the server (initial ball sender). + uint256 internal immutable SERVER_CHAIN_ID; - address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + /// @dev Flag indicating if the server has already served the ball. + bool internal _hasServerAlreadyServed; - mapping(uint256 => bool) internal allowedChainIds; + /// @dev Mapping to track which chain IDs are allowed in the game. + mapping(uint256 => bool) internal _isChainIdAllowed; - uint256 internal serverChainId; - bool internal serverAlreadyServed; + /// @dev The current received ball on this chain. + PingPongBall internal _receivedBall; - PingPongBall internal receivedBall; + /// @dev Tracks whether the ball is currently on this chain. + bool private _isBallPresent; + /** + * @notice Constructor initializes the contract with allowed chain IDs and the server chain ID. + * @param _allowedChainIds The list of chain IDs that are allowed to participate in the game. + * @param _serverChainId The chain ID that will act as the server for the first ball serve. + * @dev Ensures that the server chain ID is included in the list of allowed chain IDs. + */ constructor(uint256[] memory _allowedChainIds, uint256 _serverChainId) { for (uint256 i = 0; i < _allowedChainIds.length; i++) { - allowedChainIds[_allowedChainIds[i]] = true; + _isChainIdAllowed[_allowedChainIds[i]] = true; } - require(allowedChainIds[_serverChainId], "Invalid first server chain ID"); + if (!_isChainIdAllowed[_serverChainId]) { + revert InvalidDestinationChain(_serverChainId); + } - serverChainId = _serverChainId; + SERVER_CHAIN_ID = _serverChainId; } - function serveBall(uint256 _toChainId) external { - require(serverChainId == block.chainid, "Cannot serve ball from this chain"); - require(serverAlreadyServed == false, "Ball already served"); - require(_isValidDestinationChain(_toChainId), "Invalid destination chain ID"); - - serverAlreadyServed = true; + /** + * @notice Serve the ping-pong ball to a specified destination chain. + * @dev Can only be called once from the server chain, and this starts off the game. + * @param _toChainId The chain ID to which the ball is served. + */ + function serveBallTo(uint256 _toChainId) external { + if (SERVER_CHAIN_ID != block.chainid) { + revert UnauthorizedServer(block.chainid, SERVER_CHAIN_ID); + } + if (_hasServerAlreadyServed) { + revert BallAlreadyServed(); + } + if (!_isValidDestinationChain(_toChainId)) { + revert InvalidDestinationChain(_toChainId); + } + _hasServerAlreadyServed = true; PingPongBall memory _newBall = PingPongBall(1, block.chainid, msg.sender); - _sendBallMessage(_newBall, _toChainId); + _sendCrossDomainMessage(_newBall, _toChainId); - emit BallSent(_toChainId, _newBall); + emit BallSent(block.chainid, _toChainId, _newBall); } - function sendBall(uint256 _toChainId) public { - require(_isBallOnThisChain(), "Ball is not on this chain"); - require(_isValidDestinationChain(_toChainId), "Invalid destination chain ID"); + /** + * @notice Hit the received ping-pong ball to a specified destination chain. + * @dev Can only be called when the ball is on the current chain. + * @param _toChainId The chain ID to which the ball is hit. + */ + function hitBallTo(uint256 _toChainId) public { + if (!_isBallPresent) { + revert BallNotPresent(); + } + if (!_isValidDestinationChain(_toChainId)) { + revert InvalidDestinationChain(_toChainId); + } - PingPongBall memory _newBall = PingPongBall(receivedBall.rallyCount + 1, block.chainid, msg.sender); + PingPongBall memory _newBall = PingPongBall(_receivedBall.rallyCount + 1, block.chainid, msg.sender); - delete receivedBall; + delete _receivedBall; + _isBallPresent = false; - _sendBallMessage(_newBall, _toChainId); + _sendCrossDomainMessage(_newBall, _toChainId); - emit BallSent(_toChainId, _newBall); + emit BallSent(block.chainid, _toChainId, _newBall); } + /** + * @notice Receives the ping-pong ball from another chain. + * @dev Only callable by the L2 to L2 cross-domain messenger. + * @param _ball The PingPongBall data received from another chain. + */ function receiveBall(PingPongBall memory _ball) external { if (msg.sender != MESSENGER) { revert CallerNotL2ToL2CrossDomainMessenger(); @@ -74,24 +150,29 @@ contract CrossChainPingPong { revert InvalidCrossDomainSender(); } - receivedBall.lastHitterAddress = _ball.lastHitterAddress; - receivedBall.lastHitterChainId = _ball.lastHitterChainId; - receivedBall.rallyCount = _ball.rallyCount; + _receivedBall = _ball; + _isBallPresent = true; - emit BallReceived(IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(), _ball); + emit BallReceived(IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(), block.chainid, _ball); } - function _sendBallMessage(PingPongBall memory _ball, uint256 _toChainId) internal { + /** + * @notice Internal function to send the ping-pong ball to another chain. + * @dev Uses the L2ToL2CrossDomainMessenger to send the message. + * @param _ball The PingPongBall data to send. + * @param _toChainId The chain ID to which the ball is sent. + */ + function _sendCrossDomainMessage(PingPongBall memory _ball, uint256 _toChainId) internal { bytes memory _message = abi.encodeCall(this.receiveBall, (_ball)); IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_toChainId, address(this), _message); } - function _isBallOnThisChain() internal view returns (bool) { - // Hack to check if receivedBall has been set - return receivedBall.lastHitterAddress != address(0); - } - + /** + * @notice Checks if a destination chain ID is valid. + * @param _toChainId The destination chain ID to validate. + * @return True if the destination chain ID is allowed and different from the current chain. + */ function _isValidDestinationChain(uint256 _toChainId) internal view returns (bool) { - return allowedChainIds[_toChainId] && _toChainId != block.chainid; + return _isChainIdAllowed[_toChainId] && _toChainId != block.chainid; } } diff --git a/contracts/test/CrossChainPingPong.t.sol b/contracts/test/CrossChainPingPong.t.sol index 098cd0454..1458cc9a8 100644 --- a/contracts/test/CrossChainPingPong.t.sol +++ b/contracts/test/CrossChainPingPong.t.sol @@ -6,7 +6,8 @@ import { CrossChainPingPong, PingPongBall, CallerNotL2ToL2CrossDomainMessenger, - InvalidCrossDomainSender + InvalidCrossDomainSender, + BallAlreadyServed } from "../src/CrossChainPingPong.sol"; import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol"; @@ -38,15 +39,16 @@ contract CrossChainPingPongTest is Test { } // Test serving the ball - function testServeBall() public { + function testServeBallTo() public { + uint256 fromChainId = 901; uint256 toChainId = 902; - vm.chainId(901); + vm.chainId(fromChainId); // Expect the BallSent event PingPongBall memory _expectedBall = PingPongBall(1, block.chainid, bob); vm.expectEmit(true, true, true, true, address(crossChainPingPong)); - emit CrossChainPingPong.BallSent(toChainId, _expectedBall); + emit CrossChainPingPong.BallSent(fromChainId, toChainId, _expectedBall); // Mock cross-chain message send call bytes memory _message = abi.encodeCall(crossChainPingPong.receiveBall, (_expectedBall)); @@ -60,17 +62,19 @@ contract CrossChainPingPongTest is Test { // Serve the ball vm.prank(bob); - crossChainPingPong.serveBall(toChainId); + crossChainPingPong.serveBallTo(toChainId); // Ensure serve can only happen once - vm.expectRevert("Ball already served"); - crossChainPingPong.serveBall(toChainId); + vm.expectRevert(BallAlreadyServed.selector); + crossChainPingPong.serveBallTo(toChainId); } // Test receiving the ball from a valid cross-chain message function testReceiveBall() public { uint256 fromChainId = 901; + uint256 toChainId = 902; + vm.chainId(toChainId); // Set up the mock for cross-domain message sender validation PingPongBall memory _ball = PingPongBall(1, fromChainId, address(this)); _mockAndExpect( @@ -88,7 +92,7 @@ contract CrossChainPingPongTest is Test { // Expect the BallReceived event vm.expectEmit(true, true, true, true, address(crossChainPingPong)); - emit CrossChainPingPong.BallReceived(fromChainId, _ball); + emit CrossChainPingPong.BallReceived(fromChainId, toChainId, _ball); // Call receiveBall as if from the messenger vm.prank(MESSENGER); @@ -96,9 +100,12 @@ contract CrossChainPingPongTest is Test { } // Test receiving then sending the ball - function testSendBall() public { + function testHitBall() public { // 1. receive a ball from 901 to 902 uint256 receiveFromChainId = 901; + uint256 receiveToChainId = 902; + + vm.chainId(receiveToChainId); // Set up the mock for cross-domain message sender validation PingPongBall memory _ball = PingPongBall(1, receiveFromChainId, sally); @@ -117,7 +124,7 @@ contract CrossChainPingPongTest is Test { // Expect the BallReceived event vm.expectEmit(true, true, true, true, address(crossChainPingPong)); - emit CrossChainPingPong.BallReceived(receiveFromChainId, _ball); + emit CrossChainPingPong.BallReceived(receiveFromChainId, receiveToChainId, _ball); vm.prank(MESSENGER); crossChainPingPong.receiveBall(_ball); @@ -141,11 +148,11 @@ contract CrossChainPingPongTest is Test { ); vm.expectEmit(true, true, true, true, address(crossChainPingPong)); - emit CrossChainPingPong.BallSent(sendToChainId, _newBall); + emit CrossChainPingPong.BallSent(sendFromChainId, sendToChainId, _newBall); // Send the ball vm.prank(bob); - crossChainPingPong.sendBall(sendToChainId); + crossChainPingPong.hitBallTo(sendToChainId); } // Test receiving ball with an invalid cross-chain message sender diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..b7006d920 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# Supersim docs developer guide + +Deployed docs can be found at https://supersim.pages.dev + + +Supersim docs are built using [mdbook](https://rust-lang.github.io/mdBook/). `mdbook` uses the `SUMMARY.md` file for the high level hierarchy ([docs](https://rust-lang.github.io/mdBook/format/summary.html)). + +## Development + +### 1. Install `mdbook` CLI tool +Installation options can be found [here](https://rust-lang.github.io/mdBook/guide/installation.html). + +### 2. Go to the `docs` folder + +```sh +cd docs +``` + +### 3. Run the `mdbook` CLI tool +By default the built book is available at http://localhost:3000 + +```sh +mdbook serve --open +``` + +## Deployment +1. On every merge to `main`, [`deploy-docs`](../.github/workflows/deploy-docs.yml) workflow creates a deployable branch for the docs called [`gh-pages`](https://github.com/ethereum-optimism/supersim/tree/gh-pages). +2. Generated docs branch is then deployed using [Cloudflare Pages](https://pages.cloudflare.com/) \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index bfb4669f2..6d97cb7ff 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -25,8 +25,8 @@ - [Interoperability](./guides/interop/README.md) - [Manually relaying interop messages with cast](./guides/interop/manually-relaying-interop-messages-cast.md) - [Using viem to relay interop messages (TypeScript)](./guides/interop/relay-using-viem.md) + - [Writing cross-chain contract using `L2ToL2CrossDomainMessenger`](./guides/interop/writing-contract-using-l2cdm.md) - [Calling a contract on destination chain]() - - [Writing a contract that uses `L2ToL2CrossDomainMessenger`]() - [Bridging SuperchainWETH]() # Examples diff --git a/docs/src/guides/interop/writing-contract-using-l2cdm.md b/docs/src/guides/interop/writing-contract-using-l2cdm.md new file mode 100644 index 000000000..da03cbd53 --- /dev/null +++ b/docs/src/guides/interop/writing-contract-using-l2cdm.md @@ -0,0 +1 @@ +# Writing cross-chain contract using L2ToL2CrossDomainMessenger