diff --git a/.gitmodules b/.gitmodules index 80c46c6e3..8bb6593d2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "evm/lib/solidity-bytes-utils"] path = evm/lib/solidity-bytes-utils url = https://github.com/GNSPS/solidity-bytes-utils +[submodule "evm/lib/example-messaging-executor"] + path = evm/lib/example-messaging-executor + url = https://github.com/wormholelabs-xyz/example-messaging-executor diff --git a/evm/lib/example-messaging-executor b/evm/lib/example-messaging-executor new file mode 160000 index 000000000..e6c471e98 --- /dev/null +++ b/evm/lib/example-messaging-executor @@ -0,0 +1 @@ +Subproject commit e6c471e98814977b871bf6f1788a6b3ec19bb301 diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index 8a336fccc..e0a8a54c3 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -13,11 +13,13 @@ import "../libraries/Implementation.sol"; import "../interfaces/ITransceiver.sol"; import "../interfaces/IManagerBase.sol"; +import "../interfaces/IMsgReceiver.sol"; import "./TransceiverRegistry.sol"; abstract contract ManagerBase is IManagerBase, + IMsgReceiver, TransceiverRegistry, PausableOwnable, ReentrancyGuardUpgradeable, @@ -464,6 +466,85 @@ abstract contract ManagerBase is _getMessageSequenceStorage().num++; } + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory payload + ) external onlyTransceiver whenNotPaused { + _verifyPeer(sourceChainId, sourceManagerAddress); + + // Compute manager message digest and record transceiver attestation. + bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); + + if (isMessageApproved(nttManagerMessageHash)) { + executeMsg(sourceChainId, sourceManagerAddress, payload); + } + } + + function executeMsg( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.NttManagerMessage memory message + ) public whenNotPaused { + (bytes32 digest, bool alreadyExecuted) = + _isMessageExecuted(sourceChainId, sourceNttManagerAddress, message); + + if (alreadyExecuted) { + return; + } + + _handleMsg(sourceChainId, sourceNttManagerAddress, message, digest); + } + + /// @dev Override this function to handle your messages. + function _handleMsg( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory message, + bytes32 digest + ) internal virtual {} + + function _sendMessage( + uint64 sequence, + uint16 recipientChain, + bytes32 recipientManagerAddress, + bytes32 refundAddress, + address sender, + bytes memory payload, + bytes memory transceiverInstructions + ) internal returns (uint256 totalPriceQuote, bytes memory encodedNttManagerPayload) { + // verify chain has not forked + checkFork(evmChainId); + + address[] memory enabledTransceivers; + TransceiverStructs.TransceiverInstruction[] memory instructions; + uint256[] memory priceQuotes; + (enabledTransceivers, instructions, priceQuotes, totalPriceQuote) = + _prepareForTransfer(recipientChain, transceiverInstructions); + (recipientChain, transceiverInstructions); + + // construct the NttManagerMessage payload + encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage( + TransceiverStructs.NttManagerMessage( + bytes32(uint256(sequence)), toWormholeFormat(sender), payload + ) + ); + + // send the message + _sendMessageToTransceivers( + recipientChain, + refundAddress, + recipientManagerAddress, + priceQuotes, + instructions, + enabledTransceivers, + encodedNttManagerPayload + ); + } + + /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view virtual; + /// ============== Invariants ============================================= /// @dev When we add new immutables, this function should be updated diff --git a/evm/src/NttManager/MsgManager.sol b/evm/src/NttManager/MsgManager.sol new file mode 100644 index 000000000..2416c2687 --- /dev/null +++ b/evm/src/NttManager/MsgManager.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "wormhole-solidity-sdk/Utils.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "../interfaces/IMsgManager.sol"; +import "../interfaces/ITransceiver.sol"; +import "../libraries/TransceiverHelpers.sol"; + +import {ManagerBase} from "./ManagerBase.sol"; + +contract MsgManager is IMsgManager, ManagerBase { + string public constant MSG_MANAGER_VERSION = "1.0.0"; + + // =============== Setup ================================================================= + + constructor( + uint16 _chainId + ) ManagerBase(address(0), Mode.LOCKING, _chainId) {} + + function _initialize() internal virtual override { + _init(); + _checkThresholdInvariants(); + _checkTransceiversInvariants(); + } + + function _init() internal onlyInitializing { + // check if the owner is the deployer of this contract + if (msg.sender != deployer) { + revert UnexpectedDeployer(deployer, msg.sender); + } + if (msg.value != 0) { + revert UnexpectedMsgValue(); + } + __PausedOwnable_init(msg.sender, msg.sender); + __ReentrancyGuard_init(); + } + + // =============== Storage ============================================================== + + bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("mmgr.peers")) - 1); + + // =============== Storage Getters/Setters ============================================== + + function _getPeersStorage() + internal + pure + returns (mapping(uint16 => MsgManagerPeer) storage $) + { + uint256 slot = uint256(PEERS_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + + // =============== Public Getters ======================================================== + + /// @inheritdoc IMsgManager + function getPeer( + uint16 chainId_ + ) external view returns (MsgManagerPeer memory) { + return _getPeersStorage()[chainId_]; + } + + // =============== Admin ============================================================== + + /// @inheritdoc IMsgManager + function setPeer(uint16 peerChainId, bytes32 peerAddress) public onlyOwner { + if (peerChainId == 0) { + revert InvalidPeerChainIdZero(); + } + if (peerAddress == bytes32(0)) { + revert InvalidPeerZeroAddress(); + } + if (peerChainId == chainId) { + revert InvalidPeerSameChainId(); + } + + MsgManagerPeer memory oldPeer = _getPeersStorage()[peerChainId]; + + _getPeersStorage()[peerChainId].peerAddress = peerAddress; + + emit PeerUpdated(peerChainId, oldPeer.peerAddress, peerAddress); + } + + /// ============== Invariants ============================================= + + /// @dev When we add new immutables, this function should be updated + function _checkImmutables() internal view virtual override { + super._checkImmutables(); + } + + // ==================== External Interface =============================================== + + /// @inheritdoc IMsgManager + function sendMessage( + uint16 recipientChain, + bytes32 refundAddress, + bytes calldata payload, + bytes memory transceiverInstructions + ) external payable nonReentrant whenNotPaused returns (uint64 sequence) { + sequence = _useMessageSequence(); + + bytes32 recipientAddress = _getPeersStorage()[recipientChain].peerAddress; + + (uint256 totalPriceQuote, bytes memory encodedNttManagerPayload) = _sendMessage( + sequence, + recipientChain, + recipientAddress, + refundAddress, + msg.sender, + payload, + transceiverInstructions + ); + + emit MessageSent( + recipientChain, recipientAddress, sequence, totalPriceQuote, encodedNttManagerPayload + ); + } + + /// @dev Override this function to handle your messages. + function _handleMsg( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory message, + bytes32 digest + ) internal virtual override {} + + // ==================== Internal Helpers =============================================== + + /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view override { + if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { + revert InvalidPeer(sourceChainId, peerAddress); + } + } +} diff --git a/evm/src/NttManager/MsgManagerWithExecutor.sol b/evm/src/NttManager/MsgManagerWithExecutor.sol new file mode 100644 index 000000000..196b64e50 --- /dev/null +++ b/evm/src/NttManager/MsgManagerWithExecutor.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "example-messaging-executor/evm/src/interfaces/IExecutor.sol"; +import "example-messaging-executor/evm/src/libraries/ExecutorMessages.sol"; +import "wormhole-solidity-sdk/Utils.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "../interfaces/IMsgManagerWithExecutor.sol"; +import "../interfaces/ITransceiver.sol"; +import "../libraries/TransceiverHelpers.sol"; + +import {ManagerBase} from "./ManagerBase.sol"; + +contract MsgManagerWithExecutor is IMsgManagerWithExecutor, ManagerBase { + string public constant MSG_MANAGER_VERSION = "1.0.0"; + + IExecutor public immutable executor; + + // =============== Setup ================================================================= + + constructor( + uint16 _chainId, + address _executor + ) ManagerBase(address(0), Mode.LOCKING, _chainId) { + assert(_executor != address(0)); + executor = IExecutor(_executor); + } + + function _initialize() internal virtual override { + _init(); + _checkThresholdInvariants(); + _checkTransceiversInvariants(); + } + + function _init() internal onlyInitializing { + // check if the owner is the deployer of this contract + if (msg.sender != deployer) { + revert UnexpectedDeployer(deployer, msg.sender); + } + if (msg.value != 0) { + revert UnexpectedMsgValue(); + } + __PausedOwnable_init(msg.sender, msg.sender); + __ReentrancyGuard_init(); + } + + // =============== Storage ============================================================== + + bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("mmgr.peers")) - 1); + + // =============== Storage Getters/Setters ============================================== + + function _getPeersStorage() + internal + pure + returns (mapping(uint16 => MsgManagerPeer) storage $) + { + uint256 slot = uint256(PEERS_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + + // =============== Public Getters ======================================================== + + /// @inheritdoc IMsgManagerWithExecutor + function getPeer( + uint16 chainId_ + ) external view returns (MsgManagerPeer memory) { + return _getPeersStorage()[chainId_]; + } + + // =============== Admin ============================================================== + + /// @inheritdoc IMsgManagerWithExecutor + function setPeer(uint16 peerChainId, bytes32 peerAddress) public onlyOwner { + if (peerChainId == 0) { + revert InvalidPeerChainIdZero(); + } + if (peerAddress == bytes32(0)) { + revert InvalidPeerZeroAddress(); + } + if (peerChainId == chainId) { + revert InvalidPeerSameChainId(); + } + + MsgManagerPeer memory oldPeer = _getPeersStorage()[peerChainId]; + + _getPeersStorage()[peerChainId].peerAddress = peerAddress; + + emit PeerUpdated(peerChainId, oldPeer.peerAddress, peerAddress); + } + + /// ============== Invariants ============================================= + + /// @dev When we add new immutables, this function should be updated + function _checkImmutables() internal view virtual override { + super._checkImmutables(); + } + + // ==================== External Interface =============================================== + + /// @inheritdoc IMsgManagerWithExecutor + function sendMessage( + uint16 recipientChain, + bytes32 refundAddress, + bytes calldata payload, + bytes memory transceiverInstructions, + ExecutorArgs calldata executorArgs + ) external payable nonReentrant whenNotPaused returns (uint64 sequence) { + sequence = _useMessageSequence(); + + bytes32 recipientAddress = _getPeersStorage()[recipientChain].peerAddress; + + (uint256 totalPriceQuote,) = _sendMessage( + sequence, + recipientChain, + recipientAddress, + refundAddress, + msg.sender, + payload, + transceiverInstructions + ); + + if (totalPriceQuote + executorArgs.value > msg.value) { + revert InsufficientMsgValue(msg.value, totalPriceQuote, executorArgs.value); + } + + // emit MessageSent(recipientChain, recipientAddress, sequence, totalPriceQuote); + + // Generate the executor event. + // TODO: Not sure we want to use `makeNTTv1Request` since it doesn't have the payload. + executor.requestExecution{value: executorArgs.value}( + recipientChain, + recipientAddress, + executorArgs.refundAddress, + executorArgs.signedQuote, + ExecutorMessages.makeNTTv1Request( + chainId, bytes32(uint256(uint160(address(this)))), bytes32(uint256(sequence)) + ), + executorArgs.instructions + ); + } + + /// @dev Override this function to handle your messages. + function _handleMsg( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory message, + bytes32 digest + ) internal virtual override {} + + // ==================== Internal Helpers =============================================== + + /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view override { + if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { + revert InvalidPeer(sourceChainId, peerAddress); + } + } +} diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 7ac021b91..2fbe3b8f4 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -181,38 +181,6 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { ); } - /// @inheritdoc INttManager - function attestationReceived( - uint16 sourceChainId, - bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory payload - ) external onlyTransceiver whenNotPaused { - _verifyPeer(sourceChainId, sourceNttManagerAddress); - - // Compute manager message digest and record transceiver attestation. - bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); - - if (isMessageApproved(nttManagerMessageHash)) { - executeMsg(sourceChainId, sourceNttManagerAddress, payload); - } - } - - /// @inheritdoc INttManager - function executeMsg( - uint16 sourceChainId, - bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory message - ) public whenNotPaused { - (bytes32 digest, bool alreadyExecuted) = - _isMessageExecuted(sourceChainId, sourceNttManagerAddress, message); - - if (alreadyExecuted) { - return; - } - - _handleMsg(sourceChainId, sourceNttManagerAddress, message, digest); - } - /// @dev Override this function to handle custom NttManager payloads. /// This can also be used to customize transfer logic by using your own /// _handleTransfer implementation. @@ -221,7 +189,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { bytes32 sourceNttManagerAddress, TransceiverStructs.NttManagerMessage memory message, bytes32 digest - ) internal virtual { + ) internal virtual override { _handleTransfer(sourceChainId, sourceNttManagerAddress, message, digest); } @@ -527,55 +495,32 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { address sender, bytes memory transceiverInstructions ) internal returns (uint64 msgSequence) { - // verify chain has not forked - checkFork(evmChainId); - - ( - address[] memory enabledTransceivers, - TransceiverStructs.TransceiverInstruction[] memory instructions, - uint256[] memory priceQuotes, - uint256 totalPriceQuote - ) = _prepareForTransfer(recipientChain, transceiverInstructions); - - // push it on the stack again to avoid a stack too deep error - uint64 seq = sequence; - TransceiverStructs.NativeTokenTransfer memory ntt = _prepareNativeTokenTransfer( - amount, recipient, recipientChain, seq, sender, refundAddress - ); - - // construct the NttManagerMessage payload - bytes memory encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage( - TransceiverStructs.NttManagerMessage( - bytes32(uint256(seq)), - toWormholeFormat(sender), - TransceiverStructs.encodeNativeTokenTransfer(ntt) - ) + amount, recipient, recipientChain, sequence, sender, refundAddress ); - // push onto the stack again to avoid stack too deep error - uint16 destinationChain = recipientChain; - - // send the message - _sendMessageToTransceivers( + (uint256 totalPriceQuote, bytes memory encodedNttManagerPayload) = _sendMessage( + sequence, recipientChain, + _getPeersStorage()[recipientChain].peerAddress, refundAddress, - _getPeersStorage()[destinationChain].peerAddress, - priceQuotes, - instructions, - enabledTransceivers, - encodedNttManagerPayload + sender, + TransceiverStructs.encodeNativeTokenTransfer(ntt), + transceiverInstructions ); // push it on the stack again to avoid a stack too deep error TrimmedAmount amt = amount; + // push it on the stack again to avoid a stack too deep error + uint64 seq = sequence; + emit TransferSent( recipient, refundAddress, amt.untrim(tokenDecimals()), totalPriceQuote, - destinationChain, + recipientChain, seq ); @@ -654,7 +599,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { // ==================== Internal Helpers =============================================== /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. - function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view { + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view override { if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { revert InvalidPeer(sourceChainId, peerAddress); } diff --git a/evm/src/Transceiver/Transceiver.sol b/evm/src/Transceiver/Transceiver.sol index d0dfa2f7f..bb8e52d8f 100644 --- a/evm/src/Transceiver/Transceiver.sol +++ b/evm/src/Transceiver/Transceiver.sol @@ -8,6 +8,7 @@ import "../libraries/PausableOwnable.sol"; import "../libraries/external/ReentrancyGuardUpgradeable.sol"; import "../libraries/Implementation.sol"; +import "../interfaces/IMsgReceiver.sol"; import "../interfaces/INttManager.sol"; import "../interfaces/ITransceiver.sol"; @@ -153,7 +154,7 @@ abstract contract Transceiver is toWormholeFormat(nttManager), recipientNttManagerAddress ); } - INttManager(nttManager).attestationReceived(sourceChainId, sourceNttManagerAddress, payload); + IMsgReceiver(nttManager).attestationReceived(sourceChainId, sourceNttManagerAddress, payload); } function _quoteDeliveryPrice( diff --git a/evm/src/interfaces/IMsgManager.sol b/evm/src/interfaces/IMsgManager.sol new file mode 100644 index 000000000..670dc02fe --- /dev/null +++ b/evm/src/interfaces/IMsgManager.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "../libraries/TrimmedAmount.sol"; +import "../libraries/TransceiverStructs.sol"; + +import "./IManagerBase.sol"; + +interface IMsgManager is IManagerBase { + /// @dev The peer on another chain. + struct MsgManagerPeer { + bytes32 peerAddress; + } + + /// @notice Emitted when the peer contract is updated. + /// @dev Topic0 + /// TODO. + /// @param chainId_ The chain ID of the peer contract. + /// @param oldPeerContract The old peer contract address. + /// @param peerContract The new peer contract address. + event PeerUpdated(uint16 indexed chainId_, bytes32 oldPeerContract, bytes32 peerContract); + + /// @notice Emitted when a message is sent from the manager. + /// @dev Topic0 + /// 0xe54e51e42099622516fa3b48e9733581c9dbdcb771cafb093f745a0532a35982. + /// @param recipientChain The chain ID of the recipient. + /// @param recipientAddress The recipient of the message. + /// @param sequence The unique sequence ID of the message. + /// @param fee The amount of ether sent along with the tx to cover the delivery fee. + /// @param payload The payload of the message. + event MessageSent( + uint16 recipientChain, + bytes32 indexed recipientAddress, + uint64 sequence, + uint256 fee, + bytes payload + ); + + /// @notice Error when trying to execute a message on an unintended target chain. + /// @dev Selector 0x3dcb204a. + /// @param targetChain The target chain. + /// @param thisChain The current chain. + error InvalidTargetChain(uint16 targetChain, uint16 thisChain); + + /// @notice Peer for the chain does not match the configuration. + /// @param chainId ChainId of the source chain. + /// @param peerAddress Address of the peer nttManager contract. + error InvalidPeer(uint16 chainId, bytes32 peerAddress); + + /// @notice Peer chain ID cannot be zero. + error InvalidPeerChainIdZero(); + + /// @notice Peer cannot be the zero address. + error InvalidPeerZeroAddress(); + + /// @notice Peer cannot be on the same chain + /// @dev Selector 0x20371f2a. + error InvalidPeerSameChainId(); + + /// @notice The caller is not the deployer. + error UnexpectedDeployer(address expectedOwner, address owner); + + /// @notice An unexpected msg.value was passed with the call + /// @dev Selector 0xbd28e889. + error UnexpectedMsgValue(); + + /// @notice Sends a message to the remote peer on the specified recipient chain. + /// @dev This function enforces attestation threshold and replay logic for messages. Once all + /// validations are complete, this function calls `executeMsg` to execute the command specified + /// by the message. + /// @param recipientChain The Wormhole chain id of the recipient. + /// @param refundAddress The refund address on the recipient chain. + /// @param transceiverInstructions Instructions to be passed to the transceiver, if any. + /// @param payload The message to be sent. + function sendMessage( + uint16 recipientChain, + bytes32 refundAddress, + bytes calldata payload, + bytes memory transceiverInstructions + ) external payable returns (uint64); + + /// @notice Returns registered peer contract for a given chain. + /// @param chainId_ Wormhole chain ID. + function getPeer( + uint16 chainId_ + ) external view returns (MsgManagerPeer memory); + + /// @notice Sets the corresponding peer. + /// @dev The msgManager that executes the message sets the source msgManager as the peer. + /// @param peerChainId The Wormhole chain ID of the peer. + /// @param peerContract The address of the peer nttManager contract. + /// Set to zero if not needed. + function setPeer(uint16 peerChainId, bytes32 peerContract) external; +} diff --git a/evm/src/interfaces/IMsgManagerWithExecutor.sol b/evm/src/interfaces/IMsgManagerWithExecutor.sol new file mode 100644 index 000000000..ca8d07174 --- /dev/null +++ b/evm/src/interfaces/IMsgManagerWithExecutor.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "../libraries/TrimmedAmount.sol"; +import "../libraries/TransceiverStructs.sol"; + +import "./IManagerBase.sol"; + +interface IMsgManagerWithExecutor is IManagerBase { + /// @dev The peer on another chain. + struct MsgManagerPeer { + bytes32 peerAddress; + } + + /// @dev Instructions for the executor to perform relaying. + struct ExecutorArgs { + // The msg value to be passed into the Executor. + uint256 value; + // The refund address used by the Executor. + address refundAddress; + // The signed quote to be passed into the Executor. + bytes signedQuote; + // The relay instructions to be passed into the Executor. + bytes instructions; + } + + /// @notice Emitted when the peer contract is updated. + /// @dev Topic0 + /// TODO. + /// @param chainId_ The chain ID of the peer contract. + /// @param oldPeerContract The old peer contract address. + /// @param peerContract The new peer contract address. + event PeerUpdated(uint16 indexed chainId_, bytes32 oldPeerContract, bytes32 peerContract); + + /// @notice Emitted when a message is sent from the manager. + /// @dev Topic0 + /// 0xe54e51e42099622516fa3b48e9733581c9dbdcb771cafb093f745a0532a35982. + /// @param recipientChain The chain ID of the recipient. + /// @param recipientAddress The recipient of the message. + /// @param sequence The unique sequence ID of the message. + /// @param fee The amount of ether sent along with the tx to cover the delivery fee. + event MessageSent( + uint16 recipientChain, bytes32 indexed recipientAddress, uint64 sequence, uint256 fee + ); + + /// @notice Error when trying to execute a message on an unintended target chain. + /// @dev Selector 0x3dcb204a. + /// @param targetChain The target chain. + /// @param thisChain The current chain. + error InvalidTargetChain(uint16 targetChain, uint16 thisChain); + + /// @notice Peer for the chain does not match the configuration. + /// @param chainId ChainId of the source chain. + /// @param peerAddress Address of the peer nttManager contract. + error InvalidPeer(uint16 chainId, bytes32 peerAddress); + + /// @notice Peer chain ID cannot be zero. + error InvalidPeerChainIdZero(); + + /// @notice Peer cannot be the zero address. + error InvalidPeerZeroAddress(); + + /// @notice Peer cannot be on the same chain + /// @dev Selector 0x20371f2a. + error InvalidPeerSameChainId(); + + /// @notice The caller is not the deployer. + error UnexpectedDeployer(address expectedOwner, address owner); + + /// @notice An unexpected msg.value was passed with the call + /// @dev Selector 0xbd28e889. + error UnexpectedMsgValue(); + + /// @notice The message value is insufficient. + /// @dev Selector TODO. + error InsufficientMsgValue(uint256 value, uint256 totalPriceQuote, uint256 executorValue); + + /// @notice Sends a message to the remote peer on the specified recipient chain. + /// @dev This function enforces attestation threshold and replay logic for messages. Once all + /// validations are complete, this function calls `executeMsg` to execute the command specified + /// by the message. + /// @param recipientChain The Wormhole chain id of the recipient. + /// @param refundAddress The refund address on the recipient chain. + /// @param transceiverInstructions Instructions to be passed to the transceiver, if any. + /// @param payload The message to be sent. + function sendMessage( + uint16 recipientChain, + bytes32 refundAddress, + bytes calldata payload, + bytes memory transceiverInstructions, + ExecutorArgs calldata executorArgs + ) external payable returns (uint64); + + /// @notice Returns registered peer contract for a given chain. + /// @param chainId_ Wormhole chain ID. + function getPeer( + uint16 chainId_ + ) external view returns (MsgManagerPeer memory); + + /// @notice Sets the corresponding peer. + /// @dev The msgManager that executes the message sets the source msgManager as the peer. + /// @param peerChainId The Wormhole chain ID of the peer. + /// @param peerContract The address of the peer nttManager contract. + /// Set to zero if not needed. + function setPeer(uint16 peerChainId, bytes32 peerContract) external; +} diff --git a/evm/src/interfaces/IMsgReceiver.sol b/evm/src/interfaces/IMsgReceiver.sol new file mode 100644 index 000000000..edc72b1f7 --- /dev/null +++ b/evm/src/interfaces/IMsgReceiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "../libraries/TransceiverStructs.sol"; + +interface IMsgReceiver { + /// @notice Called by an Endpoint contract to deliver a verified attestation. + /// @dev This function enforces attestation threshold and replay logic for messages. Once all + /// validations are complete, this function calls `executeMsg` to execute the command specified + /// by the message. + /// @param sourceChainId The Wormhole chain id of the sender. + /// @param sourceNttManagerAddress The address of the sender's NTT Manager contract. + /// @param payload The VAA payload. + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.NttManagerMessage memory payload + ) external; +} diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 3ace687fe..f3ca215f4 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -200,33 +200,6 @@ interface INttManager is IManagerBase { bytes32 digest ) external; - /// @notice Called by an Endpoint contract to deliver a verified attestation. - /// @dev This function enforces attestation threshold and replay logic for messages. Once all - /// validations are complete, this function calls `executeMsg` to execute the command specified - /// by the message. - /// @param sourceChainId The Wormhole chain id of the sender. - /// @param sourceNttManagerAddress The address of the sender's NTT Manager contract. - /// @param payload The VAA payload. - function attestationReceived( - uint16 sourceChainId, - bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory payload - ) external; - - /// @notice Called after a message has been sufficiently verified to execute - /// the command in the message. This function will decode the payload - /// as an NttManagerMessage to extract the sequence, msgType, and other parameters. - /// @dev This function is exposed as a fallback for when an `Transceiver` is deregistered - /// when a message is in flight. - /// @param sourceChainId The Wormhole chain id of the sender. - /// @param sourceNttManagerAddress The address of the sender's nttManager contract. - /// @param message The message to execute. - function executeMsg( - uint16 sourceChainId, - bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory message - ) external; - /// @notice Returns the number of decimals of the token managed by the NttManager. /// @return decimals The number of decimals of the token. function tokenDecimals() external view returns (uint8); diff --git a/evm/test/MsgManager.t.sol b/evm/test/MsgManager.t.sol new file mode 100644 index 000000000..6e9728868 --- /dev/null +++ b/evm/test/MsgManager.t.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "wormhole-solidity-sdk/Utils.sol"; + +import "./libraries/TransceiverHelpers.sol"; +import "../src/interfaces/IMsgManager.sol"; +import "../src/interfaces/IMsgReceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "../src/NttManager/MsgManager.sol"; + +/// @dev MyMsgManager is what an integrator might implement. They would override _handleMsg(). +contract MyMsgManager is MsgManager { + struct Message { + uint16 sourceChainId; + bytes32 sourceManagerAddress; + bytes payload; + bytes32 digest; + } + + Message[] private messages; + + constructor( + uint16 chainId + ) MsgManager(chainId) {} + + function numMessages() public view returns (uint256) { + return messages.length; + } + + function getMessage( + uint256 idx + ) public view returns (Message memory) { + return messages[idx]; + } + + function _handleMsg( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory message, + bytes32 digest + ) internal virtual override { + messages.push(Message(sourceChainId, sourceManagerAddress, message.payload, digest)); + } +} + +contract MockTransceiver is Transceiver { + bytes4 constant TEST_TRANSCEIVER_PAYLOAD_PREFIX = 0x99455454; + + bytes[] private messages; + + constructor( + address nttManager + ) Transceiver(nttManager) {} + + function getTransceiverType() external pure override returns (string memory) { + return "dummy"; + } + + function _quoteDeliveryPrice( + uint16, /* recipientChain */ + TransceiverStructs.TransceiverInstruction memory /* transceiverInstruction */ + ) internal pure override returns (uint256) { + return 0; + } + + function _sendMessage( + uint16, /* recipientChain */ + uint256, /* deliveryPayment */ + address, /* caller */ + bytes32 recipientNttManagerAddress, + bytes32, /* refundAddres */ + TransceiverStructs.TransceiverInstruction memory, /* instruction */ + bytes memory nttManagerMessage + ) internal override { + bytes memory encodedEm; + (, encodedEm) = TransceiverStructs.buildAndEncodeTransceiverMessage( + TEST_TRANSCEIVER_PAYLOAD_PREFIX, + toWormholeFormat(address(nttManager)), + recipientNttManagerAddress, + nttManagerMessage, + new bytes(0) // TODO: encode instructions + ); + + messages.push(encodedEm); + } + + function receiveMessage(uint16 sourceChainId, bytes memory encodedMessage) external { + TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; + TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; + (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs + .parseTransceiverAndNttManagerMessage(TEST_TRANSCEIVER_PAYLOAD_PREFIX, encodedMessage); + + IMsgReceiver(fromWormholeFormat(parsedTransceiverMessage.recipientNttManagerAddress)) + .attestationReceived( + sourceChainId, parsedTransceiverMessage.sourceNttManagerAddress, parsedNttManagerMessage + ); + } + + function numMessages() public view returns (uint256) { + return messages.length; + } + + function getMessage( + uint256 idx + ) public view returns (bytes memory) { + return messages[idx]; + } +} + +contract TestMsgManager is Test { + MyMsgManager msgManager; + MyMsgManager peerMsgManager; + + uint16 constant chainId1 = 7; + uint16 constant chainId2 = 8; + + address user_A = address(0x123); + address user_B = address(0x456); + + bytes32 refundAddr = toWormholeFormat(address(0x789)); + + uint256 initialBlockTimestamp; + MockTransceiver transceiver; + MockTransceiver peerTransceiver; + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + MsgManager implementation = new MyMsgManager(chainId1); + msgManager = MyMsgManager(address(new ERC1967Proxy(address(implementation), ""))); + msgManager.initialize(); + transceiver = new MockTransceiver(address(msgManager)); + msgManager.setTransceiver(address(transceiver)); + + MsgManager peerImplementation = new MyMsgManager(chainId2); + peerMsgManager = MyMsgManager(address(new ERC1967Proxy(address(peerImplementation), ""))); + peerMsgManager.initialize(); + peerTransceiver = new MockTransceiver(address(peerMsgManager)); + peerMsgManager.setTransceiver(address(peerTransceiver)); + + msgManager.setPeer(chainId2, toWormholeFormat(address(peerMsgManager))); + peerMsgManager.setPeer(chainId1, toWormholeFormat(address(msgManager))); + } + + function testMsgManagerBasic() public { + vm.startPrank(user_A); + + bytes memory transceiverInstructions = encodeEmptyTransceiverInstructions(); + bytes memory payload1 = "Hi, Mom!"; + bytes memory payload2 = "Hello, World!"; + bytes memory payload3 = "Farewell, Cruel World!"; + uint64 s1 = msgManager.sendMessage(chainId2, refundAddr, payload1, transceiverInstructions); + uint64 s2 = msgManager.sendMessage(chainId2, refundAddr, payload2, transceiverInstructions); + uint64 s3 = msgManager.sendMessage(chainId2, refundAddr, payload3, transceiverInstructions); + vm.stopPrank(); + + // Verify our sequence number increases as expected. + assertEq(s1, 0); + assertEq(s2, 1); + assertEq(s3, 2); + + // Verify we sent the messages. + assertEq(transceiver.numMessages(), 3); + + // Receive and verify the first message. + bytes memory msg1 = transceiver.getMessage(0); + peerTransceiver.receiveMessage(chainId1, msg1); + assertEq(peerMsgManager.numMessages(), 1); + assertEq(keccak256(payload1), keccak256(peerMsgManager.getMessage(0).payload)); + + // Receive and verify the second message. + bytes memory msg2 = transceiver.getMessage(1); + peerTransceiver.receiveMessage(chainId1, msg2); + assertEq(peerMsgManager.numMessages(), 2); + assertEq(keccak256(payload2), keccak256(peerMsgManager.getMessage(1).payload)); + + // Receive and verify the third message. + bytes memory msg3 = transceiver.getMessage(2); + peerTransceiver.receiveMessage(chainId1, msg3); + assertEq(peerMsgManager.numMessages(), 3); + assertEq(keccak256(payload3), keccak256(peerMsgManager.getMessage(2).payload)); + } + + function testMsgManagerWithThreshold2() public { + // Add a second transceiver to the receiver and increase the threshold. + MockTransceiver peerTransceiver2 = new MockTransceiver(address(peerMsgManager)); + peerMsgManager.setTransceiver(address(peerTransceiver2)); + peerMsgManager.setThreshold(2); + + vm.startPrank(user_A); + + // Send a message. + bytes memory transceiverInstructions = encodeEmptyTransceiverInstructions(); + bytes memory payload = "Hi, Mom!"; + assertEq(msgManager.sendMessage(chainId2, refundAddr, payload, transceiverInstructions), 0); + assertEq(transceiver.numMessages(), 1); + + // Receive the message on the first transceiver and verify that the manager didn't receive it yet. + bytes memory msg1 = transceiver.getMessage(0); + peerTransceiver.receiveMessage(chainId1, msg1); + assertEq(peerMsgManager.numMessages(), 0); + + // Receive the message on the second transceiver and verify that the manager did now receive it. + peerTransceiver2.receiveMessage(chainId1, msg1); + assertEq(peerMsgManager.numMessages(), 1); + assertEq(keccak256(payload), keccak256(peerMsgManager.getMessage(0).payload)); + } + + function encodeEmptyTransceiverInstructions() internal pure returns (bytes memory) { + TransceiverStructs.TransceiverInstruction[] memory instructions = + new TransceiverStructs.TransceiverInstruction[](0); + return TransceiverStructs.encodeTransceiverInstructions(instructions); + } +} diff --git a/evm/test/MsgManagerWithExecutor.t.sol b/evm/test/MsgManagerWithExecutor.t.sol new file mode 100644 index 000000000..3807e2603 --- /dev/null +++ b/evm/test/MsgManagerWithExecutor.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "example-messaging-executor/evm/src/Executor.sol"; +import "example-messaging-executor/evm/src/interfaces/IExecutor.sol"; +import "wormhole-solidity-sdk/Utils.sol"; + +import "./libraries/TransceiverHelpers.sol"; +import "../src/interfaces/IMsgManagerWithExecutor.sol"; +import "../src/interfaces/IMsgReceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "../src/NttManager/MsgManagerWithExecutor.sol"; + +/// @dev MyMsgManagerWithExecutor is what an integrator might implement. They would override _handleMsg(). +contract MyMsgManagerWithExecutor is MsgManagerWithExecutor { + struct Message { + uint16 sourceChainId; + bytes32 sourceManagerAddress; + bytes payload; + bytes32 digest; + } + + Message[] private messages; + + constructor(uint16 chainId, address executor) MsgManagerWithExecutor(chainId, executor) {} + + function numMessages() public view returns (uint256) { + return messages.length; + } + + function getMessage( + uint256 idx + ) public view returns (Message memory) { + return messages[idx]; + } + + function _handleMsg( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory message, + bytes32 digest + ) internal virtual override { + messages.push(Message(sourceChainId, sourceManagerAddress, message.payload, digest)); + } +} + +contract MockExecutor is IExecutor { + struct Request { + uint16 dstChain; + bytes32 dstAddr; + address refundAddr; + bytes signedQuote; + bytes requestBytes; + bytes relayInstructions; + } + + uint16 public immutable chainId; + Request[] private requests; + + constructor( + uint16 _chainId + ) { + chainId = _chainId; + } + + // NOTE: This was copied from the tests in the executor repo. + function encodeSignedQuoteHeader( + Executor.SignedQuoteHeader memory signedQuote + ) public pure returns (bytes memory) { + return abi.encodePacked( + signedQuote.prefix, + signedQuote.quoterAddress, + signedQuote.payeeAddress, + signedQuote.srcChain, + signedQuote.dstChain, + signedQuote.expiryTime + ); + } + + function createSignedQuote( + uint16 dstChain + ) public view returns (bytes memory) { + return createSignedQuote(dstChain, 60); + } + + function createSignedQuote( + uint16 dstChain, + uint64 quoteLife + ) public view returns (bytes memory) { + Executor.SignedQuoteHeader memory signedQuote = IExecutor.SignedQuoteHeader({ + prefix: "EQ01", + quoterAddress: address(0), + payeeAddress: bytes32(0), + srcChain: chainId, + dstChain: dstChain, + expiryTime: uint64(block.timestamp + quoteLife) + }); + return encodeSignedQuoteHeader(signedQuote); + } + + function createExecutorInstructions() public pure returns (bytes memory) { + return new bytes(0); + } + + function createArgs( + uint16 dstChain + ) public view returns (IMsgManagerWithExecutor.ExecutorArgs memory args) { + args.refundAddress = msg.sender; + args.signedQuote = createSignedQuote(dstChain); + args.instructions = createExecutorInstructions(); + } + + function numRequests() public view returns (uint256) { + return requests.length; + } + + function geRequest( + uint256 idx + ) public view returns (Request memory) { + return requests[idx]; + } + + function requestExecution( + uint16 dstChain, + bytes32 dstAddr, + address refundAddr, + bytes calldata signedQuote, + bytes calldata requestBytes, + bytes calldata relayInstructions + ) external payable { + requests.push( + Request(dstChain, dstAddr, refundAddr, signedQuote, requestBytes, relayInstructions) + ); + } +} + +contract MockTransceiver is Transceiver { + bytes4 constant TEST_TRANSCEIVER_PAYLOAD_PREFIX = 0x99455454; + + bytes[] private messages; + + constructor( + address nttManager + ) Transceiver(nttManager) {} + + function getTransceiverType() external pure override returns (string memory) { + return "dummy"; + } + + function _quoteDeliveryPrice( + uint16, /* recipientChain */ + TransceiverStructs.TransceiverInstruction memory /* transceiverInstruction */ + ) internal pure override returns (uint256) { + return 0; + } + + function _sendMessage( + uint16, /* recipientChain */ + uint256, /* deliveryPayment */ + address, /* caller */ + bytes32 recipientNttManagerAddress, + bytes32, /* refundAddres */ + TransceiverStructs.TransceiverInstruction memory, /* instruction */ + bytes memory nttManagerMessage + ) internal override { + bytes memory encodedEm; + (, encodedEm) = TransceiverStructs.buildAndEncodeTransceiverMessage( + TEST_TRANSCEIVER_PAYLOAD_PREFIX, + toWormholeFormat(address(nttManager)), + recipientNttManagerAddress, + nttManagerMessage, + new bytes(0) // TODO: encode instructions + ); + + messages.push(encodedEm); + } + + function receiveMessage(uint16 sourceChainId, bytes memory encodedMessage) external { + TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; + TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; + (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs + .parseTransceiverAndNttManagerMessage(TEST_TRANSCEIVER_PAYLOAD_PREFIX, encodedMessage); + + IMsgReceiver(fromWormholeFormat(parsedTransceiverMessage.recipientNttManagerAddress)) + .attestationReceived( + sourceChainId, parsedTransceiverMessage.sourceNttManagerAddress, parsedNttManagerMessage + ); + } + + function numMessages() public view returns (uint256) { + return messages.length; + } + + function getMessage( + uint256 idx + ) public view returns (bytes memory) { + return messages[idx]; + } +} + +contract TestMsgManagerWithExecutor is Test { + MockExecutor executor; + MockExecutor peerExecutor; + MyMsgManagerWithExecutor msgManagerWithExecutor; + MyMsgManagerWithExecutor peerMsgManagerWithExecutor; + + uint16 constant chainId1 = 7; + uint16 constant chainId2 = 8; + + address user_A = address(0x123); + address user_B = address(0x456); + + bytes32 refundAddr = toWormholeFormat(address(0x789)); + + uint256 initialBlockTimestamp; + MockTransceiver transceiver; + MockTransceiver peerTransceiver; + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + executor = new MockExecutor(chainId1); + peerExecutor = new MockExecutor(chainId2); + + MsgManagerWithExecutor implementation = + new MyMsgManagerWithExecutor(chainId1, address(executor)); + msgManagerWithExecutor = + MyMsgManagerWithExecutor(address(new ERC1967Proxy(address(implementation), ""))); + msgManagerWithExecutor.initialize(); + transceiver = new MockTransceiver(address(msgManagerWithExecutor)); + msgManagerWithExecutor.setTransceiver(address(transceiver)); + + MsgManagerWithExecutor peerImplementation = + new MyMsgManagerWithExecutor(chainId2, address(peerExecutor)); + peerMsgManagerWithExecutor = + MyMsgManagerWithExecutor(address(new ERC1967Proxy(address(peerImplementation), ""))); + peerMsgManagerWithExecutor.initialize(); + peerTransceiver = new MockTransceiver(address(peerMsgManagerWithExecutor)); + peerMsgManagerWithExecutor.setTransceiver(address(peerTransceiver)); + + msgManagerWithExecutor.setPeer( + chainId2, toWormholeFormat(address(peerMsgManagerWithExecutor)) + ); + peerMsgManagerWithExecutor.setPeer( + chainId1, toWormholeFormat(address(msgManagerWithExecutor)) + ); + } + + function testMsgManagerWithExecutorBasic() public { + vm.startPrank(user_A); + + bytes memory transceiverInstructions = encodeEmptyTransceiverInstructions(); + bytes memory payload1 = "Hi, Mom!"; + bytes memory payload2 = "Hello, World!"; + bytes memory payload3 = "Farewell, Cruel World!"; + IMsgManagerWithExecutor.ExecutorArgs memory executorArgs = executor.createArgs(chainId2); + uint64 s1 = msgManagerWithExecutor.sendMessage( + chainId2, refundAddr, payload1, transceiverInstructions, executorArgs + ); + uint64 s2 = msgManagerWithExecutor.sendMessage( + chainId2, refundAddr, payload2, transceiverInstructions, executorArgs + ); + uint64 s3 = msgManagerWithExecutor.sendMessage( + chainId2, refundAddr, payload3, transceiverInstructions, executorArgs + ); + vm.stopPrank(); + + // Verify our sequence number increases as expected. + assertEq(s1, 0); + assertEq(s2, 1); + assertEq(s3, 2); + + // Verify we sent the messages. + assertEq(transceiver.numMessages(), 3); + + // Verify the executor received three requests. + assertEq(executor.numRequests(), 3); + + // Receive the first message on the transceiver. (The executor would do this. . .) + bytes memory msg1 = transceiver.getMessage(0); + peerTransceiver.receiveMessage(chainId1, msg1); + assertEq(peerMsgManagerWithExecutor.numMessages(), 1); + assertEq(keccak256(payload1), keccak256(peerMsgManagerWithExecutor.getMessage(0).payload)); + + // Receive and verify the second message. + bytes memory msg2 = transceiver.getMessage(1); + peerTransceiver.receiveMessage(chainId1, msg2); + assertEq(peerMsgManagerWithExecutor.numMessages(), 2); + assertEq(keccak256(payload2), keccak256(peerMsgManagerWithExecutor.getMessage(1).payload)); + + // Receive and verify the third message. + bytes memory msg3 = transceiver.getMessage(2); + peerTransceiver.receiveMessage(chainId1, msg3); + assertEq(peerMsgManagerWithExecutor.numMessages(), 3); + assertEq(keccak256(payload3), keccak256(peerMsgManagerWithExecutor.getMessage(2).payload)); + } + + function encodeEmptyTransceiverInstructions() internal pure returns (bytes memory) { + TransceiverStructs.TransceiverInstruction[] memory instructions = + new TransceiverStructs.TransceiverInstruction[](0); + return TransceiverStructs.encodeTransceiverInstructions(instructions); + } +}