diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index be41205ec737..5f7c0946e283 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -227,4 +227,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} \ No newline at end of file +} diff --git a/packages/contracts-bedrock/src/L2/Callbacks.sol b/packages/contracts-bedrock/src/L2/Callbacks.sol new file mode 100644 index 000000000000..6de65c564ebe --- /dev/null +++ b/packages/contracts-bedrock/src/L2/Callbacks.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Identifier, IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +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; + } + + /// @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(); +} + +/** + * @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; + + /// @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; + + /// @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 when the caller is not the owner. + */ + modifier onlyOwner() { + if (msg.sender != OWNER) revert OnlyOwner(); + _; + } + + /** + * @notice Constructs the CallbackGreeter contract. + * @param _greeting Initial greeting. + * @param _callbackEntrypoint Address of the callback entrypoint. + */ + constructor(string memory _greeting, address _callbackEntrypoint) { + OWNER = msg.sender; + CALLBACK_ENTRYPOINT = _callbackEntrypoint; + setGreeting(_greeting); + } + + /** + * @notice Sets the greeting. + * @param _greeting New greeting. + */ + function setGreeting(string memory _greeting) public onlyOwner { + greeting = _greeting; + emit GreetingSet(_greeting); + } + + /** + * @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( + 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 _remoteData Data received from the remote greeter + /// @return greeting_ The greeting + /// @return balance_ The ETH balance of the caller + function remoteGreetCallback( + uint224 _contextNonce, + bytes calldata _remoteData + ) + external + returns (string memory greeting_, uint256 balance_) + { + greeting_ = abi.decode(_remoteData, (string)); + balance_ = address(msg.sender).balance; + + // Obtain the return context + CallbackContext memory _callbackContext = callbackContexts[_contextNonce]; + // Delete the return context + delete callbackContexts[_contextNonce]; + + // Call the callback + (bool success,) = + _callbackContext.to.call(abi.encodeWithSelector(_callbackContext.selector, greeting_, balance_)); + if (!success) revert CallbackFailed(); + + emit CallbackExecuted(_contextNonce); + } + + /// @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( + uint64 _chainid, + bytes memory _targetCalldata, + bytes4 _callbackSelector, + CallbackContext calldata _callbackContext + ) + internal + returns (uint224 contextNonce_) + { + // Increment the nonce + contextNonce_ = ++returnContextNonce; + // 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, CALLBACK_ENTRYPOINT, message + ); + + emit AsyncCallSent(contextNonce_); + } + + /// @notice Setter for the remote greeter + /// @param __remoteGreeter Address of the remote greeter + function setRemoteGreeter(address __remoteGreeter) public onlyOwner { + _remoteGreeter = __remoteGreeter; + emit RemoteGreeterSet(__remoteGreeter); + } +} + +/** + * @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; + + /// @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 + ) + external + payable + returns (bytes memory returnData_) + { + // 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 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)); + + // 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; + + // 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_); + + 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 new file mode 100644 index 000000000000..4e93427b7524 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/Callbacks.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; + +import { + CallbackEntrypoint, + CallbackGreeter, + ICallbackGreeterMetadata, + 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"; + +/// @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 { + receivedGreeting = _greeting; + receivedBalance = _balance; + + emit GreetingReceived(_greeting, _balance); + } +} + +contract CallbackTest is Test { + address public constant CDM = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + 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!"; + + uint64 chainA = 1; + uint64 chainB = 2; + + CallbackGreeter public greeterA; + CallbackGreeter public greeterB; + CallbackEntrypoint public entrypointB; + Receiver public receiver; + + 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); + + // Deploy and setup entrypointB and greeters on both chains + entrypointB = new CallbackEntrypoint(); + + 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)); + + // Deploy target contract on destination + receiver = new Receiver(); + + vm.stopPrank(); + } + + /// @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 { + /** + * Step 1 - Chain A - trigger call on destination + */ + vm.chainId(chainA); + + // Mock the call over the `isInDependencySet` function to return true + vm.mockCall( + Predeploys.L1_BLOCK_ATTRIBUTES, + abi.encodeWithSelector(IDependencySet.isInDependencySet.selector), + 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); + greeterA.remoteGreet( + chainB, + ICallbackGreeterMetadata.CallbackContext({ to: address(receiver), selector: callbackContextSelector }) + ); + + // 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 + */ + vm.chainId(chainB); + + // Construct the message identifier + Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, 1); + + // 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), CDM_NONCE), // topics + abi.encode(address(greeterA), address(entrypointB), message) // 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 + 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 + */ + vm.chainId(chainA); + + // 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), CDM_NONCE), // topics + abi.encode(address(greeterB), NO_CUSTOM_ENTRYPOINT, message) // data + ); + + // Ensure the CrossL2Inbox validates this message + 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 to ensure the callback was properly executed + assertEq(abi.encodePacked(receiver.receivedGreeting()), abi.encodePacked(greeterB.greeting())); + assertEq(receiver.receivedBalance(), 0); + } +}