-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #146 from ethereum-optimism/09-16-example_add_cros…
…schainpingpong example: add CrossChainPingPong.sol
- Loading branch information
Showing
3 changed files
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |