diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 46276f3e1..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 ================================================================= @@ -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); @@ -265,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/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, 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, diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index cb8e3acc9..72849dc8e 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -237,6 +237,81 @@ 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);