From 57868a3acf7152df223abe18c60112f7eb101fd8 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 --- contracts/src/CrossChainPingPong.sol | 112 +++++++++++++++++------- contracts/test/CrossChainPingPong.t.sol | 26 +++--- 2 files changed, 98 insertions(+), 40 deletions(-) diff --git a/contracts/src/CrossChainPingPong.sol b/contracts/src/CrossChainPingPong.sol index 5ec10d03..0dfefcc2 100644 --- a/contracts/src/CrossChainPingPong.sol +++ b/contracts/src/CrossChainPingPong.sol @@ -14,57 +14,95 @@ struct PingPongBall { address lastHitterAddress; } +/** + * @title CrossChainPingPong + * @notice This contract implements a cross-chain ping-pong using the L2ToL2CrossDomainMessenger. + * Players hit a *ball* back and forth between chains, with each hit increasing the rally count. + */ contract CrossChainPingPong { - event BallSent(uint256 indexed toChainId, PingPongBall ball); - - event BallReceived(uint256 indexed fromChainId, 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; - mapping(uint256 => bool) internal allowedChainIds; + /// @notice Chain ID of the server (initial ball sender). + uint256 internal immutable SERVER_CHAIN_ID; + + /// @dev Flag indicating if the server has already served the ball. + bool internal _hasServerAlreadyServed; - uint256 internal serverChainId; - bool internal serverAlreadyServed; + /// @dev Mapping to track which chain IDs are allowed in the game. + mapping(uint256 => bool) internal _isChainIdAllowed; - PingPongBall internal receivedBall; + /// @dev The current received ball on this chain. + PingPongBall internal _receivedBall; + /** + * @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. + * @param _serverChainId The chain ID that will act as the server for the first ball serve. + */ 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"); + require(_isChainIdAllowed[_serverChainId], "Invalid first server chain ID"); - 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"); + /** + * @notice Serve the ping-pong ball to a specified destination chain. + * 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 { + require(SERVER_CHAIN_ID == block.chainid, "Cannot serve ball from this chain"); + require(_hasServerAlreadyServed == false, "Ball already served"); require(_isValidDestinationChain(_toChainId), "Invalid destination chain ID"); - serverAlreadyServed = true; + _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 { + /** + * @notice Hit the received ping-pong ball to a specified destination chain. + * @param _toChainId The chain ID to which the ball is hit. + */ + function hitBallTo(uint256 _toChainId) public { require(_isBallOnThisChain(), "Ball is not on this chain"); require(_isValidDestinationChain(_toChainId), "Invalid destination chain ID"); - 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; - _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 +112,38 @@ contract CrossChainPingPong { revert InvalidCrossDomainSender(); } - receivedBall.lastHitterAddress = _ball.lastHitterAddress; - receivedBall.lastHitterChainId = _ball.lastHitterChainId; - receivedBall.rallyCount = _ball.rallyCount; + _receivedBall.lastHitterAddress = _ball.lastHitterAddress; + _receivedBall.lastHitterChainId = _ball.lastHitterChainId; + _receivedBall.rallyCount = _ball.rallyCount; - 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. + * @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); } + /** + * @notice Checks if the ball is currently on this chain. + * @return True if the ball is on this chain, false otherwise. + */ function _isBallOnThisChain() internal view returns (bool) { - // Hack to check if receivedBall has been set - return receivedBall.lastHitterAddress != address(0); + // 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 098cd045..8842f25b 100644 --- a/contracts/test/CrossChainPingPong.t.sol +++ b/contracts/test/CrossChainPingPong.t.sol @@ -38,15 +38,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 +61,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); + 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 +91,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 +99,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 +123,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 +147,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