From 52a7157f091127172ebfa4a9472122f562723cc3 Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 17 Mar 2025 13:33:19 +0400 Subject: [PATCH 1/6] forge install: optimism v1.12.1 --- .gitmodules | 3 +++ lib/optimism | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/optimism diff --git a/.gitmodules b/.gitmodules index 0f078158..2feeb225 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/optimism"] + path = lib/optimism + url = https://github.com/ethereum-optimism/optimism diff --git a/lib/optimism b/lib/optimism new file mode 160000 index 00000000..d3b8eadd --- /dev/null +++ b/lib/optimism @@ -0,0 +1 @@ +Subproject commit d3b8eadd80457c74d9c4251948ac11e8d14a9c9c From 127642cfc1d53af90a30b89bb650288ca05a4aad Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 17 Mar 2025 13:33:44 +0400 Subject: [PATCH 2/6] feat: remappings --- remappings.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/remappings.txt b/remappings.txt index 7b75fc8f..f9f668f8 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ hardhat-deploy/=node_modules/hardhat-deploy/ hardhat/=node_modules/hardhat/ solady/=lib/solady/src/ +optimism/=lib/optimism/packages/contracts-bedrock/ From b89e90a68381236b68b2c67a4446ef01e8d7a729 Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 17 Mar 2025 13:34:07 +0400 Subject: [PATCH 3/6] feat: wip interop sb --- .../switchboard/OpInteropSwitchboard.sol | 206 ++++++++++++++++++ .../socket/switchboard/SuperchainEnabled.sol | 73 +++++++ 2 files changed, 279 insertions(+) create mode 100644 contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol create mode 100644 contracts/protocol/socket/switchboard/SuperchainEnabled.sol diff --git a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol new file mode 100644 index 00000000..47818579 --- /dev/null +++ b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {FastSwitchboard} from "./FastSwitchboard.sol"; +import {SuperchainEnabled} from "./SuperchainEnabled.sol"; +import {ISocket} from "../../../interfaces/ISocket.sol"; + +contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { + address public token; + address public remoteAddress; + uint256 public remoteChainId; + + mapping(bytes32 => bool) public isSyncedOut; + mapping(bytes32 => bytes32) public payloadIdToDigest; + mapping(address => uint256) public unminted; + + error OnlyTokenAllowed(); + + modifier onlyToken() { + if (msg.sender != token) revert OnlyTokenAllowed(); + _; + } + + constructor( + uint32 chainSlug_, + ISocket socket_, + address owner_ + ) FastSwitchboard(chainSlug_, socket_, owner_) { + if (chainSlug_ == 420120000) { + remoteChainId = 420120001; + } else if (chainSlug_ == 420120001) { + remoteChainId = 420120000; + } + } + + function attest(bytes32 payloadId_, bytes32 digest_, bytes calldata proof_) external override { + address watcher = _recoverSigner(keccak256(abi.encode(address(this), digest_)), proof_); + + if (isAttested[digest_]) revert AlreadyAttested(); + if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + + isAttested[digest_] = true; + payloadIdToDigest[payloadId_] = digest_; + emit Attested(payloadId_, digest_, watcher); + } + + function syncOut( + bytes32 digest_, + bytes32 payloadId_, + PayloadParams calldata payloadParams_ + ) external override { + if (isSyncedOut[digest_]) return; + isSyncedOut[digest_] = true; + + if (!isAttested[digest_]) return; + + bytes32 digest = payloadIdToDigest[payloadId_]; + if (digest != digest_) return; + + bytes32 expectedDigest = _packPayload(payloadParams_); + if (expectedDigest != digest_) return; + + ISocket.ExecutionStatus isExecuted = socket__.payloadExecuted(payloadId_); + if (isExecuted != ISocket.ExecutionStatus.Executed) return; + + _xMessageContract( + remoteChainId, + remoteAddress, + abi.encodeWithSelector(this.syncIn.selector, payloadId_, digest_) + ); + } + + function syncIn( + bytes32 payloadId_, + bytes32 digest_ + ) external xOnlyFromContract(remoteAddress, remoteChainId) { + remoteExecutedDigests[payloadId_] = digest_; + } + + function proveRemoteExecutions( + bytes32[] calldata payloadIds_, + ExecuteParams memory executeParams_, + bytes memory transmitterSignature_ + ) external { + bytes32 previousDigestsHash = bytes32(0); + if (!isAttested[digest_]) revert AlreadyAttested(); + if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + + isAttested[digest_] = true; + bytes32 digest = payloadIdToDigest[payloadId_]; + for (uint256 i = 0; i < payloadIds_.length; i++) { + if (remoteExecutedDigests[payloadIds_[i]] == bytes32(0)) + revert RemoteExecutionNotFound(); + previousDigestsHash = keccak256( + abi.encodePacked(previousDigestsHash, remoteExecutedDigests[payloadIds_[i]]) + ); + } + } + + /** + * @notice creates the digest for the payload + * @param transmitter_ The address of the transmitter + * @param payloadId_ The ID of the payload + * @param appGateway_ The address of the app gateway + * @param executeParams_ The parameters of the payload + * @return The packed payload as a bytes32 hash + */ + function _createDigest( + address transmitter_, + bytes32 payloadId_, + address appGateway_, + ExecuteParams memory executeParams_ + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + transmitter_, + payloadId_, + executeParams_.deadline, + executeParams_.callType, + executeParams_.writeFinality, + executeParams_.gasLimit, + msg.value, + executeParams_.readAt, + executeParams_.payload, + executeParams_.target, + appGateway_, + executeParams_.prevDigestsHash + ) + ); + } + + /** + * @notice creates the payload ID + * @param switchboard_ The address of the switchboard + * @param executeParams_ The parameters of the payload + */ + function _createPayloadId( + address switchboard_, + ExecuteParams memory executeParams_ + ) internal view returns (bytes32) { + // todo: match with watcher + return + keccak256( + abi.encode( + executeParams_.requestCount, + executeParams_.batchCount, + executeParams_.payloadCount, + switchboard_, + chainSlug + ) + ); + } + + // function _decodeBurn( + // bytes memory payload + // ) internal pure returns (address user, uint256 amount, bool isBurn) { + // // Extract function selector from payload + // bytes4 selector; + // assembly { + // // Load first 4 bytes from payload data + // selector := mload(add(payload, 32)) + // } + // // Check if selector matches burn() + // if (selector != bytes4(0x9dc29fac)) return (user, amount, false); + + // // Decode the payload after the selector (skip first 4 bytes) + // assembly { + // user := mload(add(add(payload, 36), 0)) // 32 + 4 bytes offset for first param + // amount := mload(add(add(payload, 68), 0)) // 32 + 4 + 32 bytes offset for second param + // } + // isBurn = true; + // } + + // function _packPayload(PayloadParams memory payloadParams_) internal pure returns (bytes32) { + // return + // keccak256( + // abi.encode( + // payloadParams_.payloadId, + // payloadParams_.appGateway, + // payloadParams_.transmitter, + // payloadParams_.target, + // payloadParams_.value, + // payloadParams_.deadline, + // payloadParams_.executionGasLimit, + // payloadParams_.payload + // ) + // ); + // } + + // function checkAndConsume(address user_, uint256 amount_) external onlyToken { + // unminted[user_] -= amount_; + // } + + // function setToken(address token_) external onlyOwner { + // token = token_; + // } + + function setRemoteAddress(address _remoteAddress) external onlyOwner { + remoteAddress = _remoteAddress; + } + + function setRemoteChainId(uint256 _remoteChainId) external onlyOwner { + remoteChainId = _remoteChainId; + } +} diff --git a/contracts/protocol/socket/switchboard/SuperchainEnabled.sol b/contracts/protocol/socket/switchboard/SuperchainEnabled.sol new file mode 100644 index 00000000..21105805 --- /dev/null +++ b/contracts/protocol/socket/switchboard/SuperchainEnabled.sol @@ -0,0 +1,73 @@ +pragma solidity ^0.8.13; + +// SuperchainEnabled provides utilities for cross-chain event validation, +// sending messages, and receiving messages with modifiers. + +import {IL2ToL2CrossDomainMessenger} from "optimism/interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; +import {Predeploys} from "optimism/src/libraries/Predeploys.sol"; + +abstract contract SuperchainEnabled { + // Error definitions + error CallerNotL2ToL2CrossDomainMessenger(); + error InvalidCrossDomainSender(); + error InvalidSourceChain(); + + /// @notice Sends a cross-chain message to a destination address on another chain + /// @param destChainId The chain ID of the destination chain + /// @param destAddress The address of the destination contract + /// @param data The calldata to send to the destination contract + function _xMessageContract( + uint256 destChainId, + address destAddress, + bytes memory data + ) internal { + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + destChainId, + destAddress, + data + ); + } + + /// @notice Checks if the cross-domain message is from the expected source + /// @param expectedSource The expected source address + /// @return bool True if the message is from the expected source, false otherwise + function _isValidCrossDomainSender(address expectedSource) internal view returns (bool) { + if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { + return false; + } + return + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) + .crossDomainMessageSender() == expectedSource; + } + + /// @notice Modifier to validate messages from a specific address + /// @param expectedSource The expected source address + modifier xOnlyFromAddress(address expectedSource) { + if (!_isValidCrossDomainSender(expectedSource)) { + revert InvalidCrossDomainSender(); + } + _; + } + + /// @notice Modifier to validate messages from a specific address on a specific chain + /// @param expectedSource The expected source address + /// @param expectedChainId The expected source chain ID + modifier xOnlyFromContract(address expectedSource, uint256 expectedChainId) { + if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { + revert CallerNotL2ToL2CrossDomainMessenger(); + } + if ( + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) + .crossDomainMessageSender() != expectedSource + ) { + revert InvalidCrossDomainSender(); + } + if ( + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) + .crossDomainMessageSource() != expectedChainId + ) { + revert InvalidSourceChain(); + } + _; + } +} From 8e6e5b21aff4829cfb601b22d9ccf4691c50120b Mon Sep 17 00:00:00 2001 From: arthcp Date: Tue, 18 Mar 2025 14:27:07 +0400 Subject: [PATCH 4/6] feat: finish op sb logic --- contracts/interfaces/ISocket.sol | 5 +- contracts/protocol/socket/Socket.sol | 4 +- contracts/protocol/socket/SocketConfig.sol | 6 +- .../socket/switchboard/FastSwitchboard.sol | 4 +- .../switchboard/OpInteropSwitchboard.sol | 133 +++++++++--------- test/mock/MockSocket.sol | 6 - 6 files changed, 74 insertions(+), 84 deletions(-) diff --git a/contracts/interfaces/ISocket.sol b/contracts/interfaces/ISocket.sol index 1d7e1a1e..2940b041 100644 --- a/contracts/interfaces/ISocket.sol +++ b/contracts/interfaces/ISocket.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {ExecuteParams} from "../protocol/utils/common/Structs.sol"; +import {ExecuteParams, ExecutionStatus} from "../protocol/utils/common/Structs.sol"; + /** * @title ISocket * @notice An interface for a Chain Abstraction contract @@ -83,4 +84,6 @@ interface ISocket { function getPlugConfig( address plugAddress_ ) external view returns (address appGateway, address switchboard); + + function payloadExecuted(bytes32 payloadId_) external view returns (ExecutionStatus); } diff --git a/contracts/protocol/socket/Socket.sol b/contracts/protocol/socket/Socket.sol index 84b588af..968234d0 100644 --- a/contracts/protocol/socket/Socket.sol +++ b/contracts/protocol/socket/Socket.sol @@ -50,7 +50,7 @@ contract Socket is SocketUtils { bytes calldata payload, bytes32 params ) external returns (bytes32 callId) { - PlugConfig memory plugConfig = _plugConfigs[msg.sender]; + PlugConfig memory plugConfig = plugConfigs[msg.sender]; // if no sibling plug is found for the given chain slug, revert if (plugConfig.appGateway == address(0)) revert PlugDisconnected(); @@ -75,7 +75,7 @@ contract Socket is SocketUtils { bytes memory transmitterSignature_ ) external payable returns (bytes memory) { if (executeParams_.deadline < block.timestamp) revert DeadlinePassed(); - PlugConfig memory plugConfig = _plugConfigs[executeParams_.target]; + PlugConfig memory plugConfig = plugConfigs[executeParams_.target]; if (plugConfig.appGateway == address(0)) revert PlugDisconnected(); bytes32 payloadId = _createPayloadId(plugConfig.switchboard, executeParams_); diff --git a/contracts/protocol/socket/SocketConfig.sol b/contracts/protocol/socket/SocketConfig.sol index 346a6da9..ebb7f335 100644 --- a/contracts/protocol/socket/SocketConfig.sol +++ b/contracts/protocol/socket/SocketConfig.sol @@ -18,7 +18,7 @@ abstract contract SocketConfig is ISocket, AccessControl { mapping(address => SwitchboardStatus) public isValidSwitchboard; // plug => (appGateway, switchboard__) - mapping(address => PlugConfig) internal _plugConfigs; + mapping(address => PlugConfig) public plugConfigs; // Error triggered when a connection is invalid error InvalidConnection(); @@ -50,7 +50,7 @@ abstract contract SocketConfig is ISocket, AccessControl { if (isValidSwitchboard[switchboard_] != SwitchboardStatus.REGISTERED) revert InvalidSwitchboard(); - PlugConfig storage _plugConfig = _plugConfigs[msg.sender]; + PlugConfig storage _plugConfig = plugConfigs[msg.sender]; _plugConfig.appGateway = appGateway_; _plugConfig.switchboard = switchboard_; @@ -65,7 +65,7 @@ abstract contract SocketConfig is ISocket, AccessControl { function getPlugConfig( address plugAddress_ ) external view returns (address appGateway, address switchboard) { - PlugConfig memory _plugConfig = _plugConfigs[plugAddress_]; + PlugConfig memory _plugConfig = plugConfigs[plugAddress_]; return (_plugConfig.appGateway, _plugConfig.switchboard); } } diff --git a/contracts/protocol/socket/switchboard/FastSwitchboard.sol b/contracts/protocol/socket/switchboard/FastSwitchboard.sol index 308e07f6..afe152fe 100644 --- a/contracts/protocol/socket/switchboard/FastSwitchboard.sol +++ b/contracts/protocol/socket/switchboard/FastSwitchboard.sol @@ -39,7 +39,7 @@ contract FastSwitchboard is SwitchboardBase { * @param proof_ proof from watcher * @notice we are attesting a digest uniquely identified with payloadId. */ - function attest(bytes32 digest_, bytes calldata proof_) external { + function attest(bytes32 digest_, bytes calldata proof_) external virtual { address watcher = _recoverSigner(keccak256(abi.encode(address(this), digest_)), proof_); if (isAttested[digest_]) revert AlreadyAttested(); @@ -52,7 +52,7 @@ contract FastSwitchboard is SwitchboardBase { /** * @inheritdoc ISwitchboard */ - function allowPacket(bytes32 digest_, bytes32) external view returns (bool) { + function allowPacket(bytes32 digest_, bytes32) external view virtual returns (bool) { // digest has enough attestations return isAttested[digest_]; } diff --git a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol index 47818579..8a17bbad 100644 --- a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol +++ b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.21; import {FastSwitchboard} from "./FastSwitchboard.sol"; import {SuperchainEnabled} from "./SuperchainEnabled.sol"; import {ISocket} from "../../../interfaces/ISocket.sol"; +import {ExecuteParams, ExecutionStatus} from "../../utils/common/Structs.sol"; contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { address public token; @@ -13,8 +14,17 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { mapping(bytes32 => bool) public isSyncedOut; mapping(bytes32 => bytes32) public payloadIdToDigest; mapping(address => uint256) public unminted; + mapping(bytes32 => bytes32) public remoteExecutedDigests; + mapping(bytes32 => bool) public isRemoteExecuted; error OnlyTokenAllowed(); + error RemoteExecutionNotFound(); + error DigestMismatch(); + error PreviousDigestsHashMismatch(); + error NotAttested(); + error NotExecuted(); + + event Attested(bytes32 payloadId, bytes32 digest, address watcher); modifier onlyToken() { if (msg.sender != token) revert OnlyTokenAllowed(); @@ -33,7 +43,11 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { } } - function attest(bytes32 payloadId_, bytes32 digest_, bytes calldata proof_) external override { + function attest(bytes32 /*digest_*/, bytes calldata /*proof_*/) external override { + revert("Not implemented"); + } + + function attest(bytes32 payloadId_, bytes32 digest_, bytes calldata proof_) external { address watcher = _recoverSigner(keccak256(abi.encode(address(this), digest_)), proof_); if (isAttested[digest_]) revert AlreadyAttested(); @@ -44,29 +58,35 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { emit Attested(payloadId_, digest_, watcher); } - function syncOut( + function allowPacket( bytes32 digest_, - bytes32 payloadId_, - PayloadParams calldata payloadParams_ - ) external override { - if (isSyncedOut[digest_]) return; - isSyncedOut[digest_] = true; - - if (!isAttested[digest_]) return; + bytes32 payloadId_ + ) external view override returns (bool) { + // digest has enough attestations and is remote executed + return + payloadIdToDigest[payloadId_] == digest_ && + isAttested[digest_] && + isRemoteExecuted[payloadId_]; + } + function syncOut(bytes32 payloadId_) external { bytes32 digest = payloadIdToDigest[payloadId_]; - if (digest != digest_) return; - bytes32 expectedDigest = _packPayload(payloadParams_); - if (expectedDigest != digest_) return; + // not attested + if (digest == bytes32(0) || !isAttested[digest]) revert NotAttested(); + + // already synced out + if (isSyncedOut[digest]) return; + isSyncedOut[digest] = true; - ISocket.ExecutionStatus isExecuted = socket__.payloadExecuted(payloadId_); - if (isExecuted != ISocket.ExecutionStatus.Executed) return; + // not executed + ExecutionStatus isExecuted = socket__.payloadExecuted(payloadId_); + if (isExecuted != ExecutionStatus.Executed) revert NotExecuted(); _xMessageContract( remoteChainId, remoteAddress, - abi.encodeWithSelector(this.syncIn.selector, payloadId_, digest_) + abi.encodeWithSelector(this.syncIn.selector, payloadId_, digest) ); } @@ -78,23 +98,40 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { } function proveRemoteExecutions( - bytes32[] calldata payloadIds_, - ExecuteParams memory executeParams_, - bytes memory transmitterSignature_ + bytes32[] calldata previousPayloadIds_, + bytes32 currentPayloadId_, + address transmitter_, + ExecuteParams memory executeParams_ ) external { + // Calculate previousDigestsHash from stored remoteExecutedDigests bytes32 previousDigestsHash = bytes32(0); - if (!isAttested[digest_]) revert AlreadyAttested(); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - - isAttested[digest_] = true; - bytes32 digest = payloadIdToDigest[payloadId_]; - for (uint256 i = 0; i < payloadIds_.length; i++) { - if (remoteExecutedDigests[payloadIds_[i]] == bytes32(0)) + for (uint256 i = 0; i < previousPayloadIds_.length; i++) { + if (remoteExecutedDigests[previousPayloadIds_[i]] == bytes32(0)) revert RemoteExecutionNotFound(); previousDigestsHash = keccak256( - abi.encodePacked(previousDigestsHash, remoteExecutedDigests[payloadIds_[i]]) + abi.encodePacked(previousDigestsHash, remoteExecutedDigests[previousPayloadIds_[i]]) ); } + + // Check if the calculated previousDigestsHash matches the one in executeParams_ + if (previousDigestsHash != executeParams_.prevDigestsHash) + revert PreviousDigestsHashMismatch(); + + // Construct current digest + (address appGateway, ) = socket__.getPlugConfig(executeParams_.target); + bytes32 constructedDigest = _createDigest( + transmitter_, + currentPayloadId_, + appGateway, + executeParams_ + ); + + // Verify the constructed digest matches the stored one + bytes32 storedDigest = payloadIdToDigest[currentPayloadId_]; + if (storedDigest == bytes32(0) || !isAttested[storedDigest]) revert NotAttested(); + if (constructedDigest != storedDigest) revert DigestMismatch(); + + isRemoteExecuted[currentPayloadId_] = true; } /** @@ -152,50 +189,6 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { ); } - // function _decodeBurn( - // bytes memory payload - // ) internal pure returns (address user, uint256 amount, bool isBurn) { - // // Extract function selector from payload - // bytes4 selector; - // assembly { - // // Load first 4 bytes from payload data - // selector := mload(add(payload, 32)) - // } - // // Check if selector matches burn() - // if (selector != bytes4(0x9dc29fac)) return (user, amount, false); - - // // Decode the payload after the selector (skip first 4 bytes) - // assembly { - // user := mload(add(add(payload, 36), 0)) // 32 + 4 bytes offset for first param - // amount := mload(add(add(payload, 68), 0)) // 32 + 4 + 32 bytes offset for second param - // } - // isBurn = true; - // } - - // function _packPayload(PayloadParams memory payloadParams_) internal pure returns (bytes32) { - // return - // keccak256( - // abi.encode( - // payloadParams_.payloadId, - // payloadParams_.appGateway, - // payloadParams_.transmitter, - // payloadParams_.target, - // payloadParams_.value, - // payloadParams_.deadline, - // payloadParams_.executionGasLimit, - // payloadParams_.payload - // ) - // ); - // } - - // function checkAndConsume(address user_, uint256 amount_) external onlyToken { - // unminted[user_] -= amount_; - // } - - // function setToken(address token_) external onlyOwner { - // token = token_; - // } - function setRemoteAddress(address _remoteAddress) external onlyOwner { remoteAddress = _remoteAddress; } diff --git a/test/mock/MockSocket.sol b/test/mock/MockSocket.sol index da1765a9..242ef4c4 100644 --- a/test/mock/MockSocket.sol +++ b/test/mock/MockSocket.sol @@ -68,12 +68,6 @@ contract MockSocket is ISocket { uint64 public callCounter; uint32 public chainSlug; - enum ExecutionStatus { - NotExecuted, - Executed, - Reverted - } - /** * @dev keeps track of whether a payload has been executed or not using payload id */ From 040d88ad31cadf2ca7c429181ac88faa4e9fb3b8 Mon Sep 17 00:00:00 2001 From: arthcp Date: Thu, 20 Mar 2025 16:14:44 +0400 Subject: [PATCH 5/6] feat: multi remote support for op sb --- .../switchboard/OpInteropSwitchboard.sol | 91 +++++++++++++------ .../socket/switchboard/SuperchainEnabled.sol | 73 --------------- 2 files changed, 61 insertions(+), 103 deletions(-) delete mode 100644 contracts/protocol/socket/switchboard/SuperchainEnabled.sol diff --git a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol index 8a17bbad..315d94de 100644 --- a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol +++ b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol @@ -2,46 +2,43 @@ pragma solidity ^0.8.21; import {FastSwitchboard} from "./FastSwitchboard.sol"; -import {SuperchainEnabled} from "./SuperchainEnabled.sol"; +// import {SuperchainEnabled} from "./SuperchainEnabled.sol"; import {ISocket} from "../../../interfaces/ISocket.sol"; import {ExecuteParams, ExecutionStatus} from "../../utils/common/Structs.sol"; +import {IL2ToL2CrossDomainMessenger} from "optimism/interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; +import {Predeploys} from "optimism/src/libraries/Predeploys.sol"; -contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { - address public token; - address public remoteAddress; - uint256 public remoteChainId; +contract OpInteropSwitchboard is FastSwitchboard { + struct RemoteEndpoint { + uint256 remoteChainId; + address remoteAddress; + } + + // remoteChainSlug => remoteEndpoint + mapping(uint32 => RemoteEndpoint) public remoteEndpoints; + // remoteChainId => remoteAddress + mapping(uint256 => address) public remoteAddresses; mapping(bytes32 => bool) public isSyncedOut; mapping(bytes32 => bytes32) public payloadIdToDigest; - mapping(address => uint256) public unminted; mapping(bytes32 => bytes32) public remoteExecutedDigests; mapping(bytes32 => bool) public isRemoteExecuted; - error OnlyTokenAllowed(); error RemoteExecutionNotFound(); error DigestMismatch(); error PreviousDigestsHashMismatch(); error NotAttested(); error NotExecuted(); + error CallerNotL2ToL2CrossDomainMessenger(); + error InvalidCrossDomainSender(); event Attested(bytes32 payloadId, bytes32 digest, address watcher); - modifier onlyToken() { - if (msg.sender != token) revert OnlyTokenAllowed(); - _; - } - constructor( uint32 chainSlug_, ISocket socket_, address owner_ - ) FastSwitchboard(chainSlug_, socket_, owner_) { - if (chainSlug_ == 420120000) { - remoteChainId = 420120001; - } else if (chainSlug_ == 420120001) { - remoteChainId = 420120000; - } - } + ) FastSwitchboard(chainSlug_, socket_, owner_) {} function attest(bytes32 /*digest_*/, bytes calldata /*proof_*/) external override { revert("Not implemented"); @@ -69,7 +66,7 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { isRemoteExecuted[payloadId_]; } - function syncOut(bytes32 payloadId_) external { + function syncOut(bytes32 payloadId_, uint32 remoteChainSlug_) external { bytes32 digest = payloadIdToDigest[payloadId_]; // not attested @@ -84,16 +81,28 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { if (isExecuted != ExecutionStatus.Executed) revert NotExecuted(); _xMessageContract( - remoteChainId, - remoteAddress, + remoteEndpoints[remoteChainSlug_].remoteChainId, + remoteEndpoints[remoteChainSlug_].remoteAddress, abi.encodeWithSelector(this.syncIn.selector, payloadId_, digest) ); } - function syncIn( - bytes32 payloadId_, - bytes32 digest_ - ) external xOnlyFromContract(remoteAddress, remoteChainId) { + function syncIn(bytes32 payloadId_, bytes32 digest_) external { + if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { + revert CallerNotL2ToL2CrossDomainMessenger(); + } + + address remoteAddress = IL2ToL2CrossDomainMessenger( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER + ).crossDomainMessageSender(); + uint256 remoteChainId = IL2ToL2CrossDomainMessenger( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER + ).crossDomainMessageSource(); + + if (remoteAddresses[remoteChainId] != remoteAddress) { + revert InvalidCrossDomainSender(); + } + remoteExecutedDigests[payloadId_] = digest_; } @@ -189,11 +198,33 @@ contract OpInteropSwitchboard is FastSwitchboard, SuperchainEnabled { ); } - function setRemoteAddress(address _remoteAddress) external onlyOwner { - remoteAddress = _remoteAddress; + function _xMessageContract( + uint256 destChainId, + address destAddress, + bytes memory data + ) internal { + IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( + destChainId, + destAddress, + data + ); + } + + function addRemoteEndpoint( + uint32 remoteChainSlug_, + uint256 remoteChainId_, + address remoteAddress_ + ) external onlyOwner { + remoteEndpoints[remoteChainSlug_] = RemoteEndpoint({ + remoteChainId: remoteChainId_, + remoteAddress: remoteAddress_ + }); + remoteAddresses[remoteChainId_] = remoteAddress_; } - function setRemoteChainId(uint256 _remoteChainId) external onlyOwner { - remoteChainId = _remoteChainId; + function removeRemoteEndpoint(uint32 remoteChainSlug_) external onlyOwner { + uint256 remoteChainId = remoteEndpoints[remoteChainSlug_].remoteChainId; + delete remoteEndpoints[remoteChainSlug_]; + delete remoteAddresses[remoteChainId]; } } diff --git a/contracts/protocol/socket/switchboard/SuperchainEnabled.sol b/contracts/protocol/socket/switchboard/SuperchainEnabled.sol deleted file mode 100644 index 21105805..00000000 --- a/contracts/protocol/socket/switchboard/SuperchainEnabled.sol +++ /dev/null @@ -1,73 +0,0 @@ -pragma solidity ^0.8.13; - -// SuperchainEnabled provides utilities for cross-chain event validation, -// sending messages, and receiving messages with modifiers. - -import {IL2ToL2CrossDomainMessenger} from "optimism/interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; -import {Predeploys} from "optimism/src/libraries/Predeploys.sol"; - -abstract contract SuperchainEnabled { - // Error definitions - error CallerNotL2ToL2CrossDomainMessenger(); - error InvalidCrossDomainSender(); - error InvalidSourceChain(); - - /// @notice Sends a cross-chain message to a destination address on another chain - /// @param destChainId The chain ID of the destination chain - /// @param destAddress The address of the destination contract - /// @param data The calldata to send to the destination contract - function _xMessageContract( - uint256 destChainId, - address destAddress, - bytes memory data - ) internal { - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER).sendMessage( - destChainId, - destAddress, - data - ); - } - - /// @notice Checks if the cross-domain message is from the expected source - /// @param expectedSource The expected source address - /// @return bool True if the message is from the expected source, false otherwise - function _isValidCrossDomainSender(address expectedSource) internal view returns (bool) { - if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { - return false; - } - return - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) - .crossDomainMessageSender() == expectedSource; - } - - /// @notice Modifier to validate messages from a specific address - /// @param expectedSource The expected source address - modifier xOnlyFromAddress(address expectedSource) { - if (!_isValidCrossDomainSender(expectedSource)) { - revert InvalidCrossDomainSender(); - } - _; - } - - /// @notice Modifier to validate messages from a specific address on a specific chain - /// @param expectedSource The expected source address - /// @param expectedChainId The expected source chain ID - modifier xOnlyFromContract(address expectedSource, uint256 expectedChainId) { - if (msg.sender != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { - revert CallerNotL2ToL2CrossDomainMessenger(); - } - if ( - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) - .crossDomainMessageSender() != expectedSource - ) { - revert InvalidCrossDomainSender(); - } - if ( - IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) - .crossDomainMessageSource() != expectedChainId - ) { - revert InvalidSourceChain(); - } - _; - } -} From b725f582837646db8c6ad74bed2594952cfd30df Mon Sep 17 00:00:00 2001 From: arthcp Date: Mon, 14 Apr 2025 20:25:43 +0400 Subject: [PATCH 6/6] feat: linked payload testcase --- contracts/interfaces/ISocket.sol | 2 + contracts/protocol/socket/SocketBatcher.sol | 35 ++ contracts/protocol/socket/SocketUtils.sol | 2 +- .../switchboard/OpInteropSwitchboard.sol | 14 +- test/apps/app-gateways/op-token/IOpToken.sol | 8 + test/apps/app-gateways/op-token/OpSb.t.sol | 505 ++++++++++++++++++ test/apps/app-gateways/op-token/OpToken.sol | 61 +++ .../op-token/OpTokenAppGateway.sol | 74 +++ 8 files changed, 695 insertions(+), 6 deletions(-) create mode 100644 test/apps/app-gateways/op-token/IOpToken.sol create mode 100644 test/apps/app-gateways/op-token/OpSb.t.sol create mode 100644 test/apps/app-gateways/op-token/OpToken.sol create mode 100644 test/apps/app-gateways/op-token/OpTokenAppGateway.sol diff --git a/contracts/interfaces/ISocket.sol b/contracts/interfaces/ISocket.sol index 2940b041..9d8d2cea 100644 --- a/contracts/interfaces/ISocket.sol +++ b/contracts/interfaces/ISocket.sol @@ -77,6 +77,8 @@ interface ISocket { function registerSwitchboard() external; + function chainSlug() external view returns (uint32); + /** * @notice returns the config for given `plugAddress_` and `siblingChainSlug_` * @param plugAddress_ address of plug present at current chain diff --git a/contracts/protocol/socket/SocketBatcher.sol b/contracts/protocol/socket/SocketBatcher.sol index 49eb1966..9fb3dcd1 100644 --- a/contracts/protocol/socket/SocketBatcher.sol +++ b/contracts/protocol/socket/SocketBatcher.sol @@ -7,6 +7,7 @@ import "../../interfaces/ISwitchboard.sol"; import "../utils/RescueFundsLib.sol"; import {ExecuteParams} from "../../protocol/utils/common/Structs.sol"; import "../../interfaces/ISocketBatcher.sol"; +import {OpInteropSwitchboard} from "./switchboard/OpInteropSwitchboard.sol"; /** * @title SocketBatcher @@ -36,6 +37,40 @@ contract SocketBatcher is ISocketBatcher, Ownable { return socket__.execute{value: msg.value}(executeParams_, transmitterSignature_); } + function attestOPProveAndExecute( + ExecuteParams calldata executeParams_, + bytes32[] calldata previousPayloadIds_, + bytes32 digest_, + bytes calldata proof_, + bytes calldata transmitterSignature_ + ) external payable returns (bytes memory) { + OpInteropSwitchboard(executeParams_.switchboard).attest( + _createPayloadId(executeParams_), + digest_, + proof_ + ); + OpInteropSwitchboard(executeParams_.switchboard).proveRemoteExecutions( + previousPayloadIds_, + _createPayloadId(executeParams_), + transmitterSignature_, + executeParams_ + ); + return socket__.execute{value: msg.value}(executeParams_, transmitterSignature_); + } + + function _createPayloadId(ExecuteParams memory executeParams_) internal view returns (bytes32) { + return + keccak256( + abi.encode( + executeParams_.requestCount, + executeParams_.batchCount, + executeParams_.payloadCount, + executeParams_.switchboard, + socket__.chainSlug() + ) + ); + } + function rescueFunds(address token_, address to_, uint256 amount_) external onlyOwner { RescueFundsLib._rescueFunds(token_, to_, amount_); } diff --git a/contracts/protocol/socket/SocketUtils.sol b/contracts/protocol/socket/SocketUtils.sol index 97aa46ba..40e08f0a 100644 --- a/contracts/protocol/socket/SocketUtils.sol +++ b/contracts/protocol/socket/SocketUtils.sol @@ -18,7 +18,7 @@ abstract contract SocketUtils is SocketConfig { // Version string for this socket instance bytes32 public immutable version; // ChainSlug for this deployed socket instance - uint32 public immutable chainSlug; + uint32 public immutable override chainSlug; uint64 public callCounter; diff --git a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol index 315d94de..f5ce2863 100644 --- a/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol +++ b/contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol @@ -40,7 +40,7 @@ contract OpInteropSwitchboard is FastSwitchboard { address owner_ ) FastSwitchboard(chainSlug_, socket_, owner_) {} - function attest(bytes32 /*digest_*/, bytes calldata /*proof_*/) external override { + function attest(bytes32 /*digest_*/, bytes calldata /*proof_*/) external pure override { revert("Not implemented"); } @@ -109,7 +109,7 @@ contract OpInteropSwitchboard is FastSwitchboard { function proveRemoteExecutions( bytes32[] calldata previousPayloadIds_, bytes32 currentPayloadId_, - address transmitter_, + bytes calldata transmitterSignature_, ExecuteParams memory executeParams_ ) external { // Calculate previousDigestsHash from stored remoteExecutedDigests @@ -121,22 +121,26 @@ contract OpInteropSwitchboard is FastSwitchboard { abi.encodePacked(previousDigestsHash, remoteExecutedDigests[previousPayloadIds_[i]]) ); } - // Check if the calculated previousDigestsHash matches the one in executeParams_ if (previousDigestsHash != executeParams_.prevDigestsHash) revert PreviousDigestsHashMismatch(); + address transmitter = _recoverSigner( + keccak256(abi.encode(address(socket__), currentPayloadId_)), + transmitterSignature_ + ); + // Construct current digest (address appGateway, ) = socket__.getPlugConfig(executeParams_.target); bytes32 constructedDigest = _createDigest( - transmitter_, + transmitter, currentPayloadId_, appGateway, executeParams_ ); - // Verify the constructed digest matches the stored one bytes32 storedDigest = payloadIdToDigest[currentPayloadId_]; + // Verify the constructed digest matches the stored one if (storedDigest == bytes32(0) || !isAttested[storedDigest]) revert NotAttested(); if (constructedDigest != storedDigest) revert DigestMismatch(); diff --git a/test/apps/app-gateways/op-token/IOpToken.sol b/test/apps/app-gateways/op-token/IOpToken.sol new file mode 100644 index 00000000..ca9d3d4e --- /dev/null +++ b/test/apps/app-gateways/op-token/IOpToken.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +interface IOpToken { + function burn(address user_, uint256 amount_) external; + + function mint(address receiver_, uint256 amount_) external; +} diff --git a/test/apps/app-gateways/op-token/OpSb.t.sol b/test/apps/app-gateways/op-token/OpSb.t.sol new file mode 100644 index 00000000..bf4bc835 --- /dev/null +++ b/test/apps/app-gateways/op-token/OpSb.t.sol @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {OpToken} from "./OpToken.sol"; +import {Test} from "forge-std/Test.sol"; +import {Socket} from "../../../../contracts/protocol/socket/Socket.sol"; +import {OpInteropSwitchboard} from "../../../../contracts/protocol/socket/switchboard/OpInteropSwitchboard.sol"; +import {OpTokenAppGateway} from "./OpTokenAppGateway.sol"; +import {Fees, ExecuteParams, CallType, WriteFinality} from "../../../../contracts/protocol/utils/common/Structs.sol"; +import {IAddressResolver} from "../../../../contracts/interfaces/IAddressResolver.sol"; +import {IWatcherPrecompile} from "../../../../contracts/interfaces/IWatcherPrecompile.sol"; +import {ISocket} from "../../../../contracts/interfaces/ISocket.sol"; +import {IMiddleware} from "../../../../contracts/interfaces/IMiddleware.sol"; +import {IOpToken} from "./IOpToken.sol"; +import {console} from "forge-std/console.sol"; +import {SocketBatcher} from "../../../../contracts/protocol/socket/SocketBatcher.sol"; +import {WATCHER_ROLE} from "../../../../contracts/protocol/utils/common/AccessRoles.sol"; +import {Predeploys} from "optimism/src/libraries/Predeploys.sol"; +import {IL2ToL2CrossDomainMessenger} from "optimism/interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; + +contract OpSbTest is Test { + uint256 public c = 1; + string _version = "1.0.0"; + uint256 immutable _ownerPrivateKey = c++; + uint256 immutable _transmitterPrivateKey = c++; + uint256 immutable _watcherPrivateKey = c++; + address _owner; + address _transmitter; + address _watcher; + address _ramu = address(uint160(c++)); + uint256 _amount = 10e18; + uint256 _totalSupply = 100e18; + uint256 _requestCount = 0; + struct ChainDetails { + Socket socket; + SocketBatcher batcher; + OpInteropSwitchboard switchboard; + OpToken token; + } + + struct EVMxChainDetails { + address forwarder; + address asyncPromise; + } + struct EVMxDetails { + OpTokenAppGateway appGateway; + address addressResolver; + address deliveryHelper; + address watcherPrecompile; + mapping(uint32 => EVMxChainDetails) chainDetails; + } + + struct PayloadExecDetails { + ExecuteParams executeParams; + bytes32[] previousPayloadIds; + bytes32 digest; + bytes32 payloadId; + bytes proof; + bytes transmitterSignature; + } + + // chainSlug => ChainDetails + mapping(uint256 => ChainDetails) public chainDetails; + EVMxDetails public evmxDetails; + + function setUp() public { + _owner = vm.addr(_ownerPrivateKey); + _transmitter = vm.addr(_transmitterPrivateKey); + _watcher = vm.addr(_watcherPrivateKey); + vm.startPrank(_owner); + + for (uint32 i = 0; i < 3; i++) { + Socket socket = new Socket(i, _owner, _version); + chainDetails[i] = ChainDetails({ + socket: socket, + batcher: new SocketBatcher(_owner, socket), + switchboard: new OpInteropSwitchboard(i, socket, _owner), + token: new OpToken("Test Token", "TEST", 18, _owner, _totalSupply) + }); + evmxDetails.chainDetails[i] = EVMxChainDetails({ + forwarder: address(uint160(c++)), + asyncPromise: address(uint160(c++)) + }); + chainDetails[i].switchboard.grantRole(WATCHER_ROLE, _watcher); + } + + evmxDetails.addressResolver = address(uint160(c++)); + evmxDetails.deliveryHelper = address(uint160(c++)); + evmxDetails.watcherPrecompile = address(uint160(c++)); + + evmxDetails.appGateway = new OpTokenAppGateway( + evmxDetails.addressResolver, + _owner, + Fees({feePoolChain: uint32(c++), feePoolToken: address(uint160(c++)), amount: c++}), + OpTokenAppGateway.ConstructorParams({ + name_: "Test Token", + symbol_: "TEST", + decimals_: 18, + initialSupplyHolder_: _owner, + initialSupply_: _totalSupply + }) + ); + chainDetails[0].switchboard.registerSwitchboard(); + chainDetails[1].switchboard.registerSwitchboard(); + chainDetails[2].switchboard.registerSwitchboard(); + + chainDetails[0].token.initSocket( + address(evmxDetails.appGateway), + address(chainDetails[0].socket), + address(chainDetails[0].switchboard) + ); + chainDetails[1].token.initSocket( + address(evmxDetails.appGateway), + address(chainDetails[1].socket), + address(chainDetails[1].switchboard) + ); + chainDetails[2].token.initSocket( + address(evmxDetails.appGateway), + address(chainDetails[2].socket), + address(chainDetails[2].switchboard) + ); + chainDetails[0].switchboard.addRemoteEndpoint(1, 1, address(chainDetails[1].switchboard)); + chainDetails[0].switchboard.addRemoteEndpoint(2, 2, address(chainDetails[2].switchboard)); + chainDetails[1].switchboard.addRemoteEndpoint(0, 0, address(chainDetails[0].switchboard)); + chainDetails[1].switchboard.addRemoteEndpoint(2, 2, address(chainDetails[2].switchboard)); + chainDetails[2].switchboard.addRemoteEndpoint(0, 0, address(chainDetails[0].switchboard)); + chainDetails[2].switchboard.addRemoteEndpoint(1, 1, address(chainDetails[1].switchboard)); + vm.stopPrank(); + } + + function testTokensMinted() public view { + uint256 ownerAmount = chainDetails[0].token.balanceOf(_owner); + assertEq(ownerAmount, _totalSupply); + ownerAmount = chainDetails[1].token.balanceOf(_owner); + assertEq(ownerAmount, _totalSupply); + ownerAmount = chainDetails[2].token.balanceOf(_owner); + assertEq(ownerAmount, _totalSupply); + } + + function testTokenTransfer() public { + _distributeTokens(); + _mockEVMxCalls(); + OpTokenAppGateway.TransferOrder memory t = _getTransferOrder(); + vm.prank(_ramu); + evmxDetails.appGateway.transfer(abi.encode(t)); + + PayloadExecDetails[] memory p = _getPayloadExecDetails(); + + vm.expectEmit(true, true, false, false); + emit ISocket.ExecutionSuccess(p[0].payloadId, ""); + chainDetails[0].batcher.attestOPProveAndExecute( + p[0].executeParams, + p[0].previousPayloadIds, + p[0].digest, + p[0].proof, + p[0].transmitterSignature + ); + + vm.expectEmit(true, true, false, false); + emit ISocket.ExecutionSuccess(p[1].payloadId, ""); + chainDetails[1].batcher.attestOPProveAndExecute( + p[1].executeParams, + p[1].previousPayloadIds, + p[1].digest, + p[1].proof, + p[1].transmitterSignature + ); + + _sync(p); + + vm.expectEmit(true, true, false, false); + emit ISocket.ExecutionSuccess(p[2].payloadId, ""); + chainDetails[2].batcher.attestOPProveAndExecute( + p[2].executeParams, + p[2].previousPayloadIds, + p[2].digest, + p[2].proof, + p[2].transmitterSignature + ); + } + + function _sync(PayloadExecDetails[] memory p) private { + bytes memory syncPayloadA = abi.encodeWithSelector( + OpInteropSwitchboard.syncIn.selector, + p[0].payloadId, + p[0].digest + ); + vm.mockCall( + address(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER), + abi.encodeWithSelector( + IL2ToL2CrossDomainMessenger.sendMessage.selector, + 2, + address(chainDetails[2].switchboard), + syncPayloadA + ), + abi.encode(true) + ); + chainDetails[0].switchboard.syncOut(p[0].payloadId, 2); + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(chainDetails[0].switchboard)) + ); + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), + abi.encode(0) + ); + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + (bool successA, ) = address(chainDetails[2].switchboard).call(syncPayloadA); + require(successA); + + bytes memory syncPayloadB = abi.encodeWithSelector( + OpInteropSwitchboard.syncIn.selector, + p[1].payloadId, + p[1].digest + ); + vm.mockCall( + address(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER), + abi.encodeWithSelector( + IL2ToL2CrossDomainMessenger.sendMessage.selector, + 2, + address(chainDetails[2].switchboard), + syncPayloadB + ), + abi.encode(true) + ); + chainDetails[1].switchboard.syncOut(p[1].payloadId, 2); + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector), + abi.encode(address(chainDetails[1].switchboard)) + ); + vm.mockCall( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector), + abi.encode(1) + ); + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + (bool successB, ) = address(chainDetails[2].switchboard).call(syncPayloadB); + require(successB); + } + + function _distributeTokens() private { + vm.startPrank(_owner); + chainDetails[0].token.transfer(_ramu, _amount); + chainDetails[1].token.transfer(_ramu, _amount); + vm.stopPrank(); + + assertEq(chainDetails[0].token.balanceOf(_ramu), _amount); + assertEq(chainDetails[1].token.balanceOf(_ramu), _amount); + } + + function _getTransferOrder() private view returns (OpTokenAppGateway.TransferOrder memory t) { + address[] memory srcTokens = new address[](2); + uint256[] memory srcAmounts = new uint256[](2); + address[] memory dstTokens = new address[](1); + uint256[] memory dstAmounts = new uint256[](1); + + srcTokens[0] = address(evmxDetails.chainDetails[0].forwarder); + srcAmounts[0] = _amount; + + srcTokens[1] = address(evmxDetails.chainDetails[1].forwarder); + srcAmounts[1] = _amount; + + dstTokens[0] = address(evmxDetails.chainDetails[2].forwarder); + dstAmounts[0] = 2 * _amount; + + t = OpTokenAppGateway.TransferOrder({ + srcTokens: srcTokens, + dstTokens: dstTokens, + user: _ramu, + srcAmounts: srcAmounts, + dstAmounts: dstAmounts + }); + } + + function _mockEVMxCalls() private { + vm.mockCall( + evmxDetails.addressResolver, + abi.encodeWithSelector(IAddressResolver.deliveryHelper.selector), + abi.encode(evmxDetails.deliveryHelper) + ); + vm.mockCall( + evmxDetails.deliveryHelper, + abi.encodeWithSelector(IMiddleware.clearQueue.selector), + abi.encode(true) + ); + vm.mockCall( + evmxDetails.addressResolver, + abi.encodeWithSelector(IAddressResolver.clearPromises.selector), + abi.encode(true) + ); + vm.mockCall( + evmxDetails.chainDetails[0].forwarder, + abi.encodeWithSelector(IOpToken.burn.selector), + abi.encode(true) + ); + vm.mockCall( + evmxDetails.chainDetails[1].forwarder, + abi.encodeWithSelector(IOpToken.burn.selector), + abi.encode(true) + ); + vm.mockCall( + evmxDetails.chainDetails[2].forwarder, + abi.encodeWithSelector(IOpToken.mint.selector), + abi.encode(true) + ); + vm.mockCall( + evmxDetails.addressResolver, + abi.encodeWithSelector(IAddressResolver.watcherPrecompile__.selector), + abi.encode(evmxDetails.watcherPrecompile) + ); + vm.mockCall( + evmxDetails.watcherPrecompile, + abi.encodeWithSelector(IWatcherPrecompile.getCurrentRequestCount.selector), + abi.encode(_requestCount) + ); + vm.mockCall( + evmxDetails.deliveryHelper, + abi.encodeWithSelector(IMiddleware.batch.selector), + abi.encode(_requestCount) + ); + + address[] memory promises = new address[](3); + promises[0] = evmxDetails.chainDetails[0].asyncPromise; + promises[1] = evmxDetails.chainDetails[1].asyncPromise; + promises[2] = evmxDetails.chainDetails[2].asyncPromise; + vm.mockCall( + evmxDetails.addressResolver, + abi.encodeWithSelector(IAddressResolver.getPromises.selector), + abi.encode(promises) + ); + } + + function _getPayloadExecDetails() private view returns (PayloadExecDetails[] memory) { + PayloadExecDetails[] memory p = new PayloadExecDetails[](3); + ExecuteParams memory executeParams = ExecuteParams({ + deadline: block.timestamp + 1000, + callType: CallType.WRITE, + writeFinality: WriteFinality.LOW, + gasLimit: 1000000, + readAt: 0, + payload: abi.encodeWithSelector(IOpToken.burn.selector, _ramu, _amount), + target: address(chainDetails[0].token), + requestCount: 0, + batchCount: 0, + payloadCount: 0, + prevDigestsHash: bytes32(0), + switchboard: address(chainDetails[0].switchboard) + }); + bytes32 payloadId = _createPayloadId(executeParams, chainDetails[0].socket.chainSlug()); + bytes32 digest = _createDigest( + _transmitter, + payloadId, + address(evmxDetails.appGateway), + executeParams + ); + bytes memory proof = _signDigest( + keccak256(abi.encode(chainDetails[0].switchboard, digest)), + _watcherPrivateKey + ); + bytes memory transmitterSignature = _signDigest( + keccak256(abi.encode(address(chainDetails[0].socket), payloadId)), + _transmitterPrivateKey + ); + p[0] = PayloadExecDetails({ + executeParams: executeParams, + previousPayloadIds: new bytes32[](0), + digest: digest, + payloadId: payloadId, + proof: proof, + transmitterSignature: transmitterSignature + }); + + executeParams = ExecuteParams({ + deadline: block.timestamp + 1000, + callType: CallType.WRITE, + writeFinality: WriteFinality.LOW, + gasLimit: 1000000, + readAt: 0, + payload: abi.encodeWithSelector(IOpToken.burn.selector, _ramu, _amount), + target: address(chainDetails[1].token), + requestCount: 0, + batchCount: 0, + payloadCount: 0, + prevDigestsHash: bytes32(0), + switchboard: address(chainDetails[1].switchboard) + }); + payloadId = _createPayloadId(executeParams, chainDetails[1].socket.chainSlug()); + digest = _createDigest( + _transmitter, + payloadId, + address(evmxDetails.appGateway), + executeParams + ); + proof = _signDigest( + keccak256(abi.encode(chainDetails[1].switchboard, digest)), + _watcherPrivateKey + ); + transmitterSignature = _signDigest( + keccak256(abi.encode(address(chainDetails[1].socket), payloadId)), + _transmitterPrivateKey + ); + p[1] = PayloadExecDetails({ + executeParams: executeParams, + previousPayloadIds: new bytes32[](0), + digest: digest, + payloadId: payloadId, + proof: proof, + transmitterSignature: transmitterSignature + }); + + executeParams = ExecuteParams({ + deadline: block.timestamp + 1000, + callType: CallType.WRITE, + writeFinality: WriteFinality.LOW, + gasLimit: 1000000, + readAt: 0, + payload: abi.encodeWithSelector(IOpToken.mint.selector, _ramu, 2 * _amount), + target: address(chainDetails[2].token), + requestCount: 0, + batchCount: 0, + payloadCount: 0, + prevDigestsHash: keccak256( + abi.encodePacked(keccak256(abi.encodePacked(bytes32(0), p[0].digest)), p[1].digest) + ), + switchboard: address(chainDetails[2].switchboard) + }); + payloadId = _createPayloadId(executeParams, chainDetails[2].socket.chainSlug()); + digest = _createDigest( + _transmitter, + payloadId, + address(evmxDetails.appGateway), + executeParams + ); + proof = _signDigest( + keccak256(abi.encode(chainDetails[2].switchboard, digest)), + _watcherPrivateKey + ); + transmitterSignature = _signDigest( + keccak256(abi.encode(address(chainDetails[2].socket), payloadId)), + _transmitterPrivateKey + ); + bytes32[] memory previousPayloadIds = new bytes32[](2); + previousPayloadIds[0] = p[0].payloadId; + previousPayloadIds[1] = p[1].payloadId; + p[2] = PayloadExecDetails({ + executeParams: executeParams, + previousPayloadIds: previousPayloadIds, + digest: digest, + payloadId: payloadId, + proof: proof, + transmitterSignature: transmitterSignature + }); + return p; + } + + function _signDigest(bytes32 digest_, uint256 privateKey_) private pure returns (bytes memory) { + digest_ = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey_, digest_); + return abi.encodePacked(r, s, v); + } + + function _createDigest( + address transmitter_, + bytes32 payloadId_, + address appGateway_, + ExecuteParams memory executeParams_ + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + transmitter_, + payloadId_, + executeParams_.deadline, + executeParams_.callType, + executeParams_.writeFinality, + executeParams_.gasLimit, + msg.value, + executeParams_.readAt, + executeParams_.payload, + executeParams_.target, + appGateway_, + executeParams_.prevDigestsHash + ) + ); + } + + function _createPayloadId( + ExecuteParams memory executeParams_, + uint32 chainSlug_ + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + executeParams_.requestCount, + executeParams_.batchCount, + executeParams_.payloadCount, + executeParams_.switchboard, + chainSlug_ + ) + ); + } +} diff --git a/test/apps/app-gateways/op-token/OpToken.sol b/test/apps/app-gateways/op-token/OpToken.sol new file mode 100644 index 00000000..9baa78fd --- /dev/null +++ b/test/apps/app-gateways/op-token/OpToken.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "solady/tokens/ERC20.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import "../../../../contracts/base/PlugBase.sol"; + +/** + * @title OpToken + * @notice An ERC20 contract which enables bridging a token to its sibling chains. + */ +contract OpToken is ERC20, Ownable, PlugBase { + string private _name; + string private _symbol; + uint8 private _decimals; + mapping(address => uint256) public lockedTokens; + + error InvalidSender(); + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address initialSupplyHolder_, + uint256 initialSupply_ + ) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _mint(initialSupplyHolder_, initialSupply_); + } + + function mint(address receiver_, uint256 amount_) external onlySocket { + _mint(receiver_, amount_); + } + + function burn(address user_, uint256 amount_) external onlySocket { + _burn(user_, amount_); + } + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + function setSocket(address newSocket_) external onlyOwner { + _setSocket(newSocket_); + } + + function setOwner(address owner_) external { + if (owner() != address(0) && owner() != msg.sender) revert InvalidSender(); + _initializeOwner(owner_); + } +} diff --git a/test/apps/app-gateways/op-token/OpTokenAppGateway.sol b/test/apps/app-gateways/op-token/OpTokenAppGateway.sol new file mode 100644 index 00000000..a1308082 --- /dev/null +++ b/test/apps/app-gateways/op-token/OpTokenAppGateway.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.21; + +import "solady/auth/Ownable.sol"; +import "../../../../contracts/base/AppGatewayBase.sol"; +import "./IOpToken.sol"; +import "./OpToken.sol"; + +contract OpTokenAppGateway is AppGatewayBase, Ownable { + bytes32 public opToken = _createContractId("opToken"); + event Transferred(uint40 requestCount); + + struct ConstructorParams { + string name_; + string symbol_; + uint8 decimals_; + address initialSupplyHolder_; + uint256 initialSupply_; + } + + struct TransferOrder { + address[] srcTokens; + address[] dstTokens; + address user; + uint256[] srcAmounts; + uint256[] dstAmounts; + } + + constructor( + address addressResolver_, + address owner_, + Fees memory fees_, + ConstructorParams memory params_ + ) AppGatewayBase(addressResolver_) { + creationCodeWithArgs[opToken] = abi.encodePacked( + type(OpToken).creationCode, + abi.encode( + params_.name_, + params_.symbol_, + params_.decimals_, + params_.initialSupplyHolder_, + params_.initialSupply_ + ) + ); + + // sets the fees data like max fees, chain and token for all transfers + // they can be updated for each transfer as well + _setOverrides(fees_); + _initializeOwner(owner_); + } + + function deployContracts(uint32 chainSlug_) external async { + bytes memory initData = abi.encodeWithSelector(OpToken.setOwner.selector, owner()); + _deploy(opToken, chainSlug_, IsPlug.YES, initData); + } + + // no need to call this directly, will be called automatically after all contracts are deployed. + // check AppGatewayBase._deploy and AppGatewayBase.onRequestComplete + function initialize(uint32) public pure override { + return; + } + + function transfer(bytes memory order_) external async { + TransferOrder memory order = abi.decode(order_, (TransferOrder)); + for (uint256 i = 0; i < order.srcTokens.length; i++) { + IOpToken(order.srcTokens[i]).burn(order.user, order.srcAmounts[i]); + } + for (uint256 i = 0; i < order.dstTokens.length; i++) { + IOpToken(order.dstTokens[i]).mint(order.user, order.dstAmounts[i]); + } + + emit Transferred(_getCurrentAsyncId()); + } +}