Skip to content

Commit

Permalink
Merge pull request #146 from ethereum-optimism/09-16-example_add_cros…
Browse files Browse the repository at this point in the history
…schainpingpong

example: add CrossChainPingPong.sol
  • Loading branch information
jakim929 authored Sep 17, 2024
2 parents 073392f + b096e3d commit 17fa933
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 0 deletions.
29 changes: 29 additions & 0 deletions contracts/script/DeployCrossChainPingPong.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Script, console} from "forge-std/Script.sol";
import {CrossChainPingPong} from "../src/CrossChainPingPong.sol";

contract DeployCrossChainPingPong is Script {
function setUp() public {}

function _salt() internal pure returns (bytes32) {
return bytes32(0);
}

function run() public {
uint256[] memory allowedChains = new uint256[](2);
allowedChains[0] = 901;
allowedChains[1] = 902;

uint256 serverChainId = allowedChains[0];

vm.startBroadcast();

address crossChainPingPong = address(new CrossChainPingPong{salt: _salt()}(allowedChains, serverChainId));

vm.stopBroadcast();

console.log("Deployed CrossChainPingPong at address: ", crossChainPingPong);
}
}
97 changes: 97 additions & 0 deletions contracts/src/CrossChainPingPong.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";

error CallerNotL2ToL2CrossDomainMessenger();

error InvalidCrossDomainSender();

struct PingPongBall {
uint256 rallyCount;
uint256 lastHitterChainId;
address lastHitterAddress;
}

contract CrossChainPingPong {
event BallSent(uint256 indexed toChainId, PingPongBall ball);

event BallReceived(uint256 indexed fromChainId, PingPongBall ball);

address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;

mapping(uint256 => bool) internal allowedChainIds;

uint256 internal serverChainId;
bool internal serverAlreadyServed;

PingPongBall internal receivedBall;

constructor(uint256[] memory _allowedChainIds, uint256 _serverChainId) {
for (uint256 i = 0; i < _allowedChainIds.length; i++) {
allowedChainIds[_allowedChainIds[i]] = true;
}

require(allowedChainIds[_serverChainId], "Invalid first server chain ID");

serverChainId = _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;

PingPongBall memory _newBall = PingPongBall(1, block.chainid, msg.sender);

_sendBallMessage(_newBall, _toChainId);

emit BallSent(_toChainId, _newBall);
}

function sendBall(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);

delete receivedBall;

_sendBallMessage(_newBall, _toChainId);

emit BallSent(_toChainId, _newBall);
}

function receiveBall(PingPongBall memory _ball) external {
if (msg.sender != MESSENGER) {
revert CallerNotL2ToL2CrossDomainMessenger();
}

if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) {
revert InvalidCrossDomainSender();
}

receivedBall.lastHitterAddress = _ball.lastHitterAddress;
receivedBall.lastHitterChainId = _ball.lastHitterChainId;
receivedBall.rallyCount = _ball.rallyCount;

emit BallReceived(IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(), _ball);
}

function _sendBallMessage(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);
}

function _isValidDestinationChain(uint256 _toChainId) internal view returns (bool) {
return allowedChainIds[_toChainId] && _toChainId != block.chainid;
}
}
176 changes: 176 additions & 0 deletions contracts/test/CrossChainPingPong.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {Test} from "forge-std/Test.sol";
import {
CrossChainPingPong,
PingPongBall,
CallerNotL2ToL2CrossDomainMessenger,
InvalidCrossDomainSender
} from "../src/CrossChainPingPong.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";

contract CrossChainPingPongTest is Test {
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;

CrossChainPingPong public crossChainPingPong;

address bob;
address sally;

// Helper function to mock and expect a call
function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal {
vm.mockCall(_receiver, _calldata, _returned);
vm.expectCall(_receiver, _calldata);
}

function setUp() public {
// Setting up allowed chains and server chain for testing
uint256[] memory allowedChains = new uint256[](2);
allowedChains[0] = 901;
allowedChains[1] = 902;

crossChainPingPong = new CrossChainPingPong(allowedChains, 901);

bob = vm.addr(1);
sally = vm.addr(2);
}

// Test serving the ball
function testServeBall() public {
uint256 toChainId = 902;

vm.chainId(901);

// 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);

// Mock cross-chain message send call
bytes memory _message = abi.encodeCall(crossChainPingPong.receiveBall, (_expectedBall));
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(
IL2ToL2CrossDomainMessenger.sendMessage.selector, toChainId, address(crossChainPingPong), _message
),
abi.encode("")
);

// Serve the ball
vm.prank(bob);
crossChainPingPong.serveBall(toChainId);

// Ensure serve can only happen once
vm.expectRevert("Ball already served");
crossChainPingPong.serveBall(toChainId);
}

// Test receiving the ball from a valid cross-chain message
function testReceiveBall() public {
uint256 fromChainId = 901;

// Set up the mock for cross-domain message sender validation
PingPongBall memory _ball = PingPongBall(1, fromChainId, address(this));
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(crossChainPingPong))
);

// Set up cross-domain message source mock
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector),
abi.encode(fromChainId)
);

// Expect the BallReceived event
vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallReceived(fromChainId, _ball);

// Call receiveBall as if from the messenger
vm.prank(MESSENGER);
crossChainPingPong.receiveBall(_ball);
}

// Test receiving then sending the ball
function testSendBall() public {
// 1. receive a ball from 901 to 902
uint256 receiveFromChainId = 901;

// Set up the mock for cross-domain message sender validation
PingPongBall memory _ball = PingPongBall(1, receiveFromChainId, sally);
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(crossChainPingPong))
);

// Set up cross-domain message source mock
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector),
abi.encode(receiveFromChainId)
);

// Expect the BallReceived event
vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallReceived(receiveFromChainId, _ball);

vm.prank(MESSENGER);
crossChainPingPong.receiveBall(_ball);

// 2. send a ball from 902 to 901

uint256 sendFromChainId = 902;
uint256 sendToChainId = 901;
vm.chainId(sendFromChainId);

// Set up the expected event and mock
PingPongBall memory _newBall = PingPongBall(2, sendFromChainId, bob);

bytes memory _message = abi.encodeCall(crossChainPingPong.receiveBall, (_newBall));
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(
IL2ToL2CrossDomainMessenger.sendMessage.selector, sendToChainId, address(crossChainPingPong), _message
),
abi.encode("")
);

vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallSent(sendToChainId, _newBall);

// Send the ball
vm.prank(bob);
crossChainPingPong.sendBall(sendToChainId);
}

// Test receiving ball with an invalid cross-chain message sender
function testReceiveBallInvalidSender() public {
PingPongBall memory _ball = PingPongBall(1, block.chainid, address(this));

// Expect revert due to invalid sender
vm.expectRevert(CallerNotL2ToL2CrossDomainMessenger.selector);
crossChainPingPong.receiveBall(_ball);
}

// Test receiving ball with an invalid cross-domain source
function testReceiveBallInvalidCrossDomainSender() public {
PingPongBall memory _ball = PingPongBall(1, block.chainid, address(this));

// Mock the cross-domain message sender to be invalid
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(0xdeadbeef)) // Invalid address
);

// Expect revert due to invalid cross-domain sender
vm.expectRevert(InvalidCrossDomainSender.selector);
vm.prank(MESSENGER);
crossChainPingPong.receiveBall(_ball);
}
}

0 comments on commit 17fa933

Please sign in to comment.