From 6872fe6115be89f23365f7b5378afcce8eadc95e Mon Sep 17 00:00:00 2001 From: mdulin2 Date: Mon, 29 Jun 2026 14:06:52 -0700 Subject: [PATCH 1/4] Add peer check to the executeMsg --- evm/src/NttManager/NttManager.sol | 11 ++++- evm/test/NttManager.t.sol | 79 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 46276f3e1..9620c861d 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -196,7 +196,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); if (isMessageApproved(nttManagerMessageHash)) { - executeMsg(sourceChainId, sourceNttManagerAddress, payload); + executeMsgInner(sourceChainId, sourceNttManagerAddress, payload); } } @@ -206,6 +206,15 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { bytes32 sourceNttManagerAddress, TransceiverStructs.NttManagerMessage memory message ) public whenNotPaused { + _verifyPeer(sourceChainId, sourceNttManagerAddress); + executeMsgInner(sourceChainId, sourceNttManagerAddress, message); + } + + function executeMsgInner( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.NttManagerMessage memory message + ) private { (bytes32 digest, bool alreadyExecuted) = _isMessageExecuted(sourceChainId, sourceNttManagerAddress, message); diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index cb8e3acc9..a434ddc8b 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -237,6 +237,85 @@ contract TestNttManager is Test, IRateLimiterEvents { assertEq(nttManager.isPaused(), false); } + // === executeMsg() peer verification + // _verifyPeer must reject a message whose source manager address does not + // match the configured peer for that chain. + function test_executeMsg_revertsOnInvalidPeer() public { + // Register a known, valid peer for the sending chain. + bytes32 realPeer = toWormholeFormat(address(nttManagerOther)); + nttManager.setPeer( + TransceiverHelpersLib.SENDING_CHAIN_ID, realPeer, 9, type(uint64).max + ); + TransceiverStructs.NttManagerMessage memory message = + TransceiverStructs.NttManagerMessage(bytes32(0), realPeer, abi.encode("payload")); + + // Call executeMsg with a source manager address that does NOT match the peer. Expect revert. + bytes32 wrongPeer = toWormholeFormat(address(0x1337)); + vm.expectRevert( + abi.encodeWithSelector( + INttManager.InvalidPeer.selector, + TransceiverHelpersLib.SENDING_CHAIN_ID, + wrongPeer + ) + ); + nttManager.executeMsg(TransceiverHelpersLib.SENDING_CHAIN_ID, wrongPeer, message); + } + + // executeMsg successful call + function test_executeMsg_succeedsWithValidPeer() public { + address user_B = address(0x456); + DummyToken token = DummyToken(nttManager.token()); + TrimmedAmount amount = packTrimmedAmount(50, 8); + + // setUp registered `dummyTransceiver`. Add a second one and require both. + DummyTransceiver secondTransceiver = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(secondTransceiver)); + nttManager.setThreshold(2); + + bytes32 peer = toWormholeFormat(address(nttManagerOther)); + nttManager.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); + + // LOCKING mode: fund the manager so it can unlock to the recipient. + token.mintDummy(address(nttManager), amount.untrim(token.decimals())); + + bytes memory ntt = TransceiverStructs.encodeNativeTokenTransfer( + TransceiverStructs.NativeTokenTransfer({ + amount: amount, + sourceToken: toWormholeFormat(address(token)), + to: toWormholeFormat(user_B), + toChain: chainId, + additionalPayload: "" + }) + ); + TransceiverStructs.NttManagerMessage memory message; + bytes memory transceiverMessage; + (message, transceiverMessage) = TransceiverHelpersLib + .buildTransceiverMessageWithNttManagerPayload( + bytes32(uint256(1)), bytes32(0), peer, toWormholeFormat(address(nttManager)), ntt + ); + + // Not executed because of 2/2 threshold. + dummyTransceiver.receiveMessage(transceiverMessage); + bytes32 digest = TransceiverStructs.nttManagerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, message + ); + assertEq(nttManager.messageAttestations(digest), 1); + assertEq(nttManager.isMessageApproved(digest), false); + assertEq(nttManager.isMessageExecuted(digest), false); + assertEq(token.balanceOf(user_B), 0); + + // Lower the threshold to 1. Makes executable now. + nttManager.setThreshold(1); + assertEq(nttManager.isMessageApproved(digest), true); + assertEq(nttManager.isMessageExecuted(digest), false); + + // Fallback execution via executeMsg(): passes _verifyPeer and unlocks tokens. + nttManager.executeMsg(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, message); + + assertEq(nttManager.isMessageExecuted(digest), true); + assertEq(token.balanceOf(user_B), amount.untrim(token.decimals())); + } + function test_pausePauserUnpauseOnlyOwner() public { // transfer pauser to another address address pauser = address(0x123); From 609af69ab19c7ecdfdce9823250fab35678fc517 Mon Sep 17 00:00:00 2001 From: mdulin2 Date: Tue, 30 Jun 2026 11:53:46 -0700 Subject: [PATCH 2/4] Add comments about the sourceManagerAddress being the current peer and not necessarily the original sender. --- evm/src/NttManager/NttManager.sol | 5 ++++- evm/src/interfaces/INttManager.sol | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 9620c861d..915eab9d1 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -274,7 +274,10 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { /// @dev Override this function to process an additional payload on the NativeTokenTransfer /// For integrator flexibility, this function is *not* marked pure or view /// @param - The Wormhole chain id of the sender - /// @param - The address of the sender's NTT Manager contract. + /// @param - The address verified to equal the peer currently registered for the source chain — + /// not necessarily the manager that originally sent the message, since the peer may have + /// been changed while the message was in flight. Integrators must not treat this as the + /// authenticated original sender. /// @param - The message id from the NttManagerMessage. /// @param - The original message sender address from the NttManagerMessage. /// @param - The parsed NativeTokenTransfer, which includes the additionalPayload field diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 7d986ac8b..6b9c8f96f 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -217,9 +217,13 @@ interface INttManager is IManagerBase { /// 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. + /// when a message is in flight. It verifies that `sourceNttManagerAddress` matches the + /// peer currently registered for `sourceChainId` and reverts with `InvalidPeer` otherwise; + /// note this is the *current* peer, which may differ from the manager that originally + /// emitted the message if the peer has since been changed. /// @param sourceChainId The Wormhole chain id of the sender. - /// @param sourceNttManagerAddress The address of the sender's nttManager contract. + /// @param sourceNttManagerAddress The address expected to equal the peer currently registered + /// for `sourceChainId` — not necessarily the manager that originally sent the message. /// @param message The message to execute. function executeMsg( uint16 sourceChainId, From 22346005268f2bac1bec654456f06e28ba2e2c05 Mon Sep 17 00:00:00 2001 From: mdulin2 Date: Wed, 1 Jul 2026 09:16:34 -0700 Subject: [PATCH 3/4] Fix formatting issues in test --- evm/test/NttManager.t.sol | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index a434ddc8b..72849dc8e 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -243,9 +243,7 @@ contract TestNttManager is Test, IRateLimiterEvents { function test_executeMsg_revertsOnInvalidPeer() public { // Register a known, valid peer for the sending chain. bytes32 realPeer = toWormholeFormat(address(nttManagerOther)); - nttManager.setPeer( - TransceiverHelpersLib.SENDING_CHAIN_ID, realPeer, 9, type(uint64).max - ); + nttManager.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, realPeer, 9, type(uint64).max); TransceiverStructs.NttManagerMessage memory message = TransceiverStructs.NttManagerMessage(bytes32(0), realPeer, abi.encode("payload")); @@ -253,9 +251,7 @@ contract TestNttManager is Test, IRateLimiterEvents { bytes32 wrongPeer = toWormholeFormat(address(0x1337)); vm.expectRevert( abi.encodeWithSelector( - INttManager.InvalidPeer.selector, - TransceiverHelpersLib.SENDING_CHAIN_ID, - wrongPeer + INttManager.InvalidPeer.selector, TransceiverHelpersLib.SENDING_CHAIN_ID, wrongPeer ) ); nttManager.executeMsg(TransceiverHelpersLib.SENDING_CHAIN_ID, wrongPeer, message); @@ -289,12 +285,12 @@ contract TestNttManager is Test, IRateLimiterEvents { ); TransceiverStructs.NttManagerMessage memory message; bytes memory transceiverMessage; - (message, transceiverMessage) = TransceiverHelpersLib - .buildTransceiverMessageWithNttManagerPayload( - bytes32(uint256(1)), bytes32(0), peer, toWormholeFormat(address(nttManager)), ntt - ); + (message, transceiverMessage) = + TransceiverHelpersLib.buildTransceiverMessageWithNttManagerPayload( + bytes32(uint256(1)), bytes32(0), peer, toWormholeFormat(address(nttManager)), ntt + ); - // Not executed because of 2/2 threshold. + // Not executed because of 2/2 threshold. dummyTransceiver.receiveMessage(transceiverMessage); bytes32 digest = TransceiverStructs.nttManagerMessageDigest( TransceiverHelpersLib.SENDING_CHAIN_ID, message From 2505e0a34bd363f05c0b204d5a059929653f4527 Mon Sep 17 00:00:00 2001 From: mdulin2 Date: Wed, 1 Jul 2026 09:21:32 -0700 Subject: [PATCH 4/4] Fix EVM version --- evm/src/NttManager/NttManager.sol | 2 +- evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 915eab9d1..1fd23d673 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -41,7 +41,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { using TrimmedAmountLib for uint256; using TrimmedAmountLib for TrimmedAmount; - string public constant NTT_MANAGER_VERSION = "2.0.0"; + string public constant NTT_MANAGER_VERSION = "2.0.1"; // =============== Setup ================================================================= diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol index dbc00b579..79d4081aa 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol @@ -29,7 +29,7 @@ import "./WormholeTransceiverState.sol"; contract WormholeTransceiver is IWormholeTransceiver, WormholeTransceiverState { using BytesParsing for bytes; - string public constant WORMHOLE_TRANSCEIVER_VERSION = "2.0.0"; + string public constant WORMHOLE_TRANSCEIVER_VERSION = "2.0.1"; constructor( address nttManager,