From 3d799d73caa90895cd555e393591705719200733 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Fri, 25 Apr 2025 09:20:24 -0500 Subject: [PATCH 1/3] evm: Modular Messaging --- evm/NOTES.md | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 evm/NOTES.md diff --git a/evm/NOTES.md b/evm/NOTES.md new file mode 100644 index 000000000..66d82c676 --- /dev/null +++ b/evm/NOTES.md @@ -0,0 +1,115 @@ +# Notes on splitting token from base + +## MangerBase + +### Implements (Should it implement all these?) + +- IManagerBase, +- TransceiverRegistry, +- PausableOwnable, +- ReentrancyGuardUpgradeable, +- Implementation + +### Functionality Provided + +- Stores the following: + - Transceiver registry + - Thresholds + - Attestations + - Message sequence number +- Has the following functionality: + - `quoteDeliveryPrice` + - Record attestation + - Send message + +### Simple Changes Made + +- Moved the following from `ManagerBase` to `NttManager: + + - Token + - Mode + - `_prepareForTransfer` + +### Possible Ideas + +- Maybe we could [like an external library](https://book.getfoundry.sh/reference/forge/forge-create#linker-options) for admin functionality. + +### Contract Sizes + +#### Before we started + +```bash +evm (main)$ forge build --sizes --via-ir --skip test + +╭-----------------------------------------+------------------+-------------------+--------------------+---------------------╮ +| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) | ++===========================================================================================================================+ +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 24,066 | 25,673 | 510 | 23,479 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 17,141 | 18,557 | 7,435 | 30,595 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| TestManager | 8,981 | 10,259 | 15,595 | 38,893 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| + +``` + +- Note the `TestManager` just instantiates `ManagerBase.sol`. + +#### After simple changes + +```bash +evm (main)$ forge build --sizes --via-ir --skip test + +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 24,066 | 25,676 | 510 | 23,476 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 18,788 | 20,281 | 5,788 | 28,871 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| TestManager | 8,368 | 9,511 | 16,208 | 39,641 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| + +``` + +``` +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 23,907 | 25,510 | 669 | 23,642 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerHelpersLib | 58 | 87 | 24,518 | 49,065 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 18,644 | 20,130 | 5,932 | 29,022 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +``` + +- Question: Why did `NttManagerNoRateLimiting` grow so much?? + +#### Creating TransceiverRegistryAdmin + +#### Before + +```bash +evm (main)$ forge build --sizes --via-ir --skip test + +╭-----------------------------------------+------------------+-------------------+--------------------+---------------------╮ +| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) | ++===========================================================================================================================+ +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 24,066 | 25,673 | 510 | 23,479 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 17,141 | 18,557 | 7,435 | 30,595 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| + +``` + +#### After + +```bash +evm (main)$ forge build --sizes --via-ir --skip test + +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 23,220 | 26,937 | 1,356 | 22,215 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 16,254 | 19,713 | 8,322 | 29,439 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| + +``` From 228e8b003c99f85f15215b60dd4799bd391be4a0 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 28 Apr 2025 15:57:13 -0500 Subject: [PATCH 2/3] evm: Add MsgManager --- evm/NOTES.md | 31 ++++++ evm/src/NttManager/MsgManager.sol | 131 ++++++++++++++++++++++++++ evm/src/NttManager/MsgManagerBase.sol | 101 ++++++++++++++++++++ evm/src/NttManager/NttManager.sol | 87 +++-------------- evm/src/interfaces/IMsgManager.sol | 93 ++++++++++++++++++ evm/src/interfaces/IMsgReceiver.sol | 33 +++++++ evm/src/interfaces/INttManager.sol | 30 +----- 7 files changed, 406 insertions(+), 100 deletions(-) create mode 100644 evm/src/NttManager/MsgManager.sol create mode 100644 evm/src/NttManager/MsgManagerBase.sol create mode 100644 evm/src/interfaces/IMsgManager.sol create mode 100644 evm/src/interfaces/IMsgReceiver.sol diff --git a/evm/NOTES.md b/evm/NOTES.md index 66d82c676..8468e17ab 100644 --- a/evm/NOTES.md +++ b/evm/NOTES.md @@ -113,3 +113,34 @@ evm (main)$ forge build --sizes --via-ir --skip test |-----------------------------------------+------------------+-------------------+--------------------+---------------------| ``` + +#### Creating MsgManagerBase + +#### Before + +```bash +evm (main)$ forge build --sizes --via-ir --skip test + +╭-----------------------------------------+------------------+-------------------+--------------------+---------------------╮ +| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) | ++===========================================================================================================================+ +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 24,066 | 25,673 | 510 | 23,479 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 17,141 | 18,557 | 7,435 | 30,595 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| + +``` + +#### After + +```bash +evm (main)$ forge build --sizes --via-ir --skip test + +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManager | 24,076 | 25,719 | 500 | 23,433 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| +| NttManagerNoRateLimiting | 18,496 | 19,949 | 6,080 | 29,203 | +|-----------------------------------------+------------------+-------------------+--------------------+---------------------| + +``` diff --git a/evm/src/NttManager/MsgManager.sol b/evm/src/NttManager/MsgManager.sol new file mode 100644 index 000000000..a38822cae --- /dev/null +++ b/evm/src/NttManager/MsgManager.sol @@ -0,0 +1,131 @@ +// 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 {MsgManagerBase} from "./MsgManagerBase.sol"; + +contract MsgManager is IMsgManager, MsgManagerBase { + string public constant MSG_MANAGER_VERSION = "1.0.0"; + + // =============== Setup ================================================================= + + constructor( + uint16 _chainId + ) MsgManagerBase(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, + 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( + recipientChain, recipientAddress, sequence, 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/MsgManagerBase.sol b/evm/src/NttManager/MsgManagerBase.sol new file mode 100644 index 000000000..db01598e1 --- /dev/null +++ b/evm/src/NttManager/MsgManagerBase.sol @@ -0,0 +1,101 @@ +// 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/IMsgReceiver.sol"; +import "../interfaces/ITransceiver.sol"; +import "../libraries/TransceiverHelpers.sol"; + +import "./ManagerBase.sol"; + +abstract contract MsgManagerBase is ManagerBase, IMsgReceiver { + // =============== Setup ================================================================= + + constructor(address _token, Mode _mode, uint16 _chainId) ManagerBase(_token, _mode, _chainId) {} + + // ==================== External Interface =============================================== + + 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); + } + + // ==================== Internal Helpers =============================================== + + /// @dev Override this function to handle your messages. + function _handleMsg( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory message, + bytes32 digest + ) internal virtual {} + + function _sendMessage( + uint16 recipientChain, + bytes32 recipientManagerAddress, + uint64 sequence, + 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( + // TODO: Should we use `address(this)` instead of `msg.sender`? + bytes32(uint256(sequence)), + toWormholeFormat(msg.sender), + payload + ) + ); + + // send the message + _sendMessageToTransceivers( + recipientChain, + recipientManagerAddress, // 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; +} diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 7ac021b91..25cc241e8 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -13,7 +13,7 @@ import "../interfaces/INttManager.sol"; import "../interfaces/INttToken.sol"; import "../interfaces/ITransceiver.sol"; -import {ManagerBase} from "./ManagerBase.sol"; +import {MsgManagerBase} from "./MsgManagerBase.sol"; /// @title NttManager /// @author Wormhole Project Contributors. @@ -35,7 +35,7 @@ import {ManagerBase} from "./ManagerBase.sol"; /// to be too high, users will be refunded the difference. /// - (optional) a flag to indicate whether the transfer should be queued /// if the rate limit is exceeded -contract NttManager is INttManager, RateLimiter, ManagerBase { +contract NttManager is INttManager, RateLimiter, MsgManagerBase { using BytesParsing for bytes; using SafeERC20 for IERC20; using TrimmedAmountLib for uint256; @@ -51,7 +51,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { uint16 _chainId, uint64 _rateLimitDuration, bool _skipRateLimiting - ) RateLimiter(_rateLimitDuration, _skipRateLimiting) ManagerBase(_token, _mode, _chainId) {} + ) RateLimiter(_rateLimitDuration, _skipRateLimiting) MsgManagerBase(_token, _mode, _chainId) {} function __NttManager_init() internal onlyInitializing { // check if the owner is the deployer of this contract @@ -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,30 @@ 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( recipientChain, - refundAddress, - _getPeersStorage()[destinationChain].peerAddress, - priceQuotes, - instructions, - enabledTransceivers, - encodedNttManagerPayload + _getPeersStorage()[recipientChain].peerAddress, + sequence, + 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 +597,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/interfaces/IMsgManager.sol b/evm/src/interfaces/IMsgManager.sol new file mode 100644 index 000000000..75c1efb31 --- /dev/null +++ b/evm/src/interfaces/IMsgManager.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "../libraries/TrimmedAmount.sol"; +import "../libraries/TransceiverStructs.sol"; + +import "./IManagerBase.sol"; +import "./IMsgReceiver.sol"; + +interface IMsgManager is IManagerBase, IMsgReceiver { + /// @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 transceiverInstructions Instructions to be passed to the transceiver, if any. + /// @param payload The message to be sent. + function sendMessage( + uint16 recipientChain, + 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/IMsgReceiver.sol b/evm/src/interfaces/IMsgReceiver.sol new file mode 100644 index 000000000..94c570709 --- /dev/null +++ b/evm/src/interfaces/IMsgReceiver.sol @@ -0,0 +1,33 @@ +// 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; + + /// @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; +} diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 3ace687fe..1185acf69 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -5,8 +5,9 @@ import "../libraries/TrimmedAmount.sol"; import "../libraries/TransceiverStructs.sol"; import "./IManagerBase.sol"; +import "./IMsgReceiver.sol"; -interface INttManager is IManagerBase { +interface INttManager is IManagerBase, IMsgReceiver { /// @dev The peer on another chain. struct NttManagerPeer { bytes32 peerAddress; @@ -200,33 +201,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); From f37aebe526813fc71bc3dd57c1813aac585e4842 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 28 Apr 2025 16:03:29 -0500 Subject: [PATCH 3/3] forge install: example-messaging-executor --- .gitmodules | 3 + evm/lib/example-messaging-executor | 1 + evm/src/NttManager/MsgManagerBase.sol | 5 +- evm/src/NttManager/MsgManagerWithExecutor.sol | 155 +++++++++ .../interfaces/IMsgManagerWithExecutor.sol | 105 ++++++ evm/test/MsgManager.t.sol | 217 +++++++++++++ evm/test/MsgManagerWithExecutor.t.sol | 306 ++++++++++++++++++ 7 files changed, 788 insertions(+), 4 deletions(-) create mode 160000 evm/lib/example-messaging-executor create mode 100644 evm/src/NttManager/MsgManagerWithExecutor.sol create mode 100644 evm/src/interfaces/IMsgManagerWithExecutor.sol create mode 100644 evm/test/MsgManager.t.sol create mode 100644 evm/test/MsgManagerWithExecutor.t.sol 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/MsgManagerBase.sol b/evm/src/NttManager/MsgManagerBase.sol index db01598e1..e47d4020c 100644 --- a/evm/src/NttManager/MsgManagerBase.sol +++ b/evm/src/NttManager/MsgManagerBase.sol @@ -77,10 +77,7 @@ abstract contract MsgManagerBase is ManagerBase, IMsgReceiver { // construct the NttManagerMessage payload encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage( TransceiverStructs.NttManagerMessage( - // TODO: Should we use `address(this)` instead of `msg.sender`? - bytes32(uint256(sequence)), - toWormholeFormat(msg.sender), - payload + bytes32(uint256(sequence)), toWormholeFormat(msg.sender), payload ) ); diff --git a/evm/src/NttManager/MsgManagerWithExecutor.sol b/evm/src/NttManager/MsgManagerWithExecutor.sol new file mode 100644 index 000000000..d1a525e1e --- /dev/null +++ b/evm/src/NttManager/MsgManagerWithExecutor.sol @@ -0,0 +1,155 @@ +// 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 {MsgManagerBase} from "./MsgManagerBase.sol"; + +contract MsgManagerWithExecutor is IMsgManagerWithExecutor, MsgManagerBase { + string public constant MSG_MANAGER_VERSION = "1.0.0"; + + IExecutor public immutable executor; + + // =============== Setup ================================================================= + + constructor( + uint16 _chainId, + address _executor + ) MsgManagerBase(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, + 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( + recipientChain, recipientAddress, sequence, 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/interfaces/IMsgManagerWithExecutor.sol b/evm/src/interfaces/IMsgManagerWithExecutor.sol new file mode 100644 index 000000000..ba07726cf --- /dev/null +++ b/evm/src/interfaces/IMsgManagerWithExecutor.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "../libraries/TrimmedAmount.sol"; +import "../libraries/TransceiverStructs.sol"; + +import "./IManagerBase.sol"; +import "./IMsgReceiver.sol"; + +interface IMsgManagerWithExecutor is IManagerBase, IMsgReceiver { + /// @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 transceiverInstructions Instructions to be passed to the transceiver, if any. + /// @param payload The message to be sent. + function sendMessage( + uint16 recipientChain, + 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/test/MsgManager.t.sol b/evm/test/MsgManager.t.sol new file mode 100644 index 000000000..dfdad974a --- /dev/null +++ b/evm/test/MsgManager.t.sol @@ -0,0 +1,217 @@ +// 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/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); + + IMsgManager(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); + + 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, payload1, transceiverInstructions); + uint64 s2 = msgManager.sendMessage(chainId2, payload2, transceiverInstructions); + uint64 s3 = msgManager.sendMessage(chainId2, 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, 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..91e87605a --- /dev/null +++ b/evm/test/MsgManagerWithExecutor.t.sol @@ -0,0 +1,306 @@ +// 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/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); + + IMsgManagerWithExecutor( + 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); + + 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, payload1, transceiverInstructions, executorArgs + ); + uint64 s2 = msgManagerWithExecutor.sendMessage( + chainId2, payload2, transceiverInstructions, executorArgs + ); + uint64 s3 = msgManagerWithExecutor.sendMessage( + chainId2, 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); + } +}