Skip to content

Commit

Permalink
example: clean up code for CrossChainPingPong and add walkthrough
Browse files Browse the repository at this point in the history
  • Loading branch information
jakim929 committed Sep 17, 2024
1 parent 17fa933 commit 57868a3
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 40 deletions.
112 changes: 82 additions & 30 deletions contracts/src/CrossChainPingPong.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
}
26 changes: 16 additions & 10 deletions contracts/test/CrossChainPingPong.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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(
Expand All @@ -88,17 +91,20 @@ 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);
crossChainPingPong.receiveBall(_ball);
}

// 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);
Expand All @@ -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);
Expand All @@ -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
Expand Down

0 comments on commit 57868a3

Please sign in to comment.