From 180b3ae2bca7614f3943994aaaa461bcf67231bc Mon Sep 17 00:00:00 2001 From: gator-boi Date: Wed, 6 Mar 2024 10:02:49 -0600 Subject: [PATCH 01/19] evm: refactor NttManager in preperation for nft support --- evm/src/NttManager/NttManager.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index 9372237c0..3501f1d6b 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -475,6 +475,11 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { } } + function _initializeTokenDecimals() internal view returns (uint8) { + (, bytes memory queriedDecimals) = token.staticcall(abi.encodeWithSignature("decimals()")); + return abi.decode(queriedDecimals, (uint8)); + } + function _trimTransferAmount( uint256 amount, uint16 toChain From 777272d923e24d2ee30c60e1c3606dde19a7e00c Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 7 Mar 2024 09:12:42 -0600 Subject: [PATCH 02/19] evm: rename NttManager directory and fix paths --- .../NttManager.sol | 2 +- .../shared}/ManagerBase.sol | 18 +++++++++--------- .../shared}/TransceiverRegistry.sol | 0 evm/test/IntegrationRelayer.t.sol | 2 +- evm/test/IntegrationStandalone.t.sol | 2 +- evm/test/NttManager.t.sol | 2 +- evm/test/RateLimit.t.sol | 2 +- evm/test/Upgrades.t.sol | 2 +- evm/test/libraries/NttManagerHelpers.sol | 2 +- evm/test/libraries/TransceiverHelpers.sol | 2 +- evm/test/mocks/MockNttManager.sol | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) rename evm/src/{NttManager => NativeTransfers}/NttManager.sol (99%) rename evm/src/{NttManager => NativeTransfers/shared}/ManagerBase.sol (97%) rename evm/src/{NttManager => NativeTransfers/shared}/TransceiverRegistry.sol (100%) diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NativeTransfers/NttManager.sol similarity index 99% rename from evm/src/NttManager/NttManager.sol rename to evm/src/NativeTransfers/NttManager.sol index 3501f1d6b..83f20294d 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. diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NativeTransfers/shared/ManagerBase.sol similarity index 97% rename from evm/src/NttManager/ManagerBase.sol rename to evm/src/NativeTransfers/shared/ManagerBase.sol index 7a6299084..1251c2851 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 "../interfaces/ITransceiver.sol"; -import "../interfaces/IManagerBase.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 "./TransceiverRegistry.sol"; 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/test/IntegrationRelayer.t.sol b/evm/test/IntegrationRelayer.t.sol index 2324c3faa..27345a9dd 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"; diff --git a/evm/test/IntegrationStandalone.t.sol b/evm/test/IntegrationStandalone.t.sol index abe40de5b..4ed16eb5d 100755 --- a/evm/test/IntegrationStandalone.t.sol +++ b/evm/test/IntegrationStandalone.t.sol @@ -4,7 +4,7 @@ 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"; diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index 37aeb30da..2cb2ea8d0 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -4,7 +4,7 @@ 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"; diff --git a/evm/test/RateLimit.t.sol b/evm/test/RateLimit.t.sol index 916ccaa8b..099aae8b0 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"; diff --git a/evm/test/Upgrades.t.sol b/evm/test/Upgrades.t.sol index d394b474b..a4c864ce7 100644 --- a/evm/test/Upgrades.t.sol +++ b/evm/test/Upgrades.t.sol @@ -4,7 +4,7 @@ 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"; 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..ad6ef63ae 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 { 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( From 152cf0e7cfd1b90520cea188bc34f38927a9a595 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 8 Mar 2024 12:25:20 -0600 Subject: [PATCH 03/19] evm: add outbound nft transfer support --- .../NativeTransfers/NonFungibleNttManager.sol | 205 ++++++++++++++++++ evm/src/NativeTransfers/NttManager.sol | 10 +- .../NativeTransfers/shared/ManagerBase.sol | 21 +- evm/src/Transceiver/Transceiver.sol | 8 +- .../WormholeTransceiver.sol | 21 +- evm/src/interfaces/IManagerBase.sol | 4 +- evm/src/interfaces/INonFungibleNttManager.sol | 54 +++++ evm/src/interfaces/INttManager.sol | 6 +- evm/src/interfaces/ITransceiver.sol | 4 +- evm/src/libraries/TransceiverStructs.sol | 75 +++++-- evm/src/mocks/DummyNft.sol | 64 ++++++ evm/test/NttManager.t.sol | 36 +-- evm/test/RateLimit.t.sol | 8 +- evm/test/TransceiverStructs.t.sol | 24 +- evm/test/libraries/TransceiverHelpers.sol | 24 +- evm/test/mocks/DummyTransceiver.sol | 8 +- 16 files changed, 468 insertions(+), 104 deletions(-) create mode 100644 evm/src/NativeTransfers/NonFungibleNttManager.sol create mode 100644 evm/src/interfaces/INonFungibleNttManager.sol create mode 100644 evm/src/mocks/DummyNft.sol diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol new file mode 100644 index 000000000..e22e71287 --- /dev/null +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -0,0 +1,205 @@ +// 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 "wormhole-solidity-sdk/Utils.sol"; +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +import "../interfaces/ITransceiver.sol"; +import "../interfaces/INonFungibleNttManager.sol"; + +import {ManagerBase} from "./shared/ManagerBase.sol"; + +contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { + using BytesParsing for bytes; + + // =============== Immutables ============================================================ + + uint8 public constant MAX_BATCH_SIZE = 50; + + // =============== Setup ================================================================= + + constructor( + address _token, + Mode _mode, + uint16 _chainId, + uint64 _rateLimitDuration, + bool _skipRateLimiting + ) ManagerBase(_token, _mode, _chainId) {} + + 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(); + } + + // =============== 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_]; + } + + // =============== 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 transfer( + uint256[] calldata tokenIds, + uint16 recipientChain, + bytes32 recipient + ) external payable nonReentrant whenNotPaused returns (uint64) { + return _transfer(tokenIds, recipientChain, recipient, new bytes(1)); + } + + function transfer( + uint256[] calldata tokenIds, + uint16 recipientChain, + bytes32 recipient, + bytes memory transceiverInstructions + ) external payable nonReentrant whenNotPaused returns (uint64) { + return _transfer(tokenIds, recipientChain, recipient, transceiverInstructions); + } + + function _transfer( + uint256[] calldata 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. + ( + address[] memory enabledTransceivers, + TransceiverStructs.TransceiverInstruction[] memory instructions, + uint256[] memory priceQuotes, + uint256 totalPriceQuote + ) = _prepareForTransfer(recipientChain, transceiverInstructions); + + uint64 sequence = _useMessageSequence(); + + TransceiverStructs.NonFungibleNativeTokenTransfer memory nft = + TransceiverStructs.NonFungibleNativeTokenTransfer(recipient, recipientChain, tokenIds); + + // construct the ManagerMessage payload + bytes memory encodedNttManagerPayload = TransceiverStructs.encodeManagerMessage( + TransceiverStructs.ManagerMessage( + bytes32(uint256(sequence)), + toWormholeFormat(msg.sender), + TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nft) + ) + ); + + // Cache and verify peer. + bytes32 destinationPeer = _getPeersStorage()[recipientChain].peerAddress; + if (destinationPeer == bytes32(0)) { + revert InvalidPeer(recipientChain, destinationPeer); + } + + // send the message + _sendMessageToTransceivers( + recipientChain, + destinationPeer, + priceQuotes, + instructions, + enabledTransceivers, + encodedNttManagerPayload + ); + + emit TransferSent( + recipient, uint16(tokenIds.length), totalPriceQuote, recipientChain, sequence + ); + + return sequence; + } + + // ==================== Internal Helpers =============================================== + + function _lockTokens(uint256[] calldata tokenIds) internal { + uint256 len = tokenIds.length; + + for (uint256 i = 0; i < len; ++i) { + IERC721(token).safeTransferFrom(msg.sender, address(this), tokenIds[i]); + } + } + + function _burnTokens(uint256[] calldata tokenIds) internal { + uint256 len = tokenIds.length; + + for (uint256 i = 0; i < len; ++i) { + ERC721Burnable(token).burn(tokenIds[i]); + } + } + + /// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`. + function _verifyPeerInbound(uint16 sourceChainId, bytes32 peerAddress) internal view { + if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { + revert InvalidPeer(sourceChainId, peerAddress); + } + } +} diff --git a/evm/src/NativeTransfers/NttManager.sol b/evm/src/NativeTransfers/NttManager.sol index 83f20294d..bac2924ec 100644 --- a/evm/src/NativeTransfers/NttManager.sol +++ b/evm/src/NativeTransfers/NttManager.sol @@ -173,9 +173,9 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { _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); } } @@ -408,9 +408,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) diff --git a/evm/src/NativeTransfers/shared/ManagerBase.sol b/evm/src/NativeTransfers/shared/ManagerBase.sol index 1251c2851..5ba9f62da 100644 --- a/evm/src/NativeTransfers/shared/ManagerBase.sol +++ b/evm/src/NativeTransfers/shared/ManagerBase.sol @@ -113,10 +113,9 @@ abstract contract ManagerBase is 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 +123,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); @@ -163,7 +162,7 @@ abstract contract ManagerBase is uint256[] memory priceQuotes, 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,7 +178,7 @@ abstract contract ManagerBase is ITransceiver(transceiverAddr).sendMessage{value: priceQuotes[i]}( recipientChain, transceiverInstructions[transceiverInfos[transceiverAddr].index], - nttManagerMessage, + ManagerMessage, peerAddress ); } diff --git a/evm/src/Transceiver/Transceiver.sol b/evm/src/Transceiver/Transceiver.sol index 1edf7ffaa..3a7197ffc 100644 --- a/evm/src/Transceiver/Transceiver.sol +++ b/evm/src/Transceiver/Transceiver.sol @@ -104,7 +104,7 @@ abstract contract Transceiver is function sendMessage( uint16 recipientChain, TransceiverStructs.TransceiverInstruction memory instruction, - bytes memory nttManagerMessage, + bytes memory ManagerMessage, bytes32 recipientNttManagerAddress ) external payable nonReentrant onlyNttManager { _sendMessage( @@ -113,7 +113,7 @@ abstract contract Transceiver is msg.sender, recipientNttManagerAddress, instruction, - nttManagerMessage + ManagerMessage ); } @@ -125,7 +125,7 @@ abstract contract Transceiver is 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 +135,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( diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol index 2c7e366e4..527caef02 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol @@ -61,15 +61,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 +104,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 ); } @@ -151,6 +151,7 @@ contract WormholeTransceiver is // 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; } @@ -176,7 +177,7 @@ contract WormholeTransceiver is address caller, bytes32 recipientNttManagerAddress, TransceiverStructs.TransceiverInstruction memory instruction, - bytes memory nttManagerMessage + bytes memory ManagerMessage ) internal override { ( TransceiverStructs.TransceiverMessage memory transceiverMessage, @@ -185,7 +186,7 @@ contract WormholeTransceiver is WH_TRANSCEIVER_PAYLOAD_PREFIX, toWormholeFormat(caller), recipientNttManagerAddress, - nttManagerMessage, + ManagerMessage, new bytes(0) ); diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index b3cce54e2..9c6826a19 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. diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol new file mode 100644 index 000000000..82d789cee --- /dev/null +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -0,0 +1,54 @@ +// 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 The caller is not the deployer. + error UnexpectedDeployer(address expectedOwner, address owner); + + /// @notice Peer chain ID cannot be zero. + error InvalidPeerChainIdZero(); + + /// @notice Peer cannot be the zero address. + error InvalidPeerZeroAddress(); + + 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 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 + ); +} diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 910a19e48..5a874f913 100644 --- a/evm/src/interfaces/INttManager.sol +++ b/evm/src/interfaces/INttManager.sol @@ -152,12 +152,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 +166,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..3345783c3 100644 --- a/evm/src/interfaces/ITransceiver.sol +++ b/evm/src/interfaces/ITransceiver.sol @@ -52,11 +52,11 @@ interface ITransceiver { /// @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. function sendMessage( uint16 recipientChain, TransceiverStructs.TransceiverInstruction memory instruction, - bytes memory nttManagerMessage, + bytes memory ManagerMessage, bytes32 recipientNttManagerAddress ) external payable; diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index e8afb6acc..c3cffbaac 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -24,13 +24,17 @@ library TransceiverStructs { /// This is 0x99'N''T''T' bytes4 constant NTT_PREFIX = 0x994E5454; + /// @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 the nttManager contract. /// 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 +45,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,13 +64,13 @@ 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 nttManagerMessage The parsed ManagerMessage struct. + function parseManagerMessage(bytes memory encoded) public pure - returns (NttManagerMessage memory nttManagerMessage) + returns (ManagerMessage memory nttManagerMessage) { uint256 offset = 0; (nttManagerMessage.id, offset) = encoded.asBytes32Unchecked(offset); @@ -143,6 +147,47 @@ library TransceiverStructs { encoded.checkLength(offset); } + /// @dev Native Token Transfer payload. + /// TODO: Document wire format. + struct NonFungibleNativeTokenTransfer { + /// @notice Address of the recipient. + bytes32 to; + /// @notice Chain ID of the recipient + uint16 toChain; + /// @notice Array of tokenIds. + uint256[] tokenIds; + } + + function encodeNonFungibleNativeTokenTransfer(NonFungibleNativeTokenTransfer memory nft) + public + pure + returns (bytes memory encoded) + { + return abi.encodePacked( + NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodeNftBatch(nft.tokenIds) + ); + } + + function encodeNftBatch(uint256[] memory tokenIds) public pure returns (bytes memory encoded) { + uint16 batchSize = uint16(tokenIds.length); + + encoded = abi.encodePacked(batchSize); + for (uint256 i = 0; i < batchSize; ++i) { + // For now encode each token ID as 32 bytes long. + // TODO: Optimize this to encode only the necessary bytes. + encoded = abi.encodePacked(encoded, uint8(32), tokenIds[i]); + } + } + + /// @dev Parse a NativeTokenTransfer. + /// @param encoded The byte array corresponding to the encoded message + /// @return nativeTokenTransfer The parsed NativeTokenTransfer struct. + function parseNonFungibleNativeTokenTransfer(bytes memory encoded) + public + pure + returns (NativeTokenTransfer memory nativeTokenTransfer) + {} + /// @dev Message emitted by Transceiver implementations. /// Each message includes an Transceiver-specified 4-byte prefix. /// The wire format is as follows: @@ -246,22 +291,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..c1d35c877 --- /dev/null +++ b/evm/src/mocks/DummyNft.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity >=0.8.8 <0.9.0; + +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.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", "DNTF") { + 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 _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 DummyTokenMintAndBurn is DummyNft { + constructor(bytes memory baseUri) DummyNft(baseUri) {} + + function mint(address to, uint256 tokenId) public override { + // TODO - add access control here? + _safeMint(to, tokenId); + } + + function burn(uint256 tokenId) public { + // TODO - add access control here? + _burn(tokenId); + } + + // TODO: Mint/Burn batches. +} diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index 2cb2ea8d0..34ab103b4 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.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); } @@ -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); @@ -743,7 +743,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 099aae8b0..adfa5f966 100644 --- a/evm/test/RateLimit.t.sol +++ b/evm/test/RateLimit.t.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) ); } } 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/libraries/TransceiverHelpers.sol b/evm/test/libraries/TransceiverHelpers.sol index ad6ef63ae..27b199489 100644 --- a/evm/test/libraries/TransceiverHelpers.sol +++ b/evm/test/libraries/TransceiverHelpers.sol @@ -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..50b9664f7 100644 --- a/evm/test/mocks/DummyTransceiver.sol +++ b/evm/test/mocks/DummyTransceiver.sol @@ -32,14 +32,14 @@ contract DummyTransceiver is Transceiver, ITransceiverReceiver { function receiveMessage(bytes memory encodedMessage) external { 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 ); } From 7b16e7bc57a1febbf06e8bb4eba5a51f6d95dc53 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 8 Mar 2024 17:13:02 -0600 Subject: [PATCH 04/19] evm: add inbound nft support --- .../NativeTransfers/NonFungibleNttManager.sol | 104 +++++++++++++++++- evm/src/interfaces/IManagerBase.sol | 8 ++ evm/src/interfaces/INonFungibleNttManager.sol | 33 ++++-- evm/src/interfaces/INonFungibleNttToken.sol | 12 ++ evm/src/libraries/TransceiverStructs.sol | 45 +++++--- 5 files changed, 173 insertions(+), 29 deletions(-) create mode 100644 evm/src/interfaces/INonFungibleNttToken.sol diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index e22e71287..d6a73897f 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -3,12 +3,16 @@ 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"; @@ -87,7 +91,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { // =============== External Interface ================================================== function transfer( - uint256[] calldata tokenIds, + uint256[] memory tokenIds, uint16 recipientChain, bytes32 recipient ) external payable nonReentrant whenNotPaused returns (uint64) { @@ -95,7 +99,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { } function transfer( - uint256[] calldata tokenIds, + uint256[] memory tokenIds, uint16 recipientChain, bytes32 recipient, bytes memory transceiverInstructions @@ -103,8 +107,64 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { return _transfer(tokenIds, recipientChain, recipient, transceiverInstructions); } + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory payload + ) external onlyTransceiver { + _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 { + // 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); + } + + _mintOrUnlockToRecipient(digest, fromWormholeFormat(nft.to), nft.tokenIds); + } + + 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[] calldata tokenIds, + uint256[] memory tokenIds, uint16 recipientChain, bytes32 recipient, bytes memory transceiverInstructions @@ -178,9 +238,25 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { return sequence; } + function _mintOrUnlockToRecipient( + bytes32 digest, + address recipient, + uint256[] memory tokenIds + ) internal { + emit TransferRedeemed(digest); + + if (mode == Mode.BURNING) { + _mintTokens(tokenIds, recipient); + } else if (mode == Mode.LOCKING) { + _unlockTokens(tokenIds, recipient); + } else { + revert InvalidMode(uint8(mode)); + } + } + // ==================== Internal Helpers =============================================== - function _lockTokens(uint256[] calldata tokenIds) internal { + function _lockTokens(uint256[] memory tokenIds) internal { uint256 len = tokenIds.length; for (uint256 i = 0; i < len; ++i) { @@ -188,7 +264,15 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { } } - function _burnTokens(uint256[] calldata tokenIds) internal { + 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) { @@ -196,8 +280,16 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { } } + 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 _verifyPeerInbound(uint16 sourceChainId, bytes32 peerAddress) internal view { + function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view { if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) { revert InvalidPeer(sourceChainId, peerAddress); } diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 9c6826a19..322d91e7e 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -61,6 +61,14 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverRemoved(address transceiver, uint8 threshold); + /// @notice Emitted when a message has already been executed to + /// notify client of against retries. + /// @dev Topic0 + /// 0x4069dff8c9df7e38d2867c0910bd96fd61787695e5380281148c04932d02bef2. + /// @param sourceNttManager The address of the source nttManager. + /// @param msgHash The keccak-256 hash of the message. + event MessageAlreadyExecuted(bytes32 indexed sourceNttManager, bytes32 indexed msgHash); + /// @notice payment for a transfer is too low. /// @param requiredPayment The required payment. /// @param providedPayment The provided payment. diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol index 82d789cee..cdb385fe5 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -20,6 +20,23 @@ interface INonFungibleNttManager is IManagerBase { /// @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 owner); @@ -29,6 +46,8 @@ interface INonFungibleNttManager is IManagerBase { /// @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); @@ -42,13 +61,9 @@ interface INonFungibleNttManager is IManagerBase { /// @param mode The mode. error InvalidMode(uint8 mode); - /// @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 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); } 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/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index c3cffbaac..3e1b5ddc1 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -163,30 +163,47 @@ library TransceiverStructs { pure returns (bytes memory encoded) { - return abi.encodePacked( - NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodeNftBatch(nft.tokenIds) - ); - } - - function encodeNftBatch(uint256[] memory tokenIds) public pure returns (bytes memory encoded) { - uint16 batchSize = uint16(tokenIds.length); + uint16 batchSize = uint16(nft.tokenIds.length); - encoded = abi.encodePacked(batchSize); + bytes memory encodedTokenIds = abi.encodePacked(batchSize); for (uint256 i = 0; i < batchSize; ++i) { // For now encode each token ID as 32 bytes long. // TODO: Optimize this to encode only the necessary bytes. - encoded = abi.encodePacked(encoded, uint8(32), tokenIds[i]); + encodedTokenIds = abi.encodePacked(encodedTokenIds, uint8(32), nft.tokenIds[i]); } + + return abi.encodePacked( + NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodedTokenIds + ); } - /// @dev Parse a NativeTokenTransfer. - /// @param encoded The byte array corresponding to the encoded message - /// @return nativeTokenTransfer The parsed NativeTokenTransfer struct. function parseNonFungibleNativeTokenTransfer(bytes memory encoded) public pure - returns (NativeTokenTransfer memory nativeTokenTransfer) - {} + 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) { + uint8 tokenIdLength; + (tokenIdLength, offset) = encoded.asUint8Unchecked(offset); + (tokenIds[i], offset) = encoded.asUint256Unchecked(offset); + } + + encoded.checkLength(offset); + } /// @dev Message emitted by Transceiver implementations. /// Each message includes an Transceiver-specified 4-byte prefix. From ef8a6db67535f8b188a3d833377d8e7a83d7a361 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Tue, 12 Mar 2024 10:24:36 -0500 Subject: [PATCH 05/19] evm: add admin tests for nft module --- .../NativeTransfers/NonFungibleNttManager.sol | 51 ++-- evm/src/interfaces/IManagerBase.sol | 2 +- evm/src/interfaces/INonFungibleNttManager.sol | 22 +- evm/src/libraries/TransceiverStructs.sol | 4 +- evm/src/mocks/DummyNft.sol | 2 +- evm/test/NonFungibleNttManager.sol | 243 ++++++++++++++++++ evm/test/mocks/DummyTransceiver.sol | 23 +- 7 files changed, 304 insertions(+), 43 deletions(-) create mode 100644 evm/test/NonFungibleNttManager.sol diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index d6a73897f..689fd95a8 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -21,17 +21,11 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { // =============== Immutables ============================================================ - uint8 public constant MAX_BATCH_SIZE = 50; + uint8 constant MAX_BATCH_SIZE = 50; // =============== Setup ================================================================= - constructor( - address _token, - Mode _mode, - uint16 _chainId, - uint64 _rateLimitDuration, - bool _skipRateLimiting - ) ManagerBase(_token, _mode, _chainId) {} + constructor(address _token, Mode _mode, uint16 _chainId) ManagerBase(_token, _mode, _chainId) {} function __NonFungibleNttManager_init() internal onlyInitializing { // check if the owner is the deployer of this contract @@ -71,6 +65,10 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { return _getPeersStorage()[chainId_]; } + function getMaxBatchSize() external view returns (uint8) { + return MAX_BATCH_SIZE; + } + // =============== Admin ============================================================== function setPeer(uint16 peerChainId, bytes32 peerContract) public onlyOwner { @@ -90,14 +88,6 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { // =============== External Interface ================================================== - function transfer( - uint256[] memory tokenIds, - uint16 recipientChain, - bytes32 recipient - ) external payable nonReentrant whenNotPaused returns (uint64) { - return _transfer(tokenIds, recipientChain, recipient, new bytes(1)); - } - function transfer( uint256[] memory tokenIds, uint16 recipientChain, @@ -145,7 +135,15 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { revert InvalidTargetChain(nft.toChain, chainId); } - _mintOrUnlockToRecipient(digest, fromWormholeFormat(nft.to), nft.tokenIds); + 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( @@ -153,14 +151,13 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { address, uint256, bytes calldata - ) external view returns (bytes4){ + ) external view returns (bytes4) { if (operator != address(this)) { revert InvalidOperator(operator, address(this)); } return type(IERC721Receiver).interfaceId; } - // ==================== Internal Business Logic ========================================= function _transfer( @@ -238,22 +235,6 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { return sequence; } - function _mintOrUnlockToRecipient( - bytes32 digest, - address recipient, - uint256[] memory tokenIds - ) internal { - emit TransferRedeemed(digest); - - if (mode == Mode.BURNING) { - _mintTokens(tokenIds, recipient); - } else if (mode == Mode.LOCKING) { - _unlockTokens(tokenIds, recipient); - } else { - revert InvalidMode(uint8(mode)); - } - } - // ==================== Internal Helpers =============================================== function _lockTokens(uint256[] memory tokenIds) internal { diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 322d91e7e..8c3211dcb 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -61,7 +61,7 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverRemoved(address transceiver, uint8 threshold); - /// @notice Emitted when a message has already been executed to + /// @notice Emitted when a message has already been executed to /// notify client of against retries. /// @dev Topic0 /// 0x4069dff8c9df7e38d2867c0910bd96fd61787695e5380281148c04932d02bef2. diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol index cdb385fe5..0ba3bb3e6 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -38,7 +38,7 @@ interface INonFungibleNttManager is IManagerBase { event TransferRedeemed(bytes32 indexed digest); /// @notice The caller is not the deployer. - error UnexpectedDeployer(address expectedOwner, address owner); + error UnexpectedDeployer(address expectedOwner, address caller); /// @notice Peer chain ID cannot be zero. error InvalidPeerChainIdZero(); @@ -65,5 +65,23 @@ interface INonFungibleNttManager is IManagerBase { /// @dev Selector 0x3dcb204a. /// @param targetChain The target chain. /// @param thisChain The current chain. - error InvalidTargetChain(uint16 targetChain, uint16 thisChain); + error InvalidTargetChain(uint16 targetChain, uint16 thisChain); + + /// @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 view returns (uint8); + + function transfer( + uint256[] memory tokenIds, + uint16 recipientChain, + bytes32 recipient, + bytes memory transceiverInstructions + ) external payable returns (uint64); } diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index 3e1b5ddc1..d6c2cc791 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -172,9 +172,7 @@ library TransceiverStructs { encodedTokenIds = abi.encodePacked(encodedTokenIds, uint8(32), nft.tokenIds[i]); } - return abi.encodePacked( - NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodedTokenIds - ); + return abi.encodePacked(NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodedTokenIds); } function parseNonFungibleNativeTokenTransfer(bytes memory encoded) diff --git a/evm/src/mocks/DummyNft.sol b/evm/src/mocks/DummyNft.sol index c1d35c877..84e657816 100644 --- a/evm/src/mocks/DummyNft.sol +++ b/evm/src/mocks/DummyNft.sol @@ -47,7 +47,7 @@ contract DummyNft is ERC721 { } } -contract DummyTokenMintAndBurn is DummyNft { +contract DummyNftMintAndBurn is DummyNft { constructor(bytes memory baseUri) DummyNft(baseUri) {} function mint(address to, uint256 tokenId) public override { diff --git a/evm/test/NonFungibleNttManager.sol b/evm/test/NonFungibleNttManager.sol new file mode 100644 index 000000000..536dddb08 --- /dev/null +++ b/evm/test/NonFungibleNttManager.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "forge-std/Vm.sol"; + +import "../src/interfaces/INonFungibleNttManager.sol"; +import "../src/interfaces/IManagerBase.sol"; + +import "../src/NativeTransfers/NonFungibleNttManager.sol"; +import "../src/NativeTransfers/shared/TransceiverRegistry.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 {Utils} from "./libraries/Utils.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "../src/libraries/external/OwnableUpgradeable.sol"; + +import "./mocks/DummyTransceiver.sol"; +import "../src/mocks/DummyNft.sol"; + +contract TestNonFungibleNttManager is Test { + uint16 constant chainIdOne = 2; + uint16 constant chainIdTwo = 6; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + address owner = makeAddr("owner"); + + // Deployed contracts. + DummyNftMintAndBurn nft; + INonFungibleNttManager managerOne; + INonFungibleNttManager managerTwo; + DummyTransceiverWithChainId transceiverOne; + DummyTransceiverWithChainId transceiverTwo; + + function deployNonFungibleManager( + address _nft, + IManagerBase.Mode _mode, + uint16 _chainId, + bool shouldInitialize + ) internal returns (INonFungibleNttManager) { + NonFungibleNttManager implementation = + new NonFungibleNttManager(address(_nft), _mode, _chainId); + + NonFungibleNttManager proxy = + NonFungibleNttManager(address(new ERC1967Proxy(address(implementation), ""))); + + if (shouldInitialize) { + proxy.initialize(); + } + + return INonFungibleNttManager(address(proxy)); + } + + function setUp() public { + string memory url = "https://ethereum-goerli.publicnode.com"; + IWormhole wormhole = IWormhole(0x706abc4E45D419950511e474C7B9Ed348A4a716c); + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + // Deploy contracts as owner. + vm.startPrank(owner); + + // Nft collection. + nft = new DummyNftMintAndBurn(bytes("https://metadata.dn.com/y/")); + + // Managers. + managerOne = + deployNonFungibleManager(address(nft), IManagerBase.Mode.LOCKING, chainIdOne, true); + managerTwo = + deployNonFungibleManager(address(nft), IManagerBase.Mode.BURNING, chainIdTwo, true); + + // Wormhole Transceivers. + transceiverOne = new DummyTransceiverWithChainId(address(managerOne), chainIdTwo); + transceiverTwo = new DummyTransceiverWithChainId(address(managerTwo), chainIdOne); + + // Register transceivers and peers. + managerOne.setTransceiver(address(transceiverOne)); + managerTwo.setTransceiver(address(transceiverTwo)); + + managerOne.setPeer(chainIdTwo, toWormholeFormat(address(managerTwo))); + managerTwo.setPeer(chainIdOne, toWormholeFormat(address(managerOne))); + + vm.stopPrank(); + } + + // ================================== Admin Tests ================================== + + function test_cannotInitalizeNotDeployer() public { + // Don't initialize. + vm.prank(owner); + INonFungibleNttManager dummyManager = + deployNonFungibleManager(address(nft), IManagerBase.Mode.LOCKING, chainIdOne, false); + + 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); + } + + // ============================ Business Logic Tests ================================== + + function test_transferLocked(uint256 nftCount) public { + nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = mintNftBatch(nft, recipient, nftCount, 0); + + // Transfer NFTs as the owner of the NFTs. + vm.startPrank(recipient); + nft.setApprovalForAll(address(managerOne), true); + + vm.recordLogs(); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + vm.stopPrank(); + + // Check if the NFTs are locked. + for (uint256 i = 0; i < nftCount; i++) { + uint256 tokenId = tokenIds[i]; + assertEq(nft.ownerOf(tokenId), address(managerOne), "NFT should be locked"); + } + + // Fetch the wormhole message. + //bytes memory encodedVm = getWormholeMessage(vm.getRecordedLogs(), chainIdOne)[0]; + } + + // ==================================== Helpers ======================================= + + function mintNftBatch( + DummyNftMintAndBurn _nft, + address recipient, + uint256 len, + uint256 start + ) public 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 getWormholeMessage( + Vm.Log[] memory logs, + uint16 emitterChain + ) internal 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; + } +} diff --git a/evm/test/mocks/DummyTransceiver.sol b/evm/test/mocks/DummyTransceiver.sol index 50b9664f7..0e65e6f7f 100644 --- a/evm/test/mocks/DummyTransceiver.sol +++ b/evm/test/mocks/DummyTransceiver.sol @@ -30,7 +30,7 @@ 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.ManagerMessage memory parsedManagerMessage; (parsedTransceiverMessage, parsedManagerMessage) = TransceiverStructs @@ -49,3 +49,24 @@ contract DummyTransceiver is Transceiver, ITransceiverReceiver { returns (uint16 recipientChain, bytes memory payload) {} } + +contract DummyTransceiverWithChainId is DummyTransceiver { + uint16 public fromChainId; + + constructor(address nttManager, uint16 _fromChainId) DummyTransceiver(nttManager) { + fromChainId = _fromChainId; + } + + function receiveMessage(bytes memory encodedMessage) external override { + TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; + TransceiverStructs.ManagerMessage memory parsedManagerMessage; + (parsedTransceiverMessage, parsedManagerMessage) = TransceiverStructs + .parseTransceiverAndManagerMessage(TEST_TRANSCEIVER_PAYLOAD_PREFIX, encodedMessage); + _deliverToNttManager( + fromChainId, + parsedTransceiverMessage.sourceNttManagerAddress, + parsedTransceiverMessage.recipientNttManagerAddress, + parsedManagerMessage + ); + } +} From 9294e17771d55e6af4b4d7aa1cf1a8d255b4c600 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Tue, 12 Mar 2024 14:21:20 -0500 Subject: [PATCH 06/19] evm: add arbitrary payload to NFT transfer --- .../NativeTransfers/NonFungibleNttManager.sol | 9 ++- evm/src/interfaces/INonFungibleNttManager.sol | 2 +- evm/src/libraries/TransceiverStructs.sol | 13 ++- evm/test/NonFungibleNttManager.sol | 79 ++++++++++++++++--- evm/test/mocks/DummyTransceiver.sol | 21 ----- 5 files changed, 89 insertions(+), 35 deletions(-) diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index 689fd95a8..16210d4c1 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -65,7 +65,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { return _getPeersStorage()[chainId_]; } - function getMaxBatchSize() external view returns (uint8) { + function getMaxBatchSize() external pure returns (uint8) { return MAX_BATCH_SIZE; } @@ -200,8 +200,11 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { uint64 sequence = _useMessageSequence(); - TransceiverStructs.NonFungibleNativeTokenTransfer memory nft = - TransceiverStructs.NonFungibleNativeTokenTransfer(recipient, recipientChain, tokenIds); + /// 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( diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol index 0ba3bb3e6..0dd475c01 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -76,7 +76,7 @@ interface INonFungibleNttManager is IManagerBase { function getPeer(uint16 chainId_) external view returns (NonFungibleNttManagerPeer memory); - function getMaxBatchSize() external view returns (uint8); + function getMaxBatchSize() external pure returns (uint8); function transfer( uint256[] memory tokenIds, diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index d6c2cc791..0683bccb3 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -156,6 +156,8 @@ library TransceiverStructs { uint16 toChain; /// @notice Array of tokenIds. uint256[] tokenIds; + /// @notice Arbitrary payload per manager implementation. + bytes payload; } function encodeNonFungibleNativeTokenTransfer(NonFungibleNativeTokenTransfer memory nft) @@ -164,6 +166,7 @@ library TransceiverStructs { 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) { @@ -172,7 +175,9 @@ library TransceiverStructs { encodedTokenIds = abi.encodePacked(encodedTokenIds, uint8(32), nft.tokenIds[i]); } - return abi.encodePacked(NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodedTokenIds); + return abi.encodePacked( + NON_FUNGIBLE_NTT_PREFIX, nft.to, nft.toChain, encodedTokenIds, payloadLen, nft.payload + ); } function parseNonFungibleNativeTokenTransfer(bytes memory encoded) @@ -199,6 +204,12 @@ library TransceiverStructs { (tokenIdLength, offset) = encoded.asUint8Unchecked(offset); (tokenIds[i], offset) = encoded.asUint256Unchecked(offset); } + nonFungibleNtt.tokenIds = tokenIds; + + // Decode arbitrary payload. + uint16 payloadLength; + (payloadLength, offset) = encoded.asUint16Unchecked(offset); + (nonFungibleNtt.payload, offset) = encoded.sliceUnchecked(offset, payloadLength); encoded.checkLength(offset); } diff --git a/evm/test/NonFungibleNttManager.sol b/evm/test/NonFungibleNttManager.sol index 536dddb08..331a7d03a 100644 --- a/evm/test/NonFungibleNttManager.sol +++ b/evm/test/NonFungibleNttManager.sol @@ -10,6 +10,7 @@ import "../src/interfaces/IManagerBase.sol"; import "../src/NativeTransfers/NonFungibleNttManager.sol"; import "../src/NativeTransfers/shared/TransceiverRegistry.sol"; +import "../src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol"; import "./interfaces/ITransceiverReceiver.sol"; import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; @@ -22,16 +23,20 @@ import {Utils} from "./libraries/Utils.sol"; import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../src/libraries/external/OwnableUpgradeable.sol"; -import "./mocks/DummyTransceiver.sol"; +import "./mocks/MockTransceivers.sol"; import "../src/mocks/DummyNft.sol"; contract TestNonFungibleNttManager is Test { uint16 constant chainIdOne = 2; uint16 constant chainIdTwo = 6; + bytes4 constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; uint256 constant DEVNET_GUARDIAN_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; WormholeSimulator guardian; uint256 initialBlockTimestamp; + address relayer = 0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a; + uint8 consistencyLevel = 1; + uint256 baseGasLimit = 500000; address owner = makeAddr("owner"); @@ -39,8 +44,8 @@ contract TestNonFungibleNttManager is Test { DummyNftMintAndBurn nft; INonFungibleNttManager managerOne; INonFungibleNttManager managerTwo; - DummyTransceiverWithChainId transceiverOne; - DummyTransceiverWithChainId transceiverTwo; + MockWormholeTransceiverContract transceiverOne; + MockWormholeTransceiverContract transceiverTwo; function deployNonFungibleManager( address _nft, @@ -82,8 +87,22 @@ contract TestNonFungibleNttManager is Test { deployNonFungibleManager(address(nft), IManagerBase.Mode.BURNING, chainIdTwo, true); // Wormhole Transceivers. - transceiverOne = new DummyTransceiverWithChainId(address(managerOne), chainIdTwo); - transceiverTwo = new DummyTransceiverWithChainId(address(managerTwo), chainIdOne); + transceiverOne = new MockWormholeTransceiverContract( + address(managerOne), + address(guardian.wormhole()), + relayer, + address(0), + consistencyLevel, + baseGasLimit + ); + transceiverTwo = new MockWormholeTransceiverContract( + address(managerTwo), + address(guardian.wormhole()), + relayer, + address(0), + consistencyLevel, + baseGasLimit + ); // Register transceivers and peers. managerOne.setTransceiver(address(transceiverOne)); @@ -190,14 +209,19 @@ contract TestNonFungibleNttManager is Test { nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); address recipient = makeAddr("recipient"); - uint256[] memory tokenIds = mintNftBatch(nft, recipient, nftCount, 0); + uint256[] memory tokenIds = mintNftBatch(nft, recipient, nftCount, 50); // Transfer NFTs as the owner of the NFTs. vm.startPrank(recipient); nft.setApprovalForAll(address(managerOne), true); vm.recordLogs(); - managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); + managerOne.transfer( + tokenIds, + chainIdTwo, + toWormholeFormat(recipient), + encodeTransceiverInstruction(true, transceiverOne) + ); vm.stopPrank(); // Check if the NFTs are locked. @@ -207,7 +231,28 @@ contract TestNonFungibleNttManager is Test { } // Fetch the wormhole message. - //bytes memory encodedVm = getWormholeMessage(vm.getRecordedLogs(), chainIdOne)[0]; + bytes memory encodedVm = getWormholeMessage(vm.getRecordedLogs(), chainIdOne)[0]; + + // Verify the manager message + bytes memory vmPayload = guardian.wormhole().parseVM(encodedVm).payload; + (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs + .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + + assertEq(uint256(message.id), managerOne.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, chainIdTwo); + assertEq(nftTransfer.payload, new bytes(0)); + assertEq(nftTransfer.tokenIds.length, nftCount); + + for (uint256 i = 0; i < nftCount; i++) { + assertEq(nftTransfer.tokenIds[i], tokenIds[i]); + } } // ==================================== Helpers ======================================= @@ -231,7 +276,7 @@ contract TestNonFungibleNttManager is Test { function getWormholeMessage( Vm.Log[] memory logs, uint16 emitterChain - ) internal returns (bytes[] memory) { + ) 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++) { @@ -240,4 +285,20 @@ contract TestNonFungibleNttManager is Test { return encodedVMs; } + + function encodeTransceiverInstruction( + bool relayer_off, + MockWormholeTransceiverContract transceiver + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + 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); + } } diff --git a/evm/test/mocks/DummyTransceiver.sol b/evm/test/mocks/DummyTransceiver.sol index 0e65e6f7f..137b4067e 100644 --- a/evm/test/mocks/DummyTransceiver.sol +++ b/evm/test/mocks/DummyTransceiver.sol @@ -49,24 +49,3 @@ contract DummyTransceiver is Transceiver, ITransceiverReceiver { returns (uint16 recipientChain, bytes memory payload) {} } - -contract DummyTransceiverWithChainId is DummyTransceiver { - uint16 public fromChainId; - - constructor(address nttManager, uint16 _fromChainId) DummyTransceiver(nttManager) { - fromChainId = _fromChainId; - } - - function receiveMessage(bytes memory encodedMessage) external override { - TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; - TransceiverStructs.ManagerMessage memory parsedManagerMessage; - (parsedTransceiverMessage, parsedManagerMessage) = TransceiverStructs - .parseTransceiverAndManagerMessage(TEST_TRANSCEIVER_PAYLOAD_PREFIX, encodedMessage); - _deliverToNttManager( - fromChainId, - parsedTransceiverMessage.sourceNttManagerAddress, - parsedTransceiverMessage.recipientNttManagerAddress, - parsedManagerMessage - ); - } -} From 02e5010db30f3b22de135a6926f83ff846e776fc Mon Sep 17 00:00:00 2001 From: gator-boi Date: Wed, 13 Mar 2024 13:07:45 -0500 Subject: [PATCH 07/19] evm: add ManagerType to WormholeTransceiver --- .../WormholeTransceiver.sol | 6 +- .../WormholeTransceiverState.sol | 33 ++-- .../interfaces/IWormholeTransceiverState.sol | 14 ++ evm/test/IntegrationRelayer.t.sol | 12 +- evm/test/IntegrationStandalone.t.sol | 13 +- ...anager.sol => NonFungibleNttManager.t.sol} | 149 ++++++++++++------ evm/test/Upgrades.t.sol | 28 ++-- evm/test/mocks/MockTransceivers.sol | 24 ++- 8 files changed, 186 insertions(+), 93 deletions(-) rename evm/test/{NonFungibleNttManager.sol => NonFungibleNttManager.t.sol} (70%) diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol index 527caef02..ee900001e 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 _gasLimit, + IWormholeTransceiverState.ManagerType _managerType ) WormholeTransceiverState( nttManager, @@ -47,7 +48,8 @@ contract WormholeTransceiver is wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit + _gasLimit, + _managerType ) {} diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol index feadef784..cbcddd6ea 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol @@ -28,6 +28,7 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce ISpecialRelayer public immutable specialRelayer; uint256 immutable wormholeTransceiver_evmChainId; uint256 public immutable gasLimit; + ManagerType public immutable managerType; // ==================== Constants ================================================ @@ -51,7 +52,8 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit + uint256 _gasLimit, + ManagerType _managerType ) Transceiver(nttManager) { wormhole = IWormhole(wormholeCoreBridge); wormholeRelayer = IWormholeRelayer(wormholeRelayerAddr); @@ -59,12 +61,7 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce wormholeTransceiver_evmChainId = block.chainid; consistencyLevel = _consistencyLevel; gasLimit = _gasLimit; - } - - enum RelayingType { - Standard, - Special, - Manual + managerType = _managerType; } function _initialize() internal override { @@ -73,14 +70,20 @@ 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/IWormholeTransceiverState.sol b/evm/src/interfaces/IWormholeTransceiverState.sol index 50640309a..48ff10f0d 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(); diff --git a/evm/test/IntegrationRelayer.t.sol b/evm/test/IntegrationRelayer.t.sol index 27345a9dd..42b202b75 100755 --- a/evm/test/IntegrationRelayer.t.sol +++ b/evm/test/IntegrationRelayer.t.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( @@ -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), "")) diff --git a/evm/test/IntegrationStandalone.t.sol b/evm/test/IntegrationStandalone.t.sol index 4ed16eb5d..80b8cedb0 100755 --- a/evm/test/IntegrationStandalone.t.sol +++ b/evm/test/IntegrationStandalone.t.sol @@ -11,6 +11,7 @@ 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"; @@ -77,7 +78,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 +116,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 +478,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 +497,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/NonFungibleNttManager.sol b/evm/test/NonFungibleNttManager.t.sol similarity index 70% rename from evm/test/NonFungibleNttManager.sol rename to evm/test/NonFungibleNttManager.t.sol index 331a7d03a..dc484382b 100644 --- a/evm/test/NonFungibleNttManager.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -7,10 +7,12 @@ 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"; @@ -34,18 +36,19 @@ contract TestNonFungibleNttManager is Test { 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; WormholeSimulator guardian; uint256 initialBlockTimestamp; - address relayer = 0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a; + address relayer = 0x7B1bD7a6b4E61c2a123AC6BC2cbfC614437D0470; uint8 consistencyLevel = 1; uint256 baseGasLimit = 500000; address owner = makeAddr("owner"); // Deployed contracts. - DummyNftMintAndBurn nft; + DummyNftMintAndBurn nftOne; + DummyNftMintAndBurn nftTwo; INonFungibleNttManager managerOne; INonFungibleNttManager managerTwo; - MockWormholeTransceiverContract transceiverOne; - MockWormholeTransceiverContract transceiverTwo; + WormholeTransceiver transceiverOne; + WormholeTransceiver transceiverTwo; function deployNonFungibleManager( address _nft, @@ -66,9 +69,31 @@ contract TestNonFungibleNttManager is Test { return INonFungibleNttManager(address(proxy)); } + function deployWormholeTranceiver( + address manager + ) internal 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 setUp() public { - string memory url = "https://ethereum-goerli.publicnode.com"; - IWormhole wormhole = IWormhole(0x706abc4E45D419950511e474C7B9Ed348A4a716c); + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); vm.createSelectFork(url); initialBlockTimestamp = vm.getBlockTimestamp(); @@ -78,31 +103,21 @@ contract TestNonFungibleNttManager is Test { vm.startPrank(owner); // Nft collection. - nft = new DummyNftMintAndBurn(bytes("https://metadata.dn.com/y/")); + nftOne = new DummyNftMintAndBurn(bytes("https://metadata.dn69.com/y/")); + nftTwo = new DummyNftMintAndBurn(bytes("https://metadata.dn420.com/y/")); // Managers. managerOne = - deployNonFungibleManager(address(nft), IManagerBase.Mode.LOCKING, chainIdOne, true); + deployNonFungibleManager(address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, true); managerTwo = - deployNonFungibleManager(address(nft), IManagerBase.Mode.BURNING, chainIdTwo, true); + deployNonFungibleManager(address(nftTwo), IManagerBase.Mode.BURNING, chainIdTwo, true); // Wormhole Transceivers. - transceiverOne = new MockWormholeTransceiverContract( - address(managerOne), - address(guardian.wormhole()), - relayer, - address(0), - consistencyLevel, - baseGasLimit - ); - transceiverTwo = new MockWormholeTransceiverContract( - address(managerTwo), - address(guardian.wormhole()), - relayer, - address(0), - consistencyLevel, - baseGasLimit - ); + transceiverOne = deployWormholeTranceiver(address(managerOne)); + transceiverTwo = deployWormholeTranceiver(address(managerTwo)); + + transceiverOne.setWormholePeer(chainIdTwo, toWormholeFormat(address(transceiverTwo))); + transceiverTwo.setWormholePeer(chainIdOne, toWormholeFormat(address(transceiverOne))); // Register transceivers and peers. managerOne.setTransceiver(address(transceiverOne)); @@ -120,7 +135,7 @@ contract TestNonFungibleNttManager is Test { // Don't initialize. vm.prank(owner); INonFungibleNttManager dummyManager = - deployNonFungibleManager(address(nft), IManagerBase.Mode.LOCKING, chainIdOne, false); + deployNonFungibleManager(address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, false); vm.prank(makeAddr("notOwner")); vm.expectRevert( @@ -205,34 +220,24 @@ contract TestNonFungibleNttManager is Test { // ============================ Business Logic Tests ================================== - function test_transferLocked(uint256 nftCount) public { + function test_transferLocked(uint256 nftCount, uint256 startId) public { nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); + startId = bound(startId, 0, type(uint256).max - nftCount); address recipient = makeAddr("recipient"); - uint256[] memory tokenIds = mintNftBatch(nft, recipient, nftCount, 50); - - // Transfer NFTs as the owner of the NFTs. - vm.startPrank(recipient); - nft.setApprovalForAll(address(managerOne), true); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); - vm.recordLogs(); - managerOne.transfer( - tokenIds, - chainIdTwo, - toWormholeFormat(recipient), - encodeTransceiverInstruction(true, transceiverOne) - ); - vm.stopPrank(); + // Call the specified manager to transfer the batch. + bytes memory encodedVm = _approveAndTransferBatch( + managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; // Check if the NFTs are locked. for (uint256 i = 0; i < nftCount; i++) { uint256 tokenId = tokenIds[i]; - assertEq(nft.ownerOf(tokenId), address(managerOne), "NFT should be locked"); + assertEq(nftOne.ownerOf(tokenId), address(managerOne), "NFT should be locked"); } - // Fetch the wormhole message. - bytes memory encodedVm = getWormholeMessage(vm.getRecordedLogs(), chainIdOne)[0]; - // Verify the manager message bytes memory vmPayload = guardian.wormhole().parseVM(encodedVm).payload; (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs @@ -242,8 +247,8 @@ contract TestNonFungibleNttManager is Test { assertEq(message.sender, toWormholeFormat(recipient)); // Verify the non-fungible transfer message. - TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = TransceiverStructs - .parseNonFungibleNativeTokenTransfer(message.payload); + TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = + TransceiverStructs.parseNonFungibleNativeTokenTransfer(message.payload); assertEq(nftTransfer.to, toWormholeFormat(recipient)); assertEq(nftTransfer.toChain, chainIdTwo); @@ -255,14 +260,56 @@ contract TestNonFungibleNttManager is Test { } } + function test_receiveMessageAndMint(uint256 nftCount, uint256 startId) public { + nftCount = bound(nftCount, 1, managerTwo.getMaxBatchSize()); + startId = bound(startId, 0, type(uint256).max - nftCount); + + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); + + // Transfer the NFTs to the recipient. + bytes memory encodedVm = _approveAndTransferBatch( + managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + )[0]; + + // Receive the message and mint the NFTs. + transceiverTwo.receiveMessage(encodedVm); + } + // ==================================== Helpers ======================================= - function mintNftBatch( + function _approveAndTransferBatch( + 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(managerOne), true); + + vm.recordLogs(); + managerOne.transfer( + tokenIds, + targetChain, + toWormholeFormat(recipient), + encodeTransceiverInstruction(relayerOff, transceiver) + ); + vm.stopPrank(); + + // Fetch the wormhole message. + encodedVms = getWormholeMessage(vm.getRecordedLogs(), managerOne.chainId()); + } + + function _mintNftBatch( DummyNftMintAndBurn _nft, address recipient, uint256 len, uint256 start - ) public returns (uint256[] memory) { + ) internal returns (uint256[] memory) { uint256[] memory arr = new uint256[](len); for (uint256 i = 0; i < len; i++) { uint256 tokenId = start + i; @@ -287,11 +334,11 @@ contract TestNonFungibleNttManager is Test { } function encodeTransceiverInstruction( - bool relayer_off, - MockWormholeTransceiverContract transceiver + bool relayerOff, + WormholeTransceiver transceiver ) public view returns (bytes memory) { WormholeTransceiver.WormholeTransceiverInstruction memory instruction = - IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + IWormholeTransceiver.WormholeTransceiverInstruction(relayerOff); bytes memory encodedInstructionWormhole = transceiver.encodeWormholeTransceiverInstruction(instruction); TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = TransceiverStructs diff --git a/evm/test/Upgrades.t.sol b/evm/test/Upgrades.t.sol index a4c864ce7..43eea9e45 100644 --- a/evm/test/Upgrades.t.sol +++ b/evm/test/Upgrades.t.sol @@ -9,6 +9,7 @@ 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/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 ) {} From 63693b49b01411a7ec0bb56f1c2b39d7e8fc6e50 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Wed, 13 Mar 2024 17:35:37 -0500 Subject: [PATCH 08/19] evm: add receive batch message test --- .../WormholeTransceiverState.sol | 4 +- evm/test/NonFungibleNttManager.t.sol | 60 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol index cbcddd6ea..d0ec1a5e2 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol @@ -78,7 +78,9 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce tokenAddress: toWormholeFormat(nttManagerToken), tokenDecimals: INttManager(nttManager).tokenDecimals() }); - wormhole.publishMessage(0, TransceiverStructs.encodeTransceiverInit(init), consistencyLevel); + wormhole.publishMessage( + 0, TransceiverStructs.encodeTransceiverInit(init), consistencyLevel + ); } else if (managerType == ManagerType.ERC721) { // Skip emitting message for ERC721. } else { diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index dc484382b..026184ed5 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -51,13 +51,13 @@ contract TestNonFungibleNttManager is Test { WormholeTransceiver transceiverTwo; function deployNonFungibleManager( - address _nft, + address nft, IManagerBase.Mode _mode, uint16 _chainId, bool shouldInitialize ) internal returns (INonFungibleNttManager) { NonFungibleNttManager implementation = - new NonFungibleNttManager(address(_nft), _mode, _chainId); + new NonFungibleNttManager(address(nft), _mode, _chainId); NonFungibleNttManager proxy = NonFungibleNttManager(address(new ERC1967Proxy(address(implementation), ""))); @@ -69,9 +69,7 @@ contract TestNonFungibleNttManager is Test { return INonFungibleNttManager(address(proxy)); } - function deployWormholeTranceiver( - address manager - ) internal returns (WormholeTransceiver) { + function deployWormholeTranceiver(address manager) internal returns (WormholeTransceiver) { // Wormhole Transceivers. WormholeTransceiver implementation = new WormholeTransceiver( manager, @@ -267,21 +265,53 @@ contract TestNonFungibleNttManager is Test { address recipient = makeAddr("recipient"); uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); - // Transfer the NFTs to the recipient. + // Lock the NFTs on managerOne. bytes memory encodedVm = _approveAndTransferBatch( managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; // Receive the message and mint the NFTs. transceiverTwo.receiveMessage(encodedVm); + + // Verify state changes. + assertTrue(managerTwo.isMessageExecuted(_computeMessageDigest(chainIdOne, encodedVm))); + assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); + assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); } // ==================================== Helpers ======================================= + 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 _computeMessageDigest( + 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( INonFungibleNttManager manager, WormholeTransceiver transceiver, - DummyNftMintAndBurn _nft, + DummyNftMintAndBurn nft, uint256[] memory tokenIds, address recipient, uint16 targetChain, @@ -289,23 +319,23 @@ contract TestNonFungibleNttManager is Test { ) internal returns (bytes[] memory encodedVms) { // Transfer NFTs as the owner of the NFTs. vm.startPrank(recipient); - _nft.setApprovalForAll(address(managerOne), true); + nft.setApprovalForAll(address(managerOne), true); vm.recordLogs(); managerOne.transfer( tokenIds, targetChain, toWormholeFormat(recipient), - encodeTransceiverInstruction(relayerOff, transceiver) + _encodeTransceiverInstruction(relayerOff, transceiver) ); vm.stopPrank(); // Fetch the wormhole message. - encodedVms = getWormholeMessage(vm.getRecordedLogs(), managerOne.chainId()); + encodedVms = _getWormholeMessage(vm.getRecordedLogs(), managerOne.chainId()); } function _mintNftBatch( - DummyNftMintAndBurn _nft, + DummyNftMintAndBurn nft, address recipient, uint256 len, uint256 start @@ -315,12 +345,12 @@ contract TestNonFungibleNttManager is Test { uint256 tokenId = start + i; arr[i] = tokenId; - _nft.mint(recipient, tokenId); + nft.mint(recipient, tokenId); } return arr; } - function getWormholeMessage( + function _getWormholeMessage( Vm.Log[] memory logs, uint16 emitterChain ) internal view returns (bytes[] memory) { @@ -333,10 +363,10 @@ contract TestNonFungibleNttManager is Test { return encodedVMs; } - function encodeTransceiverInstruction( + function _encodeTransceiverInstruction( bool relayerOff, WormholeTransceiver transceiver - ) public view returns (bytes memory) { + ) internal view returns (bytes memory) { WormholeTransceiver.WormholeTransceiverInstruction memory instruction = IWormholeTransceiver.WormholeTransceiverInstruction(relayerOff); bytes memory encodedInstructionWormhole = From dc5c4cdf2817a7b6a1174fd0fb8ca7d6c7605a98 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 14 Mar 2024 09:53:47 -0500 Subject: [PATCH 09/19] evm: add all positive transfer tests --- evm/src/mocks/DummyNft.sol | 4 + evm/test/NonFungibleNttManager.t.sol | 143 ++++++++++++++++++++------- 2 files changed, 114 insertions(+), 33 deletions(-) diff --git a/evm/src/mocks/DummyNft.sol b/evm/src/mocks/DummyNft.sol index 84e657816..f0653b09a 100644 --- a/evm/src/mocks/DummyNft.sol +++ b/evm/src/mocks/DummyNft.sol @@ -38,6 +38,10 @@ contract DummyNft is ERC721 { 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; diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index 026184ed5..98cdb332b 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -31,6 +31,7 @@ import "../src/mocks/DummyNft.sol"; contract TestNonFungibleNttManager is Test { uint16 constant chainIdOne = 2; uint16 constant chainIdTwo = 6; + uint16 constant chainIdThree = 10; bytes4 constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; uint256 constant DEVNET_GUARDIAN_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; @@ -47,8 +48,10 @@ contract TestNonFungibleNttManager is Test { DummyNftMintAndBurn nftTwo; INonFungibleNttManager managerOne; INonFungibleNttManager managerTwo; + INonFungibleNttManager managerThree; WormholeTransceiver transceiverOne; WormholeTransceiver transceiverTwo; + WormholeTransceiver transceiverThree; function deployNonFungibleManager( address nft, @@ -109,20 +112,32 @@ contract TestNonFungibleNttManager is Test { deployNonFungibleManager(address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, true); managerTwo = deployNonFungibleManager(address(nftTwo), IManagerBase.Mode.BURNING, chainIdTwo, true); + managerThree = deployNonFungibleManager( + address(nftOne), + IManagerBase.Mode.BURNING, + chainIdThree, + true + ); // Wormhole Transceivers. transceiverOne = deployWormholeTranceiver(address(managerOne)); transceiverTwo = deployWormholeTranceiver(address(managerTwo)); + transceiverThree = deployWormholeTranceiver(address(managerThree)); 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(); } @@ -218,65 +233,84 @@ contract TestNonFungibleNttManager is Test { // ============================ Business Logic Tests ================================== - function test_transferLocked(uint256 nftCount, uint256 startId) public { + function test_lockAndMint(uint256 nftCount, uint256 startId) public { nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); startId = bound(startId, 0, type(uint256).max - nftCount); address recipient = makeAddr("recipient"); uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); - // Call the specified manager to transfer the batch. + // Lock the NFTs on managerOne. bytes memory encodedVm = _approveAndTransferBatch( managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; - // Check if the NFTs are locked. - for (uint256 i = 0; i < nftCount; i++) { - uint256 tokenId = tokenIds[i]; - assertEq(nftOne.ownerOf(tokenId), address(managerOne), "NFT should be locked"); + _verifyTransferPayload(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(chainIdOne, encodedVm))); + assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); + assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); + } + + function test_burnAndUnlock(uint256 nftCount, uint256 startId) public { + nftCount = bound(nftCount, 1, managerTwo.getMaxBatchSize()); + startId = bound(startId, 0, type(uint256).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(); } - // Verify the manager message - bytes memory vmPayload = guardian.wormhole().parseVM(encodedVm).payload; - (, TransceiverStructs.ManagerMessage memory message) = TransceiverStructs - .parseTransceiverAndManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, vmPayload); + address recipient = makeAddr("recipient"); + uint256[] memory tokenIds = _mintNftBatch(nftTwo, recipient, nftCount, startId); - assertEq(uint256(message.id), managerOne.nextMessageSequence() - 1); - assertEq(message.sender, toWormholeFormat(recipient)); + // Burn the NFTs on managerTwo. + bytes memory encodedVm = _approveAndTransferBatch( + managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true + )[0]; - // Verify the non-fungible transfer message. - TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = - TransceiverStructs.parseNonFungibleNativeTokenTransfer(message.payload); + _verifyTransferPayload(encodedVm, managerTwo, recipient, chainIdOne, tokenIds); - assertEq(nftTransfer.to, toWormholeFormat(recipient)); - assertEq(nftTransfer.toChain, chainIdTwo); - assertEq(nftTransfer.payload, new bytes(0)); - assertEq(nftTransfer.tokenIds.length, nftCount); + // Receive the message and unlock the NFTs on managerOne. + transceiverOne.receiveMessage(encodedVm); - for (uint256 i = 0; i < nftCount; i++) { - assertEq(nftTransfer.tokenIds[i], tokenIds[i]); - } + // Verify state changes. + assertTrue(managerOne.isMessageExecuted(_computeMessageDigest(chainIdTwo, encodedVm))); + assertTrue(_isBatchBurned(nftTwo, tokenIds), "NFTs should be burned"); + assertTrue(_isBatchOwner(nftOne, tokenIds, recipient), "Recipient should own NFTs"); } - function test_receiveMessageAndMint(uint256 nftCount, uint256 startId) public { - nftCount = bound(nftCount, 1, managerTwo.getMaxBatchSize()); + function test_burnAndMint(uint256 nftCount, uint256 startId) public { + nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); startId = bound(startId, 0, type(uint256).max - nftCount); address recipient = makeAddr("recipient"); uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); - // Lock the NFTs on managerOne. + // Burn the NFTs on managerThree. bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; - // Receive the message and mint the NFTs. + _verifyTransferPayload(encodedVm, managerThree, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. transceiverTwo.receiveMessage(encodedVm); - // Verify state changes. - assertTrue(managerTwo.isMessageExecuted(_computeMessageDigest(chainIdOne, 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(chainIdThree, encodedVm))); + assertTrue(_isBatchBurned(nftOne, tokenIds), "NFTs should be burned"); assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); - assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); } // ==================================== Helpers ======================================= @@ -296,6 +330,49 @@ contract TestNonFungibleNttManager is Test { 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( + 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( uint16 sourceChain, bytes memory encodedVm @@ -319,10 +396,10 @@ contract TestNonFungibleNttManager is Test { ) internal returns (bytes[] memory encodedVms) { // Transfer NFTs as the owner of the NFTs. vm.startPrank(recipient); - nft.setApprovalForAll(address(managerOne), true); + nft.setApprovalForAll(address(manager), true); vm.recordLogs(); - managerOne.transfer( + manager.transfer( tokenIds, targetChain, toWormholeFormat(recipient), @@ -331,7 +408,7 @@ contract TestNonFungibleNttManager is Test { vm.stopPrank(); // Fetch the wormhole message. - encodedVms = _getWormholeMessage(vm.getRecordedLogs(), managerOne.chainId()); + encodedVms = _getWormholeMessage(vm.getRecordedLogs(), manager.chainId()); } function _mintNftBatch( From ccf2c05829e41354534ff8e235f76a62f96f75b5 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 14 Mar 2024 10:42:42 -0500 Subject: [PATCH 10/19] evm: add negative transfer tests --- evm/test/NonFungibleNttManager.t.sol | 119 +++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index 98cdb332b..6599ef2d5 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -112,12 +112,8 @@ contract TestNonFungibleNttManager is Test { deployNonFungibleManager(address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, true); managerTwo = deployNonFungibleManager(address(nftTwo), IManagerBase.Mode.BURNING, chainIdTwo, true); - managerThree = deployNonFungibleManager( - address(nftOne), - IManagerBase.Mode.BURNING, - chainIdThree, - true - ); + managerThree = + deployNonFungibleManager(address(nftOne), IManagerBase.Mode.BURNING, chainIdThree, true); // Wormhole Transceivers. transceiverOne = deployWormholeTranceiver(address(managerOne)); @@ -231,7 +227,7 @@ contract TestNonFungibleNttManager is Test { managerOne.setPeer(chainId, newPeer); } - // ============================ Business Logic Tests ================================== + // ============================ Transfer Tests ====================================== function test_lockAndMint(uint256 nftCount, uint256 startId) public { nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); @@ -266,7 +262,9 @@ contract TestNonFungibleNttManager is Test { vm.startPrank(address(managerOne)); uint256[] memory tokenIds = _mintNftBatch(nftOne, address(managerOne), nftCount, startId); - assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); + assertTrue( + _isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs" + ); vm.stopPrank(); } @@ -313,6 +311,111 @@ contract TestNonFungibleNttManager is Test { 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( + INonFungibleNttManager.InvalidPeer.selector, targetChain, bytes32(0) + ) + ); + managerOne.transfer( + tokenIds, + targetChain, + 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_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) + ); + } + // ==================================== Helpers ======================================= function _isBatchOwner( From b72dcba9115f70431f364c5eb1f6c1af98bbfe7a Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 14 Mar 2024 11:01:12 -0500 Subject: [PATCH 11/19] evm: fix rebase errors --- evm/script/DeployWormholeNtt.s.sol | 6 ++++-- evm/script/UpgradeNttManager.s.sol | 2 +- evm/script/UpgradeWormholeTransceiver.s.sol | 4 +++- evm/src/NativeTransfers/NttManager.sol | 4 ++-- evm/src/interfaces/IManagerBase.sol | 8 -------- evm/src/libraries/TransceiverStructs.sol | 14 +++++++------- evm/test/NttManager.t.sol | 6 +++--- 7 files changed, 20 insertions(+), 24 deletions(-) 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/NttManager.sol b/evm/src/NativeTransfers/NttManager.sol index bac2924ec..9f772c89c 100644 --- a/evm/src/NativeTransfers/NttManager.sol +++ b/evm/src/NativeTransfers/NttManager.sol @@ -168,7 +168,7 @@ 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); @@ -184,7 +184,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); diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 8c3211dcb..9c6826a19 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -61,14 +61,6 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverRemoved(address transceiver, uint8 threshold); - /// @notice Emitted when a message has already been executed to - /// notify client of against retries. - /// @dev Topic0 - /// 0x4069dff8c9df7e38d2867c0910bd96fd61787695e5380281148c04932d02bef2. - /// @param sourceNttManager The address of the source nttManager. - /// @param msgHash The keccak-256 hash of the message. - event MessageAlreadyExecuted(bytes32 indexed sourceNttManager, bytes32 indexed msgHash); - /// @notice payment for a transfer is too low. /// @param requiredPayment The required payment. /// @param providedPayment The provided payment. diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index 0683bccb3..fe5c9d0f9 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -66,18 +66,18 @@ library TransceiverStructs { /// @notice Parse a ManagerMessage. /// @param encoded The byte array corresponding to the encoded message - /// @return nttManagerMessage The parsed ManagerMessage struct. + /// @return managerMessage The parsed ManagerMessage struct. function parseManagerMessage(bytes memory encoded) public pure - returns (ManagerMessage 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); } @@ -270,13 +270,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); diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index 34ab103b4..18d9a6d0a 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -9,7 +9,7 @@ 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"; @@ -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; @@ -634,7 +634,7 @@ contract TestNttManager is Test, IRateLimiterEvents { vm.expectRevert( abi.encodeWithSelector( IManagerBase.TransceiverAlreadyAttestedToMessage.selector, - TransceiverStructs.nttManagerMessageDigest( + TransceiverStructs.managerMessageDigest( TransceiverHelpersLib.SENDING_CHAIN_ID, m ) ) From 9da2723d876ceaf8dfe0643d23a21f7c0757b93d Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 14 Mar 2024 11:12:26 -0500 Subject: [PATCH 12/19] evm: add whenNotPaused check --- .../NativeTransfers/NonFungibleNttManager.sol | 12 +++------- evm/test/NonFungibleNttManager.t.sol | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index 16210d4c1..1eb3f644c 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -101,7 +101,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { uint16 sourceChainId, bytes32 sourceNttManagerAddress, TransceiverStructs.ManagerMessage memory payload - ) external onlyTransceiver { + ) external onlyTransceiver whenNotPaused { _verifyPeer(sourceChainId, sourceNttManagerAddress); // Compute manager message digest and record transceiver attestation. @@ -116,7 +116,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { uint16 sourceChainId, bytes32 sourceNttManagerAddress, TransceiverStructs.ManagerMessage memory message - ) public { + ) public whenNotPaused { // verify chain has not forked checkFork(evmChainId); @@ -215,16 +215,10 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { ) ); - // Cache and verify peer. - bytes32 destinationPeer = _getPeersStorage()[recipientChain].peerAddress; - if (destinationPeer == bytes32(0)) { - revert InvalidPeer(recipientChain, destinationPeer); - } - // send the message _sendMessageToTransceivers( recipientChain, - destinationPeer, + _getPeersStorage()[recipientChain].peerAddress, priceQuotes, instructions, enabledTransceivers, diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index 6599ef2d5..12a884b10 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -362,7 +362,7 @@ contract TestNonFungibleNttManager is Test { nftOne.setApprovalForAll(address(managerOne), true); vm.expectRevert( abi.encodeWithSelector( - INonFungibleNttManager.InvalidPeer.selector, targetChain, bytes32(0) + IManagerBase.PeerNotRegistered.selector, targetChain ) ); managerOne.transfer( @@ -416,6 +416,26 @@ contract TestNonFungibleNttManager is Test { ); } + 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) + ); + } + // ==================================== Helpers ======================================= function _isBatchOwner( From 6f33918cf7cc0240f23f23d6beeee5cdbbd52340 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 14 Mar 2024 16:32:14 -0500 Subject: [PATCH 13/19] evm: remove unused function --- evm/src/NativeTransfers/NttManager.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/evm/src/NativeTransfers/NttManager.sol b/evm/src/NativeTransfers/NttManager.sol index 9f772c89c..ad0e22e3e 100644 --- a/evm/src/NativeTransfers/NttManager.sol +++ b/evm/src/NativeTransfers/NttManager.sol @@ -475,11 +475,6 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { } } - function _initializeTokenDecimals() internal view returns (uint8) { - (, bytes memory queriedDecimals) = token.staticcall(abi.encodeWithSignature("decimals()")); - return abi.decode(queriedDecimals, (uint8)); - } - function _trimTransferAmount( uint256 amount, uint16 toChain From e137dbcebf477a4ecb019aa85c945ea3861f3620 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 14 Mar 2024 19:18:50 -0500 Subject: [PATCH 14/19] evm: add negative tests for receiving a message --- evm/src/interfaces/INonFungibleNttManager.sol | 6 + evm/test/NonFungibleNttManager.t.sol | 119 +++++++++++++----- evm/test/NttManager.t.sol | 4 +- 3 files changed, 94 insertions(+), 35 deletions(-) diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol index 0dd475c01..7e667b562 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -84,4 +84,10 @@ interface INonFungibleNttManager is IManagerBase { bytes32 recipient, bytes memory transceiverInstructions ) external payable returns (uint64); + + function executeMsg( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory message + ) external; } diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index 12a884b10..acf600d1d 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -319,12 +319,7 @@ contract TestNonFungibleNttManager is Test { vm.startPrank(recipient); vm.expectRevert(abi.encodeWithSelector(INonFungibleNttManager.ZeroTokenIds.selector)); - managerOne.transfer( - tokenIds, - chainIdTwo, - toWormholeFormat(recipient), - new bytes(1) - ); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); } function test_cannotTransferExceedsMaxBatchSize() public { @@ -342,12 +337,7 @@ contract TestNonFungibleNttManager is Test { managerOne.getMaxBatchSize() ) ); - managerOne.transfer( - tokenIds, - chainIdTwo, - toWormholeFormat(recipient), - new bytes(1) - ); + managerOne.transfer(tokenIds, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); } function test_cannotTransferToInvalidChain() public { @@ -361,16 +351,9 @@ contract TestNonFungibleNttManager is Test { 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) + abi.encodeWithSelector(IManagerBase.PeerNotRegistered.selector, targetChain) ); + managerOne.transfer(tokenIds, targetChain, toWormholeFormat(recipient), new bytes(1)); } function test_cannotTransferInvalidRecipient() public { @@ -408,12 +391,7 @@ contract TestNonFungibleNttManager is Test { vm.startPrank(recipient); nftOne.setApprovalForAll(address(managerOne), true); vm.expectRevert("ERC721: transfer from incorrect owner"); - managerOne.transfer( - tokenIds2, - chainIdTwo, - toWormholeFormat(recipient), - new bytes(1) - ); + managerOne.transfer(tokenIds2, chainIdTwo, toWormholeFormat(recipient), new bytes(1)); } function test_cannotTransferWhenPaused() public { @@ -428,12 +406,89 @@ contract TestNonFungibleNttManager is Test { vm.startPrank(recipient); vm.expectRevert(PausableUpgradeable.RequireContractIsNotPaused.selector); - managerOne.transfer( - tokenIds, - chainIdTwo, - toWormholeFormat(recipient), - new bytes(1) + 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( + 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( + 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(chainIdOne, encodedVm) + ) + ); + managerTwo.executeMsg(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( + managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true + )[0]; + + vm.expectRevert( + abi.encodeWithSelector( + INonFungibleNttManager.InvalidTargetChain.selector, + chainIdThree, + chainIdTwo + ) ); + transceiverTwo.receiveMessage(encodedVm); } // ==================================== Helpers ======================================= diff --git a/evm/test/NttManager.t.sol b/evm/test/NttManager.t.sol index 18d9a6d0a..01f59a4d0 100644 --- a/evm/test/NttManager.t.sol +++ b/evm/test/NttManager.t.sol @@ -634,9 +634,7 @@ contract TestNttManager is Test, IRateLimiterEvents { vm.expectRevert( abi.encodeWithSelector( IManagerBase.TransceiverAlreadyAttestedToMessage.selector, - TransceiverStructs.managerMessageDigest( - TransceiverHelpersLib.SENDING_CHAIN_ID, m - ) + TransceiverStructs.managerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m) ) ); e2.receiveMessage(encodedEm); From da1f65465866e2eba9e53666a461b5ee71e21f9c Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 15 Mar 2024 16:59:23 -0500 Subject: [PATCH 15/19] evm: add variable width id encoding --- .../NativeTransfers/NonFungibleNttManager.sol | 27 ++++- evm/src/interfaces/INonFungibleNttManager.sol | 2 + evm/src/libraries/TransceiverStructs.sol | 106 +++++++++++++++--- evm/test/NonFungibleNttManager.t.sol | 87 +++++++++++--- 4 files changed, 194 insertions(+), 28 deletions(-) diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index 1eb3f644c..152bd3fed 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -21,11 +21,25 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { // =============== 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 immutable tokenIdWidth; + // =============== Setup ================================================================= - constructor(address _token, Mode _mode, uint16 _chainId) ManagerBase(_token, _mode, _chainId) {} + 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 @@ -211,7 +225,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { TransceiverStructs.ManagerMessage( bytes32(uint256(sequence)), toWormholeFormat(msg.sender), - TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nft) + TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nft, tokenIdWidth) ) ); @@ -272,4 +286,13 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { 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/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol index 7e667b562..0bf9c688c 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -67,6 +67,8 @@ interface INonFungibleNttManager is IManagerBase { /// @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. diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index fe5c9d0f9..c8b9a46a6 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -19,6 +19,7 @@ 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' @@ -147,8 +148,18 @@ library TransceiverStructs { encoded.checkLength(offset); } - /// @dev Native Token Transfer payload. - /// TODO: Document wire format. + /// @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; @@ -160,19 +171,18 @@ library TransceiverStructs { bytes payload; } - function encodeNonFungibleNativeTokenTransfer(NonFungibleNativeTokenTransfer memory nft) - public - pure - returns (bytes memory encoded) - { + 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) { - // For now encode each token ID as 32 bytes long. - // TODO: Optimize this to encode only the necessary bytes. - encodedTokenIds = abi.encodePacked(encodedTokenIds, uint8(32), nft.tokenIds[i]); + encodedTokenIds = abi.encodePacked( + encodedTokenIds, tokenIdWidth, encodeTokenId(nft.tokenIds[i], tokenIdWidth) + ); } return abi.encodePacked( @@ -200,9 +210,9 @@ library TransceiverStructs { uint256[] memory tokenIds = new uint256[](batchSize); for (uint256 i = 0; i < batchSize; ++i) { - uint8 tokenIdLength; - (tokenIdLength, offset) = encoded.asUint8Unchecked(offset); - (tokenIds[i], offset) = encoded.asUint256Unchecked(offset); + uint256 tokenId; + (offset, tokenId) = parseTokenId(encoded, offset); + tokenIds[i] = tokenId; } nonFungibleNtt.tokenIds = tokenIds; @@ -214,6 +224,76 @@ library TransceiverStructs { 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: diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index acf600d1d..cdbae4f7a 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -24,11 +24,14 @@ import "./libraries/NttManagerHelpers.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 Test { + using BytesParsing for bytes; + uint16 constant chainIdOne = 2; uint16 constant chainIdTwo = 6; uint16 constant chainIdThree = 10; @@ -40,6 +43,7 @@ contract TestNonFungibleNttManager is Test { address relayer = 0x7B1bD7a6b4E61c2a123AC6BC2cbfC614437D0470; uint8 consistencyLevel = 1; uint256 baseGasLimit = 500000; + uint8 tokenIdWidth = 2; address owner = makeAddr("owner"); @@ -60,7 +64,7 @@ contract TestNonFungibleNttManager is Test { bool shouldInitialize ) internal returns (INonFungibleNttManager) { NonFungibleNttManager implementation = - new NonFungibleNttManager(address(nft), _mode, _chainId); + new NonFungibleNttManager(address(nft), tokenIdWidth, _mode, _chainId); NonFungibleNttManager proxy = NonFungibleNttManager(address(new ERC1967Proxy(address(implementation), ""))); @@ -227,12 +231,60 @@ contract TestNonFungibleNttManager is Test { managerOne.setPeer(chainId, newPeer); } - // ============================ Transfer Tests ====================================== - - function test_lockAndMint(uint256 nftCount, uint256 startId) public { + // ============================ 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, type(uint256).max - nftCount); + TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = TransceiverStructs + .NonFungibleNativeTokenTransfer({ + to: to, + toChain: toChain, + payload: payload, + tokenIds: _createBatchTokenIds(nftCount, startId) + }); + + bytes memory encoded = + TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nftTransfer, 32); + + 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"); + } + } + + // ============================ 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); @@ -253,9 +305,9 @@ contract TestNonFungibleNttManager is Test { assertTrue(_isBatchOwner(nftOne, tokenIds, address(managerOne)), "Manager should own NFTs"); } - function test_burnAndUnlock(uint256 nftCount, uint256 startId) public { - nftCount = bound(nftCount, 1, managerTwo.getMaxBatchSize()); - startId = bound(startId, 0, type(uint256).max - nftCount); + 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. { @@ -287,9 +339,9 @@ contract TestNonFungibleNttManager is Test { assertTrue(_isBatchOwner(nftOne, tokenIds, recipient), "Recipient should own NFTs"); } - function test_burnAndMint(uint256 nftCount, uint256 startId) public { - nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); - startId = bound(startId, 0, type(uint256).max - nftCount); + 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); @@ -483,9 +535,7 @@ contract TestNonFungibleNttManager is Test { vm.expectRevert( abi.encodeWithSelector( - INonFungibleNttManager.InvalidTargetChain.selector, - chainIdThree, - chainIdTwo + INonFungibleNttManager.InvalidTargetChain.selector, chainIdThree, chainIdTwo ) ); transceiverTwo.receiveMessage(encodedVm); @@ -605,6 +655,17 @@ contract TestNonFungibleNttManager is Test { 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( Vm.Log[] memory logs, uint16 emitterChain From 0d6a720c12c8edcd1400475084c5c002b0a9f1f7 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Sun, 17 Mar 2024 12:01:32 -0500 Subject: [PATCH 16/19] evm: add immutables check --- .../NativeTransfers/NonFungibleNttManager.sol | 10 ++++- evm/test/NonFungibleNttManager.t.sol | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index 152bd3fed..e9304d864 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -27,7 +27,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { // The number of bytes each NFT token ID occupies in the payload. All tokenIDs must fit within // this width. - uint8 immutable tokenIdWidth; + uint8 public immutable tokenIdWidth; // =============== Setup ================================================================= @@ -56,6 +56,14 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { _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); diff --git a/evm/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index cdbae4f7a..7d6f5ff50 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -144,6 +144,43 @@ contract TestNonFungibleNttManager is Test { // ================================== 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 + ) + ); + NonFungibleNttManager implementation = 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); From d25c1f6199ca802e3aa087541d678044bb9c8aea Mon Sep 17 00:00:00 2001 From: gator-boi Date: Sun, 17 Mar 2024 14:46:48 -0500 Subject: [PATCH 17/19] evm: add multi-transceiver tests and other negative tests --- evm/src/interfaces/INonFungibleNttManager.sol | 6 + evm/src/libraries/TransceiverStructs.sol | 2 +- evm/src/mocks/DummyNft.sol | 40 ++- evm/test/NonFungibleNttManager.t.sol | 340 +++++++++++++++++- 4 files changed, 380 insertions(+), 8 deletions(-) diff --git a/evm/src/interfaces/INonFungibleNttManager.sol b/evm/src/interfaces/INonFungibleNttManager.sol index 0bf9c688c..72a381ee3 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -87,6 +87,12 @@ interface INonFungibleNttManager is IManagerBase { bytes memory transceiverInstructions ) external payable returns (uint64); + function attestationReceived( + uint16 sourceChainId, + bytes32 sourceNttManagerAddress, + TransceiverStructs.ManagerMessage memory payload + ) external; + function executeMsg( uint16 sourceChainId, bytes32 sourceNttManagerAddress, diff --git a/evm/src/libraries/TransceiverStructs.sol b/evm/src/libraries/TransceiverStructs.sol index c8b9a46a6..f7dc30d3b 100644 --- a/evm/src/libraries/TransceiverStructs.sol +++ b/evm/src/libraries/TransceiverStructs.sol @@ -29,7 +29,7 @@ library TransceiverStructs { /// This is 0x99'N''F''T' bytes4 constant NON_FUNGIBLE_NTT_PREFIX = 0x994E4654; - /// @dev Message emitted and received by the nttManager contract. + /// @dev Message emitted and received by any Manager contract variant. /// The wire format is as follows: /// - id - 32 bytes /// - sender - 32 bytes diff --git a/evm/src/mocks/DummyNft.sol b/evm/src/mocks/DummyNft.sol index f0653b09a..6efd5bae8 100644 --- a/evm/src/mocks/DummyNft.sol +++ b/evm/src/mocks/DummyNft.sol @@ -3,6 +3,7 @@ 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. @@ -12,7 +13,7 @@ contract DummyNft is ERC721 { error BaseUriEmpty(); error BaseUriTooLong(); - constructor(bytes memory baseUri) ERC721("DummyNft", "DNTF") { + constructor(bytes memory baseUri) ERC721("DummyNft", "DNFT") { if (baseUri.length == 0) { revert BaseUriEmpty(); } @@ -52,17 +53,46 @@ contract DummyNft is ERC721 { } contract DummyNftMintAndBurn is DummyNft { - constructor(bytes memory baseUri) DummyNft(baseUri) {} + 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 { - // TODO - add access control here? _safeMint(to, tokenId); + + _callback(); } function burn(uint256 tokenId) public { - // TODO - add access control here? _burn(tokenId); + + _callback(); } - // TODO: Mint/Burn batches. + 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/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index 7d6f5ff50..5e79fa78d 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -142,6 +142,36 @@ contract TestNonFungibleNttManager is Test { 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 = deployWormholeTranceiver(address(sourceManager)); + WormholeTransceiver targetTransceiver = deployWormholeTranceiver(address(targetManager)); + + // 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 { @@ -288,7 +318,7 @@ contract TestNonFungibleNttManager is Test { vm.assume(to != bytes32(0)); vm.assume(toChain != 0); nftCount = bound(nftCount, 1, managerOne.getMaxBatchSize()); - startId = bound(startId, 0, type(uint256).max - nftCount); + startId = bound(startId, 0, _getMaxFromTokenIdWidth(tokenIdWidth) - nftCount); TransceiverStructs.NonFungibleNativeTokenTransfer memory nftTransfer = TransceiverStructs .NonFungibleNativeTokenTransfer({ @@ -299,7 +329,7 @@ contract TestNonFungibleNttManager is Test { }); bytes memory encoded = - TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nftTransfer, 32); + TransceiverStructs.encodeNonFungibleNativeTokenTransfer(nftTransfer, tokenIdWidth); TransceiverStructs.NonFungibleNativeTokenTransfer memory out = TransceiverStructs.parseNonFungibleNativeTokenTransfer(encoded); @@ -316,6 +346,23 @@ contract TestNonFungibleNttManager is Test { } } + /// @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 { @@ -342,6 +389,37 @@ contract TestNonFungibleNttManager is Test { 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( + managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(vms[0], managerOne, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(vms[1], managerOne, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(vms[0]); + assertFalse(managerTwo.isMessageExecuted(_computeMessageDigest(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(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)); @@ -376,6 +454,47 @@ contract TestNonFungibleNttManager is Test { 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( + managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(vms[0], managerTwo, recipient, chainIdOne, tokenIds); + _verifyTransferPayload(vms[1], managerTwo, recipient, chainIdOne, tokenIds); + + // Receive the message and unlock the NFTs on managerOne. + transceiverOne.receiveMessage(vms[0]); + assertFalse(managerOne.isMessageExecuted(_computeMessageDigest(chainIdTwo, vms[0]))); + multiTransceiverOne.receiveMessage(vms[1]); + + // Verify state changes. + assertTrue(managerOne.isMessageExecuted(_computeMessageDigest(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)); @@ -400,6 +519,37 @@ contract TestNonFungibleNttManager is Test { 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( + managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(vms[0], managerThree, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(vms[1], managerThree, recipient, chainIdTwo, tokenIds); + + // Receive the message and mint the NFTs on managerTwo. + transceiverTwo.receiveMessage(vms[0]); + assertFalse(managerTwo.isMessageExecuted(_computeMessageDigest(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(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 { @@ -445,6 +595,23 @@ contract TestNonFungibleNttManager is Test { 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; @@ -498,6 +665,24 @@ contract TestNonFungibleNttManager is Test { 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; @@ -553,6 +738,65 @@ contract TestNonFungibleNttManager is Test { managerTwo.executeMsg(chainIdOne, toWormholeFormat(address(managerOne)), message); } + function test_cannotExecuteMessageNotApproveMultiTransceiver() 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( + managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(vms[0], managerOne, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(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(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( + 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; @@ -578,6 +822,78 @@ contract TestNonFungibleNttManager is Test { 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( + 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( + managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + ); + assertEq(vms.length, 2); + + _verifyTransferPayload(vms[0], managerOne, recipient, chainIdTwo, tokenIds); + _verifyTransferPayload(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]); + } + // ==================================== Helpers ======================================= function _isBatchOwner( @@ -731,4 +1047,24 @@ contract TestNonFungibleNttManager is Test { 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; + } + } } + +// TODO: +// 1) Relayer test +// 2) Add max payload size and associated tests From 3a3ed59bfd9f439cae6c78cac038cf0b476737f5 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Mon, 18 Mar 2024 13:17:00 -0500 Subject: [PATCH 18/19] evm: add max payload size check --- .../WormholeTransceiver.sol | 5 ++ .../WormholeTransceiverState.sol | 8 ++- evm/src/interfaces/IWormholeTransceiver.sol | 6 ++ .../interfaces/IWormholeTransceiverState.sol | 3 + evm/test/NonFungibleNttManager.t.sol | 58 +++++++++++++++---- 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol index ee900001e..37b7180f8 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol @@ -192,6 +192,11 @@ contract WormholeTransceiver is 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); diff --git a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol index d0ec1a5e2..ccc1f70c1 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol @@ -22,6 +22,12 @@ 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; @@ -34,7 +40,7 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce /// @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; 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 48ff10f0d..bc4088077 100644 --- a/evm/src/interfaces/IWormholeTransceiverState.sol +++ b/evm/src/interfaces/IWormholeTransceiverState.sol @@ -127,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/test/NonFungibleNttManager.t.sol b/evm/test/NonFungibleNttManager.t.sol index 5e79fa78d..5e308a5ca 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -61,10 +61,11 @@ contract TestNonFungibleNttManager is Test { address nft, IManagerBase.Mode _mode, uint16 _chainId, - bool shouldInitialize + bool shouldInitialize, + uint8 _tokenIdWidth ) internal returns (INonFungibleNttManager) { NonFungibleNttManager implementation = - new NonFungibleNttManager(address(nft), tokenIdWidth, _mode, _chainId); + new NonFungibleNttManager(address(nft), _tokenIdWidth, _mode, _chainId); NonFungibleNttManager proxy = NonFungibleNttManager(address(new ERC1967Proxy(address(implementation), ""))); @@ -112,12 +113,15 @@ contract TestNonFungibleNttManager is Test { nftTwo = new DummyNftMintAndBurn(bytes("https://metadata.dn420.com/y/")); // Managers. - managerOne = - deployNonFungibleManager(address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, true); - managerTwo = - deployNonFungibleManager(address(nftTwo), IManagerBase.Mode.BURNING, chainIdTwo, true); - managerThree = - deployNonFungibleManager(address(nftOne), IManagerBase.Mode.BURNING, chainIdThree, true); + 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 = deployWormholeTranceiver(address(managerOne)); @@ -214,8 +218,9 @@ contract TestNonFungibleNttManager is Test { function test_cannotInitalizeNotDeployer() public { // Don't initialize. vm.prank(owner); - INonFungibleNttManager dummyManager = - deployNonFungibleManager(address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, false); + INonFungibleNttManager dummyManager = deployNonFungibleManager( + address(nftOne), IManagerBase.Mode.LOCKING, chainIdOne, false, tokenIdWidth + ); vm.prank(makeAddr("notOwner")); vm.expectRevert( @@ -630,6 +635,38 @@ contract TestNonFungibleNttManager is Test { ); } + function test_cannotTransferPayloadSizeExceeded() public { + // Deploy manager with 32 byte tokenIdWidth. + INonFungibleNttManager manager = deployNonFungibleManager( + address(nftOne), IManagerBase.Mode.BURNING, chainIdThree, true, 32 + ); + WormholeTransceiver transceiver = deployWormholeTranceiver(address(manager)); + 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; @@ -1067,4 +1104,3 @@ contract TestNonFungibleNttManager is Test { // TODO: // 1) Relayer test -// 2) Add max payload size and associated tests From 62e866884296128f815f7fc9ff91df0c2e695f6c Mon Sep 17 00:00:00 2001 From: gator-boi Date: Thu, 21 Mar 2024 09:46:05 -0500 Subject: [PATCH 19/19] evm: separate manager execution cost --- .../NativeTransfers/NonFungibleNttManager.sol | 17 +- evm/src/NativeTransfers/NttManager.sol | 17 +- .../NativeTransfers/shared/ManagerBase.sol | 23 +- evm/src/Transceiver/Transceiver.sol | 13 +- .../WormholeTransceiver.sol | 14 +- .../WormholeTransceiverState.sol | 6 +- evm/src/interfaces/IManagerBase.sol | 9 - evm/src/interfaces/INonFungibleNttManager.sol | 9 + evm/src/interfaces/INttManager.sol | 9 + evm/src/interfaces/ITransceiver.sol | 12 +- evm/test/IntegrationRelayer.t.sol | 10 +- evm/test/IntegrationStandalone.t.sol | 1 - .../NonFungibleNttIntegrationRelayer.t.sol | 132 +++++++ evm/test/NonFungibleNttManager.t.sol | 347 ++++-------------- evm/test/RateLimit.t.sol | 8 +- .../NonFungibleNttManagerHelpers.sol | 241 ++++++++++++ evm/test/mocks/DummyTransceiver.sol | 4 +- 17 files changed, 558 insertions(+), 314 deletions(-) create mode 100755 evm/test/NonFungibleNttIntegrationRelayer.t.sol create mode 100644 evm/test/libraries/NonFungibleNttManagerHelpers.sol diff --git a/evm/src/NativeTransfers/NonFungibleNttManager.sol b/evm/src/NativeTransfers/NonFungibleNttManager.sol index e9304d864..eee9a56e2 100644 --- a/evm/src/NativeTransfers/NonFungibleNttManager.sol +++ b/evm/src/NativeTransfers/NonFungibleNttManager.sol @@ -110,6 +110,19 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { // =============== 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, @@ -213,12 +226,13 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { } // 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); + ) = _prepareForTransfer(recipientChain, transceiverInstructions, 0); uint64 sequence = _useMessageSequence(); @@ -242,6 +256,7 @@ contract NonFungibleNttManager is INonFungibleNttManager, ManagerBase { recipientChain, _getPeersStorage()[recipientChain].peerAddress, priceQuotes, + 0, instructions, enabledTransceivers, encodedNttManagerPayload diff --git a/evm/src/NativeTransfers/NttManager.sol b/evm/src/NativeTransfers/NttManager.sol index ad0e22e3e..f783d21e8 100644 --- a/evm/src/NativeTransfers/NttManager.sol +++ b/evm/src/NativeTransfers/NttManager.sol @@ -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, @@ -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; @@ -422,6 +436,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { recipientChain, _getPeersStorage()[recipientChain].peerAddress, priceQuotes, + 0, instructions, enabledTransceivers, encodedNttManagerPayload diff --git a/evm/src/NativeTransfers/shared/ManagerBase.sol b/evm/src/NativeTransfers/shared/ManagerBase.sol index 5ba9f62da..070e05753 100644 --- a/evm/src/NativeTransfers/shared/ManagerBase.sol +++ b/evm/src/NativeTransfers/shared/ManagerBase.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,8 +109,6 @@ abstract contract ManagerBase is return (priceQuotes, totalPriceQuote); } - // =============== Internal Logic =========================================================== - function _recordTransceiverAttestation( uint16 sourceChainId, TransceiverStructs.ManagerMessage memory payload @@ -160,6 +158,7 @@ abstract contract ManagerBase is uint16 recipientChain, bytes32 peerAddress, uint256[] memory priceQuotes, + uint256 managerExecutionCost, TransceiverStructs.TransceiverInstruction[] memory transceiverInstructions, address[] memory enabledTransceivers, bytes memory ManagerMessage @@ -179,14 +178,16 @@ abstract contract ManagerBase is recipientChain, transceiverInstructions[transceiverInfos[transceiverAddr].index], ManagerMessage, - peerAddress + peerAddress, + managerExecutionCost ); } } function _prepareForTransfer( uint16 recipientChain, - bytes memory transceiverInstructions + bytes memory transceiverInstructions, + uint256 managerExecutionCost ) internal returns ( @@ -214,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/Transceiver/Transceiver.sol b/evm/src/Transceiver/Transceiver.sol index 3a7197ffc..37b934818 100644 --- a/evm/src/Transceiver/Transceiver.sol +++ b/evm/src/Transceiver/Transceiver.sol @@ -95,9 +95,10 @@ 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 @@ -105,11 +106,13 @@ abstract contract Transceiver is uint16 recipientChain, TransceiverStructs.TransceiverInstruction memory instruction, bytes memory ManagerMessage, - bytes32 recipientNttManagerAddress + bytes32 recipientNttManagerAddress, + uint256 managerExecutionCost ) external payable nonReentrant onlyNttManager { _sendMessage( recipientChain, msg.value, + managerExecutionCost, msg.sender, recipientNttManagerAddress, instruction, @@ -122,6 +125,7 @@ abstract contract Transceiver is function _sendMessage( uint16 recipientChain, uint256 deliveryPayment, + uint256 managerExecutionCost, address caller, bytes32 recipientNttManagerAddress, TransceiverStructs.TransceiverInstruction memory transceiverInstruction, @@ -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 37b7180f8..51f70ea89 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol @@ -39,7 +39,7 @@ contract WormholeTransceiver is address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit, + uint256 _attestationGasLimit, IWormholeTransceiverState.ManagerType _managerType ) WormholeTransceiverState( @@ -48,7 +48,7 @@ contract WormholeTransceiver is wormholeRelayerAddr, specialRelayerAddr, _consistencyLevel, - _gasLimit, + _attestationGasLimit, _managerType ) {} @@ -148,7 +148,8 @@ 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 = @@ -163,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); @@ -176,6 +179,7 @@ contract WormholeTransceiver is function _sendMessage( uint16 recipientChain, uint256 deliveryPayment, + uint256 managerExecutionCost, address caller, bytes32 recipientNttManagerAddress, TransceiverStructs.TransceiverInstruction memory instruction, @@ -206,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 ccc1f70c1..6db30b173 100644 --- a/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol +++ b/evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol @@ -33,7 +33,7 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce 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 ================================================ @@ -58,7 +58,7 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce address wormholeRelayerAddr, address specialRelayerAddr, uint8 _consistencyLevel, - uint256 _gasLimit, + uint256 _attestationGasLimit, ManagerType _managerType ) Transceiver(nttManager) { wormhole = IWormhole(wormholeCoreBridge); @@ -66,7 +66,7 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce specialRelayer = ISpecialRelayer(specialRelayerAddr); wormholeTransceiver_evmChainId = block.chainid; consistencyLevel = _consistencyLevel; - gasLimit = _gasLimit; + attestationGasLimit = _attestationGasLimit; managerType = _managerType; } diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index 9c6826a19..f66390a8a 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -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 index 72a381ee3..8bd3ede59 100644 --- a/evm/src/interfaces/INonFungibleNttManager.sol +++ b/evm/src/interfaces/INonFungibleNttManager.sol @@ -80,6 +80,15 @@ interface INonFungibleNttManager is IManagerBase { 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, diff --git a/evm/src/interfaces/INttManager.sol b/evm/src/interfaces/INttManager.sol index 5a874f913..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 diff --git a/evm/src/interfaces/ITransceiver.sol b/evm/src/interfaces/ITransceiver.sol index 3345783c3..76a01e9f2 100644 --- a/evm/src/interfaces/ITransceiver.sol +++ b/evm/src/interfaces/ITransceiver.sol @@ -41,11 +41,15 @@ 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. @@ -53,11 +57,15 @@ interface ITransceiver { /// @param instruction An additional Instruction provided by the Transceiver to be /// executed 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 ManagerMessage, - bytes32 recipientNttManagerAddress + bytes32 recipientNttManagerAddress, + uint256 managerExecutionCost ) external payable; /// @notice Upgrades the transceiver to a new implementation. diff --git a/evm/test/IntegrationRelayer.t.sol b/evm/test/IntegrationRelayer.t.sol index 42b202b75..4fef58691 100755 --- a/evm/test/IntegrationRelayer.t.sol +++ b/evm/test/IntegrationRelayer.t.sol @@ -204,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); @@ -341,7 +341,7 @@ contract TestEndToEndRelayer is nttManagerChain1.transfer{ value: wormholeTransceiverChain1.quoteDeliveryPrice( - chainId2, buildTransceiverInstruction(false) + chainId2, buildTransceiverInstruction(false), 0 ) }( sendingAmount, @@ -393,7 +393,7 @@ contract TestEndToEndRelayer is supplyBefore = token2.totalSupply(); nttManagerChain2.transfer{ value: wormholeTransceiverChain2.quoteDeliveryPrice( - chainId1, buildTransceiverInstruction(false) + chainId1, buildTransceiverInstruction(false), 0 ) }( sendingAmount, @@ -574,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, @@ -705,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 80b8cedb0..c46545481 100755 --- a/evm/test/IntegrationStandalone.t.sol +++ b/evm/test/IntegrationStandalone.t.sol @@ -25,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; 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 index 5e308a5ca..0f8f63cfd 100644 --- a/evm/test/NonFungibleNttManager.t.sol +++ b/evm/test/NonFungibleNttManager.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache 2 pragma solidity >=0.8.8 <0.9.0; -import "forge-std/Test.sol"; import "forge-std/console.sol"; import "forge-std/Vm.sol"; @@ -21,6 +20,7 @@ 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"; @@ -29,18 +29,17 @@ import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; import "./mocks/MockTransceivers.sol"; import "../src/mocks/DummyNft.sol"; -contract TestNonFungibleNttManager is Test { +contract TestNonFungibleNttManager is NonFungibleNttHelpers { using BytesParsing for bytes; uint16 constant chainIdOne = 2; uint16 constant chainIdTwo = 6; uint16 constant chainIdThree = 10; - bytes4 constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; uint256 constant DEVNET_GUARDIAN_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; WormholeSimulator guardian; - uint256 initialBlockTimestamp; - address relayer = 0x7B1bD7a6b4E61c2a123AC6BC2cbfC614437D0470; + + // Constructor args. uint8 consistencyLevel = 1; uint256 baseGasLimit = 500000; uint8 tokenIdWidth = 2; @@ -57,51 +56,10 @@ contract TestNonFungibleNttManager is Test { WormholeTransceiver transceiverTwo; WormholeTransceiver transceiverThree; - function deployNonFungibleManager( - address nft, - IManagerBase.Mode _mode, - uint16 _chainId, - bool shouldInitialize, - uint8 _tokenIdWidth - ) internal 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 deployWormholeTranceiver(address manager) internal 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 setUp() public { string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); vm.createSelectFork(url); - initialBlockTimestamp = vm.getBlockTimestamp(); guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); @@ -124,9 +82,15 @@ contract TestNonFungibleNttManager is Test { ); // Wormhole Transceivers. - transceiverOne = deployWormholeTranceiver(address(managerOne)); - transceiverTwo = deployWormholeTranceiver(address(managerTwo)); - transceiverThree = deployWormholeTranceiver(address(managerThree)); + 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))); @@ -154,8 +118,12 @@ contract TestNonFungibleNttManager is Test { vm.startPrank(owner); // Deploy two new transceivers. - WormholeTransceiver sourceTransceiver = deployWormholeTranceiver(address(sourceManager)); - WormholeTransceiver targetTransceiver = deployWormholeTranceiver(address(targetManager)); + 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)); @@ -189,7 +157,7 @@ contract TestNonFungibleNttManager is Test { INonFungibleNttManager.InvalidTokenIdWidth.selector, _tokenIdWidth ) ); - NonFungibleNttManager implementation = new NonFungibleNttManager( + new NonFungibleNttManager( address(nftOne), _tokenIdWidth, IManagerBase.Mode.BURNING, chainIdOne ); } @@ -379,17 +347,19 @@ contract TestNonFungibleNttManager is Test { // Lock the NFTs on managerOne. bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; - _verifyTransferPayload(encodedVm, managerOne, recipient, chainIdTwo, tokenIds); + _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(chainIdOne, encodedVm))); + 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"); } @@ -406,21 +376,25 @@ contract TestNonFungibleNttManager is Test { // Lock the NFTs on managerOne. bytes[] memory vms = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true ); assertEq(vms.length, 2); - _verifyTransferPayload(vms[0], managerOne, recipient, chainIdTwo, tokenIds); - _verifyTransferPayload(vms[1], managerOne, recipient, chainIdTwo, tokenIds); + _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(chainIdOne, 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(chainIdOne, vms[0]))); + 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"); } @@ -445,16 +419,18 @@ contract TestNonFungibleNttManager is Test { // Burn the NFTs on managerTwo. bytes memory encodedVm = _approveAndTransferBatch( - managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true + guardian, managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true )[0]; - _verifyTransferPayload(encodedVm, managerTwo, recipient, chainIdOne, tokenIds); + _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(chainIdTwo, encodedVm))); + assertTrue( + managerOne.isMessageExecuted(_computeMessageDigest(guardian, chainIdTwo, encodedVm)) + ); assertTrue(_isBatchBurned(nftTwo, tokenIds), "NFTs should be burned"); assertTrue(_isBatchOwner(nftOne, tokenIds, recipient), "Recipient should own NFTs"); } @@ -482,20 +458,24 @@ contract TestNonFungibleNttManager is Test { // Burn the NFTs on managerTwo. bytes[] memory vms = _approveAndTransferBatch( - managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true + guardian, managerTwo, transceiverTwo, nftTwo, tokenIds, recipient, chainIdOne, true ); assertEq(vms.length, 2); - _verifyTransferPayload(vms[0], managerTwo, recipient, chainIdOne, tokenIds); - _verifyTransferPayload(vms[1], managerTwo, recipient, chainIdOne, tokenIds); + _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(chainIdTwo, vms[0]))); + assertFalse( + managerOne.isMessageExecuted(_computeMessageDigest(guardian, chainIdTwo, vms[0])) + ); multiTransceiverOne.receiveMessage(vms[1]); // Verify state changes. - assertTrue(managerOne.isMessageExecuted(_computeMessageDigest(chainIdTwo, vms[0]))); + 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"); } @@ -509,17 +489,19 @@ contract TestNonFungibleNttManager is Test { // Burn the NFTs on managerThree. bytes memory encodedVm = _approveAndTransferBatch( - managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; - _verifyTransferPayload(encodedVm, managerThree, recipient, chainIdTwo, tokenIds); + _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(chainIdThree, encodedVm))); + assertTrue( + managerTwo.isMessageExecuted(_computeMessageDigest(guardian, chainIdThree, encodedVm)) + ); assertTrue(_isBatchBurned(nftOne, tokenIds), "NFTs should be burned"); assertTrue(_isBatchOwner(nftTwo, tokenIds, recipient), "Recipient should own NFTs"); } @@ -536,21 +518,25 @@ contract TestNonFungibleNttManager is Test { // Burn the NFTs on managerThree. bytes[] memory vms = _approveAndTransferBatch( - managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerThree, transceiverThree, nftOne, tokenIds, recipient, chainIdTwo, true ); assertEq(vms.length, 2); - _verifyTransferPayload(vms[0], managerThree, recipient, chainIdTwo, tokenIds); - _verifyTransferPayload(vms[1], managerThree, recipient, chainIdTwo, tokenIds); + _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(chainIdThree, 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(chainIdThree, vms[0]))); + 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"); } @@ -640,7 +626,9 @@ contract TestNonFungibleNttManager is Test { INonFungibleNttManager manager = deployNonFungibleManager( address(nftOne), IManagerBase.Mode.BURNING, chainIdThree, true, 32 ); - WormholeTransceiver transceiver = deployWormholeTranceiver(address(manager)); + 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"))); @@ -735,7 +723,7 @@ contract TestNonFungibleNttManager is Test { // Burn the NFTs on managerThree. bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true )[0]; // Receive the message and mint the NFTs on managerTwo. @@ -757,7 +745,7 @@ contract TestNonFungibleNttManager is Test { uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; // Parse the manager message. @@ -769,30 +757,29 @@ contract TestNonFungibleNttManager is Test { vm.expectRevert( abi.encodeWithSelector( IManagerBase.MessageNotApproved.selector, - _computeMessageDigest(chainIdOne, encodedVm) + _computeMessageDigest(guardian, chainIdOne, encodedVm) ) ); managerTwo.executeMsg(chainIdOne, toWormholeFormat(address(managerOne)), message); } - function test_cannotExecuteMessageNotApproveMultiTransceiver() public { + function test_cannotExecuteMessageNotApprovedMultiTransceiver() public { uint256 nftCount = 1; uint256 startId = 0; - WormholeTransceiver multiTransceiverTwo = - _setupMultiTransceiverManagers(managerOne, managerTwo); + _setupMultiTransceiverManagers(managerOne, managerTwo); address recipient = makeAddr("recipient"); uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); // Lock the NFTs on managerOne. bytes[] memory vms = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true ); assertEq(vms.length, 2); - _verifyTransferPayload(vms[0], managerOne, recipient, chainIdTwo, tokenIds); - _verifyTransferPayload(vms[1], managerOne, recipient, chainIdTwo, tokenIds); + _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]); @@ -805,7 +792,8 @@ contract TestNonFungibleNttManager is Test { // Receive the message and mint the NFTs on managerTwo. vm.expectRevert( abi.encodeWithSelector( - IManagerBase.MessageNotApproved.selector, _computeMessageDigest(chainIdOne, vms[1]) + IManagerBase.MessageNotApproved.selector, + _computeMessageDigest(guardian, chainIdOne, vms[1]) ) ); managerTwo.executeMsg(chainIdOne, toWormholeFormat(address(managerOne)), message); @@ -819,7 +807,7 @@ contract TestNonFungibleNttManager is Test { uint256[] memory tokenIds = _mintNftBatch(nftOne, recipient, nftCount, startId); bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; // Parse the manager message. @@ -848,7 +836,7 @@ contract TestNonFungibleNttManager is Test { // Send the batch to chainThree, but receive it on chainTwo. bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdThree, true )[0]; vm.expectRevert( @@ -868,7 +856,7 @@ contract TestNonFungibleNttManager is Test { // Lock the NFTs on managerOne. bytes memory encodedVm = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true )[0]; // Pause managerTwo. @@ -893,12 +881,12 @@ contract TestNonFungibleNttManager is Test { // Lock the NFTs on managerOne. bytes[] memory vms = _approveAndTransferBatch( - managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true + guardian, managerOne, transceiverOne, nftOne, tokenIds, recipient, chainIdTwo, true ); assertEq(vms.length, 2); - _verifyTransferPayload(vms[0], managerOne, recipient, chainIdTwo, tokenIds); - _verifyTransferPayload(vms[1], managerOne, recipient, chainIdTwo, tokenIds); + _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]); @@ -930,177 +918,4 @@ contract TestNonFungibleNttManager is Test { ); nftOne.safeTransferFrom(recipient, address(managerOne), tokenIds[0]); } - - // ==================================== Helpers ======================================= - - 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( - 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( - 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( - 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(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( - 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 view 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; - } - } } - -// TODO: -// 1) Relayer test diff --git a/evm/test/RateLimit.t.sol b/evm/test/RateLimit.t.sol index adfa5f966..b34e52897 100644 --- a/evm/test/RateLimit.t.sol +++ b/evm/test/RateLimit.t.sol @@ -986,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); @@ -1008,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); @@ -1077,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/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/mocks/DummyTransceiver.sol b/evm/test/mocks/DummyTransceiver.sol index 137b4067e..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 */