diff --git a/evm/src/Transceiver/SharedTransceiver/SharedWormholeTransceiver.sol b/evm/src/Transceiver/SharedTransceiver/SharedWormholeTransceiver.sol new file mode 100644 index 000000000..d2c686879 --- /dev/null +++ b/evm/src/Transceiver/SharedTransceiver/SharedWormholeTransceiver.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "wormhole-solidity-sdk/Utils.sol"; + +import "../../interfaces/IMsgReceiver.sol"; +import "../../interfaces/ISharedWormholeTransceiver.sol"; + +string constant sharedWormholeTransceiverVersionString = "SharedWormholeTransceiver-0.0.1"; + +/// @title SharedWormholeTransceiver +/// +/// @author Wormhole Project Contributors. +/// +/// @notice The SharedWormholeTransceiver is a Wormhole transceiver implementation that can +/// be shared between multiple managers. It implements the ITransceiver interface. +/// +/// The SharedWormholeTransceiver assumes the use of the Executor at the manager level, +/// so it has no internal relayer support. It currently requires no transceiver instructions, +/// so the parameter required by the interface is not used. +/// +/// Since the ITransceiver interface requires NttManager and token addresses, this transceiver +/// implements those interfaces, but they are not used, and the values returned are zero. +/// +/// Since this transceiver is not owned by a specific manager, some of the interface +/// functions are stubbed off (transferring ownership, for instance). Additionally, +/// this transceiver is immutable, so the upgrade function reverts. +/// +/// This transceiver has an admin who is responsible for provisioning peers. There are +/// a number of admin functions for provisioning peers and transferring the admin. +/// Additionally, there is a function to discard the admin, making the contract +/// truly immutable. +/// +/// This transceiver maintains the Wormhole transceiver wire format, so in theory +/// it should be able to peer with instances of the standard `WormholeTransceiver`. +/// +contract SharedWormholeTransceiver is ISharedWormholeTransceiver { + using BytesParsing for bytes; // Used by _decodePayload + + // ==================== Constants ================================================ + // TODO: These are in `WormholeTranceiverState.sol` but I can't access them for some reason. + // TODO: Do we need to publish `WH_TRANSCEIVER_INIT_PREFIX` and `WH_PEER_REGISTRATION_PREFIX`? + + /// @dev Prefix for all TransceiverMessage payloads + /// @notice Magic string (constant value set by messaging provider) that idenfies the payload as an transceiver-emitted payload. + /// Note that this is not a security critical field. It's meant to be used by messaging providers to identify which messages are Transceiver-related. + bytes4 public constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; + + /// @dev Prefix for all Wormhole transceiver initialisation payloads + /// This is bytes4(keccak256("WormholeTransceiverInit")) + bytes4 constant WH_TRANSCEIVER_INIT_PREFIX = 0x9c23bd3b; + + /// @dev Prefix for all Wormhole peer registration payloads + /// This is bytes4(keccak256("WormholePeerRegistration")) + bytes4 constant WH_PEER_REGISTRATION_PREFIX = 0x18fc67c2; + + // ==================== Immutables =============================================== + + address public admin; + address public pendingAdmin; + uint16 public immutable ourChain; + IWormhole public immutable wormhole; + uint8 public immutable consistencyLevel; + + // ==================== Constructor ============================================== + + constructor(uint16 _ourChain, address _admin, address _wormhole, uint8 _consistencyLevel) { + assert(_ourChain != 0); + assert(_admin != address(0)); + assert(_wormhole != address(0)); + // Not checking consistency level since maybe zero is valid? + ourChain = _ourChain; + admin = _admin; + wormhole = IWormhole(_wormhole); + consistencyLevel = _consistencyLevel; + } + + // =============== Storage Keys ============================================= + + bytes32 private constant WORMHOLE_PEERS_SLOT = bytes32(uint256(keccak256("swt.peers")) - 1); + bytes32 private constant CHAINS_SLOT = bytes32(uint256(keccak256("swt.chains")) - 1); + + // =============== Storage Accessors ======================================== + + function _getPeersStorage() internal pure returns (mapping(uint16 => bytes32) storage $) { + uint256 slot = uint256(WORMHOLE_PEERS_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + + function _getChainsStorage() internal pure returns (uint16[] storage $) { + uint256 slot = uint256(CHAINS_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + + // =============== Public Getters ====================================================== + + /// @inheritdoc ISharedWormholeTransceiver + function getPeer( + uint16 chainId + ) public view returns (bytes32 peerContract) { + peerContract = _getPeersStorage()[chainId]; + if (peerContract == bytes32(0)) { + revert UnregisteredPeer(chainId); + } + } + + /// @inheritdoc ISharedWormholeTransceiver + function getPeers() public view returns (PeerEntry[] memory results) { + uint16[] storage chains = _getChainsStorage(); + uint256 len = chains.length; + results = new PeerEntry[](len); + for (uint256 idx = 0; idx < len;) { + results[idx].chain = chains[idx]; + results[idx].addr = getPeer(chains[idx]); + unchecked { + ++idx; + } + } + } + + // =============== Admin =============================================================== + + /// @inheritdoc ISharedWormholeTransceiver + function updateAdmin( + address newAdmin + ) external onlyAdmin { + // SPEC: MUST check that the caller is the current admin and there is not a pending transfer. + // - This is handled by onlyAdmin. + + // SPEC: If possible, MUST NOT allow the admin to discard admin via this command (e.g. newAdmin != address(0) on EVM) + if (newAdmin == address(0)) { + revert InvalidAdminZeroAddress(); + } + + // SPEC: Immediately sets newAdmin as the admin of the integrator. + admin = newAdmin; + emit AdminUpdated(msg.sender, newAdmin); + } + + /// @inheritdoc ISharedWormholeTransceiver + function transferAdmin( + address newAdmin + ) external onlyAdmin { + // SPEC: MUST check that the caller is the current admin and there is not a pending transfer. + // - This is handled by onlyAdmin. + + // SPEC: If possible, MUST NOT allow the admin to discard admin via this command (e.g. `newAdmin != address(0)` on EVM). + if (newAdmin == address(0)) { + revert InvalidAdminZeroAddress(); + } + + // SPEC: Initiates the first step of a two-step process in which the current admin (to cancel) or new admin must claim. + pendingAdmin = newAdmin; + emit AdminUpdateRequested(msg.sender, newAdmin); + } + + /// @inheritdoc ISharedWormholeTransceiver + function claimAdmin() external { + // This doesn't use onlyAdmin because the pending admin must be non-zero. + + // SPEC: MUST check that the caller is the current admin OR the pending admin. + if ((admin != msg.sender) && (pendingAdmin != msg.sender)) { + revert CallerNotAdmin(msg.sender); + } + + // SPEC: MUST check that there is an admin transfer pending (e. g. pendingAdmin != address(0) on EVM). + if (pendingAdmin == address(0)) { + revert NoAdminUpdatePending(); + } + + // SPEC: Cancels / Completes the second step of the two-step transfer. Sets the admin to the caller and clears the pending admin. + address oldAdmin = admin; + admin = msg.sender; + pendingAdmin = address(0); + emit AdminUpdated(oldAdmin, msg.sender); + } + + /// @inheritdoc ISharedWormholeTransceiver + function discardAdmin() external onlyAdmin { + // SPEC: MUST check that the caller is the current admin and there is not a pending transfer. + // - This is handled by onlyAdmin. + + // SPEC: Clears the current admin. THIS IS NOT REVERSIBLE. This ensures that the Integrator configuration becomes immutable. + admin = address(0); + emit AdminDiscarded(msg.sender); + } + + /// @inheritdoc ISharedWormholeTransceiver + function setPeer(uint16 peerChain, bytes32 peerContract) external onlyAdmin { + if (peerChain == 0 || peerChain == ourChain) { + revert InvalidChain(peerChain); + } + if (peerContract == bytes32(0)) { + revert InvalidPeerZeroAddress(); + } + + bytes32 oldPeerContract = _getPeersStorage()[peerChain]; + + // SPEC: MUST not set the peer if it is already set. + if (oldPeerContract != bytes32(0)) { + revert PeerAlreadySet(peerChain, oldPeerContract); + } + + _getPeersStorage()[peerChain] = peerContract; + _getChainsStorage().push(peerChain); + emit PeerAdded(peerChain, peerContract); + } + + // =============== ITransceiver Interface ============================================== + + /// @inheritdoc ITransceiver + function getTransceiverType() external pure virtual returns (string memory) { + return sharedWormholeTransceiverVersionString; + } + + /// @inheritdoc ITransceiver + function quoteDeliveryPrice( + uint16, // recipientChain + TransceiverStructs.TransceiverInstruction calldata // instruction + ) external view virtual returns (uint256) { + return wormhole.messageFee(); + } + + /// @inheritdoc ITransceiver + /// @dev The caller should set the delivery price in msg.value. + /// @dev This transceiver does not use instructions, so that parameter is ignored. + /// @dev This transceiver does not use refundAddress, so that parameter is ignored. + function sendMessage( + uint16 recipientChain, + TransceiverStructs.TransceiverInstruction memory, // instruction, + bytes memory nttManagerMessage, + bytes32 recipientNttManagerAddress, + bytes32 // refundAddress + ) external payable virtual { + ( + TransceiverStructs.TransceiverMessage memory transceiverMessage, + bytes memory encodedTransceiverPayload + ) = TransceiverStructs.buildAndEncodeTransceiverMessage( + WH_TRANSCEIVER_PAYLOAD_PREFIX, + toWormholeFormat(msg.sender), + recipientNttManagerAddress, + nttManagerMessage, + new bytes(0) + ); + + wormhole.publishMessage{value: msg.value}(0, encodedTransceiverPayload, consistencyLevel); + emit SendTransceiverMessage(recipientChain, transceiverMessage); + } + + /// @inheritdoc ITransceiver + /// @dev This transciever does not have a specific NttManager, so this function just reverts. + function getNttManagerOwner() external pure returns (address) { + revert NotImplemented(); + } + + /// @inheritdoc ITransceiver + /// @dev This transciever does not have a specific NttManager or token, so this function just reverts. + function getNttManagerToken() external pure returns (address) { + revert NotImplemented(); + } + + /// @inheritdoc ITransceiver + /// @dev Since shared transceivers are not owned by the manager, this function does nothing. + /// We don't want to revert because a manager may have both shared and unshared transceivers. + /// It should be able to call this without without worrying about the transceiver type. + function transferTransceiverOwnership( + address newOwner + ) external {} + + /// @inheritdoc ITransceiver + /// @dev Since shared transceivers are immutable, this just reverts. + function upgrade( + address // newImplementation + ) external pure { + revert NotUpgradable(); + } + + // =============== ISharedWormholeTransceiver Interface ================================ + + /// @inheritdoc ISharedWormholeTransceiver + function receiveMessage( + bytes calldata encodedMessage + ) external { + // Verify the wormhole message and extract the source chain and payload. + (uint16 sourceChainId, bytes memory payload) = _verifyMessage(encodedMessage); + + // TODO: There is no check that this message is intended for this chain, and I don't see how to do it! + + // Parse the encoded Transceiver payload and the encapsulated manager message. + TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; + TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; + (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs + .parseTransceiverAndNttManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, payload); + + // We use recipientNttManagerAddress to deliver the message so it must be set. + if (parsedTransceiverMessage.recipientNttManagerAddress == bytes32(0)) { + revert RecipientManagerAddressIsZero(); + } + + // Forward the message to the specified manager. + IMsgReceiver(fromWormholeFormat(parsedTransceiverMessage.recipientNttManagerAddress)) + .attestationReceived( + sourceChainId, parsedTransceiverMessage.sourceNttManagerAddress, parsedNttManagerMessage + ); + + // We don't need to emit an event here because _verifyMessage already did. + } + + // ============= Internal =============================================================== + + function _verifyMessage( + bytes memory encodedMessage + ) internal returns (uint16, bytes memory) { + // Verify VAA against Wormhole Core Bridge contract. + (IWormhole.VM memory vm, bool valid, string memory reason) = + wormhole.parseAndVerifyVM(encodedMessage); + if (!valid) { + revert InvalidVaa(reason); + } + + // Ensure that the message came from the registered peer contract. + if (getPeer(vm.emitterChainId) != vm.emitterAddress) { + revert InvalidPeer(vm.emitterChainId, vm.emitterAddress); + } + + emit ReceivedMessage(vm.hash, vm.emitterChainId, vm.emitterAddress, vm.sequence); + return (vm.emitterChainId, vm.payload); + } + + // =============== MODIFIERS =============================================== + + modifier onlyAdmin() { + if (admin != msg.sender) { + revert CallerNotAdmin(msg.sender); + } + if (pendingAdmin != address(0)) { + revert AdminTransferPending(); + } + _; + } +} diff --git a/evm/src/interfaces/ISharedWormholeTransceiver.sol b/evm/src/interfaces/ISharedWormholeTransceiver.sol new file mode 100644 index 000000000..ba42bf2e8 --- /dev/null +++ b/evm/src/interfaces/ISharedWormholeTransceiver.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import "./ITransceiver.sol"; + +interface ISharedWormholeTransceiver is ITransceiver { + // =============== Types ================================================================ + + /// @notice Defines each entry in the array returned by getPeers. + struct PeerEntry { + uint16 chain; + bytes32 addr; + } + + // =============== Events ================================================================ + + /// @notice Emitted when the admin is changed for an integrator. + /// @dev Topic0 + /// 0x101b8081ff3b56bbf45deb824d86a3b0fd38b7e3dd42421105cf8abe9106db0b + /// @param oldAdmin The address of the old admin contract. + /// @param newAdmin The address of the new admin contract. + event AdminUpdated(address oldAdmin, address newAdmin); + + /// @notice Emitted when an admin change request is received for an integrator. + /// @dev Topic0 + /// 0x51976906b2cd2da8d93d96f315416f8f479f1bd9f7b6c963ad81733c123587e2 + /// @param oldAdmin The address of the old admin contract. + /// @param newAdmin The address of the new admin contract. + event AdminUpdateRequested(address oldAdmin, address newAdmin); + + /// @notice Emitted when the admin is discarded (set to zero). + /// @dev Topic0 + /// 0xf59e4ee73808efe6c573443d5085333da02eaad3e9890ee65f92053f08b84f4b + /// @param oldAdmin The address of the old admin contract. + event AdminDiscarded(address oldAdmin); + + /// @notice Emitted when a peer adapter is set. + /// @dev Topic0 + /// 0xb54661e84edd2fae127113fec00db0f3a82af37b0347eefb108eba05122224e7 + /// @param chain The Wormhole chain ID of the peer. + /// @param peerContract The address of the peer contract. + event PeerAdded(uint16 chain, bytes32 peerContract); + + /// @notice Emitted when a message is sent from the transceiver. + /// @dev Topic0 + /// 0x79376a0dc6cbfe6f6f8f89ad24c262a8c6233f8df181d3fe5abb2e2442e8c738. + /// @param recipientChain The chain ID of the recipient. + /// @param message The message. + event SendTransceiverMessage( + uint16 recipientChain, TransceiverStructs.TransceiverMessage message + ); + + /// @notice Emitted when a message is received. + /// @dev Topic0 + /// 0xf6fc529540981400dc64edf649eb5e2e0eb5812a27f8c81bac2c1d317e71a5f0. + /// @param digest The digest of the message. + /// @param emitterChainId The chain ID of the emitter. + /// @param emitterAddress The address of the emitter. + /// @param sequence The sequence of the message. + event ReceivedMessage( + bytes32 digest, uint16 emitterChainId, bytes32 emitterAddress, uint64 sequence + ); + + // =============== Errors ================================================================ + + /// @notice Error when the caller is not the registered admin. + /// @dev Selector: 0xe3fb72e9 + /// @param caller The address of the caller. + error CallerNotAdmin(address caller); + + /// @notice Error when an admin action is attempted while an admin transfer is pending. + /// @dev Selector: 9e78953d + error AdminTransferPending(); + + /// @notice Error when an attempt to claim the admin is made when there is no transfer pending. + /// @dev Selector: 0x1ee0a99f + error NoAdminUpdatePending(); + + /// @notice Error when the admin is the zero address. + /// @dev Selector: 0x554ff5d7 + error InvalidAdminZeroAddress(); + + /// @notice Error if the VAA is invalid. + /// @dev Selector: 0x8ee2e336 + /// @param reason The reason the VAA is invalid. + error InvalidVaa(string reason); + + /// @notice Error if the peer has already been set. + /// @dev Selector: 0xb55eeae9 + /// @param chain The Wormhole chain ID of the peer. + /// @param peerAddress The address of the peer. + error PeerAlreadySet(uint16 chain, bytes32 peerAddress); + + /// @notice Error the peer contract cannot be the zero address. + /// @dev Selector: 0xf839a0cb + error InvalidPeerZeroAddress(); + + /// @notice Error when the chain ID is zero or our chain. + /// @dev Selector: 0x587c94c3 + /// @param chain The Wormhole chain ID of the peer. + error InvalidChain(uint16 chain); + + /// @notice Error when the peer adapter is not registered for the given chain. + /// @dev Selector: 0xa98c9e21 + /// @param chain The Wormhole chain ID of the peer. + error UnregisteredPeer(uint16 chain); + + /// @notice Error when the peer adapter is invalid. + /// @dev Selector: 0xaf1181fa + /// @param chain The Wormhole chain ID of the peer. + /// @param peerAddress The address of the invalid peer. + error InvalidPeer(uint16 chain, bytes32 peerAddress); + + /// @notice Length of adapter payload is wrong. + /// @dev Selector: 0xc37906a0 + /// @param received Number of payload bytes received. + /// @param expected Number of payload bytes expected. + error InvalidPayloadLength(uint256 received, uint256 expected); + + /// @notice Shared transceivers are not upgradable. + /// @dev Selector: 0x372c9319 + error NotUpgradable(); + + /// @notice Feature is not implemented. + /// @dev Selector: 0xd6234725 + error NotImplemented(); + + /// @notice Error when the recipient manager address in a received message is zero. + /// @dev Selector: 0x3d8c1d99. + error RecipientManagerAddressIsZero(); + + // =============== Functions ================================================================ + + /// @notice Transfers admin privileges from the current admin to another contract. + /// @dev The msg.sender must be the current admin contract. + /// @param newAdmin The address of the new admin. + function updateAdmin( + address newAdmin + ) external; + + /// @notice Starts the two step process of transferring admin privileges from the current admin to another contract. + /// @dev The msg.sender must be the current admin contract. + /// @param newAdmin The address of the new admin. + function transferAdmin( + address newAdmin + ) external; + + /// @notice Completes the two step process of transferring admin privileges from the current admin to another contract. + /// @dev The msg.sender must be the pending admin or the current admin contract (which cancels the transfer). + function claimAdmin() external; + + /// @notice Sets the admin contract to null, making the configuration immutable. THIS IS NOT REVERSIBLE. + /// @dev The msg.sender must be the current admin contract. + function discardAdmin() external; + + /// @notice Get the peer Adapter contract on the specified chain. + /// @param chain The Wormhole chain ID of the peer to get. + /// @return peerContract The address of the peer contract on the given chain. + function getPeer( + uint16 chain + ) external view returns (bytes32); + + /// @notice Returns an array of all the peers to which this adapter is connected. + /// @return results An array of all of the connected peers including the chain id and contract address of each. + function getPeers() external view returns (PeerEntry[] memory results); + + /// @notice Set the Wormhole peer contract for the given chain. + /// @dev This function is only callable by the `owner`. + /// Once the peer is set for a chain it may not be changed. + /// @param chain The Wormhole chain ID of the peer to set. + /// @param peerContract The address of the peer contract on the given chain. + function setPeer(uint16 chain, bytes32 peerContract) external; + + /// @notice Receive an attested message from the verification layer. + /// This function should verify the `encodedVm` and then deliver the attestation + /// to the adapter NttManager contract. + /// @param encodedMessage The attested message. + function receiveMessage( + bytes calldata encodedMessage + ) external; +} diff --git a/evm/test/SharedWormholeTransceiver.t.sol b/evm/test/SharedWormholeTransceiver.t.sol new file mode 100644 index 000000000..f8fd18f3f --- /dev/null +++ b/evm/test/SharedWormholeTransceiver.t.sol @@ -0,0 +1,493 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "../src/Transceiver/SharedTransceiver/SharedWormholeTransceiver.sol"; +import "../src/interfaces/ISharedWormholeTransceiver.sol"; +import "./mocks/MockWormhole.sol"; + +contract MyMsgReceiver { + uint16 public immutable chainId; + + struct Message { + uint16 sourceChainId; + bytes32 sourceManagerAddress; + TransceiverStructs.NttManagerMessage payload; + } + + Message[] private messages; + + constructor( + uint16 _chainId + ) { + chainId = _chainId; + } + + function numMessages() public view returns (uint256) { + return messages.length; + } + + function getMessage( + uint256 idx + ) public view returns (Message memory) { + return messages[idx]; + } + + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceManagerAddress, + TransceiverStructs.NttManagerMessage memory payload + ) external { + messages.push(Message(sourceChainId, sourceManagerAddress, payload)); + } +} + +contract SharedWormholeTransceiverForTest is SharedWormholeTransceiver { + constructor( + uint16 _ourChain, + address _admin, + address _wormhole, + uint8 _consistencyLevel + ) SharedWormholeTransceiver(_ourChain, _admin, _wormhole, _consistencyLevel) {} +} + +contract SharedWormholeTransceiverTest is Test { + MyMsgReceiver receiver1; + address receiverAddr1; + MyMsgReceiver receiver2; + address receiverAddr2; + + address admin = address(0xabcdef); + address userA = address(0xabcdec); + address userB = address(0xabcdeb); + MockWormhole myWormhole; + SharedWormholeTransceiverForTest public srcTransceiver; + MockWormhole destWormhole; + SharedWormholeTransceiverForTest public destTransceiver; + SharedWormholeTransceiverForTest public otherDestTransceiver; + uint8 consistencyLevel = 200; + + uint16 ourChain = 42; + + uint16 srcChain = 42; + uint16 destChain = 43; + + uint16 peerChain1 = 1; + uint16 peerChain2 = 2; + uint16 peerChain3 = 3; + + TransceiverStructs.TransceiverInstruction emptyInstructions = + TransceiverStructs.TransceiverInstruction(0, new bytes(0)); + + function setUp() public { + receiver1 = new MyMsgReceiver(peerChain1); + receiverAddr1 = address(receiver1); + receiver2 = new MyMsgReceiver(peerChain2); + receiverAddr2 = address(receiver2); + + myWormhole = new MockWormhole(srcChain); + srcTransceiver = new SharedWormholeTransceiverForTest( + srcChain, admin, address(myWormhole), consistencyLevel + ); + destWormhole = new MockWormhole(destChain); + destTransceiver = new SharedWormholeTransceiverForTest( + destChain, admin, address(destWormhole), consistencyLevel + ); + otherDestTransceiver = new SharedWormholeTransceiverForTest( + destChain, admin, address(destWormhole), consistencyLevel + ); + + // Give everyone some money to play with. + vm.deal(receiverAddr1, 1 ether); + vm.deal(receiverAddr2, 1 ether); + vm.deal(admin, 1 ether); + vm.deal(userA, 1 ether); + vm.deal(userB, 1 ether); + } + + function test_init() public view { + require(srcTransceiver.ourChain() == ourChain, "ourChain is not right"); + require(srcTransceiver.admin() == admin, "admin is not right"); + require( + address(srcTransceiver.wormhole()) == address(myWormhole), "myWormhole is not right" + ); + require( + srcTransceiver.consistencyLevel() == consistencyLevel, "consistencyLevel is not right" + ); + } + + function test_invalidInit() public { + // ourChain can't be zero. + vm.expectRevert(); + new SharedWormholeTransceiver(0, admin, address(destWormhole), consistencyLevel); + + // admin can't be zero. + vm.expectRevert(); + new SharedWormholeTransceiver( + destChain, address(0), address(destWormhole), consistencyLevel + ); + + // wormhole can't be zero. + vm.expectRevert(); + new SharedWormholeTransceiver(destChain, admin, address(0), consistencyLevel); + } + + function test_updateAdmin() public { + // Only the admin can initiate this call. + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.CallerNotAdmin.selector, userA) + ); + srcTransceiver.updateAdmin(userB); + + // Can't set the admin to zero. + vm.startPrank(admin); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.InvalidAdminZeroAddress.selector) + ); + srcTransceiver.updateAdmin(address(0)); + + // This should work. + vm.startPrank(admin); + srcTransceiver.updateAdmin(userA); + } + + function test_transferAdmin() public { + // Set up to do a receive below. + vm.startPrank(admin); + destTransceiver.setPeer(srcChain, toWormholeFormat(address(srcTransceiver))); + + // Only the admin can initiate this call. + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.CallerNotAdmin.selector, userA) + ); + srcTransceiver.transferAdmin(userB); + + // Transferring to address zero should revert. + vm.startPrank(admin); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.InvalidAdminZeroAddress.selector) + ); + srcTransceiver.transferAdmin(address(0)); + + // This should work. + vm.startPrank(admin); + srcTransceiver.transferAdmin(userA); + + // Attempting to do another transfer when one is in progress should revert. + vm.startPrank(admin); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.AdminTransferPending.selector) + ); + srcTransceiver.transferAdmin(userB); + + // Attempting to update when a transfer is in progress should revert. + vm.startPrank(admin); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.AdminTransferPending.selector) + ); + srcTransceiver.updateAdmin(userB); + + // Attempting to set a peer when a transfer is in progress should revert. + vm.startPrank(admin); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.AdminTransferPending.selector) + ); + srcTransceiver.setPeer(0, toWormholeFormat(address(destTransceiver))); + + vm.startPrank(receiverAddr1); + + // But you can quote the delivery price while a transfer is pending. + srcTransceiver.quoteDeliveryPrice(peerChain1, emptyInstructions); + + // And you can send a message while a transfer is pending. + uint16 dstChain = destChain; + bytes32 dstAddr = toWormholeFormat(address(receiverAddr2)); + bytes memory payload = "Hello, World!"; + + srcTransceiver.sendMessage( + dstChain, + emptyInstructions, + buildManagerMessage(42, receiverAddr1, payload), + dstAddr, + bytes32(0) // refundAddress + ); + + require(1 == myWormhole.messagesSent(), "VAA did not get sent"); + + // And you can receive a message while a transfer is pending. + destTransceiver.receiveMessage(myWormhole.lastVaa()); + } + + function test_claimAdmin() public { + // Can't claim when a transfer is not pending. + vm.startPrank(admin); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.NoAdminUpdatePending.selector) + ); + srcTransceiver.claimAdmin(); + + // Start a transfer. + srcTransceiver.transferAdmin(userA); + + // If someone other than the current or pending admin tries to claim, it should revert. + vm.startPrank(userB); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.CallerNotAdmin.selector, userB) + ); + srcTransceiver.claimAdmin(); + + // The admin claiming should cancel the transfer. + vm.startPrank(admin); + srcTransceiver.claimAdmin(); + require(srcTransceiver.admin() == admin, "cancel set the admin incorrectly"); + require( + srcTransceiver.pendingAdmin() == address(0), "cancel did not clear the pending admin" + ); + + // The new admin claiming it should work. + srcTransceiver.transferAdmin(userA); + vm.startPrank(userA); + srcTransceiver.claimAdmin(); + require(srcTransceiver.admin() == userA, "transfer set the admin incorrectly"); + require( + srcTransceiver.pendingAdmin() == address(0), "transfer did not clear the pending admin" + ); + } + + function test_discardAdmin() public { + // Only the admin can initiate this call. + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.CallerNotAdmin.selector, userA) + ); + srcTransceiver.discardAdmin(); + + // This should work. + vm.startPrank(admin); + srcTransceiver.discardAdmin(); + require(srcTransceiver.admin() == address(0), "transfer set the admin incorrectly"); + require( + srcTransceiver.pendingAdmin() == address(0), "transfer did not clear the pending admin" + ); + + // So now the old admin can't do anything. + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.CallerNotAdmin.selector, admin) + ); + srcTransceiver.updateAdmin(userB); + } + + function test_setPeer() public { + // Only the admin can set a peer. + vm.startPrank(userB); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.CallerNotAdmin.selector, userB) + ); + srcTransceiver.setPeer(0, toWormholeFormat(receiverAddr1)); + + // Peer chain can't be zero. + vm.startPrank(admin); + vm.expectRevert(abi.encodeWithSelector(ISharedWormholeTransceiver.InvalidChain.selector, 0)); + srcTransceiver.setPeer(0, toWormholeFormat(receiverAddr1)); + + // Peer contract can't be zero. + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.InvalidPeerZeroAddress.selector) + ); + srcTransceiver.setPeer(peerChain1, toWormholeFormat(address(0))); + + // This should work. + srcTransceiver.setPeer(peerChain1, toWormholeFormat(address(receiverAddr1))); + + // You can't set a peer when it's already set. + vm.expectRevert( + abi.encodeWithSelector( + ISharedWormholeTransceiver.PeerAlreadySet.selector, peerChain1, receiverAddr1 + ) + ); + srcTransceiver.setPeer(peerChain1, toWormholeFormat(address(receiverAddr2))); + + // But you can set the peer for another chain. + srcTransceiver.setPeer(peerChain2, toWormholeFormat(address(receiverAddr2))); + + // Test the getter. + require( + srcTransceiver.getPeer(peerChain1) == toWormholeFormat(address(receiverAddr1)), + "Peer for chain one is wrong" + ); + require( + srcTransceiver.getPeer(peerChain2) == toWormholeFormat(address(receiverAddr2)), + "Peer for chain two is wrong" + ); + + // If you get a peer for a chain that's not set, it reverts. + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.UnregisteredPeer.selector, peerChain3) + ); + srcTransceiver.getPeer(peerChain3); + } + + function test_getTransceiverType() public view { + require( + keccak256(abi.encodePacked(srcTransceiver.getTransceiverType())) + == keccak256(abi.encodePacked(sharedWormholeTransceiverVersionString)), + "transceiver type mismatch" + ); + } + + function test_quoteDeliveryPrice() public view { + require( + srcTransceiver.quoteDeliveryPrice(peerChain1, emptyInstructions) + == myWormhole.fixedMessageFee(), + "message fee is wrong" + ); + } + + function test_sendMessage() public { + vm.startPrank(admin); + destTransceiver.setPeer(srcChain, toWormholeFormat(address(srcTransceiver))); + + uint16 dstChain = destChain; + bytes32 dstAddr = toWormholeFormat(receiverAddr2); + bytes memory payload = "Hello, World!"; + uint256 deliverPrice = 382; + bytes memory managerMsg = buildManagerMessage(42, userA, payload); + + // Send the message from receiver1 to receiver2. + vm.startPrank(receiverAddr1); + srcTransceiver.sendMessage{value: deliverPrice}( + dstChain, + emptyInstructions, + managerMsg, + dstAddr, + bytes32(0) // refundAddress + ); + + // Make sure the VAA went out as expected. + require(myWormhole.messagesSent() == 1, "Message count is wrong"); + require(myWormhole.lastNonce() == 0, "Nonce is wrong"); + require(myWormhole.lastConsistencyLevel() == consistencyLevel, "Consistency level is wrong"); + require(myWormhole.lastDeliveryPrice() == deliverPrice, "Deliver price is wrong"); + + // Receive the message on the destination. + destTransceiver.receiveMessage(myWormhole.lastVaa()); + + // Make sure the destination received the message. + require(1 == receiver2.numMessages(), "Message not received"); + + // Make sure the received message is what we expect. + MyMsgReceiver.Message memory msg1 = receiver2.getMessage(0); + require(srcChain == msg1.sourceChainId, "Unexpected sourceChainId"); + require( + toWormholeFormat(receiverAddr1) == msg1.sourceManagerAddress, + "Unexpected sourceManagerAddress" + ); + require(bytes32(uint256(42)) == msg1.payload.id, "Unexpected id"); + require(toWormholeFormat(userA) == msg1.payload.sender, "Unexpected sender"); + require(keccak256(payload) == keccak256(msg1.payload.payload), "Unexpected payload"); + } + + function test_receiveMessage() public { + // Set the peers on the transceivers. + vm.startPrank(admin); + srcTransceiver.setPeer(destChain, toWormholeFormat(address(destTransceiver))); + destTransceiver.setPeer(srcChain, toWormholeFormat(address(srcTransceiver))); + otherDestTransceiver.setPeer(srcChain, bytes32(uint256(1))); + + uint16 dstChain = destChain; + bytes32 dstAddr = toWormholeFormat(receiverAddr2); + bytes memory payload = "Hello, World!"; + uint256 deliverPrice = 382; + bytes memory managerMsg = buildManagerMessage(42, userA, payload); + + // Send the message from receiver1 to receiver2. + vm.startPrank(receiverAddr1); + srcTransceiver.sendMessage{value: deliverPrice}( + dstChain, + emptyInstructions, + managerMsg, + dstAddr, + bytes32(0) // refundAddress + ); + + require(myWormhole.messagesSent() == 1, "Message count is wrong"); + bytes memory vaa = myWormhole.lastVaa(); + + // This should work. + destTransceiver.receiveMessage(vaa); + + // Make sure the destination received the message. + require(1 == receiver2.numMessages(), "Message not received"); + + // Make sure the received message is what we expect. + MyMsgReceiver.Message memory msg1 = receiver2.getMessage(0); + require(srcChain == msg1.sourceChainId, "Unexpected sourceChainId"); + require( + toWormholeFormat(receiverAddr1) == msg1.sourceManagerAddress, + "Unexpected sourceManagerAddress" + ); + require(bytes32(uint256(42)) == msg1.payload.id, "Unexpected id"); + require(toWormholeFormat(userA) == msg1.payload.sender, "Unexpected sender"); + require(keccak256(payload) == keccak256(msg1.payload.payload), "Unexpected payload"); + + // Can't post it from a chain that isn't registered + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.UnregisteredPeer.selector, srcChain) + ); + srcTransceiver.receiveMessage(vaa); + + // Can't post it to the wrong transceiver. + vm.expectRevert( + abi.encodeWithSelector( + ISharedWormholeTransceiver.InvalidPeer.selector, srcChain, address(srcTransceiver) + ) + ); + otherDestTransceiver.receiveMessage(vaa); + + // An invalid VAA should revert. + destWormhole.setValidFlag(false, "This is bad!"); + vm.expectRevert( + abi.encodeWithSelector(ISharedWormholeTransceiver.InvalidVaa.selector, "This is bad!") + ); + destTransceiver.receiveMessage(vaa); + destWormhole.setValidFlag(true, ""); + } + + function test_getPeers() public { + vm.startPrank(admin); + + require(0 == srcTransceiver.getPeers().length, "Initial peers should be zero"); + + bytes32 peerAddr1 = toWormholeFormat(address(receiverAddr1)); + bytes32 peerAddr2 = toWormholeFormat(address(receiverAddr2)); + + srcTransceiver.setPeer(peerChain1, peerAddr1); + ISharedWormholeTransceiver.PeerEntry[] memory peers = srcTransceiver.getPeers(); + require(1 == peers.length, "Should be one peer"); + require(peers[0].chain == peerChain1, "Chain is wrong"); + require(peers[0].addr == peerAddr1, "Address is wrong"); + + srcTransceiver.setPeer(peerChain2, peerAddr2); + peers = srcTransceiver.getPeers(); + require(2 == peers.length, "Should be one peer"); + require(peers[0].chain == peerChain1, "First chain is wrong"); + require(peers[0].addr == peerAddr1, "First address is wrong"); + require(peers[1].chain == peerChain2, "Second chain is wrong"); + require(peers[1].addr == peerAddr2, "Second address is wrong"); + } + + function buildManagerMessage( + uint64 sequence, + address sender, + bytes memory payload + ) public pure returns (bytes memory) { + return TransceiverStructs.encodeNttManagerMessage( + TransceiverStructs.NttManagerMessage( + bytes32(uint256(sequence)), toWormholeFormat(sender), payload + ) + ); + } +} diff --git a/evm/test/mocks/MockWormhole.sol b/evm/test/mocks/MockWormhole.sol new file mode 100644 index 000000000..854b9792e --- /dev/null +++ b/evm/test/mocks/MockWormhole.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import "wormhole-solidity-sdk/Utils.sol"; +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; + +contract MockWormhole { + uint256 public constant fixedMessageFee = 250; + + uint16 public immutable ourChain; + + bool public validFlag; + string public invalidReason; + + // These are incremented on calls. + uint256 public messagesSent; + uint32 public seqSent; + + // These are set on calls. + uint256 public lastDeliveryPrice; + uint32 public lastNonce; + uint8 public lastConsistencyLevel; + bytes public lastPayload; + bytes public lastVaa; + bytes32 public lastVaaHash; + + constructor( + uint16 _ourChain + ) { + ourChain = _ourChain; + validFlag = true; + } + + function setValidFlag(bool v, string memory reason) external { + validFlag = v; + invalidReason = reason; + } + + function messageFee() external pure returns (uint256) { + return fixedMessageFee; + } + + function publishMessage( + uint32 nonce, + bytes memory payload, + uint8 consistencyLevel + ) external payable returns (uint64 sequence) { + seqSent += 1; + sequence = seqSent; + + lastDeliveryPrice = msg.value; + lastNonce = nonce; + lastConsistencyLevel = consistencyLevel; + lastPayload = payload; + messagesSent += 1; + + bytes32 sender = toWormholeFormat(msg.sender); + bytes32 hash = keccak256(payload); + + lastVaa = abi.encode(ourChain, sender, sequence, hash, payload); + lastVaaHash = hash; + } + + function parseAndVerifyVM( + bytes calldata encodedVM + ) external view returns (IWormhole.VM memory vm, bool valid, string memory reason) { + valid = validFlag; + reason = invalidReason; + + // These are the fields that the transceiver uses: + // vm.emitterChainId + // vm.emitterAddress + // vm.hash + // vm.payload + + (vm.emitterChainId, vm.emitterAddress, vm.sequence, vm.hash, vm.payload) = + abi.decode(encodedVM, (uint16, bytes32, uint64, bytes32, bytes)); + } +}