Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

example: add CrossChainPingPong.sol #146

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
jakim929 marked this conversation as resolved.
Show resolved Hide resolved
}

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);
}
}
Loading