From a9ba33259fc5a86c323ce81b6bbd771f77d185ca Mon Sep 17 00:00:00 2001 From: Skeletor Spaceman Date: Wed, 11 Dec 2024 14:41:45 -0300 Subject: [PATCH 01/16] feat: adds return data to L2toL2 relay message --- .../contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index 2162638ddc43..c7ca4e1eba49 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -159,7 +159,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// currently being replayed. /// @param _id Identifier of the SentMessage event to be relayed /// @param _sentMessage Message payload of the `SentMessage` event - function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable nonReentrant { + /// @return returnData_ Return data from the target contract call. + function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable nonReentrant returns (bytes memory returnData_) { // Ensure the log came from the messenger. Since the log origin is the CDM, there isn't a scenario where // this can be invoked from the CrossL2Inbox as the SentMessage log is not calldata for this function if (_id.origin != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { @@ -194,7 +195,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { _storeMessageMetadata(source, sender); - bool success = SafeCall.call(target, msg.value, message); + bool success; + (success, returnData_) = target.call{value: msg.value}(message); if (!success) { revert TargetCallFailed(); From ec69727b12084799c4c594804958922e3b34d616 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:46:07 -0300 Subject: [PATCH 02/16] feat: add test for the return data * chore: update interface and run scripts --- .../L2/IL2ToL2CrossDomainMessenger.sol | 9 +++- .../abi/L2ToL2CrossDomainMessenger.json | 8 +++- .../snapshots/semver-lock.json | 4 +- .../src/L2/L2ToL2CrossDomainMessenger.sol | 17 +++++--- .../test/L2/L2ToL2CrossDomainMessenger.t.sol | 42 +++++++++++++++++++ 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol index 89311cf18f01..58f07a9c92d9 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol @@ -104,7 +104,14 @@ interface IL2ToL2CrossDomainMessenger { /// already received once and is currently being replayed. /// @param _id Identifier of the SentMessage event to be relayed /// @param _sentMessage Message payload of the `SentMessage` event - function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable; + /// @return returnData_ Return data from the target contract call. + function relayMessage( + Identifier calldata _id, + bytes calldata _sentMessage + ) + external + payable + returns (bytes memory returnData_); function messageVersion() external view returns (uint16); diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json index 9f51cc1c3a27..3d381ec9ae51 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json @@ -110,7 +110,13 @@ } ], "name": "relayMessage", - "outputs": [], + "outputs": [ + { + "internalType": "bytes", + "name": "returnData_", + "type": "bytes" + } + ], "stateMutability": "payable", "type": "function" }, diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 2878c4117c40..609f656d4802 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -100,8 +100,8 @@ "sourceCodeHash": "0xaef8ea36c5b78cd12e0e62811d51db627ccf0dfd2cc5479fb707a10ef0d42048" }, "src/L2/L2ToL2CrossDomainMessenger.sol": { - "initCodeHash": "0x45564b97c63419cc12eadc60425c6d001857a3eea688ecaf1439ae7ede6aa9aa", - "sourceCodeHash": "0xed64736338b43a42f6bc6a88cca734403e1bb9ceafa55e4738605dfdedd1a99f" + "initCodeHash": "0xc56db8cb569efa0467fd53ab3fa218af3051e54f5517d7fafb7b5831b4350618", + "sourceCodeHash": "0x72062343a044e9c56f4143dcfc71706286eb205902006c2afcf6a4cd90c3e9f8" }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0xdac32a1057a6bc8a8d2ffdce1db8f34950cd0ffd1454d2133865736d21869192", diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index c7ca4e1eba49..fc1f74e99c53 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.25; import { Encoding } from "src/libraries/Encoding.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { SafeCall } from "src/libraries/SafeCall.sol"; import { TransientReentrancyAware } from "src/libraries/TransientContext.sol"; // Interfaces @@ -72,8 +71,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { uint16 public constant messageVersion = uint16(0); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.13 - string public constant version = "1.0.0-beta.13"; + /// @custom:semver 1.0.0-beta.14 + string public constant version = "1.0.0-beta.14"; /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only be present in this /// mapping if it has successfully been relayed on this chain, and can therefore not be relayed again. @@ -160,7 +159,15 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @param _id Identifier of the SentMessage event to be relayed /// @param _sentMessage Message payload of the `SentMessage` event /// @return returnData_ Return data from the target contract call. - function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable nonReentrant returns (bytes memory returnData_) { + function relayMessage( + Identifier calldata _id, + bytes calldata _sentMessage + ) + external + payable + nonReentrant + returns (bytes memory returnData_) + { // Ensure the log came from the messenger. Since the log origin is the CDM, there isn't a scenario where // this can be invoked from the CrossL2Inbox as the SentMessage log is not calldata for this function if (_id.origin != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { @@ -196,7 +203,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { _storeMessageMetadata(source, sender); bool success; - (success, returnData_) = target.call{value: msg.value}(message); + (success, returnData_) = target.call{ value: msg.value }(message); if (!success) { revert TargetCallFailed(); diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 0c064f686055..7c6a57ea586b 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -393,6 +393,48 @@ contract L2ToL2CrossDomainMessengerTest is Test { assertEq(l2ToL2CrossDomainMessenger.crossDomainMessageSender(), address(0)); } + /// @dev Tests the `relayMessage` function returns the expected return data of the call to the target contract. + function testFuzz_relayMessage_returnData_succeeds( + uint256 _source, + uint256 _nonce, + address _sender, + uint256 _value, + uint256 _blockNum, + uint256 _logIndex, + uint256 _time, + address _target, + bytes memory _mockedReturnData + ) + public + { + // Declare a random call to be made over the target + bytes memory message = abi.encodeWithSignature("randomCall()"); + + // Construct the message + Identifier memory id = + Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); + bytes memory sentMessage = abi.encodePacked( + abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics + abi.encode(_sender, message) // data + ); + + // Ensure the CrossL2Inbox validates this message + vm.mockCall({ + callee: Predeploys.CROSS_L2_INBOX, + data: abi.encodeCall(ICrossL2Inbox.validateMessage, (id, keccak256(message))), + returnData: "" + }); + + // Mock the random call over the target with the expected return data + vm.mockCall({ callee: _target, data: message, returnData: _mockedReturnData }); + + hoax(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _value); + bytes memory returnData = l2ToL2CrossDomainMessenger.relayMessage{ value: _value }(id, sentMessage); + + // Check that the return data is the mocked one + assertEq(returnData, _mockedReturnData); + } + /// @dev Mock reentrant function that calls the `relayMessage` function. /// @param _source Source chain ID of the message. /// @param _nonce Nonce of the message. From 49ce0de2280abea8a683b86d95e6f9ea9865e162 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:22:24 -0300 Subject: [PATCH 03/16] chore: polish test natspec --- .../contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 7c6a57ea586b..367c290931b9 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -393,7 +393,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { assertEq(l2ToL2CrossDomainMessenger.crossDomainMessageSender(), address(0)); } - /// @dev Tests the `relayMessage` function returns the expected return data of the call to the target contract. + /// @dev Tests the `relayMessage` function returns the expected return data from the call to the target contract. function testFuzz_relayMessage_returnData_succeeds( uint256 _source, uint256 _nonce, From cdd1c9aef7fefc3d3a49ac39953230c3efbd0f76 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:46:49 -0300 Subject: [PATCH 04/16] feat: add first callback iteration --- .../contracts-bedrock/src/L2/Callbacks.sol | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 packages/contracts-bedrock/src/L2/Callbacks.sol diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol new file mode 100644 index 000000000000..e53ad97987b6 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; +import { IGreeter } from "interfaces/IGreeter.sol"; +import { Identifier, IL2ToL2CrossDomainMessenger } from "interfaces/IL2ToL2CrossDomainMessenger.sol"; + +struct CallbackContext { + address to; + bytes4 selector; +} + +contract Greeter is IGreeter { + /** + * @notice Empty string for revert checks + * @dev result of doing keccak256(bytes('')) + */ + bytes32 internal constant _EMPTY_STRING = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + + address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = address(0); + address public constant CALLBACK_ENTRYPOINT = address(0); + + /// @inheritdoc IGreeter + address public immutable OWNER; + + /// @inheritdoc IGreeter + string public greeting; + + /// @inheritdoc IGreeter + IERC20 public token; + + // 4 bytes are reserved for the callback selector + uint224 public returnContextNonce; + // Holds the return context for the async remoteGreet call + mapping(uint224 => CallbackContext) public callbackContexts; + + /** + * @notice Reverts in case the function was not called by the owner of the contract + */ + modifier onlyOwner() { + if (msg.sender != OWNER) { + revert Greeter_OnlyOwner(); + } + _; + } + + /** + * @notice Defines the owner to the msg.sender and sets the initial greeting + * @param _greeting Initial greeting + * @param _token Initial token + */ + constructor(string memory _greeting, IERC20 _token) { + OWNER = msg.sender; + token = _token; + setGreeting(_greeting); + } + + /// @inheritdoc IGreeter + function greet() external view returns (string memory _greeting, uint256 _balance) { + _greeting = greeting; + _balance = token.balanceOf(msg.sender); + } + + /// @inheritdoc IGreeter + function setGreeting(string memory _greeting) public onlyOwner { + if (keccak256(bytes(_greeting)) == _EMPTY_STRING) { + revert Greeter_InvalidGreeting(); + } + + greeting = _greeting; + emit GreetingSet(_greeting); + } + + /** + * @notice Initiates a remote greeting call handled by the callback + * @return contextNonce_ + */ + function remoteGreet(CallbackContext calldata _callbackContext) external returns (uint224 contextNonce_) { + contextNonce_ = async( + 1, abi.encodeWithSelector(this.greeting.selector), this.remoteGreetCallback.selector, _callbackContext + ); + } + + function remoteGreetCallback( + uint224 _contextNonce, + string memory _remoteGreeting + ) + external + view + returns (string memory _greeting, uint256 _balance) + { + _greeting = _remoteGreeting; + _balance = token.balanceOf(msg.sender); + + // obtain the return context + CallbackContext memory _callbackContext = callbackContexts[_contextNonce]; + // delete the return context + delete callbackContexts[_contextNonce]; + + // call the callback (not sure if we should check for success...) + _callbackContext.to.call(abi.encodeWithSelector(_callbackContext.selector, _greeting, _balance)); + } + + // builds the cdm call with the callback entrypoint + function async( + uint256 _chainid, + bytes memory _data, + bytes4 _callbackSelector, + CallbackContext calldata _callbackContext + ) + internal + returns (uint224 contextNonce_) + { + // increment the nonce + contextNonce_ = returnContextNonce++; + // store the return context + callbackContexts[nonce] = _callbackContext; + + _messageHash = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + _chainid, + address(this), + // data + callback selector + abi.encodePacked(_data, _callbackSelector, contextNonce_), + //abi.encodePacked(_data, _callbackSelector, contextNonce_, 32), // 32 bytes can be dynamic [IMPROVEMENT] + CALLBACK_ENTRYPOINT + ); + } +} + +contract CallbackEntrypoint { + address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = address(0); + + function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable { + // Calls the CDM contract and get return value of the function call + bytes _data = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); + + // get the last 32 bytes of _sentMessage (4 bytes for the callback selector and 28 bytes for the contextNonce) + bytes32 _callbackSelectorAndParams = abi.decode(_sentMessage[_sentMessage.length - 32:], (bytes32)); + // Creates new CDM message to _sentMessage origin, sender and _callbackSelector with the return value + IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + _id.chainId, _id.origin, abi.encodePacked(_callbackSelectorAndParams, _data) + ); + } +} From 4736fd2d47e2dd9b59e4ec79ed9f558165d10c45 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:24:24 -0300 Subject: [PATCH 05/16] feat: wip --- .../contracts-bedrock/src/L2/Callbacks.sol | 69 ++++++--- .../src/L2/L2ToL2CrossDomainMessenger.sol | 2 + .../contracts-bedrock/test/L2/Callbacks.t.sol | 136 ++++++++++++++++++ 3 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 packages/contracts-bedrock/test/L2/Callbacks.t.sol diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index e53ad97987b6..d70a1eda4518 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -2,33 +2,32 @@ pragma solidity 0.8.25; import { IERC20 } from "forge-std/interfaces/IERC20.sol"; -import { IGreeter } from "interfaces/IGreeter.sol"; -import { Identifier, IL2ToL2CrossDomainMessenger } from "interfaces/IL2ToL2CrossDomainMessenger.sol"; +import { Identifier, IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; struct CallbackContext { address to; bytes4 selector; } -contract Greeter is IGreeter { +contract Greeter { /** * @notice Empty string for revert checks * @dev result of doing keccak256(bytes('')) */ bytes32 internal constant _EMPTY_STRING = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; - address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = address(0); - address public constant CALLBACK_ENTRYPOINT = address(0); + address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + address public constant CALLBACK_ENTRYPOINT = address(0); // TODO: add setter - /// @inheritdoc IGreeter address public immutable OWNER; - /// @inheritdoc IGreeter string public greeting; - /// @inheritdoc IGreeter IERC20 public token; + address internal _remoteGreeter; + // 4 bytes are reserved for the callback selector uint224 public returnContextNonce; // Holds the return context for the async remoteGreet call @@ -39,7 +38,7 @@ contract Greeter is IGreeter { */ modifier onlyOwner() { if (msg.sender != OWNER) { - revert Greeter_OnlyOwner(); + revert("only owner"); } _; } @@ -55,20 +54,18 @@ contract Greeter is IGreeter { setGreeting(_greeting); } - /// @inheritdoc IGreeter function greet() external view returns (string memory _greeting, uint256 _balance) { _greeting = greeting; _balance = token.balanceOf(msg.sender); } - /// @inheritdoc IGreeter function setGreeting(string memory _greeting) public onlyOwner { if (keccak256(bytes(_greeting)) == _EMPTY_STRING) { - revert Greeter_InvalidGreeting(); + revert("Greeter_InvalidGreeting"); } greeting = _greeting; - emit GreetingSet(_greeting); + // emit GreetingSet(_greeting); } /** @@ -77,7 +74,10 @@ contract Greeter is IGreeter { */ function remoteGreet(CallbackContext calldata _callbackContext) external returns (uint224 contextNonce_) { contextNonce_ = async( - 1, abi.encodeWithSelector(this.greeting.selector), this.remoteGreetCallback.selector, _callbackContext + 2, // TODO: receive param + abi.encodeWithSelector(this.greeting.selector), + this.remoteGreetCallback.selector, + _callbackContext ); } @@ -86,7 +86,6 @@ contract Greeter is IGreeter { string memory _remoteGreeting ) external - view returns (string memory _greeting, uint256 _balance) { _greeting = _remoteGreeting; @@ -99,6 +98,8 @@ contract Greeter is IGreeter { // call the callback (not sure if we should check for success...) _callbackContext.to.call(abi.encodeWithSelector(_callbackContext.selector, _greeting, _balance)); + + // TODO: revert if failed } // builds the cdm call with the callback entrypoint @@ -112,33 +113,59 @@ contract Greeter is IGreeter { returns (uint224 contextNonce_) { // increment the nonce - contextNonce_ = returnContextNonce++; + contextNonce_ = ++returnContextNonce; // store the return context - callbackContexts[nonce] = _callbackContext; + callbackContexts[contextNonce_] = _callbackContext; - _messageHash = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( _chainid, - address(this), + _remoteGreeter, // data + callback selector abi.encodePacked(_data, _callbackSelector, contextNonce_), //abi.encodePacked(_data, _callbackSelector, contextNonce_, 32), // 32 bytes can be dynamic [IMPROVEMENT] CALLBACK_ENTRYPOINT ); } + + // function promise(bytes memory _promiseData, bytes memory _callbackData); + + // Setter for the remote greeter WIP without auth for now. + function setRemoteGreeter(address __remoteGreeter) public { + _remoteGreeter = __remoteGreeter; + } } contract CallbackEntrypoint { - address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = address(0); + address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable { // Calls the CDM contract and get return value of the function call - bytes _data = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); + bytes memory _data = + IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); // get the last 32 bytes of _sentMessage (4 bytes for the callback selector and 28 bytes for the contextNonce) bytes32 _callbackSelectorAndParams = abi.decode(_sentMessage[_sentMessage.length - 32:], (bytes32)); + + // + (sender, message, entrypoint_) = + abi.decode(_sentMessage[_sentMessage.length - 128:_sentMessage.length - 32], (address, bytes, address)); + + console.log("origin %s", _id.origin); + // Creates new CDM message to _sentMessage origin, sender and _callbackSelector with the return value IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( _id.chainId, _id.origin, abi.encodePacked(_callbackSelectorAndParams, _data) ); } } + +//// + +// 1. Chain A: remoteGreet -> async -> sendMessage + +// 2. Chain B: entrypoint.relayMessage -> cdm.relayMessage -> greeting + remoteGreetCallback(nonce, ) -> +// sendMessage(chainA, greeterA, remoteGreetCallback) + +// 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call + +import "forge-std/Test.sol"; diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index d25ae64434cd..531362a46786 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -12,6 +12,8 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; import { ICrossL2Inbox, Identifier } from "interfaces/L2/ICrossL2Inbox.sol"; +import "forge-std/Test.sol"; + /// @notice Thrown when a non-written slot in transient storage is attempted to be read from. error NotEntered(); diff --git a/packages/contracts-bedrock/test/L2/Callbacks.t.sol b/packages/contracts-bedrock/test/L2/Callbacks.t.sol new file mode 100644 index 000000000000..0cb54c9d64e5 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/Callbacks.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; + +import { + CallbackEntrypoint, + Greeter, + CallbackContext, + IERC20, + Identifier, + IL2ToL2CrossDomainMessenger +} from "src/L2/Callbacks.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; +import { L2ToL2CrossDomainMessenger } from "src/L2/L2ToL2CrossDomainMessenger.sol"; +import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; + +import "forge-std/Test.sol"; + +contract Counter { + function two() public pure returns (uint256) { + return 2; + } +} + +contract CallbackTest is Test { + CallbackEntrypoint public entrypointA; + CallbackEntrypoint public entrypointB; + + Greeter public greeterA; + Greeter public greeterB; + + string public greeting = "Hello, World!"; + + Counter public counter; + + uint64 chainIdA = 1; + uint64 chainIdB = 2; + + address public user; + + function setUp() public { + // Deploy the L2ToL2CrossDomainMessenger contract + vm.etch(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, address(new L2ToL2CrossDomainMessenger()).code); + + counter = new Counter(); + + entrypointA = new CallbackEntrypoint(); + entrypointB = new CallbackEntrypoint(); + + greeterA = new Greeter(greeting, IERC20(address(0))); + greeterB = new Greeter(greeting, IERC20(address(0))); + + greeterA.setRemoteGreeter(address(greeterB)); + greeterB.setRemoteGreeter(address(greeterA)); + } + + /// @notice Helper function to setup a mock and expect a call to it. + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function testCallback() public { + /* 1. Chain A: remoteGreet -> async -> sendMessage */ + // Chain ID 1 == A + vm.chainId(chainIdA); + + // Mock the call over the `isInDependencySet` function to return true + vm.mockCall( + Predeploys.L1_BLOCK_ATTRIBUTES, + abi.encodeWithSelector(IDependencySet.isInDependencySet.selector), + abi.encode(true) + ); + + bytes4 _finalTargetCall = Counter.two.selector; + greeterA.remoteGreet(CallbackContext({ to: address(counter), selector: _finalTargetCall })); + + /* 2. Chain B: entrypoint.relayMessage -> cdm.relayMessage -> greeting + remoteGreetCallback(nonce, ) -> + sendMessage(chainA, greeterA, remoteGreetCallback) */ + // Chain ID 2 == B + vm.chainId(chainIdB); + + // Construct the SentMessage payload & identifier + Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, 1); + + uint256 _cdmNonce = 1; + uint224 _greeterNonce = 1; + bytes memory _data = abi.encodeWithSignature("greeting()"); + bytes memory _message = abi.encodePacked(_data, Greeter.remoteGreetCallback.selector, _greeterNonce); + + bytes memory sentMessage = abi.encodePacked( + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainIdB, address(greeterB), _cdmNonce), // topics + abi.encode(address(greeterA), _message, address(entrypointB)) // data + ); + + // Mock the call over the `isInDependencySet` function to return true + vm.mockCall( + Predeploys.L1_BLOCK_ATTRIBUTES, + abi.encodeWithSelector(IDependencySet.isInDependencySet.selector), + abi.encode(true) + ); + + // Ensure the CrossL2Inbox validates this message + _mockAndExpect( + Predeploys.CROSS_L2_INBOX, + abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), // , abi.encode(id, + // keccak256(sentMessage))), + "" + ); + + console.log("heere1"); + + entrypointB.relayMessage(id, sentMessage); + + console.log("heere"); + + /* 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call */ + // Chain ID 1 == A + vm.chainId(chainIdA); + + // Ensure the CrossL2Inbox validates this message + _mockAndExpect( + Predeploys.CROSS_L2_INBOX, + abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), // , abi.encode(id, + // keccak256(sentMessage))), + "" + ); + + sentMessage = abi.encodePacked( + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainIdA, address(greeterA), _cdmNonce), // topics + abi.encode(address(entrypointB), _finalTargetCall, address(greeterA)) // data + ); + } +} From 0d63fc16aecaa75471da3bf9f9fb151eb11549da Mon Sep 17 00:00:00 2001 From: Flux Date: Fri, 13 Dec 2024 18:34:05 -0500 Subject: [PATCH 06/16] feat: wip finish poc --- .../contracts-bedrock/src/L2/Callbacks.sol | 44 ++++++-------- .../contracts-bedrock/test/L2/Callbacks.t.sol | 58 +++++++++---------- 2 files changed, 43 insertions(+), 59 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index d70a1eda4518..f29a6faed169 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import { IERC20 } from "forge-std/interfaces/IERC20.sol"; import { Identifier, IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; @@ -24,8 +23,6 @@ contract Greeter { string public greeting; - IERC20 public token; - address internal _remoteGreeter; // 4 bytes are reserved for the callback selector @@ -46,17 +43,15 @@ contract Greeter { /** * @notice Defines the owner to the msg.sender and sets the initial greeting * @param _greeting Initial greeting - * @param _token Initial token */ - constructor(string memory _greeting, IERC20 _token) { + constructor(string memory _greeting) { OWNER = msg.sender; - token = _token; setGreeting(_greeting); } function greet() external view returns (string memory _greeting, uint256 _balance) { _greeting = greeting; - _balance = token.balanceOf(msg.sender); + _balance = address(msg.sender).balance; } function setGreeting(string memory _greeting) public onlyOwner { @@ -89,7 +84,7 @@ contract Greeter { returns (string memory _greeting, uint256 _balance) { _greeting = _remoteGreeting; - _balance = token.balanceOf(msg.sender); + _balance = address(msg.sender).balance; // obtain the return context CallbackContext memory _callbackContext = callbackContexts[_contextNonce]; @@ -98,8 +93,6 @@ contract Greeter { // call the callback (not sure if we should check for success...) _callbackContext.to.call(abi.encodeWithSelector(_callbackContext.selector, _greeting, _balance)); - - // TODO: revert if failed } // builds the cdm call with the callback entrypoint @@ -127,8 +120,6 @@ contract Greeter { ); } - // function promise(bytes memory _promiseData, bytes memory _callbackData); - // Setter for the remote greeter WIP without auth for now. function setRemoteGreeter(address __remoteGreeter) public { _remoteGreeter = __remoteGreeter; @@ -138,34 +129,33 @@ contract Greeter { contract CallbackEntrypoint { address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - function relayMessage(Identifier calldata _id, bytes calldata _sentMessage) external payable { + function relayMessage( + Identifier calldata _id, + bytes calldata _sentMessage + ) + external + payable + returns (bytes memory returnData_) + { // Calls the CDM contract and get return value of the function call - bytes memory _data = - IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); + returnData_ = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); - // get the last 32 bytes of _sentMessage (4 bytes for the callback selector and 28 bytes for the contextNonce) - bytes32 _callbackSelectorAndParams = abi.decode(_sentMessage[_sentMessage.length - 32:], (bytes32)); - - // - (sender, message, entrypoint_) = - abi.decode(_sentMessage[_sentMessage.length - 128:_sentMessage.length - 32], (address, bytes, address)); + (address sender,,) = abi.decode(_sentMessage[128:], (address, bytes, address)); - console.log("origin %s", _id.origin); + // get the last 32 bytes of _sentMessage (4 bytes for the callback selector and 28 bytes for the contextNonce) + bytes32 _callbackSelectorAndParams = + abi.decode(_sentMessage[_sentMessage.length - 64:_sentMessage.length - 32], (bytes32)); // Creates new CDM message to _sentMessage origin, sender and _callbackSelector with the return value IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( - _id.chainId, _id.origin, abi.encodePacked(_callbackSelectorAndParams, _data) + _id.chainId, sender, abi.encodePacked(_callbackSelectorAndParams, returnData_) ); } } -//// - // 1. Chain A: remoteGreet -> async -> sendMessage // 2. Chain B: entrypoint.relayMessage -> cdm.relayMessage -> greeting + remoteGreetCallback(nonce, ) -> // sendMessage(chainA, greeterA, remoteGreetCallback) // 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call - -import "forge-std/Test.sol"; diff --git a/packages/contracts-bedrock/test/L2/Callbacks.t.sol b/packages/contracts-bedrock/test/L2/Callbacks.t.sol index 0cb54c9d64e5..5e90976f0d4a 100644 --- a/packages/contracts-bedrock/test/L2/Callbacks.t.sol +++ b/packages/contracts-bedrock/test/L2/Callbacks.t.sol @@ -4,12 +4,7 @@ pragma solidity 0.8.25; import { Test } from "forge-std/Test.sol"; import { - CallbackEntrypoint, - Greeter, - CallbackContext, - IERC20, - Identifier, - IL2ToL2CrossDomainMessenger + CallbackEntrypoint, Greeter, CallbackContext, Identifier, IL2ToL2CrossDomainMessenger } from "src/L2/Callbacks.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; @@ -18,9 +13,11 @@ import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; import "forge-std/Test.sol"; -contract Counter { - function two() public pure returns (uint256) { - return 2; +/// @notice This is the contract that will be called by the callback. +/// Greeter expects to have the the whatever function we choose to have a specific signature. +contract Receiver { + function receiveGreetings(string memory, uint256 _balance) external returns (bool) { + return _balance != 0; } } @@ -31,9 +28,10 @@ contract CallbackTest is Test { Greeter public greeterA; Greeter public greeterB; - string public greeting = "Hello, World!"; + string public greetingA = "Hello, World from Chain A!"; + string public greetingB = "Hello, World from Chain B!"; - Counter public counter; + Receiver public receiver; uint64 chainIdA = 1; uint64 chainIdB = 2; @@ -44,13 +42,13 @@ contract CallbackTest is Test { // Deploy the L2ToL2CrossDomainMessenger contract vm.etch(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, address(new L2ToL2CrossDomainMessenger()).code); - counter = new Counter(); + receiver = new Receiver(); entrypointA = new CallbackEntrypoint(); entrypointB = new CallbackEntrypoint(); - greeterA = new Greeter(greeting, IERC20(address(0))); - greeterB = new Greeter(greeting, IERC20(address(0))); + greeterA = new Greeter(greetingA); + greeterB = new Greeter(greetingB); greeterA.setRemoteGreeter(address(greeterB)); greeterB.setRemoteGreeter(address(greeterA)); @@ -74,8 +72,9 @@ contract CallbackTest is Test { abi.encode(true) ); - bytes4 _finalTargetCall = Counter.two.selector; - greeterA.remoteGreet(CallbackContext({ to: address(counter), selector: _finalTargetCall })); + bytes4 _finalTargetCall = Receiver.receiveGreetings.selector; + + greeterA.remoteGreet(CallbackContext({ to: address(receiver), selector: _finalTargetCall })); /* 2. Chain B: entrypoint.relayMessage -> cdm.relayMessage -> greeting + remoteGreetCallback(nonce, ) -> sendMessage(chainA, greeterA, remoteGreetCallback) */ @@ -87,6 +86,7 @@ contract CallbackTest is Test { uint256 _cdmNonce = 1; uint224 _greeterNonce = 1; + bytes memory _data = abi.encodeWithSignature("greeting()"); bytes memory _message = abi.encodePacked(_data, Greeter.remoteGreetCallback.selector, _greeterNonce); @@ -103,34 +103,28 @@ contract CallbackTest is Test { ); // Ensure the CrossL2Inbox validates this message - _mockAndExpect( - Predeploys.CROSS_L2_INBOX, - abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), // , abi.encode(id, - // keccak256(sentMessage))), - "" - ); - - console.log("heere1"); - - entrypointB.relayMessage(id, sentMessage); + _mockAndExpect(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); - console.log("heere"); + bytes memory _returnData = entrypointB.relayMessage(id, sentMessage); /* 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call */ // Chain ID 1 == A vm.chainId(chainIdA); + _message = abi.encodeCall(Greeter.remoteGreetCallback, (1, string(_returnData))); + + sentMessage = abi.encodePacked( + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainIdA, address(greeterA), _cdmNonce), // topics + abi.encode(address(entrypointB), _message, address(0x0)) // data + ); + // Ensure the CrossL2Inbox validates this message _mockAndExpect( Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), // , abi.encode(id, - // keccak256(sentMessage))), "" ); - sentMessage = abi.encodePacked( - abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainIdA, address(greeterA), _cdmNonce), // topics - abi.encode(address(entrypointB), _finalTargetCall, address(greeterA)) // data - ); + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(id, sentMessage); } } From 820f48ac92a69893897e702b5a779a5dd77df04d Mon Sep 17 00:00:00 2001 From: Flux Date: Fri, 13 Dec 2024 19:01:50 -0500 Subject: [PATCH 07/16] docs: add context comments --- packages/contracts-bedrock/src/L2/Callbacks.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index f29a6faed169..dd34cb2f6113 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -140,9 +140,13 @@ contract CallbackEntrypoint { // Calls the CDM contract and get return value of the function call returnData_ = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); + // 0 to 31 bytes: SentMessage selector + // 32 to 127 bytes: destination (uint256), target (address), nonce (uint256) + // 128 to end: sender (address), actual message (bytes), entrypoint (address) (address sender,,) = abi.decode(_sentMessage[128:], (address, bytes, address)); - // get the last 32 bytes of _sentMessage (4 bytes for the callback selector and 28 bytes for the contextNonce) + // the callback selector and params are in the last 32 bytes of the actual message, we need to take into + // account the 32 bytes of the entrypoint. bytes32 _callbackSelectorAndParams = abi.decode(_sentMessage[_sentMessage.length - 64:_sentMessage.length - 32], (bytes32)); From 03edaef0b3941812ed7657c552c3ca336fd5aafd Mon Sep 17 00:00:00 2001 From: Flux Date: Fri, 13 Dec 2024 19:15:51 -0500 Subject: [PATCH 08/16] refactor: retrieve only sender address in CallbackEntrypoint --- packages/contracts-bedrock/src/L2/Callbacks.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index dd34cb2f6113..28b32caaecc3 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -143,7 +143,7 @@ contract CallbackEntrypoint { // 0 to 31 bytes: SentMessage selector // 32 to 127 bytes: destination (uint256), target (address), nonce (uint256) // 128 to end: sender (address), actual message (bytes), entrypoint (address) - (address sender,,) = abi.decode(_sentMessage[128:], (address, bytes, address)); + (address sender) = abi.decode(_sentMessage[128:160], (address)); // the callback selector and params are in the last 32 bytes of the actual message, we need to take into // account the 32 bytes of the entrypoint. From 12aa0491de76ba5c6b413c5003247614100a29c7 Mon Sep 17 00:00:00 2001 From: Flux Date: Mon, 16 Dec 2024 12:46:13 -0500 Subject: [PATCH 09/16] refactor: change params order such that dynamic types are at the end --- .../L2/IL2ToL2CrossDomainMessenger.sol | 12 +++---- .../src/L2/L2ToL2CrossDomainMessenger.sol | 36 +++++++++---------- .../test/L2/L2ToL2CrossDomainMessenger.t.sol | 30 ++++++++-------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol index b35162767cd9..8f7624e51d61 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol @@ -53,15 +53,15 @@ interface IL2ToL2CrossDomainMessenger { /// @param target Target contract or wallet address. /// @param messageNonce Nonce associated with the message sent /// @param sender Address initiating this message call - /// @param message Message payload to call target with. /// @param entrypoint Address of the entrypoint on the destination chain. + /// @param message Message payload to call target with. event SentMessage( uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, - bytes message, - address entrypoint + address entrypoint, + bytes message ); /// @notice Emitted whenever a message is successfully relayed on this chain. @@ -104,15 +104,15 @@ interface IL2ToL2CrossDomainMessenger { /// `relayMessage` for successful relaying. /// @param _destination Chain ID of the destination chain. /// @param _target Target contract or wallet address. - /// @param _message Message payload to call target with. /// @param _entrypoint Address of the entrypoint on the destination chain. If it is address(0) then the /// message can be relayed by any address on the destination chain. + /// @param _message Message payload to call target with. /// @return The hash of the message being sent, used to track whether the message has successfully been relayed. function sendMessage( uint256 _destination, address _target, - bytes calldata _message, - address _entrypoint + address _entrypoint, + bytes calldata _message ) external returns (bytes32); diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index 1e81973bfbdf..61361476faea 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -69,7 +69,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @notice Event selector for the SentMessage event. Will be removed in favor of reading // the `selector` property directly once crytic/slither/#2566 is fixed. bytes32 internal constant SENT_MESSAGE_EVENT_SELECTOR = - 0xc3406924bd47cc346b038460ab86a034281a1fa54246e7996ce88ad205efcf4f; + 0xb6b27857168ee0136e68e746bb12d3abcd605fd8a719100d88901127632100e3; /// @notice Current message version identifier. uint16 public constant messageVersion = uint16(0); @@ -92,15 +92,15 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @param target Target contract or wallet address. /// @param messageNonce Nonce associated with the message sent /// @param sender Address initiating this message call - /// @param message Message payload to call target with. /// @param entrypoint Address of the entrypoint on the destination chain. + /// @param message Message payload to call target with. event SentMessage( uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, - bytes message, - address entrypoint + address entrypoint, + bytes message ); /// @notice Emitted whenever a message is successfully relayed on this chain. @@ -142,20 +142,20 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// `relayMessage` for successful relaying. /// @param _destination Chain ID of the destination chain. /// @param _target Target contract or wallet address. - /// @param _message Message payload to call target with. /// @param _entrypoint Address of the entrypoint on the destination chain. If it is address(0) then the /// message can be relayed by any address on the destination chain. + /// @param _message Message payload to call target with. /// @return The hash of the message being sent, used to track whether the message has successfully been relayed. function sendMessage( uint256 _destination, address _target, - bytes calldata _message, - address _entrypoint + address _entrypoint, + bytes calldata _message ) external returns (bytes32) { - return _sendMessage(_destination, _target, _message, _entrypoint); + return _sendMessage(_destination, _target, _entrypoint, _message); } /// @notice Sends a message to some target address on a destination chain. Note that if the call always reverts, @@ -167,7 +167,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @return The hash of the message being sent, used to track whether the message has successfully been relayed. function sendMessage(uint256 _destination, address _target, bytes calldata _message) external returns (bytes32) { // If entrypoint is not specified, then anyone can relay the message - return _sendMessage(_destination, _target, _message, address(0)); + return _sendMessage(_destination, _target, address(0), _message); } /// @notice Relays a message that was sent by the other L2ToL2CrossDomainMessenger contract. Can only be executed @@ -186,7 +186,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_id, keccak256(_sentMessage)); // Decode the payload - (uint256 destination, address target, uint256 nonce, address sender, bytes memory message, address entrypoint) = + (uint256 destination, address target, uint256 nonce, address sender, address entrypoint, bytes memory message) = _decodeSentMessagePayload(_sentMessage); // Assert invariants on the message @@ -246,8 +246,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @return target_ Target contract or wallet address. /// @return nonce_ Nonce associated with the message sent. /// @return sender_ Address initiating this message call. - /// @return message_ Message payload to call target with. /// @return entrypoint_ Address of the entrypoint on the destination chain. + /// @return message_ Message payload to call target with. function _decodeSentMessagePayload(bytes calldata _payload) internal pure @@ -256,8 +256,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { address target_, uint256 nonce_, address sender_, - bytes memory message_, - address entrypoint_ + address entrypoint_, + bytes memory message_ ) { // Validate Selector (also reverts if LOG0 with no topics) @@ -268,7 +268,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { (destination_, target_, nonce_) = abi.decode(_payload[32:128], (uint256, address, uint256)); // Data - (sender_, message_, entrypoint_) = abi.decode(_payload[128:], (address, bytes, address)); + (sender_, entrypoint_, message_) = abi.decode(_payload[128:], (address, address, bytes)); } /// @notice Sends a message to a target address on a destination chain. @@ -277,15 +277,15 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// and increments the message nonce. /// @param _destination Chain ID of the destination chain. /// @param _target Target contract or wallet address. - /// @param _message Message payload to call target with. /// @param _entrypoint Address of the entrypoint on the destination chain or address(0) if there is none -- meaning /// that anyone can relay the message. + /// @param _message Message payload to call target with. /// @return The hash of the message being sent, used to track whether the message has successfully been relayed. function _sendMessage( uint256 _destination, address _target, - bytes calldata _message, - address _entrypoint + address _entrypoint, + bytes calldata _message ) internal returns (bytes32) @@ -296,7 +296,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { if (!IDependencySet(Predeploys.L1_BLOCK_ATTRIBUTES).isInDependencySet(_destination)) revert InvalidChainId(); uint256 nonce = messageNonce(); - emit SentMessage(_destination, _target, nonce, msg.sender, _message, _entrypoint); + emit SentMessage(_destination, _target, nonce, msg.sender, _entrypoint, _message); msgNonce++; diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 36cb302c1616..4af0288b059b 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -123,7 +123,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { assertEq(logs[0].topics[3], bytes32(messageNonce)); // data - assertEq(logs[0].data, abi.encode(address(this), _message, address(0))); + assertEq(logs[0].data, abi.encode(address(this), address(0), _message)); // Check that the message nonce has been incremented assertEq(l2ToL2CrossDomainMessenger.messageNonce(), messageNonce + 1); @@ -158,7 +158,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { vm.recordLogs(); // Call the sendMessage function - bytes32 msgHash = l2ToL2CrossDomainMessenger.sendMessage(_destination, _target, _message, _entrypoint); + bytes32 msgHash = l2ToL2CrossDomainMessenger.sendMessage(_destination, _target, _entrypoint, _message); assertEq( msgHash, Hashing.hashL2toL2CrossDomainMessage( @@ -177,7 +177,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { assertEq(logs[0].topics[3], bytes32(messageNonce)); // data - assertEq(logs[0].data, abi.encode(address(this), _message, _entrypoint)); + assertEq(logs[0].data, abi.encode(address(this), _entrypoint, _message)); // Check that the message nonce has been incremented assertEq(l2ToL2CrossDomainMessenger.messageNonce(), messageNonce + 1); @@ -287,7 +287,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics - abi.encode(_sender, _message, address(_entrypoint)) // data + abi.encode(_sender, address(_entrypoint), _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -361,7 +361,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics - abi.encode(_sender, _message, address(0)) // data + abi.encode(_sender, address(0), _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -414,7 +414,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics - abi.encode(_sender, _message, _entrypoint) // data + abi.encode(_sender, _entrypoint, _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -517,7 +517,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, target, _nonce), // topics - abi.encode(_sender, message, address(0)) // data + abi.encode(_sender, address(0), message) // data ); // Ensure the CrossL2Inbox validates this message @@ -559,7 +559,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, address(0), _nonce), // topics - abi.encode(_sender, "") // data + abi.encode(_sender, address(0), "") // data ); l2ToL2CrossDomainMessenger.relayMessage(id, sentMessage); @@ -598,7 +598,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source1); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, target, _nonce), // topics - abi.encode(_sender1, message, address(0)) // data + abi.encode(_sender1, address(0), message) // data ); // Ensure the CrossL2Inbox validates this message @@ -647,7 +647,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier memory id = Identifier(_origin, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics - abi.encode(_sender, _message) // data + abi.encode(_sender, address(0), _message) // data ); // Call @@ -680,7 +680,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, _destination, _target, _nonce), // topics - abi.encode(_sender, _message) // data + abi.encode(_sender, address(0), _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -719,7 +719,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { abi.encode( L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, Predeploys.CROSS_L2_INBOX, _nonce ), // topics - abi.encode(_sender, _message) // data + abi.encode(_sender, address(0), _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -761,7 +761,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _nonce ), // topics - abi.encode(_sender, _message) // data + abi.encode(_sender, address(0), _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -811,7 +811,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics - abi.encode(_sender, _message, address(0)) // data + abi.encode(_sender, address(0), _message) // data ); // Ensure the CrossL2Inbox validates this message @@ -861,7 +861,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics - abi.encode(_sender, _message, address(0)) // data + abi.encode(_sender, address(0), _message) // data ); // Ensure the CrossL2Inbox validates this message From 606aa0e4d0824461ac9e89480218618a50a2212b Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:05:01 -0300 Subject: [PATCH 10/16] chore: polish contract and test --- .../contracts-bedrock/src/L2/Callbacks.sol | 199 +++++++++++------- .../contracts-bedrock/test/L2/Callbacks.t.sol | 116 ++++++---- 2 files changed, 197 insertions(+), 118 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index 28b32caaecc3..c29dc6eafdf5 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -4,131 +4,180 @@ pragma solidity 0.8.25; import { Identifier, IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -struct CallbackContext { - address to; - bytes4 selector; -} +interface ICallbackGreeterMetadata { + /// @notice Callback context to be executed (along with params if any) once the callback is received + /// @param to Target address + /// @param selector Function selector + struct CallbackContext { + address to; + bytes4 selector; + } -contract Greeter { - /** - * @notice Empty string for revert checks - * @dev result of doing keccak256(bytes('')) - */ - bytes32 internal constant _EMPTY_STRING = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + /// @notice Emitted when the greeting is set + /// @param greeting New greeting + event GreetingSet(string greeting); + + /// @notice Emitted when an async call is sent + /// @param contextNonce Nonce of the context + event AsyncCallSent(uint224 contextNonce); + + /// @notice Emitted when the remote greeter is set + /// @param remoteGreeter Address of the remote greeter + event RemoteGreeterSet(address remoteGreeter); + + /// @notice Emitted when the callback is executed + /// @param contextNonce Nonce of the context + event CallbackExecuted(uint224 contextNonce); + + /// @notice Thrown when caller is not the owner + error OnlyOwner(); + /// @notice Thrown when the callback fails + error CallbackFailed(); +} + +contract CallbackGreeter is ICallbackGreeterMetadata { + /// @notice Address of the L2ToL2CrossDomainMessenger contract address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - address public constant CALLBACK_ENTRYPOINT = address(0); // TODO: add setter + /// @notice Address of the callback entrypoint + address public immutable CALLBACK_ENTRYPOINT; + + /// @notice Owner of the contract address public immutable OWNER; + /// @notice Nonce for the return context to be used as an identifier for the callback. + /// 4 bytes are reserved for the callback selector. + uint224 public returnContextNonce; + + /// @notice Greeting string string public greeting; + /// @notice Remote greeter contract address address internal _remoteGreeter; - // 4 bytes are reserved for the callback selector - uint224 public returnContextNonce; - // Holds the return context for the async remoteGreet call - mapping(uint224 => CallbackContext) public callbackContexts; + /// @notice Contains the return context by nonce for the call to be made once the callback is received + mapping(uint224 _nonce => CallbackContext) public callbackContexts; /** - * @notice Reverts in case the function was not called by the owner of the contract + * @notice Reverts when the caller is not the owner. */ modifier onlyOwner() { - if (msg.sender != OWNER) { - revert("only owner"); - } + if (msg.sender != OWNER) revert OnlyOwner(); _; } /** - * @notice Defines the owner to the msg.sender and sets the initial greeting - * @param _greeting Initial greeting + * @notice Constructs the CallbackGreeter contract. + * @param _greeting Initial greeting. + * @param _callbackEntrypoint Address of the callback entrypoint. */ - constructor(string memory _greeting) { + constructor(string memory _greeting, address _callbackEntrypoint) { OWNER = msg.sender; + CALLBACK_ENTRYPOINT = _callbackEntrypoint; setGreeting(_greeting); } - function greet() external view returns (string memory _greeting, uint256 _balance) { - _greeting = greeting; - _balance = address(msg.sender).balance; - } - + /** + * @notice Sets the greeting. + * @param _greeting New greeting. + */ function setGreeting(string memory _greeting) public onlyOwner { - if (keccak256(bytes(_greeting)) == _EMPTY_STRING) { - revert("Greeter_InvalidGreeting"); - } - greeting = _greeting; - // emit GreetingSet(_greeting); + emit GreetingSet(_greeting); } /** - * @notice Initiates a remote greeting call handled by the callback - * @return contextNonce_ + * @notice Initiates a remote greeting call handled by the callback. + * @param _chainId Chain ID of the destination chain. + * @param _callbackContext Target and selector to call once the callback is received on origin chain. + * @return contextNonce_ Nonce of the context. */ - function remoteGreet(CallbackContext calldata _callbackContext) external returns (uint224 contextNonce_) { - contextNonce_ = async( - 2, // TODO: receive param - abi.encodeWithSelector(this.greeting.selector), - this.remoteGreetCallback.selector, - _callbackContext - ); + function remoteGreet( + uint64 _chainId, + CallbackContext calldata _callbackContext + ) + external + returns (uint224 contextNonce_) + { + bytes memory _targetCalldata = abi.encodeWithSelector(this.greeting.selector); + bytes4 _callbackSelector = this.remoteGreetCallback.selector; + contextNonce_ = async(_chainId, _targetCalldata, _callbackSelector, _callbackContext); } + /// @notice Callback function for the remote greeting + /// @param _contextNonce Nonce of the context + /// @param _remoteGreeting Greeting received from the remote greeter + /// @return greeting_ The greeting + /// @return balance_ The ETH balance of the caller function remoteGreetCallback( uint224 _contextNonce, string memory _remoteGreeting ) external - returns (string memory _greeting, uint256 _balance) + returns (string memory greeting_, uint256 balance_) { - _greeting = _remoteGreeting; - _balance = address(msg.sender).balance; + greeting_ = _remoteGreeting; + balance_ = address(msg.sender).balance; - // obtain the return context + // Obtain the return context CallbackContext memory _callbackContext = callbackContexts[_contextNonce]; - // delete the return context + // Delete the return context delete callbackContexts[_contextNonce]; - // call the callback (not sure if we should check for success...) - _callbackContext.to.call(abi.encodeWithSelector(_callbackContext.selector, _greeting, _balance)); + // Call the callback + (bool success,) = + _callbackContext.to.call(abi.encodeWithSelector(_callbackContext.selector, greeting_, balance_)); + if (!success) revert CallbackFailed(); + + emit CallbackExecuted(_contextNonce); } - // builds the cdm call with the callback entrypoint + /// @notice Initiates an async call to the remote greeter through the CallbackEntrypoint. + /// @param _chainid Chain ID of the destination chain. + /// @param _targetCalldata Calldata to be sent to the target contract on destination. + /// @param _callbackSelector Callback selector. + /// @param _callbackContext Context to be executed once the callback is received. + /// @return contextNonce_ Nonce of the context. function async( - uint256 _chainid, - bytes memory _data, + uint64 _chainid, + bytes memory _targetCalldata, bytes4 _callbackSelector, CallbackContext calldata _callbackContext ) internal returns (uint224 contextNonce_) { - // increment the nonce + // Increment the nonce contextNonce_ = ++returnContextNonce; - // store the return context + // Store the return context callbackContexts[contextNonce_] = _callbackContext; + // Encode the target call along with the callback selector and the context nonce as the message and send it. + bytes memory message = abi.encodePacked(_targetCalldata, _callbackSelector, contextNonce_); IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( - _chainid, - _remoteGreeter, - // data + callback selector - abi.encodePacked(_data, _callbackSelector, contextNonce_), - //abi.encodePacked(_data, _callbackSelector, contextNonce_, 32), // 32 bytes can be dynamic [IMPROVEMENT] - CALLBACK_ENTRYPOINT + _chainid, _remoteGreeter, message, CALLBACK_ENTRYPOINT ); + + emit AsyncCallSent(contextNonce_); } - // Setter for the remote greeter WIP without auth for now. - function setRemoteGreeter(address __remoteGreeter) public { + /// @notice Setter for the remote greeter + /// @param __remoteGreeter Address of the remote greeter + function setRemoteGreeter(address __remoteGreeter) public onlyOwner { _remoteGreeter = __remoteGreeter; + emit RemoteGreeterSet(__remoteGreeter); } } contract CallbackEntrypoint { + /// @notice Address of the L2ToL2CrossDomainMessenger contract address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + /// @notice Relays a message that was sent by the other CrossDomainMessenger contract. + /// @param _id Identifier of the SentMessage event to be relayed. + /// @param _sentMessage Message payload of the `SentMessage` event. + /// @return returnData_ Return data from the target contract call. function relayMessage( Identifier calldata _id, bytes calldata _sentMessage @@ -137,29 +186,21 @@ contract CallbackEntrypoint { payable returns (bytes memory returnData_) { - // Calls the CDM contract and get return value of the function call + // Call the CDM contract and get return value of the target call returnData_ = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); - // 0 to 31 bytes: SentMessage selector - // 32 to 127 bytes: destination (uint256), target (address), nonce (uint256) + // 0 to 32 bytes: SentMessage selector + // 32 to 128 bytes: destination (uint256), target (address), nonce (uint256) // 128 to end: sender (address), actual message (bytes), entrypoint (address) - (address sender) = abi.decode(_sentMessage[128:160], (address)); + address originSender = abi.decode(_sentMessage[128:160], (address)); - // the callback selector and params are in the last 32 bytes of the actual message, we need to take into - // account the 32 bytes of the entrypoint. - bytes32 _callbackSelectorAndParams = + // The callback selector and params are in the last 32 bytes of the actual message, we need to substract 32 + // taking into account the 32 bytes of the entrypoint. + bytes32 callbackSelectorAndParams = abi.decode(_sentMessage[_sentMessage.length - 64:_sentMessage.length - 32], (bytes32)); - // Creates new CDM message to _sentMessage origin, sender and _callbackSelector with the return value - IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( - _id.chainId, sender, abi.encodePacked(_callbackSelectorAndParams, returnData_) - ); + // Encode the callback selector, params and returned data as the message, and sent back to the origin sender + bytes memory _message = abi.encodePacked(callbackSelectorAndParams, returnData_); + IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage(_id.chainId, originSender, _message); } } - -// 1. Chain A: remoteGreet -> async -> sendMessage - -// 2. Chain B: entrypoint.relayMessage -> cdm.relayMessage -> greeting + remoteGreetCallback(nonce, ) -> -// sendMessage(chainA, greeterA, remoteGreetCallback) - -// 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call diff --git a/packages/contracts-bedrock/test/L2/Callbacks.t.sol b/packages/contracts-bedrock/test/L2/Callbacks.t.sol index 5e90976f0d4a..4cb812359da8 100644 --- a/packages/contracts-bedrock/test/L2/Callbacks.t.sol +++ b/packages/contracts-bedrock/test/L2/Callbacks.t.sol @@ -4,7 +4,11 @@ pragma solidity 0.8.25; import { Test } from "forge-std/Test.sol"; import { - CallbackEntrypoint, Greeter, CallbackContext, Identifier, IL2ToL2CrossDomainMessenger + CallbackEntrypoint, + CallbackGreeter, + ICallbackGreeterMetadata, + Identifier, + IL2ToL2CrossDomainMessenger } from "src/L2/Callbacks.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; @@ -14,44 +18,59 @@ import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; import "forge-std/Test.sol"; /// @notice This is the contract that will be called by the callback. -/// Greeter expects to have the the whatever function we choose to have a specific signature. +/// CallbackGreeter expects to have the the whatever function we choose to have a specific signature. contract Receiver { - function receiveGreetings(string memory, uint256 _balance) external returns (bool) { - return _balance != 0; + event GreetingReceived(string greeting, uint256 balance); + + string public receivedGreeting; + uint256 public receivedBalance; + + function receiveGreetings(string memory _greeting, uint256 _balance) external returns (bool _balanceIsZero) { + receivedGreeting = _greeting; + receivedBalance = _balance; + + emit GreetingReceived(_greeting, _balance); } } contract CallbackTest is Test { - CallbackEntrypoint public entrypointA; + address public constant CDM = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + CallbackEntrypoint public entrypointB; - Greeter public greeterA; - Greeter public greeterB; + CallbackGreeter public greeterA; + CallbackGreeter public greeterB; string public greetingA = "Hello, World from Chain A!"; string public greetingB = "Hello, World from Chain B!"; - Receiver public receiver; + uint64 chainA = 1; + uint64 chainB = 2; - uint64 chainIdA = 1; - uint64 chainIdB = 2; + Receiver public receiver; - address public user; + address public owner; + address public relayer; function setUp() public { + vm.startPrank(owner); + // Deploy the L2ToL2CrossDomainMessenger contract vm.etch(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, address(new L2ToL2CrossDomainMessenger()).code); - receiver = new Receiver(); - - entrypointA = new CallbackEntrypoint(); + // Deploy and setup entrypointB and greeters on both chains entrypointB = new CallbackEntrypoint(); - greeterA = new Greeter(greetingA); - greeterB = new Greeter(greetingB); + greeterA = new CallbackGreeter(greetingA, address(entrypointB)); + greeterB = new CallbackGreeter(greetingB, address(0)); greeterA.setRemoteGreeter(address(greeterB)); greeterB.setRemoteGreeter(address(greeterA)); + + // Deploy target contract on destination + receiver = new Receiver(); + + vm.stopPrank(); } /// @notice Helper function to setup a mock and expect a call to it. @@ -60,10 +79,15 @@ contract CallbackTest is Test { vm.expectCall(_receiver, _calldata); } + /// @notice Tests the callback functionality. the complete flow is as follows: + /// 1. Chain A: remoteGreet() -> async() -> sendMessage() + /// 2. Chain B: entrypoint.relayMessage() -> cdm.relayMessage() -> greeting() + remoteGreetCallback(nonce, ) + /// -> + /// sendMessage(chainA, greeterA, remoteGreetCallback(nonce, )) + /// 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call function testCallback() public { - /* 1. Chain A: remoteGreet -> async -> sendMessage */ - // Chain ID 1 == A - vm.chainId(chainIdA); + /* Step 1 - Chain A - trigger call on destination */ + vm.chainId(chainA); // Mock the call over the `isInDependencySet` function to return true vm.mockCall( @@ -72,27 +96,30 @@ contract CallbackTest is Test { abi.encode(true) ); - bytes4 _finalTargetCall = Receiver.receiveGreetings.selector; + // Construct the callback call with the target and selector and call `remoteGreeter()` + vm.prank(owner); + bytes4 _callbackSelector = Receiver.receiveGreetings.selector; + greeterA.remoteGreet( + chainB, ICallbackGreeterMetadata.CallbackContext({ to: address(receiver), selector: _callbackSelector }) + ); - greeterA.remoteGreet(CallbackContext({ to: address(receiver), selector: _finalTargetCall })); + // Get the context + uint224 callbackNonce = greeterA.returnContextNonce(); - /* 2. Chain B: entrypoint.relayMessage -> cdm.relayMessage -> greeting + remoteGreetCallback(nonce, ) -> - sendMessage(chainA, greeterA, remoteGreetCallback) */ - // Chain ID 2 == B - vm.chainId(chainIdB); + /* Step 2 - Chain B - relay message on destination and send message back */ + vm.chainId(chainB); // Construct the SentMessage payload & identifier Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, 1); - uint256 _cdmNonce = 1; - uint224 _greeterNonce = 1; - - bytes memory _data = abi.encodeWithSignature("greeting()"); - bytes memory _message = abi.encodePacked(_data, Greeter.remoteGreetCallback.selector, _greeterNonce); + uint256 cdmNonce = 1; + bytes memory targetCalladata = abi.encodeWithSignature("greeting()"); + bytes memory message = + abi.encodePacked(targetCalladata, CallbackGreeter.remoteGreetCallback.selector, callbackNonce); bytes memory sentMessage = abi.encodePacked( - abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainIdB, address(greeterB), _cdmNonce), // topics - abi.encode(address(greeterA), _message, address(entrypointB)) // data + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainB, address(greeterB), cdmNonce), // topics + abi.encode(address(greeterA), message, address(entrypointB)) // data ); // Mock the call over the `isInDependencySet` function to return true @@ -105,17 +132,17 @@ contract CallbackTest is Test { // Ensure the CrossL2Inbox validates this message _mockAndExpect(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); + vm.prank(relayer); bytes memory _returnData = entrypointB.relayMessage(id, sentMessage); - /* 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call */ - // Chain ID 1 == A - vm.chainId(chainIdA); - - _message = abi.encodeCall(Greeter.remoteGreetCallback, (1, string(_returnData))); + /* Step 3 - Chain A - callback */ + vm.chainId(chainA); + message = abi.encodeCall(CallbackGreeter.remoteGreetCallback, (callbackNonce, string(_returnData))); + // message = abi.encodeWithSelector(CallbackGreeter.remoteGreetCallback.selector, callbackNonce, _returnData); sentMessage = abi.encodePacked( - abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainIdA, address(greeterA), _cdmNonce), // topics - abi.encode(address(entrypointB), _message, address(0x0)) // data + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainA, address(greeterA), cdmNonce), // topics + abi.encode(address(entrypointB), message, address(0)) // data ); // Ensure the CrossL2Inbox validates this message @@ -125,6 +152,17 @@ contract CallbackTest is Test { "" ); + vm.prank(relayer); IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(id, sentMessage); + + console.log("received callback Greeting on origin"); + console.logString(receiver.receivedGreeting()); + + console.log("actual greeting"); + console.logString(greetingB); + + // Check the receiver contract state + assertEq(abi.encodePacked(receiver.receivedGreeting()), abi.encodePacked(greetingB), "1"); + assertEq(receiver.receivedBalance(), 0); } } From 8d7866fb0098b3d2351daf6bc13b472442070c80 Mon Sep 17 00:00:00 2001 From: Flux Date: Mon, 16 Dec 2024 15:27:15 -0500 Subject: [PATCH 11/16] chore: update semver in L2ToL2CrossDomainMessenger --- .../abi/L2ToL2CrossDomainMessenger.json | 22 +++++++++---------- .../snapshots/semver-lock.json | 4 ++-- .../src/L2/L2ToL2CrossDomainMessenger.sol | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json index 348d4b4d43f8..a5764e76a138 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json @@ -130,11 +130,6 @@ "internalType": "bytes", "name": "_message", "type": "bytes" - }, - { - "internalType": "address", - "name": "_entrypoint", - "type": "address" } ], "name": "sendMessage", @@ -160,6 +155,11 @@ "name": "_target", "type": "address" }, + { + "internalType": "address", + "name": "_entrypoint", + "type": "address" + }, { "internalType": "bytes", "name": "_message", @@ -261,17 +261,17 @@ "name": "sender", "type": "address" }, - { - "indexed": false, - "internalType": "bytes", - "name": "message", - "type": "bytes" - }, { "indexed": false, "internalType": "address", "name": "entrypoint", "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "message", + "type": "bytes" } ], "name": "SentMessage", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index dae941e0a82e..1e2cde01f2e2 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -100,8 +100,8 @@ "sourceCodeHash": "0xaef8ea36c5b78cd12e0e62811d51db627ccf0dfd2cc5479fb707a10ef0d42048" }, "src/L2/L2ToL2CrossDomainMessenger.sol": { - "initCodeHash": "0xeae09cf9c04e4edfa7bcb1282d1f2d0ed753fc24f122ab51b4ca9fd6c5230d2f", - "sourceCodeHash": "0x5f1452f6c299e4589fbb4ad7a2d96e7497724630888d7f17dfdc6297af5e8b64" + "initCodeHash": "0xce538694aa7471738330a247e09e6b36a2702bc36b4b58c01dbf819cd3ab393c", + "sourceCodeHash": "0xfabbf852a0886e59595435c5d6b7e8b09ad5c743cc6bdf7fbac9ffe2d08e17ec" }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0xdac32a1057a6bc8a8d2ffdce1db8f34950cd0ffd1454d2133865736d21869192", diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index 61361476faea..33a19738e0f2 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -75,8 +75,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { uint16 public constant messageVersion = uint16(0); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.14 - string public constant version = "1.0.0-beta.14"; + /// @custom:semver 1.0.0-beta.15 + string public constant version = "1.0.0-beta.15"; /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only be present in this /// mapping if it has successfully been relayed on this chain, and can therefore not be relayed again. From 291c7fea406f58d963773d8c98cb080bd2016c8a Mon Sep 17 00:00:00 2001 From: Flux Date: Mon, 16 Dec 2024 16:16:10 -0500 Subject: [PATCH 12/16] chore: ignore semgrep warnings --- .../contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol | 2 ++ packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol | 1 + packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 4af0288b059b..52a8e12ef9d3 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -206,6 +206,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { // Call the sendMessage function with value to provoke revert // NOTE: using encodeWithSignature to target the correct overloaded function signature + // nosemgrep: sol-style-use-abi-encodecall (bool success,) = address(l2ToL2CrossDomainMessenger).call{ value: _value }( abi.encodeWithSignature("sendMessage(uint256, address, bytes)", _destination, _target, _message) ); @@ -291,6 +292,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { ); // Ensure the CrossL2Inbox validates this message + // nosemgrep: sol-style-use-abi-encodecall vm.mockCall({ callee: Predeploys.CROSS_L2_INBOX, data: abi.encodeCall(ICrossL2Inbox.validateMessage, (id, keccak256(sentMessage))), diff --git a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol index 30243a5bac66..ea009dac4ace 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainTokenBridge.t.sol @@ -121,6 +121,7 @@ contract SuperchainTokenBridgeTest is CommonTest { // Mock the call over the `sendMessage` function and expect it to be called properly bytes memory _message = abi.encodeCall(superchainTokenBridge.relayERC20, (address(superchainERC20), _sender, _to, _amount)); + // nosemgrep: sol-style-use-abi-encodecall _mockAndExpect( Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, abi.encodeWithSignature( diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index e23b88b5fd76..d470d7874c7b 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -525,6 +525,7 @@ contract SuperchainWETH_Test is CommonTest { // Mock the call over the `sendMessage` function and expect it to be called properly bytes memory _message = abi.encodeCall(superchainWeth.relayETH, (_sender, _to, _amount)); + // nosemgrep: sol-style-use-abi-encodecall _mockAndExpect( Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, abi.encodeWithSignature("sendMessage(uint256,address,bytes)", _chainId, address(superchainWeth), _message), From 3e98a98e9157ff88986138bc5f0086abd90dc013 Mon Sep 17 00:00:00 2001 From: Flux Date: Tue, 17 Dec 2024 09:57:16 -0500 Subject: [PATCH 13/16] chore: use correct semver for L2ToL2CrossDomainMessenger --- packages/contracts-bedrock/snapshots/semver-lock.json | 4 ++-- .../contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 1e2cde01f2e2..aa568892a7a4 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -100,8 +100,8 @@ "sourceCodeHash": "0xaef8ea36c5b78cd12e0e62811d51db627ccf0dfd2cc5479fb707a10ef0d42048" }, "src/L2/L2ToL2CrossDomainMessenger.sol": { - "initCodeHash": "0xce538694aa7471738330a247e09e6b36a2702bc36b4b58c01dbf819cd3ab393c", - "sourceCodeHash": "0xfabbf852a0886e59595435c5d6b7e8b09ad5c743cc6bdf7fbac9ffe2d08e17ec" + "initCodeHash": "0x18c5491bf03bff26d6166fbae7305f46bdf723db13faa46e7cb59b43d523357d", + "sourceCodeHash": "0xae2bfa1fc28912d40d87f3ce3be772c56c987372d0a2f8d17780d96ff26fc6f5" }, "src/L2/OptimismSuperchainERC20.sol": { "initCodeHash": "0xdac32a1057a6bc8a8d2ffdce1db8f34950cd0ffd1454d2133865736d21869192", diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index 33a19738e0f2..61361476faea 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -75,8 +75,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { uint16 public constant messageVersion = uint16(0); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.15 - string public constant version = "1.0.0-beta.15"; + /// @custom:semver 1.0.0-beta.14 + string public constant version = "1.0.0-beta.14"; /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only be present in this /// mapping if it has successfully been relayed on this chain, and can therefore not be relayed again. From b43ae9b063e01377ad9d2739634d5c84b90746ec Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:58:08 -0300 Subject: [PATCH 14/16] fix: decode properly and make the test work --- .../contracts-bedrock/src/L2/Callbacks.sol | 23 ++++++---- .../contracts-bedrock/test/L2/Callbacks.t.sol | 42 +++++++++---------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index c29dc6eafdf5..c8efb7ea8d55 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -107,17 +107,18 @@ contract CallbackGreeter is ICallbackGreeterMetadata { /// @notice Callback function for the remote greeting /// @param _contextNonce Nonce of the context - /// @param _remoteGreeting Greeting received from the remote greeter + /// @param _remoteData Data received from the remote greeter /// @return greeting_ The greeting /// @return balance_ The ETH balance of the caller function remoteGreetCallback( uint224 _contextNonce, - string memory _remoteGreeting + bytes calldata _remoteData ) external returns (string memory greeting_, uint256 balance_) { - greeting_ = _remoteGreeting; + greeting_ = abi.decode(_remoteData, (string)); + balance_ = address(msg.sender).balance; // Obtain the return context @@ -155,6 +156,7 @@ contract CallbackGreeter is ICallbackGreeterMetadata { // Encode the target call along with the callback selector and the context nonce as the message and send it. bytes memory message = abi.encodePacked(_targetCalldata, _callbackSelector, contextNonce_); + IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( _chainid, _remoteGreeter, message, CALLBACK_ENTRYPOINT ); @@ -192,15 +194,20 @@ contract CallbackEntrypoint { // 0 to 32 bytes: SentMessage selector // 32 to 128 bytes: destination (uint256), target (address), nonce (uint256) // 128 to end: sender (address), actual message (bytes), entrypoint (address) - address originSender = abi.decode(_sentMessage[128:160], (address)); + (address originSender,, bytes memory inputMessage) = abi.decode(_sentMessage[128:], (address, address, bytes)); // The callback selector and params are in the last 32 bytes of the actual message, we need to substract 32 // taking into account the 32 bytes of the entrypoint. - bytes32 callbackSelectorAndParams = - abi.decode(_sentMessage[_sentMessage.length - 64:_sentMessage.length - 32], (bytes32)); + uint256 totalLen = 128 + 32 + 32 + 32 + 32 + inputMessage.length; + + bytes4 selector = bytes4(_sentMessage[totalLen - 32:totalLen - 28]); + uint224 nonce = uint224(bytes28(_sentMessage[totalLen - 28:totalLen])); // Encode the callback selector, params and returned data as the message, and sent back to the origin sender - bytes memory _message = abi.encodePacked(callbackSelectorAndParams, returnData_); - IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage(_id.chainId, originSender, _message); + bytes memory messageToSend = abi.encodeWithSelector(bytes4(selector), nonce, returnData_); + + IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + _id.chainId, originSender, messageToSend + ); } } diff --git a/packages/contracts-bedrock/test/L2/Callbacks.t.sol b/packages/contracts-bedrock/test/L2/Callbacks.t.sol index 4cb812359da8..c701b4d656c5 100644 --- a/packages/contracts-bedrock/test/L2/Callbacks.t.sol +++ b/packages/contracts-bedrock/test/L2/Callbacks.t.sol @@ -15,8 +15,6 @@ import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; import { L2ToL2CrossDomainMessenger } from "src/L2/L2ToL2CrossDomainMessenger.sol"; import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; -import "forge-std/Test.sol"; - /// @notice This is the contract that will be called by the callback. /// CallbackGreeter expects to have the the whatever function we choose to have a specific signature. contract Receiver { @@ -61,9 +59,16 @@ contract CallbackTest is Test { // Deploy and setup entrypointB and greeters on both chains entrypointB = new CallbackEntrypoint(); - greeterA = new CallbackGreeter(greetingA, address(entrypointB)); + vm.chainId(chainB); + greeterB = new CallbackGreeter(greetingB, address(0)); + vm.label(address(greeterB), "greeterB"); + + vm.chainId(chainA); + greeterA = new CallbackGreeter(greetingA, address(entrypointB)); + vm.label(address(greeterA), "greeterA"); + vm.chainId(chainB); greeterA.setRemoteGreeter(address(greeterB)); greeterB.setRemoteGreeter(address(greeterA)); @@ -113,13 +118,11 @@ contract CallbackTest is Test { Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, 1); uint256 cdmNonce = 1; - bytes memory targetCalladata = abi.encodeWithSignature("greeting()"); - bytes memory message = - abi.encodePacked(targetCalladata, CallbackGreeter.remoteGreetCallback.selector, callbackNonce); + bytes memory message = hex"ef690cc098b43dfb00000000000000000000000000000000000000000000000000000001"; bytes memory sentMessage = abi.encodePacked( abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainB, address(greeterB), cdmNonce), // topics - abi.encode(address(greeterA), message, address(entrypointB)) // data + abi.encode(address(greeterA), address(entrypointB), message) // data ); // Mock the call over the `isInDependencySet` function to return true @@ -133,36 +136,29 @@ contract CallbackTest is Test { _mockAndExpect(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); vm.prank(relayer); - bytes memory _returnData = entrypointB.relayMessage(id, sentMessage); + bytes memory returnData = entrypointB.relayMessage(id, sentMessage); /* Step 3 - Chain A - callback */ vm.chainId(chainA); - message = abi.encodeCall(CallbackGreeter.remoteGreetCallback, (callbackNonce, string(_returnData))); - // message = abi.encodeWithSelector(CallbackGreeter.remoteGreetCallback.selector, callbackNonce, _returnData); + // message = abi.encodeCall(CallbackGreeter.remoteGreetCallback, (callbackNonce, string(returnData))); + message = + hex"98b43dfb0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a48656c6c6f2c20576f726c642066726f6d20436861696e204221000000000000"; sentMessage = abi.encodePacked( abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainA, address(greeterA), cdmNonce), // topics - abi.encode(address(entrypointB), message, address(0)) // data + // // TODO: Check topic + abi.encode(address(greeterA), address(0), message) // data ); // Ensure the CrossL2Inbox validates this message - _mockAndExpect( - Predeploys.CROSS_L2_INBOX, - abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), // , abi.encode(id, - "" - ); + _mockAndExpect(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); vm.prank(relayer); IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(id, sentMessage); - console.log("received callback Greeting on origin"); - console.logString(receiver.receivedGreeting()); - - console.log("actual greeting"); - console.logString(greetingB); - // Check the receiver contract state - assertEq(abi.encodePacked(receiver.receivedGreeting()), abi.encodePacked(greetingB), "1"); + // assertEq(abi.encode(receiver.receivedGreeting()), abi.encode(greetingB), "1"); + assertEq(abi.encodePacked(receiver.receivedGreeting()), abi.encodePacked(greeterB.greeting())); assertEq(receiver.receivedBalance(), 0); } } From 3eaa7d065db74423bf3b22898c57ca868eda592e Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:56:36 -0300 Subject: [PATCH 15/16] chore: polish and update natspec --- .../contracts-bedrock/src/L2/Callbacks.sol | 39 ++++++---- .../contracts-bedrock/test/L2/Callbacks.t.sol | 76 ++++++++++--------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol index c8efb7ea8d55..6de65c564ebe 100644 --- a/packages/contracts-bedrock/src/L2/Callbacks.sol +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -36,6 +36,11 @@ interface ICallbackGreeterMetadata { error CallbackFailed(); } +/** + * @title CallbackGreeter + * @notice Initiates a remote greeting call to a greeter contract on a different chain via the CallbackEntrypoint. It + * also handles the callback received from the remote greeter. + */ contract CallbackGreeter is ICallbackGreeterMetadata { /// @notice Address of the L2ToL2CrossDomainMessenger contract address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; @@ -100,9 +105,9 @@ contract CallbackGreeter is ICallbackGreeterMetadata { external returns (uint224 contextNonce_) { - bytes memory _targetCalldata = abi.encodeWithSelector(this.greeting.selector); - bytes4 _callbackSelector = this.remoteGreetCallback.selector; - contextNonce_ = async(_chainId, _targetCalldata, _callbackSelector, _callbackContext); + bytes memory targetCalldata = abi.encodeWithSelector(this.greeting.selector); + bytes4 callbackSelector = this.remoteGreetCallback.selector; + contextNonce_ = async(_chainId, targetCalldata, callbackSelector, _callbackContext); } /// @notice Callback function for the remote greeting @@ -118,7 +123,6 @@ contract CallbackGreeter is ICallbackGreeterMetadata { returns (string memory greeting_, uint256 balance_) { greeting_ = abi.decode(_remoteData, (string)); - balance_ = address(msg.sender).balance; // Obtain the return context @@ -158,7 +162,7 @@ contract CallbackGreeter is ICallbackGreeterMetadata { bytes memory message = abi.encodePacked(_targetCalldata, _callbackSelector, contextNonce_); IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( - _chainid, _remoteGreeter, message, CALLBACK_ENTRYPOINT + _chainid, _remoteGreeter, CALLBACK_ENTRYPOINT, message ); emit AsyncCallSent(contextNonce_); @@ -172,6 +176,11 @@ contract CallbackGreeter is ICallbackGreeterMetadata { } } +/** + * @title CallbackEntrypoint + * @notice This contract serves as an entry point to relay messages on the Cross-Domain Messenger (CDM). It subsequently + * sends a callback using the input message and return data to the original sender. + */ contract CallbackEntrypoint { /// @notice Address of the L2ToL2CrossDomainMessenger contract address public constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; @@ -191,17 +200,19 @@ contract CallbackEntrypoint { // Call the CDM contract and get return value of the target call returnData_ = IL2ToL2CrossDomainMessenger(L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(_id, _sentMessage); - // 0 to 32 bytes: SentMessage selector - // 32 to 128 bytes: destination (uint256), target (address), nonce (uint256) - // 128 to end: sender (address), actual message (bytes), entrypoint (address) - (address originSender,, bytes memory inputMessage) = abi.decode(_sentMessage[128:], (address, address, bytes)); + // 0 to 128 bytes: SentMessage selector, destination (uint256), target (address), nonce (uint256) + uint256 topicsLength = 128; + // 128 to end: sender (address), entrypoint (address), actual message (bytes) + (address originSender,, bytes memory inputMessage) = + abi.decode(_sentMessage[topicsLength:], (address, address, bytes)); - // The callback selector and params are in the last 32 bytes of the actual message, we need to substract 32 - // taking into account the 32 bytes of the entrypoint. - uint256 totalLen = 128 + 32 + 32 + 32 + 32 + inputMessage.length; + // Get the position in which the message content ends + // 128 to message: sender (address), entrypoint (address), message bytes offset, message bytes length + uint256 messageContentLength = topicsLength + 32 + 32 + 32 + 32 + inputMessage.length; - bytes4 selector = bytes4(_sentMessage[totalLen - 32:totalLen - 28]); - uint224 nonce = uint224(bytes28(_sentMessage[totalLen - 28:totalLen])); + // From the content, get the callback selector and params that are in the last 32 bytes + bytes4 selector = bytes4(_sentMessage[messageContentLength - 32:messageContentLength - 28]); + uint224 nonce = uint224(bytes28(_sentMessage[messageContentLength - 28:messageContentLength])); // Encode the callback selector, params and returned data as the message, and sent back to the origin sender bytes memory messageToSend = abi.encodeWithSelector(bytes4(selector), nonce, returnData_); diff --git a/packages/contracts-bedrock/test/L2/Callbacks.t.sol b/packages/contracts-bedrock/test/L2/Callbacks.t.sol index c701b4d656c5..4e93427b7524 100644 --- a/packages/contracts-bedrock/test/L2/Callbacks.t.sol +++ b/packages/contracts-bedrock/test/L2/Callbacks.t.sol @@ -15,15 +15,14 @@ import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; import { L2ToL2CrossDomainMessenger } from "src/L2/L2ToL2CrossDomainMessenger.sol"; import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; -/// @notice This is the contract that will be called by the callback. -/// CallbackGreeter expects to have the the whatever function we choose to have a specific signature. +/// @notice This is the contract that will be called by the callback. It only stores what it receives. contract Receiver { event GreetingReceived(string greeting, uint256 balance); string public receivedGreeting; uint256 public receivedBalance; - function receiveGreetings(string memory _greeting, uint256 _balance) external returns (bool _balanceIsZero) { + function receiveGreetings(string memory _greeting, uint256 _balance) external { receivedGreeting = _greeting; receivedBalance = _balance; @@ -33,11 +32,8 @@ contract Receiver { contract CallbackTest is Test { address public constant CDM = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; - - CallbackEntrypoint public entrypointB; - - CallbackGreeter public greeterA; - CallbackGreeter public greeterB; + uint256 public CDM_NONCE = 1; + address public NO_CUSTOM_ENTRYPOINT = address(0); string public greetingA = "Hello, World from Chain A!"; string public greetingB = "Hello, World from Chain B!"; @@ -45,6 +41,9 @@ contract CallbackTest is Test { uint64 chainA = 1; uint64 chainB = 2; + CallbackGreeter public greeterA; + CallbackGreeter public greeterB; + CallbackEntrypoint public entrypointB; Receiver public receiver; address public owner; @@ -78,20 +77,15 @@ contract CallbackTest is Test { vm.stopPrank(); } - /// @notice Helper function to setup a mock and expect a call to it. - function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { - vm.mockCall(_receiver, _calldata, _returned); - vm.expectCall(_receiver, _calldata); - } - /// @notice Tests the callback functionality. the complete flow is as follows: /// 1. Chain A: remoteGreet() -> async() -> sendMessage() /// 2. Chain B: entrypoint.relayMessage() -> cdm.relayMessage() -> greeting() + remoteGreetCallback(nonce, ) - /// -> - /// sendMessage(chainA, greeterA, remoteGreetCallback(nonce, )) + /// -> sendMessage(chainA, greeterA, remoteGreetCallback(nonce, )) /// 3. Chain A: CDM.relayMessage -> remoteGreetCallback -> target call function testCallback() public { - /* Step 1 - Chain A - trigger call on destination */ + /** + * Step 1 - Chain A - trigger call on destination + */ vm.chainId(chainA); // Mock the call over the `isInDependencySet` function to return true @@ -101,27 +95,34 @@ contract CallbackTest is Test { abi.encode(true) ); + // Selector of the function to be executed once the callback is received + bytes4 callbackContextSelector = Receiver.receiveGreetings.selector; + // Construct the callback call with the target and selector and call `remoteGreeter()` vm.prank(owner); - bytes4 _callbackSelector = Receiver.receiveGreetings.selector; greeterA.remoteGreet( - chainB, ICallbackGreeterMetadata.CallbackContext({ to: address(receiver), selector: _callbackSelector }) + chainB, + ICallbackGreeterMetadata.CallbackContext({ to: address(receiver), selector: callbackContextSelector }) ); - // Get the context + // Get the context that was stored and sent on the message uint224 callbackNonce = greeterA.returnContextNonce(); - /* Step 2 - Chain B - relay message on destination and send message back */ + /** + * Step 2 - Chain B - relay message on destination and send message back + */ vm.chainId(chainB); - // Construct the SentMessage payload & identifier + // Construct the message identifier Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, 1); - uint256 cdmNonce = 1; - bytes memory message = hex"ef690cc098b43dfb00000000000000000000000000000000000000000000000000000001"; + // Define the message that was built on the `async` function, when the message was sent in origin + bytes memory message = + abi.encodePacked(abi.encodeWithSignature("greeting()"), callbackContextSelector, callbackNonce); + // Build the whole message to be relayed by the entrypoint bytes memory sentMessage = abi.encodePacked( - abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainB, address(greeterB), cdmNonce), // topics + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainB, address(greeterB), CDM_NONCE), // topics abi.encode(address(greeterA), address(entrypointB), message) // data ); @@ -133,31 +134,34 @@ contract CallbackTest is Test { ); // Ensure the CrossL2Inbox validates this message - _mockAndExpect(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); + vm.mockCall(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); + // Relay the message on the entrypoint vm.prank(relayer); bytes memory returnData = entrypointB.relayMessage(id, sentMessage); - /* Step 3 - Chain A - callback */ + /** + * Step 3 - Chain A - callback + */ vm.chainId(chainA); - // message = abi.encodeCall(CallbackGreeter.remoteGreetCallback, (callbackNonce, string(returnData))); - message = - hex"98b43dfb0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a48656c6c6f2c20576f726c642066726f6d20436861696e204221000000000000"; + // Define the message that was built on the entrypoint, when the message was sent back to origin + message = abi.encodeWithSelector(CallbackGreeter.remoteGreetCallback.selector, callbackNonce, returnData); + + // Build the whole message to be relayed by the CDM sentMessage = abi.encodePacked( - abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainA, address(greeterA), cdmNonce), // topics - // // TODO: Check topic - abi.encode(address(greeterA), address(0), message) // data + abi.encode(IL2ToL2CrossDomainMessenger.SentMessage.selector, chainA, address(greeterA), CDM_NONCE), // topics + abi.encode(address(greeterB), NO_CUSTOM_ENTRYPOINT, message) // data ); // Ensure the CrossL2Inbox validates this message - _mockAndExpect(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); + vm.mockCall(Predeploys.CROSS_L2_INBOX, abi.encodeWithSelector(ICrossL2Inbox.validateMessage.selector), ""); + // Relay the message on the CDM vm.prank(relayer); IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).relayMessage(id, sentMessage); - // Check the receiver contract state - // assertEq(abi.encode(receiver.receivedGreeting()), abi.encode(greetingB), "1"); + // Check the receiver contract state to ensure the callback was properly executed assertEq(abi.encodePacked(receiver.receivedGreeting()), abi.encodePacked(greeterB.greeting())); assertEq(receiver.receivedBalance(), 0); } From cbd788ec74dae5ca57f13f594648c6a9e4ea88a4 Mon Sep 17 00:00:00 2001 From: 0xDiscotech <131301107+0xDiscotech@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:06:44 -0300 Subject: [PATCH 16/16] chore: remove forge import on cdm --- .../contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index ac943c20d080..02ddebbaf744 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -12,8 +12,6 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; import { ICrossL2Inbox, Identifier } from "interfaces/L2/ICrossL2Inbox.sol"; -import "forge-std/Test.sol"; - /// @notice Thrown when a non-written slot in transient storage is attempted to be read from. error NotEntered();