From 7dbdfe8c0a831105a82b2c0fd76795675b64bc63 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 10 May 2024 16:20:09 +0900 Subject: [PATCH 01/42] adding cctp support --- src/CCTPReceiver.sol | 56 +++++++++++++++ src/XChainForwarders.sol | 63 +++++++++++++++++ src/testing/CircleCCTPDomain.sol | 94 ++++++++++++++++++++++++ test/CCTPIntegration.t.sol | 118 +++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 src/CCTPReceiver.sol create mode 100644 src/testing/CircleCCTPDomain.sol create mode 100644 test/CCTPIntegration.t.sol diff --git a/src/CCTPReceiver.sol b/src/CCTPReceiver.sol new file mode 100644 index 0000000..8c5c9b5 --- /dev/null +++ b/src/CCTPReceiver.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +/** + * @title CCTPReceiver + * @notice Receive messages from CCTP-style bridge. + */ +abstract contract CCTPReceiver { + + address public immutable l2CrossDomain; + uint32 public immutable sourceDomain; + address public immutable l1Authority; + + constructor( + address _l2CrossDomain, + uint32 _sourceDomain, + address _l1Authority + ) { + l2CrossDomain = _l2CrossDomain; + sourceDomain = _sourceDomain; + l1Authority = _l1Authority; + } + + function _getL1MessageSender() internal view returns (address) { + return l1Authority; + } + + function _onlyCrossChainMessage() internal view { + require(msg.sender == address(this), "Receiver/invalid-sender"); + } + + modifier onlyCrossChainMessage() { + _onlyCrossChainMessage(); + _; + } + + function handleReceiveMessage( + uint32 _sourceDomain, + bytes32 sender, + bytes calldata messageBody + ) external returns (bool) { + require(msg.sender == l2CrossDomain, "Receiver/invalid-sender"); + require(_sourceDomain == sourceDomain, "Receiver/invalid-sourceDomain"); + require(sender == bytes32(uint256(uint160(l1Authority))), "Receiver/invalid-l1Authority"); + + (bool success, bytes memory ret) = address(this).call(messageBody); + if (!success) { + assembly { + revert(add(ret, 0x20), mload(ret)) + } + } + + return true; + } + +} diff --git a/src/XChainForwarders.sol b/src/XChainForwarders.sol index d0c60b2..e39fa15 100644 --- a/src/XChainForwarders.sol +++ b/src/XChainForwarders.sol @@ -32,6 +32,14 @@ interface ICrossDomainZkEVM { ) external payable; } +interface ICrossDomainCircleCCTP { + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external; +} + /** * @title XChainForwarders * @notice Helper functions to abstract over L1 -> L2 message passing. @@ -196,4 +204,59 @@ library XChainForwarders { ); } + /// ================================ CCTP ================================ + + function sendMessageCCTP( + address l1CrossDomain, + uint32 destinationDomain, + bytes32 recipient, + bytes memory messageBody + ) internal { + ICrossDomainCircleCCTP(l1CrossDomain).sendMessage( + destinationDomain, + recipient, + messageBody + ); + } + + function sendMessageCCTP( + address l1CrossDomain, + uint32 destinationDomain, + address recipient, + bytes memory messageBody + ) internal { + sendMessageCCTP( + l1CrossDomain, + destinationDomain, + bytes32(uint256(uint160(recipient))), + messageBody + ); + } + + function sendMessageCircleCCTP( + uint32 destinationDomain, + bytes32 recipient, + bytes memory messageBody + ) internal { + sendMessageCCTP( + 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81, + destinationDomain, + recipient, + messageBody + ); + } + + function sendMessageCircleCCTP( + uint32 destinationDomain, + address recipient, + bytes memory messageBody + ) internal { + sendMessageCCTP( + 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81, + destinationDomain, + bytes32(uint256(uint160(recipient))), + messageBody + ); + } + } diff --git a/src/testing/CircleCCTPDomain.sol b/src/testing/CircleCCTPDomain.sol new file mode 100644 index 0000000..00ca37d --- /dev/null +++ b/src/testing/CircleCCTPDomain.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { StdChains } from "forge-std/StdChains.sol"; +import { Vm } from "forge-std/Vm.sol"; + +import { Domain, BridgedDomain } from "./BridgedDomain.sol"; +import { RecordedLogs } from "./RecordedLogs.sol"; + +interface MessengerLike { + function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); +} + +contract CircleCCTPDomain is BridgedDomain { + + bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); + + MessengerLike public constant L1_MESSENGER = MessengerLike(0x0a992d191DEeC32aFe36203Ad87D7d289a738F81); + MessengerLike public L2_MESSENGER; + + uint256 internal lastFromHostLogIndex; + uint256 internal lastToHostLogIndex; + + constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { + bytes32 name = keccak256(bytes(_chain.chainAlias)); + if (name == keccak256("avalanche")) { + L2_MESSENGER = MessengerLike(0x8186359aF5F57FbB40c6b14A588d2A59C0C29880); + } else if (name == keccak256("optimism")) { + L2_MESSENGER = MessengerLike(0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8); + } else if (name == keccak256("arbitrum_one")) { + L2_MESSENGER = MessengerLike(0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca); + } else if (name == keccak256("base")) { + L2_MESSENGER = MessengerLike(0xAD09780d193884d503182aD4588450C416D6F9D4); + } else if (name == keccak256("polygon")) { + L2_MESSENGER = MessengerLike(0xF3be9355363857F3e001be68856A2f96b4C39Ba9); + } else { + revert("Unsupported chain"); + } + + selectFork(); + + // Set minimum required signatures to zero + vm.store( + address(L2_MESSENGER), + bytes32(uint256(4)), + 0 + ); + + hostDomain.selectFork(); + + vm.store( + address(L1_MESSENGER), + bytes32(uint256(4)), + 0 + ); + + vm.recordLogs(); + } + + function relayFromHost(bool switchToGuest) external override { + selectFork(); + + // Read all L1 -> L2 messages and relay them under CCTP fork + Vm.Log[] memory logs = RecordedLogs.getLogs(); + for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { + Vm.Log memory log = logs[lastFromHostLogIndex]; + if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L1_MESSENGER)) { + L2_MESSENGER.receiveMessage(log.data, ""); + } + } + + if (!switchToGuest) { + hostDomain.selectFork(); + } + } + + function relayToHost(bool switchToHost) external override { + hostDomain.selectFork(); + + // Read all L2 -> L1 messages and relay them under host fork + Vm.Log[] memory logs = RecordedLogs.getLogs(); + for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { + Vm.Log memory log = logs[lastToHostLogIndex]; + if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L2_MESSENGER)) { + L1_MESSENGER.receiveMessage(log.data, ""); + } + } + + if (!switchToHost) { + selectFork(); + } + } + +} diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol new file mode 100644 index 0000000..4391ff9 --- /dev/null +++ b/test/CCTPIntegration.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import "./IntegrationBase.t.sol"; + +import { CircleCCTPDomain } from "../src/testing/CircleCCTPDomain.sol"; + +import { CCTPReceiver } from "../src/CCTPReceiver.sol"; + +contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { + + constructor( + address _l2CrossDomain, + uint32 _chainId, + address _l1Authority + ) CCTPReceiver( + _l2CrossDomain, + _chainId, + _l1Authority + ) {} + + function push(uint256 messageId) public override onlyCrossChainMessage { + super.push(messageId); + } + +} + +contract CircleCCTPIntegrationTest is IntegrationBaseTest { + + function test_optimism() public { + CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("optimism"), mainnet); + checkCircleCCTPStyle(cctp, 2); + } + + function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 guestDomain) public { + Domain host = cctp.hostDomain(); + + host.selectFork(); + + MessageOrdering moHost = new MessageOrdering(); + + cctp.selectFork(); + + MessageOrderingCCTP moCCTP = new MessageOrderingCCTP( + address(cctp.L2_MESSENGER()), + 0, // Ethereum + l1Authority + ); + + // Queue up some L2 -> L1 messages + XChainForwarders.sendMessageCCTP( + address(cctp.L2_MESSENGER()), + 0, // Ethereum + address(moHost), + abi.encodeWithSelector(MessageOrdering.push.selector, 3) + ); + XChainForwarders.sendMessageCCTP( + address(cctp.L2_MESSENGER()), + 0, + address(moHost), + abi.encodeWithSelector(MessageOrdering.push.selector, 4) + ); + + assertEq(moCCTP.length(), 0); + + // Do not relay right away + host.selectFork(); + + // Queue up two more L1 -> L2 messages + vm.startPrank(l1Authority); + XChainForwarders.sendMessageCircleCCTP( + guestDomain, + address(moCCTP), + abi.encodeWithSelector(MessageOrdering.push.selector, 1) + ); + XChainForwarders.sendMessageCircleCCTP( + guestDomain, + address(moCCTP), + abi.encodeWithSelector(MessageOrdering.push.selector, 2) + ); + vm.stopPrank(); + + assertEq(moHost.length(), 0); + + cctp.relayFromHost(true); + + assertEq(moCCTP.length(), 2); + assertEq(moCCTP.messages(0), 1); + assertEq(moCCTP.messages(1), 2); + + cctp.relayToHost(true); + + assertEq(moHost.length(), 2); + assertEq(moHost.messages(0), 3); + assertEq(moHost.messages(1), 4); + + return; + + // Validate the message receiver failure modes + vm.startPrank(notL1Authority); + XChainForwarders.sendMessageCircleCCTP( + guestDomain, + address(moCCTP), + abi.encodeWithSelector(MessageOrdering.push.selector, 999) + ); + vm.stopPrank(); + + vm.expectRevert("handleReceiveMessage() failed"); + cctp.relayFromHost(true); + + cctp.selectFork(); + vm.expectRevert("Receiver/invalid-sender"); + moCCTP.push(999); + + // TODO test the source domain doesn't match will revert + } + +} From cf2c3c11fe7d4d77d8e27cd5b5ff781c890beb1a Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 10 May 2024 17:04:27 +0900 Subject: [PATCH 02/42] complete circle cctp --- src/testing/CircleCCTPDomain.sol | 17 ++++++++----- test/CCTPIntegration.t.sol | 41 ++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/testing/CircleCCTPDomain.sol b/src/testing/CircleCCTPDomain.sol index 00ca37d..c4be813 100644 --- a/src/testing/CircleCCTPDomain.sol +++ b/src/testing/CircleCCTPDomain.sol @@ -37,17 +37,14 @@ contract CircleCCTPDomain is BridgedDomain { revert("Unsupported chain"); } + // Set minimum required signatures to zero for both domains selectFork(); - - // Set minimum required signatures to zero vm.store( address(L2_MESSENGER), bytes32(uint256(4)), 0 ); - hostDomain.selectFork(); - vm.store( address(L1_MESSENGER), bytes32(uint256(4)), @@ -65,7 +62,7 @@ contract CircleCCTPDomain is BridgedDomain { for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { Vm.Log memory log = logs[lastFromHostLogIndex]; if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L1_MESSENGER)) { - L2_MESSENGER.receiveMessage(log.data, ""); + L2_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); } } @@ -82,7 +79,7 @@ contract CircleCCTPDomain is BridgedDomain { for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { Vm.Log memory log = logs[lastToHostLogIndex]; if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L2_MESSENGER)) { - L1_MESSENGER.receiveMessage(log.data, ""); + L1_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); } } @@ -91,4 +88,12 @@ contract CircleCCTPDomain is BridgedDomain { } } + function removeFirst64Bytes(bytes memory inputData) public pure returns (bytes memory) { + bytes memory returnValue = new bytes(inputData.length - 64); + for (uint256 i = 0; i < inputData.length - 64; i++) { + returnValue[i] = inputData[i + 64]; + } + return returnValue; + } + } diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index 4391ff9..7ca0ac5 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -11,11 +11,11 @@ contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { constructor( address _l2CrossDomain, - uint32 _chainId, + uint32 _sourceDomain, address _l1Authority ) CCTPReceiver( _l2CrossDomain, - _chainId, + _sourceDomain, _l1Authority ) {} @@ -32,34 +32,51 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { checkCircleCCTPStyle(cctp, 2); } + function test_arbitrum_one() public { + CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("arbitrum_one"), mainnet); + checkCircleCCTPStyle(cctp, 3); + } + + function test_base() public { + CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("base"), mainnet); + checkCircleCCTPStyle(cctp, 6); + } + function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 guestDomain) public { Domain host = cctp.hostDomain(); + uint32 hostDomain = 0; // Ethereum host.selectFork(); - MessageOrdering moHost = new MessageOrdering(); + MessageOrderingCCTP moHost = new MessageOrderingCCTP( + address(cctp.L1_MESSENGER()), + guestDomain, + l1Authority + ); cctp.selectFork(); MessageOrderingCCTP moCCTP = new MessageOrderingCCTP( address(cctp.L2_MESSENGER()), - 0, // Ethereum + hostDomain, l1Authority ); // Queue up some L2 -> L1 messages + vm.startPrank(l1Authority); XChainForwarders.sendMessageCCTP( address(cctp.L2_MESSENGER()), - 0, // Ethereum + hostDomain, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); XChainForwarders.sendMessageCCTP( address(cctp.L2_MESSENGER()), - 0, + hostDomain, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4) ); + vm.stopPrank(); assertEq(moCCTP.length(), 0); @@ -94,8 +111,6 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { assertEq(moHost.messages(0), 3); assertEq(moHost.messages(1), 4); - return; - // Validate the message receiver failure modes vm.startPrank(notL1Authority); XChainForwarders.sendMessageCircleCCTP( @@ -105,14 +120,20 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { ); vm.stopPrank(); - vm.expectRevert("handleReceiveMessage() failed"); + vm.expectRevert("Receiver/invalid-l1Authority"); cctp.relayFromHost(true); cctp.selectFork(); vm.expectRevert("Receiver/invalid-sender"); moCCTP.push(999); - // TODO test the source domain doesn't match will revert + vm.expectRevert("Receiver/invalid-sender"); + moCCTP.handleReceiveMessage(0, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); + + assertEq(moCCTP.sourceDomain(), 0); + vm.prank(address(cctp.L2_MESSENGER())); + vm.expectRevert("Receiver/invalid-sourceDomain"); + moCCTP.handleReceiveMessage(1, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); } } From ca6a389c088042ad13ce4147f9d7d0473c7e7fbe Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 10 May 2024 20:11:44 +0900 Subject: [PATCH 03/42] remove get sender function as its pointless in this style of callback --- src/CCTPReceiver.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/CCTPReceiver.sol b/src/CCTPReceiver.sol index 8c5c9b5..30dc322 100644 --- a/src/CCTPReceiver.sol +++ b/src/CCTPReceiver.sol @@ -21,10 +21,6 @@ abstract contract CCTPReceiver { l1Authority = _l1Authority; } - function _getL1MessageSender() internal view returns (address) { - return l1Authority; - } - function _onlyCrossChainMessage() internal view { require(msg.sender == address(this), "Receiver/invalid-sender"); } From ede7a789aa2f7d816763fefcf529a6f1260ed305 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 10 May 2024 22:01:38 +0900 Subject: [PATCH 04/42] add polygon and avalanche support --- .github/workflows/ci.yml | 2 ++ test/CCTPIntegration.t.sol | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 283f602..250fb1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} + POLYGON_RPC_URL: ${{secrets.POLYGON_RPC_URL}} run: FOUNDRY_PROFILE=ci forge test coverage: @@ -58,6 +59,7 @@ jobs: ARBITRUM_NOVA_RPC_URL: ${{secrets.ARBITRUM_NOVA_RPC_URL}} GNOSIS_CHAIN_RPC_URL: ${{secrets.GNOSIS_CHAIN_RPC_URL}} BASE_RPC_URL: ${{secrets.BASE_RPC_URL}} + POLYGON_RPC_URL: ${{secrets.POLYGON_RPC_URL}} run: forge coverage --report summary --report lcov # To ignore coverage for certain directories modify the paths in this step as needed. The diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index 7ca0ac5..01db0ee 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -27,6 +27,11 @@ contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { contract CircleCCTPIntegrationTest is IntegrationBaseTest { + function test_avalanche() public { + CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("avalanche"), mainnet); + checkCircleCCTPStyle(cctp, 1); + } + function test_optimism() public { CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("optimism"), mainnet); checkCircleCCTPStyle(cctp, 2); @@ -42,6 +47,11 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { checkCircleCCTPStyle(cctp, 6); } + function test_polygon() public { + CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("polygon"), mainnet); + checkCircleCCTPStyle(cctp, 7); + } + function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 guestDomain) public { Domain host = cctp.hostDomain(); uint32 hostDomain = 0; // Ethereum From 8e5b368cb1feeb6bd66b4f74ba0c0f8acbae7ece Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Tue, 14 May 2024 13:42:12 +0900 Subject: [PATCH 05/42] use more general language for cctp domain --- src/testing/CircleCCTPDomain.sol | 51 ++++++++++++++++++-------------- test/CCTPIntegration.t.sol | 10 +++---- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/testing/CircleCCTPDomain.sol b/src/testing/CircleCCTPDomain.sol index c4be813..9c1a82a 100644 --- a/src/testing/CircleCCTPDomain.sol +++ b/src/testing/CircleCCTPDomain.sol @@ -15,38 +15,26 @@ contract CircleCCTPDomain is BridgedDomain { bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); - MessengerLike public constant L1_MESSENGER = MessengerLike(0x0a992d191DEeC32aFe36203Ad87D7d289a738F81); - MessengerLike public L2_MESSENGER; + MessengerLike public SOURCE_MESSENGER; + MessengerLike public DESTINATION_MESSENGER; uint256 internal lastFromHostLogIndex; uint256 internal lastToHostLogIndex; constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - bytes32 name = keccak256(bytes(_chain.chainAlias)); - if (name == keccak256("avalanche")) { - L2_MESSENGER = MessengerLike(0x8186359aF5F57FbB40c6b14A588d2A59C0C29880); - } else if (name == keccak256("optimism")) { - L2_MESSENGER = MessengerLike(0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8); - } else if (name == keccak256("arbitrum_one")) { - L2_MESSENGER = MessengerLike(0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca); - } else if (name == keccak256("base")) { - L2_MESSENGER = MessengerLike(0xAD09780d193884d503182aD4588450C416D6F9D4); - } else if (name == keccak256("polygon")) { - L2_MESSENGER = MessengerLike(0xF3be9355363857F3e001be68856A2f96b4C39Ba9); - } else { - revert("Unsupported chain"); - } + SOURCE_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_hostDomain.details().chainAlias)); + DESTINATION_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_chain.chainAlias)); // Set minimum required signatures to zero for both domains selectFork(); vm.store( - address(L2_MESSENGER), + address(DESTINATION_MESSENGER), bytes32(uint256(4)), 0 ); hostDomain.selectFork(); vm.store( - address(L1_MESSENGER), + address(SOURCE_MESSENGER), bytes32(uint256(4)), 0 ); @@ -54,6 +42,25 @@ contract CircleCCTPDomain is BridgedDomain { vm.recordLogs(); } + function _getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { + bytes32 name = keccak256(bytes(chainAlias)); + if (name == keccak256("mainnet")) { + return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; + } else if (name == keccak256("avalanche")) { + return 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; + } else if (name == keccak256("optimism")) { + return 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; + } else if (name == keccak256("arbitrum_one")) { + return 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; + } else if (name == keccak256("base")) { + return 0xAD09780d193884d503182aD4588450C416D6F9D4; + } else if (name == keccak256("polygon")) { + return 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; + } else { + revert("Unsupported chain"); + } + } + function relayFromHost(bool switchToGuest) external override { selectFork(); @@ -61,8 +68,8 @@ contract CircleCCTPDomain is BridgedDomain { Vm.Log[] memory logs = RecordedLogs.getLogs(); for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { Vm.Log memory log = logs[lastFromHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L1_MESSENGER)) { - L2_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); + if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(SOURCE_MESSENGER)) { + DESTINATION_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); } } @@ -78,8 +85,8 @@ contract CircleCCTPDomain is BridgedDomain { Vm.Log[] memory logs = RecordedLogs.getLogs(); for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { Vm.Log memory log = logs[lastToHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L2_MESSENGER)) { - L1_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); + if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(DESTINATION_MESSENGER)) { + SOURCE_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); } } diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index 01db0ee..d04a165 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -59,7 +59,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { host.selectFork(); MessageOrderingCCTP moHost = new MessageOrderingCCTP( - address(cctp.L1_MESSENGER()), + address(cctp.SOURCE_MESSENGER()), guestDomain, l1Authority ); @@ -67,7 +67,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { cctp.selectFork(); MessageOrderingCCTP moCCTP = new MessageOrderingCCTP( - address(cctp.L2_MESSENGER()), + address(cctp.DESTINATION_MESSENGER()), hostDomain, l1Authority ); @@ -75,13 +75,13 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Queue up some L2 -> L1 messages vm.startPrank(l1Authority); XChainForwarders.sendMessageCCTP( - address(cctp.L2_MESSENGER()), + address(cctp.DESTINATION_MESSENGER()), hostDomain, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); XChainForwarders.sendMessageCCTP( - address(cctp.L2_MESSENGER()), + address(cctp.DESTINATION_MESSENGER()), hostDomain, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4) @@ -141,7 +141,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { moCCTP.handleReceiveMessage(0, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); assertEq(moCCTP.sourceDomain(), 0); - vm.prank(address(cctp.L2_MESSENGER())); + vm.prank(address(cctp.DESTINATION_MESSENGER())); vm.expectRevert("Receiver/invalid-sourceDomain"); moCCTP.handleReceiveMessage(1, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); } From d3d7e218dd476983f278c238c3832265d844a820 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Tue, 14 May 2024 13:48:21 +0900 Subject: [PATCH 06/42] rename some of the variables --- src/CCTPReceiver.sol | 26 +++++++++++++------------- test/CCTPIntegration.t.sol | 26 +++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/CCTPReceiver.sol b/src/CCTPReceiver.sol index 30dc322..bebe9f2 100644 --- a/src/CCTPReceiver.sol +++ b/src/CCTPReceiver.sol @@ -7,18 +7,18 @@ pragma solidity ^0.8.0; */ abstract contract CCTPReceiver { - address public immutable l2CrossDomain; - uint32 public immutable sourceDomain; - address public immutable l1Authority; + address public immutable destinationCrossDomain; + uint32 public immutable sourceDomainId; + address public immutable sourceAuthority; constructor( - address _l2CrossDomain, - uint32 _sourceDomain, - address _l1Authority + address _destinationCrossDomain, + uint32 _sourceDomainId, + address _sourceAuthority ) { - l2CrossDomain = _l2CrossDomain; - sourceDomain = _sourceDomain; - l1Authority = _l1Authority; + destinationCrossDomain = _destinationCrossDomain; + sourceDomainId = _sourceDomainId; + sourceAuthority = _sourceAuthority; } function _onlyCrossChainMessage() internal view { @@ -31,13 +31,13 @@ abstract contract CCTPReceiver { } function handleReceiveMessage( - uint32 _sourceDomain, + uint32 sourceDomain, bytes32 sender, bytes calldata messageBody ) external returns (bool) { - require(msg.sender == l2CrossDomain, "Receiver/invalid-sender"); - require(_sourceDomain == sourceDomain, "Receiver/invalid-sourceDomain"); - require(sender == bytes32(uint256(uint160(l1Authority))), "Receiver/invalid-l1Authority"); + require(msg.sender == destinationCrossDomain, "Receiver/invalid-sender"); + require(sourceDomainId == sourceDomain, "Receiver/invalid-sourceDomain"); + require(sender == bytes32(uint256(uint160(sourceAuthority))), "Receiver/invalid-sourceAuthority"); (bool success, bytes memory ret) = address(this).call(messageBody); if (!success) { diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index d04a165..3e74c9b 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -12,11 +12,11 @@ contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { constructor( address _l2CrossDomain, uint32 _sourceDomain, - address _l1Authority + address _sourceAuthority ) CCTPReceiver( _l2CrossDomain, _sourceDomain, - _l1Authority + _sourceAuthority ) {} function push(uint256 messageId) public override onlyCrossChainMessage { @@ -52,15 +52,15 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { checkCircleCCTPStyle(cctp, 7); } - function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 guestDomain) public { + function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 destinationDomainId) public { Domain host = cctp.hostDomain(); - uint32 hostDomain = 0; // Ethereum + uint32 sourceDomainId = 0; // Ethereum host.selectFork(); MessageOrderingCCTP moHost = new MessageOrderingCCTP( address(cctp.SOURCE_MESSENGER()), - guestDomain, + destinationDomainId, l1Authority ); @@ -68,7 +68,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { MessageOrderingCCTP moCCTP = new MessageOrderingCCTP( address(cctp.DESTINATION_MESSENGER()), - hostDomain, + sourceDomainId, l1Authority ); @@ -76,13 +76,13 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { vm.startPrank(l1Authority); XChainForwarders.sendMessageCCTP( address(cctp.DESTINATION_MESSENGER()), - hostDomain, + sourceDomainId, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); XChainForwarders.sendMessageCCTP( address(cctp.DESTINATION_MESSENGER()), - hostDomain, + sourceDomainId, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4) ); @@ -96,12 +96,12 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); XChainForwarders.sendMessageCircleCCTP( - guestDomain, + destinationDomainId, address(moCCTP), abi.encodeWithSelector(MessageOrdering.push.selector, 1) ); XChainForwarders.sendMessageCircleCCTP( - guestDomain, + destinationDomainId, address(moCCTP), abi.encodeWithSelector(MessageOrdering.push.selector, 2) ); @@ -124,13 +124,13 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure modes vm.startPrank(notL1Authority); XChainForwarders.sendMessageCircleCCTP( - guestDomain, + destinationDomainId, address(moCCTP), abi.encodeWithSelector(MessageOrdering.push.selector, 999) ); vm.stopPrank(); - vm.expectRevert("Receiver/invalid-l1Authority"); + vm.expectRevert("Receiver/invalid-sourceAuthority"); cctp.relayFromHost(true); cctp.selectFork(); @@ -140,7 +140,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { vm.expectRevert("Receiver/invalid-sender"); moCCTP.handleReceiveMessage(0, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); - assertEq(moCCTP.sourceDomain(), 0); + assertEq(moCCTP.sourceDomainId(), 0); vm.prank(address(cctp.DESTINATION_MESSENGER())); vm.expectRevert("Receiver/invalid-sourceDomain"); moCCTP.handleReceiveMessage(1, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); From 616c215c114d6a26c15984afd02027e2ee2823aa Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Tue, 14 May 2024 13:51:14 +0900 Subject: [PATCH 07/42] more var renaming; use l2 authority --- src/XChainForwarders.sol | 24 ++++++++++++------------ test/CCTPIntegration.t.sol | 6 ++++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/XChainForwarders.sol b/src/XChainForwarders.sol index e39fa15..01b577f 100644 --- a/src/XChainForwarders.sol +++ b/src/XChainForwarders.sol @@ -207,53 +207,53 @@ library XChainForwarders { /// ================================ CCTP ================================ function sendMessageCCTP( - address l1CrossDomain, - uint32 destinationDomain, + address sourceCrossDomain, + uint32 destinationDomainId, bytes32 recipient, bytes memory messageBody ) internal { - ICrossDomainCircleCCTP(l1CrossDomain).sendMessage( - destinationDomain, + ICrossDomainCircleCCTP(sourceCrossDomain).sendMessage( + destinationDomainId, recipient, messageBody ); } function sendMessageCCTP( - address l1CrossDomain, - uint32 destinationDomain, + address sourceCrossDomain, + uint32 destinationDomainId, address recipient, bytes memory messageBody ) internal { sendMessageCCTP( - l1CrossDomain, - destinationDomain, + sourceCrossDomain, + destinationDomainId, bytes32(uint256(uint160(recipient))), messageBody ); } function sendMessageCircleCCTP( - uint32 destinationDomain, + uint32 destinationDomainId, bytes32 recipient, bytes memory messageBody ) internal { sendMessageCCTP( 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81, - destinationDomain, + destinationDomainId, recipient, messageBody ); } function sendMessageCircleCCTP( - uint32 destinationDomain, + uint32 destinationDomainId, address recipient, bytes memory messageBody ) internal { sendMessageCCTP( 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81, - destinationDomain, + destinationDomainId, bytes32(uint256(uint160(recipient))), messageBody ); diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index 3e74c9b..60783a5 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -27,6 +27,8 @@ contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { contract CircleCCTPIntegrationTest is IntegrationBaseTest { + address l2Authority = makeAddr("l2Authority"); + function test_avalanche() public { CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("avalanche"), mainnet); checkCircleCCTPStyle(cctp, 1); @@ -61,7 +63,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { MessageOrderingCCTP moHost = new MessageOrderingCCTP( address(cctp.SOURCE_MESSENGER()), destinationDomainId, - l1Authority + l2Authority ); cctp.selectFork(); @@ -73,7 +75,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { ); // Queue up some L2 -> L1 messages - vm.startPrank(l1Authority); + vm.startPrank(l2Authority); XChainForwarders.sendMessageCCTP( address(cctp.DESTINATION_MESSENGER()), sourceDomainId, From 506ecc0bfbe2e6fc93f50f126c69122facc3a304 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 17 May 2024 17:42:58 +0900 Subject: [PATCH 08/42] review fixes --- src/CCTPReceiver.sol | 14 +++++++------- src/XChainForwarders.sol | 8 ++++---- src/testing/CircleCCTPDomain.sol | 2 +- test/CCTPIntegration.t.sol | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/CCTPReceiver.sol b/src/CCTPReceiver.sol index bebe9f2..8f6860d 100644 --- a/src/CCTPReceiver.sol +++ b/src/CCTPReceiver.sol @@ -7,18 +7,18 @@ pragma solidity ^0.8.0; */ abstract contract CCTPReceiver { - address public immutable destinationCrossDomain; - uint32 public immutable sourceDomainId; + address public immutable destinationMessenger; + uint32 public immutable sourceDomainId; address public immutable sourceAuthority; constructor( - address _destinationCrossDomain, + address _destinationMessenger, uint32 _sourceDomainId, address _sourceAuthority ) { - destinationCrossDomain = _destinationCrossDomain; - sourceDomainId = _sourceDomainId; - sourceAuthority = _sourceAuthority; + destinationMessenger = _destinationMessenger; + sourceDomainId = _sourceDomainId; + sourceAuthority = _sourceAuthority; } function _onlyCrossChainMessage() internal view { @@ -35,7 +35,7 @@ abstract contract CCTPReceiver { bytes32 sender, bytes calldata messageBody ) external returns (bool) { - require(msg.sender == destinationCrossDomain, "Receiver/invalid-sender"); + require(msg.sender == destinationMessenger, "Receiver/invalid-sender"); require(sourceDomainId == sourceDomain, "Receiver/invalid-sourceDomain"); require(sender == bytes32(uint256(uint160(sourceAuthority))), "Receiver/invalid-sourceAuthority"); diff --git a/src/XChainForwarders.sol b/src/XChainForwarders.sol index 01b577f..4d0e482 100644 --- a/src/XChainForwarders.sol +++ b/src/XChainForwarders.sol @@ -207,12 +207,12 @@ library XChainForwarders { /// ================================ CCTP ================================ function sendMessageCCTP( - address sourceCrossDomain, + address sourceMessenger, uint32 destinationDomainId, bytes32 recipient, bytes memory messageBody ) internal { - ICrossDomainCircleCCTP(sourceCrossDomain).sendMessage( + ICrossDomainCircleCCTP(sourceMessenger).sendMessage( destinationDomainId, recipient, messageBody @@ -220,13 +220,13 @@ library XChainForwarders { } function sendMessageCCTP( - address sourceCrossDomain, + address sourceMessenger, uint32 destinationDomainId, address recipient, bytes memory messageBody ) internal { sendMessageCCTP( - sourceCrossDomain, + sourceMessenger, destinationDomainId, bytes32(uint256(uint160(recipient))), messageBody diff --git a/src/testing/CircleCCTPDomain.sol b/src/testing/CircleCCTPDomain.sol index 9c1a82a..93591ab 100644 --- a/src/testing/CircleCCTPDomain.sol +++ b/src/testing/CircleCCTPDomain.sol @@ -22,7 +22,7 @@ contract CircleCCTPDomain is BridgedDomain { uint256 internal lastToHostLogIndex; constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - SOURCE_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_hostDomain.details().chainAlias)); + SOURCE_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_hostDomain.details().chainAlias)); DESTINATION_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_chain.chainAlias)); // Set minimum required signatures to zero for both domains diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index 60783a5..a3ed28d 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -10,12 +10,12 @@ import { CCTPReceiver } from "../src/CCTPReceiver.sol"; contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { constructor( - address _l2CrossDomain, - uint32 _sourceDomain, + address _destinationMessenger, + uint32 _sourceDomainId, address _sourceAuthority ) CCTPReceiver( - _l2CrossDomain, - _sourceDomain, + _destinationMessenger, + _sourceDomainId, _sourceAuthority ) {} From 9bea858fee4b9a4fd6f4b228dbbfdec936273dba Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sun, 19 May 2024 00:34:35 +0900 Subject: [PATCH 09/42] convert domain into a struct + library; starting to split out bridges from the domains --- src/testing/Domain.sol | 54 ++++++++++++++----- .../ArbitrumNativeBridge.sol} | 45 +++++++++------- src/testing/bridges/IBidirectionalBridge.sol | 8 +++ src/testing/bridges/IUnidirectionalBridge.sol | 6 +++ src/testing/{ => utils}/RecordedLogs.sol | 0 5 files changed, 81 insertions(+), 32 deletions(-) rename src/testing/{ArbitrumDomain.sol => bridges/ArbitrumNativeBridge.sol} (82%) create mode 100644 src/testing/bridges/IBidirectionalBridge.sol create mode 100644 src/testing/bridges/IUnidirectionalBridge.sol rename src/testing/{ => utils}/RecordedLogs.sol (100%) diff --git a/src/testing/Domain.sol b/src/testing/Domain.sol index 6d48bf8..d3ac82e 100644 --- a/src/testing/Domain.sol +++ b/src/testing/Domain.sol @@ -4,30 +4,56 @@ pragma solidity >=0.8.0; import { StdChains } from "forge-std/StdChains.sol"; import { Vm } from "forge-std/Vm.sol"; -contract Domain { +struct Domain { + StdChains.Chain chain; + uint256 forkId; +} + +library DomainHelpers { Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - StdChains.Chain private _details; - uint256 public forkId; + function createFork(StdChains.Chain memory chain, uint256 blockNumber) internal returns (Domain memory domain) { + domain = Domain({ + chain: chain, + forkId: vm.createFork(chain.rpcUrl, blockNum) + }); + } + + function createFork(StdChains.Chain memory chain) internal returns (Domain memory domain) { + domain = Domain({ + chain: chain, + forkId: vm.createFork(chain.rpcUrl) + }); + } + + function createSelectFork(StdChains.Chain memory chain, uint256 blockNumber) internal returns (Domain memory domain) { + domain = Domain({ + chain: chain, + forkId: vm.createSelectFork(chain.rpcUrl, blockNum) + }); + _assertExpectedRpc(chain); + } - constructor(StdChains.Chain memory _chain) { - _details = _chain; - forkId = vm.createFork(_chain.rpcUrl); - vm.makePersistent(address(this)); + function createSelectFork(StdChains.Chain memory chain) internal returns (Domain memory domain) { + domain = Domain({ + chain: chain, + forkId: vm.createSelectFork(chain.rpcUrl) + }); + _assertExpectedRpc(chain); } - function details() public view returns (StdChains.Chain memory) { - return _details; + function selectFork(Domain memory domain) internal { + vm.selectFork(domain.forkId); + _assertExpectedRpc(domain); } - function selectFork() public { - vm.selectFork(forkId); - require(block.chainid == _details.chainId, string(abi.encodePacked(_details.chainAlias, " is pointing to the wrong RPC endpoint '", _details.rpcUrl, "'"))); + function rollFork(Domain memory domain, uint256 blockNumber) internal { + vm.rollFork(domain.forkId, blockNumber); } - function rollFork(uint256 blocknum) public { - vm.rollFork(forkId, blocknum); + function _assertExpectedRpc(StdChains.Chain memory chain) private { + require(block.chainid == chain.chainId, string(abi.encodePacked(chain.chainAlias, " is pointing to the wrong RPC endpoint '", chain.rpcUrl, "'"))); } } diff --git a/src/testing/ArbitrumDomain.sol b/src/testing/bridges/ArbitrumNativeBridge.sol similarity index 82% rename from src/testing/ArbitrumDomain.sol rename to src/testing/bridges/ArbitrumNativeBridge.sol index a0546db..1feb185 100644 --- a/src/testing/ArbitrumDomain.sol +++ b/src/testing/bridges/ArbitrumNativeBridge.sol @@ -4,8 +4,8 @@ pragma solidity >=0.8.0; import { StdChains } from "forge-std/StdChains.sol"; import { Vm } from "forge-std/Vm.sol"; -import { Domain, BridgedDomain } from "./BridgedDomain.sol"; -import { RecordedLogs } from "./RecordedLogs.sol"; +import { Domain, DomainHelpers } from "src/testing/Domain.sol"; +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; interface InboxLike { function createRetryableTicket( @@ -42,7 +42,9 @@ contract ArbSysOverride { } -contract ArbitrumDomain is BridgedDomain { +contract ArbitrumNativeBridge is IBidirectionalBridge { + + using DomainHelpers for *; bytes32 private constant MESSAGE_DELIVERED_TOPIC = keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"); bytes32 private constant SEND_TO_L1_TOPIC = keccak256("SendTxToL1(address,address,bytes)"); @@ -56,21 +58,28 @@ contract ArbitrumDomain is BridgedDomain { uint256 internal lastFromHostLogIndex; uint256 internal lastToHostLogIndex; - constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - bytes32 name = keccak256(bytes(_chain.chainAlias)); + Domain public source; + Domain public destination; + + constructor(Domain memory _source, Domain memory _destination) { + require(keccak256(bytes(destination.chain.chainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); + + bytes32 name = keccak256(bytes(destination.chain.chainAlias)); if (name == keccak256("arbitrum_one")) { INBOX = InboxLike(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); - } else if (name == keccak256("arbitrum_one_goerli")) { - INBOX = InboxLike(0x6BEbC4925716945D46F0Ec336D5C2564F419682C); } else if (name == keccak256("arbitrum_nova")) { INBOX = InboxLike(0xc4448b71118c9071Bcb9734A0EAc55D18A153949); } else { revert("Unsupported chain"); } - _hostDomain.selectFork(); + source = _source; + destination = _destination; + + source.selectFork(); BRIDGE = BridgeLike(INBOX.bridge()); vm.recordLogs(); + vm.makePersistent(address(this)); // Make this contract a valid outbox address _rollup = BRIDGE.rollup(); @@ -87,7 +96,7 @@ contract ArbitrumDomain is BridgedDomain { ); // Need to replace ArbSys contract with custom code to make it compatible with revm - selectFork(); + destination.selectFork(); bytes memory bytecode = vm.getCode("ArbitrumDomain.sol:ArbSysOverride"); address deployed; assembly { @@ -95,7 +104,7 @@ contract ArbitrumDomain is BridgedDomain { } vm.etch(ARB_SYS, deployed.code); - _hostDomain.selectFork(); + source.selectFork(); } function parseData(bytes memory orig) private pure returns (address target, bytes memory message) { @@ -108,8 +117,8 @@ contract ArbitrumDomain is BridgedDomain { } } - function relayFromHost(bool switchToGuest) external override { - selectFork(); + function relayMessagesToSource(bool switchToDestinationFork) external override { + destination.selectFork(); // Read all L1 -> L2 messages and relay them under Arbitrum fork Vm.Log[] memory logs = RecordedLogs.getLogs(); @@ -131,13 +140,13 @@ contract ArbitrumDomain is BridgedDomain { } } - if (!switchToGuest) { - hostDomain.selectFork(); + if (!switchToDestinationFork) { + source.selectFork(); } } - function relayToHost(bool switchToHost) external override { - hostDomain.selectFork(); + function relayMessagesToSource(bool switchToSourceFork) external override { + source.selectFork(); // Read all L2 -> L1 messages and relay them under host fork Vm.Log[] memory logs = RecordedLogs.getLogs(); @@ -155,8 +164,8 @@ contract ArbitrumDomain is BridgedDomain { } } - if (!switchToHost) { - selectFork(); + if (!switchToSourceFork) { + destination.selectFork(); } } diff --git a/src/testing/bridges/IBidirectionalBridge.sol b/src/testing/bridges/IBidirectionalBridge.sol new file mode 100644 index 0000000..62fdb92 --- /dev/null +++ b/src/testing/bridges/IBidirectionalBridge.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { IUnidirectionalBridge } from "./IUnidirectionalBridge.sol"; + +interface IUnidirectionalBridge is IUnidirectionalBridge { + function relayMessagesToSource(bool switchToSourceFork) external; +} diff --git a/src/testing/bridges/IUnidirectionalBridge.sol b/src/testing/bridges/IUnidirectionalBridge.sol new file mode 100644 index 0000000..756c154 --- /dev/null +++ b/src/testing/bridges/IUnidirectionalBridge.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +interface IUnidirectionalBridge { + function relayMessagesToDestination(bool switchToDestinationFork) external; +} diff --git a/src/testing/RecordedLogs.sol b/src/testing/utils/RecordedLogs.sol similarity index 100% rename from src/testing/RecordedLogs.sol rename to src/testing/utils/RecordedLogs.sol From 418dbb40e46b2616fb9e0053be7970c56f1075f8 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sun, 19 May 2024 01:05:11 +0900 Subject: [PATCH 10/42] more refactoring of bridge --- src/testing/bridges/ArbitrumNativeBridge.sol | 42 +++++-------------- .../CCTPBridge.sol} | 2 +- src/testing/bridges/data/Arbitrum.sol | 31 ++++++++++++++ src/testing/bridges/data/BridgeData.sol | 14 +++++++ 4 files changed, 57 insertions(+), 32 deletions(-) rename src/testing/{CircleCCTPDomain.sol => bridges/CCTPBridge.sol} (98%) create mode 100644 src/testing/bridges/data/Arbitrum.sol create mode 100644 src/testing/bridges/data/BridgeData.sol diff --git a/src/testing/bridges/ArbitrumNativeBridge.sol b/src/testing/bridges/ArbitrumNativeBridge.sol index 1feb185..ca24442 100644 --- a/src/testing/bridges/ArbitrumNativeBridge.sol +++ b/src/testing/bridges/ArbitrumNativeBridge.sol @@ -18,7 +18,7 @@ interface InboxLike { uint256 gasPriceBid, bytes calldata data ) external payable returns (uint256); - function bridge() external view returns (address); + function bridge() external view returns (BridgeLike); } interface BridgeLike { @@ -49,48 +49,28 @@ contract ArbitrumNativeBridge is IBidirectionalBridge { bytes32 private constant MESSAGE_DELIVERED_TOPIC = keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"); bytes32 private constant SEND_TO_L1_TOPIC = keccak256("SendTxToL1(address,address,bytes)"); - address public constant ARB_SYS = 0x0000000000000000000000000000000000000064; - InboxLike public INBOX; - BridgeLike public immutable BRIDGE; - address public l2ToL1Sender; - uint256 internal lastFromHostLogIndex; - uint256 internal lastToHostLogIndex; - - Domain public source; - Domain public destination; - - constructor(Domain memory _source, Domain memory _destination) { - require(keccak256(bytes(destination.chain.chainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); + BridgeData public data; - bytes32 name = keccak256(bytes(destination.chain.chainAlias)); - if (name == keccak256("arbitrum_one")) { - INBOX = InboxLike(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); - } else if (name == keccak256("arbitrum_nova")) { - INBOX = InboxLike(0xc4448b71118c9071Bcb9734A0EAc55D18A153949); - } else { - revert("Unsupported chain"); - } - - source = _source; - destination = _destination; + constructor(BridgeData memory _data) { + data = _data; - source.selectFork(); - BRIDGE = BridgeLike(INBOX.bridge()); + data.source.selectFork(); + BridgeLike bridge = InboxLike(data.sourceCrossChainMessenger).bridge(); vm.recordLogs(); vm.makePersistent(address(this)); // Make this contract a valid outbox - address _rollup = BRIDGE.rollup(); + address _rollup = bridge.rollup(); vm.store( - address(BRIDGE), + address(bridge), bytes32(uint256(8)), bytes32(uint256(uint160(address(this)))) ); - BRIDGE.setOutbox(address(this), true); + bridge.setOutbox(address(this), true); vm.store( - address(BRIDGE), + address(bridge), bytes32(uint256(8)), bytes32(uint256(uint160(_rollup))) ); @@ -155,7 +135,7 @@ contract ArbitrumNativeBridge is IBidirectionalBridge { if (log.topics[0] == SEND_TO_L1_TOPIC) { (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); l2ToL1Sender = sender; - (bool success, bytes memory response) = BRIDGE.executeCall(target, 0, message); + (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); if (!success) { assembly { revert(add(response, 32), mload(response)) diff --git a/src/testing/CircleCCTPDomain.sol b/src/testing/bridges/CCTPBridge.sol similarity index 98% rename from src/testing/CircleCCTPDomain.sol rename to src/testing/bridges/CCTPBridge.sol index 93591ab..e3e6b49 100644 --- a/src/testing/CircleCCTPDomain.sol +++ b/src/testing/bridges/CCTPBridge.sol @@ -11,7 +11,7 @@ interface MessengerLike { function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); } -contract CircleCCTPDomain is BridgedDomain { +contract CCTPBridge is IUnidirectionalBridge { bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); diff --git a/src/testing/bridges/data/Arbitrum.sol b/src/testing/bridges/data/Arbitrum.sol new file mode 100644 index 0000000..15da267 --- /dev/null +++ b/src/testing/bridges/data/Arbitrum.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { BridgeData } from "./BridgeData.sol"; + +library Arbitrum { + + function createArbitrumNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (ArbitrumNativeBridge memory bridge) { + require(keccak256(bytes(ethereum.chain.chainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); + + bytes32 name = keccak256(bytes(arbitrumInstance.chain.chainAlias)); + address inbox; + if (name == keccak256("arbitrum_one")) { + inbox = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; + } else if (name == keccak256("arbitrum_nova")) { + inbox = 0xc4448b71118c9071Bcb9734A0EAc55D18A153949; + } else { + revert("Unsupported destination chain"); + } + + return new ArbitrumNativeBridge(BridgeData({ + source: ethereum, + destination: arbitrumInstance, + sourceCrossChainMessenger: inbox, + destinationCrossChainMessenger: 0x0000000000000000000000000000000000000064, + lastSourceLogIndex: 0, + lastDestinationLogIndex: 0 + })); + } + +} diff --git a/src/testing/bridges/data/BridgeData.sol b/src/testing/bridges/data/BridgeData.sol new file mode 100644 index 0000000..56903c2 --- /dev/null +++ b/src/testing/bridges/data/BridgeData.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { Domain } from "src/testing/Domain.sol"; + +struct BridgeData { + Domain source; + Domain destination; + address sourceCrossChainMessenger; + address destinationCrossChainMessenger; + // These are used internally for log tracking + uint256 lastSourceLogIndex; + uint256 lastDestinationLogIndex; +} From 17c1b3d84e10efe6fb5b0ac16621e939a07034e9 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Mon, 20 May 2024 10:06:38 +0200 Subject: [PATCH 11/42] part way through testing refactor --- src/testing/Domain.sol | 2 +- ...veBridge.sol => ArbitrumBridgeTesting.sol} | 107 +++++++++++------- src/testing/bridges/{data => }/BridgeData.sol | 1 + src/testing/bridges/CCTPBridgeTesting.sol | 94 +++++++++++++++ src/testing/bridges/IBidirectionalBridge.sol | 8 -- src/testing/bridges/IUnidirectionalBridge.sol | 6 - src/testing/bridges/data/Arbitrum.sol | 31 ----- 7 files changed, 165 insertions(+), 84 deletions(-) rename src/testing/bridges/{ArbitrumNativeBridge.sol => ArbitrumBridgeTesting.sol} (57%) rename src/testing/bridges/{data => }/BridgeData.sol (94%) create mode 100644 src/testing/bridges/CCTPBridgeTesting.sol delete mode 100644 src/testing/bridges/IBidirectionalBridge.sol delete mode 100644 src/testing/bridges/IUnidirectionalBridge.sol delete mode 100644 src/testing/bridges/data/Arbitrum.sol diff --git a/src/testing/Domain.sol b/src/testing/Domain.sol index d3ac82e..37b50ba 100644 --- a/src/testing/Domain.sol +++ b/src/testing/Domain.sol @@ -11,7 +11,7 @@ struct Domain { library DomainHelpers { - Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); function createFork(StdChains.Chain memory chain, uint256 blockNumber) internal returns (Domain memory domain) { domain = Domain({ diff --git a/src/testing/bridges/ArbitrumNativeBridge.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol similarity index 57% rename from src/testing/bridges/ArbitrumNativeBridge.sol rename to src/testing/bridges/ArbitrumBridgeTesting.sol index ca24442..b0ffffb 100644 --- a/src/testing/bridges/ArbitrumNativeBridge.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity >=0.8.0; -import { StdChains } from "forge-std/StdChains.sol"; import { Vm } from "forge-std/Vm.sol"; -import { Domain, DomainHelpers } from "src/testing/Domain.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { BridgeData } from "./BridgeData.sol"; interface InboxLike { function createRetryableTicket( @@ -42,63 +41,85 @@ contract ArbSysOverride { } -contract ArbitrumNativeBridge is IBidirectionalBridge { +library ArbitrumBridgeTesting { using DomainHelpers for *; + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + bytes32 private constant MESSAGE_DELIVERED_TOPIC = keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"); bytes32 private constant SEND_TO_L1_TOPIC = keccak256("SendTxToL1(address,address,bytes)"); + + function createNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (BridgeData memory bridge) { + ( + address sourceCrossChainMessenger, + address destinationCrossChainMessenger + ) = getMessengerFromChainAlias(ethereum.chain.chainAlias, arbitrumInstance.chain.chainAlias) + + return init(BridgeData({ + source: ethereum, + destination: arbitrumInstance, + sourceCrossChainMessenger: sourceCrossChainMessenger, + destinationCrossChainMessenger: destinationCrossChainMessenger, + lastSourceLogIndex: 0, + lastDestinationLogIndex: 0, + extraData: "" + })); + } - address public l2ToL1Sender; - - BridgeData public data; - - constructor(BridgeData memory _data) { - data = _data; + function getMessengerFromChainAlias( + string memory sourceChainAlias, + string memory destinationChainAlias + ) internal pure returns ( + address sourceCrossChainMessenger, + address destinationCrossChainMessenger + ) { + require(keccak256(bytes(sourceChainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); + + bytes32 name = keccak256(bytes(destinationChainAlias)); + if (name == keccak256("arbitrum_one")) { + sourceCrossChainMessenger = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; + } else if (name == keccak256("arbitrum_nova")) { + sourceCrossChainMessenger = 0xc4448b71118c9071Bcb9734A0EAc55D18A153949; + } else { + revert("Unsupported destination chain"); + } + destinationCrossChainMessenger = 0x0000000000000000000000000000000000000064; + } - data.source.selectFork(); - BridgeLike bridge = InboxLike(data.sourceCrossChainMessenger).bridge(); + function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { + bridge.source.selectFork(); + BridgeLike underlyingBridge = InboxLike(data.sourceCrossChainMessenger).bridge(); vm.recordLogs(); - vm.makePersistent(address(this)); // Make this contract a valid outbox - address _rollup = bridge.rollup(); + address _rollup = underlyingBridge.rollup(); vm.store( - address(bridge), + address(underlyingBridge), bytes32(uint256(8)), bytes32(uint256(uint160(address(this)))) ); - bridge.setOutbox(address(this), true); + underlyingBridge.setOutbox(address(this), true); vm.store( - address(bridge), + address(underlyingBridge), bytes32(uint256(8)), bytes32(uint256(uint160(_rollup))) ); // Need to replace ArbSys contract with custom code to make it compatible with revm - destination.selectFork(); - bytes memory bytecode = vm.getCode("ArbitrumDomain.sol:ArbSysOverride"); + bridge.destination.selectFork(); + bytes memory bytecode = vm.getCode("ArbitrumBridgeTesting.sol:ArbSysOverride"); address deployed; assembly { deployed := create(0, add(bytecode, 0x20), mload(bytecode)) } vm.etch(ARB_SYS, deployed.code); - source.selectFork(); - } - - function parseData(bytes memory orig) private pure returns (address target, bytes memory message) { - // FIXME - this is not robust enough, only handling messages of a specific format - uint256 mlen; - (,,target ,,,,,,,, mlen) = abi.decode(orig, (uint256, uint256, address, uint256, uint256, uint256, address, address, uint256, uint256, uint256)); - message = new bytes(mlen); - for (uint256 i = 0; i < mlen; i++) { - message[i] = orig[i + 352]; - } + bridge.source.selectFork(); } - function relayMessagesToSource(bool switchToDestinationFork) external override { - destination.selectFork(); + function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + bridge.destination.selectFork(); // Read all L1 -> L2 messages and relay them under Arbitrum fork Vm.Log[] memory logs = RecordedLogs.getLogs(); @@ -108,7 +129,7 @@ contract ArbitrumNativeBridge is IBidirectionalBridge { // We need both the current event and the one that follows for all the relevant data Vm.Log memory logWithData = logs[lastFromHostLogIndex + 1]; (,, address sender,,,) = abi.decode(log.data, (address, uint8, address, bytes32, uint256, uint64)); - (address target, bytes memory message) = parseData(logWithData.data); + (address target, bytes memory message) = _parseData(logWithData.data); vm.startPrank(sender); (bool success, bytes memory response) = target.call(message); vm.stopPrank(); @@ -121,12 +142,12 @@ contract ArbitrumNativeBridge is IBidirectionalBridge { } if (!switchToDestinationFork) { - source.selectFork(); + bridge.source.selectFork(); } } - function relayMessagesToSource(bool switchToSourceFork) external override { - source.selectFork(); + function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + bridge.source.selectFork(); // Read all L2 -> L1 messages and relay them under host fork Vm.Log[] memory logs = RecordedLogs.getLogs(); @@ -134,7 +155,7 @@ contract ArbitrumNativeBridge is IBidirectionalBridge { Vm.Log memory log = logs[lastToHostLogIndex]; if (log.topics[0] == SEND_TO_L1_TOPIC) { (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); - l2ToL1Sender = sender; + //l2ToL1Sender = sender; (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); if (!success) { assembly { @@ -145,7 +166,17 @@ contract ArbitrumNativeBridge is IBidirectionalBridge { } if (!switchToSourceFork) { - destination.selectFork(); + bridge.destination.selectFork(); + } + } + + function _parseData(bytes memory orig) private pure returns (address target, bytes memory message) { + // FIXME - this is not robust enough, only handling messages of a specific format + uint256 mlen; + (,,target ,,,,,,,, mlen) = abi.decode(orig, (uint256, uint256, address, uint256, uint256, uint256, address, address, uint256, uint256, uint256)); + message = new bytes(mlen); + for (uint256 i = 0; i < mlen; i++) { + message[i] = orig[i + 352]; } } diff --git a/src/testing/bridges/data/BridgeData.sol b/src/testing/bridges/BridgeData.sol similarity index 94% rename from src/testing/bridges/data/BridgeData.sol rename to src/testing/bridges/BridgeData.sol index 56903c2..f9633bf 100644 --- a/src/testing/bridges/data/BridgeData.sol +++ b/src/testing/bridges/BridgeData.sol @@ -11,4 +11,5 @@ struct BridgeData { // These are used internally for log tracking uint256 lastSourceLogIndex; uint256 lastDestinationLogIndex; + bytes extraData; } diff --git a/src/testing/bridges/CCTPBridgeTesting.sol b/src/testing/bridges/CCTPBridgeTesting.sol new file mode 100644 index 0000000..a50d05d --- /dev/null +++ b/src/testing/bridges/CCTPBridgeTesting.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { Vm } from "forge-std/Vm.sol"; + +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { BridgeData } from "./BridgeData.sol"; + +interface InboxLike { + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256); + function bridge() external view returns (BridgeLike); +} + +interface BridgeLike { + function rollup() external view returns (address); + function executeCall( + address, + uint256, + bytes calldata + ) external returns (bool, bytes memory); + function setOutbox(address, bool) external; +} + +contract ArbSysOverride { + + event SendTxToL1(address sender, address target, bytes data); + + function sendTxToL1(address target, bytes calldata message) external payable returns (uint256) { + emit SendTxToL1(msg.sender, target, message); + return 0; + } + +} + +library ArbitrumBridgeTesting { + + using DomainHelpers for *; + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function createBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { + + return init(BridgeData({ + source: ethereum, + destination: arbitrumInstance, + sourceCrossChainMessenger: _getMessengerFromChainAlias(source.chain.chainAlias), + destinationCrossChainMessenger: _getMessengerFromChainAlias(destination.chain.chainAlias), + lastSourceLogIndex: 0, + lastDestinationLogIndex: 0, + extraData: "" + })); + } + + function getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { + bytes32 name = keccak256(bytes(chainAlias)); + if (name == keccak256("mainnet")) { + return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; + } else if (name == keccak256("avalanche")) { + return 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; + } else if (name == keccak256("optimism")) { + return 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; + } else if (name == keccak256("arbitrum_one")) { + return 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; + } else if (name == keccak256("base")) { + return 0xAD09780d193884d503182aD4588450C416D6F9D4; + } else if (name == keccak256("polygon")) { + return 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; + } else { + revert("Unsupported chain"); + } + } + + function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { + + } + + function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + + } + + function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + + } + +} diff --git a/src/testing/bridges/IBidirectionalBridge.sol b/src/testing/bridges/IBidirectionalBridge.sol deleted file mode 100644 index 62fdb92..0000000 --- a/src/testing/bridges/IBidirectionalBridge.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { IUnidirectionalBridge } from "./IUnidirectionalBridge.sol"; - -interface IUnidirectionalBridge is IUnidirectionalBridge { - function relayMessagesToSource(bool switchToSourceFork) external; -} diff --git a/src/testing/bridges/IUnidirectionalBridge.sol b/src/testing/bridges/IUnidirectionalBridge.sol deleted file mode 100644 index 756c154..0000000 --- a/src/testing/bridges/IUnidirectionalBridge.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -interface IUnidirectionalBridge { - function relayMessagesToDestination(bool switchToDestinationFork) external; -} diff --git a/src/testing/bridges/data/Arbitrum.sol b/src/testing/bridges/data/Arbitrum.sol deleted file mode 100644 index 15da267..0000000 --- a/src/testing/bridges/data/Arbitrum.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { BridgeData } from "./BridgeData.sol"; - -library Arbitrum { - - function createArbitrumNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (ArbitrumNativeBridge memory bridge) { - require(keccak256(bytes(ethereum.chain.chainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); - - bytes32 name = keccak256(bytes(arbitrumInstance.chain.chainAlias)); - address inbox; - if (name == keccak256("arbitrum_one")) { - inbox = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; - } else if (name == keccak256("arbitrum_nova")) { - inbox = 0xc4448b71118c9071Bcb9734A0EAc55D18A153949; - } else { - revert("Unsupported destination chain"); - } - - return new ArbitrumNativeBridge(BridgeData({ - source: ethereum, - destination: arbitrumInstance, - sourceCrossChainMessenger: inbox, - destinationCrossChainMessenger: 0x0000000000000000000000000000000000000064, - lastSourceLogIndex: 0, - lastDestinationLogIndex: 0 - })); - } - -} From 5b03ea7961185a863381e8aeee07261c630ed068 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Mon, 20 May 2024 11:10:39 +0200 Subject: [PATCH 12/42] wip cctp --- src/testing/bridges/CCTPBridgeTesting.sol | 36 ----------------------- 1 file changed, 36 deletions(-) diff --git a/src/testing/bridges/CCTPBridgeTesting.sol b/src/testing/bridges/CCTPBridgeTesting.sol index a50d05d..3b0c86f 100644 --- a/src/testing/bridges/CCTPBridgeTesting.sol +++ b/src/testing/bridges/CCTPBridgeTesting.sol @@ -6,41 +6,6 @@ import { Vm } from "forge-std/Vm.sol"; import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; import { BridgeData } from "./BridgeData.sol"; -interface InboxLike { - function createRetryableTicket( - address destAddr, - uint256 arbTxCallValue, - uint256 maxSubmissionCost, - address submissionRefundAddress, - address valueRefundAddress, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data - ) external payable returns (uint256); - function bridge() external view returns (BridgeLike); -} - -interface BridgeLike { - function rollup() external view returns (address); - function executeCall( - address, - uint256, - bytes calldata - ) external returns (bool, bytes memory); - function setOutbox(address, bool) external; -} - -contract ArbSysOverride { - - event SendTxToL1(address sender, address target, bytes data); - - function sendTxToL1(address target, bytes calldata message) external payable returns (uint256) { - emit SendTxToL1(msg.sender, target, message); - return 0; - } - -} - library ArbitrumBridgeTesting { using DomainHelpers for *; @@ -48,7 +13,6 @@ library ArbitrumBridgeTesting { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); function createBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { - return init(BridgeData({ source: ethereum, destination: arbitrumInstance, From 2ece5a49431ffbd97b9f475422d1fbeb9d75dec2 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Mon, 20 May 2024 11:59:56 +0200 Subject: [PATCH 13/42] still wip for refactoring bridge testing --- src/testing/ZkEVMDomain.sol | 112 ----------- src/testing/bridges/AMBBridgeTesting.sol | 58 ++++++ src/testing/bridges/ArbitrumBridgeTesting.sol | 27 ++- src/testing/bridges/CCTPBridge.sol | 106 ---------- src/testing/bridges/CCTPBridgeTesting.sol | 48 ++++- src/testing/bridges/OptimismBridgeTesting.sol | 183 ++++++++++++++++++ src/testing/utils/RecordedLogs.sol | 21 +- test/ZkEVMIntegration.t.sol | 68 ------- 8 files changed, 315 insertions(+), 308 deletions(-) delete mode 100644 src/testing/ZkEVMDomain.sol create mode 100644 src/testing/bridges/AMBBridgeTesting.sol delete mode 100644 src/testing/bridges/CCTPBridge.sol create mode 100644 src/testing/bridges/OptimismBridgeTesting.sol delete mode 100644 test/ZkEVMIntegration.t.sol diff --git a/src/testing/ZkEVMDomain.sol b/src/testing/ZkEVMDomain.sol deleted file mode 100644 index dc851cf..0000000 --- a/src/testing/ZkEVMDomain.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { StdChains } from "forge-std/StdChains.sol"; -import { Vm } from "forge-std/Vm.sol"; - -import { Domain, BridgedDomain } from "./BridgedDomain.sol"; -import { RecordedLogs } from "./RecordedLogs.sol"; - -interface IBridgeMessageReceiver { - function onMessageReceived(address originAddress, uint32 originNetwork, bytes memory data) external payable; -} - -interface IZkEVMBridgeLike { - function bridgeMessage( - uint32 destinationNetwork, - address destinationAddress, - bool forceUpdateGlobalExitRoot, - bytes calldata metadata - ) external payable; -} - -contract ZkEVMDomain is BridgedDomain { - IZkEVMBridgeLike public L1_MESSENGER; - - bytes32 constant BRIDGE_EVENT_TOPIC = - keccak256("BridgeEvent(uint8,uint32,address,uint32,address,uint256,bytes,uint32)"); - - uint256 internal lastFromHostLogIndex; - uint256 internal lastToHostLogIndex; - - constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - bytes32 name = keccak256(bytes(_chain.chainAlias)); - if (name == keccak256("zkevm")) { - L1_MESSENGER = IZkEVMBridgeLike(0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe); - } else { - revert("Unsupported chain"); - } - vm.recordLogs(); - } - - function relayFromHost(bool switchToGuest) external override { - selectFork(); - - // Read all L1 -> L2 messages and relay them under zkevm fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; - if (_isBridgeMessageEvent(log, true)) _claimMessage(log); - } - - if (!switchToGuest) { - hostDomain.selectFork(); - } - } - - function relayToHost(bool switchToHost) external override { - hostDomain.selectFork(); - - // Read all L2 -> L1 messages and relay them under Primary fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if (_isBridgeMessageEvent(log, false)) _claimMessage(log); - } - - if (!switchToHost) { - selectFork(); - } - } - - function _isBridgeMessageEvent(Vm.Log memory log, bool host) internal view returns (bool) { - // early return to prevent abi decode errors - if (log.topics[0] != BRIDGE_EVENT_TOPIC) return false; - - (uint8 messageType, uint32 originNetwork,,,,,,) = - abi.decode(log.data, (uint8, uint32, address, uint32, address, uint256, bytes, uint32)); - return - log.emitter == address(L1_MESSENGER) && messageType == 1 && (host ? originNetwork == 0 : originNetwork == 1); - } - - function _claimMessage(Vm.Log memory log) internal { - ( - /* uint8 messageType */ - , - uint32 originNetwork, - address originAddress, - /* uint32 destinationNetwork */ - , - address destinationAddress, - uint256 msgValue, - bytes memory metadata, - /* uint32 depositCount */ - ) = abi.decode(log.data, (uint8, uint32, address, uint32, address, uint256, bytes, uint32)); - - // mock bridged eth balance increase - uint256 prevBalance = address(L1_MESSENGER).balance; - vm.deal(address(L1_MESSENGER), prevBalance + msgValue); - - // mock bridge callback - // ref: https://github.com/0xPolygonHermez/zkevm-contracts/blob/main/contracts/PolygonZkEVMBridge.sol#L455-L465 - vm.prank(address(L1_MESSENGER)); - (bool success, bytes memory response) = destinationAddress.call{value: msgValue}( - abi.encodeCall(IBridgeMessageReceiver.onMessageReceived, (originAddress, originNetwork, metadata)) - ); - if (!success) { - assembly { - revert(add(response, 32), mload(response)) - } - } - } -} diff --git a/src/testing/bridges/AMBBridgeTesting.sol b/src/testing/bridges/AMBBridgeTesting.sol new file mode 100644 index 0000000..1293bba --- /dev/null +++ b/src/testing/bridges/AMBBridgeTesting.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { Vm } from "forge-std/Vm.sol"; + +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { BridgeData } from "./BridgeData.sol"; + +library AMBBridgeTesting { + + using DomainHelpers for *; + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function createBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { + return init(BridgeData({ + source: ethereum, + destination: arbitrumInstance, + sourceCrossChainMessenger: _getMessengerFromChainAlias(source.chain.chainAlias), + destinationCrossChainMessenger: _getMessengerFromChainAlias(destination.chain.chainAlias), + lastSourceLogIndex: 0, + lastDestinationLogIndex: 0, + extraData: "" + })); + } + + function getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { + bytes32 name = keccak256(bytes(chainAlias)); + if (name == keccak256("mainnet")) { + return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; + } else if (name == keccak256("avalanche")) { + return 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; + } else if (name == keccak256("optimism")) { + return 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; + } else if (name == keccak256("arbitrum_one")) { + return 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; + } else if (name == keccak256("base")) { + return 0xAD09780d193884d503182aD4588450C416D6F9D4; + } else if (name == keccak256("polygon")) { + return 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; + } else { + revert("Unsupported chain"); + } + } + + function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { + + } + + function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + + } + + function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + + } + +} diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index b0ffffb..9935c53 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -44,6 +44,7 @@ contract ArbSysOverride { library ArbitrumBridgeTesting { using DomainHelpers for *; + using RecordedLogs for *; Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -123,11 +124,11 @@ library ArbitrumBridgeTesting { // Read all L1 -> L2 messages and relay them under Arbitrum fork Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; + for (; bridge.lastSourceLogIndex < logs.length; bridge.lastSourceLogIndex++) { + Vm.Log memory log = logs[bridge.lastSourceLogIndex]; if (log.topics[0] == MESSAGE_DELIVERED_TOPIC) { // We need both the current event and the one that follows for all the relevant data - Vm.Log memory logWithData = logs[lastFromHostLogIndex + 1]; + Vm.Log memory logWithData = logs[bridge.lastSourceLogIndex + 1]; (,, address sender,,,) = abi.decode(log.data, (address, uint8, address, bytes32, uint256, uint64)); (address target, bytes memory message) = _parseData(logWithData.data); vm.startPrank(sender); @@ -150,17 +151,15 @@ library ArbitrumBridgeTesting { bridge.source.selectFork(); // Read all L2 -> L1 messages and relay them under host fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if (log.topics[0] == SEND_TO_L1_TOPIC) { - (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); - //l2ToL1Sender = sender; - (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); - if (!success) { - assembly { - revert(add(response, 32), mload(response)) - } + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SEND_TO_L1_TOPIC, address(0)); + for (uint256 i = 0; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); + //l2ToL1Sender = sender; + (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); + if (!success) { + assembly { + revert(add(response, 32), mload(response)) } } } diff --git a/src/testing/bridges/CCTPBridge.sol b/src/testing/bridges/CCTPBridge.sol deleted file mode 100644 index e3e6b49..0000000 --- a/src/testing/bridges/CCTPBridge.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { StdChains } from "forge-std/StdChains.sol"; -import { Vm } from "forge-std/Vm.sol"; - -import { Domain, BridgedDomain } from "./BridgedDomain.sol"; -import { RecordedLogs } from "./RecordedLogs.sol"; - -interface MessengerLike { - function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); -} - -contract CCTPBridge is IUnidirectionalBridge { - - bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); - - MessengerLike public SOURCE_MESSENGER; - MessengerLike public DESTINATION_MESSENGER; - - uint256 internal lastFromHostLogIndex; - uint256 internal lastToHostLogIndex; - - constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - SOURCE_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_hostDomain.details().chainAlias)); - DESTINATION_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_chain.chainAlias)); - - // Set minimum required signatures to zero for both domains - selectFork(); - vm.store( - address(DESTINATION_MESSENGER), - bytes32(uint256(4)), - 0 - ); - hostDomain.selectFork(); - vm.store( - address(SOURCE_MESSENGER), - bytes32(uint256(4)), - 0 - ); - - vm.recordLogs(); - } - - function _getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { - bytes32 name = keccak256(bytes(chainAlias)); - if (name == keccak256("mainnet")) { - return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; - } else if (name == keccak256("avalanche")) { - return 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; - } else if (name == keccak256("optimism")) { - return 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; - } else if (name == keccak256("arbitrum_one")) { - return 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; - } else if (name == keccak256("base")) { - return 0xAD09780d193884d503182aD4588450C416D6F9D4; - } else if (name == keccak256("polygon")) { - return 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; - } else { - revert("Unsupported chain"); - } - } - - function relayFromHost(bool switchToGuest) external override { - selectFork(); - - // Read all L1 -> L2 messages and relay them under CCTP fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(SOURCE_MESSENGER)) { - DESTINATION_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); - } - } - - if (!switchToGuest) { - hostDomain.selectFork(); - } - } - - function relayToHost(bool switchToHost) external override { - hostDomain.selectFork(); - - // Read all L2 -> L1 messages and relay them under host fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(DESTINATION_MESSENGER)) { - SOURCE_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); - } - } - - if (!switchToHost) { - selectFork(); - } - } - - function removeFirst64Bytes(bytes memory inputData) public pure returns (bytes memory) { - bytes memory returnValue = new bytes(inputData.length - 64); - for (uint256 i = 0; i < inputData.length - 64; i++) { - returnValue[i] = inputData[i + 64]; - } - return returnValue; - } - -} diff --git a/src/testing/bridges/CCTPBridgeTesting.sol b/src/testing/bridges/CCTPBridgeTesting.sol index 3b0c86f..65e531e 100644 --- a/src/testing/bridges/CCTPBridgeTesting.sol +++ b/src/testing/bridges/CCTPBridgeTesting.sol @@ -6,25 +6,27 @@ import { Vm } from "forge-std/Vm.sol"; import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; import { BridgeData } from "./BridgeData.sol"; -library ArbitrumBridgeTesting { +library CCTPBridgeTesting { + + bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); using DomainHelpers for *; Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - function createBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { + function createCircleBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { return init(BridgeData({ source: ethereum, destination: arbitrumInstance, - sourceCrossChainMessenger: _getMessengerFromChainAlias(source.chain.chainAlias), - destinationCrossChainMessenger: _getMessengerFromChainAlias(destination.chain.chainAlias), + sourceCrossChainMessenger: getCircleMessengerFromChainAlias(source.chain.chainAlias), + destinationCrossChainMessenger: getCircleMessengerFromChainAlias(destination.chain.chainAlias), lastSourceLogIndex: 0, lastDestinationLogIndex: 0, extraData: "" })); } - function getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { + function getCircleMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { bytes32 name = keccak256(bytes(chainAlias)); if (name == keccak256("mainnet")) { return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; @@ -44,15 +46,47 @@ library ArbitrumBridgeTesting { } function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { - + // Set minimum required signatures to zero for both domains + bridge.destination.selectFork(); + vm.store( + bridge.destinationCrossChainMessenger, + bytes32(uint256(4)), + 0 + ); + bridge.source.selectFork(); + vm.store( + bridge.sourceCrossChainMessenger, + bytes32(uint256(4)), + 0 + ); + + vm.recordLogs(); } function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { - + bridge.destination.selectFork(); + + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, SENT_MESSAGE_TOPIC, address(0)); + for (uint256 i = 0; i < logs.length; i++) { + bridge.destinationCrossChainMessenger.receiveMessage(abi.decode(logs[i].data, (bytes)), ""); + } + + if (!switchToDestinationFork) { + bridge.source.selectFork(); + } } function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + bridge.source.selectFork(); + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, address(0)); + for (uint256 i = 0; i < logs.length; i++) { + bridge.sourceCrossChainMessenger.receiveMessage(abi.decode(logs[i].data, (bytes)), ""); + } + + if (!switchToSourceFork) { + bridge.destination.selectFork(); + } } } diff --git a/src/testing/bridges/OptimismBridgeTesting.sol b/src/testing/bridges/OptimismBridgeTesting.sol new file mode 100644 index 0000000..a3547bb --- /dev/null +++ b/src/testing/bridges/OptimismBridgeTesting.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import { Vm } from "forge-std/Vm.sol"; + +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { BridgeData } from "./BridgeData.sol"; + +interface InboxLike { + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256); + function bridge() external view returns (BridgeLike); +} + +interface BridgeLike { + function rollup() external view returns (address); + function executeCall( + address, + uint256, + bytes calldata + ) external returns (bool, bytes memory); + function setOutbox(address, bool) external; +} + +contract ArbSysOverride { + + event SendTxToL1(address sender, address target, bytes data); + + function sendTxToL1(address target, bytes calldata message) external payable returns (uint256) { + emit SendTxToL1(msg.sender, target, message); + return 0; + } + +} + +library OptimismBridgeTesting { + + using DomainHelpers for *; + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + bytes32 private constant MESSAGE_DELIVERED_TOPIC = keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"); + bytes32 private constant SEND_TO_L1_TOPIC = keccak256("SendTxToL1(address,address,bytes)"); + + function createNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (BridgeData memory bridge) { + ( + address sourceCrossChainMessenger, + address destinationCrossChainMessenger + ) = getMessengerFromChainAlias(ethereum.chain.chainAlias, arbitrumInstance.chain.chainAlias) + + return init(BridgeData({ + source: ethereum, + destination: arbitrumInstance, + sourceCrossChainMessenger: sourceCrossChainMessenger, + destinationCrossChainMessenger: destinationCrossChainMessenger, + lastSourceLogIndex: 0, + lastDestinationLogIndex: 0, + extraData: "" + })); + } + + function getMessengerFromChainAlias( + string memory sourceChainAlias, + string memory destinationChainAlias + ) internal pure returns ( + address sourceCrossChainMessenger, + address destinationCrossChainMessenger + ) { + require(keccak256(bytes(sourceChainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); + + bytes32 name = keccak256(bytes(destinationChainAlias)); + if (name == keccak256("arbitrum_one")) { + sourceCrossChainMessenger = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; + } else if (name == keccak256("arbitrum_nova")) { + sourceCrossChainMessenger = 0xc4448b71118c9071Bcb9734A0EAc55D18A153949; + } else { + revert("Unsupported destination chain"); + } + destinationCrossChainMessenger = 0x0000000000000000000000000000000000000064; + } + + function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { + bridge.source.selectFork(); + BridgeLike underlyingBridge = InboxLike(data.sourceCrossChainMessenger).bridge(); + vm.recordLogs(); + + // Make this contract a valid outbox + address _rollup = underlyingBridge.rollup(); + vm.store( + address(underlyingBridge), + bytes32(uint256(8)), + bytes32(uint256(uint160(address(this)))) + ); + underlyingBridge.setOutbox(address(this), true); + vm.store( + address(underlyingBridge), + bytes32(uint256(8)), + bytes32(uint256(uint160(_rollup))) + ); + + // Need to replace ArbSys contract with custom code to make it compatible with revm + bridge.destination.selectFork(); + bytes memory bytecode = vm.getCode("ArbitrumBridgeTesting.sol:ArbSysOverride"); + address deployed; + assembly { + deployed := create(0, add(bytecode, 0x20), mload(bytecode)) + } + vm.etch(ARB_SYS, deployed.code); + + bridge.source.selectFork(); + } + + function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + bridge.destination.selectFork(); + + // Read all L1 -> L2 messages and relay them under Arbitrum fork + Vm.Log[] memory logs = RecordedLogs.getLogs(); + for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { + Vm.Log memory log = logs[lastFromHostLogIndex]; + if (log.topics[0] == MESSAGE_DELIVERED_TOPIC) { + // We need both the current event and the one that follows for all the relevant data + Vm.Log memory logWithData = logs[lastFromHostLogIndex + 1]; + (,, address sender,,,) = abi.decode(log.data, (address, uint8, address, bytes32, uint256, uint64)); + (address target, bytes memory message) = _parseData(logWithData.data); + vm.startPrank(sender); + (bool success, bytes memory response) = target.call(message); + vm.stopPrank(); + if (!success) { + assembly { + revert(add(response, 32), mload(response)) + } + } + } + } + + if (!switchToDestinationFork) { + bridge.source.selectFork(); + } + } + + function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + bridge.source.selectFork(); + + // Read all L2 -> L1 messages and relay them under host fork + Vm.Log[] memory logs = RecordedLogs.getLogs(); + for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { + Vm.Log memory log = logs[lastToHostLogIndex]; + if (log.topics[0] == SEND_TO_L1_TOPIC) { + (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); + //l2ToL1Sender = sender; + (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); + if (!success) { + assembly { + revert(add(response, 32), mload(response)) + } + } + } + } + + if (!switchToSourceFork) { + bridge.destination.selectFork(); + } + } + + function _parseData(bytes memory orig) private pure returns (address target, bytes memory message) { + // FIXME - this is not robust enough, only handling messages of a specific format + uint256 mlen; + (,,target ,,,,,,,, mlen) = abi.decode(orig, (uint256, uint256, address, uint256, uint256, uint256, address, address, uint256, uint256, uint256)); + message = new bytes(mlen); + for (uint256 i = 0; i < mlen; i++) { + message[i] = orig[i + 352]; + } + } + +} diff --git a/src/testing/utils/RecordedLogs.sol b/src/testing/utils/RecordedLogs.sol index 5ca5826..ca62d29 100644 --- a/src/testing/utils/RecordedLogs.sol +++ b/src/testing/utils/RecordedLogs.sol @@ -5,7 +5,7 @@ import { Vm } from "forge-std/Vm.sol"; library RecordedLogs { - Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); function getLogs() internal returns (Vm.Log[] memory) { string memory _logs = vm.serializeUint("RECORDED_LOGS", "a", 0); // this is the only way to get the logs from the memory object @@ -31,4 +31,23 @@ library RecordedLogs { return logs; } + function ingestAndFilterLogs(BridgeData memory bridge, bool sourceToDestination, bytes32 topic, address emitter) internal pure returns (Vm.Log[] memory filteredLogs) { + Vm.Log[] memory logs = RecordedLogs.getLogs(); + uint256 lastIndex = sourceToDestination ? bridge.lastSourceLogIndex : bridge.lastDestinationLogIndex; + uint256 pushedIndex = 0; + + filteredLogs = new Vm.Log[](logs.length - lastIndex); + + for (; lastIndex < logs.length; lastIndex++) { + Vm.Log memory log = logs[lastIndex]; + if (log.topics[0] == topic && log.emitter == emitter) { + filteredLogs[pushedIndex++] = log; + } + } + + if (sourceToDestination) bridge.lastSourceLogIndex = lastIndex; + else bridge.lastDestinationLogIndex = lastIndex; + filteredLogs.length = pushedIndex; + } + } diff --git a/test/ZkEVMIntegration.t.sol b/test/ZkEVMIntegration.t.sol deleted file mode 100644 index 85f968b..0000000 --- a/test/ZkEVMIntegration.t.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import "./IntegrationBase.t.sol"; - -import { ZkEVMDomain, IBridgeMessageReceiver } from "../src/testing/ZkEVMDomain.sol"; - -contract ZkevmMessageOrdering is MessageOrdering, IBridgeMessageReceiver { - function onMessageReceived(address /*originAddress*/, uint32 /*originNetwork*/, bytes memory data) external payable { - // call the specific method - (bool success, bytes memory ret) = address(this).call(data); - if (!success) { - assembly { - revert(add(ret, 0x20), mload(ret)) - } - } - } - -} - -// FIXME: zkEVM bridging is broken, marking as abstract to temporarily disable until it's fixed -abstract contract ZkEVMIntegrationTest is IntegrationBaseTest { - - function test_zkevm() public { - setChain("zkevm", ChainData("ZkEVM", 1101, "https://zkevm-rpc.com")); - - checkZkEVMStyle(new ZkEVMDomain(getChain("zkevm"), mainnet)); - } - - function checkZkEVMStyle(ZkEVMDomain zkevm) public { - Domain host = zkevm.hostDomain(); - - host.selectFork(); - - ZkevmMessageOrdering moHost = new ZkevmMessageOrdering(); - - zkevm.selectFork(); - - ZkevmMessageOrdering moZkevm = new ZkevmMessageOrdering(); - - // Queue up two more L2 -> L1 messages - zkevm.L1_MESSENGER().bridgeMessage(0, address(moHost), true, abi.encodeCall(MessageOrdering.push, (3))); - zkevm.L1_MESSENGER().bridgeMessage(0, address(moHost), true, abi.encodeCall(MessageOrdering.push, (4))); - - assertEq(moZkevm.length(), 0); - - host.selectFork(); - - // Queue up two more L1 -> L2 messages - XChainForwarders.sendMessageZkEVM(address(moZkevm), abi.encodeCall(MessageOrdering.push, (1))); - XChainForwarders.sendMessageZkEVM(address(moZkevm), abi.encodeCall(MessageOrdering.push, (2))); - - assertEq(moHost.length(), 0); - - zkevm.relayFromHost(true); - - assertEq(moZkevm.length(), 2); - assertEq(moZkevm.messages(0), 1); - assertEq(moZkevm.messages(1), 2); - - zkevm.relayToHost(true); - - assertEq(moHost.length(), 2); - assertEq(moHost.messages(0), 3); - assertEq(moHost.messages(1), 4); - } - -} From 1d966d5d5f2110d6753698186b671ca751bcbc83 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 24 May 2024 21:40:16 +0200 Subject: [PATCH 14/42] large refactor to split out domains and bridges and move into library structure --- .../{bridges/BridgeData.sol => Bridge.sol} | 4 +- src/testing/BridgedDomain.sol | 16 -- src/testing/CircleCCTPDomain.sol | 106 ----------- src/testing/Domain.sol | 8 +- src/testing/GnosisDomain.sol | 118 ------------ src/testing/OptimismDomain.sol | 116 ------------ src/testing/bridges/AMBBridgeTesting.sol | 108 ++++++++--- src/testing/bridges/ArbitrumBridgeTesting.sol | 52 ++--- src/testing/bridges/CCTPBridgeTesting.sol | 34 ++-- src/testing/bridges/OptimismBridgeTesting.sol | 178 +++++++----------- src/testing/utils/RecordedLogs.sol | 13 +- test/ArbitrumIntegration.t.sol | 37 ++-- test/CCTPIntegration.t.sol | 51 +++-- test/GnosisIntegration.t.sol | 39 ++-- test/IntegrationBase.t.sol | 7 +- test/OptimismIntegration.t.sol | 35 ++-- 16 files changed, 303 insertions(+), 619 deletions(-) rename src/testing/{bridges/BridgeData.sol => Bridge.sol} (83%) delete mode 100644 src/testing/BridgedDomain.sol delete mode 100644 src/testing/CircleCCTPDomain.sol delete mode 100644 src/testing/GnosisDomain.sol delete mode 100644 src/testing/OptimismDomain.sol diff --git a/src/testing/bridges/BridgeData.sol b/src/testing/Bridge.sol similarity index 83% rename from src/testing/bridges/BridgeData.sol rename to src/testing/Bridge.sol index f9633bf..f5740fb 100644 --- a/src/testing/bridges/BridgeData.sol +++ b/src/testing/Bridge.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity >=0.8.0; -import { Domain } from "src/testing/Domain.sol"; +import { Domain } from "./Domain.sol"; -struct BridgeData { +struct Bridge { Domain source; Domain destination; address sourceCrossChainMessenger; diff --git a/src/testing/BridgedDomain.sol b/src/testing/BridgedDomain.sol deleted file mode 100644 index 78a1a55..0000000 --- a/src/testing/BridgedDomain.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { Domain } from "./Domain.sol"; - -abstract contract BridgedDomain is Domain { - - Domain public immutable hostDomain; - - constructor(Domain _hostDomain) { - hostDomain = _hostDomain; - } - - function relayFromHost(bool switchToGuest) external virtual; - function relayToHost(bool switchToHost) external virtual; -} diff --git a/src/testing/CircleCCTPDomain.sol b/src/testing/CircleCCTPDomain.sol deleted file mode 100644 index 93591ab..0000000 --- a/src/testing/CircleCCTPDomain.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { StdChains } from "forge-std/StdChains.sol"; -import { Vm } from "forge-std/Vm.sol"; - -import { Domain, BridgedDomain } from "./BridgedDomain.sol"; -import { RecordedLogs } from "./RecordedLogs.sol"; - -interface MessengerLike { - function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); -} - -contract CircleCCTPDomain is BridgedDomain { - - bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); - - MessengerLike public SOURCE_MESSENGER; - MessengerLike public DESTINATION_MESSENGER; - - uint256 internal lastFromHostLogIndex; - uint256 internal lastToHostLogIndex; - - constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - SOURCE_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_hostDomain.details().chainAlias)); - DESTINATION_MESSENGER = MessengerLike(_getMessengerFromChainAlias(_chain.chainAlias)); - - // Set minimum required signatures to zero for both domains - selectFork(); - vm.store( - address(DESTINATION_MESSENGER), - bytes32(uint256(4)), - 0 - ); - hostDomain.selectFork(); - vm.store( - address(SOURCE_MESSENGER), - bytes32(uint256(4)), - 0 - ); - - vm.recordLogs(); - } - - function _getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { - bytes32 name = keccak256(bytes(chainAlias)); - if (name == keccak256("mainnet")) { - return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; - } else if (name == keccak256("avalanche")) { - return 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; - } else if (name == keccak256("optimism")) { - return 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; - } else if (name == keccak256("arbitrum_one")) { - return 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; - } else if (name == keccak256("base")) { - return 0xAD09780d193884d503182aD4588450C416D6F9D4; - } else if (name == keccak256("polygon")) { - return 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; - } else { - revert("Unsupported chain"); - } - } - - function relayFromHost(bool switchToGuest) external override { - selectFork(); - - // Read all L1 -> L2 messages and relay them under CCTP fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(SOURCE_MESSENGER)) { - DESTINATION_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); - } - } - - if (!switchToGuest) { - hostDomain.selectFork(); - } - } - - function relayToHost(bool switchToHost) external override { - hostDomain.selectFork(); - - // Read all L2 -> L1 messages and relay them under host fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(DESTINATION_MESSENGER)) { - SOURCE_MESSENGER.receiveMessage(removeFirst64Bytes(log.data), ""); - } - } - - if (!switchToHost) { - selectFork(); - } - } - - function removeFirst64Bytes(bytes memory inputData) public pure returns (bytes memory) { - bytes memory returnValue = new bytes(inputData.length - 64); - for (uint256 i = 0; i < inputData.length - 64; i++) { - returnValue[i] = inputData[i + 64]; - } - return returnValue; - } - -} diff --git a/src/testing/Domain.sol b/src/testing/Domain.sol index 37b50ba..d7492a2 100644 --- a/src/testing/Domain.sol +++ b/src/testing/Domain.sol @@ -16,7 +16,7 @@ library DomainHelpers { function createFork(StdChains.Chain memory chain, uint256 blockNumber) internal returns (Domain memory domain) { domain = Domain({ chain: chain, - forkId: vm.createFork(chain.rpcUrl, blockNum) + forkId: vm.createFork(chain.rpcUrl, blockNumber) }); } @@ -30,7 +30,7 @@ library DomainHelpers { function createSelectFork(StdChains.Chain memory chain, uint256 blockNumber) internal returns (Domain memory domain) { domain = Domain({ chain: chain, - forkId: vm.createSelectFork(chain.rpcUrl, blockNum) + forkId: vm.createSelectFork(chain.rpcUrl, blockNumber) }); _assertExpectedRpc(chain); } @@ -45,14 +45,14 @@ library DomainHelpers { function selectFork(Domain memory domain) internal { vm.selectFork(domain.forkId); - _assertExpectedRpc(domain); + _assertExpectedRpc(domain.chain); } function rollFork(Domain memory domain, uint256 blockNumber) internal { vm.rollFork(domain.forkId, blockNumber); } - function _assertExpectedRpc(StdChains.Chain memory chain) private { + function _assertExpectedRpc(StdChains.Chain memory chain) private view { require(block.chainid == chain.chainId, string(abi.encodePacked(chain.chainAlias, " is pointing to the wrong RPC endpoint '", chain.rpcUrl, "'"))); } diff --git a/src/testing/GnosisDomain.sol b/src/testing/GnosisDomain.sol deleted file mode 100644 index 054cc03..0000000 --- a/src/testing/GnosisDomain.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { StdChains } from "forge-std/StdChains.sol"; -import { Vm } from "forge-std/Vm.sol"; - -import { Domain, BridgedDomain } from "./BridgedDomain.sol"; -import { RecordedLogs } from "./RecordedLogs.sol"; - -interface IAMB { - function requireToPassMessage(address, bytes memory, uint256) external returns (bytes32); - function validatorContract() external view returns (address); -} - -interface IHomeAMB is IAMB { - function executeAffirmation(bytes memory) external; -} - -interface IForeignAMB is IAMB { - function executeSignatures(bytes memory, bytes memory) external; -} - -interface IValidatorContract { - function validatorList() external view returns (address[] memory); - function requiredSignatures() external view returns (uint256); -} - -contract GnosisDomain is BridgedDomain { - - bytes32 private constant USER_REQUEST_FOR_AFFIRMATION_TOPIC = keccak256("UserRequestForAffirmation(bytes32,bytes)"); - bytes32 private constant USER_REQUEST_FOR_SIGNATURE_TOPIC = keccak256("UserRequestForSignature(bytes32,bytes)"); - - IForeignAMB public L1_AMB_CROSS_DOMAIN_MESSENGER; - IHomeAMB public L2_AMB_CROSS_DOMAIN_MESSENGER; - - uint256 internal lastFromHostLogIndex; - uint256 internal lastToHostLogIndex; - - constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - bytes32 name = keccak256(bytes(_chain.chainAlias)); - if (name == keccak256("gnosis_chain")) { - L1_AMB_CROSS_DOMAIN_MESSENGER = IForeignAMB(0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e); - L2_AMB_CROSS_DOMAIN_MESSENGER = IHomeAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59); - } else if (name == keccak256("chiado")) { - L1_AMB_CROSS_DOMAIN_MESSENGER = IForeignAMB(0x87A19d769D875964E9Cd41dDBfc397B2543764E6); - L2_AMB_CROSS_DOMAIN_MESSENGER = IHomeAMB(0x99Ca51a3534785ED619f46A79C7Ad65Fa8d85e7a); - } else { - revert("Unsupported chain"); - } - - hostDomain.selectFork(); - - // Set minimum required signatures on L1 to 0 - IValidatorContract validatorContract = IValidatorContract(L1_AMB_CROSS_DOMAIN_MESSENGER.validatorContract()); - vm.store( - address(validatorContract), - 0x8a247e09a5673bd4d93a4e76d8fb9553523aa0d77f51f3d576e7421f5295b9bc, - 0 - ); - - vm.recordLogs(); - } - - function relayFromHost(bool switchToGuest) external override { - selectFork(); // switch to Gnosis domain - - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; - if ( - log.topics[0] == USER_REQUEST_FOR_AFFIRMATION_TOPIC - && log.emitter == address(L1_AMB_CROSS_DOMAIN_MESSENGER) - ) { - IValidatorContract validatorContract = IValidatorContract(L2_AMB_CROSS_DOMAIN_MESSENGER.validatorContract()); - address[] memory validators = validatorContract.validatorList(); - uint256 requiredSignatures = validatorContract.requiredSignatures(); - bytes memory messageToRelay = removeFirst64Bytes(log.data); - for (uint256 i = 0; i < requiredSignatures; i++) { - vm.prank(validators[i]); - L2_AMB_CROSS_DOMAIN_MESSENGER.executeAffirmation(messageToRelay); - } - } - } - - if (!switchToGuest) { - hostDomain.selectFork(); - } - } - - function relayToHost(bool switchToHost) external override { - hostDomain.selectFork(); - - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if ( - log.topics[0] == USER_REQUEST_FOR_SIGNATURE_TOPIC - && log.emitter == address(L2_AMB_CROSS_DOMAIN_MESSENGER) - ) { - bytes memory messageToRelay = removeFirst64Bytes(log.data); - L1_AMB_CROSS_DOMAIN_MESSENGER.executeSignatures(messageToRelay, abi.encodePacked(uint256(0))); - } - } - - if (!switchToHost) { - selectFork(); - } - } - - function removeFirst64Bytes(bytes memory inputData) public pure returns (bytes memory) { - bytes memory returnValue = new bytes(inputData.length - 64); - for (uint256 i = 0; i < inputData.length - 64; i++) { - returnValue[i] = inputData[i + 64]; - } - return returnValue; - } - -} diff --git a/src/testing/OptimismDomain.sol b/src/testing/OptimismDomain.sol deleted file mode 100644 index 90b6297..0000000 --- a/src/testing/OptimismDomain.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.8.0; - -import { StdChains } from "forge-std/StdChains.sol"; -import { Vm } from "forge-std/Vm.sol"; - -import { Domain, BridgedDomain } from "./BridgedDomain.sol"; -import { RecordedLogs } from "./RecordedLogs.sol"; - -interface MessengerLike { - function sendMessage( - address target, - bytes memory message, - uint32 gasLimit - ) external; - function relayMessage( - uint256 _nonce, - address _sender, - address _target, - uint256 _value, - uint256 _minGasLimit, - bytes calldata _message - ) external payable; -} - -contract OptimismDomain is BridgedDomain { - - bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("SentMessage(address,address,bytes,uint256,uint256)"); - uint160 private constant OFFSET = uint160(0x1111000000000000000000000000000000001111); - - MessengerLike public L1_MESSENGER; - MessengerLike public constant L2_MESSENGER = MessengerLike(0x4200000000000000000000000000000000000007); - - uint256 internal lastFromHostLogIndex; - uint256 internal lastToHostLogIndex; - - constructor(StdChains.Chain memory _chain, Domain _hostDomain) Domain(_chain) BridgedDomain(_hostDomain) { - bytes32 name = keccak256(bytes(_chain.chainAlias)); - if (name == keccak256("optimism")) { - L1_MESSENGER = MessengerLike(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1); - } else if (name == keccak256("optimism_goerli")) { - L1_MESSENGER = MessengerLike(0x5086d1eEF304eb5284A0f6720f79403b4e9bE294); - } else if (name == keccak256("base")) { - L1_MESSENGER = MessengerLike(0x866E82a600A1414e583f7F13623F1aC5d58b0Afa); - } else if (name == keccak256("base_goerli")) { - L1_MESSENGER = MessengerLike(0x8e5693140eA606bcEB98761d9beB1BC87383706D); - } else { - revert("Unsupported chain"); - } - - vm.recordLogs(); - } - - function relayFromHost(bool switchToGuest) external override { - selectFork(); - address malias; - unchecked { - malias = address(uint160(address(L1_MESSENGER)) + OFFSET); - } - - // Read all L1 -> L2 messages and relay them under Optimism fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L1_MESSENGER)) { - address target = address(uint160(uint256(log.topics[1]))); - (address sender, bytes memory message, uint256 nonce, uint256 gasLimit) = abi.decode(log.data, (address, bytes, uint256, uint256)); - vm.prank(malias); - L2_MESSENGER.relayMessage(nonce, sender, target, 0, gasLimit, message); - } - } - - if (!switchToGuest) { - hostDomain.selectFork(); - } - } - - function relayToHost(bool switchToHost) external override { - hostDomain.selectFork(); - - // Read all L2 -> L1 messages and relay them under Primary fork - // Note: We bypass the L1 messenger relay here because it's easier to not have to generate valid state roots / merkle proofs - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if (log.topics[0] == SENT_MESSAGE_TOPIC && log.emitter == address(L2_MESSENGER)) { - address target = address(uint160(uint256(log.topics[1]))); - (address sender, bytes memory message,,) = abi.decode(log.data, (address, bytes, uint256, uint256)); - // Set xDomainMessageSender - vm.store( - address(L1_MESSENGER), - bytes32(uint256(204)), - bytes32(uint256(uint160(sender))) - ); - vm.startPrank(address(L1_MESSENGER)); - (bool success, bytes memory response) = target.call(message); - vm.stopPrank(); - vm.store( - address(L1_MESSENGER), - bytes32(uint256(204)), - bytes32(uint256(0)) - ); - if (!success) { - assembly { - revert(add(response, 32), mload(response)) - } - } - } - } - - if (!switchToHost) { - selectFork(); - } - } - -} diff --git a/src/testing/bridges/AMBBridgeTesting.sol b/src/testing/bridges/AMBBridgeTesting.sol index 1293bba..320d3fe 100644 --- a/src/testing/bridges/AMBBridgeTesting.sol +++ b/src/testing/bridges/AMBBridgeTesting.sol @@ -3,56 +3,112 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; -import { BridgeData } from "./BridgeData.sol"; +import { Bridge } from "src/testing/Bridge.sol"; +import { Domain, DomainHelpers } from "src/testing/Domain.sol"; +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; + +interface IAMB { + function validatorContract() external view returns (address); + function executeSignatures(bytes memory, bytes memory) external; + function executeAffirmation(bytes memory) external; +} + +interface IValidatorContract { + function validatorList() external view returns (address[] memory); + function requiredSignatures() external view returns (uint256); +} library AMBBridgeTesting { using DomainHelpers for *; + using RecordedLogs for *; Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + bytes32 private constant USER_REQUEST_FOR_AFFIRMATION_TOPIC = keccak256("UserRequestForAffirmation(bytes32,bytes)"); + bytes32 private constant USER_REQUEST_FOR_SIGNATURE_TOPIC = keccak256("UserRequestForSignature(bytes32,bytes)"); - function createBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { - return init(BridgeData({ - source: ethereum, - destination: arbitrumInstance, - sourceCrossChainMessenger: _getMessengerFromChainAlias(source.chain.chainAlias), - destinationCrossChainMessenger: _getMessengerFromChainAlias(destination.chain.chainAlias), + function createGnosisBridge(Domain memory source, Domain memory destination) internal returns (Bridge memory bridge) { + return init(Bridge({ + source: source, + destination: destination, + sourceCrossChainMessenger: getGnosisMessengerFromChainAlias(source.chain.chainAlias), + destinationCrossChainMessenger: getGnosisMessengerFromChainAlias(destination.chain.chainAlias), lastSourceLogIndex: 0, lastDestinationLogIndex: 0, extraData: "" })); } - function getMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { + function getGnosisMessengerFromChainAlias(string memory chainAlias) internal pure returns (address) { bytes32 name = keccak256(bytes(chainAlias)); if (name == keccak256("mainnet")) { - return 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; - } else if (name == keccak256("avalanche")) { - return 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; - } else if (name == keccak256("optimism")) { - return 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; - } else if (name == keccak256("arbitrum_one")) { - return 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; - } else if (name == keccak256("base")) { - return 0xAD09780d193884d503182aD4588450C416D6F9D4; - } else if (name == keccak256("polygon")) { - return 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; + return 0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e; + } else if (name == keccak256("gnosis_chain")) { + return 0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59; } else { revert("Unsupported chain"); } } - function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { - + function init(Bridge memory bridge) internal returns (Bridge memory) { + vm.recordLogs(); + + // Set minimum required signatures to zero for both domains + bridge.destination.selectFork(); + vm.store( + IAMB(bridge.destinationCrossChainMessenger).validatorContract(), + 0x8a247e09a5673bd4d93a4e76d8fb9553523aa0d77f51f3d576e7421f5295b9bc, + 0 + ); + bridge.source.selectFork(); + vm.store( + IAMB(bridge.sourceCrossChainMessenger).validatorContract(), + 0x8a247e09a5673bd4d93a4e76d8fb9553523aa0d77f51f3d576e7421f5295b9bc, + 0 + ); + + return bridge; + } + + function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { + bridge.destination.selectFork(); + + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, USER_REQUEST_FOR_AFFIRMATION_TOPIC, USER_REQUEST_FOR_SIGNATURE_TOPIC, bridge.sourceCrossChainMessenger); + _relayAllMessages(logs, bridge.destinationCrossChainMessenger); + + if (!switchToDestinationFork) { + bridge.source.selectFork(); + } } - function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { - + function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { + bridge.source.selectFork(); + + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, USER_REQUEST_FOR_AFFIRMATION_TOPIC, USER_REQUEST_FOR_SIGNATURE_TOPIC, bridge.destinationCrossChainMessenger); + _relayAllMessages(logs, bridge.sourceCrossChainMessenger); + + if (!switchToSourceFork) { + bridge.destination.selectFork(); + } } - function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { - + function _relayAllMessages(Vm.Log[] memory logs, address amb) private { + for (uint256 i = 0; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + bytes memory messageToRelay = abi.decode(log.data, (bytes)); + if (log.topics[0] == USER_REQUEST_FOR_AFFIRMATION_TOPIC) { + IValidatorContract validatorContract = IValidatorContract(IAMB(amb).validatorContract()); + address[] memory validators = validatorContract.validatorList(); + uint256 requiredSignatures = validatorContract.requiredSignatures(); + for (uint256 o = 0; o < requiredSignatures; o++) { + vm.prank(validators[o]); + IAMB(amb).executeAffirmation(messageToRelay); + } + } else if (log.topics[0] == USER_REQUEST_FOR_SIGNATURE_TOPIC) { + IAMB(amb).executeSignatures(messageToRelay, abi.encodePacked(uint256(0))); + } + } } } diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index 9935c53..7f66b5c 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -3,8 +3,9 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; -import { BridgeData } from "./BridgeData.sol"; +import { Bridge } from "src/testing/Bridge.sol"; +import { Domain, DomainHelpers } from "src/testing/Domain.sol"; +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; interface InboxLike { function createRetryableTicket( @@ -51,13 +52,13 @@ library ArbitrumBridgeTesting { bytes32 private constant MESSAGE_DELIVERED_TOPIC = keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"); bytes32 private constant SEND_TO_L1_TOPIC = keccak256("SendTxToL1(address,address,bytes)"); - function createNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (BridgeData memory bridge) { + function createNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (Bridge memory bridge) { ( address sourceCrossChainMessenger, address destinationCrossChainMessenger - ) = getMessengerFromChainAlias(ethereum.chain.chainAlias, arbitrumInstance.chain.chainAlias) + ) = getMessengerFromChainAlias(ethereum.chain.chainAlias, arbitrumInstance.chain.chainAlias); - return init(BridgeData({ + return init(Bridge({ source: ethereum, destination: arbitrumInstance, sourceCrossChainMessenger: sourceCrossChainMessenger, @@ -88,11 +89,21 @@ library ArbitrumBridgeTesting { destinationCrossChainMessenger = 0x0000000000000000000000000000000000000064; } - function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { - bridge.source.selectFork(); - BridgeLike underlyingBridge = InboxLike(data.sourceCrossChainMessenger).bridge(); + function init(Bridge memory bridge) internal returns (Bridge memory) { vm.recordLogs(); + // Need to replace ArbSys contract with custom code to make it compatible with revm + bridge.destination.selectFork(); + bytes memory bytecode = vm.getCode("ArbitrumBridgeTesting.sol:ArbSysOverride"); + address deployed; + assembly { + deployed := create(0, add(bytecode, 0x20), mload(bytecode)) + } + vm.etch(bridge.destinationCrossChainMessenger, deployed.code); + + bridge.source.selectFork(); + BridgeLike underlyingBridge = InboxLike(bridge.sourceCrossChainMessenger).bridge(); + // Make this contract a valid outbox address _rollup = underlyingBridge.rollup(); vm.store( @@ -107,26 +118,16 @@ library ArbitrumBridgeTesting { bytes32(uint256(uint160(_rollup))) ); - // Need to replace ArbSys contract with custom code to make it compatible with revm - bridge.destination.selectFork(); - bytes memory bytecode = vm.getCode("ArbitrumBridgeTesting.sol:ArbSysOverride"); - address deployed; - assembly { - deployed := create(0, add(bytecode, 0x20), mload(bytecode)) - } - vm.etch(ARB_SYS, deployed.code); - - bridge.source.selectFork(); + return bridge; } - function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); - // Read all L1 -> L2 messages and relay them under Arbitrum fork Vm.Log[] memory logs = RecordedLogs.getLogs(); for (; bridge.lastSourceLogIndex < logs.length; bridge.lastSourceLogIndex++) { Vm.Log memory log = logs[bridge.lastSourceLogIndex]; - if (log.topics[0] == MESSAGE_DELIVERED_TOPIC) { + if (log.topics[0] == MESSAGE_DELIVERED_TOPIC && log.emitter == bridge.sourceCrossChainMessenger) { // We need both the current event and the one that follows for all the relevant data Vm.Log memory logWithData = logs[bridge.lastSourceLogIndex + 1]; (,, address sender,,,) = abi.decode(log.data, (address, uint8, address, bytes32, uint256, uint64)); @@ -147,16 +148,15 @@ library ArbitrumBridgeTesting { } } - function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); - // Read all L2 -> L1 messages and relay them under host fork - Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SEND_TO_L1_TOPIC, address(0)); + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SEND_TO_L1_TOPIC, bridge.destinationCrossChainMessenger); for (uint256 i = 0; i < logs.length; i++) { Vm.Log memory log = logs[i]; - (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); + (, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); //l2ToL1Sender = sender; - (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); + (bool success, bytes memory response) = InboxLike(bridge.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); if (!success) { assembly { revert(add(response, 32), mload(response)) diff --git a/src/testing/bridges/CCTPBridgeTesting.sol b/src/testing/bridges/CCTPBridgeTesting.sol index 65e531e..229a2b2 100644 --- a/src/testing/bridges/CCTPBridgeTesting.sol +++ b/src/testing/bridges/CCTPBridgeTesting.sol @@ -3,21 +3,27 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; -import { BridgeData } from "./BridgeData.sol"; +import { Bridge } from "src/testing/Bridge.sol"; +import { Domain, DomainHelpers } from "src/testing/Domain.sol"; +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; + +interface IMessenger { + function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); +} library CCTPBridgeTesting { bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("MessageSent(bytes)"); using DomainHelpers for *; + using RecordedLogs for *; Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - function createCircleBridge(Domain memory source, Domain memory destination) internal returns (BridgeData memory bridge) { - return init(BridgeData({ - source: ethereum, - destination: arbitrumInstance, + function createCircleBridge(Domain memory source, Domain memory destination) internal returns (Bridge memory bridge) { + return init(Bridge({ + source: source, + destination: destination, sourceCrossChainMessenger: getCircleMessengerFromChainAlias(source.chain.chainAlias), destinationCrossChainMessenger: getCircleMessengerFromChainAlias(destination.chain.chainAlias), lastSourceLogIndex: 0, @@ -45,7 +51,7 @@ library CCTPBridgeTesting { } } - function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { + function init(Bridge memory bridge) internal returns (Bridge memory) { // Set minimum required signatures to zero for both domains bridge.destination.selectFork(); vm.store( @@ -61,14 +67,16 @@ library CCTPBridgeTesting { ); vm.recordLogs(); + + return bridge; } - function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); - Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, SENT_MESSAGE_TOPIC, address(0)); + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, SENT_MESSAGE_TOPIC, bridge.sourceCrossChainMessenger); for (uint256 i = 0; i < logs.length; i++) { - bridge.destinationCrossChainMessenger.receiveMessage(abi.decode(logs[i].data, (bytes)), ""); + IMessenger(bridge.destinationCrossChainMessenger).receiveMessage(abi.decode(logs[i].data, (bytes)), ""); } if (!switchToDestinationFork) { @@ -76,12 +84,12 @@ library CCTPBridgeTesting { } } - function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); - Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, address(0)); + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, bridge.destinationCrossChainMessenger); for (uint256 i = 0; i < logs.length; i++) { - bridge.sourceCrossChainMessenger.receiveMessage(abi.decode(logs[i].data, (bytes)), ""); + IMessenger(bridge.sourceCrossChainMessenger).receiveMessage(abi.decode(logs[i].data, (bytes)), ""); } if (!switchToSourceFork) { diff --git a/src/testing/bridges/OptimismBridgeTesting.sol b/src/testing/bridges/OptimismBridgeTesting.sol index a3547bb..54edde4 100644 --- a/src/testing/bridges/OptimismBridgeTesting.sol +++ b/src/testing/bridges/OptimismBridgeTesting.sol @@ -3,62 +3,44 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; -import { BridgeData } from "./BridgeData.sol"; - -interface InboxLike { - function createRetryableTicket( - address destAddr, - uint256 arbTxCallValue, - uint256 maxSubmissionCost, - address submissionRefundAddress, - address valueRefundAddress, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data - ) external payable returns (uint256); - function bridge() external view returns (BridgeLike); -} - -interface BridgeLike { - function rollup() external view returns (address); - function executeCall( - address, - uint256, - bytes calldata - ) external returns (bool, bytes memory); - function setOutbox(address, bool) external; -} - -contract ArbSysOverride { - - event SendTxToL1(address sender, address target, bytes data); - - function sendTxToL1(address target, bytes calldata message) external payable returns (uint256) { - emit SendTxToL1(msg.sender, target, message); - return 0; - } - +import { Bridge } from "src/testing/Bridge.sol"; +import { Domain, DomainHelpers } from "src/testing/Domain.sol"; +import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; + +interface IMessenger { + function sendMessage( + address target, + bytes memory message, + uint32 gasLimit + ) external; + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) external payable; } library OptimismBridgeTesting { using DomainHelpers for *; + using RecordedLogs for *; Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - bytes32 private constant MESSAGE_DELIVERED_TOPIC = keccak256("MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"); - bytes32 private constant SEND_TO_L1_TOPIC = keccak256("SendTxToL1(address,address,bytes)"); + bytes32 private constant SENT_MESSAGE_TOPIC = keccak256("SentMessage(address,address,bytes,uint256,uint256)"); - function createNativeBridge(Domain memory ethereum, Domain memory arbitrumInstance) internal returns (BridgeData memory bridge) { + function createNativeBridge(Domain memory ethereum, Domain memory optimismInstance) internal returns (Bridge memory bridge) { ( address sourceCrossChainMessenger, address destinationCrossChainMessenger - ) = getMessengerFromChainAlias(ethereum.chain.chainAlias, arbitrumInstance.chain.chainAlias) + ) = getMessengerFromChainAlias(ethereum.chain.chainAlias, optimismInstance.chain.chainAlias); - return init(BridgeData({ + return init(Bridge({ source: ethereum, - destination: arbitrumInstance, + destination: optimismInstance, sourceCrossChainMessenger: sourceCrossChainMessenger, destinationCrossChainMessenger: destinationCrossChainMessenger, lastSourceLogIndex: 0, @@ -77,68 +59,40 @@ library OptimismBridgeTesting { require(keccak256(bytes(sourceChainAlias)) == keccak256("mainnet"), "Source must be Ethereum."); bytes32 name = keccak256(bytes(destinationChainAlias)); - if (name == keccak256("arbitrum_one")) { - sourceCrossChainMessenger = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; - } else if (name == keccak256("arbitrum_nova")) { - sourceCrossChainMessenger = 0xc4448b71118c9071Bcb9734A0EAc55D18A153949; + if (name == keccak256("optimism")) { + sourceCrossChainMessenger = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + } else if (name == keccak256("base")) { + sourceCrossChainMessenger = 0x866E82a600A1414e583f7F13623F1aC5d58b0Afa; } else { revert("Unsupported destination chain"); } - destinationCrossChainMessenger = 0x0000000000000000000000000000000000000064; + destinationCrossChainMessenger = 0x4200000000000000000000000000000000000007; } - function init(BridgeData memory bridge) internal returns (BridgeData memory bridge) { - bridge.source.selectFork(); - BridgeLike underlyingBridge = InboxLike(data.sourceCrossChainMessenger).bridge(); + function init(Bridge memory bridge) internal returns (Bridge memory) { vm.recordLogs(); - // Make this contract a valid outbox - address _rollup = underlyingBridge.rollup(); - vm.store( - address(underlyingBridge), - bytes32(uint256(8)), - bytes32(uint256(uint160(address(this)))) - ); - underlyingBridge.setOutbox(address(this), true); - vm.store( - address(underlyingBridge), - bytes32(uint256(8)), - bytes32(uint256(uint160(_rollup))) - ); - - // Need to replace ArbSys contract with custom code to make it compatible with revm - bridge.destination.selectFork(); - bytes memory bytecode = vm.getCode("ArbitrumBridgeTesting.sol:ArbSysOverride"); - address deployed; - assembly { - deployed := create(0, add(bytecode, 0x20), mload(bytecode)) - } - vm.etch(ARB_SYS, deployed.code); - + // For consistency with other bridges bridge.source.selectFork(); + + return bridge; } - function relayMessagesToDestination(BridgeData memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); - // Read all L1 -> L2 messages and relay them under Arbitrum fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastFromHostLogIndex < logs.length; lastFromHostLogIndex++) { - Vm.Log memory log = logs[lastFromHostLogIndex]; - if (log.topics[0] == MESSAGE_DELIVERED_TOPIC) { - // We need both the current event and the one that follows for all the relevant data - Vm.Log memory logWithData = logs[lastFromHostLogIndex + 1]; - (,, address sender,,,) = abi.decode(log.data, (address, uint8, address, bytes32, uint256, uint64)); - (address target, bytes memory message) = _parseData(logWithData.data); - vm.startPrank(sender); - (bool success, bytes memory response) = target.call(message); - vm.stopPrank(); - if (!success) { - assembly { - revert(add(response, 32), mload(response)) - } - } - } + address malias; + unchecked { + malias = address(uint160(bridge.sourceCrossChainMessenger) + uint160(0x1111000000000000000000000000000000001111)); + } + + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, SENT_MESSAGE_TOPIC, bridge.sourceCrossChainMessenger); + for (uint256 i = 0; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + address target = address(uint160(uint256(log.topics[1]))); + (address sender, bytes memory message, uint256 nonce, uint256 gasLimit) = abi.decode(log.data, (address, bytes, uint256, uint256)); + vm.prank(malias); + IMessenger(bridge.destinationCrossChainMessenger).relayMessage(nonce, sender, target, 0, gasLimit, message); } if (!switchToDestinationFork) { @@ -146,23 +100,33 @@ library OptimismBridgeTesting { } } - function relayMessagesToSource(BridgeData memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); - // Read all L2 -> L1 messages and relay them under host fork - Vm.Log[] memory logs = RecordedLogs.getLogs(); - for (; lastToHostLogIndex < logs.length; lastToHostLogIndex++) { - Vm.Log memory log = logs[lastToHostLogIndex]; - if (log.topics[0] == SEND_TO_L1_TOPIC) { - (address sender, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); - //l2ToL1Sender = sender; - (bool success, bytes memory response) = InboxLike(data.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, bridge.sourceCrossChainMessenger); + for (uint256 i = 0; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + address target = address(uint160(uint256(log.topics[1]))); + (address sender, bytes memory message,,) = abi.decode(log.data, (address, bytes, uint256, uint256)); + // Set xDomainMessageSender + vm.store( + bridge.sourceCrossChainMessenger, + bytes32(uint256(204)), + bytes32(uint256(uint160(sender))) + ); + vm.startPrank(bridge.sourceCrossChainMessenger); + (bool success, bytes memory response) = target.call(message); + vm.stopPrank(); + vm.store( + bridge.sourceCrossChainMessenger, + bytes32(uint256(204)), + bytes32(uint256(0)) + ); if (!success) { assembly { revert(add(response, 32), mload(response)) } } - } } if (!switchToSourceFork) { @@ -170,14 +134,4 @@ library OptimismBridgeTesting { } } - function _parseData(bytes memory orig) private pure returns (address target, bytes memory message) { - // FIXME - this is not robust enough, only handling messages of a specific format - uint256 mlen; - (,,target ,,,,,,,, mlen) = abi.decode(orig, (uint256, uint256, address, uint256, uint256, uint256, address, address, uint256, uint256, uint256)); - message = new bytes(mlen); - for (uint256 i = 0; i < mlen; i++) { - message[i] = orig[i + 352]; - } - } - } diff --git a/src/testing/utils/RecordedLogs.sol b/src/testing/utils/RecordedLogs.sol index ca62d29..4912a24 100644 --- a/src/testing/utils/RecordedLogs.sol +++ b/src/testing/utils/RecordedLogs.sol @@ -3,6 +3,8 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; +import { Bridge } from "src/testing/Bridge.sol"; + library RecordedLogs { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -31,7 +33,7 @@ library RecordedLogs { return logs; } - function ingestAndFilterLogs(BridgeData memory bridge, bool sourceToDestination, bytes32 topic, address emitter) internal pure returns (Vm.Log[] memory filteredLogs) { + function ingestAndFilterLogs(Bridge memory bridge, bool sourceToDestination, bytes32 topic0, bytes32 topic1, address emitter) internal returns (Vm.Log[] memory filteredLogs) { Vm.Log[] memory logs = RecordedLogs.getLogs(); uint256 lastIndex = sourceToDestination ? bridge.lastSourceLogIndex : bridge.lastDestinationLogIndex; uint256 pushedIndex = 0; @@ -40,14 +42,19 @@ library RecordedLogs { for (; lastIndex < logs.length; lastIndex++) { Vm.Log memory log = logs[lastIndex]; - if (log.topics[0] == topic && log.emitter == emitter) { + if ((log.topics[0] == topic0 || log.topics[0] == topic1) && log.emitter == emitter) { filteredLogs[pushedIndex++] = log; } } if (sourceToDestination) bridge.lastSourceLogIndex = lastIndex; else bridge.lastDestinationLogIndex = lastIndex; - filteredLogs.length = pushedIndex; + // Reduce the array length + assembly { mstore(filteredLogs, pushedIndex) } + } + + function ingestAndFilterLogs(Bridge memory bridge, bool sourceToDestination, bytes32 topic, address emitter) internal returns (Vm.Log[] memory filteredLogs) { + return ingestAndFilterLogs(bridge, sourceToDestination, topic, bytes32(0), emitter); } } diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index 441e879..6f63ac9 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; -import { ArbitrumDomain, ArbSysOverride } from "../src/testing/ArbitrumDomain.sol"; +import { ArbitrumBridgeTesting, ArbSysOverride } from "src/testing/bridges/ArbitrumBridgeTesting.sol"; -import { ArbitrumReceiver } from "../src/ArbitrumReceiver.sol"; +import { ArbitrumReceiver } from "src/ArbitrumReceiver.sol"; contract MessageOrderingArbitrum is MessageOrdering, ArbitrumReceiver { @@ -19,21 +19,24 @@ contract MessageOrderingArbitrum is MessageOrdering, ArbitrumReceiver { contract ArbitrumIntegrationTest is IntegrationBaseTest { + using ArbitrumBridgeTesting for *; + using DomainHelpers for *; + function test_arbitrumOne() public { - checkArbitrumStyle(new ArbitrumDomain(getChain("arbitrum_one"), mainnet)); + checkArbitrumStyle(getChain("arbitrum_one").createFork()); } function test_arbitrumNova() public { - checkArbitrumStyle(new ArbitrumDomain(getChain("arbitrum_nova"), mainnet)); + checkArbitrumStyle(getChain("arbitrum_nova").createFork()); } - function checkArbitrumStyle(ArbitrumDomain arbitrum) public { + function checkArbitrumStyle(Domain memory arbitrum) public { + Bridge memory bridge = ArbitrumBridgeTesting.createNativeBridge(mainnet, arbitrum); + deal(l1Authority, 100 ether); deal(notL1Authority, 100 ether); - Domain host = arbitrum.hostDomain(); - - host.selectFork(); + mainnet.selectFork(); MessageOrdering moHost = new MessageOrdering(); @@ -42,11 +45,11 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { MessageOrdering moArbitrum = new MessageOrderingArbitrum(l1Authority); // Queue up some L2 -> L1 messages - ArbSysOverride(arbitrum.ARB_SYS()).sendTxToL1( + ArbSysOverride(bridge.destinationCrossChainMessenger).sendTxToL1( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); - ArbSysOverride(arbitrum.ARB_SYS()).sendTxToL1( + ArbSysOverride(bridge.destinationCrossChainMessenger).sendTxToL1( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4) ); @@ -54,12 +57,12 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { assertEq(moArbitrum.length(), 0); // Do not relay right away - host.selectFork(); + mainnet.selectFork(); // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); XChainForwarders.sendMessageArbitrum( - address(arbitrum.INBOX()), + bridge.sourceCrossChainMessenger, address(moArbitrum), abi.encodeWithSelector(MessageOrdering.push.selector, 1), 100000, @@ -67,7 +70,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { block.basefee + 10 gwei ); XChainForwarders.sendMessageArbitrum( - address(arbitrum.INBOX()), + bridge.sourceCrossChainMessenger, address(moArbitrum), abi.encodeWithSelector(MessageOrdering.push.selector, 2), 100000, @@ -78,13 +81,13 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { assertEq(moHost.length(), 0); - arbitrum.relayFromHost(true); + bridge.relayMessagesToDestination(true); assertEq(moArbitrum.length(), 2); assertEq(moArbitrum.messages(0), 1); assertEq(moArbitrum.messages(1), 2); - arbitrum.relayToHost(true); + bridge.relayMessagesToSource(true); assertEq(moHost.length(), 2); assertEq(moHost.messages(0), 3); @@ -93,7 +96,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure mode vm.startPrank(notL1Authority); XChainForwarders.sendMessageArbitrum( - address(arbitrum.INBOX()), + bridge.sourceCrossChainMessenger, address(moArbitrum), abi.encodeWithSelector(MessageOrdering.push.selector, 999), 100000, @@ -103,7 +106,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { vm.stopPrank(); vm.expectRevert("Receiver/invalid-l1Authority"); - arbitrum.relayFromHost(true); + bridge.relayMessagesToDestination(true); } } diff --git a/test/CCTPIntegration.t.sol b/test/CCTPIntegration.t.sol index a3ed28d..9e04a54 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CCTPIntegration.t.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; -import { CircleCCTPDomain } from "../src/testing/CircleCCTPDomain.sol"; +import { CCTPBridgeTesting } from "src/testing/bridges/CCTPBridgeTesting.sol"; -import { CCTPReceiver } from "../src/CCTPReceiver.sol"; +import { CCTPReceiver } from "src/CCTPReceiver.sol"; contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { @@ -27,49 +27,48 @@ contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { contract CircleCCTPIntegrationTest is IntegrationBaseTest { + using CCTPBridgeTesting for *; + using DomainHelpers for *; + address l2Authority = makeAddr("l2Authority"); function test_avalanche() public { - CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("avalanche"), mainnet); - checkCircleCCTPStyle(cctp, 1); + checkCircleCCTPStyle(getChain("avalanche").createFork(), 1); } function test_optimism() public { - CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("optimism"), mainnet); - checkCircleCCTPStyle(cctp, 2); + checkCircleCCTPStyle(getChain("optimism").createFork(), 2); } function test_arbitrum_one() public { - CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("arbitrum_one"), mainnet); - checkCircleCCTPStyle(cctp, 3); + checkCircleCCTPStyle(getChain("arbitrum_one").createFork(), 3); } function test_base() public { - CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("base"), mainnet); - checkCircleCCTPStyle(cctp, 6); + checkCircleCCTPStyle(getChain("base").createFork(), 6); } function test_polygon() public { - CircleCCTPDomain cctp = new CircleCCTPDomain(getChain("polygon"), mainnet); - checkCircleCCTPStyle(cctp, 7); + checkCircleCCTPStyle(getChain("polygon").createFork(), 7); } - function checkCircleCCTPStyle(CircleCCTPDomain cctp, uint32 destinationDomainId) public { - Domain host = cctp.hostDomain(); + function checkCircleCCTPStyle(Domain memory destination, uint32 destinationDomainId) public { + Bridge memory bridge = CCTPBridgeTesting.createCircleBridge(mainnet, destination); + uint32 sourceDomainId = 0; // Ethereum - host.selectFork(); + mainnet.selectFork(); MessageOrderingCCTP moHost = new MessageOrderingCCTP( - address(cctp.SOURCE_MESSENGER()), + bridge.sourceCrossChainMessenger, destinationDomainId, l2Authority ); - cctp.selectFork(); + destination.selectFork(); MessageOrderingCCTP moCCTP = new MessageOrderingCCTP( - address(cctp.DESTINATION_MESSENGER()), + bridge.destinationCrossChainMessenger, sourceDomainId, l1Authority ); @@ -77,13 +76,13 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Queue up some L2 -> L1 messages vm.startPrank(l2Authority); XChainForwarders.sendMessageCCTP( - address(cctp.DESTINATION_MESSENGER()), + bridge.destinationCrossChainMessenger, sourceDomainId, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); XChainForwarders.sendMessageCCTP( - address(cctp.DESTINATION_MESSENGER()), + bridge.destinationCrossChainMessenger, sourceDomainId, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4) @@ -93,7 +92,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { assertEq(moCCTP.length(), 0); // Do not relay right away - host.selectFork(); + mainnet.selectFork(); // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); @@ -111,13 +110,13 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { assertEq(moHost.length(), 0); - cctp.relayFromHost(true); + bridge.relayMessagesToDestination(true); assertEq(moCCTP.length(), 2); assertEq(moCCTP.messages(0), 1); assertEq(moCCTP.messages(1), 2); - cctp.relayToHost(true); + bridge.relayMessagesToSource(true); assertEq(moHost.length(), 2); assertEq(moHost.messages(0), 3); @@ -133,9 +132,9 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { vm.stopPrank(); vm.expectRevert("Receiver/invalid-sourceAuthority"); - cctp.relayFromHost(true); + bridge.relayMessagesToDestination(true); - cctp.selectFork(); + destination.selectFork(); vm.expectRevert("Receiver/invalid-sender"); moCCTP.push(999); @@ -143,7 +142,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { moCCTP.handleReceiveMessage(0, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); assertEq(moCCTP.sourceDomainId(), 0); - vm.prank(address(cctp.DESTINATION_MESSENGER())); + vm.prank(bridge.destinationCrossChainMessenger); vm.expectRevert("Receiver/invalid-sourceDomain"); moCCTP.handleReceiveMessage(1, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); } diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index 0828082..c6b94f5 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -3,9 +3,13 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; -import { GnosisDomain } from "../src/testing/GnosisDomain.sol"; +import { AMBBridgeTesting } from "src/testing/bridges/AMBBridgeTesting.sol"; -import { GnosisReceiver } from "../src/GnosisReceiver.sol"; +import { GnosisReceiver } from "src/GnosisReceiver.sol"; + +interface IAMB { + function requireToPassMessage(address, bytes memory, uint256) external returns (bytes32); +} contract MessageOrderingGnosis is MessageOrdering, GnosisReceiver { @@ -18,30 +22,33 @@ contract MessageOrderingGnosis is MessageOrdering, GnosisReceiver { } contract GnosisIntegrationTest is IntegrationBaseTest { + + using AMBBridgeTesting for *; + using DomainHelpers for *; function test_gnosisChain() public { - checkGnosisStyle(new GnosisDomain(getChain('gnosis_chain'), mainnet), 0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59); + checkGnosisStyle(getChain('gnosis_chain').createFork()); } - function checkGnosisStyle(GnosisDomain gnosis, address _l2CrossDomain) public { - Domain host = gnosis.hostDomain(); + function checkGnosisStyle(Domain memory gnosis) public { + Bridge memory bridge = AMBBridgeTesting.createGnosisBridge(mainnet, gnosis); - host.selectFork(); + mainnet.selectFork(); MessageOrdering moHost = new MessageOrdering(); uint256 _chainId = block.chainid; gnosis.selectFork(); - MessageOrderingGnosis moGnosis = new MessageOrderingGnosis(_l2CrossDomain, _chainId, l1Authority); + MessageOrderingGnosis moGnosis = new MessageOrderingGnosis(bridge.destinationCrossChainMessenger, _chainId, l1Authority); // Queue up some L2 -> L1 messages - gnosis.L2_AMB_CROSS_DOMAIN_MESSENGER().requireToPassMessage( + IAMB(bridge.destinationCrossChainMessenger).requireToPassMessage( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3), 100000 ); - gnosis.L2_AMB_CROSS_DOMAIN_MESSENGER().requireToPassMessage( + IAMB(bridge.destinationCrossChainMessenger).requireToPassMessage( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4), 100000 @@ -50,18 +57,18 @@ contract GnosisIntegrationTest is IntegrationBaseTest { assertEq(moGnosis.length(), 0); // Do not relay right away - host.selectFork(); + mainnet.selectFork(); // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); XChainForwarders.sendMessageGnosis( - address(gnosis.L1_AMB_CROSS_DOMAIN_MESSENGER()), + bridge.sourceCrossChainMessenger, address(moGnosis), abi.encodeWithSelector(MessageOrdering.push.selector, 1), 100000 ); XChainForwarders.sendMessageGnosis( - address(gnosis.L1_AMB_CROSS_DOMAIN_MESSENGER()), + bridge.sourceCrossChainMessenger, address(moGnosis), abi.encodeWithSelector(MessageOrdering.push.selector, 2), 100000 @@ -70,13 +77,13 @@ contract GnosisIntegrationTest is IntegrationBaseTest { assertEq(moHost.length(), 0); - gnosis.relayFromHost(true); + bridge.relayMessagesToDestination(true); assertEq(moGnosis.length(), 2); assertEq(moGnosis.messages(0), 1); assertEq(moGnosis.messages(1), 2); - gnosis.relayToHost(true); + bridge.relayMessagesToSource(true); assertEq(moHost.length(), 2); assertEq(moHost.messages(0), 3); @@ -85,7 +92,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure modes vm.startPrank(notL1Authority); XChainForwarders.sendMessageGnosis( - address(gnosis.L1_AMB_CROSS_DOMAIN_MESSENGER()), + bridge.sourceCrossChainMessenger, address(moGnosis), abi.encodeWithSelector(MessageOrdering.push.selector, 999), 100000 @@ -94,7 +101,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { // The revert is caught so it doesn't propagate // Just look at the no change to verify it didn't go through - gnosis.relayFromHost(true); + bridge.relayMessagesToDestination(true); assertEq(moGnosis.length(), 2); // No change gnosis.selectFork(); diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index b9df3e7..49405a4 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -3,7 +3,8 @@ pragma solidity >=0.8.0; import "forge-std/Test.sol"; -import { Domain } from "../src/testing/Domain.sol"; +import { Bridge } from "src/testing/Bridge.sol"; +import { Domain, DomainHelpers } from "src/testing/Domain.sol"; import { XChainForwarders } from "../src/XChainForwarders.sol"; @@ -23,13 +24,15 @@ contract MessageOrdering { abstract contract IntegrationBaseTest is Test { + using DomainHelpers for *; + Domain mainnet; address l1Authority = makeAddr("l1Authority"); address notL1Authority = makeAddr("notL1Authority"); function setUp() public { - mainnet = new Domain(getChain("mainnet")); + mainnet = getChain("mainnet").createFork(); } } diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index 69e6793..292c685 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; -import { OptimismDomain } from "../src/testing/OptimismDomain.sol"; +import { OptimismBridgeTesting, IMessenger } from "src/testing/bridges/OptimismBridgeTesting.sol"; -import { OptimismReceiver } from "../src/OptimismReceiver.sol"; +import { OptimismReceiver } from "src/OptimismReceiver.sol"; contract MessageOrderingOptimism is MessageOrdering, OptimismReceiver { @@ -19,20 +19,23 @@ contract MessageOrderingOptimism is MessageOrdering, OptimismReceiver { contract OptimismIntegrationTest is IntegrationBaseTest { + using OptimismBridgeTesting for *; + using DomainHelpers for *; + event FailedRelayedMessage(bytes32); function test_optimism() public { - checkOptimismStyle(new OptimismDomain(getChain("optimism"), mainnet)); + checkOptimismStyle(getChain("optimism").createFork()); } function test_base() public { - checkOptimismStyle(new OptimismDomain(getChain("base"), mainnet)); + checkOptimismStyle(getChain("base").createFork()); } - function checkOptimismStyle(OptimismDomain optimism) public { - Domain host = optimism.hostDomain(); + function checkOptimismStyle(Domain memory optimism) public { + Bridge memory bridge = OptimismBridgeTesting.createNativeBridge(mainnet, optimism); - host.selectFork(); + mainnet.selectFork(); MessageOrdering moHost = new MessageOrdering(); @@ -41,12 +44,12 @@ contract OptimismIntegrationTest is IntegrationBaseTest { MessageOrdering moOptimism = new MessageOrderingOptimism(l1Authority); // Queue up some L2 -> L1 messages - optimism.L2_MESSENGER().sendMessage( + IMessenger(bridge.destinationCrossChainMessenger).sendMessage( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3), 100000 ); - optimism.L2_MESSENGER().sendMessage( + IMessenger(bridge.destinationCrossChainMessenger).sendMessage( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4), 100000 @@ -55,18 +58,18 @@ contract OptimismIntegrationTest is IntegrationBaseTest { assertEq(moOptimism.length(), 0); // Do not relay right away - host.selectFork(); + mainnet.selectFork(); // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); XChainForwarders.sendMessageOptimism( - address(optimism.L1_MESSENGER()), + bridge.sourceCrossChainMessenger, address(moOptimism), abi.encodeWithSelector(MessageOrdering.push.selector, 1), 100000 ); XChainForwarders.sendMessageOptimism( - address(optimism.L1_MESSENGER()), + bridge.sourceCrossChainMessenger, address(moOptimism), abi.encodeWithSelector(MessageOrdering.push.selector, 2), 100000 @@ -75,13 +78,13 @@ contract OptimismIntegrationTest is IntegrationBaseTest { assertEq(moHost.length(), 0); - optimism.relayFromHost(true); + bridge.relayMessagesToDestination(true); assertEq(moOptimism.length(), 2); assertEq(moOptimism.messages(0), 1); assertEq(moOptimism.messages(1), 2); - optimism.relayToHost(true); + bridge.relayMessagesToSource(true); assertEq(moHost.length(), 2); assertEq(moHost.messages(0), 3); @@ -90,7 +93,7 @@ contract OptimismIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure modes vm.startPrank(notL1Authority); XChainForwarders.sendMessageOptimism( - address(optimism.L1_MESSENGER()), + bridge.sourceCrossChainMessenger, address(moOptimism), abi.encodeWithSelector(MessageOrdering.push.selector, 999), 100000 @@ -99,7 +102,7 @@ contract OptimismIntegrationTest is IntegrationBaseTest { // The revert is caught so it doesn't propagate // Just look at the no change to verify it didn't go through - optimism.relayFromHost(true); + bridge.relayMessagesToDestination(true); assertEq(moOptimism.length(), 2); // No change optimism.selectFork(); From 17d300c85b4e1caa3acd578f7c0c3cae83717f8d Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Fri, 24 May 2024 21:44:05 +0200 Subject: [PATCH 15/42] fix optimism --- src/testing/bridges/OptimismBridgeTesting.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/bridges/OptimismBridgeTesting.sol b/src/testing/bridges/OptimismBridgeTesting.sol index 54edde4..97626a5 100644 --- a/src/testing/bridges/OptimismBridgeTesting.sol +++ b/src/testing/bridges/OptimismBridgeTesting.sol @@ -103,7 +103,7 @@ library OptimismBridgeTesting { function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); - Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, bridge.sourceCrossChainMessenger); + Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, bridge.destinationCrossChainMessenger); for (uint256 i = 0; i < logs.length; i++) { Vm.Log memory log = logs[i]; address target = address(uint160(uint256(log.topics[1]))); From 8975d01f7d98982b1e4ee55f8b3b0946953b9802 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 29 May 2024 16:16:40 -0500 Subject: [PATCH 16/42] fix AMB --- src/testing/bridges/AMBBridgeTesting.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/testing/bridges/AMBBridgeTesting.sol b/src/testing/bridges/AMBBridgeTesting.sol index 320d3fe..e1b28c6 100644 --- a/src/testing/bridges/AMBBridgeTesting.sol +++ b/src/testing/bridges/AMBBridgeTesting.sol @@ -98,13 +98,8 @@ library AMBBridgeTesting { Vm.Log memory log = logs[i]; bytes memory messageToRelay = abi.decode(log.data, (bytes)); if (log.topics[0] == USER_REQUEST_FOR_AFFIRMATION_TOPIC) { - IValidatorContract validatorContract = IValidatorContract(IAMB(amb).validatorContract()); - address[] memory validators = validatorContract.validatorList(); - uint256 requiredSignatures = validatorContract.requiredSignatures(); - for (uint256 o = 0; o < requiredSignatures; o++) { - vm.prank(validators[o]); - IAMB(amb).executeAffirmation(messageToRelay); - } + vm.prank(IValidatorContract(IAMB(amb).validatorContract()).validatorList()[0]); + IAMB(amb).executeAffirmation(messageToRelay); } else if (log.topics[0] == USER_REQUEST_FOR_SIGNATURE_TOPIC) { IAMB(amb).executeSignatures(messageToRelay, abi.encodePacked(uint256(0))); } From 46c876f485efa8b88b760e1bb19f5382882b8537 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 29 May 2024 16:33:46 -0500 Subject: [PATCH 17/42] got arbitrum working --- src/testing/bridges/ArbitrumBridgeTesting.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index 7f66b5c..32012c9 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -7,6 +7,8 @@ import { Bridge } from "src/testing/Bridge.sol"; import { Domain, DomainHelpers } from "src/testing/Domain.sol"; import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import {console} from "./../../../lib/forge-std/src/console.sol"; + interface InboxLike { function createRetryableTicket( address destAddr, @@ -103,6 +105,7 @@ library ArbitrumBridgeTesting { bridge.source.selectFork(); BridgeLike underlyingBridge = InboxLike(bridge.sourceCrossChainMessenger).bridge(); + bridge.extraData = abi.encode(address(underlyingBridge)); // Make this contract a valid outbox address _rollup = underlyingBridge.rollup(); @@ -127,7 +130,7 @@ library ArbitrumBridgeTesting { Vm.Log[] memory logs = RecordedLogs.getLogs(); for (; bridge.lastSourceLogIndex < logs.length; bridge.lastSourceLogIndex++) { Vm.Log memory log = logs[bridge.lastSourceLogIndex]; - if (log.topics[0] == MESSAGE_DELIVERED_TOPIC && log.emitter == bridge.sourceCrossChainMessenger) { + if (log.topics[0] == MESSAGE_DELIVERED_TOPIC && log.emitter == abi.decode(bridge.extraData, (address))) { // We need both the current event and the one that follows for all the relevant data Vm.Log memory logWithData = logs[bridge.lastSourceLogIndex + 1]; (,, address sender,,,) = abi.decode(log.data, (address, uint8, address, bytes32, uint256, uint64)); From 7af6b4c63a96c06b002ebe2c99e3327800ba3e6a Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sat, 1 Jun 2024 12:57:46 -0500 Subject: [PATCH 18/42] refactor XChainForwaders into separate libraries and support both directions on every bridge --- src/XChainForwarders.sol | 262 ------------------ src/forwarders/AMBForwarder.sol | 52 ++++ src/forwarders/ArbitrumForwarder.sol | 94 +++++++ src/forwarders/CCTPForwarder.sol | 57 ++++ src/forwarders/OptimismForwarder.sol | 65 +++++ test/ArbitrumIntegration.t.sol | 13 +- ...tion.t.sol => CircleCCTPIntegration.t.sol} | 28 +- test/GnosisIntegration.t.sol | 22 +- test/IntegrationBase.t.sol | 2 - test/OptimismIntegration.t.sol | 13 +- 10 files changed, 306 insertions(+), 302 deletions(-) delete mode 100644 src/XChainForwarders.sol create mode 100644 src/forwarders/AMBForwarder.sol create mode 100644 src/forwarders/ArbitrumForwarder.sol create mode 100644 src/forwarders/CCTPForwarder.sol create mode 100644 src/forwarders/OptimismForwarder.sol rename test/{CCTPIntegration.t.sol => CircleCCTPIntegration.t.sol} (83%) diff --git a/src/XChainForwarders.sol b/src/XChainForwarders.sol deleted file mode 100644 index 4d0e482..0000000 --- a/src/XChainForwarders.sol +++ /dev/null @@ -1,262 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.0; - -interface ICrossDomainOptimism { - function sendMessage(address _target, bytes calldata _message, uint32 _gasLimit) external; -} - -interface ICrossDomainArbitrum { - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) external view returns (uint256); -} - -interface ICrossDomainGnosis { - function requireToPassMessage(address _contract, bytes memory _data, uint256 _gas) external returns (bytes32); -} - -interface ICrossDomainZkEVM { - function bridgeMessage( - uint32 destinationNetwork, - address destinationAddress, - bool forceUpdateGlobalExitRoot, - bytes calldata metadata - ) external payable; -} - -interface ICrossDomainCircleCCTP { - function sendMessage( - uint32 destinationDomain, - bytes32 recipient, - bytes calldata messageBody - ) external; -} - -/** - * @title XChainForwarders - * @notice Helper functions to abstract over L1 -> L2 message passing. - * @dev General structure is sendMessageXXX(target, message, gasLimit) where XXX is the remote domain name (IE OptimismMainnet, ArbitrumOne, Base, etc). - */ -library XChainForwarders { - - /// ================================ Optimism Style ================================ - - function sendMessageOptimism( - address l1CrossDomain, - address target, - bytes memory message, - uint256 gasLimit - ) internal { - ICrossDomainOptimism(l1CrossDomain).sendMessage( - target, - message, - uint32(gasLimit) - ); - } - - function sendMessageOptimismMainnet( - address target, - bytes memory message, - uint256 gasLimit - ) internal { - sendMessageOptimism( - 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1, - target, - message, - uint32(gasLimit) - ); - } - - function sendMessageBase( - address target, - bytes memory message, - uint256 gasLimit - ) internal { - sendMessageOptimism( - 0x866E82a600A1414e583f7F13623F1aC5d58b0Afa, - target, - message, - uint32(gasLimit) - ); - } - - /// ================================ Arbitrum Style ================================ - - function sendMessageArbitrum( - address l1CrossDomain, - address target, - bytes memory message, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 baseFee - ) internal { - uint256 maxSubmission = ICrossDomainArbitrum(l1CrossDomain).calculateRetryableSubmissionFee(message.length, baseFee); - uint256 maxRedemption = gasLimit * maxFeePerGas; - ICrossDomainArbitrum(l1CrossDomain).createRetryableTicket{value: maxSubmission + maxRedemption}( - target, - 0, // we always assume that l2CallValue = 0 - maxSubmission, - address(0), // burn the excess gas - address(0), // burn the excess gas - gasLimit, - maxFeePerGas, - message - ); - } - - function sendMessageArbitrumOne( - address target, - bytes memory message, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 baseFee - ) internal { - sendMessageArbitrum( - 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f, - target, - message, - gasLimit, - maxFeePerGas, - baseFee - ); - } - - function sendMessageArbitrumNova( - address target, - bytes memory message, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 baseFee - ) internal { - sendMessageArbitrum( - 0xc4448b71118c9071Bcb9734A0EAc55D18A153949, - target, - message, - gasLimit, - maxFeePerGas, - baseFee - ); - } - - /// ================================ Gnosis ================================ - - function sendMessageGnosis( - address l1CrossDomain, - address target, - bytes memory message, - uint256 gasLimit - ) internal { - ICrossDomainGnosis(l1CrossDomain).requireToPassMessage( - target, - message, - gasLimit - ); - } - - function sendMessageGnosis( - address target, - bytes memory message, - uint256 gasLimit - ) internal { - sendMessageGnosis( - 0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e, - target, - message, - gasLimit - ); - } - - /// ================================ zkEVM ================================ - - function sendMessageZkEVM( - address l1CrossDomain, - uint32 destinationNetworkId, - address destinationAddress, - bool forceUpdateGlobalExitRoot, - bytes memory metadata - ) internal { - ICrossDomainZkEVM(l1CrossDomain).bridgeMessage( - destinationNetworkId, - destinationAddress, - forceUpdateGlobalExitRoot, - metadata - ); - } - - function sendMessageZkEVM( - address destinationAddress, - bytes memory metadata - ) internal { - sendMessageZkEVM( - 0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe, - 1, - destinationAddress, - true, - metadata - ); - } - - /// ================================ CCTP ================================ - - function sendMessageCCTP( - address sourceMessenger, - uint32 destinationDomainId, - bytes32 recipient, - bytes memory messageBody - ) internal { - ICrossDomainCircleCCTP(sourceMessenger).sendMessage( - destinationDomainId, - recipient, - messageBody - ); - } - - function sendMessageCCTP( - address sourceMessenger, - uint32 destinationDomainId, - address recipient, - bytes memory messageBody - ) internal { - sendMessageCCTP( - sourceMessenger, - destinationDomainId, - bytes32(uint256(uint160(recipient))), - messageBody - ); - } - - function sendMessageCircleCCTP( - uint32 destinationDomainId, - bytes32 recipient, - bytes memory messageBody - ) internal { - sendMessageCCTP( - 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81, - destinationDomainId, - recipient, - messageBody - ); - } - - function sendMessageCircleCCTP( - uint32 destinationDomainId, - address recipient, - bytes memory messageBody - ) internal { - sendMessageCCTP( - 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81, - destinationDomainId, - bytes32(uint256(uint160(recipient))), - messageBody - ); - } - -} diff --git a/src/forwarders/AMBForwarder.sol b/src/forwarders/AMBForwarder.sol new file mode 100644 index 0000000..3c6a344 --- /dev/null +++ b/src/forwarders/AMBForwarder.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface IArbitraryMessagingBridge { + function requireToPassMessage(address _contract, bytes memory _data, uint256 _gas) external returns (bytes32); +} + +library AMBForwarder { + + address constant internal GNOSIS_AMB_ETHEREUM = 0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e; + address constant internal GNOSIS_AMB_GNOSIS_CHAIN = 0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59; + + function sendMessage( + address amb, + address target, + bytes memory message, + uint256 gasLimit + ) internal { + IArbitraryMessagingBridge(amb).requireToPassMessage( + target, + message, + gasLimit + ); + } + + function sendMessageEthereumToGnosisChain( + address target, + bytes memory message, + uint256 gasLimit + ) internal { + sendMessage( + GNOSIS_AMB_ETHEREUM, + target, + message, + gasLimit + ); + } + + function sendMessageGnosisChainToEthereum( + address target, + bytes memory message, + uint256 gasLimit + ) internal { + sendMessage( + GNOSIS_AMB_GNOSIS_CHAIN, + target, + message, + gasLimit + ); + } + +} diff --git a/src/forwarders/ArbitrumForwarder.sol b/src/forwarders/ArbitrumForwarder.sol new file mode 100644 index 0000000..fb42db1 --- /dev/null +++ b/src/forwarders/ArbitrumForwarder.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface ICrossDomainArbitrum { + function createRetryableTicket( + address to, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); + function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) external view returns (uint256); +} + +interface IArbSys { + function sendTxToL1(address target, bytes calldata message) external; +} + +library ArbitrumForwarder { + + address constant internal L1_CROSS_DOMAIN_ARBITRUM_ONE = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; + address constant internal L1_CROSS_DOMAIN_ARBITRUM_NOVA = 0xc4448b71118c9071Bcb9734A0EAc55D18A153949; + address constant internal L2_CROSS_DOMAIN = 0x0000000000000000000000000000000000000064; + + function sendMessageL1toL2( + address l1CrossDomain, + address target, + bytes memory message, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 baseFee + ) internal { + uint256 maxSubmission = ICrossDomainArbitrum(l1CrossDomain).calculateRetryableSubmissionFee(message.length, baseFee); + uint256 maxRedemption = gasLimit * maxFeePerGas; + ICrossDomainArbitrum(l1CrossDomain).createRetryableTicket{value: maxSubmission + maxRedemption}( + target, + 0, // we always assume that l2CallValue = 0 + maxSubmission, + address(0), // burn the excess gas + address(0), // burn the excess gas + gasLimit, + maxFeePerGas, + message + ); + } + + function sendMessageL1toL2ArbitrumOne( + address target, + bytes memory message, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 baseFee + ) internal { + sendMessageL1toL2( + L1_CROSS_DOMAIN_ARBITRUM_ONE, + target, + message, + gasLimit, + maxFeePerGas, + baseFee + ); + } + + function sendMessageL1toL2ArbitrumNova( + address target, + bytes memory message, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 baseFee + ) internal { + sendMessageL1toL2( + L1_CROSS_DOMAIN_ARBITRUM_NOVA, + target, + message, + gasLimit, + maxFeePerGas, + baseFee + ); + } + + function sendMessageL2toL1( + address target, + bytes memory message + ) internal { + IArbSys(L2_CROSS_DOMAIN).sendTxToL1( + target, + message + ); + } + +} diff --git a/src/forwarders/CCTPForwarder.sol b/src/forwarders/CCTPForwarder.sol new file mode 100644 index 0000000..f914307 --- /dev/null +++ b/src/forwarders/CCTPForwarder.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface IMessageTransmitter { + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external; +} + +library CCTPForwarder { + + address constant internal MESSAGE_TRANSMITTER_CIRCLE_ETHEREUM = 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81; + address constant internal MESSAGE_TRANSMITTER_CIRCLE_AVALANCHE = 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880; + address constant internal MESSAGE_TRANSMITTER_CIRCLE_OPTIMISM = 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8; + address constant internal MESSAGE_TRANSMITTER_CIRCLE_ARBITRUM_ONE = 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca; + address constant internal MESSAGE_TRANSMITTER_CIRCLE_BASE = 0xAD09780d193884d503182aD4588450C416D6F9D4; + address constant internal MESSAGE_TRANSMITTER_CIRCLE_POLYGON_POS = 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; + + uint32 constant internal DOMAIN_ID_CIRCLE_ETHEREUM = 0; + uint32 constant internal DOMAIN_ID_CIRCLE_AVALANCHE = 1; + uint32 constant internal DOMAIN_ID_CIRCLE_OPTIMISM = 2; + uint32 constant internal DOMAIN_ID_CIRCLE_ARBITRUM_ONE = 3; + uint32 constant internal DOMAIN_ID_CIRCLE_NOBLE = 4; + uint32 constant internal DOMAIN_ID_CIRCLE_SOLANA = 5; + uint32 constant internal DOMAIN_ID_CIRCLE_BASE = 6; + uint32 constant internal DOMAIN_ID_CIRCLE_POLYGON_POS = 7; + + function sendMessage( + address messageTransmitter, + uint32 destinationDomainId, + bytes32 recipient, + bytes memory messageBody + ) internal { + IMessageTransmitter(messageTransmitter).sendMessage( + destinationDomainId, + recipient, + messageBody + ); + } + + function sendMessage( + address messageTransmitter, + uint32 destinationDomainId, + address recipient, + bytes memory messageBody + ) internal { + sendMessage( + messageTransmitter, + destinationDomainId, + bytes32(uint256(uint160(recipient))), + messageBody + ); + } + +} diff --git a/src/forwarders/OptimismForwarder.sol b/src/forwarders/OptimismForwarder.sol new file mode 100644 index 0000000..0148f5e --- /dev/null +++ b/src/forwarders/OptimismForwarder.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface ICrossDomainOptimism { + function sendMessage(address _target, bytes calldata _message, uint32 _gasLimit) external; +} + +library OptimismForwarder { + + address constant internal L1_CROSS_DOMAIN_OPTIMISM = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + address constant internal L1_CROSS_DOMAIN_BASE = 0x866E82a600A1414e583f7F13623F1aC5d58b0Afa; + address constant internal L2_CROSS_DOMAIN = 0x4200000000000000000000000000000000000007; + + function sendMessageL1toL2( + address l1CrossDomain, + address target, + bytes memory message, + uint256 gasLimit + ) internal { + ICrossDomainOptimism(l1CrossDomain).sendMessage( + target, + message, + uint32(gasLimit) + ); + } + + function sendMessageL1toL2Optimism( + address target, + bytes memory message, + uint256 gasLimit + ) internal { + sendMessageL1toL2( + L1_CROSS_DOMAIN_OPTIMISM, + target, + message, + uint32(gasLimit) + ); + } + + function sendMessageL1toL2Base( + address target, + bytes memory message, + uint256 gasLimit + ) internal { + sendMessageL1toL2( + L1_CROSS_DOMAIN_BASE, + target, + message, + uint32(gasLimit) + ); + } + + function sendMessageL2toL1( + address target, + bytes memory message, + uint256 gasLimit + ) internal { + ICrossDomainOptimism(L2_CROSS_DOMAIN).sendMessage( + target, + message, + uint32(gasLimit) + ); + } + +} diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index 6f63ac9..5a2888c 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -5,7 +5,8 @@ import "./IntegrationBase.t.sol"; import { ArbitrumBridgeTesting, ArbSysOverride } from "src/testing/bridges/ArbitrumBridgeTesting.sol"; -import { ArbitrumReceiver } from "src/ArbitrumReceiver.sol"; +import { ArbitrumForwarder } from "src/forwarders/ArbitrumForwarder.sol"; +import { ArbitrumReceiver } from "src/ArbitrumReceiver.sol"; contract MessageOrderingArbitrum is MessageOrdering, ArbitrumReceiver { @@ -45,11 +46,11 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { MessageOrdering moArbitrum = new MessageOrderingArbitrum(l1Authority); // Queue up some L2 -> L1 messages - ArbSysOverride(bridge.destinationCrossChainMessenger).sendTxToL1( + ArbitrumForwarder.sendMessageL2toL1( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); - ArbSysOverride(bridge.destinationCrossChainMessenger).sendTxToL1( + ArbitrumForwarder.sendMessageL2toL1( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4) ); @@ -61,7 +62,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); - XChainForwarders.sendMessageArbitrum( + ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, address(moArbitrum), abi.encodeWithSelector(MessageOrdering.push.selector, 1), @@ -69,7 +70,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { 1 gwei, block.basefee + 10 gwei ); - XChainForwarders.sendMessageArbitrum( + ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, address(moArbitrum), abi.encodeWithSelector(MessageOrdering.push.selector, 2), @@ -95,7 +96,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure mode vm.startPrank(notL1Authority); - XChainForwarders.sendMessageArbitrum( + ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, address(moArbitrum), abi.encodeWithSelector(MessageOrdering.push.selector, 999), diff --git a/test/CCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol similarity index 83% rename from test/CCTPIntegration.t.sol rename to test/CircleCCTPIntegration.t.sol index 9e04a54..9e935a5 100644 --- a/test/CCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -5,7 +5,8 @@ import "./IntegrationBase.t.sol"; import { CCTPBridgeTesting } from "src/testing/bridges/CCTPBridgeTesting.sol"; -import { CCTPReceiver } from "src/CCTPReceiver.sol"; +import { CCTPForwarder } from "src/forwarders/CCTPForwarder.sol"; +import { CCTPReceiver } from "src/CCTPReceiver.sol"; contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { @@ -33,29 +34,29 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { address l2Authority = makeAddr("l2Authority"); function test_avalanche() public { - checkCircleCCTPStyle(getChain("avalanche").createFork(), 1); + checkCircleCCTPStyle(getChain("avalanche").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_AVALANCHE); } function test_optimism() public { - checkCircleCCTPStyle(getChain("optimism").createFork(), 2); + checkCircleCCTPStyle(getChain("optimism").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM); } function test_arbitrum_one() public { - checkCircleCCTPStyle(getChain("arbitrum_one").createFork(), 3); + checkCircleCCTPStyle(getChain("arbitrum_one").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_ARBITRUM_ONE); } function test_base() public { - checkCircleCCTPStyle(getChain("base").createFork(), 6); + checkCircleCCTPStyle(getChain("base").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_BASE); } function test_polygon() public { - checkCircleCCTPStyle(getChain("polygon").createFork(), 7); + checkCircleCCTPStyle(getChain("polygon").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_POLYGON_POS); } function checkCircleCCTPStyle(Domain memory destination, uint32 destinationDomainId) public { Bridge memory bridge = CCTPBridgeTesting.createCircleBridge(mainnet, destination); - uint32 sourceDomainId = 0; // Ethereum + uint32 sourceDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM; mainnet.selectFork(); @@ -75,13 +76,13 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Queue up some L2 -> L1 messages vm.startPrank(l2Authority); - XChainForwarders.sendMessageCCTP( + CCTPForwarder.sendMessage( bridge.destinationCrossChainMessenger, sourceDomainId, address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); - XChainForwarders.sendMessageCCTP( + CCTPForwarder.sendMessage( bridge.destinationCrossChainMessenger, sourceDomainId, address(moHost), @@ -96,12 +97,14 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); - XChainForwarders.sendMessageCircleCCTP( + CCTPForwarder.sendMessage( + bridge.sourceCrossChainMessenger, destinationDomainId, address(moCCTP), abi.encodeWithSelector(MessageOrdering.push.selector, 1) ); - XChainForwarders.sendMessageCircleCCTP( + CCTPForwarder.sendMessage( + bridge.sourceCrossChainMessenger, destinationDomainId, address(moCCTP), abi.encodeWithSelector(MessageOrdering.push.selector, 2) @@ -124,7 +127,8 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure modes vm.startPrank(notL1Authority); - XChainForwarders.sendMessageCircleCCTP( + CCTPForwarder.sendMessage( + bridge.sourceCrossChainMessenger, destinationDomainId, address(moCCTP), abi.encodeWithSelector(MessageOrdering.push.selector, 999) diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index c6b94f5..d8ab26d 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -5,12 +5,9 @@ import "./IntegrationBase.t.sol"; import { AMBBridgeTesting } from "src/testing/bridges/AMBBridgeTesting.sol"; +import { AMBForwarder } from "src/forwarders/AMBForwarder.sol"; import { GnosisReceiver } from "src/GnosisReceiver.sol"; -interface IAMB { - function requireToPassMessage(address, bytes memory, uint256) external returns (bytes32); -} - contract MessageOrderingGnosis is MessageOrdering, GnosisReceiver { constructor(address _l2CrossDomain, uint256 _chainId, address _l1Authority) GnosisReceiver(_l2CrossDomain, _chainId, _l1Authority) {} @@ -42,13 +39,13 @@ contract GnosisIntegrationTest is IntegrationBaseTest { MessageOrderingGnosis moGnosis = new MessageOrderingGnosis(bridge.destinationCrossChainMessenger, _chainId, l1Authority); - // Queue up some L2 -> L1 messages - IAMB(bridge.destinationCrossChainMessenger).requireToPassMessage( + // Queue up some Gnosis -> Ethereum messages + AMBForwarder.sendMessageGnosisChainToEthereum( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3), 100000 ); - IAMB(bridge.destinationCrossChainMessenger).requireToPassMessage( + AMBForwarder.sendMessageGnosisChainToEthereum( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4), 100000 @@ -59,16 +56,14 @@ contract GnosisIntegrationTest is IntegrationBaseTest { // Do not relay right away mainnet.selectFork(); - // Queue up two more L1 -> L2 messages + // Queue up two more Ethereum -> Gnosis messages vm.startPrank(l1Authority); - XChainForwarders.sendMessageGnosis( - bridge.sourceCrossChainMessenger, + AMBForwarder.sendMessageEthereumToGnosisChain( address(moGnosis), abi.encodeWithSelector(MessageOrdering.push.selector, 1), 100000 ); - XChainForwarders.sendMessageGnosis( - bridge.sourceCrossChainMessenger, + AMBForwarder.sendMessageEthereumToGnosisChain( address(moGnosis), abi.encodeWithSelector(MessageOrdering.push.selector, 2), 100000 @@ -91,8 +86,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure modes vm.startPrank(notL1Authority); - XChainForwarders.sendMessageGnosis( - bridge.sourceCrossChainMessenger, + AMBForwarder.sendMessageEthereumToGnosisChain( address(moGnosis), abi.encodeWithSelector(MessageOrdering.push.selector, 999), 100000 diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index 49405a4..fb56daf 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -6,8 +6,6 @@ import "forge-std/Test.sol"; import { Bridge } from "src/testing/Bridge.sol"; import { Domain, DomainHelpers } from "src/testing/Domain.sol"; -import { XChainForwarders } from "../src/XChainForwarders.sol"; - contract MessageOrdering { uint256[] public messages; diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index 292c685..888f2ee 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -5,7 +5,8 @@ import "./IntegrationBase.t.sol"; import { OptimismBridgeTesting, IMessenger } from "src/testing/bridges/OptimismBridgeTesting.sol"; -import { OptimismReceiver } from "src/OptimismReceiver.sol"; +import { OptimismForwarder } from "src/forwarders/OptimismForwarder.sol"; +import { OptimismReceiver } from "src/OptimismReceiver.sol"; contract MessageOrderingOptimism is MessageOrdering, OptimismReceiver { @@ -44,12 +45,12 @@ contract OptimismIntegrationTest is IntegrationBaseTest { MessageOrdering moOptimism = new MessageOrderingOptimism(l1Authority); // Queue up some L2 -> L1 messages - IMessenger(bridge.destinationCrossChainMessenger).sendMessage( + OptimismForwarder.sendMessageL2toL1( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 3), 100000 ); - IMessenger(bridge.destinationCrossChainMessenger).sendMessage( + OptimismForwarder.sendMessageL2toL1( address(moHost), abi.encodeWithSelector(MessageOrdering.push.selector, 4), 100000 @@ -62,13 +63,13 @@ contract OptimismIntegrationTest is IntegrationBaseTest { // Queue up two more L1 -> L2 messages vm.startPrank(l1Authority); - XChainForwarders.sendMessageOptimism( + OptimismForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, address(moOptimism), abi.encodeWithSelector(MessageOrdering.push.selector, 1), 100000 ); - XChainForwarders.sendMessageOptimism( + OptimismForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, address(moOptimism), abi.encodeWithSelector(MessageOrdering.push.selector, 2), @@ -92,7 +93,7 @@ contract OptimismIntegrationTest is IntegrationBaseTest { // Validate the message receiver failure modes vm.startPrank(notL1Authority); - XChainForwarders.sendMessageOptimism( + OptimismForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, address(moOptimism), abi.encodeWithSelector(MessageOrdering.push.selector, 999), From 5540ccd9c686cf7dd80cd40a460177a9b4753e9b Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sat, 1 Jun 2024 14:40:05 -0500 Subject: [PATCH 19/42] refactor receivers; started adjusting integration tests --- src/ArbitrumReceiver.sol | 33 --------------- src/GnosisReceiver.sol | 44 -------------------- src/OptimismReceiver.sol | 38 ----------------- src/receivers/AMBReceiver.sol | 45 ++++++++++++++++++++ src/receivers/ArbitrumReceiver.sol | 38 +++++++++++++++++ src/{ => receivers}/CCTPReceiver.sol | 26 +++++------- src/receivers/OptimismReceiver.sol | 39 +++++++++++++++++ test/ArbitrumIntegration.t.sol | 62 +++++++++++----------------- test/CircleCCTPIntegration.t.sol | 39 ++++++----------- test/IntegrationBase.t.sol | 36 +++++++++++++--- 10 files changed, 200 insertions(+), 200 deletions(-) delete mode 100644 src/ArbitrumReceiver.sol delete mode 100644 src/GnosisReceiver.sol delete mode 100644 src/OptimismReceiver.sol create mode 100644 src/receivers/AMBReceiver.sol create mode 100644 src/receivers/ArbitrumReceiver.sol rename src/{ => receivers}/CCTPReceiver.sol (67%) create mode 100644 src/receivers/OptimismReceiver.sol diff --git a/src/ArbitrumReceiver.sol b/src/ArbitrumReceiver.sol deleted file mode 100644 index 73d0dae..0000000 --- a/src/ArbitrumReceiver.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.0; - -/** - * @title ArbitrumReceiver - * @notice Receive messages to an Arbitrum-style chain. - */ -abstract contract ArbitrumReceiver { - - address public immutable l1Authority; - - constructor( - address _l1Authority - ) { - l1Authority = _l1Authority; - } - - function _getL1MessageSender() internal view returns (address) { - unchecked { - return address(uint160(msg.sender) - uint160(0x1111000000000000000000000000000000001111)); - } - } - - function _onlyCrossChainMessage() internal view { - require(_getL1MessageSender() == l1Authority, "Receiver/invalid-l1Authority"); - } - - modifier onlyCrossChainMessage() { - _onlyCrossChainMessage(); - _; - } - -} diff --git a/src/GnosisReceiver.sol b/src/GnosisReceiver.sol deleted file mode 100644 index b313de2..0000000 --- a/src/GnosisReceiver.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.0; - -interface ICrossDomainGnosis { - function messageSender() external view returns (address); - function messageSourceChainId() external view returns (bytes32); -} - -/** - * @title GnosisReceiver - * @notice Receive messages to Gnosis-style chain. - */ -abstract contract GnosisReceiver { - - ICrossDomainGnosis public immutable l2CrossDomain; - bytes32 public immutable chainId; - address public immutable l1Authority; - - constructor( - address _l2CrossDomain, - uint256 _chainId, - address _l1Authority - ) { - l2CrossDomain = ICrossDomainGnosis(_l2CrossDomain); - chainId = bytes32(_chainId); - l1Authority = _l1Authority; - } - - function _getL1MessageSender() internal view returns (address) { - return l2CrossDomain.messageSender(); - } - - function _onlyCrossChainMessage() internal view { - require(msg.sender == address(l2CrossDomain), "Receiver/invalid-sender"); - require(l2CrossDomain.messageSourceChainId() == chainId, "Receiver/invalid-chainId"); - require(_getL1MessageSender() == l1Authority, "Receiver/invalid-l1Authority"); - } - - modifier onlyCrossChainMessage() { - _onlyCrossChainMessage(); - _; - } - -} diff --git a/src/OptimismReceiver.sol b/src/OptimismReceiver.sol deleted file mode 100644 index 398cd4d..0000000 --- a/src/OptimismReceiver.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.0; - -interface ICrossDomainOptimism { - function xDomainMessageSender() external view returns (address); -} - -/** - * @title OptimismReceiver - * @notice Receive messages to an Optimism-style chain. - */ -abstract contract OptimismReceiver { - - ICrossDomainOptimism public constant l2CrossDomain = ICrossDomainOptimism(0x4200000000000000000000000000000000000007); - - address public immutable l1Authority; - - constructor( - address _l1Authority - ) { - l1Authority = _l1Authority; - } - - function _getL1MessageSender() internal view returns (address) { - return l2CrossDomain.xDomainMessageSender(); - } - - function _onlyCrossChainMessage() internal view { - require(msg.sender == address(l2CrossDomain), "Receiver/invalid-sender"); - require(_getL1MessageSender() == l1Authority, "Receiver/invalid-l1Authority"); - } - - modifier onlyCrossChainMessage() { - _onlyCrossChainMessage(); - _; - } - -} diff --git a/src/receivers/AMBReceiver.sol b/src/receivers/AMBReceiver.sol new file mode 100644 index 0000000..0dcc121 --- /dev/null +++ b/src/receivers/AMBReceiver.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface IArbitraryMessagingBridge { + function messageSender() external view returns (address); + function messageSourceChainId() external view returns (bytes32); +} + +/** + * @title AMBReceiver + * @notice Receive messages to AMB-style chain. + */ +contract AMBReceiver { + + IArbitraryMessagingBridge public immutable amb; + bytes32 public immutable chainId; + address public immutable sourceAuthority; + address public immutable target; + + constructor( + address _amb, + uint256 _chainId, + address _authority, + address _target + ) { + amb = IArbitraryMessagingBridge(_amb); + chainId = bytes32(_chainId); + authority = _authority; + target = _target; + } + + function forward(bytes memory message) external { + require(msg.sender == address(amb), "AMBReceiver/invalid-sender"); + require(l2CrossDomain.messageSourceChainId() == chainId, "AMBReceiver/invalid-chainId"); + require(l2CrossDomain.messageSender() == sourceAuthority, "AMBReceiver/invalid-sourceAuthority"); + + (bool success, bytes memory ret) = target.call(message); + if (!success) { + assembly { + revert(add(ret, 0x20), mload(ret)) + } + } + } + +} diff --git a/src/receivers/ArbitrumReceiver.sol b/src/receivers/ArbitrumReceiver.sol new file mode 100644 index 0000000..4f48127 --- /dev/null +++ b/src/receivers/ArbitrumReceiver.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +/** + * @title ArbitrumReceiver + * @notice Receive messages to an Arbitrum-style chain. + */ +contract ArbitrumReceiver { + + address public immutable l1Authority; + address public immutable target; + + constructor( + address _l1Authority, + address _target + ) { + l1Authority = _l1Authority; + target = _target; + } + + function _getL1MessageSender() internal view returns (address) { + unchecked { + return address(uint160(msg.sender) - uint160(0x1111000000000000000000000000000000001111)); + } + } + + function forward(bytes memory message) external { + require(_getL1MessageSender() == l1Authority, "ArbitrumReceiver/invalid-l1Authority"); + + (bool success, bytes memory ret) = target.call(message); + if (!success) { + assembly { + revert(add(ret, 0x20), mload(ret)) + } + } + } + +} diff --git a/src/CCTPReceiver.sol b/src/receivers/CCTPReceiver.sol similarity index 67% rename from src/CCTPReceiver.sol rename to src/receivers/CCTPReceiver.sol index 8f6860d..5b2206d 100644 --- a/src/CCTPReceiver.sol +++ b/src/receivers/CCTPReceiver.sol @@ -2,32 +2,26 @@ pragma solidity ^0.8.0; /** - * @title CCTPReceiver + * @title CCTPReceiver * @notice Receive messages from CCTP-style bridge. */ -abstract contract CCTPReceiver { +contract CCTPReceiver { address public immutable destinationMessenger; uint32 public immutable sourceDomainId; address public immutable sourceAuthority; + address public immutable target; constructor( address _destinationMessenger, uint32 _sourceDomainId, - address _sourceAuthority + address _sourceAuthority, + address _target ) { destinationMessenger = _destinationMessenger; sourceDomainId = _sourceDomainId; sourceAuthority = _sourceAuthority; - } - - function _onlyCrossChainMessage() internal view { - require(msg.sender == address(this), "Receiver/invalid-sender"); - } - - modifier onlyCrossChainMessage() { - _onlyCrossChainMessage(); - _; + target = _target; } function handleReceiveMessage( @@ -35,11 +29,11 @@ abstract contract CCTPReceiver { bytes32 sender, bytes calldata messageBody ) external returns (bool) { - require(msg.sender == destinationMessenger, "Receiver/invalid-sender"); - require(sourceDomainId == sourceDomain, "Receiver/invalid-sourceDomain"); - require(sender == bytes32(uint256(uint160(sourceAuthority))), "Receiver/invalid-sourceAuthority"); + require(msg.sender == destinationMessenger, "CCTPReceiver/invalid-sender"); + require(sourceDomainId == sourceDomain, "CCTPReceiver/invalid-sourceDomain"); + require(sender == bytes32(uint256(uint160(sourceAuthority))), "CCTPReceiver/invalid-sourceAuthority"); - (bool success, bytes memory ret) = address(this).call(messageBody); + (bool success, bytes memory ret) = target.call(messageBody); if (!success) { assembly { revert(add(ret, 0x20), mload(ret)) diff --git a/src/receivers/OptimismReceiver.sol b/src/receivers/OptimismReceiver.sol new file mode 100644 index 0000000..65c524d --- /dev/null +++ b/src/receivers/OptimismReceiver.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface ICrossDomainOptimism { + function xDomainMessageSender() external view returns (address); +} + +/** + * @title OptimismReceiver + * @notice Receive messages to an Optimism-style chain. + */ +contract OptimismReceiver { + + ICrossDomainOptimism public constant l2CrossDomain = ICrossDomainOptimism(0x4200000000000000000000000000000000000007); + + address public immutable l1Authority; + address public immutable target; + + constructor( + address _l1Authority, + address _target + ) { + l1Authority = _l1Authority; + target = _target; + } + + function forward(bytes memory message) external { + require(msg.sender == address(l2CrossDomain), "OptimismReceiver/invalid-sender"); + require(l2CrossDomain.xDomainMessageSender() == l1Authority, "OptimismReceiver/invalid-l1Authority"); + + (bool success, bytes memory ret) = target.call(message); + if (!success) { + assembly { + revert(add(ret, 0x20), mload(ret)) + } + } + } + +} diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index 5a2888c..7589002 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -6,17 +6,7 @@ import "./IntegrationBase.t.sol"; import { ArbitrumBridgeTesting, ArbSysOverride } from "src/testing/bridges/ArbitrumBridgeTesting.sol"; import { ArbitrumForwarder } from "src/forwarders/ArbitrumForwarder.sol"; -import { ArbitrumReceiver } from "src/ArbitrumReceiver.sol"; - -contract MessageOrderingArbitrum is MessageOrdering, ArbitrumReceiver { - - constructor(address _l1Authority) ArbitrumReceiver(_l1Authority) {} - - function push(uint256 messageId) public override onlyCrossChainMessage { - super.push(messageId); - } - -} +import { ArbitrumReceiver } from "src/receivers/ArbitrumReceiver.sol"; contract ArbitrumIntegrationTest is IntegrationBaseTest { @@ -31,40 +21,38 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { checkArbitrumStyle(getChain("arbitrum_nova").createFork()); } - function checkArbitrumStyle(Domain memory arbitrum) public { - Bridge memory bridge = ArbitrumBridgeTesting.createNativeBridge(mainnet, arbitrum); - - deal(l1Authority, 100 ether); - deal(notL1Authority, 100 ether); - - mainnet.selectFork(); + function initDestinationReceiver(address target) internal virtual returns (address receiver) { + return new ArbitrumReceiver(sourceAuthority, target); + } - MessageOrdering moHost = new MessageOrdering(); + function checkArbitrumStyle(Domain memory _destination) internal { + initDestination(_destination); - arbitrum.selectFork(); + Bridge memory bridge = ArbitrumBridgeTesting.createNativeBridge(source, destination); - MessageOrdering moArbitrum = new MessageOrderingArbitrum(l1Authority); + deal(sourceAuthority, 100 ether); + deal(randomAddress, 100 ether); // Queue up some L2 -> L1 messages ArbitrumForwarder.sendMessageL2toL1( - address(moHost), + address(moSource), abi.encodeWithSelector(MessageOrdering.push.selector, 3) ); ArbitrumForwarder.sendMessageL2toL1( - address(moHost), + address(moSource), abi.encodeWithSelector(MessageOrdering.push.selector, 4) ); - assertEq(moArbitrum.length(), 0); + assertEq(moDestination.length(), 0); // Do not relay right away - mainnet.selectFork(); + source.selectFork(); // Queue up two more L1 -> L2 messages - vm.startPrank(l1Authority); + vm.startPrank(sourceAuthority); ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, - address(moArbitrum), + address(moDestination), abi.encodeWithSelector(MessageOrdering.push.selector, 1), 100000, 1 gwei, @@ -72,7 +60,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { ); ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, - address(moArbitrum), + address(moDestination), abi.encodeWithSelector(MessageOrdering.push.selector, 2), 100000, 1 gwei, @@ -80,25 +68,25 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { ); vm.stopPrank(); - assertEq(moHost.length(), 0); + assertEq(moSource.length(), 0); bridge.relayMessagesToDestination(true); - assertEq(moArbitrum.length(), 2); - assertEq(moArbitrum.messages(0), 1); - assertEq(moArbitrum.messages(1), 2); + assertEq(moDestination.length(), 2); + assertEq(moDestination.messages(0), 1); + assertEq(moDestination.messages(1), 2); bridge.relayMessagesToSource(true); - assertEq(moHost.length(), 2); - assertEq(moHost.messages(0), 3); - assertEq(moHost.messages(1), 4); + assertEq(moSource.length(), 2); + assertEq(moSource.messages(0), 3); + assertEq(moSource.messages(1), 4); // Validate the message receiver failure mode vm.startPrank(notL1Authority); ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, - address(moArbitrum), + address(moDestination), abi.encodeWithSelector(MessageOrdering.push.selector, 999), 100000, 1 gwei, @@ -106,7 +94,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { ); vm.stopPrank(); - vm.expectRevert("Receiver/invalid-l1Authority"); + vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); bridge.relayMessagesToDestination(true); } diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index 9e935a5..6e01649 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -4,35 +4,14 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; import { CCTPBridgeTesting } from "src/testing/bridges/CCTPBridgeTesting.sol"; - -import { CCTPForwarder } from "src/forwarders/CCTPForwarder.sol"; -import { CCTPReceiver } from "src/CCTPReceiver.sol"; - -contract MessageOrderingCCTP is MessageOrdering, CCTPReceiver { - - constructor( - address _destinationMessenger, - uint32 _sourceDomainId, - address _sourceAuthority - ) CCTPReceiver( - _destinationMessenger, - _sourceDomainId, - _sourceAuthority - ) {} - - function push(uint256 messageId) public override onlyCrossChainMessage { - super.push(messageId); - } - -} +import { CCTPForwarder } from "src/forwarders/CCTPForwarder.sol"; +import { CCTPReceiver } from "src/CCTPReceiver.sol"; contract CircleCCTPIntegrationTest is IntegrationBaseTest { using CCTPBridgeTesting for *; using DomainHelpers for *; - address l2Authority = makeAddr("l2Authority"); - function test_avalanche() public { checkCircleCCTPStyle(getChain("avalanche").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_AVALANCHE); } @@ -53,8 +32,14 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { checkCircleCCTPStyle(getChain("polygon").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_POLYGON_POS); } - function checkCircleCCTPStyle(Domain memory destination, uint32 destinationDomainId) public { - Bridge memory bridge = CCTPBridgeTesting.createCircleBridge(mainnet, destination); + function initDestinationReceiver(address target) internal virtual returns (address receiver) { + return new CCTPReceiver(sourceAuthority, target); + } + + function checkCircleCCTPStyle(Domain memory _destination, uint32 destinationDomainId) public { + initDestination(_destination); + + Bridge memory bridge = CCTPBridgeTesting.createCircleBridge(source, destination); uint32 sourceDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM; @@ -74,7 +59,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { l1Authority ); - // Queue up some L2 -> L1 messages + // Queue up some Destination -> Source messages vm.startPrank(l2Authority); CCTPForwarder.sendMessage( bridge.destinationCrossChainMessenger, @@ -95,7 +80,7 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Do not relay right away mainnet.selectFork(); - // Queue up two more L1 -> L2 messages + // Queue up two more Source -> Destination messages vm.startPrank(l1Authority); CCTPForwarder.sendMessage( bridge.sourceCrossChainMessenger, diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index fb56daf..6cc9ab7 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -8,9 +8,12 @@ import { Domain, DomainHelpers } from "src/testing/Domain.sol"; contract MessageOrdering { + address public receiver; uint256[] public messages; - function push(uint256 messageId) public virtual { + function push(uint256 messageId) external { + require(msg.sender == receiver, "only-receiver"); + messages.push(messageId); } @@ -18,19 +21,42 @@ contract MessageOrdering { return messages.length; } + function setReceiver(address _receiver) external { + receiver = _receiver; + } + } abstract contract IntegrationBaseTest is Test { using DomainHelpers for *; - Domain mainnet; + address sourceAuthority = makeAddr("sourceAuthority"); + address destinationAuthority = makeAddr("destinationAuthority"); + address randomAddress = makeAddr("randomAddress"); - address l1Authority = makeAddr("l1Authority"); - address notL1Authority = makeAddr("notL1Authority"); + Domain source; + Domain destination; + + MessageOrdering moSource; + MessageOrdering moDestination; function setUp() public { - mainnet = getChain("mainnet").createFork(); + source = getChain("mainnet").createFork(); + + source.selectFork(); + moSource = new MessageOrdering(); } + function initDestination(Domain memory _destination) internal { + destination = _destination; + + destination.selectFork(); + moDestination = new MessageOrdering(); + + moDestination.setReceiver(initDestinationReceiver(address(moDestination))); + } + + function initDestinationReceiver(address target) internal virtual returns (address receiver); + } From 0040372d5d7dd7c8fae10371c6c3428ace6b3c19 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Mon, 3 Jun 2024 22:57:33 +0900 Subject: [PATCH 20/42] refactor integration tests --- src/receivers/AMBReceiver.sol | 20 ++-- test/ArbitrumIntegration.t.sol | 113 +++++++++------------- test/CircleCCTPIntegration.t.sol | 158 +++++++++++++------------------ test/GnosisIntegration.t.sol | 141 +++++++++++++-------------- test/IntegrationBase.t.sol | 70 ++++++++++++-- test/OptimismIntegration.t.sol | 128 ++++++++++--------------- 6 files changed, 301 insertions(+), 329 deletions(-) diff --git a/src/receivers/AMBReceiver.sol b/src/receivers/AMBReceiver.sol index 0dcc121..cd4ddd1 100644 --- a/src/receivers/AMBReceiver.sol +++ b/src/receivers/AMBReceiver.sol @@ -13,26 +13,26 @@ interface IArbitraryMessagingBridge { contract AMBReceiver { IArbitraryMessagingBridge public immutable amb; - bytes32 public immutable chainId; + bytes32 public immutable sourceChainId; address public immutable sourceAuthority; address public immutable target; constructor( address _amb, - uint256 _chainId, - address _authority, + bytes32 _sourceChainId, + address _sourceAuthority, address _target ) { - amb = IArbitraryMessagingBridge(_amb); - chainId = bytes32(_chainId); - authority = _authority; - target = _target; + amb = IArbitraryMessagingBridge(_amb); + sourceChainId = _sourceChainId; + sourceAuthority = _sourceAuthority; + target = _target; } function forward(bytes memory message) external { - require(msg.sender == address(amb), "AMBReceiver/invalid-sender"); - require(l2CrossDomain.messageSourceChainId() == chainId, "AMBReceiver/invalid-chainId"); - require(l2CrossDomain.messageSender() == sourceAuthority, "AMBReceiver/invalid-sourceAuthority"); + require(msg.sender == address(amb), "AMBReceiver/invalid-sender"); + require(amb.messageSourceChainId() == sourceChainId, "AMBReceiver/invalid-sourceChainId"); + require(amb.messageSender() == sourceAuthority, "AMBReceiver/invalid-sourceAuthority"); (bool success, bytes memory ret) = target.call(message); if (!success) { diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index 7589002..d1f2fc2 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -3,99 +3,80 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; -import { ArbitrumBridgeTesting, ArbSysOverride } from "src/testing/bridges/ArbitrumBridgeTesting.sol"; - -import { ArbitrumForwarder } from "src/forwarders/ArbitrumForwarder.sol"; -import { ArbitrumReceiver } from "src/receivers/ArbitrumReceiver.sol"; +import { ArbitrumBridgeTesting } from "src/testing/bridges/ArbitrumBridgeTesting.sol"; +import { ArbitrumForwarder } from "src/forwarders/ArbitrumForwarder.sol"; +import { ArbitrumReceiver } from "src/receivers/ArbitrumReceiver.sol"; contract ArbitrumIntegrationTest is IntegrationBaseTest { using ArbitrumBridgeTesting for *; using DomainHelpers for *; - function test_arbitrumOne() public { - checkArbitrumStyle(getChain("arbitrum_one").createFork()); - } - - function test_arbitrumNova() public { - checkArbitrumStyle(getChain("arbitrum_nova").createFork()); - } - - function initDestinationReceiver(address target) internal virtual returns (address receiver) { - return new ArbitrumReceiver(sourceAuthority, target); - } - - function checkArbitrumStyle(Domain memory _destination) internal { - initDestination(_destination); - - Bridge memory bridge = ArbitrumBridgeTesting.createNativeBridge(source, destination); + function setUp() public override { + super.setUp(); + // Needed for arbitrum cross-chain messages deal(sourceAuthority, 100 ether); deal(randomAddress, 100 ether); + } - // Queue up some L2 -> L1 messages - ArbitrumForwarder.sendMessageL2toL1( - address(moSource), - abi.encodeWithSelector(MessageOrdering.push.selector, 3) - ); - ArbitrumForwarder.sendMessageL2toL1( - address(moSource), - abi.encodeWithSelector(MessageOrdering.push.selector, 4) - ); - - assertEq(moDestination.length(), 0); + // Use Arbitrum One for failure test as the code logic is the same - // Do not relay right away - source.selectFork(); + function test_invalidSourceAuthority() public { + initBaseContracts(getChain("arbitrum_one").createFork()); - // Queue up two more L1 -> L2 messages - vm.startPrank(sourceAuthority); - ArbitrumForwarder.sendMessageL1toL2( - bridge.sourceCrossChainMessenger, - address(moDestination), - abi.encodeWithSelector(MessageOrdering.push.selector, 1), - 100000, - 1 gwei, - block.basefee + 10 gwei - ); - ArbitrumForwarder.sendMessageL1toL2( - bridge.sourceCrossChainMessenger, - address(moDestination), - abi.encodeWithSelector(MessageOrdering.push.selector, 2), - 100000, - 1 gwei, - block.basefee + 10 gwei - ); + vm.startPrank(randomAddress); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); vm.stopPrank(); - assertEq(moSource.length(), 0); - + vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); bridge.relayMessagesToDestination(true); + } - assertEq(moDestination.length(), 2); - assertEq(moDestination.messages(0), 1); - assertEq(moDestination.messages(1), 2); + function test_arbitrumOne() public { + runCrossChainTests(getChain("arbitrum_one").createFork()); + } - bridge.relayMessagesToSource(true); + function test_arbitrumNova() public { + runCrossChainTests(getChain("arbitrum_nova").createFork()); + } - assertEq(moSource.length(), 2); - assertEq(moSource.messages(0), 3); - assertEq(moSource.messages(1), 4); + function initSourceReceiver() internal override pure returns (address) { + return address(0); + } + + function initDestinationReceiver() internal override returns (address) { + return address(new ArbitrumReceiver(sourceAuthority, address(moDestination))); + } - // Validate the message receiver failure mode - vm.startPrank(notL1Authority); + function initBridgeTesting() internal override returns (Bridge memory) { + return ArbitrumBridgeTesting.createNativeBridge(source, destination); + } + + function queueSourceToDestination(bytes memory message) internal override { ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, - address(moDestination), - abi.encodeWithSelector(MessageOrdering.push.selector, 999), + destinationReceiver, + abi.encodeCall(ArbitrumReceiver.forward, (message)), 100000, 1 gwei, block.basefee + 10 gwei ); - vm.stopPrank(); + } - vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); + function queueDestinationToSource(bytes memory message) internal override { + ArbitrumForwarder.sendMessageL2toL1( + address(moSource), // No receiver so send directly to the message ordering contract + message + ); + } + + function relaySourceToDestination() internal override { bridge.relayMessagesToDestination(true); } + function relayDestinationToSource() internal override { + bridge.relayMessagesToSource(true); + } + } diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index 6e01649..fff4c4b 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -5,135 +5,109 @@ import "./IntegrationBase.t.sol"; import { CCTPBridgeTesting } from "src/testing/bridges/CCTPBridgeTesting.sol"; import { CCTPForwarder } from "src/forwarders/CCTPForwarder.sol"; -import { CCTPReceiver } from "src/CCTPReceiver.sol"; +import { CCTPReceiver } from "src/receivers/CCTPReceiver.sol"; contract CircleCCTPIntegrationTest is IntegrationBaseTest { using CCTPBridgeTesting for *; using DomainHelpers for *; + uint32 sourceDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM; + uint32 destinationDomainId; + + // Use Optimism for failure tests as the code logic is the same + + function test_invalidSourceAuthority() public { + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; + initBaseContracts(getChain("optimism").createFork()); + + vm.startPrank(randomAddress); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); + vm.stopPrank(); + + vm.expectRevert("CCTPReceiver/invalid-sourceAuthority"); + bridge.relayMessagesToDestination(true); + } + + function test_invalidSender() public { + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; + initBaseContracts(getChain("optimism").createFork()); + + vm.prank(randomAddress); + vm.expectRevert("CCTPReceiver/invalid-sender"); + CCTPReceiver(destinationReceiver).handleReceiveMessage(0, bytes32(uint256(uint160(sourceAuthority))), abi.encodeCall(MessageOrdering.push, (1))); + } + + function test_invalidSourceDomain() public { + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; + initBaseContracts(getChain("optimism").createFork()); + + vm.prank(bridge.destinationCrossChainMessenger); + vm.expectRevert("CCTPReceiver/invalid-sourceDomain"); + CCTPReceiver(destinationReceiver).handleReceiveMessage(1, bytes32(uint256(uint160(sourceAuthority))), abi.encodeCall(MessageOrdering.push, (1))); + } + function test_avalanche() public { - checkCircleCCTPStyle(getChain("avalanche").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_AVALANCHE); + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_AVALANCHE; + runCrossChainTests(getChain("avalanche").createFork()); } function test_optimism() public { - checkCircleCCTPStyle(getChain("optimism").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM); + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; + runCrossChainTests(getChain("optimism").createFork()); } function test_arbitrum_one() public { - checkCircleCCTPStyle(getChain("arbitrum_one").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_ARBITRUM_ONE); + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ARBITRUM_ONE; + runCrossChainTests(getChain("arbitrum_one").createFork()); } function test_base() public { - checkCircleCCTPStyle(getChain("base").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_BASE); + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_BASE; + runCrossChainTests(getChain("base").createFork()); } function test_polygon() public { - checkCircleCCTPStyle(getChain("polygon").createFork(), CCTPForwarder.DOMAIN_ID_CIRCLE_POLYGON_POS); + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_POLYGON_POS; + runCrossChainTests(getChain("polygon").createFork()); } - function initDestinationReceiver(address target) internal virtual returns (address receiver) { - return new CCTPReceiver(sourceAuthority, target); + function initSourceReceiver() internal override returns (address) { + return address(new CCTPReceiver(bridge.sourceCrossChainMessenger, destinationDomainId, destinationAuthority, address(moSource))); } - function checkCircleCCTPStyle(Domain memory _destination, uint32 destinationDomainId) public { - initDestination(_destination); - - Bridge memory bridge = CCTPBridgeTesting.createCircleBridge(source, destination); - - uint32 sourceDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM; + function initDestinationReceiver() internal override returns (address) { + return address(new CCTPReceiver(bridge.destinationCrossChainMessenger, sourceDomainId, sourceAuthority, address(moDestination))); + } - mainnet.selectFork(); + function initBridgeTesting() internal override returns (Bridge memory) { + return CCTPBridgeTesting.createCircleBridge(source, destination); + } - MessageOrderingCCTP moHost = new MessageOrderingCCTP( + function queueSourceToDestination(bytes memory message) internal override { + CCTPForwarder.sendMessage( bridge.sourceCrossChainMessenger, destinationDomainId, - l2Authority - ); - - destination.selectFork(); - - MessageOrderingCCTP moCCTP = new MessageOrderingCCTP( - bridge.destinationCrossChainMessenger, - sourceDomainId, - l1Authority + destinationReceiver, + message ); + } - // Queue up some Destination -> Source messages - vm.startPrank(l2Authority); - CCTPForwarder.sendMessage( - bridge.destinationCrossChainMessenger, - sourceDomainId, - address(moHost), - abi.encodeWithSelector(MessageOrdering.push.selector, 3) - ); + function queueDestinationToSource(bytes memory message) internal override { CCTPForwarder.sendMessage( bridge.destinationCrossChainMessenger, sourceDomainId, - address(moHost), - abi.encodeWithSelector(MessageOrdering.push.selector, 4) + sourceReceiver, + message ); - vm.stopPrank(); - - assertEq(moCCTP.length(), 0); - - // Do not relay right away - mainnet.selectFork(); - - // Queue up two more Source -> Destination messages - vm.startPrank(l1Authority); - CCTPForwarder.sendMessage( - bridge.sourceCrossChainMessenger, - destinationDomainId, - address(moCCTP), - abi.encodeWithSelector(MessageOrdering.push.selector, 1) - ); - CCTPForwarder.sendMessage( - bridge.sourceCrossChainMessenger, - destinationDomainId, - address(moCCTP), - abi.encodeWithSelector(MessageOrdering.push.selector, 2) - ); - vm.stopPrank(); - - assertEq(moHost.length(), 0); + } + function relaySourceToDestination() internal override { bridge.relayMessagesToDestination(true); + } - assertEq(moCCTP.length(), 2); - assertEq(moCCTP.messages(0), 1); - assertEq(moCCTP.messages(1), 2); - + function relayDestinationToSource() internal override { bridge.relayMessagesToSource(true); - - assertEq(moHost.length(), 2); - assertEq(moHost.messages(0), 3); - assertEq(moHost.messages(1), 4); - - // Validate the message receiver failure modes - vm.startPrank(notL1Authority); - CCTPForwarder.sendMessage( - bridge.sourceCrossChainMessenger, - destinationDomainId, - address(moCCTP), - abi.encodeWithSelector(MessageOrdering.push.selector, 999) - ); - vm.stopPrank(); - - vm.expectRevert("Receiver/invalid-sourceAuthority"); - bridge.relayMessagesToDestination(true); - - destination.selectFork(); - vm.expectRevert("Receiver/invalid-sender"); - moCCTP.push(999); - - vm.expectRevert("Receiver/invalid-sender"); - moCCTP.handleReceiveMessage(0, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); - - assertEq(moCCTP.sourceDomainId(), 0); - vm.prank(bridge.destinationCrossChainMessenger); - vm.expectRevert("Receiver/invalid-sourceDomain"); - moCCTP.handleReceiveMessage(1, bytes32(uint256(uint160(l1Authority))), abi.encodeWithSelector(MessageOrdering.push.selector, 999)); } } diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index d8ab26d..23e9dcf 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -4,108 +4,97 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; import { AMBBridgeTesting } from "src/testing/bridges/AMBBridgeTesting.sol"; - -import { AMBForwarder } from "src/forwarders/AMBForwarder.sol"; -import { GnosisReceiver } from "src/GnosisReceiver.sol"; - -contract MessageOrderingGnosis is MessageOrdering, GnosisReceiver { - - constructor(address _l2CrossDomain, uint256 _chainId, address _l1Authority) GnosisReceiver(_l2CrossDomain, _chainId, _l1Authority) {} - - function push(uint256 messageId) public override onlyCrossChainMessage { - super.push(messageId); - } - -} +import { AMBForwarder } from "src/forwarders/AMBForwarder.sol"; +import { AMBReceiver } from "src/receivers/AMBReceiver.sol"; contract GnosisIntegrationTest is IntegrationBaseTest { using AMBBridgeTesting for *; using DomainHelpers for *; - function test_gnosisChain() public { - checkGnosisStyle(getChain('gnosis_chain').createFork()); - } - - function checkGnosisStyle(Domain memory gnosis) public { - Bridge memory bridge = AMBBridgeTesting.createGnosisBridge(mainnet, gnosis); + function test_invalidSourceAuthority() public { + initBaseContracts(getChain("gnosis_chain").createFork()); - mainnet.selectFork(); - - MessageOrdering moHost = new MessageOrdering(); - uint256 _chainId = block.chainid; + vm.startPrank(randomAddress); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); + vm.stopPrank(); - gnosis.selectFork(); + // The revert is caught so it doesn't propagate + // Just look at the no change to verify it didn't go through + assertEq(moDestination.length(), 0); + bridge.relayMessagesToDestination(true); + assertEq(moDestination.length(), 0); + } - MessageOrderingGnosis moGnosis = new MessageOrderingGnosis(bridge.destinationCrossChainMessenger, _chainId, l1Authority); + function test_invalidSender() public { + initBaseContracts(getChain("gnosis_chain").createFork()); - // Queue up some Gnosis -> Ethereum messages - AMBForwarder.sendMessageGnosisChainToEthereum( - address(moHost), - abi.encodeWithSelector(MessageOrdering.push.selector, 3), - 100000 - ); - AMBForwarder.sendMessageGnosisChainToEthereum( - address(moHost), - abi.encodeWithSelector(MessageOrdering.push.selector, 4), - 100000 - ); + vm.prank(randomAddress); + vm.expectRevert("AMBReceiver/invalid-sender"); + AMBReceiver(destinationReceiver).forward(abi.encodeCall(MessageOrdering.push, (1))); + } - assertEq(moGnosis.length(), 0); + function test_invalidSourceChainId() public { + initBaseContracts(getChain("gnosis_chain").createFork()); - // Do not relay right away - mainnet.selectFork(); + destination.selectFork(); + destinationReceiver = address(new AMBReceiver( + bridge.destinationCrossChainMessenger, + bytes32(uint256(2)), // Random chain id (not Ethereum) + sourceAuthority, + address(moDestination) + )); - // Queue up two more Ethereum -> Gnosis messages - vm.startPrank(l1Authority); - AMBForwarder.sendMessageEthereumToGnosisChain( - address(moGnosis), - abi.encodeWithSelector(MessageOrdering.push.selector, 1), - 100000 - ); - AMBForwarder.sendMessageEthereumToGnosisChain( - address(moGnosis), - abi.encodeWithSelector(MessageOrdering.push.selector, 2), - 100000 - ); + source.selectFork(); + vm.startPrank(sourceAuthority); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); vm.stopPrank(); - assertEq(moHost.length(), 0); - + // The revert is caught so it doesn't propagate + // Just look at the no change to verify it didn't go through + assertEq(moDestination.length(), 0); bridge.relayMessagesToDestination(true); + assertEq(moDestination.length(), 0); + } - assertEq(moGnosis.length(), 2); - assertEq(moGnosis.messages(0), 1); - assertEq(moGnosis.messages(1), 2); + function test_gnosisChain() public { + runCrossChainTests(getChain('gnosis_chain').createFork()); + } - bridge.relayMessagesToSource(true); + function initSourceReceiver() internal override returns (address) { + return address(new AMBReceiver(bridge.sourceCrossChainMessenger, bytes32(uint256(100)), destinationAuthority, address(moSource))); + } + + function initDestinationReceiver() internal override returns (address) { + return address(new AMBReceiver(bridge.destinationCrossChainMessenger, bytes32(uint256(1)), sourceAuthority, address(moDestination))); + } - assertEq(moHost.length(), 2); - assertEq(moHost.messages(0), 3); - assertEq(moHost.messages(1), 4); + function initBridgeTesting() internal override returns (Bridge memory) { + return AMBBridgeTesting.createGnosisBridge(source, destination); + } - // Validate the message receiver failure modes - vm.startPrank(notL1Authority); + function queueSourceToDestination(bytes memory message) internal override { AMBForwarder.sendMessageEthereumToGnosisChain( - address(moGnosis), - abi.encodeWithSelector(MessageOrdering.push.selector, 999), + destinationReceiver, + abi.encodeCall(AMBReceiver.forward, (message)), 100000 ); - vm.stopPrank(); + } - // The revert is caught so it doesn't propagate - // Just look at the no change to verify it didn't go through - bridge.relayMessagesToDestination(true); - assertEq(moGnosis.length(), 2); // No change + function queueDestinationToSource(bytes memory message) internal override { + AMBForwarder.sendMessageGnosisChainToEthereum( + sourceReceiver, + abi.encodeCall(AMBReceiver.forward, (message)), + 100000 + ); + } - gnosis.selectFork(); - vm.expectRevert("Receiver/invalid-sender"); - moGnosis.push(999); + function relaySourceToDestination() internal override { + bridge.relayMessagesToDestination(true); + } - assertEq(moGnosis.l2CrossDomain().messageSourceChainId(), 0); - vm.prank(address(moGnosis.l2CrossDomain())); - vm.expectRevert("Receiver/invalid-chainId"); - moGnosis.push(999); + function relayDestinationToSource() internal override { + bridge.relayMessagesToSource(true); } } diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index 6cc9ab7..30141f9 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -12,7 +12,8 @@ contract MessageOrdering { uint256[] public messages; function push(uint256 messageId) external { - require(msg.sender == receiver, "only-receiver"); + // Null receiver means there is no code for this path so we ignore the check + require(receiver == address(0) || msg.sender == receiver, "only-receiver"); messages.push(messageId); } @@ -41,22 +42,77 @@ abstract contract IntegrationBaseTest is Test { MessageOrdering moSource; MessageOrdering moDestination; - function setUp() public { + address sourceReceiver; + address destinationReceiver; + + Bridge bridge; + + function setUp() public virtual { source = getChain("mainnet").createFork(); + } + + function initBaseContracts(Domain memory _destination) internal { + destination = _destination; source.selectFork(); moSource = new MessageOrdering(); + sourceReceiver = initSourceReceiver(); + moSource.setReceiver(sourceReceiver); + + destination.selectFork(); + moDestination = new MessageOrdering(); + destinationReceiver = initDestinationReceiver(); + moDestination.setReceiver(destinationReceiver); + + bridge = initBridgeTesting(); + + // Default to source fork as it's an obvious default + source.selectFork(); } - function initDestination(Domain memory _destination) internal { - destination = _destination; + function runCrossChainTests(Domain memory _destination) internal { + initBaseContracts(_destination); destination.selectFork(); - moDestination = new MessageOrdering(); - moDestination.setReceiver(initDestinationReceiver(address(moDestination))); + // Queue up some Destination -> Source messages + vm.startPrank(sourceAuthority); + queueDestinationToSource(abi.encodeCall(MessageOrdering.push, (3))); + queueDestinationToSource(abi.encodeCall(MessageOrdering.push, (4))); + vm.stopPrank(); + + assertEq(moDestination.length(), 0); + + // Do not relay right away + source.selectFork(); + + // Queue up two more Source -> Destination messages + vm.startPrank(sourceAuthority); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (2))); + vm.stopPrank(); + + assertEq(moSource.length(), 0); + + relaySourceToDestination(); + + assertEq(moDestination.length(), 2); + assertEq(moDestination.messages(0), 1); + assertEq(moDestination.messages(1), 2); + + relayDestinationToSource(); + + assertEq(moSource.length(), 2); + assertEq(moSource.messages(0), 3); + assertEq(moSource.messages(1), 4); } - function initDestinationReceiver(address target) internal virtual returns (address receiver); + function initSourceReceiver() internal virtual returns (address); + function initDestinationReceiver() internal virtual returns (address); + function initBridgeTesting() internal virtual returns (Bridge memory); + function queueSourceToDestination(bytes memory message) internal virtual; + function queueDestinationToSource(bytes memory message) internal virtual; + function relaySourceToDestination() internal virtual; + function relayDestinationToSource() internal virtual; } diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index 888f2ee..3c3dfaa 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -3,20 +3,9 @@ pragma solidity >=0.8.0; import "./IntegrationBase.t.sol"; -import { OptimismBridgeTesting, IMessenger } from "src/testing/bridges/OptimismBridgeTesting.sol"; - -import { OptimismForwarder } from "src/forwarders/OptimismForwarder.sol"; -import { OptimismReceiver } from "src/OptimismReceiver.sol"; - -contract MessageOrderingOptimism is MessageOrdering, OptimismReceiver { - - constructor(address _l1Authority) OptimismReceiver(_l1Authority) {} - - function push(uint256 messageId) public override onlyCrossChainMessage { - super.push(messageId); - } - -} +import { OptimismBridgeTesting } from "src/testing/bridges/OptimismBridgeTesting.sol"; +import { OptimismForwarder } from "src/forwarders/OptimismForwarder.sol"; +import { OptimismReceiver } from "src/receivers/OptimismReceiver.sol"; contract OptimismIntegrationTest is IntegrationBaseTest { @@ -25,90 +14,73 @@ contract OptimismIntegrationTest is IntegrationBaseTest { event FailedRelayedMessage(bytes32); - function test_optimism() public { - checkOptimismStyle(getChain("optimism").createFork()); - } + // Use Arbitrum One for failure test as the code logic is the same - function test_base() public { - checkOptimismStyle(getChain("base").createFork()); - } + function test_invalidSourceAuthority() public { + initBaseContracts(getChain("optimism").createFork()); - function checkOptimismStyle(Domain memory optimism) public { - Bridge memory bridge = OptimismBridgeTesting.createNativeBridge(mainnet, optimism); + vm.startPrank(randomAddress); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); + vm.stopPrank(); - mainnet.selectFork(); + // The revert is caught so it doesn't propagate + // Just look at the no change to verify it didn't go through + assertEq(moDestination.length(), 0); + bridge.relayMessagesToDestination(true); + assertEq(moDestination.length(), 0); + } - MessageOrdering moHost = new MessageOrdering(); + function test_invalidSender() public { + initBaseContracts(getChain("optimism").createFork()); - optimism.selectFork(); + vm.prank(randomAddress); + vm.expectRevert("OptimismReceiver/invalid-sender"); + OptimismReceiver(destinationReceiver).forward(abi.encodeCall(MessageOrdering.push, (1))); + } - MessageOrdering moOptimism = new MessageOrderingOptimism(l1Authority); + function test_optimism() public { + runCrossChainTests(getChain("optimism").createFork()); + } - // Queue up some L2 -> L1 messages - OptimismForwarder.sendMessageL2toL1( - address(moHost), - abi.encodeWithSelector(MessageOrdering.push.selector, 3), - 100000 - ); - OptimismForwarder.sendMessageL2toL1( - address(moHost), - abi.encodeWithSelector(MessageOrdering.push.selector, 4), - 100000 - ); + function test_base() public { + runCrossChainTests(getChain("base").createFork()); + } - assertEq(moOptimism.length(), 0); + function initSourceReceiver() internal override pure returns (address) { + return address(0); + } - // Do not relay right away - mainnet.selectFork(); + function initDestinationReceiver() internal override returns (address) { + return address(new OptimismReceiver(sourceAuthority, address(moDestination))); + } - // Queue up two more L1 -> L2 messages - vm.startPrank(l1Authority); - OptimismForwarder.sendMessageL1toL2( - bridge.sourceCrossChainMessenger, - address(moOptimism), - abi.encodeWithSelector(MessageOrdering.push.selector, 1), - 100000 - ); + function initBridgeTesting() internal override returns (Bridge memory) { + return OptimismBridgeTesting.createNativeBridge(source, destination); + } + + function queueSourceToDestination(bytes memory message) internal override { OptimismForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, - address(moOptimism), - abi.encodeWithSelector(MessageOrdering.push.selector, 2), + destinationReceiver, + abi.encodeCall(OptimismReceiver.forward, (message)), 100000 ); - vm.stopPrank(); - - assertEq(moHost.length(), 0); - - bridge.relayMessagesToDestination(true); - - assertEq(moOptimism.length(), 2); - assertEq(moOptimism.messages(0), 1); - assertEq(moOptimism.messages(1), 2); - - bridge.relayMessagesToSource(true); - - assertEq(moHost.length(), 2); - assertEq(moHost.messages(0), 3); - assertEq(moHost.messages(1), 4); + } - // Validate the message receiver failure modes - vm.startPrank(notL1Authority); - OptimismForwarder.sendMessageL1toL2( - bridge.sourceCrossChainMessenger, - address(moOptimism), - abi.encodeWithSelector(MessageOrdering.push.selector, 999), + function queueDestinationToSource(bytes memory message) internal override { + OptimismForwarder.sendMessageL2toL1( + address(moSource), // No receiver so send directly to the message ordering contract + message, 100000 ); - vm.stopPrank(); + } - // The revert is caught so it doesn't propagate - // Just look at the no change to verify it didn't go through + function relaySourceToDestination() internal override { bridge.relayMessagesToDestination(true); - assertEq(moOptimism.length(), 2); // No change + } - optimism.selectFork(); - vm.expectRevert("Receiver/invalid-sender"); - moOptimism.push(999); + function relayDestinationToSource() internal override { + bridge.relayMessagesToSource(true); } } From d8288114acaa40d6b08f59dd30602ffb68a6ea5c Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Tue, 4 Jun 2024 22:22:43 +0900 Subject: [PATCH 21/42] fix all tests; update readme --- README.md | 17 ++++++++++++++++- test/ArbitrumIntegration.t.sol | 6 +++--- test/CircleCCTPIntegration.t.sol | 6 +++++- test/GnosisIntegration.t.sol | 8 ++++---- test/IntegrationBase.t.sol | 10 +++++----- test/OptimismIntegration.t.sol | 5 +++-- 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4ebfb0b..3285473 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # xchain-helpers -This repository contains tooling for multichain testing such as cross-chain e2e testing support. +This repository three tools for use with multi-chain development. Domains refer to blockchains which are connected by bridges. Domains may have multiple bridges connecting them, for example both the Optimism Native Bridge and Circle CCTP connect Ethereum and Optimism domains. + +## Forwarders + +These libraries provide standardized syntax for sending a message to a bridge. + +## Receivers + +The most common pattern is to have an authorized contract forward a message to another "business logic" contract to abstract away bridge dependencies. Receivers are contracts which perform this generic translation - decoding the bridge-specific message and forwarding to another `target` contract. The `target` contract should have logic to restrict who can call it and permission this to one or more bridge receivers. + +TODO diagram + +## E2E Testing Infrastructure + +Provides tooling to record messages sent to supported bridges and relay them on the other side simulating a real message going across. + *** *The IP in this repository was assigned to Mars SPC Limited in respect of the MarsOne SP* diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index d1f2fc2..f7a2037 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -12,8 +12,8 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { using ArbitrumBridgeTesting for *; using DomainHelpers for *; - function setUp() public override { - super.setUp(); + function initBaseContracts(Domain memory _destination) internal override { + super.initBaseContracts(_destination); // Needed for arbitrum cross-chain messages deal(sourceAuthority, 100 ether); @@ -30,7 +30,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { vm.stopPrank(); vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); - bridge.relayMessagesToDestination(true); + relaySourceToDestination(); } function test_arbitrumOne() public { diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index fff4c4b..267a66d 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -26,13 +26,15 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { vm.stopPrank(); vm.expectRevert("CCTPReceiver/invalid-sourceAuthority"); - bridge.relayMessagesToDestination(true); + relaySourceToDestination(); } function test_invalidSender() public { destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; initBaseContracts(getChain("optimism").createFork()); + destination.selectFork(); + vm.prank(randomAddress); vm.expectRevert("CCTPReceiver/invalid-sender"); CCTPReceiver(destinationReceiver).handleReceiveMessage(0, bytes32(uint256(uint160(sourceAuthority))), abi.encodeCall(MessageOrdering.push, (1))); @@ -42,6 +44,8 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; initBaseContracts(getChain("optimism").createFork()); + destination.selectFork(); + vm.prank(bridge.destinationCrossChainMessenger); vm.expectRevert("CCTPReceiver/invalid-sourceDomain"); CCTPReceiver(destinationReceiver).handleReceiveMessage(1, bytes32(uint256(uint160(sourceAuthority))), abi.encodeCall(MessageOrdering.push, (1))); diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index 23e9dcf..bf08229 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -21,14 +21,15 @@ contract GnosisIntegrationTest is IntegrationBaseTest { // The revert is caught so it doesn't propagate // Just look at the no change to verify it didn't go through - assertEq(moDestination.length(), 0); - bridge.relayMessagesToDestination(true); + relaySourceToDestination(); assertEq(moDestination.length(), 0); } function test_invalidSender() public { initBaseContracts(getChain("gnosis_chain").createFork()); + destination.selectFork(); + vm.prank(randomAddress); vm.expectRevert("AMBReceiver/invalid-sender"); AMBReceiver(destinationReceiver).forward(abi.encodeCall(MessageOrdering.push, (1))); @@ -52,8 +53,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { // The revert is caught so it doesn't propagate // Just look at the no change to verify it didn't go through - assertEq(moDestination.length(), 0); - bridge.relayMessagesToDestination(true); + relaySourceToDestination(); assertEq(moDestination.length(), 0); } diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index 30141f9..2f40cc9 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -47,13 +47,15 @@ abstract contract IntegrationBaseTest is Test { Bridge bridge; - function setUp() public virtual { + function setUp() public { source = getChain("mainnet").createFork(); } - function initBaseContracts(Domain memory _destination) internal { + function initBaseContracts(Domain memory _destination) internal virtual { destination = _destination; + bridge = initBridgeTesting(); + source.selectFork(); moSource = new MessageOrdering(); sourceReceiver = initSourceReceiver(); @@ -64,8 +66,6 @@ abstract contract IntegrationBaseTest is Test { destinationReceiver = initDestinationReceiver(); moDestination.setReceiver(destinationReceiver); - bridge = initBridgeTesting(); - // Default to source fork as it's an obvious default source.selectFork(); } @@ -76,7 +76,7 @@ abstract contract IntegrationBaseTest is Test { destination.selectFork(); // Queue up some Destination -> Source messages - vm.startPrank(sourceAuthority); + vm.startPrank(destinationAuthority); queueDestinationToSource(abi.encodeCall(MessageOrdering.push, (3))); queueDestinationToSource(abi.encodeCall(MessageOrdering.push, (4))); vm.stopPrank(); diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index 3c3dfaa..a2c2f8c 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -25,14 +25,15 @@ contract OptimismIntegrationTest is IntegrationBaseTest { // The revert is caught so it doesn't propagate // Just look at the no change to verify it didn't go through - assertEq(moDestination.length(), 0); - bridge.relayMessagesToDestination(true); + relaySourceToDestination(); assertEq(moDestination.length(), 0); } function test_invalidSender() public { initBaseContracts(getChain("optimism").createFork()); + destination.selectFork(); + vm.prank(randomAddress); vm.expectRevert("OptimismReceiver/invalid-sender"); OptimismReceiver(destinationReceiver).forward(abi.encodeCall(MessageOrdering.push, (1))); From 200b503c47154eaf4150ab85db10c2324bdbeb40 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 14:44:03 +0900 Subject: [PATCH 22/42] rm unused console --- src/testing/bridges/ArbitrumBridgeTesting.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index 32012c9..073d40e 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -7,8 +7,6 @@ import { Bridge } from "src/testing/Bridge.sol"; import { Domain, DomainHelpers } from "src/testing/Domain.sol"; import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; -import {console} from "./../../../lib/forge-std/src/console.sol"; - interface InboxLike { function createRetryableTicket( address destAddr, From 1928687a4223aaa0cc16c25cc03ad2985f86456f Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 14:47:00 +0900 Subject: [PATCH 23/42] remove the chain specific helper functions --- src/forwarders/ArbitrumForwarder.sol | 34 ---------------------------- src/forwarders/OptimismForwarder.sol | 26 --------------------- 2 files changed, 60 deletions(-) diff --git a/src/forwarders/ArbitrumForwarder.sol b/src/forwarders/ArbitrumForwarder.sol index fb42db1..9c733e9 100644 --- a/src/forwarders/ArbitrumForwarder.sol +++ b/src/forwarders/ArbitrumForwarder.sol @@ -47,40 +47,6 @@ library ArbitrumForwarder { ); } - function sendMessageL1toL2ArbitrumOne( - address target, - bytes memory message, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 baseFee - ) internal { - sendMessageL1toL2( - L1_CROSS_DOMAIN_ARBITRUM_ONE, - target, - message, - gasLimit, - maxFeePerGas, - baseFee - ); - } - - function sendMessageL1toL2ArbitrumNova( - address target, - bytes memory message, - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 baseFee - ) internal { - sendMessageL1toL2( - L1_CROSS_DOMAIN_ARBITRUM_NOVA, - target, - message, - gasLimit, - maxFeePerGas, - baseFee - ); - } - function sendMessageL2toL1( address target, bytes memory message diff --git a/src/forwarders/OptimismForwarder.sol b/src/forwarders/OptimismForwarder.sol index 0148f5e..5566504 100644 --- a/src/forwarders/OptimismForwarder.sol +++ b/src/forwarders/OptimismForwarder.sol @@ -24,32 +24,6 @@ library OptimismForwarder { ); } - function sendMessageL1toL2Optimism( - address target, - bytes memory message, - uint256 gasLimit - ) internal { - sendMessageL1toL2( - L1_CROSS_DOMAIN_OPTIMISM, - target, - message, - uint32(gasLimit) - ); - } - - function sendMessageL1toL2Base( - address target, - bytes memory message, - uint256 gasLimit - ) internal { - sendMessageL1toL2( - L1_CROSS_DOMAIN_BASE, - target, - message, - uint32(gasLimit) - ); - } - function sendMessageL2toL1( address target, bytes memory message, From 5a8dcfc7311d109721d740af2156bca49fca690a Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 14:56:51 +0900 Subject: [PATCH 24/42] add constructor tests for coverage --- src/receivers/AMBReceiver.sol | 16 ++++++++-------- test/ArbitrumIntegration.t.sol | 10 ++++++++++ test/CircleCCTPIntegration.t.sol | 13 +++++++++++++ test/GnosisIntegration.t.sol | 12 ++++++++++++ test/OptimismIntegration.t.sol | 10 ++++++++++ 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/receivers/AMBReceiver.sol b/src/receivers/AMBReceiver.sol index cd4ddd1..df285b4 100644 --- a/src/receivers/AMBReceiver.sol +++ b/src/receivers/AMBReceiver.sol @@ -12,10 +12,10 @@ interface IArbitraryMessagingBridge { */ contract AMBReceiver { - IArbitraryMessagingBridge public immutable amb; - bytes32 public immutable sourceChainId; - address public immutable sourceAuthority; - address public immutable target; + address public immutable amb; + bytes32 public immutable sourceChainId; + address public immutable sourceAuthority; + address public immutable target; constructor( address _amb, @@ -23,16 +23,16 @@ contract AMBReceiver { address _sourceAuthority, address _target ) { - amb = IArbitraryMessagingBridge(_amb); + amb = _amb; sourceChainId = _sourceChainId; sourceAuthority = _sourceAuthority; target = _target; } function forward(bytes memory message) external { - require(msg.sender == address(amb), "AMBReceiver/invalid-sender"); - require(amb.messageSourceChainId() == sourceChainId, "AMBReceiver/invalid-sourceChainId"); - require(amb.messageSender() == sourceAuthority, "AMBReceiver/invalid-sourceAuthority"); + require(msg.sender == amb, "AMBReceiver/invalid-sender"); + require(IArbitraryMessagingBridge(amb).messageSourceChainId() == sourceChainId, "AMBReceiver/invalid-sourceChainId"); + require(IArbitraryMessagingBridge(amb).messageSender() == sourceAuthority, "AMBReceiver/invalid-sourceAuthority"); (bool success, bytes memory ret) = target.call(message); if (!success) { diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index f7a2037..e6d8482 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -20,6 +20,16 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { deal(randomAddress, 100 ether); } + function test_receiver_constructor() public { + initBaseContracts(getChain("arbitrum_one").createFork()); + destination.selectFork(); + + ArbitrumReceiver receiver = new ArbitrumReceiver(sourceAuthority, address(moDestination)); + + assertEq(receiver.l1Authority(), sourceAuthority); + assertEq(receiver.target(), address(moDestination)); + } + // Use Arbitrum One for failure test as the code logic is the same function test_invalidSourceAuthority() public { diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index 267a66d..0fd2629 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -15,6 +15,19 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { uint32 sourceDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM; uint32 destinationDomainId; + function test_receiver_constructor() public { + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; + initBaseContracts(getChain("optimism").createFork()); + destination.selectFork(); + + CCTPReceiver receiver = new CCTPReceiver(bridge.destinationCrossChainMessenger, sourceDomainId, sourceAuthority, address(moDestination)); + + assertEq(receiver.destinationMessenger(), bridge.destinationCrossChainMessenger); + assertEq(receiver.sourceDomainId(), sourceDomainId); + assertEq(receiver.sourceAuthority(), sourceAuthority); + assertEq(receiver.target(), address(moDestination)); + } + // Use Optimism for failure tests as the code logic is the same function test_invalidSourceAuthority() public { diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index bf08229..2e6d2ed 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -12,6 +12,18 @@ contract GnosisIntegrationTest is IntegrationBaseTest { using AMBBridgeTesting for *; using DomainHelpers for *; + function test_receiver_constructor() public { + initBaseContracts(getChain("gnosis_chain").createFork()); + destination.selectFork(); + + AMBReceiver receiver = new AMBReceiver(bridge.destinationCrossChainMessenger, bytes32(uint256(1)), sourceAuthority, address(moDestination)); + + assertEq(receiver.amb(), bridge.destinationCrossChainMessenger); + assertEq(receiver.sourceChainId(), bytes32(uint256(1))); + assertEq(receiver.sourceAuthority(), sourceAuthority); + assertEq(receiver.target(), address(moDestination)); + } + function test_invalidSourceAuthority() public { initBaseContracts(getChain("gnosis_chain").createFork()); diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index a2c2f8c..94bc493 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -14,6 +14,16 @@ contract OptimismIntegrationTest is IntegrationBaseTest { event FailedRelayedMessage(bytes32); + function test_receiver_constructor() public { + initBaseContracts(getChain("optimism").createFork()); + destination.selectFork(); + + OptimismReceiver receiver = new OptimismReceiver(sourceAuthority, address(moDestination)); + + assertEq(receiver.l1Authority(), sourceAuthority); + assertEq(receiver.target(), address(moDestination)); + } + // Use Arbitrum One for failure test as the code logic is the same function test_invalidSourceAuthority() public { From 75e24ce107f750ad1fb3c9e5171cb18b0986aafe Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 17:48:40 +0900 Subject: [PATCH 25/42] remove constructor test; add diagram --- .assets/xchain-helpers.png | Bin 0 -> 197116 bytes README.md | 2 +- test/ArbitrumIntegration.t.sol | 10 ---------- test/CircleCCTPIntegration.t.sol | 13 ------------- test/GnosisIntegration.t.sol | 12 ------------ test/OptimismIntegration.t.sol | 10 ---------- 6 files changed, 1 insertion(+), 46 deletions(-) create mode 100644 .assets/xchain-helpers.png diff --git a/.assets/xchain-helpers.png b/.assets/xchain-helpers.png new file mode 100644 index 0000000000000000000000000000000000000000..b415406fefc52321ea072070eb6b50e6018a4464 GIT binary patch literal 197116 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ðug;{~7Xo4_t9>3{p43l{cY^bRjlLRAl-v-_`9+Q@oJ=Ycx5 z+6=H3WNzsyG`l4OFzKOlTVJ6Ma?l}<*JG}iPJXtNn3A8y>xC(32X5qBS^NJyTy`NT zum1^|4Ee(>)zAc+NE$kx3qRcZ4NC)>O`+*dROOaXIe)Nc>cakMRTfEHp644DdF;ccg#ObEe#Zd|(Gak0f>6XYg76-+lZlP|5YY1_fJ7d8 z_SYsV!maz6uM{@oh8F!FEG<1@?3jK8Q6mr1__2j7CD4c8J?l3`01U%TtAGcRw&^v$ zF%=$DZ?T#_vXoA|YW%t6dVf4WE$)7rp!$F#By2h`O2j66NxUBf=>qmf5g-dU*UBN7 zA|HVkU25k?RcI;~lPE%(&LjT%Ji+q%geDD9om7DPJt=;yVWP))kP#6;Zp->2wgG@aE{-r0I0$tb z5D#w0ra=^P6$e{VD(Lb%t`QD`i5ITPH4+vOKO;zZF%mBsyrp^?MO}yow!fZ%dq-{- zLt3adN9AQ2FxBsB!(TwJjE3Y5!6~6mV9WDCP3NuR`T@iV++|pTXNmu3?l;2?hKl@9 z&+NB%j6dewiNEdCX+vbO<)Zw;rX}tFrYiwDMB3e<<2dd4)fAPdqz#l~xAx`nWqyxH z1Lm;tPsV|%={w!ZNxZz-p`*bH@mhs|G%UdJDN{`jE1s#w#9%SMJ0FbWRCMYn&+UcP z(@0U^`^5AK+WiI{Uiw&`WbKQX+ZIju_JC`gj+s@&PQEH;oY&aB|D0#E`DFUn2}hFHLvh zOoH%pYz3-kg6XQfG3h|Q3O}|{MJ_boH4LI0H?>aT_>9GjhREQ1ZeX^G-yXXVf$qhu zfb;WGC|%Y^77zS!Z9uTy>Il$21|>|(@j{Na00LnBNf`AA_`5~T$XULLkbF4;XmY;i zx9m)$M?H5zPMKUeK|W5#gD$%kf`C~KgoyMVtW!Q?6|Jn~UxeS3z_#+3j@$s)DV7>A z+l!ynZ`sS}{@F@kdI!x&*boJ@Pl8w@4TwqHj-C%E9p_Ob;8B%$2_`NxX}%nWf(iaK{eZbprzG0WtvIIeiS!n|9bKl_x8$>-g#3*ofvPinKh&6Ax^c^PI+6 zA2XdNJ99{wyeLiGk2#YjOd=joJvq)MPJl)Mn{=C7NPHn4^XWG_k}-rAa>1Ha00^iW zEV#kRE3yQ8h0IPKPiQ>?FY7!A0O4Boji~*oEB8zbxkMESG&9fL56NM@9fUWvt`K;g z@shTyeS@49f>xFZc#*YASMg9~nY3FzoKJ&REHd--6Q#zYR3=lS(7mA7JE>&nG5J##JUWx&$-;5#O*LYOALvmqGsX>8 zREo-T(nz9YV`HU|EnoJ!cQ5?$)(i}tfj2xKQzOQezP7yFxfzi5WIDYM+71AZh=e|O zLNQ;C1A4U~bgEvw?TRG4rvnR~G^D#a<>e0P6UYT7$>yhLKrU$U`49Ic=n#x^P?ny% zUI({eEiS=I2~F<-TI-+{31^Dlv0is8gPy5}Y{&BD+G_+XfVW%GK8jv)FRMnH0HjSq z{Gx(lN_;{3F`10FSU_tLrt_e)>v4a{RCT zx0|7oj6HqMKmA*Psf-VJbqe9>;()WAS=l^ttA4~>0CtU}jYp#-3YTc}@*com^C*P| zV7v?)4Ef;gv}jur27R`M43S5kph*7m%<8D z0mhKp+8vGBE%`3Vcs*=(?J=GaQYWPsrQRu9UPs@IC=u%W&p5f^jK~OVde%q{%wBUe$d7QIuu|6l|nwB3_&4l91_D4aEH6IgEywP(JTV+ij|7~(ex;y zmKD5#Ka7DI@p-9emCe>Bw}7(+@24?Pe`rjZ`_+*7)j#^&mMJvfdwPI&r3wC%)UKz( zOae!Ui>b_PZbkUlnE=L~IY}$-%KpZBw1H-c^IMg3wFc0} zl_8AlmI(da$G$2HjR=SWHpXv=rGZox{;X===HtD0F8ygj*Qy*X;bJ{?nN9sjKzc=X z%2MyM;ns4yFwC}pVNB2^G3F9rtHCb3IoOCTq_c6)t%+!9fy9uJV?2B1m>OmwAyVX;5%mTEj1vRQbmzVjO+2y+Z%s!$Fb>_4_B|2pj&k(KP~ z?U$AgA_~tixOsc_I4OxJNuD@p@I5N}RO^|!-MvFoJo;5F@LsI5TT3^nSJ+R|>W*n9 z!;-ddW8QZDiqV{NAZsssDP>r6CSbhi)*O%t3xbHqYFN(b9Pf6#qh&WDG^Tud9xn?e zsxaD#h2>Ur%f)FeNrSFad-&js5`iWkYnL~+ZT_2AJrBfJYI;buOK>KFr7`N+CpAnM zd%N72kkD*~U71i!a+_2@DSc*reyj_lr02GNq!^7l6=9wEQ#+9JekskcR`S;!H)@zK z+fE>GXiKPaxmV78Q{?rPQIC(RtK#i%v(4BTA-n_Jc9qYhXXEt9+TQ(ie*V4(ugO(D zg9D>($cYiS&oopdzPL^o2d_X&kNis)KU*S%y;&07h{N2-e8;JUc#7pWyb^EC)wbn# zX*|9pb@|`6;125Q%z|v+)%yxl!pf_CNIvn{2Bcgxk3 zZ=CWL58Az(^wBzJk=oaGkF)#ypEa}Ix%mf{ZT7`b6>h%Jbf(v>=a^+{@tRN6miXRY=bR&w&&8@$Qfq0VJv}+5jz5?Qx@1(iA=xc4+R{1OBEV z-uHZo{PlM|-~4px2{kXeVz!ZYA^+K11_`#fr7|^3tH-Qw>A(pZyuG{LE9wt-;GS}*!i z@TG)jeQy+eRp93epV6gX2KL&11`!3|cCRQj8 z=c!EfEIsQJ3+E^MZt*==F?Y|#WVEvBX-?3aObbOvTnjU|U!ok{wb#rk(+W?xt>0`1 zLb?J7l>K`!-=&R{Pk;NLU{DnT|K@kIw875DggnT9J=Yx`(S#EZ^Uh-i8)FjT7LgsQ z!6?Ta-sYngWx`&ncvCqiPKk1x^4HbJ+i9Fonh%UV0G#CM^~@m6zw6MW)*+HK;?x_| zjCy!N(EuEjfM-yiZ)AmM?La=~?*-|g z$)H2;gJ$3FN2ym}VGiMpU3GcZ#JOLRE{Z+?D0yb;1RLxRl4#dTimxy^yk}L0L*~Kn zRV1}T(3C@G{t|>y7dAV^8n2;4wpuuComiaQE~!UA4n?HwmtC_6sR<&+B)v2CJxY=P z4xy3_#igc#riK4dMl?({oDu0o0vD{x{$Aa6zP&&f1_f7rKnRPZXwKrNvL}cFZWUhn zLpo?OG>{kQ3=tN;xphEXEcy;D?Wb_Wp2)ASB8PJZ$CEnyZmEQD2!K(*NIs<_0q(LZY)j$MNO>*h?;?-QkS#(^o63>nMxs?|oalKeN4HdvMJq zOh%M+7m8XW=5CH$Q-%FZxFNd#(O4g4C^9qdRs#2ol9x37muvO_0#vf}H%uZ}GU_5F z?SiQA1>6Fn7S__J^HxV14i?Hu8FJo$;VP4<{6iHF!ZAb9nUn|wE%KJCd-+DEALIo6 zp%!rCfsACcyNf^}TIfraK!P%`)}fwo>lT6cQF~wSE1D8`aH?qVx1Bn21ZpQs9hN}b z$N!~AVoWjm$KCsvr47Bk@0W7@BUmZ~ZvYJ~7C(*t{b~TwE~+Yo(n8Yh*kH4hBU*k# zyYA+x@Ryv7MATANm!Kzp@Nt~@XI>+GK(=!4mPSAp5#Wtago-ti%Qi_sZ`&pi~ zG%)S(EP{-aTTEYnsTz}I-{?G+d7<6BUEHYj=F&f7!NTT&r01~zosC37J51Fh)s>cZ z>q+T^OgbFG`US6uS7DCo9Z`$x+nU>Mm8qz%hpgSvJ2HQt1LO&Y0dXc4k=Ob|t9}xi zH;xY5LbTIyeht?yS2!h108n2*Mpv$|JNf4@Q!pyyeg^I!qP|x$M7iY?T2S7&H2{iYoWO^JAHUk#*1+fg8?+adiF#bnSRfYkPzx_dO z1;dVDlwR*9fQW@O=6C(lvev;u3Fal4M&i@NQcgpVGt@)4tV3Zhn5B z{=s7H_1{{6APEO%$ou=Vo#}yoN|43P|9MxF3Jl>a*uGz$oXgF0Rzb%sSfWdnBQwE4B}7s%CHfXSzD*b3i)&Np}J#Gm2k;iXubQxjXS_#oL{h8v6Hnfa$?X4;w^~GdRgmXFe2`e@HIrkBXh!7S zBLJp}}TR`4<=$@W>!r#U?T8S4lC(>X95gXH)HcZL9OI-xQD} zh*&F$^6Fh>)!liy^avAFzdv6&c((7ajDSc56d-PU;s5dX8vp__FDKR^V3PE@)WQHR z5#{q-3-Yv*RMbi1n}%uMTxRmQY$RQu+UhS&gL20I7bI1nKOGn;)|Xi}tN!RuLoQ0% zmC21E0_I=Bk5jG`Cs@m0ub ztS!T)y+7w|l8O_71(Bq%aY+JWU$Okh{=BP!lzHIAI3t+gq(JoBgo$%pRfu-1c_K6O zT=}6<@4_@J$1B(zV7lTR@jvqH0fwE>dg@m5Tfjj=Yk~Jtw3YMophrZ0X3Bm6Uu9~C zs1ZF|swU;REG?%}+Ru`vQjOnGMFmNC01%Z^n{PYr6V)-ujp_eq^#&l~F>PKo{m;p| zNR2R)eTJ}mj*hl131NOK4?qbHff!^{_&=RiCQ#jFpSAw~j0Ou^g%)$}YS16kO2;0=&(7_c zkojzL{J=)F;F#%F`h1c~b?wg)9=#Yk*_6J1A?(hkP))~0THFJ3Fv~tZ>^HcctmQvr z0>*cKZbx9;ew`OJb#VKRi~@LqbiFuDF`@- zHKPN;cv;Kmh1O2v$Mcd=au49Uri}6i5P)JX%uv|(k2-{6xk^EE)qjoxMlE}0RUyF< zRGc%Dgn1#35fo==6A(d%11)vNxvT|#=K;7rK6GR;%7FVQxq|Ha2Au|Grjxa3}UB&?lkba4Nf`!Pt$^T~_| z|Ho$eH_7v7#=vS^Maz@`Df48h)xA}{Y67h6zsZ;%+GzOWJ{4XdI9xz*N_|cHXBK}f zGiXC8**}sqaue{ya4(_$kG$X0f(Z_ZtzY^!K1TGyi%9ajm|lfT%HVu`9MAyeGsV?H zFk{I#oa%AoiVvMLe{fek5e1e(cx(Yah4^Py|ABc1uW>ya<^_Vv-Bnd1(w;)~y(&0L zQlBI+9C7Yi(vp^aj(H-$-}rS^cmL%!h>?3Y;`6b8ID&PTzlt}IZOJ}5>oHG)>WnLrkL8WF>7|~T$8yKmw0M=c-@Miwdkf0J{aLh1 zI{3ZstLy}**a7=B!9-wIHa zLu%>%Asp~e<0n~q2r7+!4wk%~ep;VAO-m=9AU8{dG zMzr|dEyq%F*0I5oa`zxqy50P481a8h4*=#KZha~TP5M`3+?*;xx-WidN(YMoMHOe@ zgZs&}e%UH5Zczvm0Qddl-ROUm)&FH{lMpymSHcz3?C&U(u3b8kPg}jFvWUFbnwsi_eoX(H9&@+Xlai@ zaxt?S`5sE*L40VxNw`3hm@*+yaqHCJP0p|G|MCo4FEFGOJoElq11f`rmN&D_3CYNU zcO$UQf9w$*`}+iL!DG5>r4CWy(+4lM4E`M>><}tSX``t9vx8t^StOBBazu4m52m4% zDL5^&RBV*VDN~Aq%#A;PGrzm_myX$&>zglDK7io8$?o=)2 zKUxjEX664e(EUR;6#}a?KCCSH zU-4g|0kHRtmT&o<%hBW4lpz)-Gs{_)+Srz58rW;Jb@!ji0^bFSS@Zh4{&ofcaZ{+P z68;TVTuk~zz_@QNg&pBG+~+m>mqb9l=K{fbr-(iNJDMOzQVEtC>d(V-=?!T&AFE-h zO>Vvmo@c*j>yrqc+P}Ys`17k)9!lDiP>E4#>9fBgZk(s0vrliTFkzuw(L%%uMehD# z#1fRO&b$17nEL9lD7*D*L0$n7Vd(CZl192aB_x#Y6zOglX{0+u0qG72rMnxXySwAt z<2mp7e%IxnGV{!S_P%57b+1(={co^9My`wX7WK{ePxIPDgFdD7G;o{5Lx7m*zmUY= z4f6sV$s*5a;w$`Tu?*&_|NRNzz*~VxQr)Re@V^M4 z1)m+MEv>G8!5{VCophEC+M;pCD6dRNBurfYzpsT1Rx~pI?;B;{CslTHb%4Yd` znR5u zP4a)T*DS0YZE7C-d!q~cpkQ<){}hxXMNH(XOh6Nf362c%*6~5QJ1_MafXO#Myl{=A zjQEGM|21&ePGA0eclzIlf{_J_?U_gZQ&)q}+Mz&qo+KQ3i>q=Q^UD*g(m=>i0jYl zizE-^|FeS5u&tTcQuQA$4>+{X5n3tu&l8|;D-|SvnaA{8xm{d=&+jyw+F*w`0bO4y z#uv*vi#HlI$DM?U1f!_j^U)vN*5hYzbgZ+H-i2x=bU-ZTxo6^5?klPI(f$ow;FWFt z1IiBFx*3uG{;(SW``9chJwK0_HIKQ`oK=N!mnK}D*0=gtTKMNI^3rjFPr;H4{Ji~h zDEP@0_k*KK4WP@ecm37(=T@@@)54TPySH%UKTrbHHsg)^mF%BbEHrb!&_~`=8x*6E~$x*GU;u~s@EiuxR z4-NyIGk~d^dJhwKwsq^%G2Jb$)vek7o@(mSe42i06vcN?x5s4X*4OF={Y0DLY6xQ# zo?{yKsC1o=`PZ4Ej($1fhuwbs_nc+nf;DwtHbwmJdD&ut+s&WYytV%-A$YfpW(MQJ zIfz{>cpUBlKLso2FrE1v!M44E|Ihc*_2L2-5#U3cWdNJJ&x-`d1rGIdmP@e%*&?J@Ga@pIR zcP%MEdj7IkNkokgs7kZv7+KUdpBGoc6uPs2>VYXQH-c`bNv*tIYF-a(#(Y-kF#+ev z2|(@n-@53-gKxq7b*lF7TTD>`DdqgyeBL}JvE^=krXVk)p*tEzD4heNa=Yw<8N4vO zZ0um*uipbAiL3s)h~N?&T6fKY`!0X{9*p`5B-1s*Zmz)0RLTcHsK!r)vjt>h(Hq`-})8aa_9B#mo%gr6z!xNUa$_z85-~Me@o`qv}}mzTHnO2IFazLl$Fj_6=Pe z+Qka1!y5lS)Ha(8iWn4wdWXQFX{rS-KD(PcVI*=xu!G8BXCa>n( z2B9-|y|+%oF%`~~r2p~%hs!`bIfH`i7L~TI{NIr11)6BY&-!$g=5y~94d%x}ajt>5 zdL~fcbo+Y<{+>SA^z$Adoe$#H`oP4_%e%RH5fk5kk|ELh6y-5b>9K;D$+PoO$nFg| z^xJRH;kLe~+V~BP zGP5Z?A1|1A{#uLyjHUaq7%&S|brYM`ne4RAj=jJF?7>uOgQqcPrGS3EbzZtNfkj){ z)6vCRGhlo&10)MNv%M%H_ksKmN*B!U=Ify5JT#aRBhmgBqs_P@C0og7rby{kL75-M zhlvVjaa{kVbFeP`;KF{kQDEu(-&6reZoB#FV*GvPF|d7oE$U?s95Bohb8X~`&6ZOf zW1t_aSiH97W`LmZVyj3+T@WljDb8<#>2_xv1=3=9gLr<=^hjI>WPVSfB`{x* zIUth@c;PUAhw-nkt$_Fh9??9BtMcbuCgZPJ_Jkl>5t2b#|6Ssn;n1eEiJWK!Dd z8mIi%XU$>>DFLx&F} zK7}qNm|%PbknZL?yd!;BCjG?vkBXJDSKD8=8kpk-btlbakbKRv*t*!RGeac6e60kIz?F=CfD*dDC{nN{$?>u=nmjN<>ad<2|bm|?-O zL0ldTghT2qs%ATI_0oRl$S{E(EX+#0VCH|7%ipr$|KISz@@v*9!t7hA;;0ddaVk+Y zlzkvH+?Vlqw~3Y(E1vh4pbqoa!@W@x+&tdhC_s(=&W-6gz?$!2CSAhIyjo4O`2Gf#$e#^u|KrINbSv zMHFDp#()6uSA6=v=?R8zVc<+O7u*{fIZvOPL*BB#71{?@EABO&1XgQtu+arGn0<-A za00Vu9^DZCY5yL~$)Gh;MnvjW!!T6Z08S8l4Em$~s1*g*2_}6{bqmzdIltHP+a!Q6 z_g(B376d5l&4A9i&K2_~iJh=?ummXE>SN|5RU|V4hi*gRbZnMX!3p0r*9d?;#2^L4 zb9(Da;n%ZSh!j!Ge#_&-H~o2FA_J>DVo1Y2yl1y9SLV*=(jjnzg~D}1FLToSEh${& za3qsE8ki#qO4kvg81KNaBF}Q5AH?_DGmTS`@+`@z?;b_?WqKj25~XiPF_Pl0yIMWulC>|D&*^ zm*h05ixFc?lY^Fb4K~><$vXgIw`UAgl7kS!QDcf}E*ED}NzYt;;E%%G4XVLR(Mr9u z99)wS5_fZEaiQxTC0(8kTA^#18ek}73Nj!|C9%uA{qk+_^u_g%kKkiT2ddvbP>DUw zX(69QpHctb(TY1#Gz1)4deai_!Nc_&fN;b@8$bxizcMLPN0R|5jcGsIjAcUSS z2G_-KC74e~SHyI)dl!mwFlGX+nTs%QJLt*RXG8@Ub|5ORx0vLng%k2~&zgUIYn0R> zVlS>uP|Ov!>w)^87y-C4P;@;|C+!|gK7cY3AN>l*A7z;N)Pca>yw+s+ERn?t`Jd{y zN&Nrof@(04{0UagEs3j{rNW|Vn8cmY4McNg%*A8>fZ;m5tH;N?p~rq8@*DDP6zfaw zJO0DQ(@!J7_6D9aS8eUq;};wvBX3aR0jyYetFlUIjbDt{=kAL3XXD7x5^ItSEQ9Il zp=if6hi*SQYS+*IcDaJKiDB-WvVKwPNx6`ccQIG2j1& z-Pca=uv{lubv@O5Q5}@>oj;HN>Ni@(O*L$0K$#Q{kUR#0ogfRi(%6}kvZaTXtCzW1 zT!MTW<#9bcE3*WoDGD8vKLMc@w+Wm4)kV1<+Z=!?it_LX^- z7f25sxwL;{0om*NG1Gb00-s(D5(0*dhSq%eZnnNw<_YXu$nB9ytbl3yX3<-i#P41Q z`0vtlur$enq?79bb7rdFA_{DTsY+ijLdn8#hopaZqr71%{?lm(zlsDg7teLN5M5%QB%REW2)q>X@hb02v9BE)tKu z)S^_59+^98|R~*jmi{P=kHOJd9z!$!V{_eJkJOvAFl6KHzn9zP~+nE$2&bQr_KhIwBVn%6#Om~ z=S|86( znc;f@-_cJZCQ+>J0Ftw;e=gA@x?Z*pGlRS&u{}EoQj#@j(gw%sj{UL1Oh5urbOuq; zVR_!sK6R^_XI*Y24G&9Ym0KF#i$Yj-oae_Ys0sbk$Yo7}k+0%06{=_Bs!9WolD*=( zj*w{A$#1r^L%`N2$QFGBV&jVQompodeaOiERTI|`ZWob*{Dav}Qcyn6!(%X^ZTBvC z_mx#F&Urqd3h5nE2y%Z2c9kSZj>chY;%Z@s9f#B7jtIwis7H2C!zY_mKKuKsqs6P9 zsvkDsg4NmY$q=9T%u%F-5$2N?CF?sGpMZwfx?L~>RWiX?4c_LESc*uq*ed}b?&2?lQc9J;h+gNn9Qy-pvpQqbO6vL+(T1YKBeo=q{bBVn+F0 zF*1-V0;4#=ikM_3g8&#w2#-m;g~^wuliYGV>l%!u9R?0cZgx4 za2;%hwN-(5ve6 zGUNX-hXAfCQ*COr9@6P$Wx5YmU}psG89c^0;t?SoD%SB4e(VW_iO z9^cBCWtj#5l_N`i>~yXz+Q4`q!sFJz;zcp8(}O7sK^y> zKc(+KviMxfWiU}FwhZdebSv##b56Z?I55ft^2v(A6mp@V14HT1XVO`c@Gvmp0wfcA zm8ixvQqQB`Ghp`KF0_$V^Yr%j!J0wDKiHwecEB6x+P@E$`8-+M)4hzIkbZiulDKEA zL`c7@Zx;*m4g5#k+G>UQu0$#fJsXEYCVviVha{+8^h{QT`1{ctKD1Re>*Yh}^;JTr zX*_z&X#1Od1q^cxxVKmbpkTi~u+`NDz`&*7#_^vuK3NnZCV8L;Q; zhFP|U+`N(a650^Q*;lFG*c56MYkye@GHW{0!HA^AHYVciT!gfX-ea}5Loz$=IMh@)<~yWASjAn`u`ROWIAkRm=77AT6kS1XyZ81A=uYLAvHKPF zB3TABn3V?*qegk{8J(U9gGjW1kl?xl)9A5=DHO+zBEQ-br{#=`@gIz#0o2=kP8H={ z_bQ|od>GCp$oXtt#OF7PZZvs+UPOZhl8HpiAA4CIJ^hrcfwAv2nEJF#@FV%|{^Ehh zcaE92^R&1>u9z!^J1d#OH0>$j<51yY4aM>^HVTa>o)xG>H3sBD)UvCN#W@>Rs1ADH zjAg!Tmp;RXS4nwEl>IUBgLBU=%S#V9Gg`)rXzhfpY3`kLxg&Q>RZSuYgOPCQ?1G z8cuo{V$GO`7<3e47O&3`B+#?l9a_R&knVaen#d*(tMpFC>>tJSfol`t>4Ect+h%qA zRcWZYrj2heHOxs$y$zROb5xGS35FwC%~i4O1U(y*MD_!gcH{-jMyQ!j*2xMa$CP$~ zQVxfqcClRGGjF70Bl<%rX(7Mu=>R!j8o-;}?ugRJ>2)0`8!_R*fo`N*d{2IcYxvYO zay9rwv?ft8Z_F?afK9Yh2BX_oU&~0Rct-l0=fCK&j~HH<7-DIfM+Asv^>~d33+Su? z49>#jTXe?&TJiAJax*ZHr1DB?T;?#mt=f4OH&r%cd)G}f_Z@ki?Ac~&c5s=XEvImoDN(fCHd9MW^J>5tV!t~--Mg6=B#P$s_ z0($FFhh;v54qpl24<(?kAD2klor_}i`Fho7JqY26<}PL%eK&4D|Cl!_9rBH&h2KU% zpYPd0s&pN@ATy7(6wgFPl!P5Pqs z(tAU^{u02_h3>Bj$|>Pl$pihmH#YwiTcQLojKOpF5hc3Jv0aXr;qpF6R-76Cfk5L! z^RzW|$@E>M1{Qzj`4fy~uLavs5vNiNJS_++luo_F%x5}5MpqYe^mn-udbnDxHiM3Y z_#q)%fug!e$M7@t32hP6C*&o(dVmaQM-uAZw9Yov2PC1o;^!KxsB9`FmrEJw5Z>EN z$@xG`NTXV@U%OgayOUNM4C0i>Ole&rRmebhQrS#Gbxh*biPp(0-GiqeQXWEc*HG@` zE4+~)dmXI6gtj_2E~1wfb>UKrorD-3YRSU@RIwv+GYPeb${$RdsQh^(eb5iISjF@! zNA8!ojO!rEvsPj89%n1srJFgcpM85$t(87#dD6Ym~{QVw}PO09~p1O8*3II zK^k(TyJeQb+{quo#&CK2Jk8_i>Ui}Ja8XmGLlWx$BDed8&vHt%c6Yr=2+JV#XmHj+ z1QlI;&^UhYsLYL=EGV7MPoSt>&{EGSe_+=7h`NKjb=&Jo9pMITxs}Ma1oBSdLDuU4aGP z7h%F)iD{QWATBcRU@<&5(Gi)^PLo^s>F7Z{IS{q`%85CcUt}$ag;Dqi049_n%Afc7ND?SX>6TV#Nu>QC zAH6Pu@N$NBaxIN{=&i@NQr5QD_ztb|03;=e34<$u`R8bZ_gZ{0^I7QSdTodNJyY z=K!VVKseo>pKUL}@KpGVgdBe}PWA%RIDq#JK<XvbGH`R`m-H(i&pz?!LP{0wZH&+3H|8{_1 zrYPc(oRMm;)tv_7EAtyd%|O07Rv2509cgJ(4@5Q9zVXTP5{NEt%j5ka-z4g2Gp<%M zleih1I$w{p(E(CNg*80%5dluKaI7WMMeycT{!9-jj2&cl95o``S_CN2e%O`TPP?Ip z>^t;Y2aBqGaI)PsOw=e%T%EWM`g(B2-EuuJS%E%9Co~;LI_z|Oa9L;khbw#=XUp+cq>n`x2}jEt_~A*Euu)MJxo`kC_c0N0#6-YkU|frn(4yD z?Jms-E6XLl=?ndgv?hR&hxD=lX0hB$MLH-aQiuTuK|*T3$t#%u%8Usy=Yp#c=yp7U zDT&k%0ZqH&7FF$tB4VABejn*zh~k3xWtuhD@=E2`DOP+7gJf~|M_5U<@|07*&(3p| zFmlv&66P+RyRVos!Z)t#VVzj3q%Idx{muIBR(JA@h*eqwsayv8>KIi8HTR_s zP)*XI(5>wPZNkHsxYqB~;ha@Sue4*3ySRW;TP6{;v<0hbZn64o1HFOe;i1zJelRPt z#@E>Z&MzhC*S7`{ka1g3#^mOH<(B2op);Gvp?&&~fr1HHVUy#QkpC9e0uQ(kw&d<( ztW@(TBajT6sICbqPz6-yuNm$;1u|&dT1^Pubd;@=gabk(2UEjFC11Kk>cFjTEZ{!5U{MW)=nfj33Wfk0N;) zhsMi_p1W@M7q^+%J$u`A`%2ps+xpyo+##lAR+R4QwGE5{U% za0(Oa>z{j{$p?hlsm11WK1``Wrok#cfyc8GU)lo>caaOyJbfy`MyRMN88R#fW|R#S zS%?;dIdY1E*K4e*BnT5}8;QAnrV@zbfR=07Kyfg@F(x12c!T(Rp%hg)ryJN;FGI24LfoX?o-3t!`GDWlO>psxM9P=4mUcOdb+C|GBeY$ zFW%Ff)def*f&gu;ox#wezUUs!d$EumLc(k64+_w1@#=;2nod$=@|B}DG`kNv#_k`B zP4KDj6Lj6TYZP2-tU<$DUCO;@Kr2%0ycUG!%aYLF0hE5x3U09=LUe_)K^zEH{^alZW}i zh6DfHCCD@mRK7)dNa%Gz{05N&{R!Z{!CYQT76Tq8vz0c1bgpw4V}RaD4xIs@qrqO{d}V;N4wz-idMQ#}@}rEe(p+PAWq zJMSg+0L^ec=;|8wrzO~SKH*|p|2k|x9THkpS%_tGp!#%_F7g^?znO&*3&~>DWBg_T zGijfkbo1BN4JLd_p7SL|BkON2Ft-Ui}gHu+w7^EQ*J;>b-sQxBNYKTBquEsF#&(`Bp)ZboSETlDZqbVb?Vq7a$! zl_2|fA@i2)$6mQCRP$B?XqaQ)*)D8Z8-;eYa>r%1sS&~2$fsW<-c z&S|im6_~B!qp~)wndzF7S0k=XlHuTNUN;aPEq?{_PB2?*FxSr-;nNmH!6yfK4qTO%zYyVhR z4&eOB?2liK~F>aX5L`XO#m_q}s3n${=s0avXyx6pp9)U=Up zsyz?W1-v5s$C1A}X9}@H$UGa3t5f8d5Y`g<=I^2?-P1_f?m(R0@@IyOHQAe0K;UsG zEUJ*TI;_$lV`)4R{UjuZc~H3l&6gamXR=RC$Qs+%p244$t#Sb4__^DFOAt+zbBifymcTyB3;yz z|J8yA8D;4nfju5NkL3qtXWUCvukmJ~e)(Da2*Qx|k;FOo z!w{eyePbrDnOF2ZGq<(HYt=|*k-=vCEd2bd=MKg=`WX5|qsrW6P8jnKL;PY^SM)Kv z=so(JA}GgF@Mj980m~dA_hSmx?VMM5jC6@ z-xXPFGUCN9W~$wlC##yEQ;tK_@GqXVsthKfFjXn1DnY-vBbf+NLc?*0*0{cc)jp7| zsBa_}M~}{oCl6##B-5Fj;;%Wt<3x2f#%3k2-!QMX^!qn^m7xk0g`# zm&T_vvKrpE05P11apUcV>q1%vGIHxlhxim2h8Tj*Iw@SmOfUCEyC0rM9OC3qU&g+^ z?XxA+pqT^YDvaOH^f-KuQcv{6DG(G&@ICL@2YHM(uD^DXS&^l3+UY!7;i(yr$rE@$ zviwy`h=TCzGHfey5jXRr_7HD7$B&1gOr>PDZ^Khjy}k>y4dBGk@E5Z(6Scjqp5Hw( z_|#%{AJ>!$Ff86Otg=wi2EK%prdM{Yf}Vc!<@N zFhwy$f~ybeo6XET!(_qoB94jD39GuX2ZBKNGyleO=6W=?=3s1EW}mT;X4C#Mb2X3_ z<;4VV6^uY~07x8m_w3t0`SabioPYJ0zdLF}PW`bD?%pka_Iz|8|MKJ2MQ(+I zYMq|ncqo!#~2%KDN*z8UGi$|K)@j#M~P8j2Fx>CCIJF3LHF?&1y&S($RVLO`k;q~ zGu0w(;JyeaL&Fv4^dSqrWfjdaPG7X>68RVdM_m!n9khYxXFNW4XxbMUP*Llx53 z?VJMudy?_gG3}WuWwZU^G7+U=%s4vV)I}22E*X{fZf+N;i8D-bwZ?u_He!X8=Jyh? z4l$o2$-b@VV5o|rvbP49_-S5o zJL&UbGwevaiA%3_Da#LBp3<_vXy!-y!O<-28S}bs4+vitFOQ{gUd(0>CJHhJ>DwWw zpCIjCusO$={&sU%l6(f zwh%tPjQVyw zwBmR*i1YhT88;SQDH~dN6C`P61~Iw0P+=div36N}h`o-Gr^BmW>)0H<%fp8L`iS{U zMOuczj)%HQta7Ut;@9n4VS4Kv1bKAk^Hy42>MY-jc){cyvU-dBbo}~{6b}AGrYLPC z%e&8XA#MDQ?zcY}c+8M{U(PjtysL7C=ShT&wO+rrV@pMU+9PiufB+hs>Kw0eu}3jY z+i()6Nukc`R$K^glhI}gY2-4+l(Hr^)T{eN>)~vltQxH5?(?(T^g3+1OelFB9EB>v zQKgAL{CY0b%h2oa2s38g;%V5}m2=8(wKdO)CWUZfcx?D@+|8>NBxq+_U+=O)DcDAm~#0T*50_Llwulo!?v?|NKtmzjp{ekLrI#0I|X-Td@e z72J)Z;aiazyC({dviS7CXU@7Z;_FmWbxwRExLGb|wW=0<1RULj`AgQbXQcTe&TROW z*(0QAnbh8v{?83HYR+u&s(hc+gWs^0o*^q_7oOGGA?F}xCq5jrtTt}wlh+uZEzgtw3_hZFyi8$`ULR#Mw%FZit^Y8(RDtMBH8^c-AxWZhBiw8I37Ye%bR3Gj*L%7o~7Uow+7YIBENg8tab#`l+9QXz%&t8{5j#r8=^OGwod>by+nQ9l0O?-s0)P5fnz zs4bYM;|a6zq4_ZqeT?~R zi9PR7 zN=+Z_yPjS0_L*0I_kmM}if*THsezi6vgk<=S>S8q2rJi>_RmIaR^5%{-7|y~?28jc z&6m#aS@M_lB*=1JVIQ&@nmlT zg4^y2|66B_MLX`!%S4aROYD$|!Ml6LLp%dh+{^Ld%na^li=2egzitdo#Kt=PkcUv* zd$hjNrVQZ8&k23!jGozu4J*p(#Jf^9gd_C!MBx15=ec3OSI2KAy6A|Lr|d#E%EcJv zF*8I+nIA+OD2=7VT-M#{x<%x5He!=1c+yQ-oRVR%zdoJBQ)E%mgBlUy!btEekL^_h z6aCC?T$d*59>3&#Qk@8v#Zs@PpejwVq?7B{B{^|=0p0eBw^n=wQ7N@GRYL{eh@Oi% z@yfTk5B4+QRKGPA;1B;Sf28H>Olm7mgd&#pqVJjUw@6$YHhI+{#T0!UEuD$ExPhur z;+Cy|pMw|}r%buJ5yQa{u=&{E}xu7c_rf4a?*8wT%$E}@4(Cx74NRm=dA=LjN4i`m9KY)>{Q!* zFio@49Y$<{f6Q1%`bGim-1g0K|4}jZ=Lx!M_Cx`q>$8UmU zknb7aNiPXaDQ~&-_gPm=iD*&}_&oC|@mL7a|HB`Tr5Sff%jm3|wXGXHz%#BB_pRZ%o3+KFhXo5FO%5g=kd0(@)CQsEH%@ z+~%~1xXw|HW39MsWo)5Xr@dCRZ<_suaGR$8GTU2BMh*VOsJjLm?v=?lf=MN(mBYO=M>7m39RikE#QI?IMiHkPt9d;fEwq%h3hdY;12 zE#?{>wO!6dTWd-;E}E=@0w?)*THRuFPwEsYn^p00nBQUoDS|VL%xW>%QnnA$K z>n~;9ZX;Z%&ZZ*79;hcZ-WcJbN=^GP zS@E}$dUr%LEiC@aaiL=7QG#~}JLKpWd7E@5#MKDFCxnYPRv&FKrNe>@2_J)n7&X%A zYh*pudA#FU#~d!~diQR!Vm6|^=JFP&C+?R} zwfi5Ef4KnWx93ap7x(n|v^0u0$HhsVEyt;Z*&TeTeY^=jn^w9*MUkGfSD$p6+`kvf zhJLBlQ_swcw`$kJU|2Pey1kgQ`x*aE_f2h=#^if35$^uZliQWGOjLl$;(EEn8DDm=7YbkrbQx&P=3K^4S9T_DpLf0dr@HY?$H6v# zSZhUs$&J(rNuQmVorK({je+X^O;A~(S6YYiggPPBcDJ^&)HZF6-MAlrq~ZNN8SpX+ zyU({=#j8oyE!=CnUL`sqJld4W^Y!gesQ7#t)9z`(W;s&lYcbTwWz<;nr@C2tL4(tD zP^pm{butB4SaZvG?d zyLAkOnbKT11{|{GdAd!ix-!jGNi%*t;TuoA>>;$>==%^~uR)c9wot1Y@nlzU8_JRR zL@=#PNPcU0DCHh7{t}Gi19N%+R6F-$D|TIuK{S{|GP_Qc?7Yr?RTkNDx%}#bA~KsN z6OTdD%qSG$Eqc;U#0%02?@{p=KWMb@~>CSyBq)65GJ-Q3(Xvgqh2UejY0_ z_SaMv6y%0)N;NINRnBwB{epK%`s#%y>9;0YY~neMwBy@pODBufKttUmU&u{k&r%v- zO{(TeD2r%Dd>##pYkLv#10DK6#W$UiR8HbY6*`?DCs8;3%H8|6@mAJV@ANk4+lRjA zmdl=AJu9zI8^S;0nR4lR9(UM+fffe;?-e!x0F04TRM57{9ZJ~8#FC_+W=0m3*Jsr3|?NPeLdw`_ zf(bw9=nz})`0>xk4Bh2J*|bJuT3~D7*n^&(g>!>-vK6utJ_JRQ6AWHOKTAs>?!dca zHY1d+K2DuTJ>L(+H`uEje=8@E7mmKdJ~ujvRe@X^cK*C}@FFQ7lXY}9Gq6>e1%9}# zBHnG2`_Lhf@=n%-fqgw9_56)DCVT*-TNHG1_npvT@6@9^^s?})4ks+Ddc&?xq2NrK z_hyVA-?02bgj)qSN1t@i1R{f8cM+u;Uy+RU<|QQ4B9lXt3rm~bD|C`ZCVFPYL{jX- zjE~-Gv&pY5yVT(dAG6Nx=t_K1ZGAAaxTIjChupIsI3W4HS2dXUM=&qfFqu*B;&VhS zHc`|NR;In?Y+SiC&(UUBiy5-z6^@Nk_t9*b)(AB3sbq@&Fr z*V0=*uv4zaZe?Z{1H+q{f4nZCmBelNeGlHM30c4Hi&`;$YNRaH-P{-EN#1%~>rG?z zmO>CZu`CO(@i?Zp8+#UWD&%A$#m($C0eYU}=3Y{T-fP3;qKgDNp@lb;FDIc7_?vw( z1@o)==|Te<2xPz|gcRs5UVqqO+ONg;E>E>%49>;TGd*6j z?>bx29dbs0A-REp6g_FVJCyZl!_=LvDBC zldR?JWyx}b zIT%Fvk~@Pj%*5mvzF~lC&XKoC`ywnpk*v2c|Fz6DVH#_c;ElPJH@`L1av4#)R8BfD zTG#7*LR@}xIrbc*O&~TgO7qP`Rmy^d5>gCJveX^%`lZ>8i14%;+qQMdX^pjG8@pFDb{Dw9jK^042d;N-=xbUaNgs zWpg%#t7bqNrE(&L>)n79f;d_FlLtcYN_R2FrH6q-{y^%qg{u!&vFhuA_iY%ucRKws zeTQ=XAB}Pd1ko;*PnBIog!P-%CVDW&KWb27On-{-_A1|0!6VyAKQ$0$Z&`;pBQ;P< z#~TeBb=CA7b^R`tjJ^R77EdtY#D_Y&&R3#ALnNO5Roz*<%0>J-8qRW>GT#rr_(0iF zz=lW)1D7jm9j%#Com%F`2U;rJ&ebd2WMn*C-X9k2N?h*jCVLK|f~5V?`rh4!p69Q4 z<<9jtROz1g2UyE`a;WLIT^ch82gw^6zV=7-CfO|&4M+0Yaysg$4d=;lQd&+^wM7$R zU!h#|c|bi5@TtX@s|kG)AJdw31S1!R^JLbRXm--wwzt1=ZzRXTe2-P~`8JHnkAS8C zD_JKg5R*Zu_nJz$HFe+48C;U^h`+IVSoqRKe#gHtw9Y5bMYH+&_6gkOJYi+{F0QFA zlH8ckQ9-l;o+WPhz`3yRGiu+aprN$JBCF<~N2ndA{G=XQwi2}f9l;4ZLYi2f&iTmf zP}wy?B*##-<3g8@ZPU9MMc6laygsy}==OrWwE|1ib}i1T$LdwDoRr2QJw-V5%Vp=& zcwIS7?V(pmUx=|Mp{RCoH49E7Vb!twMW;dc9@})(JAsMsQz`bQ&^`Rt*;8DDtMfi% ze0@Pm|GT`L5trooe4r@GSmZT73w1(1*QmYNsz=qZD2mf3UdunU^4PTNf{e#0oPL^< z-*96*K8RA-WbhwBGgc%LN&m6vbm}@c)Vr&Xgz}zs$_d6W8_%4)jWub^w~t~Vw7@Zi zUgWG<7UL#ABh`ji*!>F1!0e{+sxoHK-tNNd4wlp;zBoidpqjL);YTy+6x|dVS?yX_ zbf z)QQzhEsJ8*6X+a<5vxedJu=?rwZJrRt$hE`nso(dsFCw-bjK)rq>*Y2ri?Ey|`jY;tOK;GO?r8sBj#Ua5}MB78Q z#Y26CLk6HalAn`dFEAL|`Uo^paK?m| zf*(Y=%;l9S`CNcGyA2<|2!7o0`4rbsisswrg35s{n%gV7>e*rTRsh6XL$@}k+GLVD zYEABM!wPXp3F35kgG|>u89GTJ0cXOWO6)I|!a_tp=ak1Etbzqd!+2ccYo=j8NJ1^% z&l{Mc7%eF)S&_(Iz&MCwtrG4uXawObPw}m@kA>>8(kf5v@pOM&#kS}K8zn}~wfb?a9; z-D1-5D3M^C>*p+vr<<(=GdW1)7vB$sHajLmiyjv1Yg}UtnV>W#9;68dDK$3J%2+jS z)gx`?`sO~uSJsWXiP3be0o~XL*P=hi)E>?LTNksnUx`IDqo&My=DiX1ncy)3_I^5a*taW9YM%n}L8HgP;?--Df z7hn^G?MfjXH+0_y2uYE!Jz8rZ2>o5&r4<4*B%T|U9Ic^>Y7Pb;5MnW(*s<3y^)^&6 zVj=7dHvAINt0I2B+x1AGE5y?d(I|M1ty}2^cPCL=z&5w+k;U;LWvid>W7}!ct88^dPGtBGTF>ngoBK(+5Ai*RUCEPd-lNwG~%NDiM;5gqGvlD65h{v z7lF5l(xPg9RoNHXL>xiwFTta3D2u7=pDuu}Z@*!R7P2nk#!5q~|RRP;)4U(=f5j--;WkYs?#jR@wMw@|b#UUKy9)dElhABkY+9F&;}K ze^#S9^^;NS>B+|G-L)XB;~?A*wz^u$N(OdZoy!}Y_0@|yZ{0k!#**Cv&m;OKM(-b{tML;&i~WYohnhT^KXb3a)Rk$LPkGJPK2igo4W zg>iARIAy{8nKsKM*Idz8d#r*Jh)KDSMu(~wZksMw{mfE8Y^vzJ<3g+a725A-v~2fl zo8XyOrH>{FM2PfvDqhYDEru)fqc_XZgFm1~b6V4*P@eFfw4TE8K(uEqT3h^=yDx5& zXU^x$jHmN57FRRa1r`d3+S5W!69aA3i~b*DUmX?I_r)uS2nxthQqnDmbV_$4AtfQ5 zf{1iVr*wBJpwb~FAdPf4NT+lNymL|c{(SGBx7IAytYPNfd(PQspB9`cxM;qh z@`<^Xv@#2AsF}VX=g{s#se!fvLHlmRtKac@IL2VXoX!X`KEH_t(hXXwHO`oqlxcAN`# z_@DafZ}cXj3J@^(`2rXX>7IDk2EiKGLCI_R>Zx0lq2uu;SL&oba)&%iDJ+8TI8;~9 zhi`(9R^~IZ^kBsEpnHVE=)$Rzn7Tjt_kwqQx5s0HvUgQA;Nd{Ze#8K0uMXMF#l|F} zRW^xOWuGZ?i&l}&aj~@+$9u&a-y&0WGWvbU6167Z)i@)!s)x9vaMf+LG}IUA56f$vNrsF9JpSqWw9! zml#FojR_UCbZmIvDW#S5HP`wg*6^KoUXg=1=~#73zvWBzW~;>NF7N&f)eb(BPWn)1 z#1X2#JDkX~Vq#jjV+u=xT_>LMaTl99ap27N+Wt_;7Gj$`xLe>B_LwxHSb*M%3_r$Qy~&!>o_&w>OVAOI8lZpZtDI8%K(&8*l`y8=PThe~0= zX7^rxP7{NzaL}g%;u#d2J*or&2dM2lR`ezt04@`D7}-r=KFU`SPi3GDuyK(V4SEZi z2mS&B0JkZBKbah^_!sGHqP8qTgkQ8tVCc6u240GFp>*=5a1{o23h*eYas_@BPo@Dm z$x7Q+!|`)-=dV9sMnyEQN~^QGyiylycv2sBf-3*G7%)8wPv3v>#B;|u9L{?sHum}3 z&tEIqarrmMn~e=-&_(VzdxaIwJ#*PC8+kQ0>KeyU#}z6{B|W@-$-K;BsrjoIVU{eT zMT!F>DQxuJ;xZ`J&%jdb4#G>iE#-6Aw3CbeI{3y5)V>?U@*3`rbUfkFDp{%c+wuqk z=BF!5Pzmuom6pZdUVH|-U;b

FuwDbXwqljoT?PRF%0*7zeA|PLY3|vQ7~+R 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؟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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 3285473..880c691 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ These libraries provide standardized syntax for sending a message to a bridge. The most common pattern is to have an authorized contract forward a message to another "business logic" contract to abstract away bridge dependencies. Receivers are contracts which perform this generic translation - decoding the bridge-specific message and forwarding to another `target` contract. The `target` contract should have logic to restrict who can call it and permission this to one or more bridge receivers. -TODO diagram +![xchain-helpers](.assets/xchain-helpers.png) ## E2E Testing Infrastructure diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index e6d8482..f7a2037 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -20,16 +20,6 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { deal(randomAddress, 100 ether); } - function test_receiver_constructor() public { - initBaseContracts(getChain("arbitrum_one").createFork()); - destination.selectFork(); - - ArbitrumReceiver receiver = new ArbitrumReceiver(sourceAuthority, address(moDestination)); - - assertEq(receiver.l1Authority(), sourceAuthority); - assertEq(receiver.target(), address(moDestination)); - } - // Use Arbitrum One for failure test as the code logic is the same function test_invalidSourceAuthority() public { diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index 0fd2629..267a66d 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -15,19 +15,6 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { uint32 sourceDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM; uint32 destinationDomainId; - function test_receiver_constructor() public { - destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; - initBaseContracts(getChain("optimism").createFork()); - destination.selectFork(); - - CCTPReceiver receiver = new CCTPReceiver(bridge.destinationCrossChainMessenger, sourceDomainId, sourceAuthority, address(moDestination)); - - assertEq(receiver.destinationMessenger(), bridge.destinationCrossChainMessenger); - assertEq(receiver.sourceDomainId(), sourceDomainId); - assertEq(receiver.sourceAuthority(), sourceAuthority); - assertEq(receiver.target(), address(moDestination)); - } - // Use Optimism for failure tests as the code logic is the same function test_invalidSourceAuthority() public { diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index 2e6d2ed..bf08229 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -12,18 +12,6 @@ contract GnosisIntegrationTest is IntegrationBaseTest { using AMBBridgeTesting for *; using DomainHelpers for *; - function test_receiver_constructor() public { - initBaseContracts(getChain("gnosis_chain").createFork()); - destination.selectFork(); - - AMBReceiver receiver = new AMBReceiver(bridge.destinationCrossChainMessenger, bytes32(uint256(1)), sourceAuthority, address(moDestination)); - - assertEq(receiver.amb(), bridge.destinationCrossChainMessenger); - assertEq(receiver.sourceChainId(), bytes32(uint256(1))); - assertEq(receiver.sourceAuthority(), sourceAuthority); - assertEq(receiver.target(), address(moDestination)); - } - function test_invalidSourceAuthority() public { initBaseContracts(getChain("gnosis_chain").createFork()); diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index 94bc493..a2c2f8c 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -14,16 +14,6 @@ contract OptimismIntegrationTest is IntegrationBaseTest { event FailedRelayedMessage(bytes32); - function test_receiver_constructor() public { - initBaseContracts(getChain("optimism").createFork()); - destination.selectFork(); - - OptimismReceiver receiver = new OptimismReceiver(sourceAuthority, address(moDestination)); - - assertEq(receiver.l1Authority(), sourceAuthority); - assertEq(receiver.target(), address(moDestination)); - } - // Use Arbitrum One for failure test as the code logic is the same function test_invalidSourceAuthority() public { From 9d081f173c7db76d1141bb0f5794bdf5074f00d7 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 17:59:26 +0900 Subject: [PATCH 26/42] add unit tests for amb receiver --- test/AMBReceiver.t.sol | 113 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/AMBReceiver.t.sol diff --git a/test/AMBReceiver.t.sol b/test/AMBReceiver.t.sol new file mode 100644 index 0000000..1d232a1 --- /dev/null +++ b/test/AMBReceiver.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; + +import { AMBReceiver } from "src/receivers/AMBReceiver.sol"; + +contract AMBMock { + + bytes32 public messageSourceChainId; + address public messageSender; + + constructor(bytes32 _messageSourceChainId, address _messageSender) { + messageSourceChainId = _messageSourceChainId; + messageSender = _messageSender; + } + + function __setSourceChainId(bytes32 _messageSourceChainId) public { + messageSourceChainId = _messageSourceChainId; + } + + function __setSender(address _messageSender) public { + messageSender = _messageSender; + } + +} + +contract TargetContractMock { + + uint256 public s; + + function someFunc() external { + s++; + } + + function revertFunc() external pure { + revert("error"); + } + +} + +contract AMBReceiverTest is Test { + + AMBMock amb; + TargetContractMock target; + + AMBReceiver receiver; + + bytes32 sourceChainId = bytes32(uint256(1)); + address sourceAuthority = makeAddr("sourceAuthority"); + address randomAddress = makeAddr("randomAddress"); + + function setUp() public { + amb = new AMBMock(sourceChainId, sourceAuthority); + target = new TargetContractMock(); + + receiver = new AMBReceiver( + address(amb), + sourceChainId, + sourceAuthority, + address(target) + ); + } + + function test_constructor() public { + receiver = new AMBReceiver( + address(amb), + sourceChainId, + sourceAuthority, + address(target) + ); + + assertEq(receiver.amb(), address(amb)); + assertEq(receiver.sourceChainId(), sourceChainId); + assertEq(receiver.sourceAuthority(), sourceAuthority); + assertEq(receiver.target(), address(target)); + } + + function test_forward_invalidSender() public { + vm.prank(randomAddress); + vm.expectRevert("AMBReceiver/invalid-sender"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_invalidSourceChainId() public { + amb.__setSourceChainId(bytes32(uint256(2))); + + vm.prank(address(amb)); + vm.expectRevert("AMBReceiver/invalid-sourceChainId"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_invalidSourceAuthority() public { + amb.__setSender(randomAddress); + + vm.prank(address(amb)); + vm.expectRevert("AMBReceiver/invalid-sourceAuthority"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_success() public { + vm.prank(address(amb)); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + assertEq(target.s(), 1); + } + + function test_forward_revert() public { + vm.prank(address(amb)); + vm.expectRevert("error"); + receiver.forward(abi.encodeCall(TargetContractMock.revertFunc, ())); + } + +} From 747c4288b0cb1e1034f1b425794812966c0ca941 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 19:03:09 +0900 Subject: [PATCH 27/42] add cctp unit test --- test/AMBReceiver.t.sol | 19 ++---- test/CCTPReceiver.t.sol | 97 +++++++++++++++++++++++++++++++ test/mocks/TargetContractMock.sol | 16 +++++ 3 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 test/CCTPReceiver.t.sol create mode 100644 test/mocks/TargetContractMock.sol diff --git a/test/AMBReceiver.t.sol b/test/AMBReceiver.t.sol index 1d232a1..b1b5986 100644 --- a/test/AMBReceiver.t.sol +++ b/test/AMBReceiver.t.sol @@ -3,6 +3,8 @@ pragma solidity >=0.8.0; import "forge-std/Test.sol"; +import { TargetContractMock } from "test/mocks/TargetContractMock.sol"; + import { AMBReceiver } from "src/receivers/AMBReceiver.sol"; contract AMBMock { @@ -25,20 +27,6 @@ contract AMBMock { } -contract TargetContractMock { - - uint256 public s; - - function someFunc() external { - s++; - } - - function revertFunc() external pure { - revert("error"); - } - -} - contract AMBReceiverTest is Test { AMBMock amb; @@ -99,9 +87,10 @@ contract AMBReceiverTest is Test { } function test_forward_success() public { + assertEq(target.data(), 0); vm.prank(address(amb)); receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); - assertEq(target.s(), 1); + assertEq(target.data(), 1); } function test_forward_revert() public { diff --git a/test/CCTPReceiver.t.sol b/test/CCTPReceiver.t.sol new file mode 100644 index 0000000..2c81175 --- /dev/null +++ b/test/CCTPReceiver.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; + +import { TargetContractMock } from "test/mocks/TargetContractMock.sol"; + +import { CCTPReceiver } from "src/receivers/CCTPReceiver.sol"; + +contract CCTPReceiverTest is Test { + + TargetContractMock target; + + CCTPReceiver receiver; + + address destinationMessenger = makeAddr("destinationMessenger"); + uint32 sourceDomainId = 1; + address sourceAuthority = makeAddr("sourceAuthority"); + address randomAddress = makeAddr("randomAddress"); + + function setUp() public { + target = new TargetContractMock(); + + receiver = new CCTPReceiver( + destinationMessenger, + sourceDomainId, + sourceAuthority, + address(target) + ); + } + + function test_constructor() public { + receiver = new CCTPReceiver( + destinationMessenger, + sourceDomainId, + sourceAuthority, + address(target) + ); + + assertEq(receiver.destinationMessenger(), destinationMessenger); + assertEq(receiver.sourceDomainId(), sourceDomainId); + assertEq(receiver.sourceAuthority(), sourceAuthority); + assertEq(receiver.target(), address(target)); + } + + function test_handleReceiveMessage_invalidSender() public { + vm.prank(randomAddress); + vm.expectRevert("CCTPReceiver/invalid-sender"); + receiver.handleReceiveMessage( + sourceDomainId, + bytes32(uint256(uint160(sourceAuthority))), + abi.encodeCall(TargetContractMock.someFunc, ()) + ); + } + + function test_handleReceiveMessage_invalidSourceChainId() public { + vm.prank(destinationMessenger); + vm.expectRevert("CCTPReceiver/invalid-sourceDomain"); + receiver.handleReceiveMessage( + 2, + bytes32(uint256(uint160(sourceAuthority))), + abi.encodeCall(TargetContractMock.someFunc, ()) + ); + } + + function test_handleReceiveMessage_invalidSourceAuthority() public { + vm.prank(destinationMessenger); + vm.expectRevert("CCTPReceiver/invalid-sourceAuthority"); + receiver.handleReceiveMessage( + sourceDomainId, + bytes32(uint256(uint160(randomAddress))), + abi.encodeCall(TargetContractMock.someFunc, ()) + ); + } + + function test_handleReceiveMessage_success() public { + assertEq(target.data(), 0); + vm.prank(destinationMessenger); + receiver.handleReceiveMessage( + sourceDomainId, + bytes32(uint256(uint160(sourceAuthority))), + abi.encodeCall(TargetContractMock.someFunc, ()) + ); + assertEq(target.data(), 1); + } + + function test_handleReceiveMessage_revert() public { + vm.prank(destinationMessenger); + vm.expectRevert("error"); + receiver.handleReceiveMessage( + sourceDomainId, + bytes32(uint256(uint160(sourceAuthority))), + abi.encodeCall(TargetContractMock.revertFunc, ()) + ); + } + +} diff --git a/test/mocks/TargetContractMock.sol b/test/mocks/TargetContractMock.sol new file mode 100644 index 0000000..77fddb0 --- /dev/null +++ b/test/mocks/TargetContractMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +contract TargetContractMock { + + uint256 public data; + + function someFunc() external { + data++; + } + + function revertFunc() external pure { + revert("error"); + } + +} From 8190995d6f2e51b27cd0b531906bf8702444da20 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 19:05:27 +0900 Subject: [PATCH 28/42] change cctp authority type --- src/receivers/CCTPReceiver.sol | 10 +++++----- test/CircleCCTPIntegration.t.sol | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/receivers/CCTPReceiver.sol b/src/receivers/CCTPReceiver.sol index 5b2206d..04bc204 100644 --- a/src/receivers/CCTPReceiver.sol +++ b/src/receivers/CCTPReceiver.sol @@ -9,13 +9,13 @@ contract CCTPReceiver { address public immutable destinationMessenger; uint32 public immutable sourceDomainId; - address public immutable sourceAuthority; + bytes32 public immutable sourceAuthority; address public immutable target; constructor( address _destinationMessenger, uint32 _sourceDomainId, - address _sourceAuthority, + bytes32 _sourceAuthority, address _target ) { destinationMessenger = _destinationMessenger; @@ -29,9 +29,9 @@ contract CCTPReceiver { bytes32 sender, bytes calldata messageBody ) external returns (bool) { - require(msg.sender == destinationMessenger, "CCTPReceiver/invalid-sender"); - require(sourceDomainId == sourceDomain, "CCTPReceiver/invalid-sourceDomain"); - require(sender == bytes32(uint256(uint160(sourceAuthority))), "CCTPReceiver/invalid-sourceAuthority"); + require(msg.sender == destinationMessenger, "CCTPReceiver/invalid-sender"); + require(sourceDomainId == sourceDomain, "CCTPReceiver/invalid-sourceDomain"); + require(sender == sourceAuthority, "CCTPReceiver/invalid-sourceAuthority"); (bool success, bytes memory ret) = target.call(messageBody); if (!success) { diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index 267a66d..0f262de 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -77,11 +77,11 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { } function initSourceReceiver() internal override returns (address) { - return address(new CCTPReceiver(bridge.sourceCrossChainMessenger, destinationDomainId, destinationAuthority, address(moSource))); + return address(new CCTPReceiver(bridge.sourceCrossChainMessenger, destinationDomainId, bytes32(uint256(uint160(destinationAuthority))), address(moSource))); } function initDestinationReceiver() internal override returns (address) { - return address(new CCTPReceiver(bridge.destinationCrossChainMessenger, sourceDomainId, sourceAuthority, address(moDestination))); + return address(new CCTPReceiver(bridge.destinationCrossChainMessenger, sourceDomainId, bytes32(uint256(uint160(sourceAuthority))), address(moDestination))); } function initBridgeTesting() internal override returns (Bridge memory) { From 3fcbcce7ce31ab516c97762a9c9cd1f030e282f7 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 19:08:54 +0900 Subject: [PATCH 29/42] update to new cctp receiver --- test/CCTPReceiver.t.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/CCTPReceiver.t.sol b/test/CCTPReceiver.t.sol index 2c81175..4f77c04 100644 --- a/test/CCTPReceiver.t.sol +++ b/test/CCTPReceiver.t.sol @@ -15,7 +15,7 @@ contract CCTPReceiverTest is Test { address destinationMessenger = makeAddr("destinationMessenger"); uint32 sourceDomainId = 1; - address sourceAuthority = makeAddr("sourceAuthority"); + bytes32 sourceAuthority = bytes32(uint256(uint160(makeAddr("sourceAuthority")))); address randomAddress = makeAddr("randomAddress"); function setUp() public { @@ -48,7 +48,7 @@ contract CCTPReceiverTest is Test { vm.expectRevert("CCTPReceiver/invalid-sender"); receiver.handleReceiveMessage( sourceDomainId, - bytes32(uint256(uint160(sourceAuthority))), + sourceAuthority, abi.encodeCall(TargetContractMock.someFunc, ()) ); } @@ -58,7 +58,7 @@ contract CCTPReceiverTest is Test { vm.expectRevert("CCTPReceiver/invalid-sourceDomain"); receiver.handleReceiveMessage( 2, - bytes32(uint256(uint160(sourceAuthority))), + sourceAuthority, abi.encodeCall(TargetContractMock.someFunc, ()) ); } @@ -78,7 +78,7 @@ contract CCTPReceiverTest is Test { vm.prank(destinationMessenger); receiver.handleReceiveMessage( sourceDomainId, - bytes32(uint256(uint160(sourceAuthority))), + sourceAuthority, abi.encodeCall(TargetContractMock.someFunc, ()) ); assertEq(target.data(), 1); @@ -89,7 +89,7 @@ contract CCTPReceiverTest is Test { vm.expectRevert("error"); receiver.handleReceiveMessage( sourceDomainId, - bytes32(uint256(uint160(sourceAuthority))), + sourceAuthority, abi.encodeCall(TargetContractMock.revertFunc, ()) ); } From 5669f7db781c83cc1782a474cc68493febcd19f8 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 19:24:50 +0900 Subject: [PATCH 30/42] add arbitrum receiever unit tests --- test/ArbitrumReceiver.t.sol | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/ArbitrumReceiver.t.sol diff --git a/test/ArbitrumReceiver.t.sol b/test/ArbitrumReceiver.t.sol new file mode 100644 index 0000000..02d57c6 --- /dev/null +++ b/test/ArbitrumReceiver.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; + +import { TargetContractMock } from "test/mocks/TargetContractMock.sol"; + +import { ArbitrumReceiver } from "src/receivers/ArbitrumReceiver.sol"; + +contract ArbitrumReceiverTest is Test { + + TargetContractMock target; + + ArbitrumReceiver receiver; + + address sourceAuthority = makeAddr("sourceAuthority"); + address sourceAuthorityWithOffset; + address randomAddress = makeAddr("randomAddress"); + + function setUp() public { + target = new TargetContractMock(); + + receiver = new ArbitrumReceiver( + sourceAuthority, + address(target) + ); + unchecked { + sourceAuthorityWithOffset = address(uint160(sourceAuthority) + uint160(0x1111000000000000000000000000000000001111)); + } + } + + function test_constructor() public { + receiver = new ArbitrumReceiver( + sourceAuthority, + address(target) + ); + + assertEq(receiver.l1Authority(), sourceAuthority); + assertEq(receiver.target(), address(target)); + } + + function test_forward_invalidL1Authority() public { + vm.prank(randomAddress); + vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_invalidL1AuthoritySourceAuthorityNoOffset() public { + vm.prank(sourceAuthority); + vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_success() public { + assertEq(target.data(), 0); + vm.prank(sourceAuthorityWithOffset); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + assertEq(target.data(), 1); + } + + function test_forward_revert() public { + vm.prank(sourceAuthorityWithOffset); + vm.expectRevert("error"); + receiver.forward(abi.encodeCall(TargetContractMock.revertFunc, ())); + } + +} From dc07c16bf3da0a39c61b77568492559628cf015f Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 5 Jun 2024 19:35:53 +0900 Subject: [PATCH 31/42] add optimism test --- test/OptimismReceiver.t.sol | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/OptimismReceiver.t.sol diff --git a/test/OptimismReceiver.t.sol b/test/OptimismReceiver.t.sol new file mode 100644 index 0000000..0fdb387 --- /dev/null +++ b/test/OptimismReceiver.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; + +import { TargetContractMock } from "test/mocks/TargetContractMock.sol"; + +import { OptimismReceiver } from "src/receivers/OptimismReceiver.sol"; + +contract OptimismMessengerMock { + + address public xDomainMessageSender; + + function __setSender(address _xDomainMessageSender) public { + xDomainMessageSender = _xDomainMessageSender; + } + +} + +contract OptimismReceiverTest is Test { + + OptimismMessengerMock l2CrossDomain; + TargetContractMock target; + + OptimismReceiver receiver; + + address l2CrossDomainAddr = 0x4200000000000000000000000000000000000007; + + address sourceAuthority = makeAddr("sourceAuthority"); + address randomAddress = makeAddr("randomAddress"); + + function setUp() public { + // Set the code at the particular address + l2CrossDomain = new OptimismMessengerMock(); + vm.etch(l2CrossDomainAddr, address(l2CrossDomain).code); + l2CrossDomain = OptimismMessengerMock(l2CrossDomainAddr); + l2CrossDomain.__setSender(sourceAuthority); + + target = new TargetContractMock(); + + receiver = new OptimismReceiver( + sourceAuthority, + address(target) + ); + } + + function test_constructor() public { + receiver = new OptimismReceiver( + sourceAuthority, + address(target) + ); + + assertEq(receiver.l1Authority(), sourceAuthority); + assertEq(receiver.target(), address(target)); + } + + function test_forward_invalidSender() public { + vm.prank(randomAddress); + vm.expectRevert("OptimismReceiver/invalid-sender"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_invalidL1Authority() public { + l2CrossDomain.__setSender(randomAddress); + + vm.prank(address(l2CrossDomain)); + vm.expectRevert("OptimismReceiver/invalid-l1Authority"); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + } + + function test_forward_success() public { + assertEq(target.data(), 0); + vm.prank(address(l2CrossDomain)); + receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + assertEq(target.data(), 1); + } + + function test_forward_revert() public { + vm.prank(address(l2CrossDomain)); + vm.expectRevert("error"); + receiver.forward(abi.encodeCall(TargetContractMock.revertFunc, ())); + } + +} From 9874e5c5a158f8cb33457363ba0c77219e4456d6 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sat, 8 Jun 2024 19:52:33 +0900 Subject: [PATCH 32/42] use relative paths for library code --- src/testing/bridges/AMBBridgeTesting.sol | 6 +++--- src/testing/bridges/ArbitrumBridgeTesting.sol | 6 +++--- src/testing/bridges/CCTPBridgeTesting.sol | 6 +++--- src/testing/bridges/OptimismBridgeTesting.sol | 6 +++--- src/testing/utils/RecordedLogs.sol | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/testing/bridges/AMBBridgeTesting.sol b/src/testing/bridges/AMBBridgeTesting.sol index e1b28c6..32c3cec 100644 --- a/src/testing/bridges/AMBBridgeTesting.sol +++ b/src/testing/bridges/AMBBridgeTesting.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { Bridge } from "src/testing/Bridge.sol"; -import { Domain, DomainHelpers } from "src/testing/Domain.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { Bridge } from "../Bridge.sol"; +import { Domain, DomainHelpers } from "../Domain.sol"; +import { RecordedLogs } from "../utils/RecordedLogs.sol"; interface IAMB { function validatorContract() external view returns (address); diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index 073d40e..0046b88 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { Bridge } from "src/testing/Bridge.sol"; -import { Domain, DomainHelpers } from "src/testing/Domain.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { Bridge } from "../Bridge.sol"; +import { Domain, DomainHelpers } from "../Domain.sol"; +import { RecordedLogs } from "../utils/RecordedLogs.sol"; interface InboxLike { function createRetryableTicket( diff --git a/src/testing/bridges/CCTPBridgeTesting.sol b/src/testing/bridges/CCTPBridgeTesting.sol index 229a2b2..bc91f6d 100644 --- a/src/testing/bridges/CCTPBridgeTesting.sol +++ b/src/testing/bridges/CCTPBridgeTesting.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { Bridge } from "src/testing/Bridge.sol"; -import { Domain, DomainHelpers } from "src/testing/Domain.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { Bridge } from "../Bridge.sol"; +import { Domain, DomainHelpers } from "../Domain.sol"; +import { RecordedLogs } from "../utils/RecordedLogs.sol"; interface IMessenger { function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); diff --git a/src/testing/bridges/OptimismBridgeTesting.sol b/src/testing/bridges/OptimismBridgeTesting.sol index 97626a5..4e3fbee 100644 --- a/src/testing/bridges/OptimismBridgeTesting.sol +++ b/src/testing/bridges/OptimismBridgeTesting.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { Bridge } from "src/testing/Bridge.sol"; -import { Domain, DomainHelpers } from "src/testing/Domain.sol"; -import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol"; +import { Bridge } from "../Bridge.sol"; +import { Domain, DomainHelpers } from "../Domain.sol"; +import { RecordedLogs } from "../utils/RecordedLogs.sol"; interface IMessenger { function sendMessage( diff --git a/src/testing/utils/RecordedLogs.sol b/src/testing/utils/RecordedLogs.sol index 4912a24..0658f33 100644 --- a/src/testing/utils/RecordedLogs.sol +++ b/src/testing/utils/RecordedLogs.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0; import { Vm } from "forge-std/Vm.sol"; -import { Bridge } from "src/testing/Bridge.sol"; +import { Bridge } from "../Bridge.sol"; library RecordedLogs { From 46ee62c4ec1f2abf51842c952c13b142704fcfc1 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sat, 8 Jun 2024 20:23:21 +0900 Subject: [PATCH 33/42] fix for subsequent messages --- src/testing/bridges/AMBBridgeTesting.sol | 4 ++-- src/testing/bridges/ArbitrumBridgeTesting.sol | 4 ++-- src/testing/bridges/CCTPBridgeTesting.sol | 4 ++-- src/testing/bridges/OptimismBridgeTesting.sol | 4 ++-- src/testing/utils/RecordedLogs.sol | 4 ++-- test/IntegrationBase.t.sol | 10 ++++++++++ 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/testing/bridges/AMBBridgeTesting.sol b/src/testing/bridges/AMBBridgeTesting.sol index 32c3cec..0340e7d 100644 --- a/src/testing/bridges/AMBBridgeTesting.sol +++ b/src/testing/bridges/AMBBridgeTesting.sol @@ -71,7 +71,7 @@ library AMBBridgeTesting { return bridge; } - function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge storage bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, USER_REQUEST_FOR_AFFIRMATION_TOPIC, USER_REQUEST_FOR_SIGNATURE_TOPIC, bridge.sourceCrossChainMessenger); @@ -82,7 +82,7 @@ library AMBBridgeTesting { } } - function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge storage bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, USER_REQUEST_FOR_AFFIRMATION_TOPIC, USER_REQUEST_FOR_SIGNATURE_TOPIC, bridge.destinationCrossChainMessenger); diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index 0046b88..4ab8ece 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -122,7 +122,7 @@ library ArbitrumBridgeTesting { return bridge; } - function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge storage bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); Vm.Log[] memory logs = RecordedLogs.getLogs(); @@ -149,7 +149,7 @@ library ArbitrumBridgeTesting { } } - function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge storage bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SEND_TO_L1_TOPIC, bridge.destinationCrossChainMessenger); diff --git a/src/testing/bridges/CCTPBridgeTesting.sol b/src/testing/bridges/CCTPBridgeTesting.sol index bc91f6d..5a90714 100644 --- a/src/testing/bridges/CCTPBridgeTesting.sol +++ b/src/testing/bridges/CCTPBridgeTesting.sol @@ -71,7 +71,7 @@ library CCTPBridgeTesting { return bridge; } - function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge storage bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); Vm.Log[] memory logs = bridge.ingestAndFilterLogs(true, SENT_MESSAGE_TOPIC, bridge.sourceCrossChainMessenger); @@ -84,7 +84,7 @@ library CCTPBridgeTesting { } } - function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge storage bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, bridge.destinationCrossChainMessenger); diff --git a/src/testing/bridges/OptimismBridgeTesting.sol b/src/testing/bridges/OptimismBridgeTesting.sol index 4e3fbee..7823e6f 100644 --- a/src/testing/bridges/OptimismBridgeTesting.sol +++ b/src/testing/bridges/OptimismBridgeTesting.sol @@ -78,7 +78,7 @@ library OptimismBridgeTesting { return bridge; } - function relayMessagesToDestination(Bridge memory bridge, bool switchToDestinationFork) internal { + function relayMessagesToDestination(Bridge storage bridge, bool switchToDestinationFork) internal { bridge.destination.selectFork(); address malias; @@ -100,7 +100,7 @@ library OptimismBridgeTesting { } } - function relayMessagesToSource(Bridge memory bridge, bool switchToSourceFork) internal { + function relayMessagesToSource(Bridge storage bridge, bool switchToSourceFork) internal { bridge.source.selectFork(); Vm.Log[] memory logs = bridge.ingestAndFilterLogs(false, SENT_MESSAGE_TOPIC, bridge.destinationCrossChainMessenger); diff --git a/src/testing/utils/RecordedLogs.sol b/src/testing/utils/RecordedLogs.sol index 0658f33..1ff477d 100644 --- a/src/testing/utils/RecordedLogs.sol +++ b/src/testing/utils/RecordedLogs.sol @@ -33,7 +33,7 @@ library RecordedLogs { return logs; } - function ingestAndFilterLogs(Bridge memory bridge, bool sourceToDestination, bytes32 topic0, bytes32 topic1, address emitter) internal returns (Vm.Log[] memory filteredLogs) { + function ingestAndFilterLogs(Bridge storage bridge, bool sourceToDestination, bytes32 topic0, bytes32 topic1, address emitter) internal returns (Vm.Log[] memory filteredLogs) { Vm.Log[] memory logs = RecordedLogs.getLogs(); uint256 lastIndex = sourceToDestination ? bridge.lastSourceLogIndex : bridge.lastDestinationLogIndex; uint256 pushedIndex = 0; @@ -53,7 +53,7 @@ library RecordedLogs { assembly { mstore(filteredLogs, pushedIndex) } } - function ingestAndFilterLogs(Bridge memory bridge, bool sourceToDestination, bytes32 topic, address emitter) internal returns (Vm.Log[] memory filteredLogs) { + function ingestAndFilterLogs(Bridge storage bridge, bool sourceToDestination, bytes32 topic, address emitter) internal returns (Vm.Log[] memory filteredLogs) { return ingestAndFilterLogs(bridge, sourceToDestination, topic, bytes32(0), emitter); } diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index 2f40cc9..aa16544 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -105,6 +105,16 @@ abstract contract IntegrationBaseTest is Test { assertEq(moSource.length(), 2); assertEq(moSource.messages(0), 3); assertEq(moSource.messages(1), 4); + + // One more message to destination + vm.startPrank(sourceAuthority); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (5))); + vm.stopPrank(); + + relaySourceToDestination(); + + assertEq(moDestination.length(), 3); + assertEq(moDestination.messages(2), 5); } function initSourceReceiver() internal virtual returns (address); From 743b951032cad01dacf149cb40bb057a3272aa13 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sat, 8 Jun 2024 20:45:32 +0900 Subject: [PATCH 34/42] forge install: openzeppelin-contracts v5.0.2 --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 888d42d..e80ffd8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..dbb6104 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 From 7eb7d7a6af52a70060e1b6c89c7dbacd25c052d5 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sat, 8 Jun 2024 20:55:01 +0900 Subject: [PATCH 35/42] use fallback() instead of forward() --- src/receivers/AMBReceiver.sol | 13 ++++++------- src/receivers/ArbitrumReceiver.sol | 13 ++++++------- src/receivers/CCTPReceiver.sol | 11 +++++------ src/receivers/OptimismReceiver.sol | 13 ++++++------- test/ArbitrumIntegration.t.sol | 2 +- test/GnosisIntegration.t.sol | 6 +++--- test/OptimismIntegration.t.sol | 4 ++-- 7 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/receivers/AMBReceiver.sol b/src/receivers/AMBReceiver.sol index df285b4..f9f3cb1 100644 --- a/src/receivers/AMBReceiver.sol +++ b/src/receivers/AMBReceiver.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; +import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; + interface IArbitraryMessagingBridge { function messageSender() external view returns (address); function messageSourceChainId() external view returns (bytes32); @@ -12,6 +14,8 @@ interface IArbitraryMessagingBridge { */ contract AMBReceiver { + using Address for address; + address public immutable amb; bytes32 public immutable sourceChainId; address public immutable sourceAuthority; @@ -29,17 +33,12 @@ contract AMBReceiver { target = _target; } - function forward(bytes memory message) external { + fallback(bytes calldata message) external returns (bytes memory) { require(msg.sender == amb, "AMBReceiver/invalid-sender"); require(IArbitraryMessagingBridge(amb).messageSourceChainId() == sourceChainId, "AMBReceiver/invalid-sourceChainId"); require(IArbitraryMessagingBridge(amb).messageSender() == sourceAuthority, "AMBReceiver/invalid-sourceAuthority"); - (bool success, bytes memory ret) = target.call(message); - if (!success) { - assembly { - revert(add(ret, 0x20), mload(ret)) - } - } + return target.functionCall(message); } } diff --git a/src/receivers/ArbitrumReceiver.sol b/src/receivers/ArbitrumReceiver.sol index 4f48127..1807551 100644 --- a/src/receivers/ArbitrumReceiver.sol +++ b/src/receivers/ArbitrumReceiver.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; +import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; + /** * @title ArbitrumReceiver * @notice Receive messages to an Arbitrum-style chain. */ contract ArbitrumReceiver { + using Address for address; + address public immutable l1Authority; address public immutable target; @@ -24,15 +28,10 @@ contract ArbitrumReceiver { } } - function forward(bytes memory message) external { + fallback(bytes calldata message) external returns (bytes memory) { require(_getL1MessageSender() == l1Authority, "ArbitrumReceiver/invalid-l1Authority"); - (bool success, bytes memory ret) = target.call(message); - if (!success) { - assembly { - revert(add(ret, 0x20), mload(ret)) - } - } + return target.functionCall(message); } } diff --git a/src/receivers/CCTPReceiver.sol b/src/receivers/CCTPReceiver.sol index 04bc204..0ca5b7c 100644 --- a/src/receivers/CCTPReceiver.sol +++ b/src/receivers/CCTPReceiver.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; +import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; + /** * @title CCTPReceiver * @notice Receive messages from CCTP-style bridge. */ contract CCTPReceiver { + using Address for address; + address public immutable destinationMessenger; uint32 public immutable sourceDomainId; bytes32 public immutable sourceAuthority; @@ -33,12 +37,7 @@ contract CCTPReceiver { require(sourceDomainId == sourceDomain, "CCTPReceiver/invalid-sourceDomain"); require(sender == sourceAuthority, "CCTPReceiver/invalid-sourceAuthority"); - (bool success, bytes memory ret) = target.call(messageBody); - if (!success) { - assembly { - revert(add(ret, 0x20), mload(ret)) - } - } + target.functionCall(messageBody); return true; } diff --git a/src/receivers/OptimismReceiver.sol b/src/receivers/OptimismReceiver.sol index 65c524d..ec39e95 100644 --- a/src/receivers/OptimismReceiver.sol +++ b/src/receivers/OptimismReceiver.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; +import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; + interface ICrossDomainOptimism { function xDomainMessageSender() external view returns (address); } @@ -11,6 +13,8 @@ interface ICrossDomainOptimism { */ contract OptimismReceiver { + using Address for address; + ICrossDomainOptimism public constant l2CrossDomain = ICrossDomainOptimism(0x4200000000000000000000000000000000000007); address public immutable l1Authority; @@ -24,16 +28,11 @@ contract OptimismReceiver { target = _target; } - function forward(bytes memory message) external { + fallback(bytes calldata message) external returns (bytes memory) { require(msg.sender == address(l2CrossDomain), "OptimismReceiver/invalid-sender"); require(l2CrossDomain.xDomainMessageSender() == l1Authority, "OptimismReceiver/invalid-l1Authority"); - (bool success, bytes memory ret) = target.call(message); - if (!success) { - assembly { - revert(add(ret, 0x20), mload(ret)) - } - } + return target.functionCall(message); } } diff --git a/test/ArbitrumIntegration.t.sol b/test/ArbitrumIntegration.t.sol index f7a2037..6ef0e77 100644 --- a/test/ArbitrumIntegration.t.sol +++ b/test/ArbitrumIntegration.t.sol @@ -57,7 +57,7 @@ contract ArbitrumIntegrationTest is IntegrationBaseTest { ArbitrumForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, destinationReceiver, - abi.encodeCall(ArbitrumReceiver.forward, (message)), + message, 100000, 1 gwei, block.basefee + 10 gwei diff --git a/test/GnosisIntegration.t.sol b/test/GnosisIntegration.t.sol index bf08229..2703b7d 100644 --- a/test/GnosisIntegration.t.sol +++ b/test/GnosisIntegration.t.sol @@ -32,7 +32,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { vm.prank(randomAddress); vm.expectRevert("AMBReceiver/invalid-sender"); - AMBReceiver(destinationReceiver).forward(abi.encodeCall(MessageOrdering.push, (1))); + MessageOrdering(destinationReceiver).push(1); } function test_invalidSourceChainId() public { @@ -76,7 +76,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { function queueSourceToDestination(bytes memory message) internal override { AMBForwarder.sendMessageEthereumToGnosisChain( destinationReceiver, - abi.encodeCall(AMBReceiver.forward, (message)), + message, 100000 ); } @@ -84,7 +84,7 @@ contract GnosisIntegrationTest is IntegrationBaseTest { function queueDestinationToSource(bytes memory message) internal override { AMBForwarder.sendMessageGnosisChainToEthereum( sourceReceiver, - abi.encodeCall(AMBReceiver.forward, (message)), + message, 100000 ); } diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index a2c2f8c..8043136 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -36,7 +36,7 @@ contract OptimismIntegrationTest is IntegrationBaseTest { vm.prank(randomAddress); vm.expectRevert("OptimismReceiver/invalid-sender"); - OptimismReceiver(destinationReceiver).forward(abi.encodeCall(MessageOrdering.push, (1))); + MessageOrdering(destinationReceiver).push(1); } function test_optimism() public { @@ -63,7 +63,7 @@ contract OptimismIntegrationTest is IntegrationBaseTest { OptimismForwarder.sendMessageL1toL2( bridge.sourceCrossChainMessenger, destinationReceiver, - abi.encodeCall(OptimismReceiver.forward, (message)), + message, 100000 ); } From 842bf178dbdb040295163f1314d8d6a4931d5bf6 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sun, 9 Jun 2024 17:51:23 +0900 Subject: [PATCH 36/42] dont use absolute paths --- src/receivers/AMBReceiver.sol | 2 +- src/receivers/ArbitrumReceiver.sol | 2 +- src/receivers/CCTPReceiver.sol | 2 +- src/receivers/OptimismReceiver.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/receivers/AMBReceiver.sol b/src/receivers/AMBReceiver.sol index f9f3cb1..aa61762 100644 --- a/src/receivers/AMBReceiver.sol +++ b/src/receivers/AMBReceiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; -import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; +import { Address } from "openzeppelin-contracts/contracts/utils/Address.sol"; interface IArbitraryMessagingBridge { function messageSender() external view returns (address); diff --git a/src/receivers/ArbitrumReceiver.sol b/src/receivers/ArbitrumReceiver.sol index 1807551..82098ea 100644 --- a/src/receivers/ArbitrumReceiver.sol +++ b/src/receivers/ArbitrumReceiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; -import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; +import { Address } from "openzeppelin-contracts/contracts/utils/Address.sol"; /** * @title ArbitrumReceiver diff --git a/src/receivers/CCTPReceiver.sol b/src/receivers/CCTPReceiver.sol index 0ca5b7c..05114f5 100644 --- a/src/receivers/CCTPReceiver.sol +++ b/src/receivers/CCTPReceiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; -import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; +import { Address } from "openzeppelin-contracts/contracts/utils/Address.sol"; /** * @title CCTPReceiver diff --git a/src/receivers/OptimismReceiver.sol b/src/receivers/OptimismReceiver.sol index ec39e95..0d6ec44 100644 --- a/src/receivers/OptimismReceiver.sol +++ b/src/receivers/OptimismReceiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; -import { Address } from "lib/openzeppelin-contracts/contracts/utils/Address.sol"; +import { Address } from "openzeppelin-contracts/contracts/utils/Address.sol"; interface ICrossDomainOptimism { function xDomainMessageSender() external view returns (address); From 6be2397aa4f6e9465a763f609e25a9111ffff200 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Sun, 9 Jun 2024 17:54:49 +0900 Subject: [PATCH 37/42] fix tests to remove forward() --- test/AMBReceiver.t.sol | 10 +++++----- test/ArbitrumReceiver.t.sol | 8 ++++---- test/OptimismReceiver.t.sol | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/AMBReceiver.t.sol b/test/AMBReceiver.t.sol index b1b5986..689e218 100644 --- a/test/AMBReceiver.t.sol +++ b/test/AMBReceiver.t.sol @@ -67,7 +67,7 @@ contract AMBReceiverTest is Test { function test_forward_invalidSender() public { vm.prank(randomAddress); vm.expectRevert("AMBReceiver/invalid-sender"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_invalidSourceChainId() public { @@ -75,7 +75,7 @@ contract AMBReceiverTest is Test { vm.prank(address(amb)); vm.expectRevert("AMBReceiver/invalid-sourceChainId"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_invalidSourceAuthority() public { @@ -83,20 +83,20 @@ contract AMBReceiverTest is Test { vm.prank(address(amb)); vm.expectRevert("AMBReceiver/invalid-sourceAuthority"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_success() public { assertEq(target.data(), 0); vm.prank(address(amb)); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); assertEq(target.data(), 1); } function test_forward_revert() public { vm.prank(address(amb)); vm.expectRevert("error"); - receiver.forward(abi.encodeCall(TargetContractMock.revertFunc, ())); + TargetContractMock(address(receiver)).revertFunc(); } } diff --git a/test/ArbitrumReceiver.t.sol b/test/ArbitrumReceiver.t.sol index 02d57c6..c205420 100644 --- a/test/ArbitrumReceiver.t.sol +++ b/test/ArbitrumReceiver.t.sol @@ -42,26 +42,26 @@ contract ArbitrumReceiverTest is Test { function test_forward_invalidL1Authority() public { vm.prank(randomAddress); vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_invalidL1AuthoritySourceAuthorityNoOffset() public { vm.prank(sourceAuthority); vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_success() public { assertEq(target.data(), 0); vm.prank(sourceAuthorityWithOffset); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); assertEq(target.data(), 1); } function test_forward_revert() public { vm.prank(sourceAuthorityWithOffset); vm.expectRevert("error"); - receiver.forward(abi.encodeCall(TargetContractMock.revertFunc, ())); + TargetContractMock(address(receiver)).revertFunc(); } } diff --git a/test/OptimismReceiver.t.sol b/test/OptimismReceiver.t.sol index 0fdb387..85be4c7 100644 --- a/test/OptimismReceiver.t.sol +++ b/test/OptimismReceiver.t.sol @@ -57,7 +57,7 @@ contract OptimismReceiverTest is Test { function test_forward_invalidSender() public { vm.prank(randomAddress); vm.expectRevert("OptimismReceiver/invalid-sender"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_invalidL1Authority() public { @@ -65,20 +65,20 @@ contract OptimismReceiverTest is Test { vm.prank(address(l2CrossDomain)); vm.expectRevert("OptimismReceiver/invalid-l1Authority"); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); } function test_forward_success() public { assertEq(target.data(), 0); vm.prank(address(l2CrossDomain)); - receiver.forward(abi.encodeCall(TargetContractMock.someFunc, ())); + TargetContractMock(address(receiver)).someFunc(); assertEq(target.data(), 1); } function test_forward_revert() public { vm.prank(address(l2CrossDomain)); vm.expectRevert("error"); - receiver.forward(abi.encodeCall(TargetContractMock.revertFunc, ())); + TargetContractMock(address(receiver)).revertFunc(); } } From 177227ab8d61673268e15e003555aad9b7e2f49b Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 12 Jun 2024 01:05:24 +0900 Subject: [PATCH 38/42] move image up; more about the receiver; typo --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 880c691..28e67b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # xchain-helpers -This repository three tools for use with multi-chain development. Domains refer to blockchains which are connected by bridges. Domains may have multiple bridges connecting them, for example both the Optimism Native Bridge and Circle CCTP connect Ethereum and Optimism domains. +This repository has three tools for use with multi-chain development. Domains refer to blockchains which are connected by bridges. Domains may have multiple bridges connecting them, for example both the Optimism Native Bridge and Circle CCTP connect Ethereum and Optimism domains. ## Forwarders @@ -8,9 +8,11 @@ These libraries provide standardized syntax for sending a message to a bridge. ## Receivers +![xchain-helpers](.assets/xchain-helpers.png) + The most common pattern is to have an authorized contract forward a message to another "business logic" contract to abstract away bridge dependencies. Receivers are contracts which perform this generic translation - decoding the bridge-specific message and forwarding to another `target` contract. The `target` contract should have logic to restrict who can call it and permission this to one or more bridge receivers. -![xchain-helpers](.assets/xchain-helpers.png) +Most receivers implement a `fallback()` function which after validating that the call came from an authorized party on the other side of the bridge will forward the call to the `target` contract with the same function signature. This separation of concerns makes it easy for the receiver contract to focus on validating the bridge message, and the business logic `target` contract can validate the `msg.sender` comes from the receiver which validates the whole process. This ensures no chain-specific code is required for the business logic contract. ## E2E Testing Infrastructure From 611fe91838d361642ff2961c71aa592570a449ec Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 12 Jun 2024 01:07:17 +0900 Subject: [PATCH 39/42] formatting --- test/CircleCCTPIntegration.t.sol | 24 ++++++++++++------------ test/OptimismIntegration.t.sol | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/test/CircleCCTPIntegration.t.sol b/test/CircleCCTPIntegration.t.sol index 0f262de..5ddb79b 100644 --- a/test/CircleCCTPIntegration.t.sol +++ b/test/CircleCCTPIntegration.t.sol @@ -17,18 +17,6 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { // Use Optimism for failure tests as the code logic is the same - function test_invalidSourceAuthority() public { - destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; - initBaseContracts(getChain("optimism").createFork()); - - vm.startPrank(randomAddress); - queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); - vm.stopPrank(); - - vm.expectRevert("CCTPReceiver/invalid-sourceAuthority"); - relaySourceToDestination(); - } - function test_invalidSender() public { destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; initBaseContracts(getChain("optimism").createFork()); @@ -51,6 +39,18 @@ contract CircleCCTPIntegrationTest is IntegrationBaseTest { CCTPReceiver(destinationReceiver).handleReceiveMessage(1, bytes32(uint256(uint160(sourceAuthority))), abi.encodeCall(MessageOrdering.push, (1))); } + function test_invalidSourceAuthority() public { + destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_OPTIMISM; + initBaseContracts(getChain("optimism").createFork()); + + vm.startPrank(randomAddress); + queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (1))); + vm.stopPrank(); + + vm.expectRevert("CCTPReceiver/invalid-sourceAuthority"); + relaySourceToDestination(); + } + function test_avalanche() public { destinationDomainId = CCTPForwarder.DOMAIN_ID_CIRCLE_AVALANCHE; runCrossChainTests(getChain("avalanche").createFork()); diff --git a/test/OptimismIntegration.t.sol b/test/OptimismIntegration.t.sol index 8043136..41a045e 100644 --- a/test/OptimismIntegration.t.sol +++ b/test/OptimismIntegration.t.sol @@ -14,7 +14,17 @@ contract OptimismIntegrationTest is IntegrationBaseTest { event FailedRelayedMessage(bytes32); - // Use Arbitrum One for failure test as the code logic is the same + // Use Optimism mainnet for failure test as the code logic is the same + + function test_invalidSender() public { + initBaseContracts(getChain("optimism").createFork()); + + destination.selectFork(); + + vm.prank(randomAddress); + vm.expectRevert("OptimismReceiver/invalid-sender"); + MessageOrdering(destinationReceiver).push(1); + } function test_invalidSourceAuthority() public { initBaseContracts(getChain("optimism").createFork()); @@ -29,16 +39,6 @@ contract OptimismIntegrationTest is IntegrationBaseTest { assertEq(moDestination.length(), 0); } - function test_invalidSender() public { - initBaseContracts(getChain("optimism").createFork()); - - destination.selectFork(); - - vm.prank(randomAddress); - vm.expectRevert("OptimismReceiver/invalid-sender"); - MessageOrdering(destinationReceiver).push(1); - } - function test_optimism() public { runCrossChainTests(getChain("optimism").createFork()); } From f222fe2b430d08f8181971c72cad5bc983d0ed23 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 12 Jun 2024 01:07:48 +0900 Subject: [PATCH 40/42] rm old unused commented out line --- src/testing/bridges/ArbitrumBridgeTesting.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/testing/bridges/ArbitrumBridgeTesting.sol b/src/testing/bridges/ArbitrumBridgeTesting.sol index 073d40e..e721b51 100644 --- a/src/testing/bridges/ArbitrumBridgeTesting.sol +++ b/src/testing/bridges/ArbitrumBridgeTesting.sol @@ -156,7 +156,6 @@ library ArbitrumBridgeTesting { for (uint256 i = 0; i < logs.length; i++) { Vm.Log memory log = logs[i]; (, address target, bytes memory message) = abi.decode(log.data, (address, address, bytes)); - //l2ToL1Sender = sender; (bool success, bytes memory response) = InboxLike(bridge.sourceCrossChainMessenger).bridge().executeCall(target, 0, message); if (!success) { assembly { From 0c2893daddc98cb8c10bd50d287e6bbe7cbf2ea0 Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Wed, 12 Jun 2024 19:14:47 +0900 Subject: [PATCH 41/42] review fixes --- test/AMBReceiver.t.sol | 14 +++++++------- test/ArbitrumReceiver.t.sol | 12 ++++++------ test/CCTPReceiver.t.sol | 17 +++++++++-------- test/IntegrationBase.t.sol | 11 ++++++++++- test/OptimismReceiver.t.sol | 12 ++++++------ test/mocks/TargetContractMock.sol | 8 ++++---- 6 files changed, 42 insertions(+), 32 deletions(-) diff --git a/test/AMBReceiver.t.sol b/test/AMBReceiver.t.sol index 689e218..322ef6b 100644 --- a/test/AMBReceiver.t.sol +++ b/test/AMBReceiver.t.sol @@ -67,7 +67,7 @@ contract AMBReceiverTest is Test { function test_forward_invalidSender() public { vm.prank(randomAddress); vm.expectRevert("AMBReceiver/invalid-sender"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_invalidSourceChainId() public { @@ -75,7 +75,7 @@ contract AMBReceiverTest is Test { vm.prank(address(amb)); vm.expectRevert("AMBReceiver/invalid-sourceChainId"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_invalidSourceAuthority() public { @@ -83,19 +83,19 @@ contract AMBReceiverTest is Test { vm.prank(address(amb)); vm.expectRevert("AMBReceiver/invalid-sourceAuthority"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_success() public { - assertEq(target.data(), 0); + assertEq(target.count(), 0); vm.prank(address(amb)); - TargetContractMock(address(receiver)).someFunc(); - assertEq(target.data(), 1); + TargetContractMock(address(receiver)).increment(); + assertEq(target.count(), 1); } function test_forward_revert() public { vm.prank(address(amb)); - vm.expectRevert("error"); + vm.expectRevert("TargetContract/error"); TargetContractMock(address(receiver)).revertFunc(); } diff --git a/test/ArbitrumReceiver.t.sol b/test/ArbitrumReceiver.t.sol index c205420..7ae6bf3 100644 --- a/test/ArbitrumReceiver.t.sol +++ b/test/ArbitrumReceiver.t.sol @@ -42,25 +42,25 @@ contract ArbitrumReceiverTest is Test { function test_forward_invalidL1Authority() public { vm.prank(randomAddress); vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_invalidL1AuthoritySourceAuthorityNoOffset() public { vm.prank(sourceAuthority); vm.expectRevert("ArbitrumReceiver/invalid-l1Authority"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_success() public { - assertEq(target.data(), 0); + assertEq(target.count(), 0); vm.prank(sourceAuthorityWithOffset); - TargetContractMock(address(receiver)).someFunc(); - assertEq(target.data(), 1); + TargetContractMock(address(receiver)).increment(); + assertEq(target.count(), 1); } function test_forward_revert() public { vm.prank(sourceAuthorityWithOffset); - vm.expectRevert("error"); + vm.expectRevert("TargetContract/error"); TargetContractMock(address(receiver)).revertFunc(); } diff --git a/test/CCTPReceiver.t.sol b/test/CCTPReceiver.t.sol index 4f77c04..00b6c74 100644 --- a/test/CCTPReceiver.t.sol +++ b/test/CCTPReceiver.t.sol @@ -49,7 +49,7 @@ contract CCTPReceiverTest is Test { receiver.handleReceiveMessage( sourceDomainId, sourceAuthority, - abi.encodeCall(TargetContractMock.someFunc, ()) + abi.encodeCall(TargetContractMock.increment, ()) ); } @@ -59,7 +59,7 @@ contract CCTPReceiverTest is Test { receiver.handleReceiveMessage( 2, sourceAuthority, - abi.encodeCall(TargetContractMock.someFunc, ()) + abi.encodeCall(TargetContractMock.increment, ()) ); } @@ -69,24 +69,25 @@ contract CCTPReceiverTest is Test { receiver.handleReceiveMessage( sourceDomainId, bytes32(uint256(uint160(randomAddress))), - abi.encodeCall(TargetContractMock.someFunc, ()) + abi.encodeCall(TargetContractMock.increment, ()) ); } function test_handleReceiveMessage_success() public { - assertEq(target.data(), 0); + assertEq(target.count(), 0); vm.prank(destinationMessenger); - receiver.handleReceiveMessage( + bool result = receiver.handleReceiveMessage( sourceDomainId, sourceAuthority, - abi.encodeCall(TargetContractMock.someFunc, ()) + abi.encodeCall(TargetContractMock.increment, ()) ); - assertEq(target.data(), 1); + assertEq(result, true); + assertEq(target.count(), 1); } function test_handleReceiveMessage_revert() public { vm.prank(destinationMessenger); - vm.expectRevert("error"); + vm.expectRevert("TargetContract/error"); receiver.handleReceiveMessage( sourceDomainId, sourceAuthority, diff --git a/test/IntegrationBase.t.sol b/test/IntegrationBase.t.sol index aa16544..b81f644 100644 --- a/test/IntegrationBase.t.sol +++ b/test/IntegrationBase.t.sol @@ -106,7 +106,7 @@ abstract contract IntegrationBaseTest is Test { assertEq(moSource.messages(0), 3); assertEq(moSource.messages(1), 4); - // One more message to destination + // Do one more message both ways to ensure subsequent calls don't repeat already sent messages vm.startPrank(sourceAuthority); queueSourceToDestination(abi.encodeCall(MessageOrdering.push, (5))); vm.stopPrank(); @@ -115,6 +115,15 @@ abstract contract IntegrationBaseTest is Test { assertEq(moDestination.length(), 3); assertEq(moDestination.messages(2), 5); + + vm.startPrank(destinationAuthority); + queueDestinationToSource(abi.encodeCall(MessageOrdering.push, (6))); + vm.stopPrank(); + + relayDestinationToSource(); + + assertEq(moSource.length(), 3); + assertEq(moSource.messages(2), 6); } function initSourceReceiver() internal virtual returns (address); diff --git a/test/OptimismReceiver.t.sol b/test/OptimismReceiver.t.sol index 85be4c7..85d18a1 100644 --- a/test/OptimismReceiver.t.sol +++ b/test/OptimismReceiver.t.sol @@ -57,7 +57,7 @@ contract OptimismReceiverTest is Test { function test_forward_invalidSender() public { vm.prank(randomAddress); vm.expectRevert("OptimismReceiver/invalid-sender"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_invalidL1Authority() public { @@ -65,19 +65,19 @@ contract OptimismReceiverTest is Test { vm.prank(address(l2CrossDomain)); vm.expectRevert("OptimismReceiver/invalid-l1Authority"); - TargetContractMock(address(receiver)).someFunc(); + TargetContractMock(address(receiver)).increment(); } function test_forward_success() public { - assertEq(target.data(), 0); + assertEq(target.count(), 0); vm.prank(address(l2CrossDomain)); - TargetContractMock(address(receiver)).someFunc(); - assertEq(target.data(), 1); + TargetContractMock(address(receiver)).increment(); + assertEq(target.count(), 1); } function test_forward_revert() public { vm.prank(address(l2CrossDomain)); - vm.expectRevert("error"); + vm.expectRevert("TargetContract/error"); TargetContractMock(address(receiver)).revertFunc(); } diff --git a/test/mocks/TargetContractMock.sol b/test/mocks/TargetContractMock.sol index 77fddb0..47c3052 100644 --- a/test/mocks/TargetContractMock.sol +++ b/test/mocks/TargetContractMock.sol @@ -3,14 +3,14 @@ pragma solidity >=0.8.0; contract TargetContractMock { - uint256 public data; + uint256 public count; - function someFunc() external { - data++; + function increment() external { + count++; } function revertFunc() external pure { - revert("error"); + revert("TargetContract/error"); } } From fd8a5f825259c15f47a4ddb5e383c4aca2f1789e Mon Sep 17 00:00:00 2001 From: Sam MacPherson Date: Thu, 13 Jun 2024 00:36:18 +0900 Subject: [PATCH 42/42] align --- test/CCTPReceiver.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CCTPReceiver.t.sol b/test/CCTPReceiver.t.sol index 00b6c74..7ad684f 100644 --- a/test/CCTPReceiver.t.sol +++ b/test/CCTPReceiver.t.sol @@ -81,7 +81,7 @@ contract CCTPReceiverTest is Test { sourceAuthority, abi.encodeCall(TargetContractMock.increment, ()) ); - assertEq(result, true); + assertEq(result, true); assertEq(target.count(), 1); }