diff --git a/evm/script/DeployWormholeNtt.s.sol b/evm/script/DeployWormholeNtt.s.sol index 9be86eefc..73381bb7f 100644 --- a/evm/script/DeployWormholeNtt.s.sol +++ b/evm/script/DeployWormholeNtt.s.sol @@ -6,8 +6,9 @@ import {Script, console2} from "forge-std/Script.sol"; import "../src/interfaces/IManagerBase.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IWormholeTransceiver.sol"; +import "../src/interfaces/IWormholeTransceiverState.sol"; -import {NttManager} from "../src/NttManager/NttManager.sol"; +import {NttManager} from "../src/NativeTransfers/NttManager.sol"; import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {ParseNttConfig} from "./helpers/ParseNttConfig.sol"; @@ -64,7 +65,8 @@ contract DeployWormholeNtt is Script, ParseNttConfig { params.wormholeRelayerAddr, params.specialRelayerAddr, params.consistencyLevel, - params.gasLimit + params.gasLimit, + IWormholeTransceiverState.ManagerType.ERC20 ); WormholeTransceiver transceiverProxy = diff --git a/evm/script/UpgradeNttManager.s.sol b/evm/script/UpgradeNttManager.s.sol index 2ae864624..e2d8e52f9 100644 --- a/evm/script/UpgradeNttManager.s.sol +++ b/evm/script/UpgradeNttManager.s.sol @@ -6,7 +6,7 @@ import {console2} from "forge-std/Script.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IManagerBase.sol"; -import {NttManager} from "../src/NttManager/NttManager.sol"; +import {NttManager} from "../src/NativeTransfers/NttManager.sol"; import {ERC1967Proxy} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {ParseNttConfig} from "./helpers/ParseNttConfig.sol"; diff --git a/evm/script/UpgradeWormholeTransceiver.s.sol b/evm/script/UpgradeWormholeTransceiver.s.sol index 503ed4931..dfc77b404 100644 --- a/evm/script/UpgradeWormholeTransceiver.s.sol +++ b/evm/script/UpgradeWormholeTransceiver.s.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.8 <0.9.0; import {console2} from "forge-std/Script.sol"; import "../src/interfaces/IWormholeTransceiver.sol"; +import "../src/interfaces/IWormholeTransceiverState.sol"; import "../src/interfaces/ITransceiver.sol"; import "../src/interfaces/INttManager.sol"; @@ -39,7 +40,8 @@ contract UpgradeWormholeTransceiver is ParseNttConfig { params.wormholeRelayerAddr, params.specialRelayerAddr, params.consistencyLevel, - params.gasLimit + params.gasLimit, + IWormholeTransceiverState.ManagerType.ERC20 ); console2.log("WormholeTransceiver Implementation deployed at: ", address(implementation)); diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol new file mode 100644 index 000000000..eee9a56e2 --- /dev/null +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Burnable.sol"; +import "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; +import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; + +import "wormhole-solidity-sdk/Utils.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "../libraries/TransceiverHelpers.sol"; + +import "../interfaces/ITransceiver.sol"; +import "../interfaces/INonFungibleNttManager.sol"; +import "../interfaces/INonFungibleNttToken.sol"; + +import {ManagerBase} from "./shared/ManagerBase.sol"; + +contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { + using BytesParsing for bytes; + + // =============== Immutables ============================================================ + + // Hard cap on the number of NFTs that can be transferred in a single batch. This is to prevent + // the contract from running out of gas when processing large batches of NFTs. + uint8 constant MAX_BATCH_SIZE = 50; + + // The number of bytes each NFT token ID occupies in the payload. All tokenIDs must fit within + // this width. + uint8 public immutable tokenIdWidth; + + // =============== Setup ================================================================= + + constructor( + address _token, + uint8 _tokenIdWidth, + Mode _mode, + uint16 _chainId + ) ManagerBase(_token, _mode, _chainId) { + _validateTokenIdWidth(_tokenIdWidth); + tokenIdWidth = _tokenIdWidth; + } + + function __NonFungibleNttManager_init() internal onlyInitializing { + // check if the owner is the deployer of this contract + if (msg.sender != deployer) { + revert UnexpectedDeployer(deployer, msg.sender); + } + __PausedOwnable_init(msg.sender, msg.sender); + __ReentrancyGuard_init(); + } + + function _initialize() internal virtual override { + __NonFungibleNttManager_init(); + _checkThresholdInvariants(); + _checkTransceiversInvariants(); + } + + /// ============== Invariants ============================================= + + /// @dev When we add new immutables, this function should be updated + function _checkImmutables() internal view override { + super._checkImmutables(); + assert(this.tokenIdWidth() == tokenIdWidth); + } + + // =============== Storage ============================================================== + + bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("nonFungibleNtt.peers")) - 1); + + // =============== Storage Getters/Setters ============================================== + + function _getPeersStorage() + internal + pure + returns (mapping(uint16 => NonFungibleNttManagerPeer) storage $) + { + uint256 slot = uint256(PEERS_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } + + // =============== Public Getters ======================================================== + + function getPeer(uint16 chainId_) external view returns (NonFungibleNttManagerPeer memory) { + return _getPeersStorage()[chainId_]; + } + + function getMaxBatchSize() external pure returns (uint8) { + return MAX_BATCH_SIZE; + } + + // =============== Admin ============================================================== + + function setPeer(uint16 peerChainId, bytes32 peerContract) public onlyOwner { + if (peerChainId == 0) { + revert InvalidPeerChainIdZero(); + } + if (peerContract == bytes32(0)) { + revert InvalidPeerZeroAddress(); + } + + NonFungibleNttManagerPeer memory oldPeer = _getPeersStorage()[peerChainId]; + + _getPeersStorage()[peerChainId].peerAddress = peerContract; + + emit PeerUpdated(peerChainId, oldPeer.peerAddress, peerContract); + } + + // =============== External Interface ================================================== + + function quoteDeliveryPrice( + uint16 recipientChain, + bytes memory transceiverInstructions + ) public view virtual returns (uint256[] memory, uint256) { + address[] memory enabledTransceivers = _getEnabledTransceiversStorage(); + + TransceiverStructs.TransceiverInstruction[] memory instructions = TransceiverStructs + .parseTransceiverInstructions(transceiverInstructions, enabledTransceivers.length); + + // TODO: Compute execution cost here. + return _quoteDeliveryPrice(recipientChain, instructions, enabledTransceivers, 0); + } + + function transfer( + uint256[] memory tokenIds, + uint16 recipientChain, + bytes32 recipient, + bytes memory transceiverInstructions + ) external payable nonReentrant whenNotPaused returns (uint64) { + return _transfer(tokenIds, recipientChain, recipient, transceiverInstructions); + } + + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory payload + ) external onlyTransceiver whenNotPaused { + _verifyPeer(sourceChainId, sourceNttManagerAddress); + + // Compute manager message digest and record transceiver attestation. + bytes32 ManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); + + if (isMessageApproved(ManagerMessageHash)) { + executeMsg(sourceChainId, sourceNttManagerAddress, payload); + } + } + + function executeMsg( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory message + ) public whenNotPaused { + // verify chain has not forked + checkFork(evmChainId); + + (bytes32 digest, bool alreadyExecuted) = + _isMessageExecuted(sourceChainId, sourceNttManagerAddress, message); + + if (alreadyExecuted) { + return; + } + + TransceiverStructs.NonFungibleNativeTokenTransfer memory nft = + TransceiverStructs.parseNonFungibleNativeTokenTransfer(message.payload); + + // verify that the destination chain is valid + if (nft.toChain != chainId) { + revert InvalidTargetChain(nft.toChain, chainId); + } + + emit TransferRedeemed(digest); + + if (mode == Mode.BURNING) { + _mintTokens(nft.tokenIds, fromWormholeFormat(nft.to)); + } else if (mode == Mode.LOCKING) { + _unlockTokens(nft.tokenIds, fromWormholeFormat(nft.to)); + } else { + revert InvalidMode(uint8(mode)); + } + } + + function onERC721Received( + address operator, + address, + uint256, + bytes calldata + ) external view returns (bytes4) { + if (operator != address(this)) { + revert InvalidOperator(operator, address(this)); + } + return type(IERC721Receiver).interfaceId; + } + + // ==================== Internal Business Logic ========================================= + + function _transfer( + uint256[] memory tokenIds, + uint16 recipientChain, + bytes32 recipient, + bytes memory transceiverInstructions + ) internal returns (uint64) { + if (tokenIds.length == 0) { + revert ZeroTokenIds(); + } + + if (tokenIds.length > MAX_BATCH_SIZE) { + revert ExceedsMaxBatchSize(tokenIds.length, MAX_BATCH_SIZE); + } + + if (recipient == bytes32(0)) { + revert InvalidRecipient(); + } + + // NOTE: Burn or lock tokens depending on the Mode. There are no validation checks + // performed on the array of tokenIds. It is the caller's responsibility to ensure + // that the tokenIds are unique and approved. Otherwise, the call to burn or transfer + // the same tokenId will fail. + if (mode == Mode.BURNING) { + _burnTokens(tokenIds); + } else if (mode == Mode.LOCKING) { + _lockTokens(tokenIds); + } else { + revert InvalidMode(uint8(mode)); + } + + // Fetch quotes and prepare for transfer. + // TODO: compute execution cost here. + ( + address[] memory enabledTransceivers, + TransceiverStructs.TransceiverInstruction[] memory instructions, + uint256[] memory priceQuotes, + uint256 totalPriceQuote + ) = _prepareForTransfer(recipientChain, transceiverInstructions, 0); + + uint64 sequence = _useMessageSequence(); + + /// NOTE: The integrator that deploys this code can modify the arbitrary payload + /// to include any custom instructions they wish to communicate to the target + /// contract. + TransceiverStructs.NonFungibleNativeTokenTransfer memory nft = TransceiverStructs + .NonFungibleNativeTokenTransfer(recipient, recipientChain, tokenIds, new bytes(0)); + + // construct the ManagerMessage payload + bytes memory encodedNttManagerPayload = TransceiverStructs.encodeManagerMessage( + TransceiverStructs.ManagerMessage( + bytes32(uint256(sequence)), + toWormholeFormat(msg.sender), + TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nft, tokenIdWidth) + ) + ); + + // send the message + _sendMessageToTransceivers( + recipientChain, + _getPeersStorage()[recipientChain].peerAddress, + priceQuotes, + 0, + instructions, + enabledTransceivers, + encodedNttManagerPayload + ); + + emit TransferSent( + recipient, uint16(tokenIds.length), totalPriceQuote, recipientChain, sequence + ); + + return sequence; + } + + // ==================== Internal Helpers =============================================== + + function _lockTokens(uint256[] memory tokenIds) internal { + uint256 len = tokenIds.length; + + for (uint256 i = 0; i < len; ++i) { + IERC721(token).safeTransferFrom(msg.sender, address(this), tokenIds[i]); + } + } + + function _unlockTokens(uint256[] memory tokenIds, address recipient) internal { + uint256 len = tokenIds.length; + + for (uint256 i = 0; i < len; ++i) { + IERC721(token).safeTransferFrom(address(this), recipient, tokenIds[i]); + } + } + + function _burnTokens(uint256[] memory tokenIds) internal { + uint256 len = tokenIds.length; + + for (uint256 i = 0; i < len; ++i) { + ERC721Burnable(token).burn(tokenIds[i]); + } + } + + function _mintTokens(uint256[] memory tokenIds, address recipient) internal { + uint256 len = tokenIds.length; + + for (uint256 i = 0; i < len; ++i) { + INonFungibleNttToken(token).mint(recipient, tokenIds[i]); + } + } + + /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view { + if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { + revert InvalidPeer(sourceChainId, peerAddress); + } + } + + function _validateTokenIdWidth(uint8 _tokenIdWidth) internal pure { + if ( + _tokenIdWidth != 1 && _tokenIdWidth != 2 && _tokenIdWidth != 4 && _tokenIdWidth != 8 + && _tokenIdWidth != 16 && _tokenIdWidth != 32 + ) { + revert InvalidTokenIdWidth(_tokenIdWidth); + } + } +} diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NativeTransfers/NttManager.sol similarity index 94% rename from evm/src/NttManager/NttManager.sol rename to evm/src/NativeTransfers/NttManager.sol index 9372237c0..f783d21e8 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NativeTransfers/NttManager.sol @@ -13,7 +13,7 @@ import "../interfaces/INttManager.sol"; import "../interfaces/INttToken.sol"; import "../interfaces/ITransceiver.sol"; -import {ManagerBase} from "./ManagerBase.sol"; +import {ManagerBase} from "./shared/ManagerBase.sol"; /// @title NttManager /// @author Wormhole Project Contributors. @@ -142,6 +142,19 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { // ==================== External Interface =============================================== + /// @inheritdoc INttManager + function quoteDeliveryPrice( + uint16 recipientChain, + bytes memory transceiverInstructions + ) public view virtual returns (uint256[] memory, uint256) { + address[] memory enabledTransceivers = _getEnabledTransceiversStorage(); + + TransceiverStructs.TransceiverInstruction[] memory instructions = TransceiverStructs + .parseTransceiverInstructions(transceiverInstructions, enabledTransceivers.length); + + return _quoteDeliveryPrice(recipientChain, instructions, enabledTransceivers, 0); + } + /// @inheritdoc INttManager function transfer( uint256 amount, @@ -168,14 +181,14 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { function attestationReceived( uint16 sourceChainId, bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory payload + TransceiverStructs.ManagerMessage memory payload ) external onlyTransceiver whenNotPaused { _verifyPeer(sourceChainId, sourceNttManagerAddress); // Compute manager message digest and record transceiver attestation. - bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); + bytes32 ManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload); - if (isMessageApproved(nttManagerMessageHash)) { + if (isMessageApproved(ManagerMessageHash)) { executeMsg(sourceChainId, sourceNttManagerAddress, payload); } } @@ -184,7 +197,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { function executeMsg( uint16 sourceChainId, bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory message + TransceiverStructs.ManagerMessage memory message ) public whenNotPaused { // verify chain has not forked checkFork(evmChainId); @@ -394,12 +407,13 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { address sender, bytes memory transceiverInstructions ) internal returns (uint64 msgSequence) { + // TODO: compute execution cost here. ( address[] memory enabledTransceivers, TransceiverStructs.TransceiverInstruction[] memory instructions, uint256[] memory priceQuotes, uint256 totalPriceQuote - ) = _prepareForTransfer(recipientChain, transceiverInstructions); + ) = _prepareForTransfer(recipientChain, transceiverInstructions, 0); // push it on the stack again to avoid a stack too deep error uint64 seq = sequence; @@ -408,9 +422,9 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { amount, toWormholeFormat(token), recipient, recipientChain ); - // construct the NttManagerMessage payload - bytes memory encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage( - TransceiverStructs.NttManagerMessage( + // construct the ManagerMessage payload + bytes memory encodedNttManagerPayload = TransceiverStructs.encodeManagerMessage( + TransceiverStructs.ManagerMessage( bytes32(uint256(seq)), toWormholeFormat(sender), TransceiverStructs.encodeNativeTokenTransfer(ntt) @@ -422,6 +436,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { recipientChain, _getPeersStorage()[recipientChain].peerAddress, priceQuotes, + 0, instructions, enabledTransceivers, encodedNttManagerPayload diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NativeTransfers/shared/ManagerBase.sol similarity index 90% rename from evm/src/NttManager/ManagerBase.sol rename to evm/src/NativeTransfers/shared/ManagerBase.sol index 7a6299084..070e05753 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NativeTransfers/shared/ManagerBase.sol @@ -4,15 +4,15 @@ pragma solidity >=0.8.8 <0.9.0; import "wormhole-solidity-sdk/Utils.sol"; import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; -import "../libraries/external/OwnableUpgradeable.sol"; -import "../libraries/external/ReentrancyGuardUpgradeable.sol"; -import "../libraries/TransceiverStructs.sol"; -import "../libraries/TransceiverHelpers.sol"; -import "../libraries/PausableOwnable.sol"; -import "../libraries/Implementation.sol"; +import "../../libraries/external/OwnableUpgradeable.sol"; +import "../../libraries/external/ReentrancyGuardUpgradeable.sol"; +import "../../libraries/TransceiverStructs.sol"; +import "../../libraries/TransceiverHelpers.sol"; +import "../../libraries/PausableOwnable.sol"; +import "../../libraries/Implementation.sol"; -import "../interfaces/ITransceiver.sol"; -import "../interfaces/IManagerBase.sol"; +import "../../interfaces/ITransceiver.sol"; +import "../../interfaces/IManagerBase.sol"; import "./TransceiverRegistry.sol"; @@ -84,14 +84,14 @@ abstract contract ManagerBase is } } - // =============== External Logic ============================================================= + // =============== Internal Logic =========================================================== - /// @inheritdoc IManagerBase - function quoteDeliveryPrice( + function _quoteDeliveryPrice( uint16 recipientChain, TransceiverStructs.TransceiverInstruction[] memory transceiverInstructions, - address[] memory enabledTransceivers - ) public view returns (uint256[] memory, uint256) { + address[] memory enabledTransceivers, + uint256 managerExecutionCost + ) internal view returns (uint256[] memory, uint256) { uint256 numEnabledTransceivers = enabledTransceivers.length; mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); @@ -101,7 +101,7 @@ abstract contract ManagerBase is address transceiverAddr = enabledTransceivers[i]; uint8 registeredTransceiverIndex = transceiverInfos[transceiverAddr].index; uint256 transceiverPriceQuote = ITransceiver(transceiverAddr).quoteDeliveryPrice( - recipientChain, transceiverInstructions[registeredTransceiverIndex] + recipientChain, transceiverInstructions[registeredTransceiverIndex], managerExecutionCost ); priceQuotes[i] = transceiverPriceQuote; totalPriceQuote += transceiverPriceQuote; @@ -109,14 +109,11 @@ abstract contract ManagerBase is return (priceQuotes, totalPriceQuote); } - // =============== Internal Logic =========================================================== - function _recordTransceiverAttestation( uint16 sourceChainId, - TransceiverStructs.NttManagerMessage memory payload + TransceiverStructs.ManagerMessage memory payload ) internal returns (bytes32) { - bytes32 nttManagerMessageHash = - TransceiverStructs.nttManagerMessageDigest(sourceChainId, payload); + bytes32 ManagerMessageHash = TransceiverStructs.managerMessageDigest(sourceChainId, payload); // set the attested flag for this transceiver. // NOTE: Attestation is idempotent (bitwise or 1), but we revert @@ -124,22 +121,22 @@ abstract contract ManagerBase is // to receive the same message through the same transceiver. if ( transceiverAttestedToMessage( - nttManagerMessageHash, _getTransceiverInfosStorage()[msg.sender].index + ManagerMessageHash, _getTransceiverInfosStorage()[msg.sender].index ) ) { - revert TransceiverAlreadyAttestedToMessage(nttManagerMessageHash); + revert TransceiverAlreadyAttestedToMessage(ManagerMessageHash); } - _setTransceiverAttestedToMessage(nttManagerMessageHash, msg.sender); + _setTransceiverAttestedToMessage(ManagerMessageHash, msg.sender); - return nttManagerMessageHash; + return ManagerMessageHash; } function _isMessageExecuted( uint16 sourceChainId, bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory message + TransceiverStructs.ManagerMessage memory message ) internal returns (bytes32, bool) { - bytes32 digest = TransceiverStructs.nttManagerMessageDigest(sourceChainId, message); + bytes32 digest = TransceiverStructs.managerMessageDigest(sourceChainId, message); if (!isMessageApproved(digest)) { revert MessageNotApproved(digest); @@ -161,9 +158,10 @@ abstract contract ManagerBase is uint16 recipientChain, bytes32 peerAddress, uint256[] memory priceQuotes, + uint256 managerExecutionCost, TransceiverStructs.TransceiverInstruction[] memory transceiverInstructions, address[] memory enabledTransceivers, - bytes memory nttManagerMessage + bytes memory ManagerMessage ) internal { uint256 numEnabledTransceivers = enabledTransceivers.length; mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); @@ -179,15 +177,17 @@ abstract contract ManagerBase is ITransceiver(transceiverAddr).sendMessage{value: priceQuotes[i]}( recipientChain, transceiverInstructions[transceiverInfos[transceiverAddr].index], - nttManagerMessage, - peerAddress + ManagerMessage, + peerAddress, + managerExecutionCost ); } } function _prepareForTransfer( uint16 recipientChain, - bytes memory transceiverInstructions + bytes memory transceiverInstructions, + uint256 managerExecutionCost ) internal returns ( @@ -215,7 +215,7 @@ abstract contract ManagerBase is } (uint256[] memory priceQuotes, uint256 totalPriceQuote) = - quoteDeliveryPrice(recipientChain, instructions, enabledTransceivers); + _quoteDeliveryPrice(recipientChain, instructions, enabledTransceivers, managerExecutionCost); { // check up front that msg.value will cover the delivery price if (msg.value < totalPriceQuote) { diff --git a/evm/src/NttManager/TransceiverRegistry.sol b/evm/src/NativeTransfers/shared/TransceiverRegistry.sol similarity index 100% rename from evm/src/NttManager/TransceiverRegistry.sol rename to evm/src/NativeTransfers/shared/TransceiverRegistry.sol diff --git a/evm/src/Transceiver/Transceiver.sol b/evm/src/Transceiver/Transceiver.sol index 1edf7ffaa..37b934818 100644 --- a/evm/src/Transceiver/Transceiver.sol +++ b/evm/src/Transceiver/Transceiver.sol @@ -95,25 +95,28 @@ abstract contract Transceiver is /// @inheritdoc ITransceiver function quoteDeliveryPrice( uint16 targetChain, - TransceiverStructs.TransceiverInstruction memory instruction + TransceiverStructs.TransceiverInstruction memory instruction, + uint256 managerExecutionCost ) external view returns (uint256) { - return _quoteDeliveryPrice(targetChain, instruction); + return _quoteDeliveryPrice(targetChain, instruction, managerExecutionCost); } /// @inheritdoc ITransceiver function sendMessage( uint16 recipientChain, TransceiverStructs.TransceiverInstruction memory instruction, - bytes memory nttManagerMessage, - bytes32 recipientNttManagerAddress + bytes memory ManagerMessage, + bytes32 recipientNttManagerAddress, + uint256 managerExecutionCost ) external payable nonReentrant onlyNttManager { _sendMessage( recipientChain, msg.value, + managerExecutionCost, msg.sender, recipientNttManagerAddress, instruction, - nttManagerMessage + ManagerMessage ); } @@ -122,10 +125,11 @@ abstract contract Transceiver is function _sendMessage( uint16 recipientChain, uint256 deliveryPayment, + uint256 managerExecutionCost, address caller, bytes32 recipientNttManagerAddress, TransceiverStructs.TransceiverInstruction memory transceiverInstruction, - bytes memory nttManagerMessage + bytes memory ManagerMessage ) internal virtual; // @define This method is called by the BridgeNttManager contract to send a cross-chain message. @@ -135,7 +139,7 @@ abstract contract Transceiver is uint16 sourceChainId, bytes32 sourceNttManagerAddress, bytes32 recipientNttManagerAddress, - TransceiverStructs.NttManagerMessage memory payload + TransceiverStructs.ManagerMessage memory payload ) internal virtual { if (recipientNttManagerAddress != toWormholeFormat(nttManager)) { revert UnexpectedRecipientNttManagerAddress( @@ -147,6 +151,7 @@ abstract contract Transceiver is function _quoteDeliveryPrice( uint16 targetChain, - TransceiverStructs.TransceiverInstruction memory transceiverInstruction + TransceiverStructs.TransceiverInstruction memory transceiverInstruction, + uint256 managerExecutionCost ) internal view virtual returns (uint256); } diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol index 2c7e366e4..51f70ea89 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol @@ -39,7 +39,8 @@ contract WormholeTransceiver is address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _attestationGasLimit, + IWormholeTransceiverState.ManagerType _managerType ) WormholeTransceiverState( nttManager, @@ -47,7 +48,8 @@ contract WormholeTransceiver is wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit + _attestationGasLimit, + _managerType ) {} @@ -61,15 +63,15 @@ contract WormholeTransceiver is // parse the encoded Transceiver payload TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; - TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; - (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs - .parseTransceiverAndNttManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, payload); + TransceiverStructs.ManagerMessage memory parsedManagerMessage; + (parsedTransceiverMessage, parsedManagerMessage) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, payload); _deliverToNttManager( sourceChainId, parsedTransceiverMessage.sourceNttManagerAddress, parsedTransceiverMessage.recipientNttManagerAddress, - parsedNttManagerMessage + parsedManagerMessage ); } @@ -104,15 +106,15 @@ contract WormholeTransceiver is // parse the encoded Transceiver payload TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; - TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; - (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs - .parseTransceiverAndNttManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, payload); + TransceiverStructs.ManagerMessage memory parsedManagerMessage; + (parsedTransceiverMessage, parsedManagerMessage) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, payload); _deliverToNttManager( sourceChain, parsedTransceiverMessage.sourceNttManagerAddress, parsedTransceiverMessage.recipientNttManagerAddress, - parsedNttManagerMessage + parsedManagerMessage ); } @@ -146,11 +148,13 @@ contract WormholeTransceiver is function _quoteDeliveryPrice( uint16 targetChain, - TransceiverStructs.TransceiverInstruction memory instruction + TransceiverStructs.TransceiverInstruction memory instruction, + uint256 managerExecutionCost ) internal view override returns (uint256 nativePriceQuote) { // Check the special instruction up front to see if we should skip sending via a relayer WormholeTransceiverInstruction memory weIns = parseWormholeTransceiverInstruction(instruction.payload); + if (weIns.shouldSkipRelayerSend) { return 0; } @@ -160,7 +164,9 @@ contract WormholeTransceiver is } if (_shouldRelayViaStandardRelaying(targetChain)) { - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, gasLimit); + (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, 0, attestationGasLimit + managerExecutionCost + ); return cost; } else if (isSpecialRelayingEnabled(targetChain)) { uint256 cost = specialRelayer.quoteDeliveryPrice(getNttManagerToken(), targetChain, 0); @@ -173,10 +179,11 @@ contract WormholeTransceiver is function _sendMessage( uint16 recipientChain, uint256 deliveryPayment, + uint256 managerExecutionCost, address caller, bytes32 recipientNttManagerAddress, TransceiverStructs.TransceiverInstruction memory instruction, - bytes memory nttManagerMessage + bytes memory ManagerMessage ) internal override { ( TransceiverStructs.TransceiverMessage memory transceiverMessage, @@ -185,10 +192,15 @@ contract WormholeTransceiver is WH_TRANSCEIVER_PAYLOAD_PREFIX, toWormholeFormat(caller), recipientNttManagerAddress, - nttManagerMessage, + ManagerMessage, new bytes(0) ); + // Verify that the transceiver message is small enough to be posted on Solana. + if (encodedTransceiverPayload.length > MAX_PAYLOAD_SIZE) { + revert ExceedsMaxPayloadSize(encodedTransceiverPayload.length, MAX_PAYLOAD_SIZE); + } + WormholeTransceiverInstruction memory weIns = parseWormholeTransceiverInstruction(instruction.payload); @@ -198,7 +210,7 @@ contract WormholeTransceiver is fromWormholeFormat(getWormholePeer(recipientChain)), encodedTransceiverPayload, 0, - gasLimit + attestationGasLimit + managerExecutionCost ); emit RelayingInfo(uint8(RelayingType.Standard), deliveryPayment); diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol index feadef784..6db30b173 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol @@ -22,18 +22,25 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce using BooleanFlagLib for BooleanFlag; // ==================== Immutables =============================================== + + /// @dev Maximum payload size for any message. Since posting a message on Solana has a + /// maximum size, all messages are restricted to this size. If this program is + /// only used on EVM chains, this restriction can be removed. + uint16 public constant MAX_PAYLOAD_SIZE = 850; + uint8 public immutable consistencyLevel; IWormhole public immutable wormhole; IWormholeRelayer public immutable wormholeRelayer; ISpecialRelayer public immutable specialRelayer; uint256 immutable wormholeTransceiver_evmChainId; - uint256 public immutable gasLimit; + uint256 public immutable attestationGasLimit; + ManagerType public immutable managerType; // ==================== Constants ================================================ /// @dev Prefix for all TransceiverMessage payloads /// This is 0x99'E''W''H' - /// @notice Magic string (constant value set by messaging provider) that idenfies the payload as an transceiver-emitted payload. + /// @notice Magic string (constant value set by messaging provider) that identifies 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 constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; @@ -51,20 +58,16 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _attestationGasLimit, + ManagerType _managerType ) Transceiver(nttManager) { wormhole = IWormhole(wormholeCoreBridge); wormholeRelayer = IWormholeRelayer(wormholeRelayerAddr); specialRelayer = ISpecialRelayer(specialRelayerAddr); wormholeTransceiver_evmChainId = block.chainid; consistencyLevel = _consistencyLevel; - gasLimit = _gasLimit; - } - - enum RelayingType { - Standard, - Special, - Manual + attestationGasLimit = _attestationGasLimit; + managerType = _managerType; } function _initialize() internal override { @@ -73,14 +76,22 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce } function _initializeTransceiver() internal { - TransceiverStructs.TransceiverInit memory init = TransceiverStructs.TransceiverInit({ - transceiverIdentifier: WH_TRANSCEIVER_INIT_PREFIX, - nttManagerAddress: toWormholeFormat(nttManager), - nttManagerMode: INttManager(nttManager).getMode(), - tokenAddress: toWormholeFormat(nttManagerToken), - tokenDecimals: INttManager(nttManager).tokenDecimals() - }); - wormhole.publishMessage(0, TransceiverStructs.encodeTransceiverInit(init), consistencyLevel); + if (managerType == ManagerType.ERC20) { + TransceiverStructs.TransceiverInit memory init = TransceiverStructs.TransceiverInit({ + transceiverIdentifier: WH_TRANSCEIVER_INIT_PREFIX, + nttManagerAddress: toWormholeFormat(nttManager), + nttManagerMode: INttManager(nttManager).getMode(), + tokenAddress: toWormholeFormat(nttManagerToken), + tokenDecimals: INttManager(nttManager).tokenDecimals() + }); + wormhole.publishMessage( + 0, TransceiverStructs.encodeTransceiverInit(init), consistencyLevel + ); + } else if (managerType == ManagerType.ERC721) { + // Skip emitting message for ERC721. + } else { + revert InvalidManagerType(); + } } function _checkImmutables() internal view override { diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index b3cce54e2..f66390a8a 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -84,8 +84,8 @@ interface IManagerBase { /// @notice Error when the tranceiver already attested to the message. /// To ensure the client does not continue to initiate calls to the attestationReceived function. /// @dev Selector 0x2113894. - /// @param nttManagerMessageHash The hash of the message. - error TransceiverAlreadyAttestedToMessage(bytes32 nttManagerMessageHash); + /// @param ManagerMessageHash The hash of the message. + error TransceiverAlreadyAttestedToMessage(bytes32 ManagerMessageHash); /// @notice Error when the message is not approved. /// @dev Selector 0x451c4fb0. @@ -109,15 +109,6 @@ interface IManagerBase { /// @param chainId The target chain id error PeerNotRegistered(uint16 chainId); - /// @notice Fetch the delivery price for a given recipient chain transfer. - /// @param recipientChain The chain ID of the transfer destination. - /// @return - The delivery prices associated with each endpoint and the total price. - function quoteDeliveryPrice( - uint16 recipientChain, - TransceiverStructs.TransceiverInstruction[] memory transceiverInstructions, - address[] memory enabledTransceivers - ) external view returns (uint256[] memory, uint256); - /// @notice Sets the threshold for the number of attestations required for a message /// to be considered valid. /// @param threshold The new threshold. diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol new file mode 100644 index 000000000..8bd3ede59 --- /dev/null +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "../libraries/TrimmedAmount.sol"; +import "../libraries/TransceiverStructs.sol"; + +import "./IManagerBase.sol"; + +interface INonFungibleNttManager is IManagerBase { + /// @dev The peer on another chain. + struct NonFungibleNttManagerPeer { + bytes32 peerAddress; + } + + /// @notice Emitted when the peer contract is updated. + /// @dev Topic0 + /// 0x1456404e7f41f35c3daac941bb50bad417a66275c3040061b4287d787719599d. + /// @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 nttManager. + /// @param recipient The recipient of the message. + /// @param batchSize The number of NFTs transferred. + /// @param fee The amount of ether sent along with the tx to cover the delivery fee. + /// @param recipientChain The chain ID of the recipient. + /// @param msgSequence The unique sequence ID of the message. + event TransferSent( + bytes32 recipient, uint16 batchSize, uint256 fee, uint16 recipientChain, uint64 msgSequence + ); + + /// @notice Emitted when a transfer has been redeemed + /// (either minted or unlocked on the recipient chain). + /// @dev Topic0 + /// 0x504e6efe18ab9eed10dc6501a417f5b12a2f7f2b1593aed9b89f9bce3cf29a91. + /// @param digest The digest of the message. + event TransferRedeemed(bytes32 indexed digest); + + /// @notice The caller is not the deployer. + error UnexpectedDeployer(address expectedOwner, address caller); + + /// @notice Peer chain ID cannot be zero. + error InvalidPeerChainIdZero(); + + /// @notice Peer cannot be the zero address. + error InvalidPeerZeroAddress(); + + error InvalidOperator(address operator, address expectedOperator); + + error InvalidRecipient(); + error ZeroTokenIds(); + error ExceedsMaxBatchSize(uint256 batchSize, uint256 maxBatchSize); + + /// @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 The mode is invalid. It is neither in LOCKING or BURNING mode. + /// @param mode The mode. + error InvalidMode(uint8 mode); + + /// @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); + + error InvalidTokenIdWidth(uint8 tokenIdWidth); + + /// @notice Sets the corresponding peer. + /// @dev The NonFungiblenttManager that executes the message sets the source NonFungibleNttManager + /// as the peer. + /// @param peerChainId The chain ID of the peer. + /// @param peerContract The address of the peer nttManager contract.c + function setPeer(uint16 peerChainId, bytes32 peerContract) external; + + function getPeer(uint16 chainId_) external view returns (NonFungibleNttManagerPeer memory); + + function getMaxBatchSize() external pure returns (uint8); + + /// @notice Fetch the delivery price for a given recipient chain transfer. + /// @param recipientChain The chain ID of the transfer destination. + /// @param transceiverInstructions The transceiver specific instructions for quoting and sending + /// @return - The delivery prices associated with each enabled endpoint and the total price. + function quoteDeliveryPrice( + uint16 recipientChain, + bytes memory transceiverInstructions + ) external view returns (uint256[] memory, uint256); + + function transfer( + uint256[] memory tokenIds, + uint16 recipientChain, + bytes32 recipient, + bytes memory transceiverInstructions + ) external payable returns (uint64); + + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory payload + ) external; + + function executeMsg( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory message + ) external; +} diff --git a/evm/src/interfaces/INonFungibleNttToken.sol b/evm/src/interfaces/INonFungibleNttToken.sol new file mode 100644 index 000000000..05a5d17fc --- /dev/null +++ b/evm/src/interfaces/INonFungibleNttToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +interface INonFungibleNttToken { + error CallerNotMinter(address caller); + error InvalidMinterZeroAddress(); + + event NewMinter(address newMinter); + + function mint(address account, uint256 tokenId) external; + function setMinter(address newMinter) external; +} diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 910a19e48..a9178702c 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -98,6 +98,15 @@ interface INttManager is IManagerBase { /// @notice Peer cannot have zero decimals. error InvalidPeerDecimals(); + /// @notice Fetch the delivery price for a given recipient chain transfer. + /// @param recipientChain The chain ID of the transfer destination. + /// @param transceiverInstructions The transceiver specific instructions for quoting and sending + /// @return - The delivery prices associated with each enabled endpoint and the total price. + function quoteDeliveryPrice( + uint16 recipientChain, + bytes memory transceiverInstructions + ) external view returns (uint256[] memory, uint256); + /// @notice Transfer a given amount to a recipient on a given chain. This function is called /// by the user to send the token cross-chain. This function will either lock or burn the /// sender's tokens. Finally, this function will call into registered `Endpoint` contracts @@ -152,12 +161,12 @@ interface INttManager is IManagerBase { function attestationReceived( uint16 sourceChainId, bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory payload + TransceiverStructs.ManagerMessage 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. + /// as an ManagerMessage 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 chain id of the sender. @@ -166,7 +175,7 @@ interface INttManager is IManagerBase { function executeMsg( uint16 sourceChainId, bytes32 sourceNttManagerAddress, - TransceiverStructs.NttManagerMessage memory message + TransceiverStructs.ManagerMessage memory message ) external; /// @notice Returns the number of decimals of the token managed by the NttManager. diff --git a/evm/src/interfaces/ITransceiver.sol b/evm/src/interfaces/ITransceiver.sol index deff48aca..76a01e9f2 100644 --- a/evm/src/interfaces/ITransceiver.sol +++ b/evm/src/interfaces/ITransceiver.sol @@ -41,23 +41,31 @@ interface ITransceiver { /// @param recipientChain The Wormhole chain ID of the target chain. /// @param instruction An additional Instruction provided by the Transceiver to be /// executed on the recipient chain. + /// @param managerExecutionCost The cost of executing the manager message on the recipient chain. + /// Depending on the target chain, the units may vary. For example, on Ethereum, this is + /// the additional gas cost (in excess of the attestation cost) for executing the manager message. /// @return deliveryPrice The cost of delivering a message to the recipient chain, /// in this chain's native token. function quoteDeliveryPrice( uint16 recipientChain, - TransceiverStructs.TransceiverInstruction memory instruction + TransceiverStructs.TransceiverInstruction memory instruction, + uint256 managerExecutionCost ) external view returns (uint256); /// @dev Send a message to another chain. /// @param recipientChain The Wormhole chain ID of the recipient. /// @param instruction An additional Instruction provided by the Transceiver to be /// executed on the recipient chain. - /// @param nttManagerMessage A message to be sent to the nttManager on the recipient chain. + /// @param ManagerMessage A message to be sent to the nttManager on the recipient chain. + /// @param managerExecutionCost The cost of executing the manager message on the recipient chain. + /// Depending on the target chain, the units may vary. For example, on Ethereum, this is + /// the additional gas cost (in excess of the attestation cost) for executing the manager message. function sendMessage( uint16 recipientChain, TransceiverStructs.TransceiverInstruction memory instruction, - bytes memory nttManagerMessage, - bytes32 recipientNttManagerAddress + bytes memory ManagerMessage, + bytes32 recipientNttManagerAddress, + uint256 managerExecutionCost ) external payable; /// @notice Upgrades the transceiver to a new implementation. diff --git a/evm/src/interfaces/IWormholeTransceiver.sol b/evm/src/interfaces/IWormholeTransceiver.sol index ba2de4dd2..c81c650ef 100644 --- a/evm/src/interfaces/IWormholeTransceiver.sol +++ b/evm/src/interfaces/IWormholeTransceiver.sol @@ -56,6 +56,12 @@ interface IWormholeTransceiver is IWormholeTransceiverState { /// @param vaaHash The hash of the VAA. error TransferAlreadyCompleted(bytes32 vaaHash); + /// @notice Error when the payload size exceeds the maximum allowed size. + /// @dev Selector: 0xf39ac4ba. + /// @param payloadSize The size of the payload. + /// @param maxPayloadSize The maximum allowed size. + error ExceedsMaxPayloadSize(uint256 payloadSize, uint256 maxPayloadSize); + /// @notice Receive an attested message from the verification layer. /// This function should verify the `encodedVm` and then deliver the attestation /// to the transceiver NttManager contract. diff --git a/evm/src/interfaces/IWormholeTransceiverState.sol b/evm/src/interfaces/IWormholeTransceiverState.sol index 50640309a..bc4088077 100644 --- a/evm/src/interfaces/IWormholeTransceiverState.sol +++ b/evm/src/interfaces/IWormholeTransceiverState.sol @@ -4,6 +4,17 @@ pragma solidity >=0.8.8 <0.9.0; import "../libraries/TransceiverStructs.sol"; interface IWormholeTransceiverState { + enum ManagerType { + ERC20, + ERC721 + } + + enum RelayingType { + Standard, + Special, + Manual + } + /// @notice Emitted when a message is sent from the transceiver. /// @dev Topic0 /// 0x375a56c053c4d19a2e3445e97b7a28bf4e908617ce6d766e1e03a9d3f5276271. @@ -38,6 +49,9 @@ interface IWormholeTransceiverState { /// @param isEvm A boolean indicating whether relaying is enabled. event SetIsWormholeEvmChain(uint16 chainId, bool isEvm); + /// @notice Emitted when a ManagerType does not exist. + error InvalidManagerType(); + /// @notice Additonal messages are not allowed. /// @dev Selector: 0xc504ea29. error UnexpectedAdditionalMessages(); @@ -113,4 +127,7 @@ interface IWormholeTransceiverState { /// @param chainId The Wormhole chain ID to set. /// @param isRelayingEnabled A boolean indicating whether special relaying is enabled. function setIsSpecialRelayingEnabled(uint16 chainId, bool isRelayingEnabled) external; + + /// @notice Returns the maximum payload size for a Wormhole Transceiver message. + function MAX_PAYLOAD_SIZE() external view returns (uint16); } diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index e8afb6acc..f7dc30d3b 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -19,18 +19,23 @@ library TransceiverStructs { /// @param prefix The prefix that was found in the encoded message. error IncorrectPrefix(bytes4 prefix); error UnorderedInstructions(); + error TokenIdTooLarge(uint256 tokenId, uint256 max); /// @dev Prefix for all NativeTokenTransfer payloads /// This is 0x99'N''T''T' bytes4 constant NTT_PREFIX = 0x994E5454; - /// @dev Message emitted and received by the nttManager contract. + /// @dev Prefix for all NonFungibleNativeTokenTransfer payloads + /// This is 0x99'N''F''T' + bytes4 constant NON_FUNGIBLE_NTT_PREFIX = 0x994E4654; + + /// @dev Message emitted and received by any Manager contract variant. /// The wire format is as follows: /// - id - 32 bytes /// - sender - 32 bytes /// - payloadLength - 2 bytes /// - payload - `payloadLength` bytes - struct NttManagerMessage { + struct ManagerMessage { /// @notice unique message identifier /// @dev This is incrementally assigned on EVM chains, but this is not /// guaranteed on other runtimes. @@ -41,14 +46,14 @@ library TransceiverStructs { bytes payload; } - function nttManagerMessageDigest( + function managerMessageDigest( uint16 sourceChainId, - NttManagerMessage memory m + ManagerMessage memory m ) public pure returns (bytes32) { - return keccak256(abi.encodePacked(sourceChainId, encodeNttManagerMessage(m))); + return keccak256(abi.encodePacked(sourceChainId, encodeManagerMessage(m))); } - function encodeNttManagerMessage(NttManagerMessage memory m) + function encodeManagerMessage(ManagerMessage memory m) public pure returns (bytes memory encoded) @@ -60,20 +65,20 @@ library TransceiverStructs { return abi.encodePacked(m.id, m.sender, payloadLength, m.payload); } - /// @notice Parse a NttManagerMessage. + /// @notice Parse a ManagerMessage. /// @param encoded The byte array corresponding to the encoded message - /// @return nttManagerMessage The parsed NttManagerMessage struct. - function parseNttManagerMessage(bytes memory encoded) + /// @return managerMessage The parsed ManagerMessage struct. + function parseManagerMessage(bytes memory encoded) public pure - returns (NttManagerMessage memory nttManagerMessage) + returns (ManagerMessage memory managerMessage) { uint256 offset = 0; - (nttManagerMessage.id, offset) = encoded.asBytes32Unchecked(offset); - (nttManagerMessage.sender, offset) = encoded.asBytes32Unchecked(offset); + (managerMessage.id, offset) = encoded.asBytes32Unchecked(offset); + (managerMessage.sender, offset) = encoded.asBytes32Unchecked(offset); uint256 payloadLength; (payloadLength, offset) = encoded.asUint16Unchecked(offset); - (nttManagerMessage.payload, offset) = encoded.sliceUnchecked(offset, payloadLength); + (managerMessage.payload, offset) = encoded.sliceUnchecked(offset, payloadLength); encoded.checkLength(offset); } @@ -143,6 +148,152 @@ library TransceiverStructs { encoded.checkLength(offset); } + /// @dev Non-Fungible Native Token Transfer payload. + /// The wire format is as follows: + /// - NON_FUNGIBLE_NTT_PREFIX - 4 bytes + /// - to - 32 bytes + /// - toChain - 2 bytes + /// - batchSize - 2 bytes + /// - for each encoded tokenId: + /// - tokenIdWidth - 1 byte + /// - tokenId - `tokenIdWidth` bytes + /// - payloadLength - 2 bytes + /// - payload - `payloadLength` bytes + + struct NonFungibleNativeTokenTransfer { + /// @notice Address of the recipient. + bytes32 to; + /// @notice Chain ID of the recipient + uint16 toChain; + /// @notice Array of tokenIds. + uint256[] tokenIds; + /// @notice Arbitrary payload per manager implementation. + bytes payload; + } + + function encodeNonFungibleNativeTokenTransfer( + NonFungibleNativeTokenTransfer memory nft, + uint8 tokenIdWidth + ) public pure returns (bytes memory encoded) { + uint16 batchSize = uint16(nft.tokenIds.length); + uint16 payloadLen = uint16(nft.payload.length); + + bytes memory encodedTokenIds = abi.encodePacked(batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + encodedTokenIds = abi.encodePacked( + encodedTokenIds, tokenIdWidth, encodeTokenId(nft.tokenIds[i], tokenIdWidth) + ); + } + + return abi.encodePacked( + NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodedTokenIds, payloadLen, nft.payload + ); + } + + function parseNonFungibleNativeTokenTransfer(bytes memory encoded) + public + pure + returns (NonFungibleNativeTokenTransfer memory nonFungibleNtt) + { + uint256 offset = 0; + bytes4 prefix; + (prefix, offset) = encoded.asBytes4Unchecked(offset); + if (prefix != NON_FUNGIBLE_NTT_PREFIX) { + revert IncorrectPrefix(prefix); + } + + (nonFungibleNtt.to, offset) = encoded.asBytes32Unchecked(offset); + (nonFungibleNtt.toChain, offset) = encoded.asUint16Unchecked(offset); + + uint16 batchSize; + (batchSize, offset) = encoded.asUint16Unchecked(offset); + + uint256[] memory tokenIds = new uint256[](batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + uint256 tokenId; + (offset, tokenId) = parseTokenId(encoded, offset); + tokenIds[i] = tokenId; + } + nonFungibleNtt.tokenIds = tokenIds; + + // Decode arbitrary payload. + uint16 payloadLength; + (payloadLength, offset) = encoded.asUint16Unchecked(offset); + (nonFungibleNtt.payload, offset) = encoded.sliceUnchecked(offset, payloadLength); + + encoded.checkLength(offset); + } + + function encodeTokenId( + uint256 tokenId, + uint8 tokenIdWidth + ) public pure returns (bytes memory) { + if (tokenIdWidth == 1) { + if (tokenId > type(uint8).max) { + revert TokenIdTooLarge(tokenId, type(uint8).max); + } else { + return abi.encodePacked(uint8(tokenId)); + } + } else if (tokenIdWidth == 2) { + if (tokenId > type(uint16).max) { + revert TokenIdTooLarge(tokenId, type(uint16).max); + } else { + return abi.encodePacked(uint16(tokenId)); + } + } else if (tokenIdWidth == 4) { + if (tokenId > type(uint32).max) { + revert TokenIdTooLarge(tokenId, type(uint32).max); + } else { + return abi.encodePacked(uint32(tokenId)); + } + } else if (tokenIdWidth == 8) { + if (tokenId > type(uint64).max) { + revert TokenIdTooLarge(tokenId, type(uint64).max); + } else { + return abi.encodePacked(uint64(tokenId)); + } + } else if (tokenIdWidth == 16) { + if (tokenId > type(uint128).max) { + revert TokenIdTooLarge(tokenId, type(uint128).max); + } else { + return abi.encodePacked(uint128(tokenId)); + } + } else { + return abi.encodePacked(uint256(tokenId)); + } + } + + function parseTokenId( + bytes memory encoded, + uint256 offset + ) public pure returns (uint256 nextOffset, uint256 tokenId) { + uint8 tokenIdWidth; + (tokenIdWidth, nextOffset) = encoded.asUint8Unchecked(offset); + if (tokenIdWidth == 1) { + uint8 tokenIdValue; + (tokenIdValue, nextOffset) = encoded.asUint8Unchecked(nextOffset); + tokenId = tokenIdValue; + } else if (tokenIdWidth == 2) { + uint16 tokenIdValue; + (tokenIdValue, nextOffset) = encoded.asUint16Unchecked(nextOffset); + tokenId = tokenIdValue; + } else if (tokenIdWidth == 4) { + uint32 tokenIdValue; + (tokenIdValue, nextOffset) = encoded.asUint32Unchecked(nextOffset); + tokenId = tokenIdValue; + } else if (tokenIdWidth == 8) { + uint64 tokenIdValue; + (tokenIdValue, nextOffset) = encoded.asUint64Unchecked(nextOffset); + tokenId = tokenIdValue; + } else if (tokenIdWidth == 16) { + uint128 tokenIdValue; + (tokenIdValue, nextOffset) = encoded.asUint128Unchecked(nextOffset); + tokenId = tokenIdValue; + } else { + (tokenId, nextOffset) = encoded.asUint256Unchecked(nextOffset); + } + } + /// @dev Message emitted by Transceiver implementations. /// Each message includes an Transceiver-specified 4-byte prefix. /// The wire format is as follows: @@ -199,13 +350,13 @@ library TransceiverStructs { bytes4 prefix, bytes32 sourceNttManagerAddress, bytes32 recipientNttManagerAddress, - bytes memory nttManagerMessage, + bytes memory managerMessage, bytes memory transceiverPayload ) public pure returns (TransceiverMessage memory, bytes memory) { TransceiverMessage memory transceiverMessage = TransceiverMessage({ sourceNttManagerAddress: sourceNttManagerAddress, recipientNttManagerAddress: recipientNttManagerAddress, - nttManagerPayload: nttManagerMessage, + nttManagerPayload: managerMessage, transceiverPayload: transceiverPayload }); bytes memory encoded = encodeTransceiverMessage(prefix, transceiverMessage); @@ -246,22 +397,22 @@ library TransceiverStructs { } /// @dev Parses the payload of an Transceiver message and returns - /// the parsed NttManagerMessage struct. + /// the parsed ManagerMessage struct. /// @param expectedPrefix The prefix that should be encoded in the nttManager message. /// @param payload The payload sent across the wire. - function parseTransceiverAndNttManagerMessage( + function parseTransceiverAndManagerMessage( bytes4 expectedPrefix, bytes memory payload - ) public pure returns (TransceiverMessage memory, NttManagerMessage memory) { + ) public pure returns (TransceiverMessage memory, ManagerMessage memory) { // parse the encoded message payload from the Transceiver TransceiverMessage memory parsedTransceiverMessage = parseTransceiverMessage(expectedPrefix, payload); // parse the encoded message payload from the NttManager - NttManagerMessage memory parsedNttManagerMessage = - parseNttManagerMessage(parsedTransceiverMessage.nttManagerPayload); + ManagerMessage memory parsedManagerMessage = + parseManagerMessage(parsedTransceiverMessage.nttManagerPayload); - return (parsedTransceiverMessage, parsedNttManagerMessage); + return (parsedTransceiverMessage, parsedManagerMessage); } /// @dev Variable-length transceiver-specific instruction that can be passed by the caller to the nttManager. diff --git a/evm/src/mocks/DummyNft.sol b/evm/src/mocks/DummyNft.sol new file mode 100644 index 000000000..6efd5bae8 --- /dev/null +++ b/evm/src/mocks/DummyNft.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.8 <0.9.0; + +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {INonFungibleNttManager} from "../interfaces/INonFungibleNttManager.sol"; + +contract DummyNft is ERC721 { + // Common URI for all NFTs handled by this contract. + bytes32 private immutable _baseUri; + uint8 private immutable _baseUriLength; + + error BaseUriEmpty(); + error BaseUriTooLong(); + + constructor(bytes memory baseUri) ERC721("DummyNft", "DNFT") { + if (baseUri.length == 0) { + revert BaseUriEmpty(); + } + if (baseUri.length > 32) { + revert BaseUriTooLong(); + } + + _baseUri = bytes32(baseUri); + _baseUriLength = uint8(baseUri.length); + } + + // NOTE: this is purposefully not called mint() to so we can test that in + // locking mode the NttManager contract doesn't call mint (or burn) + function mintDummy(address to, uint256 amount) public { + _safeMint(to, amount); + } + + function mint(address, uint256) public virtual { + revert("Locking nttManager should not call 'mint()'"); + } + + function burn(address, uint256) public virtual { + revert("Locking nttManager should not call 'burn()'"); + } + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function _baseURI() internal view virtual override returns (string memory baseUri) { + baseUri = new string(_baseUriLength); + bytes32 tmp = _baseUri; + assembly ("memory-safe") { + mstore(add(baseUri, 32), tmp) + } + } +} + +contract DummyNftMintAndBurn is DummyNft { + bool private reentrant; + address private owner; + + constructor(bytes memory baseUri) DummyNft(baseUri) { + owner = msg.sender; + } + + function setReentrant(bool enabled) public { + require(msg.sender == owner, "DummyNftMintAndBurn: not owner"); + reentrant = enabled; + } + + function mint(address to, uint256 tokenId) public override { + _safeMint(to, tokenId); + + _callback(); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + + _callback(); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) public override { + super.safeTransferFrom(from, to, tokenId, ""); + + _callback(); + } + + function _callback() public { + if (!reentrant) { + return; + } + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + + INonFungibleNttManager(msg.sender).transfer( + tokenIds, 69, bytes32(uint256(uint160(msg.sender))), "" + ); + } +} diff --git a/evm/test/IntegrationRelayer.t.sol b/evm/test/IntegrationRelayer.t.sol index 2324c3faa..4fef58691 100755 --- a/evm/test/IntegrationRelayer.t.sol +++ b/evm/test/IntegrationRelayer.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/NttManager/NttManager.sol"; +import "../src/NativeTransfers/NttManager.sol"; import "../src/Transceiver/Transceiver.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IRateLimiter.sol"; @@ -111,7 +111,8 @@ contract TestEndToEndRelayer is address(relayerSource), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1 = MockWormholeTransceiverContract( @@ -144,7 +145,8 @@ contract TestEndToEndRelayer is address(relayerTarget), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain2 = MockWormholeTransceiverContract( @@ -202,7 +204,7 @@ contract TestEndToEndRelayer is uint256 userBalanceBefore = token1.balanceOf(address(userA)); uint256 priceQuote1 = wormholeTransceiverChain1.quoteDeliveryPrice( - chainId2, buildTransceiverInstruction(false) + chainId2, buildTransceiverInstruction(false), 0 ); bytes memory instructions = encodeTransceiverInstruction(false); @@ -339,7 +341,7 @@ contract TestEndToEndRelayer is nttManagerChain1.transfer{ value: wormholeTransceiverChain1.quoteDeliveryPrice( - chainId2, buildTransceiverInstruction(false) + chainId2, buildTransceiverInstruction(false), 0 ) }( sendingAmount, @@ -391,7 +393,7 @@ contract TestEndToEndRelayer is supplyBefore = token2.totalSupply(); nttManagerChain2.transfer{ value: wormholeTransceiverChain2.quoteDeliveryPrice( - chainId1, buildTransceiverInstruction(false) + chainId1, buildTransceiverInstruction(false), 0 ) }( sendingAmount, @@ -497,7 +499,8 @@ contract TestRelayerEndToEndManual is TestEndToEndRelayerBase, IRateLimiterEvent address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1 = MockWormholeTransceiverContract( address(new ERC1967Proxy(address(wormholeTransceiverChain1), "")) @@ -524,7 +527,8 @@ contract TestRelayerEndToEndManual is TestEndToEndRelayerBase, IRateLimiterEvent address(relayer), // TODO - add support for this later address(0x0), // TODO - add support for this later FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain2 = MockWormholeTransceiverContract( address(new ERC1967Proxy(address(wormholeTransceiverChain2), "")) @@ -570,7 +574,7 @@ contract TestRelayerEndToEndManual is TestEndToEndRelayerBase, IRateLimiterEvent vm.deal(userA, 1 ether); nttManagerChain1.transfer{ value: wormholeTransceiverChain1.quoteDeliveryPrice( - chainId2, buildTransceiverInstruction(false) + chainId2, buildTransceiverInstruction(false), 0 ) }( sendingAmount, @@ -701,7 +705,7 @@ contract TestRelayerEndToEndManual is TestEndToEndRelayerBase, IRateLimiterEvent vm.deal(userA, 1 ether); nttManagerChain1.transfer{ value: wormholeTransceiverChain1.quoteDeliveryPrice( - chainId2, buildTransceiverInstruction(false) + chainId2, buildTransceiverInstruction(false), 0 ) }( sendingAmount, diff --git a/evm/test/IntegrationStandalone.t.sol b/evm/test/IntegrationStandalone.t.sol index abe40de5b..c46545481 100755 --- a/evm/test/IntegrationStandalone.t.sol +++ b/evm/test/IntegrationStandalone.t.sol @@ -4,13 +4,14 @@ pragma solidity >=0.8.8 <0.9.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/NttManager/NttManager.sol"; +import "../src/NativeTransfers/NttManager.sol"; import "../src/Transceiver/Transceiver.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IRateLimiter.sol"; import "../src/interfaces/ITransceiver.sol"; import "../src/interfaces/IManagerBase.sol"; import "../src/interfaces/IRateLimiterEvents.sol"; +import "../src/interfaces/IWormholeTransceiverState.sol"; import {Utils} from "./libraries/Utils.sol"; import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; import "../src/interfaces/IWormholeTransceiver.sol"; @@ -24,7 +25,6 @@ import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; import "wormhole-solidity-sdk/Utils.sol"; -//import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; contract TestEndToEndBase is Test, IRateLimiterEvents { NttManager nttManagerChain1; @@ -77,7 +77,8 @@ contract TestEndToEndBase is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1 = MockWormholeTransceiverContract( address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) @@ -114,7 +115,8 @@ contract TestEndToEndBase is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain2 = MockWormholeTransceiverContract( address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) @@ -475,7 +477,8 @@ contract TestEndToEndBase is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1_2 = MockWormholeTransceiverContract( @@ -493,7 +496,8 @@ contract TestEndToEndBase is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain2_2 = MockWormholeTransceiverContract( diff --git a/evm/test/NonFungibleNttIntegrationRelayer.t.sol b/evm/test/NonFungibleNttIntegrationRelayer.t.sol new file mode 100755 index 000000000..91218f5e3 --- /dev/null +++ b/evm/test/NonFungibleNttIntegrationRelayer.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/interfaces/INonFungibleNttManager.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IWormholeTransceiverState.sol"; + +import "../src/NativeTransfers/NonFungibleNttManager.sol"; +import "../src/NativeTransfers/shared/TransceiverRegistry.sol"; +import "../src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol"; +import "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "./interfaces/ITransceiverReceiver.sol"; + +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; +import "wormhole-solidity-sdk/Utils.sol"; + +import "./libraries/TransceiverHelpers.sol"; +import "./libraries/NttManagerHelpers.sol"; +import "./libraries/NonFungibleNttManagerHelpers.sol"; +import {Utils} from "./libraries/Utils.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "../src/libraries/external/OwnableUpgradeable.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "./mocks/MockTransceivers.sol"; +import "../src/mocks/DummyNft.sol"; + +import {WormholeRelayerBasicTest} from "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; + +contract TestNonFungibleNttManagerWithRelayer is NonFungibleNttHelpers, WormholeRelayerBasicTest { + uint16 constant SOURCE_CHAIN_ID = 6; + uint16 constant TARGET_CHAIN_ID = 5; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + uint8 constant TOKEN_ID_WIDTH = 2; + + DummyNftMintAndBurn sourceNft; + DummyNftMintAndBurn targetNft; + INonFungibleNttManager sourceManager; + INonFungibleNttManager targetManager; + WormholeTransceiver sourceTransceiver; + WormholeTransceiver targetTransceiver; + + address sender = makeAddr("sender"); + address recipient = makeAddr("recipient"); + + constructor() { + setTestnetForkChains(SOURCE_CHAIN_ID, TARGET_CHAIN_ID); + } + + function setUpSource() public override { + guardianSource = new WormholeSimulator(address(wormholeSource), DEVNET_GUARDIAN_PK); + sourceNft = new DummyNftMintAndBurn(bytes("https://metadata.dn69.com/y/")); + sourceManager = deployNonFungibleManager( + address(sourceNft), IManagerBase.Mode.LOCKING, SOURCE_CHAIN_ID, true, TOKEN_ID_WIDTH + ); + sourceTransceiver = deployWormholeTransceiver( + guardianSource, + address(sourceManager), + address(relayerSource), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + + sourceManager.setTransceiver(address(sourceTransceiver)); + sourceManager.setThreshold(1); + + vm.deal(sender, 10 ether); + } + + function setUpTarget() public override { + guardianTarget = new WormholeSimulator(address(wormholeTarget), DEVNET_GUARDIAN_PK); + targetNft = new DummyNftMintAndBurn(bytes("https://metadata.dn420.com/y/")); + targetManager = deployNonFungibleManager( + address(targetNft), IManagerBase.Mode.BURNING, TARGET_CHAIN_ID, true, TOKEN_ID_WIDTH + ); + targetTransceiver = deployWormholeTransceiver( + guardianTarget, + address(targetManager), + address(relayerTarget), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + + targetManager.setTransceiver(address(targetTransceiver)); + targetManager.setThreshold(1); + + vm.deal(recipient, 10 ether); + } + + /// TODO: Fuzz test this in the same way as `test_lockAndMint` in `NonFungibleNttManager.t.sol`. + function testRoundTripEvm() public { + // Cross-register the contracts. + { + vm.selectFork(sourceFork); + sourceTransceiver.setWormholePeer( + TARGET_CHAIN_ID, toWormholeFormat(address(targetTransceiver)) + ); + sourceTransceiver.setIsWormholeRelayingEnabled(TARGET_CHAIN_ID, true); + sourceTransceiver.setIsWormholeEvmChain(TARGET_CHAIN_ID, true); + sourceManager.setPeer(TARGET_CHAIN_ID, toWormholeFormat(address(targetManager))); + + vm.selectFork(targetFork); + targetTransceiver.setWormholePeer( + SOURCE_CHAIN_ID, toWormholeFormat(address(sourceTransceiver)) + ); + targetTransceiver.setIsWormholeRelayingEnabled(SOURCE_CHAIN_ID, true); + targetTransceiver.setIsWormholeEvmChain(SOURCE_CHAIN_ID, true); + targetManager.setPeer(SOURCE_CHAIN_ID, toWormholeFormat(address(sourceManager))); + } + + vm.selectFork(sourceFork); + vm.recordLogs(); + + // Mint a token on the source chain. + uint16 nftCount = 1; + uint256[] memory tokenIds = _mintNftBatch(sourceNft, sender, nftCount, 0); + + // Fetch quote and execute the transfer. + { + bytes memory transceiverInstructions = + _encodeTransceiverInstruction(false, sourceTransceiver); + (, uint256 quote) = + sourceManager.quoteDeliveryPrice(TARGET_CHAIN_ID, transceiverInstructions); + console.log("Quote: ", quote); + } + } +} diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol new file mode 100644 index 000000000..0f8f63cfd --- /dev/null +++ b/evm/test/NonFungibleNttManager.t.sol @@ -0,0 +1,921 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/console.sol"; +import "forge-std/Vm.sol"; + +import "../src/interfaces/INonFungibleNttManager.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IWormholeTransceiverState.sol"; + +import "../src/NativeTransfers/NonFungibleNttManager.sol"; +import "../src/NativeTransfers/shared/TransceiverRegistry.sol"; +import "../src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol"; +import "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "./interfaces/ITransceiverReceiver.sol"; + +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; +import "wormhole-solidity-sdk/Utils.sol"; + +import "./libraries/TransceiverHelpers.sol"; +import "./libraries/NttManagerHelpers.sol"; +import "./libraries/NonFungibleNttManagerHelpers.sol"; +import {Utils} from "./libraries/Utils.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "../src/libraries/external/OwnableUpgradeable.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "./mocks/MockTransceivers.sol"; +import "../src/mocks/DummyNft.sol"; + +contract TestNonFungibleNttManager is NonFungibleNttHelpers { + using BytesParsing for bytes; + + uint16 constant chainIdOne = 2; + uint16 constant chainIdTwo = 6; + uint16 constant chainIdThree = 10; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + + // Constructor args. + uint8 consistencyLevel = 1; + uint256 baseGasLimit = 500000; + uint8 tokenIdWidth = 2; + + address owner = makeAddr("owner"); + + // Deployed contracts. + DummyNftMintAndBurn nftOne; + DummyNftMintAndBurn nftTwo; + INonFungibleNttManager managerOne; + INonFungibleNttManager managerTwo; + INonFungibleNttManager managerThree; + WormholeTransceiver transceiverOne; + WormholeTransceiver transceiverTwo; + WormholeTransceiver transceiverThree; + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + vm.createSelectFork(url); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + // Deploy contracts as owner. + vm.startPrank(owner); + + // Nft collection. + nftOne = new DummyNftMintAndBurn(bytes("https://metadata.dn69.com/y/")); + nftTwo = new DummyNftMintAndBurn(bytes("https://metadata.dn420.com/y/")); + + // Managers. + managerOne = deployNonFungibleManager( + address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, true, tokenIdWidth + ); + managerTwo = deployNonFungibleManager( + address(nftTwo), IManagerBase.Mode.BURNING, chainIdTwo, true, tokenIdWidth + ); + managerThree = deployNonFungibleManager( + address(nftOne), IManagerBase.Mode.BURNING, chainIdThree, true, tokenIdWidth + ); + + // Wormhole Transceivers. + transceiverOne = deployWormholeTransceiver( + guardian, address(managerOne), address(0), consistencyLevel, baseGasLimit + ); + transceiverTwo = deployWormholeTransceiver( + guardian, address(managerTwo), address(0), consistencyLevel, baseGasLimit + ); + transceiverThree = deployWormholeTransceiver( + guardian, address(managerThree), address(0), consistencyLevel, baseGasLimit + ); + + transceiverOne.setWormholePeer(chainIdTwo, toWormholeFormat(address(transceiverTwo))); + transceiverTwo.setWormholePeer(chainIdOne, toWormholeFormat(address(transceiverOne))); + transceiverTwo.setWormholePeer(chainIdThree, toWormholeFormat(address(transceiverThree))); + transceiverThree.setWormholePeer(chainIdTwo, toWormholeFormat(address(transceiverTwo))); + + // Register transceivers and peers. + managerOne.setTransceiver(address(transceiverOne)); + managerTwo.setTransceiver(address(transceiverTwo)); + managerThree.setTransceiver(address(transceiverThree)); + + managerOne.setPeer(chainIdTwo, toWormholeFormat(address(managerTwo))); + managerTwo.setPeer(chainIdOne, toWormholeFormat(address(managerOne))); + managerTwo.setPeer(chainIdThree, toWormholeFormat(address(managerThree))); + managerThree.setPeer(chainIdTwo, toWormholeFormat(address(managerTwo))); + + vm.stopPrank(); + } + + /// @dev This function assumes that the two managers are already cross registered. + function _setupMultiTransceiverManagers( + INonFungibleNttManager sourceManager, + INonFungibleNttManager targetManager + ) internal returns (WormholeTransceiver) { + vm.startPrank(owner); + + // Deploy two new transceivers. + WormholeTransceiver sourceTransceiver = deployWormholeTransceiver( + guardian, address(sourceManager), address(0), consistencyLevel, baseGasLimit + ); + WormholeTransceiver targetTransceiver = deployWormholeTransceiver( + guardian, address(targetManager), address(0), consistencyLevel, baseGasLimit + ); + + // Register transceivers and peers. + sourceManager.setTransceiver(address(sourceTransceiver)); + targetManager.setTransceiver(address(targetTransceiver)); + + sourceTransceiver.setWormholePeer( + targetManager.chainId(), toWormholeFormat(address(targetTransceiver)) + ); + targetTransceiver.setWormholePeer( + sourceManager.chainId(), toWormholeFormat(address(sourceTransceiver)) + ); + + sourceManager.setThreshold(2); + targetManager.setThreshold(2); + + vm.stopPrank(); + + return targetTransceiver; + } + + // ================================== Admin Tests ================================== + + function test_cannotDeployWithInvalidTokenIdWidth(uint8 _tokenIdWidth) public { + vm.assume( + _tokenIdWidth != 1 && _tokenIdWidth != 2 && _tokenIdWidth != 4 && _tokenIdWidth != 8 + && _tokenIdWidth != 16 && _tokenIdWidth != 32 + ); + + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.InvalidTokenIdWidth.selector, _tokenIdWidth + ) + ); + new NonFungibleNttManager( + address(nftOne), _tokenIdWidth, IManagerBase.Mode.BURNING, chainIdOne + ); + } + + /// @dev We perform an upgrade with the existing tokenIdWidth to show that upgrades + /// are possible with the same tokenIdWidth. There is no specific error thrown when + /// the immutables check throws. + function test_cannotUpgradeWithDifferentTokenIdWidth() public { + vm.startPrank(owner); + { + NonFungibleNttManager newImplementation = new NonFungibleNttManager( + address(nftOne), tokenIdWidth, IManagerBase.Mode.LOCKING, chainIdOne + ); + managerOne.upgrade(address(newImplementation)); + } + + uint8 newTokenIdWidth = 4; + NonFungibleNttManager newImplementation = new NonFungibleNttManager( + address(nftOne), newTokenIdWidth, IManagerBase.Mode.LOCKING, chainIdOne + ); + + vm.expectRevert(); + managerOne.upgrade(address(newImplementation)); + } + + function test_cannotInitalizeNotDeployer() public { + // Don't initialize. + vm.prank(owner); + INonFungibleNttManager dummyManager = deployNonFungibleManager( + address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, false, tokenIdWidth + ); + + vm.prank(makeAddr("notOwner")); + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.UnexpectedDeployer.selector, owner, makeAddr("notOwner") + ) + ); + NonFungibleNttManager(address(dummyManager)).initialize(); + } + + function test_setPeerAsOwner() public { + uint16 chainId = 69; + bytes32 newPeer = toWormholeFormat(makeAddr("newPeer")); + + vm.startPrank(owner); + + bytes32 oldPeer = managerOne.getPeer(chainId).peerAddress; + assertEq(oldPeer, bytes32(0), "Old peer should be zero address"); + + managerOne.setPeer(chainId, newPeer); + + bytes32 updatedPeer = managerOne.getPeer(chainId).peerAddress; + assertEq(updatedPeer, newPeer, "Peer should be updated"); + + vm.stopPrank(); + } + + function test_updatePeerAsOwner() public { + uint16 chainId = 69; + bytes32 newPeer = toWormholeFormat(makeAddr("newPeer")); + bytes32 updatedPeer = toWormholeFormat(makeAddr("updatedPeer")); + + vm.startPrank(owner); + + // Set the peer to newPeer. + { + managerOne.setPeer(chainId, newPeer); + bytes32 peer = managerOne.getPeer(chainId).peerAddress; + assertEq(peer, newPeer, "Peer should be newPeer"); + } + + // Update to a new peer. + { + managerOne.setPeer(chainId, updatedPeer); + bytes32 peer = managerOne.getPeer(chainId).peerAddress; + assertEq(peer, updatedPeer, "Peer should be updatedPeer"); + } + + vm.stopPrank(); + } + + function test_cannotUpdatePeerOwnerOnly() public { + uint16 chainId = 69; + bytes32 newPeer = toWormholeFormat(makeAddr("newPeer")); + + vm.prank(makeAddr("notOwner")); + vm.expectRevert( + abi.encodeWithSelector( + OwnableUpgradeable.OwnableUnauthorizedAccount.selector, makeAddr("notOwner") + ) + ); + managerOne.setPeer(chainId, newPeer); + } + + function test_cannotSetPeerWithZeroChainId() public { + uint16 chainId = 0; + bytes32 newPeer = toWormholeFormat(makeAddr("newPeer")); + + vm.prank(owner); + vm.expectRevert(INonFungibleNttManager.InvalidPeerChainIdZero.selector); + managerOne.setPeer(chainId, newPeer); + } + + function test_cannotSetPeerWithZeroAddress() public { + uint16 chainId = 69; + bytes32 newPeer = bytes32(0); + + vm.prank(owner); + vm.expectRevert(INonFungibleNttManager.InvalidPeerZeroAddress.selector); + managerOne.setPeer(chainId, newPeer); + } + + // ============================ Serde Tests ====================================== + + function test_serde( + uint8 tokenIdWidth, + bytes32 to, + uint16 toChain, + bytes memory payload, + uint256 nftCount, + uint256 startId + ) public { + // Narrow the search. + tokenIdWidth = uint8(bound(tokenIdWidth, 1, 32)); + // Ugly, but necessary. + vm.assume( + tokenIdWidth == 1 || tokenIdWidth == 2 || tokenIdWidth == 4 || tokenIdWidth == 8 + || tokenIdWidth == 16 || tokenIdWidth == 32 + ); + vm.assume(to != bytes32(0)); + vm.assume(toChain != 0); + nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); + startId = bound(startId, 0, _getMaxFromTokenIdWidth(tokenIdWidth) - nftCount); + + TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = TransceiverStructs + .NonFungibleNativeTokenTransfer({ + to: to, + toChain: toChain, + payload: payload, + tokenIds: _createBatchTokenIds(nftCount, startId) + }); + + bytes memory encoded = + TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nftTransfer, tokenIdWidth); + + TransceiverStructs.NonFungibleNativeTokenTransfer memory out = + TransceiverStructs.parseNonFungibleNativeTokenTransfer(encoded); + + assertEq(out.to, to, "To address should be the same"); + assertEq(out.toChain, toChain, "To chain should be the same"); + assertEq(out.payload, payload, "Payload should be the same"); + assertEq( + out.tokenIds.length, nftTransfer.tokenIds.length, "TokenIds length should be the same" + ); + + for (uint256 i = 0; i < nftCount; i++) { + assertEq(out.tokenIds[i], nftTransfer.tokenIds[i], "TokenId should be the same"); + } + } + + /// @dev Skip testing 32byte tokenIdWidth as uint256(max) + 1 is too large to test. + function test_cannotEncodeTokenIdTooLarge(uint8 tokenIdWidth) public { + tokenIdWidth = uint8(bound(tokenIdWidth, 1, 16)); + vm.assume( + tokenIdWidth == 1 || tokenIdWidth == 2 || tokenIdWidth == 4 || tokenIdWidth == 8 + || tokenIdWidth == 16 + ); + uint256 tokenId = _getMaxFromTokenIdWidth(tokenIdWidth) + 1; + + vm.expectRevert( + abi.encodeWithSelector( + TransceiverStructs.TokenIdTooLarge.selector, tokenId, tokenId - 1 + ) + ); + TransceiverStructs.encodeTokenId(tokenId, tokenIdWidth); + } + + // ============================ Transfer Tests ====================================== + + function test_lockAndMint(uint16 nftCount, uint16 startId) public { + nftCount = uint16(bound(nftCount, 1, managerOne.getMaxBatchSize())); + startId = uint16(bound(startId, 0, type(uint16).max - nftCount)); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Lock the NFTs on managerOne. + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; + + _verifyTransferPayload(guardian, encodedVm, managerOne, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(encodedVm); + + // Verify state changes. The NFTs should still be locked on managerOne, and a new + // batch of NFTs should be minted on managerTwo. + assertTrue( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdOne, encodedVm)) + ); + assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); + assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); + } + + function test_lockAndMintMultiTransceiver(uint16 nftCount, uint16 startId) public { + nftCount = uint16(bound(nftCount, 1, managerOne.getMaxBatchSize())); + startId = uint16(bound(startId, 0, type(uint16).max - nftCount)); + + WormholeTransceiver multiTransceiverTwo = + _setupMultiTransceiverManagers(managerOne, managerTwo); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Lock the NFTs on managerOne. + bytes[] memory vms = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(guardian, vms[0], managerOne, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(guardian, vms[1], managerOne, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(vms[0]); + assertFalse( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdOne, vms[0])) + ); + multiTransceiverTwo.receiveMessage(vms[1]); + + // Verify state changes. The NFTs should still be locked on managerOne, and a new + // batch of NFTs should be minted on managerTwo. + assertTrue( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdOne, vms[0])) + ); + assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); + assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); + } + + function test_burnAndUnlock(uint16 nftCount, uint16 startId) public { + nftCount = uint16(bound(nftCount, 1, managerOne.getMaxBatchSize())); + startId = uint16(bound(startId, 0, type(uint16).max - nftCount)); + + // Mint nftOne to managerOne to "lock" them. + { + vm.startPrank(address(managerOne)); + uint256[] memory tokenIds = + _mintNftBatch(nftOne, address(managerOne), nftCount, startId); + assertTrue( + _isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs" + ); + vm.stopPrank(); + } + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftTwo, recipient, nftCount, startId); + + // Burn the NFTs on managerTwo. + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true + )[0]; + + _verifyTransferPayload(guardian, encodedVm, managerTwo, recipient, chainIdOne, tokenIds); + + // Receive the message and unlock the NFTs on managerOne. + transceiverOne.receiveMessage(encodedVm); + + // Verify state changes. + assertTrue( + managerOne.isMessageExecuted(_computeMessageDigest(guardian, chainIdTwo, encodedVm)) + ); + assertTrue(_isBatchBurned(nftTwo, tokenIds), "NFTs should be burned"); + assertTrue(_isBatchOwner(nftOne, tokenIds, recipient), "Recipient should own NFTs"); + } + + function test_burnAndUnlockMultiTransceiver(uint16 nftCount, uint16 startId) public { + nftCount = uint16(bound(nftCount, 1, managerOne.getMaxBatchSize())); + startId = uint16(bound(startId, 0, type(uint16).max - nftCount)); + + WormholeTransceiver multiTransceiverOne = + _setupMultiTransceiverManagers(managerTwo, managerOne); + + // Mint nftOne to managerOne to "lock" them. + { + vm.startPrank(address(managerOne)); + uint256[] memory tokenIds = + _mintNftBatch(nftOne, address(managerOne), nftCount, startId); + assertTrue( + _isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs" + ); + vm.stopPrank(); + } + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftTwo, recipient, nftCount, startId); + + // Burn the NFTs on managerTwo. + bytes[] memory vms = _approveAndTransferBatch( + guardian, managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(guardian, vms[0], managerTwo, recipient, chainIdOne, tokenIds); + _verifyTransferPayload(guardian, vms[1], managerTwo, recipient, chainIdOne, tokenIds); + + // Receive the message and unlock the NFTs on managerOne. + transceiverOne.receiveMessage(vms[0]); + assertFalse( + managerOne.isMessageExecuted(_computeMessageDigest(guardian, chainIdTwo, vms[0])) + ); + multiTransceiverOne.receiveMessage(vms[1]); + + // Verify state changes. + assertTrue( + managerOne.isMessageExecuted(_computeMessageDigest(guardian, chainIdTwo, vms[0])) + ); + assertTrue(_isBatchBurned(nftTwo, tokenIds), "NFTs should be burned"); + assertTrue(_isBatchOwner(nftOne, tokenIds, recipient), "Recipient should own NFTs"); + } + + function test_burnAndMint(uint16 nftCount, uint16 startId) public { + nftCount = uint16(bound(nftCount, 1, managerOne.getMaxBatchSize())); + startId = uint16(bound(startId, 0, type(uint16).max - nftCount)); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Burn the NFTs on managerThree. + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; + + _verifyTransferPayload(guardian, encodedVm, managerThree, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(encodedVm); + + // Verify state changes. The NFTs should've been burned on managerThree, and a new + // batch of NFTs should be minted on managerTwo. + assertTrue( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdThree, encodedVm)) + ); + assertTrue(_isBatchBurned(nftOne, tokenIds), "NFTs should be burned"); + assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); + } + + function test_burnAndMintMultiTransceiver(uint16 nftCount, uint16 startId) public { + nftCount = uint16(bound(nftCount, 1, managerOne.getMaxBatchSize())); + startId = uint16(bound(startId, 0, type(uint16).max - nftCount)); + + WormholeTransceiver multiTransceiverTwo = + _setupMultiTransceiverManagers(managerThree, managerTwo); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Burn the NFTs on managerThree. + bytes[] memory vms = _approveAndTransferBatch( + guardian, managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(guardian, vms[0], managerThree, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(guardian, vms[1], managerThree, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(vms[0]); + assertFalse( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdThree, vms[0])) + ); + multiTransceiverTwo.receiveMessage(vms[1]); + + // Verify state changes. The NFTs should've been burned on managerThree, and a new + // batch of NFTs should be minted on managerTwo. + assertTrue( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdThree, vms[0])) + ); + assertTrue(_isBatchBurned(nftOne, tokenIds), "NFTs should be burned"); + assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); + } + + // ================================ Negative Transfer Tests ================================== + + function test_cannotTransferZeroTokens() public { + uint256[] memory tokenIds = new uint256[](0); + address recipient = makeAddr("recipient"); + + vm.startPrank(recipient); + vm.expectRevert(abi.encodeWithSelector(INonFungibleNttManager.ZeroTokenIds.selector)); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferExceedsMaxBatchSize() public { + uint256 nftCount = managerOne.getMaxBatchSize() + 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.startPrank(recipient); + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.ExceedsMaxBatchSize.selector, + nftCount, + managerOne.getMaxBatchSize() + ) + ); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferToInvalidChain() public { + uint256 nftCount = 1; + uint256 startId = 0; + uint16 targetChain = 69; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.startPrank(recipient); + nftOne.setApprovalForAll(address(managerOne), true); + vm.expectRevert( + abi.encodeWithSelector(IManagerBase.PeerNotRegistered.selector, targetChain) + ); + managerOne.transfer(tokenIds, targetChain, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferTokenIdTooLarge() public { + uint256 nftCount = 1; + uint256 startId = type(uint256).max - nftCount; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.startPrank(recipient); + nftOne.setApprovalForAll(address(managerOne), true); + vm.expectRevert( + abi.encodeWithSelector( + TransceiverStructs.TokenIdTooLarge.selector, tokenIds[0], type(uint16).max + ) + ); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferInvalidRecipient() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.startPrank(recipient); + nftOne.setApprovalForAll(address(managerOne), true); + vm.expectRevert(INonFungibleNttManager.InvalidRecipient.selector); + managerOne.transfer( + tokenIds, + chainIdTwo, + bytes32(0), // Invalid Recipient. + new bytes(1) + ); + } + + function test_cannotTransferPayloadSizeExceeded() public { + // Deploy manager with 32 byte tokenIdWidth. + INonFungibleNttManager manager = deployNonFungibleManager( + address(nftOne), IManagerBase.Mode.BURNING, chainIdThree, true, 32 + ); + WormholeTransceiver transceiver = deployWormholeTransceiver( + guardian, address(manager), address(0), consistencyLevel, baseGasLimit + ); + transceiver.setWormholePeer(chainIdTwo, toWormholeFormat(makeAddr("random"))); + manager.setTransceiver(address(transceiver)); + manager.setPeer(chainIdTwo, toWormholeFormat(makeAddr("random"))); + + // Since the NonFungibleNtt payload is currently 180 bytes (without the tokenIds), + // we should be able to cause the error by transferring with 21 tokenIds. + // floor((850 - 180) / 33) + 1 (32 bytes per tokenId, 1 byte for length). + uint256 nftCount = 21; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.startPrank(recipient); + nftOne.setApprovalForAll(address(managerOne), true); + + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.ExceedsMaxPayloadSize.selector, + 873, + transceiver.MAX_PAYLOAD_SIZE() + ) + ); + manager.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferDuplicateNfts() public { + uint256 nftCount = 2; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Create new tokenIds array. + uint256[] memory tokenIds2 = new uint256[](nftCount + 1); + for (uint256 i = 0; i < nftCount; i++) { + tokenIds2[i] = tokenIds[i]; + } + tokenIds2[nftCount] = tokenIds[0]; + + vm.startPrank(recipient); + nftOne.setApprovalForAll(address(managerOne), true); + vm.expectRevert("ERC721: transfer from incorrect owner"); + managerOne.transfer(tokenIds2, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferWhenPaused() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.prank(owner); + managerOne.pause(); + + vm.startPrank(recipient); + vm.expectRevert(PausableUpgradeable.RequireContractIsNotPaused.selector); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotTransferReentrant() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Enable reentrancy on nft. + vm.prank(owner); + nftOne.setReentrant(true); + + vm.startPrank(recipient); + nftOne.setApprovalForAll(address(managerOne), true); + + vm.expectRevert(ReentrancyGuardUpgradeable.ReentrancyGuardReentrantCall.selector); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + } + + function test_cannotExecuteMessagePeerNotRegistered() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Register managerThree on managerOne, but not vice versa. + vm.startPrank(owner); + managerOne.setPeer(chainIdThree, toWormholeFormat(address(managerThree))); + transceiverThree.setWormholePeer(chainIdOne, toWormholeFormat(address(transceiverOne))); + vm.stopPrank(); + + // Burn the NFTs on managerThree. + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true + )[0]; + + // Receive the message and mint the NFTs on managerTwo. + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.InvalidPeer.selector, + chainIdOne, + toWormholeFormat(address(managerOne)) + ) + ); + transceiverThree.receiveMessage(encodedVm); + } + + function test_cannotExecuteMessageNotApproved() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; + + // Parse the manager message. + bytes memory vmPayload = guardian.wormhole().parseVM(encodedVm).payload; + (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + + // Receive the message and mint the NFTs on managerTwo. + vm.expectRevert( + abi.encodeWithSelector( + IManagerBase.MessageNotApproved.selector, + _computeMessageDigest(guardian, chainIdOne, encodedVm) + ) + ); + managerTwo.executeMsg(chainIdOne, toWormholeFormat(address(managerOne)), message); + } + + function test_cannotExecuteMessageNotApprovedMultiTransceiver() public { + uint256 nftCount = 1; + uint256 startId = 0; + + _setupMultiTransceiverManagers(managerOne, managerTwo); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Lock the NFTs on managerOne. + bytes[] memory vms = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(guardian, vms[0], managerOne, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(guardian, vms[1], managerOne, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(vms[0]); + + // Parse the manager message. + bytes memory vmPayload = guardian.wormhole().parseVM(vms[1]).payload; + (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + + // Receive the message and mint the NFTs on managerTwo. + vm.expectRevert( + abi.encodeWithSelector( + IManagerBase.MessageNotApproved.selector, + _computeMessageDigest(guardian, chainIdOne, vms[1]) + ) + ); + managerTwo.executeMsg(chainIdOne, toWormholeFormat(address(managerOne)), message); + } + + function test_cannotAttestMessageOnlyTranceiver() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; + + // Parse the manager message. + bytes memory vmPayload = guardian.wormhole().parseVM(encodedVm).payload; + (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + + // Receive the message and mint the NFTs on managerTwo. + vm.expectRevert( + abi.encodeWithSelector(TransceiverRegistry.CallerNotTransceiver.selector, address(this)) + ); + managerTwo.attestationReceived(chainIdOne, toWormholeFormat(address(managerOne)), message); + } + + function test_cannotExecuteMessageInvalidTargetChain() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Register a manager on managerOne with chainIdThree, but use managerTwo address. + // Otherwise, the recipient manager address would be unexpected. + vm.prank(owner); + managerOne.setPeer(chainIdThree, toWormholeFormat(address(managerTwo))); + + // Send the batch to chainThree, but receive it on chainTwo. + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true + )[0]; + + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.InvalidTargetChain.selector, chainIdThree, chainIdTwo + ) + ); + transceiverTwo.receiveMessage(encodedVm); + } + + function test_cannotExecuteMessageWhenPaused() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Lock the NFTs on managerOne. + bytes memory encodedVm = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; + + // Pause managerTwo. + vm.prank(owner); + managerTwo.pause(); + + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + transceiverTwo.receiveMessage(encodedVm); + } + + function test_cannotAttestWhenPaused() public { + uint256 nftCount = 1; + uint256 startId = 0; + + WormholeTransceiver multiTransceiverTwo = + _setupMultiTransceiverManagers(managerOne, managerTwo); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Lock the NFTs on managerOne. + bytes[] memory vms = _approveAndTransferBatch( + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(guardian, vms[0], managerOne, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(guardian, vms[1], managerOne, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(vms[0]); + + // Pause managerTwo. + vm.prank(owner); + managerTwo.pause(); + + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + multiTransceiverTwo.receiveMessage(vms[1]); + } + + function test_cannotReceiveErc721FromNonOperator() public { + uint256 nftCount = 1; + uint256 startId = 0; + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + vm.startPrank(recipient); + nftOne.approve(address(managerOne), tokenIds[0]); + + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.InvalidOperator.selector, recipient, address(managerOne) + ) + ); + nftOne.safeTransferFrom(recipient, address(managerOne), tokenIds[0]); + } +} diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index 37aeb30da..01f59a4d0 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -4,12 +4,12 @@ pragma solidity >=0.8.8 <0.9.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/NttManager/NttManager.sol"; +import "../src/NativeTransfers/NttManager.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IRateLimiter.sol"; import "../src/interfaces/IManagerBase.sol"; import "../src/interfaces/IRateLimiterEvents.sol"; -import "../src/NttManager/TransceiverRegistry.sol"; +import "../src/NativeTransfers/shared/TransceiverRegistry.sol"; import "../src/libraries/PausableUpgradeable.sol"; import {Utils} from "./libraries/Utils.sol"; @@ -142,9 +142,9 @@ contract TestNttManager is Test, IRateLimiterEvents { TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max ); - TransceiverStructs.NttManagerMessage memory nttManagerMessage; + TransceiverStructs.ManagerMessage memory ManagerMessage; bytes memory transceiverMessage; - (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + (ManagerMessage, transceiverMessage) = TransceiverHelpersLib .buildTransceiverMessageWithNttManagerPayload( 0, bytes32(0), @@ -155,8 +155,8 @@ contract TestNttManager is Test, IRateLimiterEvents { e1.receiveMessage(transceiverMessage); - bytes32 hash = TransceiverStructs.nttManagerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, nttManagerMessage + bytes32 hash = TransceiverStructs.managerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, ManagerMessage ); assertEq(nttManagerZeroRateLimiter.messageAttestations(hash), 1); } @@ -209,7 +209,7 @@ contract TestNttManager is Test, IRateLimiterEvents { vm.expectRevert( abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) ); - TransceiverStructs.NttManagerMessage memory message; + TransceiverStructs.ManagerMessage memory message; nttManager.executeMsg(0, bytes32(0), message); bytes memory transceiverMessage; @@ -431,9 +431,9 @@ contract TestNttManager is Test, IRateLimiterEvents { bytes32 peer = toWormholeFormat(address(nttManager)); - TransceiverStructs.NttManagerMessage memory nttManagerMessage; + TransceiverStructs.ManagerMessage memory ManagerMessage; bytes memory transceiverMessage; - (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + (ManagerMessage, transceiverMessage) = TransceiverHelpersLib .buildTransceiverMessageWithNttManagerPayload( 0, bytes32(0), peer, toWormholeFormat(address(nttManagerOther)), abi.encode("payload") ); @@ -454,17 +454,17 @@ contract TestNttManager is Test, IRateLimiterEvents { bytes32 peer = toWormholeFormat(address(nttManager)); nttManagerOther.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); - TransceiverStructs.NttManagerMessage memory nttManagerMessage; + TransceiverStructs.ManagerMessage memory ManagerMessage; bytes memory transceiverMessage; - (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + (ManagerMessage, transceiverMessage) = TransceiverHelpersLib .buildTransceiverMessageWithNttManagerPayload( 0, bytes32(0), peer, toWormholeFormat(address(nttManagerOther)), abi.encode("payload") ); e1.receiveMessage(transceiverMessage); - bytes32 hash = TransceiverStructs.nttManagerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, nttManagerMessage + bytes32 hash = TransceiverStructs.managerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, ManagerMessage ); assertEq(nttManagerOther.messageAttestations(hash), 1); } @@ -477,15 +477,15 @@ contract TestNttManager is Test, IRateLimiterEvents { bytes32 peer = toWormholeFormat(address(nttManager)); nttManagerOther.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); - TransceiverStructs.NttManagerMessage memory nttManagerMessage; + TransceiverStructs.ManagerMessage memory ManagerMessage; bytes memory transceiverMessage; - (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + (ManagerMessage, transceiverMessage) = TransceiverHelpersLib .buildTransceiverMessageWithNttManagerPayload( 0, bytes32(0), peer, toWormholeFormat(address(nttManagerOther)), abi.encode("payload") ); - bytes32 hash = TransceiverStructs.nttManagerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, nttManagerMessage + bytes32 hash = TransceiverStructs.managerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, ManagerMessage ); e1.receiveMessage(transceiverMessage); @@ -508,7 +508,7 @@ contract TestNttManager is Test, IRateLimiterEvents { ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](1); transceivers[0] = e1; - TransceiverStructs.NttManagerMessage memory m; + TransceiverStructs.ManagerMessage memory m; (m,) = TransceiverHelpersLib.attestTransceiversHelper( address(0x456), 0, @@ -523,7 +523,7 @@ contract TestNttManager is Test, IRateLimiterEvents { nttManagerOther.removeTransceiver(address(e1)); bytes32 hash = - TransceiverStructs.nttManagerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); // a disabled transceiver's vote no longer counts assertEq(nttManagerOther.messageAttestations(hash), 0); @@ -601,7 +601,7 @@ contract TestNttManager is Test, IRateLimiterEvents { TrimmedAmount transferAmount = packTrimmedAmount(50, 8); - TransceiverStructs.NttManagerMessage memory m; + TransceiverStructs.ManagerMessage memory m; bytes memory encodedEm; { ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](2); @@ -634,9 +634,7 @@ contract TestNttManager is Test, IRateLimiterEvents { vm.expectRevert( abi.encodeWithSelector( IManagerBase.TransceiverAlreadyAttestedToMessage.selector, - TransceiverStructs.nttManagerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, m - ) + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m) ) ); e2.receiveMessage(encodedEm); @@ -743,7 +741,7 @@ contract TestNttManager is Test, IRateLimiterEvents { transceivers[0] = e1; transceivers[1] = e2; - TransceiverStructs.NttManagerMessage memory m; + TransceiverStructs.ManagerMessage memory m; bytes memory encodedEm; { TransceiverStructs.TransceiverMessage memory em; diff --git a/evm/test/RateLimit.t.sol b/evm/test/RateLimit.t.sol index 916ccaa8b..b34e52897 100644 --- a/evm/test/RateLimit.t.sol +++ b/evm/test/RateLimit.t.sol @@ -3,7 +3,7 @@ import "forge-std/Test.sol"; import "../src/interfaces/IRateLimiterEvents.sol"; import "../src/interfaces/IManagerBase.sol"; -import "../src/NttManager/NttManager.sol"; +import "../src/NativeTransfers/NttManager.sol"; import "./mocks/DummyTransceiver.sol"; import "../src/mocks/DummyToken.sol"; import "./mocks/MockNttManager.sol"; @@ -506,7 +506,7 @@ contract TestRateLimit is Test, IRateLimiterEvents { ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](1); transceivers[0] = e1; - TransceiverStructs.NttManagerMessage memory m; + TransceiverStructs.ManagerMessage memory m; bytes memory encodedEm; { TransceiverStructs.TransceiverMessage memory em; @@ -526,7 +526,7 @@ contract TestRateLimit is Test, IRateLimiterEvents { } bytes32 digest = - TransceiverStructs.nttManagerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); // no quorum yet assertEq(token.balanceOf(address(user_B)), 0); @@ -592,9 +592,7 @@ contract TestRateLimit is Test, IRateLimiterEvents { assertEq(entries[0].topics[1], toWormholeFormat(address(nttManager))); assertEq( entries[0].topics[2], - TransceiverStructs.nttManagerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, m - ) + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m) ); } } @@ -988,7 +986,7 @@ contract TestRateLimit is Test, IRateLimiterEvents { ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](1); transceivers[0] = e1; - TransceiverStructs.NttManagerMessage memory m; + TransceiverStructs.ManagerMessage memory m; bytes memory encodedEm; uint256 inboundLimit = inboundLimitAmt; TrimmedAmount trimmedAmount = packTrimmedAmount(uint64(amount), 8); @@ -1010,7 +1008,7 @@ contract TestRateLimit is Test, IRateLimiterEvents { } bytes32 digest = - TransceiverStructs.nttManagerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); // no quorum yet assertEq(token.balanceOf(address(user_B)), 0); @@ -1079,9 +1077,7 @@ contract TestRateLimit is Test, IRateLimiterEvents { assertEq(entries[0].topics[1], toWormholeFormat(address(nttManager))); assertEq( entries[0].topics[2], - TransceiverStructs.nttManagerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, m - ) + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m) ); } } diff --git a/evm/test/TransceiverStructs.t.sol b/evm/test/TransceiverStructs.t.sol index 3f80bdaf1..1959f69ca 100644 --- a/evm/test/TransceiverStructs.t.sol +++ b/evm/test/TransceiverStructs.t.sol @@ -83,7 +83,7 @@ contract TestTransceiverStructs is Test { toChain: 17 }); - TransceiverStructs.NttManagerMessage memory mm = TransceiverStructs.NttManagerMessage({ + TransceiverStructs.ManagerMessage memory mm = TransceiverStructs.ManagerMessage({ id: hex"128434bafe23430000000000000000000000000000000000ce00aa0000000000", sender: hex"46679213412343", payload: TransceiverStructs.encodeNativeTokenTransfer(ntt) @@ -93,7 +93,7 @@ contract TestTransceiverStructs is Test { TransceiverStructs.TransceiverMessage memory em = TransceiverStructs.TransceiverMessage({ sourceNttManagerAddress: hex"042942FAFABE", recipientNttManagerAddress: hex"042942FABABE", - nttManagerPayload: TransceiverStructs.encodeNttManagerMessage(mm), + nttManagerPayload: TransceiverStructs.encodeManagerMessage(mm), transceiverPayload: new bytes(0) }); @@ -108,8 +108,8 @@ contract TestTransceiverStructs is Test { TransceiverStructs.TransceiverMessage memory emParsed = TransceiverStructs.parseTransceiverMessage(wh_prefix, encodedTransceiverMessage); - TransceiverStructs.NttManagerMessage memory mmParsed = - TransceiverStructs.parseNttManagerMessage(emParsed.nttManagerPayload); + TransceiverStructs.ManagerMessage memory mmParsed = + TransceiverStructs.parseManagerMessage(emParsed.nttManagerPayload); // deep equality check assertEq(abi.encode(mmParsed), abi.encode(mm)); @@ -121,23 +121,21 @@ contract TestTransceiverStructs is Test { assertEq(abi.encode(nttParsed), abi.encode(ntt)); } - function test_SerdeRoundtrip_NttManagerMessage(TransceiverStructs.NttManagerMessage memory m) + function test_SerdeRoundtrip_ManagerMessage(TransceiverStructs.ManagerMessage memory m) public { - bytes memory message = TransceiverStructs.encodeNttManagerMessage(m); + bytes memory message = TransceiverStructs.encodeManagerMessage(m); - TransceiverStructs.NttManagerMessage memory parsed = - TransceiverStructs.parseNttManagerMessage(message); + TransceiverStructs.ManagerMessage memory parsed = + TransceiverStructs.parseManagerMessage(message); assertEq(m.id, parsed.id); assertEq(m.sender, parsed.sender); assertEq(m.payload, parsed.payload); } - function test_SerdeJunk_NttManagerMessage(TransceiverStructs.NttManagerMessage memory m) - public - { - bytes memory message = TransceiverStructs.encodeNttManagerMessage(m); + function test_SerdeJunk_ManagerMessage(TransceiverStructs.ManagerMessage memory m) public { + bytes memory message = TransceiverStructs.encodeManagerMessage(m); bytes memory junk = "junk"; @@ -146,7 +144,7 @@ contract TestTransceiverStructs is Test { BytesParsing.LengthMismatch.selector, message.length + junk.length, message.length ) ); - TransceiverStructs.parseNttManagerMessage(abi.encodePacked(message, junk)); + TransceiverStructs.parseManagerMessage(abi.encodePacked(message, junk)); } function test_SerdeRoundtrip_NativeTokenTransfer( diff --git a/evm/test/Upgrades.t.sol b/evm/test/Upgrades.t.sol index d394b474b..43eea9e45 100644 --- a/evm/test/Upgrades.t.sol +++ b/evm/test/Upgrades.t.sol @@ -4,11 +4,12 @@ pragma solidity >=0.8.8 <0.9.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/NttManager/NttManager.sol"; +import "../src/NativeTransfers/NttManager.sol"; import "../src/interfaces/INttManager.sol"; import "../src/interfaces/IManagerBase.sol"; import "../src/interfaces/IRateLimiter.sol"; import "../src/interfaces/IRateLimiterEvents.sol"; +import "../src/interfaces/IWormholeTransceiverState.sol"; import "../src/libraries/external/OwnableUpgradeable.sol"; import "../src/libraries/external/Initializable.sol"; import "../src/libraries/Implementation.sol"; @@ -76,7 +77,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1 = MockWormholeTransceiverContract( address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) @@ -104,7 +106,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain2 = MockWormholeTransceiverContract( address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) @@ -160,7 +163,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1.upgrade(address(wormholeTransceiverChain1Implementation)); @@ -193,7 +197,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1.upgrade(address(wormholeTransceiverChain1Implementation)); @@ -229,7 +234,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1.upgrade(address(newImplementation)); @@ -263,7 +269,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); vm.expectRevert("Proper migrate called"); @@ -298,7 +305,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); vm.expectRevert(); // Reverts with a panic on the assert. So, no way to tell WHY this happened. @@ -332,7 +340,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); //vm.expectRevert(); // Reverts with a panic on the assert. So, no way to tell WHY this happened. @@ -399,7 +408,8 @@ contract TestUpgrades is Test, IRateLimiterEvents { address(relayer), address(0x0), FAST_CONSISTENCY_LEVEL, - GAS_LIMIT + GAS_LIMIT, + IWormholeTransceiverState.ManagerType.ERC20 ); wormholeTransceiverChain1.upgrade(address(wormholeTransceiverChain1Implementation)); basicFunctionality(); // Ensure that the upgrade was proper diff --git a/evm/test/libraries/NonFungibleNttManagerHelpers.sol b/evm/test/libraries/NonFungibleNttManagerHelpers.sol new file mode 100644 index 000000000..32f568afc --- /dev/null +++ b/evm/test/libraries/NonFungibleNttManagerHelpers.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; + +import "../../src/interfaces/INonFungibleNttManager.sol"; +import "../../src/interfaces/IManagerBase.sol"; +import "../../src/interfaces/IWormholeTransceiverState.sol"; + +import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "../../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../../src/NativeTransfers/NonFungibleNttManager.sol"; + +import "../mocks/MockTransceivers.sol"; +import "../../src/mocks/DummyNft.sol"; + +contract NonFungibleNttHelpers is Test { + bytes4 constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; + + function deployNonFungibleManager( + address nft, + IManagerBase.Mode _mode, + uint16 _chainId, + bool shouldInitialize, + uint8 _tokenIdWidth + ) public returns (INonFungibleNttManager) { + NonFungibleNttManager implementation = + new NonFungibleNttManager(address(nft), _tokenIdWidth, _mode, _chainId); + + NonFungibleNttManager proxy = + NonFungibleNttManager(address(new ERC1967Proxy(address(implementation), ""))); + + if (shouldInitialize) { + proxy.initialize(); + } + + return INonFungibleNttManager(address(proxy)); + } + + function deployWormholeTransceiver( + WormholeSimulator guardian, + address manager, + address relayer, + uint8 consistencyLevel, + uint256 baseGasLimit + ) public returns (WormholeTransceiver) { + // Wormhole Transceivers. + WormholeTransceiver implementation = new WormholeTransceiver( + manager, + address(guardian.wormhole()), + relayer, + address(0), + consistencyLevel, + baseGasLimit, + IWormholeTransceiverState.ManagerType.ERC721 + ); + + WormholeTransceiver transceiverProxy = + WormholeTransceiver(address(new ERC1967Proxy(address(implementation), ""))); + + transceiverProxy.initialize(); + + return transceiverProxy; + } + + function _isBatchOwner( + DummyNftMintAndBurn nft, + uint256[] memory tokenIds, + address _owner + ) internal view returns (bool) { + bool isOwner = true; + for (uint256 i = 0; i < tokenIds.length; i++) { + if (nft.ownerOf(tokenIds[i]) != _owner) { + isOwner = false; + break; + } + } + return isOwner; + } + + function _isBatchBurned( + DummyNftMintAndBurn nft, + uint256[] memory tokenIds + ) internal view returns (bool) { + bool isBurned = true; + for (uint256 i = 0; i < tokenIds.length; i++) { + if (nft.exists(tokenIds[i])) { + isBurned = false; + break; + } + } + return isBurned; + } + + function _verifyTransferPayload( + WormholeSimulator guardian, + bytes memory transferMessage, + INonFungibleNttManager manager, + address recipient, + uint16 targetChain, + uint256[] memory tokenIds + ) internal { + // Verify the manager message + bytes memory vmPayload = guardian.wormhole().parseVM(transferMessage).payload; + (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + + assertEq(uint256(message.id), manager.nextMessageSequence() - 1); + assertEq(message.sender, toWormholeFormat(recipient)); + + // Verify the non-fungible transfer message. + TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = + TransceiverStructs.parseNonFungibleNativeTokenTransfer(message.payload); + + assertEq(nftTransfer.to, toWormholeFormat(recipient)); + assertEq(nftTransfer.toChain, targetChain); + assertEq(nftTransfer.payload, new bytes(0)); + assertEq(nftTransfer.tokenIds.length, tokenIds.length); + + for (uint256 i = 0; i < tokenIds.length; i++) { + assertEq(nftTransfer.tokenIds[i], tokenIds[i]); + } + } + + function _computeMessageDigest( + WormholeSimulator guardian, + uint16 sourceChain, + bytes memory encodedVm + ) internal view returns (bytes32 digest) { + // Parse the manager message. + bytes memory vmPayload = guardian.wormhole().parseVM(encodedVm).payload; + (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + + digest = TransceiverStructs.managerMessageDigest(sourceChain, message); + } + + function _approveAndTransferBatch( + WormholeSimulator guardian, + INonFungibleNttManager manager, + WormholeTransceiver transceiver, + DummyNftMintAndBurn nft, + uint256[] memory tokenIds, + address recipient, + uint16 targetChain, + bool relayerOff + ) internal returns (bytes[] memory encodedVms) { + // Transfer NFTs as the owner of the NFTs. + vm.startPrank(recipient); + nft.setApprovalForAll(address(manager), true); + + vm.recordLogs(); + manager.transfer( + tokenIds, + targetChain, + toWormholeFormat(recipient), + _encodeTransceiverInstruction(relayerOff, transceiver) + ); + vm.stopPrank(); + + // Fetch the wormhole message. + encodedVms = _getWormholeMessage(guardian, vm.getRecordedLogs(), manager.chainId()); + } + + function _mintNftBatch( + DummyNftMintAndBurn nft, + address recipient, + uint256 len, + uint256 start + ) internal returns (uint256[] memory) { + uint256[] memory arr = new uint256[](len); + for (uint256 i = 0; i < len; i++) { + uint256 tokenId = start + i; + arr[i] = tokenId; + + nft.mint(recipient, tokenId); + } + return arr; + } + + function _createBatchTokenIds( + uint256 len, + uint256 start + ) internal pure returns (uint256[] memory) { + uint256[] memory arr = new uint256[](len); + for (uint256 i = 0; i < len; i++) { + arr[i] = start + i; + } + return arr; + } + + function _getWormholeMessage( + WormholeSimulator guardian, + Vm.Log[] memory logs, + uint16 emitterChain + ) internal view returns (bytes[] memory) { + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(logs); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], emitterChain); + } + + return encodedVMs; + } + + function _encodeTransceiverInstruction( + bool relayerOff, + WormholeTransceiver transceiver + ) internal pure returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayerOff); + bytes memory encodedInstructionWormhole = + transceiver.encodeWormholeTransceiverInstruction(instruction); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = TransceiverStructs + .TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](1); + TransceiverInstructions[0] = TransceiverInstruction; + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } + + function _getMaxFromTokenIdWidth(uint8 tokenIdWidth) internal pure returns (uint256) { + if (tokenIdWidth == 1) { + return type(uint8).max; + } else if (tokenIdWidth == 2) { + return type(uint16).max; + } else if (tokenIdWidth == 4) { + return type(uint32).max; + } else if (tokenIdWidth == 8) { + return type(uint64).max; + } else if (tokenIdWidth == 16) { + return type(uint128).max; + } else if (tokenIdWidth == 32) { + return type(uint256).max; + } + } +} diff --git a/evm/test/libraries/NttManagerHelpers.sol b/evm/test/libraries/NttManagerHelpers.sol index 415364b33..5d7a594f4 100644 --- a/evm/test/libraries/NttManagerHelpers.sol +++ b/evm/test/libraries/NttManagerHelpers.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.8 <0.9.0; import "../../src/libraries/TrimmedAmount.sol"; -import "../../src/NttManager/NttManager.sol"; +import "../../src/NativeTransfers/NttManager.sol"; library NttManagerHelpersLib { uint16 constant SENDING_CHAIN_ID = 1; diff --git a/evm/test/libraries/TransceiverHelpers.sol b/evm/test/libraries/TransceiverHelpers.sol index 11a3551db..27b199489 100644 --- a/evm/test/libraries/TransceiverHelpers.sol +++ b/evm/test/libraries/TransceiverHelpers.sol @@ -5,7 +5,7 @@ pragma solidity >=0.8.8 <0.9.0; import "./NttManagerHelpers.sol"; import "../mocks/DummyTransceiver.sol"; import "../../src/mocks/DummyToken.sol"; -import "../../src/NttManager/NttManager.sol"; +import "../../src/NativeTransfers/NttManager.sol"; import "../../src/libraries/TrimmedAmount.sol"; library TransceiverHelpersLib { @@ -39,13 +39,13 @@ library TransceiverHelpersLib { ) internal returns ( - TransceiverStructs.NttManagerMessage memory, + TransceiverStructs.ManagerMessage memory, TransceiverStructs.TransceiverMessage memory ) { - TransceiverStructs.NttManagerMessage memory m = - buildNttManagerMessage(to, id, toChain, nttManager, amount); - bytes memory encodedM = TransceiverStructs.encodeNttManagerMessage(m); + TransceiverStructs.ManagerMessage memory m = + buildManagerMessage(to, id, toChain, nttManager, amount); + bytes memory encodedM = TransceiverStructs.encodeManagerMessage(m); prepTokenReceive(nttManager, recipientNttManager, amount, inboundLimit); @@ -67,16 +67,16 @@ library TransceiverHelpersLib { return (m, em); } - function buildNttManagerMessage( + function buildManagerMessage( address to, bytes32 id, uint16 toChain, NttManager nttManager, TrimmedAmount amount - ) internal view returns (TransceiverStructs.NttManagerMessage memory) { + ) internal view returns (TransceiverStructs.ManagerMessage memory) { DummyToken token = DummyToken(nttManager.token()); - return TransceiverStructs.NttManagerMessage( + return TransceiverStructs.ManagerMessage( id, bytes32(0), TransceiverStructs.encodeNativeTokenTransfer( @@ -109,16 +109,16 @@ library TransceiverHelpersLib { bytes32 sourceNttManager, bytes32 recipientNttManager, bytes memory payload - ) internal pure returns (TransceiverStructs.NttManagerMessage memory, bytes memory) { - TransceiverStructs.NttManagerMessage memory m = - TransceiverStructs.NttManagerMessage(id, sender, payload); - bytes memory nttManagerMessage = TransceiverStructs.encodeNttManagerMessage(m); + ) internal pure returns (TransceiverStructs.ManagerMessage memory, bytes memory) { + TransceiverStructs.ManagerMessage memory m = + TransceiverStructs.ManagerMessage(id, sender, payload); + bytes memory ManagerMessage = TransceiverStructs.encodeManagerMessage(m); bytes memory transceiverMessage; (, transceiverMessage) = TransceiverStructs.buildAndEncodeTransceiverMessage( TEST_TRANSCEIVER_PAYLOAD_PREFIX, sourceNttManager, recipientNttManager, - nttManagerMessage, + ManagerMessage, new bytes(0) ); return (m, transceiverMessage); diff --git a/evm/test/mocks/DummyTransceiver.sol b/evm/test/mocks/DummyTransceiver.sol index b5d040712..3c6829885 100644 --- a/evm/test/mocks/DummyTransceiver.sol +++ b/evm/test/mocks/DummyTransceiver.sol @@ -14,7 +14,8 @@ contract DummyTransceiver is Transceiver, ITransceiverReceiver { function _quoteDeliveryPrice( uint16, /* recipientChain */ - TransceiverStructs.TransceiverInstruction memory /* transceiverInstruction */ + TransceiverStructs.TransceiverInstruction memory, /* transceiverInstruction */ + uint256 /* managerExecutionCost */ ) internal pure override returns (uint256) { return 0; } @@ -22,6 +23,7 @@ contract DummyTransceiver is Transceiver, ITransceiverReceiver { function _sendMessage( uint16, /* recipientChain */ uint256, /* deliveryPayment */ + uint256, /* managerExecutionCost */ address, /* caller */ bytes32, /* recipientNttManagerAddress */ TransceiverStructs.TransceiverInstruction memory, /* instruction */ @@ -30,16 +32,16 @@ contract DummyTransceiver is Transceiver, ITransceiverReceiver { // do nothing } - function receiveMessage(bytes memory encodedMessage) external { + function receiveMessage(bytes memory encodedMessage) external virtual { TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; - TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; - (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs - .parseTransceiverAndNttManagerMessage(TEST_TRANSCEIVER_PAYLOAD_PREFIX, encodedMessage); + TransceiverStructs.ManagerMessage memory parsedManagerMessage; + (parsedTransceiverMessage, parsedManagerMessage) = TransceiverStructs + .parseTransceiverAndManagerMessage(TEST_TRANSCEIVER_PAYLOAD_PREFIX, encodedMessage); _deliverToNttManager( SENDING_CHAIN_ID, parsedTransceiverMessage.sourceNttManagerAddress, parsedTransceiverMessage.recipientNttManagerAddress, - parsedNttManagerMessage + parsedManagerMessage ); } diff --git a/evm/test/mocks/MockNttManager.sol b/evm/test/mocks/MockNttManager.sol index 6adfcaa2c..fd2e9f38f 100644 --- a/evm/test/mocks/MockNttManager.sol +++ b/evm/test/mocks/MockNttManager.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.8 <0.9.0; -import "../../src/NttManager/NttManager.sol"; +import "../../src/NativeTransfers/NttManager.sol"; contract MockNttManagerContract is NttManager { constructor( diff --git a/evm/test/mocks/MockTransceivers.sol b/evm/test/mocks/MockTransceivers.sol index 4d8c85323..b41eb0b48 100644 --- a/evm/test/mocks/MockTransceivers.sol +++ b/evm/test/mocks/MockTransceivers.sol @@ -11,7 +11,8 @@ contract MockWormholeTransceiverContract is WormholeTransceiver { address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _gasLimit, + ManagerType _managerType ) WormholeTransceiver( nttManager, @@ -19,7 +20,8 @@ contract MockWormholeTransceiverContract is WormholeTransceiver { wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit + _gasLimit, + _managerType ) {} @@ -37,7 +39,8 @@ contract MockWormholeTransceiverMigrateBasic is WormholeTransceiver { address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _gasLimit, + ManagerType _managerType ) WormholeTransceiver( nttManager, @@ -45,7 +48,8 @@ contract MockWormholeTransceiverMigrateBasic is WormholeTransceiver { wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit + _gasLimit, + _managerType ) {} @@ -61,7 +65,8 @@ contract MockWormholeTransceiverImmutableAllow is WormholeTransceiver { address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _gasLimit, + ManagerType _managerType ) WormholeTransceiver( nttManager, @@ -69,7 +74,8 @@ contract MockWormholeTransceiverImmutableAllow is WormholeTransceiver { wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit + _gasLimit, + _managerType ) {} @@ -91,7 +97,8 @@ contract MockWormholeTransceiverLayoutChange is WormholeTransceiver { address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _gasLimit, + ManagerType _managerType ) WormholeTransceiver( nttManager, @@ -99,7 +106,8 @@ contract MockWormholeTransceiverLayoutChange is WormholeTransceiver { wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit + _gasLimit, + _managerType ) {}