diff --git a/README.md b/README.md index 891356699..3e7d137b5 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ The payload includes the trimmed amount, together with the decimals that trimmed NTT supports rate-limiting both on the sending and destination chains. If a transfer is rate-limited on the source chain and queueing is enabled via `shouldQueue = true`, transfers are placed into an outbound queue and can be released after the expiry of the rate limit duration. Transfers that are rate-limited on the destination chain are added to an inbound queue with a similar release delay. +## No-Rate-Limiting + +Although the rate limiting feature can be disabled during contract instantiation, it still occupies code space in the `NttManager` contract. If you wish to free up code space for custom development, you can instead instantiate the +`NttManagerNoRateLimiting` contract. This contract is built without the bulk of the rate limiting code. Note that the current immutable checks do not allow modifying the rate-limiting parameters during migration. This means migrating +from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRateLimiting` or vice versa is not officially supported. + ## Cancel-Flows If users bridge frequently between a given source chain and destination chain, the capacity could be exhausted quickly. This can leave other users rate-limited, potentially delaying their transfers. To mitigate this issue, the outbound transfer cancels the inbound rate-limit on the source chain (refills the inbound rate-limit by an amount equal to that of the outbound transfer amount) and vice-versa, the inbound transfer cancels the outbound rate-limit on the destination chain (refills the outbound rate-limit by an amount equal to the inbound transfer amount). diff --git a/evm/README.md b/evm/README.md index 71355d6b8..be147b110 100644 --- a/evm/README.md +++ b/evm/README.md @@ -226,9 +226,11 @@ cp WormholeNttConfig.json.sample WormholeNttConfig.json Configure each network to your liking (including adding/removing networks). We will eventually add the addresses of the deployed contracts to this file. Navigate back to the `evm` directory. -___ +--- + ⚠️ **WARNING:** Ensure that if the `NttManager` on the source chain is configured to be in `LOCKING` mode, the corresponding `NttManager`s on the target chains are configured to be in `BURNING` mode. If not, transfers will NOT go through and user funds may be lost! Proceed with caution! -___ + +--- Currently the per-chain `inBoundLimit` is set to zero by default. This means all inbound transfers will be queued by the rate limiter. Set this value accordingly. @@ -267,3 +269,9 @@ bash sh/configure_wormhole_ntt.sh -n NETWORK_TYPE -c CHAIN_NAME -k PRIVATE_KEY Tokens powered by NTT in **burn** mode require the `burn` method to be present. This method is not present in the standard ERC20 interface, but is found in the `ERC20Burnable` interface. The `mint` and `setMinter` methods found in the [`INttToken` Interface](src/interfaces/INttToken.sol) are not found in the standard `ERC20` interface. + +#### No-Rate-Limiting + +Although the rate limiting feature can be disabled during contract instantiation, it still occupies code space in the `NttManager` contract. If you wish to free up code space for custom development, you can instead instantiate the +`NttManagerNoRateLimiting` contract. This contract is built without the bulk of the rate limiting code. Note that the current immutable checks do not allow modifying the rate-limiting parameters during migration. This means migrating +from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRateLimiting` or vice versa is not officially supported. diff --git a/evm/src/NttManager/ManagerBase.sol b/evm/src/NttManager/ManagerBase.sol index 8a336fccc..a269660b3 100644 --- a/evm/src/NttManager/ManagerBase.sol +++ b/evm/src/NttManager/ManagerBase.sol @@ -64,11 +64,11 @@ abstract contract ManagerBase is bytes32 private constant MESSAGE_SEQUENCE_SLOT = bytes32(uint256(keccak256("ntt.messageSequence")) - 1); - bytes32 private constant THRESHOLD_SLOT = bytes32(uint256(keccak256("ntt.threshold")) - 1); + bytes32 internal constant THRESHOLD_SLOT = bytes32(uint256(keccak256("ntt.threshold")) - 1); // =============== Storage Getters/Setters ============================================== - function _getThresholdStorage() private pure returns (_Threshold storage $) { + function _getThresholdStorage() internal pure returns (_Threshold storage $) { uint256 slot = uint256(THRESHOLD_SLOT); assembly ("memory-safe") { $.slot := slot @@ -122,6 +122,9 @@ abstract contract ManagerBase is uint256 totalPriceQuote = 0; for (uint256 i = 0; i < numEnabledTransceivers; i++) { address transceiverAddr = enabledTransceivers[i]; + if (!_isSendTransceiverEnabledForChain(transceiverAddr, recipientChain)) { + continue; + } uint8 registeredTransceiverIndex = transceiverInfos[transceiverAddr].index; uint256 transceiverPriceQuote = ITransceiver(transceiverAddr).quoteDeliveryPrice( recipientChain, transceiverInstructions[registeredTransceiverIndex] @@ -150,6 +153,7 @@ abstract contract ManagerBase is ) { revert TransceiverAlreadyAttestedToMessage(nttManagerMessageHash); } + _getMessageAttestationsStorage()[nttManagerMessageHash].sourceChainId = sourceChainId; _setTransceiverAttestedToMessage(nttManagerMessageHash, msg.sender); return nttManagerMessageHash; @@ -200,6 +204,9 @@ abstract contract ManagerBase is // call into transceiver contracts to send the message for (uint256 i = 0; i < numEnabledTransceivers; i++) { address transceiverAddr = enabledTransceivers[i]; + if (!_isSendTransceiverEnabledForChain(transceiverAddr, recipientChain)) { + continue; + } // send it to the recipient nttManager based on the chain ITransceiver(transceiverAddr).sendMessage{value: priceQuotes[i]}( @@ -284,11 +291,19 @@ abstract contract ManagerBase is return _getThresholdStorage().num; } + /// @inheritdoc IManagerBase + function getPerChainThreshold( + uint16 // forChainId + ) public view virtual returns (uint8) { + return _getThresholdStorage().num; + } + /// @inheritdoc IManagerBase function isMessageApproved( bytes32 digest ) public view returns (bool) { - uint8 threshold = getThreshold(); + uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; + uint8 threshold = getPerChainThreshold(sourceChainId); return messageAttestations(digest) >= threshold && threshold > 0; } @@ -396,6 +411,22 @@ abstract contract ManagerBase is _checkThresholdInvariants(); } + /// @inheritdoc IManagerBase + function enableSendTransceiverForChain( + address, // transceiver, + uint16 // forChainId + ) external virtual onlyOwner { + revert NotImplemented(); + } + + /// @inheritdoc IManagerBase + function enableRecvTransceiverForChain( + address, // transceiver, + uint16 // forChainId + ) external virtual onlyOwner { + revert NotImplemented(); + } + /// @inheritdoc IManagerBase function setThreshold( uint8 threshold @@ -413,6 +444,14 @@ abstract contract ManagerBase is emit ThresholdChanged(oldThreshold, threshold); } + /// @inheritdoc IManagerBase + function setPerChainThreshold( + uint16, // forChainId, + uint8 // threshold + ) external virtual onlyOwner { + revert NotImplemented(); + } + // =============== Internal ============================================================== function _setTransceiverAttestedToMessage(bytes32 digest, uint8 index) internal { @@ -432,8 +471,10 @@ abstract contract ManagerBase is bytes32 digest ) internal view returns (uint64) { uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap(); - return - _getMessageAttestationsStorage()[digest].attestedTransceivers & enabledTransceiverBitmap; + uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId; + uint64 enabledTransceiversForChain = _getEnabledRecvTransceiversForChain(sourceChainId); + return _getMessageAttestationsStorage()[digest].attestedTransceivers + & enabledTransceiverBitmap & enabledTransceiversForChain; } function _getEnabledTransceiverAttestedToMessage( @@ -464,6 +505,19 @@ abstract contract ManagerBase is _getMessageSequenceStorage().num++; } + function _isSendTransceiverEnabledForChain( + address, // transceiver, + uint16 // chainId + ) internal view virtual returns (bool) { + return true; + } + + function _getEnabledRecvTransceiversForChain( + uint16 // forChainId + ) internal view virtual returns (uint64 bitmap) { + return type(uint64).max; + } + /// ============== Invariants ============================================= /// @dev When we add new immutables, this function should be updated @@ -482,7 +536,12 @@ abstract contract ManagerBase is } function _checkThresholdInvariants() internal view { - uint8 threshold = _getThresholdStorage().num; + _checkThresholdInvariants(_getThresholdStorage().num); + } + + function _checkThresholdInvariants( + uint8 threshold + ) internal pure { _NumTransceivers memory numTransceivers = _getNumTransceiversStorage(); // invariant: threshold <= enabledTransceivers.length diff --git a/evm/src/NttManager/NttManager.sol b/evm/src/NttManager/NttManager.sol index b41c5f923..0578b3784 100644 --- a/evm/src/NttManager/NttManager.sol +++ b/evm/src/NttManager/NttManager.sol @@ -223,16 +223,31 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { address transferRecipient = fromWormholeFormat(nativeTokenTransfer.to); - { - // Check inbound rate limits - bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId); - if (isRateLimited) { - // queue up the transfer - _enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient); - - // end execution early - return; - } + bool enqueued = _enqueueOrConsumeInboundRateLimit( + digest, sourceChainId, nativeTransferAmount, transferRecipient + ); + + if (enqueued) { + return; + } + + _mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false); + } + + function _enqueueOrConsumeInboundRateLimit( + bytes32 digest, + uint16 sourceChainId, + TrimmedAmount nativeTransferAmount, + address transferRecipient + ) internal virtual returns (bool) { + // Check inbound rate limits + bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId); + if (isRateLimited) { + // queue up the transfer + _enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient); + + // end execution early + return true; } // consume the amount for the inbound rate limit @@ -240,16 +255,15 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { // When receiving a transfer, we refill the outbound rate limit // by the same amount (we call this "backflow") _backfillOutboundAmount(nativeTransferAmount); - - _mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false); + return false; } /// @inheritdoc INttManager function completeInboundQueuedTransfer( bytes32 digest - ) external nonReentrant whenNotPaused { + ) external virtual nonReentrant whenNotPaused { // find the message in the queue - InboundQueuedTransfer memory queuedTransfer = getInboundQueuedTransfer(digest); + InboundQueuedTransfer memory queuedTransfer = RateLimiter.getInboundQueuedTransfer(digest); if (queuedTransfer.txTimestamp == 0) { revert InboundQueuedTransferNotFound(digest); } @@ -269,7 +283,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { /// @inheritdoc INttManager function completeOutboundQueuedTransfer( uint64 messageSequence - ) external payable nonReentrant whenNotPaused returns (uint64) { + ) external payable virtual nonReentrant whenNotPaused returns (uint64) { // find the message in the queue OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence]; if (queuedTransfer.txTimestamp == 0) { @@ -299,7 +313,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { /// @inheritdoc INttManager function cancelOutboundQueuedTransfer( uint64 messageSequence - ) external nonReentrant whenNotPaused { + ) external virtual nonReentrant whenNotPaused { // find the message in the queue OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence]; if (queuedTransfer.txTimestamp == 0) { @@ -382,50 +396,24 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { // trim amount after burning to ensure transfer amount matches (amount - fee) TrimmedAmount trimmedAmount = _trimTransferAmount(amount, recipientChain); - TrimmedAmount internalAmount = trimmedAmount.shift(tokenDecimals()); // get the sequence for this transfer uint64 sequence = _useMessageSequence(); - { - // now check rate limits - bool isAmountRateLimited = _isOutboundAmountRateLimited(internalAmount); - if (!shouldQueue && isAmountRateLimited) { - revert NotEnoughCapacity(getCurrentOutboundCapacity(), amount); - } - if (shouldQueue && isAmountRateLimited) { - // verify chain has not forked - checkFork(evmChainId); - - // emit an event to notify the user that the transfer is rate limited - emit OutboundTransferRateLimited( - msg.sender, sequence, amount, getCurrentOutboundCapacity() - ); - - // queue up and return - _enqueueOutboundTransfer( - sequence, - trimmedAmount, - recipientChain, - recipient, - refundAddress, - msg.sender, - transceiverInstructions - ); - - // refund price quote back to sender - _refundToSender(msg.value); - - // return the sequence in the queue - return sequence; - } - } + bool enqueued = _enqueueOrConsumeOutboundRateLimit( + amount, + recipientChain, + recipient, + refundAddress, + shouldQueue, + transceiverInstructions, + trimmedAmount, + sequence + ); - // otherwise, consume the outbound amount - _consumeOutboundAmount(internalAmount); - // When sending a transfer, we refill the inbound rate limit for - // that chain by the same amount (we call this "backflow") - _backfillInboundAmount(internalAmount, recipientChain); + if (enqueued) { + return sequence; + } return _transfer( sequence, @@ -438,6 +426,58 @@ contract NttManager is INttManager, RateLimiter, ManagerBase { ); } + function _enqueueOrConsumeOutboundRateLimit( + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + bytes32 refundAddress, + bool shouldQueue, + bytes memory transceiverInstructions, + TrimmedAmount trimmedAmount, + uint64 sequence + ) internal virtual returns (bool enqueued) { + TrimmedAmount internalAmount = trimmedAmount.shift(tokenDecimals()); + + // now check rate limits + bool isAmountRateLimited = _isOutboundAmountRateLimited(internalAmount); + if (!shouldQueue && isAmountRateLimited) { + revert NotEnoughCapacity(getCurrentOutboundCapacity(), amount); + } + if (shouldQueue && isAmountRateLimited) { + // verify chain has not forked + checkFork(evmChainId); + + // emit an event to notify the user that the transfer is rate limited + emit OutboundTransferRateLimited( + msg.sender, sequence, amount, getCurrentOutboundCapacity() + ); + + // queue up and return + _enqueueOutboundTransfer( + sequence, + trimmedAmount, + recipientChain, + recipient, + refundAddress, + msg.sender, + transceiverInstructions + ); + + // refund price quote back to sender + _refundToSender(msg.value); + + // return that the transfer has been enqued + return true; + } + + // otherwise, consume the outbound amount + _consumeOutboundAmount(internalAmount); + // When sending a transfer, we refill the inbound rate limit for + // that chain by the same amount (we call this "backflow") + _backfillInboundAmount(internalAmount, recipientChain); + return false; + } + function _transfer( uint64 sequence, TrimmedAmount amount, diff --git a/evm/src/NttManager/NttManagerNoRateLimiting.sol b/evm/src/NttManager/NttManagerNoRateLimiting.sol new file mode 100644 index 000000000..76c84f336 --- /dev/null +++ b/evm/src/NttManager/NttManagerNoRateLimiting.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "./NttManagerWithPerChainTransceivers.sol"; + +/// @title NttManagerNoRateLimiting +/// @author Wormhole Project Contributors. +/// @notice The NttManagerNoRateLimiting contract is an implementation of +/// NttManager that eliminates most of the rate limiting code to +/// free up code space. +/// +/// @dev All of the developer notes from `NttManager` apply here. +contract NttManagerNoRateLimiting is NttManagerWithPerChainTransceivers { + constructor( + address _token, + Mode _mode, + uint16 _chainId + ) NttManagerWithPerChainTransceivers(_token, _mode, _chainId, 0, true) {} + + // ==================== Override RateLimiter functions ========================= + + /// @notice Not used, always returns empty RateLimitParams. + function getOutboundLimitParams() public pure override returns (RateLimitParams memory) {} + + /// @notice Not used, always returns zero. + function getCurrentOutboundCapacity() public pure override returns (uint256) { + return 0; + } + + /// @notice Not used, always reverts with NotImplemented. + function getOutboundQueuedTransfer( + uint64 // queueSequence + ) public pure override returns (OutboundQueuedTransfer memory) { + revert NotImplemented(); + } + + /// @notice Not used, always returns empty RateLimitParams. + function getInboundLimitParams( + uint16 // chainId_ + ) public pure override returns (RateLimitParams memory) {} + + /// @notice Not used, always returns zero. + function getCurrentInboundCapacity( + uint16 // chainId_ + ) public pure override returns (uint256) { + return 0; + } + + /// @notice Not used, always reverts with NotImplemented. + function getInboundQueuedTransfer( + bytes32 // digest + ) public pure override returns (InboundQueuedTransfer memory) { + revert NotImplemented(); + } + + /// @notice Ignore RateLimiter setting. + function _setOutboundLimit( + TrimmedAmount // limit + ) internal override {} + + /// @notice Ignore RateLimiter setting. + function _setInboundLimit( + TrimmedAmount, // limit + uint16 // chainId_ + ) internal override {} + + // ==================== Unimplemented INttManager External Interface ================================= + + /// @notice Not used, always reverts with NotImplemented. + function completeOutboundQueuedTransfer( + uint64 // queueSequence + ) external payable override whenNotPaused returns (uint64) { + revert NotImplemented(); + } + + /// @notice Not used, always reverts with NotImplemented. + function cancelOutboundQueuedTransfer( + uint64 // queueSequence + ) external view override whenNotPaused { + revert NotImplemented(); + } + + /// @notice Not used, always reverts with NotImplemented. + function completeInboundQueuedTransfer( + bytes32 // digest + ) external view override whenNotPaused { + revert NotImplemented(); + } + + // ==================== Overridden NttManager Implementations ================================= + + function _enqueueOrConsumeOutboundRateLimit( + uint256, // amount + uint16, // recipientChain + bytes32, // recipient + bytes32, // refundAddress + bool, // shouldQueue + bytes memory, // transceiverInstructions + TrimmedAmount, // trimmedAmount + uint64 // sequence + ) internal pure override returns (bool) { + return false; + } + + function _enqueueOrConsumeInboundRateLimit( + bytes32, // digest + uint16, // sourceChainId + TrimmedAmount, // nativeTransferAmount + address // transferRecipient + ) internal pure override returns (bool) { + return false; + } +} diff --git a/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol new file mode 100644 index 000000000..9d66948c1 --- /dev/null +++ b/evm/src/NttManager/NttManagerWithPerChainTransceivers.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "./NttManager.sol"; + +/// @title NttManagerNoRateLimiting +/// @author Wormhole Project Contributors. +/// @notice The NttManagerNoRateLimiting abstract contract is an implementation of +/// NttManager that allows configuring different transceivers and thresholds +/// for each chain. Note that you can configure a different set of send and +/// receive transceivers for each chain, and if you don't specifically enable +/// any transceivers for a chain, then all transceivers will be used for it. +/// +/// @dev All of the developer notes from `NttManager` apply here. +abstract contract NttManagerWithPerChainTransceivers is NttManager { + constructor( + address _token, + Mode _mode, + uint16 _chainId, + uint64 _rateLimitDuration, + bool _skipRateLimiting + ) NttManager(_token, _mode, _chainId, _rateLimitDuration, _skipRateLimiting) {} + + bytes32 private constant SEND_TRANSCEIVER_BITMAP_SLOT = + bytes32(uint256(keccak256("nttpct.sendTransceiverBitmap")) - 1); + + bytes32 private constant RECV_TRANSCEIVER_BITMAP_SLOT = + bytes32(uint256(keccak256("nttpct.recvTransceiverBitmap")) - 1); + + // ==================== Override / implementation of transceiver stuff ========================= + + /// @inheritdoc IManagerBase + function enableSendTransceiverForChain( + address transceiver, + uint16 forChainId + ) external override(ManagerBase, IManagerBase) onlyOwner { + _enableTranceiverForChain(transceiver, forChainId, SEND_TRANSCEIVER_BITMAP_SLOT); + } + + /// @inheritdoc IManagerBase + function enableRecvTransceiverForChain( + address transceiver, + uint16 forChainId + ) external override(ManagerBase, IManagerBase) onlyOwner { + _enableTranceiverForChain(transceiver, forChainId, RECV_TRANSCEIVER_BITMAP_SLOT); + } + + function _enableTranceiverForChain( + address transceiver, + uint16 forChainId, + bytes32 tag + ) internal onlyOwner { + if (transceiver == address(0)) { + revert InvalidTransceiverZeroAddress(); + } + + mapping(address => TransceiverInfo) storage transceiverInfos = _getTransceiverInfosStorage(); + if (!transceiverInfos[transceiver].registered) { + revert NonRegisteredTransceiver(transceiver); + } + + uint8 index = _getTransceiverInfosStorage()[transceiver].index; + mapping(uint16 => _EnabledTransceiverBitmap) storage _bitmaps = + _getPerChainTransceiverBitmapStorage(tag); + _bitmaps[forChainId].bitmap |= uint64(1 << index); + + emit TransceiverEnabledForChain(transceiver, forChainId); + } + + function _isSendTransceiverEnabledForChain( + address transceiver, + uint16 forChainId + ) internal view override returns (bool) { + uint64 bitmap = + _getPerChainTransceiverBitmapStorage(SEND_TRANSCEIVER_BITMAP_SLOT)[forChainId].bitmap; + if (bitmap == 0) { + // NOTE: this makes it backwards compatible -- if the bitmap is not + // set, it's assumed the corridor uses all transceivers. + bitmap = type(uint64).max; + } + uint8 index = _getTransceiverInfosStorage()[transceiver].index; + return (bitmap & uint64(1 << index)) != 0; + } + + function _getEnabledRecvTransceiversForChain( + uint16 forChainId + ) internal view override returns (uint64 bitmap) { + bitmap = + _getPerChainTransceiverBitmapStorage(RECV_TRANSCEIVER_BITMAP_SLOT)[forChainId].bitmap; + if (bitmap == 0) { + // NOTE: this makes it backwards compatible -- if the bitmap is not + // set, it's assumed the corridor uses all transceivers. + bitmap = type(uint64).max; + } + } + + function _getPerChainTransceiverBitmapStorage( + bytes32 tag + ) internal pure returns (mapping(uint16 => _EnabledTransceiverBitmap) storage $) { + // TODO: this is safe (reusing the storage slot, because the mapping + // doesn't write into the slot itself) buy maybe we shouldn't? + uint256 slot = uint256(tag); + assembly ("memory-safe") { + $.slot := slot + } + } + + // ==================== Override / implementation of threshold stuff ========================= + + /// @inheritdoc IManagerBase + function setPerChainThreshold( + uint16 forChainId, + uint8 threshold + ) external override(ManagerBase, IManagerBase) onlyOwner { + if (threshold == 0) { + revert ZeroThreshold(); + } + + mapping(uint16 => _Threshold) storage _threshold = _getThresholdStoragePerChain(); + uint8 oldThreshold = _threshold[forChainId].num; + + _threshold[forChainId].num = threshold; + _checkThresholdInvariants(_threshold[forChainId].num); + + emit PerChainThresholdChanged(forChainId, oldThreshold, threshold); + } + + function getPerChainThreshold( + uint16 forChainId + ) public view override(ManagerBase, IManagerBase) returns (uint8) { + uint8 threshold = _getThresholdStoragePerChain()[forChainId].num; + if (threshold == 0) { + return _getThresholdStorage().num; + } + return threshold; + } + + function _getThresholdStoragePerChain() + private + pure + returns (mapping(uint16 => _Threshold) storage $) + { + // TODO: this is safe (reusing the storage slot, because the mapping + // doesn't write into the slot itself) buy maybe we shouldn't? + uint256 slot = uint256(THRESHOLD_SLOT); + assembly ("memory-safe") { + $.slot := slot + } + } +} diff --git a/evm/src/interfaces/IManagerBase.sol b/evm/src/interfaces/IManagerBase.sol index c4d1d565f..708b2380c 100644 --- a/evm/src/interfaces/IManagerBase.sol +++ b/evm/src/interfaces/IManagerBase.sol @@ -21,6 +21,9 @@ interface IManagerBase { struct AttestationInfo { bool executed; uint64 attestedTransceivers; + // TODO: think about potential migration issues here (this field was + // introduced later, so for older messages it will be 0). + uint16 sourceChainId; } struct _Sequence { @@ -46,6 +49,14 @@ interface IManagerBase { /// @param threshold The new threshold. event ThresholdChanged(uint8 oldThreshold, uint8 threshold); + /// @notice Emmitted when the per-chain threshold required transceivers is changed. + /// @dev Topic0 + /// 0x2a855b929b9a53c6fb5b5ed248b27e502b709c088e036a5aa17620c8fc5085a9. + /// @param chainId The chain to which the threshold applies. + /// @param oldThreshold The old threshold. + /// @param threshold The new threshold. + event PerChainThresholdChanged(uint16 chainId, uint8 oldThreshold, uint8 threshold); + /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. @@ -54,6 +65,13 @@ interface IManagerBase { /// @param threshold The current threshold of transceivers. event TransceiverAdded(address transceiver, uint256 transceiversNum, uint8 threshold); + /// @notice Emitted when a transceiver is enabled for a chain. + /// @dev Topic0 + /// 0xf05962b5774c658e85ed80c91a75af9d66d2af2253dda480f90bce78aff5eda5. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain to which the threshold applies. + event TransceiverEnabledForChain(address transceiver, uint16 chainId); + /// @notice Emitted when an transceiver is removed from the nttManager. /// @dev Topic0 /// 0x697a3853515b88013ad432f29f53d406debc9509ed6d9313dcfe115250fcd18f. @@ -109,6 +127,9 @@ interface IManagerBase { /// @param chainId The target Wormhole chain id error PeerNotRegistered(uint16 chainId); + /// @notice Feature is not implemented. + error NotImplemented(); + /// @notice Fetch the delivery price for a given recipient chain transfer. /// @param recipientChain The Wormhole chain ID of the transfer destination. /// @param transceiverInstructions The transceiver specific instructions for quoting and sending @@ -126,6 +147,13 @@ interface IManagerBase { uint8 threshold ) external; + /// @notice Sets the per-chain threshold for the number of attestations required for a message + /// to be considered valid. Note that if a threshold is not specified for a chain, the default applies. + /// @param chainId The chain for which the threshold applies. + /// @param threshold The new threshold. + /// @dev This method can only be executed by the `owner`. + function setPerChainThreshold(uint16 chainId, uint8 threshold) external; + /// @notice Sets the transceiver for the given chain. /// @param transceiver The address of the transceiver. /// @dev This method can only be executed by the `owner`. @@ -140,6 +168,18 @@ interface IManagerBase { address transceiver ) external; + /// @notice Enables the transceiver for sending on the given chain. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain for which the threshold applies. + /// @dev This method can only be executed by the `owner`. + function enableSendTransceiverForChain(address transceiver, uint16 chainId) external; + + /// @notice Enables the transceiver for receiving on the given chain. + /// @param transceiver The address of the transceiver. + /// @param chainId The chain for which the threshold applies. + /// @dev This method can only be executed by the `owner`. + function enableRecvTransceiverForChain(address transceiver, uint16 chainId) external; + /// @notice Checks if a message has been approved. The message should have at least /// the minimum threshold of attestations from distinct endpoints. /// @param digest The digest of the message. @@ -177,6 +217,13 @@ interface IManagerBase { /// it to be considered valid and acted upon. function getThreshold() external view returns (uint8); + /// @notice Returns the number of Transceivers that must attest to a msgId for + /// it to be considered valid and acted upon. + /// @param chainId The chain for which the threshold applies. + function getPerChainThreshold( + uint16 chainId + ) external view returns (uint8); + /// @notice Returns a boolean indicating if the transceiver has attested to the message. /// @param digest The digest of the message. /// @param index The index of the transceiver diff --git a/evm/src/libraries/RateLimiter.sol b/evm/src/libraries/RateLimiter.sol index 0fead5ce2..2543c542f 100644 --- a/evm/src/libraries/RateLimiter.sol +++ b/evm/src/libraries/RateLimiter.sol @@ -95,15 +95,15 @@ abstract contract RateLimiter is IRateLimiter, IRateLimiterEvents { function _setOutboundLimit( TrimmedAmount limit - ) internal { + ) internal virtual { _setLimit(limit, _getOutboundLimitParamsStorage()); } - function getOutboundLimitParams() public pure returns (RateLimitParams memory) { + function getOutboundLimitParams() public pure virtual returns (RateLimitParams memory) { return _getOutboundLimitParamsStorage(); } - function getCurrentOutboundCapacity() public view returns (uint256) { + function getCurrentOutboundCapacity() public view virtual returns (uint256) { TrimmedAmount trimmedCapacity = _getCurrentCapacity(getOutboundLimitParams()); uint8 decimals = tokenDecimals(); return trimmedCapacity.untrim(decimals); @@ -111,23 +111,23 @@ abstract contract RateLimiter is IRateLimiter, IRateLimiterEvents { function getOutboundQueuedTransfer( uint64 queueSequence - ) public view returns (OutboundQueuedTransfer memory) { + ) public view virtual returns (OutboundQueuedTransfer memory) { return _getOutboundQueueStorage()[queueSequence]; } - function _setInboundLimit(TrimmedAmount limit, uint16 chainId_) internal { + function _setInboundLimit(TrimmedAmount limit, uint16 chainId_) internal virtual { _setLimit(limit, _getInboundLimitParamsStorage()[chainId_]); } function getInboundLimitParams( uint16 chainId_ - ) public view returns (RateLimitParams memory) { + ) public view virtual returns (RateLimitParams memory) { return _getInboundLimitParamsStorage()[chainId_]; } function getCurrentInboundCapacity( uint16 chainId_ - ) public view returns (uint256) { + ) public view virtual returns (uint256) { TrimmedAmount trimmedCapacity = _getCurrentCapacity(getInboundLimitParams(chainId_)); uint8 decimals = tokenDecimals(); return trimmedCapacity.untrim(decimals); @@ -135,7 +135,7 @@ abstract contract RateLimiter is IRateLimiter, IRateLimiterEvents { function getInboundQueuedTransfer( bytes32 digest - ) public view returns (InboundQueuedTransfer memory) { + ) public view virtual returns (InboundQueuedTransfer memory) { return _getInboundQueueStorage()[digest]; } diff --git a/evm/test/IntegrationWithoutRateLimiting.t.sol b/evm/test/IntegrationWithoutRateLimiting.t.sol new file mode 100755 index 000000000..83951297b --- /dev/null +++ b/evm/test/IntegrationWithoutRateLimiting.t.sol @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManagerNoRateLimiting.sol"; +import "../src/Transceiver/Transceiver.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/ITransceiver.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; +import "../src/interfaces/IWormholeTransceiver.sol"; +import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "./mocks/MockNttManager.sol"; +import "./mocks/MockTransceivers.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +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 TestEndToEndNoRateLimiting is Test { + NttManagerNoRateLimiting nttManagerChain1; + NttManagerNoRateLimiting nttManagerChain2; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + uint16 constant chainId1 = 7; + uint16 constant chainId2 = 100; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + + uint16 constant SENDING_CHAIN_ID = 1; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + WormholeTransceiver wormholeTransceiverChain1; + WormholeTransceiver wormholeTransceiverChain2; + address userA = address(0x123); + address userB = address(0x456); + address userC = address(0x789); + address userD = address(0xABC); + + address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + vm.chainId(chainId1); + DummyToken t1 = new DummyToken(); + NttManagerNoRateLimiting implementation = new MockNttManagerNoRateLimitingContract( + address(t1), IManagerBase.Mode.LOCKING, chainId1 + ); + + nttManagerChain1 = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementation), "")) + ); + nttManagerChain1.initialize(); + + WormholeTransceiver wormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) + ); + + // Only the deployer should be able to initialize + vm.prank(userA); + vm.expectRevert( + abi.encodeWithSelector(ITransceiver.UnexpectedDeployer.selector, address(this), userA) + ); + wormholeTransceiverChain1.initialize(); + + // Actually initialize properly now + wormholeTransceiverChain1.initialize(); + + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + // nttManagerChain1.setOutboundLimit(type(uint64).max); + // nttManagerChain1.setInboundLimit(type(uint64).max, chainId2); + + // Chain 2 setup + vm.chainId(chainId2); + DummyToken t2 = new DummyTokenMintAndBurn(); + NttManagerNoRateLimiting implementationChain2 = new MockNttManagerNoRateLimitingContract( + address(t2), IManagerBase.Mode.BURNING, chainId2 + ); + + nttManagerChain2 = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementationChain2), "")) + ); + nttManagerChain2.initialize(); + + WormholeTransceiver wormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) + ); + wormholeTransceiverChain2.initialize(); + + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + // nttManagerChain2.setOutboundLimit(type(uint64).max); + // nttManagerChain2.setInboundLimit(type(uint64).max, chainId1); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + nttManagerChain2.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + + require(nttManagerChain1.getThreshold() != 0, "Threshold is zero with active transceivers"); + + // Actually set it + nttManagerChain1.setThreshold(1); + nttManagerChain2.setThreshold(1); + + INttManager.NttManagerPeer memory peer = nttManagerChain1.getPeer(chainId2); + require(9 == peer.tokenDecimals, "Peer has the wrong number of token decimals"); + } + + function test_chainToChainBase() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token through standard means (not relayer) + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain2 verification and checks + vm.chainId(chainId2); + + // Wrong chain receiving the signed VAA + vm.expectRevert(abi.encodeWithSelector(InvalidFork.selector, chainId1, chainId2)); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + { + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManagerNoRateLimiting has unintended funds" + ); + } + + // Can't resubmit the same message twice + (IWormhole.VM memory wormholeVM,,) = wormhole.parseAndVerifyVM(encodedVMs[0]); + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.TransferAlreadyCompleted.selector, wormholeVM.hash + ) + ); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManagerNoRateLimiting didn't receive unintended funds" + ); + } + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + { + uint256 supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + } + + // This test triggers some basic reverts to increase our code coverage. + function test_someReverts() public { + // These shouldn't revert. + nttManagerChain1.setOutboundLimit(0); + nttManagerChain1.setInboundLimit(0, chainId2); + + require( + nttManagerChain1.getCurrentOutboundCapacity() == 0, + "getCurrentOutboundCapacity returned unexpected value" + ); + + require( + nttManagerChain1.getCurrentInboundCapacity(chainId2) == 0, + "getCurrentInboundCapacity returned unexpected value" + ); + + // Everything else should. + vm.expectRevert(abi.encodeWithSelector(INttManager.InvalidPeerChainIdZero.selector)); + nttManagerChain1.setPeer( + 0, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + + vm.expectRevert(abi.encodeWithSelector(INttManager.InvalidPeerZeroAddress.selector)); + nttManagerChain1.setPeer(chainId2, bytes32(0), 9, type(uint64).max); + + vm.expectRevert(abi.encodeWithSelector(INttManager.InvalidPeerDecimals.selector)); + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 0, type(uint64).max + ); + + vm.expectRevert(abi.encodeWithSelector(INttManager.InvalidPeerSameChainId.selector)); + nttManagerChain1.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManagerChain1.getOutboundQueuedTransfer(0); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManagerChain1.getInboundQueuedTransfer(bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManagerChain1.completeInboundQueuedTransfer(bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManagerChain1.completeOutboundQueuedTransfer(0); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManagerChain1.cancelOutboundQueuedTransfer(0); + } + + function test_lotsOfReverts() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token through standard means (not relayer) + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer( + sendingAmount, + chainId2, + toWormholeFormat(userB), + toWormholeFormat(userA), + true, + encodeTransceiverInstruction(true) + ); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.InvalidWormholePeer.selector, + chainId1, + wormholeTransceiverChain1 + ) + ); // Wrong chain receiving the signed VAA + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + + vm.chainId(chainId2); + { + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManagerNoRateLimiting has unintended funds" + ); + } + + // Can't resubmit the same message twice + (IWormhole.VM memory wormholeVM,,) = wormhole.parseAndVerifyVM(encodedVMs[0]); + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.TransferAlreadyCompleted.selector, wormholeVM.hash + ) + ); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + + vm.stopPrank(); + // nttManagerChain2.setOutboundLimit(0); + + vm.startPrank(userC); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + true, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManagerNoRateLimiting didn't receive unintended funds" + ); + } + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + vm.stopPrank(); // Back to the owner of everything for this one. + vm.recordLogs(); + + { + uint256 supplyBefore = token1.totalSupply(); + + // nttManagerChain1.setInboundLimit(0, chainId2); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + + bytes32[] memory queuedDigests = + Utils.fetchQueuedTransferDigestsFromLogs(vm.getRecordedLogs()); + + require(0 == queuedDigests.length, "Should not queue inbound messages"); + + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + } + + function test_multiTransceiver() public { + vm.chainId(chainId1); + + WormholeTransceiver wormholeTransceiverChain1_1 = wormholeTransceiverChain1; + + // Dual transceiver setup + WormholeTransceiver wormholeTransceiverChain1_2 = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + + wormholeTransceiverChain1_2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain1_2), "")) + ); + wormholeTransceiverChain1_2.initialize(); + + vm.chainId(chainId2); + WormholeTransceiver wormholeTransceiverChain2_1 = wormholeTransceiverChain2; + + // Dual transceiver setup + WormholeTransceiver wormholeTransceiverChain2_2 = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + + wormholeTransceiverChain2_2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain2_2), "")) + ); + wormholeTransceiverChain2_2.initialize(); + + // Setup the new entrypoint hook ups to allow the transfers to occur + wormholeTransceiverChain1_2.setWormholePeer( + chainId2, bytes32(uint256(uint160((address(wormholeTransceiverChain2_2))))) + ); + wormholeTransceiverChain2_2.setWormholePeer( + chainId1, bytes32(uint256(uint160((address(wormholeTransceiverChain1_2))))) + ); + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2_2)); + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1_2)); + + // Change the threshold from the setUp functions 1 to 2. + nttManagerChain1.setThreshold(2); + nttManagerChain2.setThreshold(2); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + vm.startPrank(userA); + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), sendingAmount); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.chainId(chainId1); + vm.recordLogs(); + + // Send token through standard means (not relayer) + { + nttManagerChain1.transfer( + sendingAmount, + chainId2, + toWormholeFormat(userB), + toWormholeFormat(userA), + false, + encodeTransceiverInstructions(true) + ); + } + + // Get and sign the event emissions to go to the other chain. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + vm.chainId(chainId2); + + // Send in the messages for the two transceivers to complete the transfer from chain1 to chain2 + { + // vm.stopPrank(); + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2_1.receiveMessage(encodedVMs[0]); + + // Playing the VAA to the wrong transceiver should revert. + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.InvalidWormholePeer.selector, + chainId1, + wormholeTransceiverChain1_1 + ) + ); + wormholeTransceiverChain2_2.receiveMessage(encodedVMs[0]); + + // Threshold check + require(supplyBefore == token2.totalSupply(), "Supplies have been updated too early"); + require(token2.balanceOf(userB) == 0, "User received tokens to early"); + + // Finish the transfer out once the second VAA arrives + wormholeTransceiverChain2_2.receiveMessage(encodedVMs[1]); + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManagerNoRateLimiting has unintended funds" + ); + } + + // Back the other way for the burn! + vm.startPrank(userB); + token2.approve(address(nttManagerChain2), sendingAmount); + + vm.recordLogs(); + + // Send token through standard means (not relayer) + { + uint256 userBalanceBefore = token1.balanceOf(address(userB)); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userA), + toWormholeFormat(userB), + false, + encodeTransceiverInstructions(true) + ); + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain2)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + + require(userBalanceBefore - userBalanceAfter == 0, "No funds left for user"); + require( + nttManagerBalanceAfter == 0, + "NttManagerNoRateLimiting should burn all tranferred tokens" + ); + } + + // Get the VAA proof for the transfers to use + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + vm.chainId(chainId1); + { + uint256 supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1_1.receiveMessage(encodedVMs[0]); + + require(supplyBefore == token1.totalSupply(), "Supplies have been updated too early"); + require(token2.balanceOf(userA) == 0, "User received tokens to early"); + + // Finish the transfer out once the second VAA arrives + wormholeTransceiverChain1_2.receiveMessage(encodedVMs[1]); + uint256 supplyAfter = token1.totalSupply(); + + require( + supplyBefore == supplyAfter, + "Supplies don't match between operations. Should not increase." + ); + require(token1.balanceOf(userB) == 0, "Sending user receive tokens"); + require( + token1.balanceOf(userA) == sendingAmount, "Receiving user didn't receive tokens" + ); + } + + vm.stopPrank(); + } + + function copyBytes( + bytes memory _bytes + ) private pure returns (bytes memory) { + bytes memory copy = new bytes(_bytes.length); + uint256 max = _bytes.length + 31; + for (uint256 i = 32; i <= max; i += 32) { + assembly { + mstore(add(copy, i), mload(add(_bytes, i))) + } + } + return copy; + } + + function encodeTransceiverInstruction( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.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); + } + + // Encode an instruction for each of the relayers + function encodeTransceiverInstructions( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); + + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = + TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = + TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); + + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](2); + + TransceiverInstructions[0] = TransceiverInstruction1; + TransceiverInstructions[1] = TransceiverInstruction2; + + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } +} diff --git a/evm/test/NttManagerNoRateLimiting.t.sol b/evm/test/NttManagerNoRateLimiting.t.sol new file mode 100644 index 000000000..c58d7c305 --- /dev/null +++ b/evm/test/NttManagerNoRateLimiting.t.sol @@ -0,0 +1,1183 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManagerNoRateLimiting.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import "../src/NttManager/TransceiverRegistry.sol"; +import "../src/libraries/PausableUpgradeable.sol"; +import "../src/libraries/TransceiverHelpers.sol"; +import {Utils} from "./libraries/Utils.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +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 "./libraries/TransceiverHelpers.sol"; +import "./libraries/NttManagerHelpers.sol"; +import "./interfaces/ITransceiverReceiver.sol"; +import "./mocks/DummyTransceiver.sol"; +import "../src/mocks/DummyToken.sol"; +import "./mocks/MockNttManager.sol"; + +// TODO: set this up so the common functionality tests can be run against both +contract TestNttManagerNoRateLimiting is Test, IRateLimiterEvents { + MockNttManagerNoRateLimitingContract nttManager; + MockNttManagerNoRateLimitingContract nttManagerOther; + MockNttManagerNoRateLimitingContract nttManagerZeroRateLimiter; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + // 0x99'E''T''T' + uint16 constant chainId = 7; + uint16 constant chainId2 = 8; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + DummyTransceiver dummyTransceiver; + + 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); + + DummyToken t = new DummyToken(); + NttManagerNoRateLimiting implementation = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + + NttManagerNoRateLimiting otherImplementation = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + + nttManager = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementation), "")) + ); + nttManager.initialize(); + + nttManagerOther = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(otherImplementation), "")) + ); + nttManagerOther.initialize(); + + dummyTransceiver = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(dummyTransceiver)); + } + + // === pure unit tests + + // naive implementation of countSetBits to test against + function simpleCount( + uint64 n + ) public pure returns (uint8) { + uint8 count; + + while (n > 0) { + count += uint8(n & 1); + n >>= 1; + } + + return count; + } + + function testFuzz_countSetBits( + uint64 n + ) public { + assertEq(simpleCount(n), countSetBits(n)); + } + + // === ownership + + function test_owner() public { + // TODO: implement separate governance contract + assertEq(nttManager.owner(), address(this)); + } + + function test_transferOwnership() public { + address newOwner = address(0x123); + nttManager.transferOwnership(newOwner); + assertEq(nttManager.owner(), newOwner); + } + + function test_onlyOwnerCanTransferOwnership() public { + address notOwner = address(0x123); + vm.startPrank(notOwner); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, notOwner) + ); + nttManager.transferOwnership(address(0x456)); + } + + function test_pauseUnpause() public { + assertEq(nttManager.isPaused(), false); + nttManager.pause(); + assertEq(nttManager.isPaused(), true); + + // When the NttManagerNoRateLimiting is paused, initiating transfers, completing queued transfers on both source and destination chains, + // executing transfers and attesting to transfers should all revert + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + nttManager.transfer(0, 0, bytes32(0)); + + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + nttManager.completeOutboundQueuedTransfer(0); + + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + nttManager.completeInboundQueuedTransfer(bytes32(0)); + + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + TransceiverStructs.NttManagerMessage memory message; + nttManager.executeMsg(0, bytes32(0), message); + + bytes memory transceiverMessage; + (, transceiverMessage) = TransceiverHelpersLib.buildTransceiverMessageWithNttManagerPayload( + 0, + bytes32(0), + toWormholeFormat(address(nttManagerOther)), + toWormholeFormat(address(nttManager)), + abi.encode("payload") + ); + vm.expectRevert( + abi.encodeWithSelector(PausableUpgradeable.RequireContractIsNotPaused.selector) + ); + dummyTransceiver.receiveMessage(transceiverMessage); + + nttManager.unpause(); + assertEq(nttManager.isPaused(), false); + } + + function test_pausePauserUnpauseOnlyOwner() public { + // transfer pauser to another address + address pauser = address(0x123); + nttManager.transferPauserCapability(pauser); + + // execute from pauser context + vm.startPrank(pauser); + assertEq(nttManager.isPaused(), false); + nttManager.pause(); + assertEq(nttManager.isPaused(), true); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, pauser) + ); + nttManager.unpause(); + + // execute from owner context + // ensures that owner can still unpause + vm.startPrank(address(this)); + nttManager.unpause(); + assertEq(nttManager.isPaused(), false); + } + + // === deployment with invalid token + function test_brokenToken() public { + DummyToken t = new DummyTokenBroken(); + NttManagerNoRateLimiting implementation = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + + NttManagerNoRateLimiting newNttManagerNoRateLimiting = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementation), "")) + ); + vm.expectRevert(abi.encodeWithSelector(INttManager.StaticcallFailed.selector)); + newNttManagerNoRateLimiting.initialize(); + + vm.expectRevert(abi.encodeWithSelector(INttManager.StaticcallFailed.selector)); + newNttManagerNoRateLimiting.transfer(1, 1, bytes32("1")); + } + + // === transceiver registration + + function test_registerTransceiver() public { + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + } + + function test_onlyOwnerCanModifyTransceivers() public { + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + + address notOwner = address(0x123); + vm.startPrank(notOwner); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, notOwner) + ); + nttManager.setTransceiver(address(e)); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, notOwner) + ); + nttManager.removeTransceiver(address(e)); + } + + function test_cantEnableTransceiverTwice() public { + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + + vm.expectRevert( + abi.encodeWithSelector( + TransceiverRegistry.TransceiverAlreadyEnabled.selector, address(e) + ) + ); + nttManager.setTransceiver(address(e)); + } + + function test_disableReenableTransceiver() public { + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + nttManager.removeTransceiver(address(e)); + nttManager.setTransceiver(address(e)); + } + + function test_disableAllTransceiversFails() public { + vm.expectRevert(abi.encodeWithSelector(IManagerBase.ZeroThreshold.selector)); + nttManager.removeTransceiver(address(dummyTransceiver)); + } + + function test_multipleTransceivers() public { + DummyTransceiver e1 = new DummyTransceiver(address(nttManager)); + DummyTransceiver e2 = new DummyTransceiver(address(nttManager)); + + nttManager.setTransceiver(address(e1)); + nttManager.setTransceiver(address(e2)); + } + + function test_transceiverIncompatibleNttManagerNoRateLimiting() public { + // Transceiver instantiation reverts if the nttManager doesn't have the proper token method + vm.expectRevert(bytes("")); + new DummyTransceiver(address(0xBEEF)); + } + + function test_transceiverWrongNttManagerNoRateLimiting() public { + // TODO: this is accepted currently. should we include a check to ensure + // only transceivers whose nttManager is us can be registered? (this would be + // a convenience check, not a security one) + DummyToken t = new DummyToken(); + NttManagerNoRateLimiting altNttManagerNoRateLimiting = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + DummyTransceiver e = new DummyTransceiver(address(altNttManagerNoRateLimiting)); + nttManager.setTransceiver(address(e)); + } + + function test_noEnabledTransceivers() public { + DummyToken token = new DummyToken(); + NttManagerNoRateLimiting implementation = new MockNttManagerNoRateLimitingContract( + address(token), IManagerBase.Mode.LOCKING, chainId + ); + + MockNttManagerNoRateLimitingContract newNttManagerNoRateLimiting = + MockNttManagerNoRateLimitingContract(address(new ERC1967Proxy(address(implementation), ""))); + newNttManagerNoRateLimiting.initialize(); + + address user_A = address(0x123); + address user_B = address(0x456); + + uint8 decimals = token.decimals(); + + newNttManagerNoRateLimiting.setPeer( + chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max + ); + newNttManagerNoRateLimiting.setOutboundLimit( + packTrimmedAmount(type(uint64).max, 8).untrim(decimals) + ); + + token.mintDummy(address(user_A), 5 * 10 ** decimals); + + vm.startPrank(user_A); + + token.approve(address(newNttManagerNoRateLimiting), 3 * 10 ** decimals); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NoEnabledTransceivers.selector)); + newNttManagerNoRateLimiting.transfer( + 1 * 10 ** decimals, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + } + + function test_notTransceiver() public { + // TODO: this is accepted currently. should we include a check to ensure + // only transceivers can be registered? (this would be a convenience check, not a security one) + nttManager.setTransceiver(address(0x123)); + } + + function test_maxOutTransceivers() public { + // Let's register a transceiver and then disable it. We now have 2 registered managers + // since we register 1 in the setup + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + nttManager.removeTransceiver(address(e)); + + // We should be able to register 64 transceivers total + for (uint256 i = 0; i < 62; ++i) { + DummyTransceiver d = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(d)); + } + + // Registering a new transceiver should fail as we've hit the cap + DummyTransceiver c = new DummyTransceiver(address(nttManager)); + vm.expectRevert(TransceiverRegistry.TooManyTransceivers.selector); + nttManager.setTransceiver(address(c)); + + // We should be able to renable an already registered transceiver at the cap + nttManager.setTransceiver(address(e)); + } + + function test_passingInstructionsToTransceivers() public { + // Let's register a transceiver and then disable the original transceiver. We now have 2 registered transceivers + // since we register 1 in the setup + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + nttManager.removeTransceiver(address(dummyTransceiver)); + + address user_A = address(0x123); + address user_B = address(0x456); + + DummyToken token = DummyToken(nttManager.token()); + + uint8 decimals = token.decimals(); + + nttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + nttManager.setOutboundLimit(packTrimmedAmount(type(uint64).max, 8).untrim(decimals)); + + token.mintDummy(address(user_A), 5 * 10 ** decimals); + + vm.startPrank(user_A); + + token.approve(address(nttManager), 3 * 10 ** decimals); + + // Pass some instructions for the enabled transceiver + TransceiverStructs.TransceiverInstruction memory transceiverInstruction = + TransceiverStructs.TransceiverInstruction({index: 1, payload: new bytes(1)}); + TransceiverStructs.TransceiverInstruction[] memory transceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](1); + transceiverInstructions[0] = transceiverInstruction; + bytes memory instructions = + TransceiverStructs.encodeTransceiverInstructions(transceiverInstructions); + + nttManager.transfer( + 1 * 10 ** decimals, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + instructions + ); + } + + // == threshold + + function test_cantSetThresholdTooHigh() public { + // 1 transceiver set, so can't set threshold to 2 + vm.expectRevert(abi.encodeWithSelector(IManagerBase.ThresholdTooHigh.selector, 2, 1)); + nttManager.setThreshold(2); + } + + function test_canSetThreshold() public { + DummyTransceiver e1 = new DummyTransceiver(address(nttManager)); + DummyTransceiver e2 = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e1)); + nttManager.setTransceiver(address(e2)); + + nttManager.setThreshold(1); + nttManager.setThreshold(2); + nttManager.setThreshold(1); + } + + function test_cantSetThresholdToZero() public { + DummyTransceiver e = new DummyTransceiver(address(nttManager)); + nttManager.setTransceiver(address(e)); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.ZeroThreshold.selector)); + nttManager.setThreshold(0); + } + + function test_onlyOwnerCanSetThreshold() public { + address notOwner = address(0x123); + vm.startPrank(notOwner); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, notOwner) + ); + nttManager.setThreshold(1); + } + + // == threshold + + function test_peerRegistrationLimitsCantBeUpdated() public { + bytes32 peer = toWormholeFormat(address(nttManager)); + nttManager.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, 0); + + IRateLimiter.RateLimitParams memory params = + nttManager.getInboundLimitParams(TransceiverHelpersLib.SENDING_CHAIN_ID); + assertEq(params.limit.getAmount(), 0); + assertEq(params.limit.getDecimals(), 0); + + nttManager.setInboundLimit(type(uint64).max, TransceiverHelpersLib.SENDING_CHAIN_ID); + params = nttManager.getInboundLimitParams(TransceiverHelpersLib.SENDING_CHAIN_ID); + assertEq(params.limit.getAmount(), 0); + assertEq(params.limit.getDecimals(), 0); + } + + // === attestation + + function test_onlyEnabledTransceiversCanAttest() public { + (DummyTransceiver e1,) = TransceiverHelpersLib.setup_transceivers(nttManagerOther); + nttManagerOther.removeTransceiver(address(e1)); + bytes32 peer = toWormholeFormat(address(nttManager)); + nttManagerOther.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); + + bytes memory transceiverMessage; + (, transceiverMessage) = TransceiverHelpersLib.buildTransceiverMessageWithNttManagerPayload( + 0, bytes32(0), peer, toWormholeFormat(address(nttManagerOther)), abi.encode("payload") + ); + + vm.expectRevert( + abi.encodeWithSelector(TransceiverRegistry.CallerNotTransceiver.selector, address(e1)) + ); + e1.receiveMessage(transceiverMessage); + } + + function test_onlyPeerNttManagerNoRateLimitingCanAttest() public { + (DummyTransceiver e1,) = TransceiverHelpersLib.setup_transceivers(nttManagerOther); + nttManagerOther.setThreshold(2); + + bytes32 peer = toWormholeFormat(address(nttManager)); + + TransceiverStructs.NttManagerMessage memory nttManagerMessage; + bytes memory transceiverMessage; + (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + .buildTransceiverMessageWithNttManagerPayload( + 0, bytes32(0), peer, toWormholeFormat(address(nttManagerOther)), abi.encode("payload") + ); + + vm.expectRevert( + abi.encodeWithSelector( + INttManager.InvalidPeer.selector, TransceiverHelpersLib.SENDING_CHAIN_ID, peer + ) + ); + e1.receiveMessage(transceiverMessage); + } + + function test_attestSimple() public { + (DummyTransceiver e1,) = TransceiverHelpersLib.setup_transceivers(nttManagerOther); + nttManagerOther.setThreshold(2); + + // register nttManager peer + bytes32 peer = toWormholeFormat(address(nttManager)); + nttManagerOther.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); + + TransceiverStructs.NttManagerMessage memory nttManagerMessage; + bytes memory transceiverMessage; + (nttManagerMessage, 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 + ); + assertEq(nttManagerOther.messageAttestations(hash), 1); + } + + function test_attestTwice() public { + (DummyTransceiver e1,) = TransceiverHelpersLib.setup_transceivers(nttManagerOther); + nttManagerOther.setThreshold(2); + + // register nttManager peer + bytes32 peer = toWormholeFormat(address(nttManager)); + nttManagerOther.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); + + TransceiverStructs.NttManagerMessage memory nttManagerMessage; + bytes memory transceiverMessage; + (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + .buildTransceiverMessageWithNttManagerPayload( + 0, bytes32(0), peer, toWormholeFormat(address(nttManagerOther)), abi.encode("payload") + ); + + bytes32 hash = TransceiverStructs.nttManagerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, nttManagerMessage + ); + + e1.receiveMessage(transceiverMessage); + vm.expectRevert( + abi.encodeWithSelector(IManagerBase.TransceiverAlreadyAttestedToMessage.selector, hash) + ); + e1.receiveMessage(transceiverMessage); + + // can't double vote + assertEq(nttManagerOther.messageAttestations(hash), 1); + } + + function test_attestDisabled() public { + (DummyTransceiver e1,) = TransceiverHelpersLib.setup_transceivers(nttManagerOther); + nttManagerOther.setThreshold(2); + + bytes32 peer = toWormholeFormat(address(nttManager)); + nttManagerOther.setPeer(TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max); + + ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](1); + transceivers[0] = e1; + + TransceiverStructs.NttManagerMessage memory m; + (m,) = TransceiverHelpersLib.attestTransceiversHelper( + address(0x456), + 0, + chainId, + nttManager, + nttManagerOther, + packTrimmedAmount(50, 8), + packTrimmedAmount(type(uint64).max, 8), + transceivers + ); + + nttManagerOther.removeTransceiver(address(e1)); + + bytes32 hash = + TransceiverStructs.nttManagerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m); + // a disabled transceiver's vote no longer counts + assertEq(nttManagerOther.messageAttestations(hash), 0); + + nttManagerOther.setTransceiver(address(e1)); + // it counts again when reenabled + assertEq(nttManagerOther.messageAttestations(hash), 1); + } + + function test_transfer_sequences() public { + address user_A = address(0x123); + address user_B = address(0x456); + + DummyToken token = DummyToken(nttManager.token()); + + uint8 decimals = token.decimals(); + + nttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + nttManager.setOutboundLimit(packTrimmedAmount(type(uint64).max, 8).untrim(decimals)); + + token.mintDummy(address(user_A), 5 * 10 ** decimals); + + vm.startPrank(user_A); + + token.approve(address(nttManager), 3 * 10 ** decimals); + + uint64 s1 = nttManager.transfer( + 1 * 10 ** decimals, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + uint64 s2 = nttManager.transfer( + 1 * 10 ** decimals, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + uint64 s3 = nttManager.transfer( + 1 * 10 ** decimals, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + + assertEq(s1, 0); + assertEq(s2, 1); + assertEq(s3, 2); + } + + function test_transferWithAmountAndDecimalsThatCouldOverflow() public { + // The source chain has 18 decimals trimmed to 8, and the peer has 6 decimals trimmed to 6 + nttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 6, type(uint64).max); + + address user_A = address(0x123); + address user_B = address(0x456); + DummyToken token = DummyToken(nttManager.token()); + uint8 decimals = token.decimals(); + assertEq(decimals, 18); + + token.mintDummy(address(user_A), type(uint256).max); + + vm.startPrank(user_A); + token.approve(address(nttManager), type(uint256).max); + + // When transferring to a chain with 6 decimals the amount will get trimmed to 6 decimals. + // Without rate limiting, this won't be scaled back up to 8 for local accounting. + uint256 amount = type(uint64).max * 10 ** (decimals - 6); + nttManager.transfer( + amount, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + + // However, attempting to transfer an amount higher than the destination chain can handle will revert. + amount = type(uint64).max * 10 ** (decimals - 4); + vm.expectRevert("SafeCast: value doesn't fit in 64 bits"); + nttManager.transfer( + amount, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + + // A (slightly) more sensible amount should work normally + amount = (type(uint64).max * 10 ** (decimals - 6 - 2)) - 150000000000; // Subtract this to make sure we don't have dust + nttManager.transfer( + amount, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + } + + function test_attestationQuorum() public { + address user_B = address(0x456); + + (DummyTransceiver e1, DummyTransceiver e2) = + TransceiverHelpersLib.setup_transceivers(nttManagerOther); + + TrimmedAmount transferAmount = packTrimmedAmount(50, 8); + + TransceiverStructs.NttManagerMessage memory m; + bytes memory encodedEm; + { + ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](2); + transceivers[0] = e1; + transceivers[1] = e2; + + TransceiverStructs.TransceiverMessage memory em; + (m, em) = TransceiverHelpersLib.attestTransceiversHelper( + user_B, + 0, + chainId, + nttManager, + nttManagerOther, + transferAmount, + packTrimmedAmount(type(uint64).max, 8), + transceivers + ); + encodedEm = TransceiverStructs.encodeTransceiverMessage( + TransceiverHelpersLib.TEST_TRANSCEIVER_PAYLOAD_PREFIX, em + ); + } + + { + DummyToken token = DummyToken(nttManager.token()); + assertEq(token.balanceOf(address(user_B)), transferAmount.untrim(token.decimals())); + } + + // replay protection for transceiver + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector( + IManagerBase.TransceiverAlreadyAttestedToMessage.selector, + TransceiverStructs.nttManagerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, m + ) + ) + ); + e2.receiveMessage(encodedEm); + } + + function test_transfersOnForkedChains() public { + uint256 evmChainId = block.chainid; + + address user_A = address(0x123); + address user_B = address(0x456); + + DummyToken token = DummyToken(nttManager.token()); + + uint8 decimals = token.decimals(); + + nttManager.setPeer( + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(address(nttManagerOther)), + 9, + type(uint64).max + ); + nttManager.setOutboundLimit(0); + + token.mintDummy(address(user_A), 5 * 10 ** decimals); + + vm.startPrank(user_A); + + token.approve(address(nttManager), 3 * 10 ** decimals); + + uint64 sequence = nttManager.transfer( + 1 * 10 ** decimals, + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + true, + new bytes(1) + ); + + vm.warp(vm.getBlockTimestamp() + 1 days); + + vm.chainId(chainId); + + // Queued outbound transfers can't be completed, per usual + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManager.completeOutboundQueuedTransfer(sequence); + + // Queued outbound transfers can't be cancelled, per usual + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManager.cancelOutboundQueuedTransfer(sequence); + + // Outbound transfers fail when queued + vm.expectRevert(abi.encodeWithSelector(InvalidFork.selector, evmChainId, chainId)); + nttManager.transfer( + 1 * 10 ** decimals, + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + true, + new bytes(1) + ); + vm.stopPrank(); + + nttManager.setOutboundLimit(packTrimmedAmount(type(uint64).max, 8).untrim(decimals)); + // Outbound transfers fail when not queued + vm.prank(user_A); + vm.expectRevert(abi.encodeWithSelector(InvalidFork.selector, evmChainId, chainId)); + nttManager.transfer( + 1 * 10 ** decimals, + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + + // INBOUND + + bytes memory tokenTransferMessage = TransceiverStructs.encodeNativeTokenTransfer( + TransceiverStructs.NativeTokenTransfer({ + amount: packTrimmedAmount(100, 8), + sourceToken: toWormholeFormat(address(token)), + to: toWormholeFormat(user_B), + toChain: chainId + }) + ); + + bytes memory transceiverMessage; + TransceiverStructs.NttManagerMessage memory nttManagerMessage; + (nttManagerMessage, transceiverMessage) = TransceiverHelpersLib + .buildTransceiverMessageWithNttManagerPayload( + 0, + toWormholeFormat(address(0x1)), + toWormholeFormat(address(nttManagerOther)), + toWormholeFormat(address(nttManager)), + tokenTransferMessage + ); + + // Inbound transfers can't be completed + vm.expectRevert(abi.encodeWithSelector(InvalidFork.selector, evmChainId, chainId)); + dummyTransceiver.receiveMessage(transceiverMessage); + + // Inbound queued transfers can't be completed, per usual + nttManager.setInboundLimit(0, TransceiverHelpersLib.SENDING_CHAIN_ID); + + vm.chainId(evmChainId); + + bytes32 hash = TransceiverStructs.nttManagerMessageDigest( + TransceiverHelpersLib.SENDING_CHAIN_ID, nttManagerMessage + ); + dummyTransceiver.receiveMessage(transceiverMessage); + + vm.chainId(chainId); + + vm.warp(vm.getBlockTimestamp() + 1 days); + + vm.expectRevert(abi.encodeWithSelector(IManagerBase.NotImplemented.selector)); + nttManager.completeInboundQueuedTransfer(hash); + } + + // TODO: + // currently there is no way to test the threshold logic and the duplicate + // protection logic without setting up the business logic as well. + // + // we should separate the business logic out from the transceiver handling. + // that way the functionality could be tested separately (and the contracts + // would also be more reusable) + + // === storage + + function test_noAutomaticSlot() public { + DummyToken t = new DummyToken(); + MockNttManagerNoRateLimitingContract c = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, 1); + assertEq(c.lastSlot(), 0x0); + } + + function test_constructor() public { + DummyToken t = new DummyToken(); + + vm.startStateDiffRecording(); + + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, 1); + + Utils.assertSafeUpgradeableConstructor(vm.stopAndReturnStateDiff()); + } + + // === token transfer logic + + function test_dustReverts() public { + // transfer 3 tokens + address from = address(0x123); + address to = address(0x456); + + DummyToken token = DummyToken(nttManager.token()); + + uint8 decimals = token.decimals(); + + uint256 maxAmount = 5 * 10 ** decimals; + token.mintDummy(from, maxAmount); + nttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + nttManager.setOutboundLimit(packTrimmedAmount(type(uint64).max, 8).untrim(decimals)); + nttManager.setInboundLimit( + packTrimmedAmount(type(uint64).max, 8).untrim(decimals), + TransceiverHelpersLib.SENDING_CHAIN_ID + ); + + vm.startPrank(from); + + uint256 transferAmount = 3 * 10 ** decimals; + assertEq( + transferAmount < maxAmount - 500, true, "Transferring more tokens than what exists" + ); + + uint256 dustAmount = 500; + uint256 amountWithDust = transferAmount + dustAmount; // An amount with 19 digits, which will result in dust due to 18 decimals + token.approve(address(nttManager), amountWithDust); + + vm.expectRevert( + abi.encodeWithSelector( + INttManager.TransferAmountHasDust.selector, amountWithDust, dustAmount + ) + ); + nttManager.transfer( + amountWithDust, + chainId2, + toWormholeFormat(to), + toWormholeFormat(from), + false, + new bytes(1) + ); + + vm.stopPrank(); + } + + // === upgradeability + function expectRevert( + address contractAddress, + bytes memory encodedSignature, + bytes memory expectedRevert + ) internal { + (bool success, bytes memory result) = contractAddress.call(encodedSignature); + require(!success, "call did not revert"); + + require(keccak256(result) == keccak256(expectedRevert), "call did not revert as expected"); + } + + function test_upgradeNttManagerNoRateLimiting() public { + // The testing strategy here is as follows: + // Step 1: Deploy the nttManager contract with two transceivers and + // receive a message through it. + // Step 2: Upgrade it to a new nttManager contract an use the same transceivers to receive + // a new message through it. + // Step 3: Upgrade back to the standalone contract (with two + // transceivers) and receive a message through it. + // This ensures that the storage slots don't get clobbered through the upgrades. + + address user_B = address(0x456); + DummyToken token = DummyToken(nttManager.token()); + TrimmedAmount transferAmount = packTrimmedAmount(50, 8); + (ITransceiverReceiver e1, ITransceiverReceiver e2) = + TransceiverHelpersLib.setup_transceivers(nttManagerOther); + + // Step 1 (contract is deployed by setUp()) + ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](2); + transceivers[0] = e1; + transceivers[1] = e2; + + TransceiverStructs.NttManagerMessage memory m; + bytes memory encodedEm; + { + TransceiverStructs.TransceiverMessage memory em; + (m, em) = TransceiverHelpersLib.attestTransceiversHelper( + user_B, + 0, + chainId, + nttManager, + nttManagerOther, + transferAmount, + packTrimmedAmount(type(uint64).max, 8), + transceivers + ); + encodedEm = TransceiverStructs.encodeTransceiverMessage( + TransceiverHelpersLib.TEST_TRANSCEIVER_PAYLOAD_PREFIX, em + ); + } + + assertEq(token.balanceOf(address(user_B)), transferAmount.untrim(token.decimals())); + + // Step 2 (upgrade to a new nttManager) + MockNttManagerNoRateLimitingContract newNttManagerNoRateLimiting = new MockNttManagerNoRateLimitingContract( + nttManager.token(), IManagerBase.Mode.LOCKING, chainId + ); + nttManagerOther.upgrade(address(newNttManagerNoRateLimiting)); + + TransceiverHelpersLib.attestTransceiversHelper( + user_B, + bytes32(uint256(1)), + chainId, + nttManager, // this is the proxy + nttManagerOther, // this is the proxy + transferAmount, + packTrimmedAmount(type(uint64).max, 8), + transceivers + ); + + assertEq(token.balanceOf(address(user_B)), transferAmount.untrim(token.decimals()) * 2); + } + + // NOTE: There are additional tests in `Upgrades.t.sol` to verifying upgrading from `NttManager` to `NttManagerNoRateLimiting`. + + function test_canUpgradeFromNoRateLimitingToRateLimitingDisabled() public { + // Create a standard manager with rate limiting disabled. + DummyToken t = new DummyToken(); + NttManager implementation = + new MockNttManagerContract(address(t), IManagerBase.Mode.LOCKING, chainId, 0, true); + + MockNttManagerContract thisNttManager = + MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); + thisNttManager.initialize(); + + thisNttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + + // Upgrade from NttManagerNoRateLimiting to NttManager with rate limiting enabled. This should work. + NttManager rateLimitingImplementation = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + + thisNttManager.upgrade(address(rateLimitingImplementation)); + } + + function test_cannotUpgradeFromNoRateLimitingToRateLimitingEnaabled() public { + // Create a standard manager with rate limiting enabled. + DummyToken t = new DummyToken(); + NttManager implementation = new MockNttManagerContract( + address(t), IManagerBase.Mode.LOCKING, chainId, 1 days, false + ); + + MockNttManagerContract thisNttManager = + MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); + thisNttManager.initialize(); + + thisNttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + + // Upgrade from NttManagerNoRateLimiting to NttManager with rate limiting enabled. The immutable check should panic. + NttManager rateLimitingImplementation = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + + vm.expectRevert(); // Reverts with a panic on the assert. So, no way to tell WHY this happened. + thisNttManager.upgrade(address(rateLimitingImplementation)); + } + + function test_tokenUpgradedAndDecimalsChanged() public { + DummyToken dummy1 = new DummyTokenMintAndBurn(); + + // Make the token an upgradeable token + DummyTokenMintAndBurn t = + DummyTokenMintAndBurn(address(new ERC1967Proxy(address(dummy1), ""))); + + NttManagerNoRateLimiting implementation = + new MockNttManagerNoRateLimitingContract(address(t), IManagerBase.Mode.LOCKING, chainId); + + MockNttManagerNoRateLimitingContract newNttManagerNoRateLimiting = + MockNttManagerNoRateLimitingContract(address(new ERC1967Proxy(address(implementation), ""))); + newNttManagerNoRateLimiting.initialize(); + + // register nttManager peer and transceiver + bytes32 peer = toWormholeFormat(address(nttManager)); + newNttManagerNoRateLimiting.setPeer( + TransceiverHelpersLib.SENDING_CHAIN_ID, peer, 9, type(uint64).max + ); + { + DummyTransceiver e = new DummyTransceiver(address(newNttManagerNoRateLimiting)); + newNttManagerNoRateLimiting.setTransceiver(address(e)); + } + + address user_A = address(0x123); + address user_B = address(0x456); + t.mintDummy(address(user_A), 5 * 10 ** t.decimals()); + + // Check that we can initiate a transfer + vm.startPrank(user_A); + t.approve(address(newNttManagerNoRateLimiting), 3 * 10 ** t.decimals()); + newNttManagerNoRateLimiting.transfer( + 1 * 10 ** t.decimals(), + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + vm.stopPrank(); + + // Check that we can receive a transfer + (DummyTransceiver e1,) = + TransceiverHelpersLib.setup_transceivers(newNttManagerNoRateLimiting); + newNttManagerNoRateLimiting.setThreshold(1); + + bytes memory transceiverMessage; + bytes memory tokenTransferMessage; + + TrimmedAmount transferAmount = packTrimmedAmount(100, 8); + + tokenTransferMessage = TransceiverStructs.encodeNativeTokenTransfer( + TransceiverStructs.NativeTokenTransfer({ + amount: transferAmount, + sourceToken: toWormholeFormat(address(t)), + to: toWormholeFormat(user_B), + toChain: chainId + }) + ); + + (, transceiverMessage) = TransceiverHelpersLib.buildTransceiverMessageWithNttManagerPayload( + 0, + bytes32(0), + peer, + toWormholeFormat(address(newNttManagerNoRateLimiting)), + tokenTransferMessage + ); + + e1.receiveMessage(transceiverMessage); + uint256 userBExpectedBalance = transferAmount.untrim(t.decimals()); + assertEq(t.balanceOf(address(user_B)), userBExpectedBalance); + + // If the token decimals change to the same trimmed amount, we should safely receive the correct number of tokens + DummyTokenDifferentDecimals dummy2 = new DummyTokenDifferentDecimals(10); // 10 gets trimmed to 8 + t.upgrade(address(dummy2)); + + vm.startPrank(user_A); + newNttManagerNoRateLimiting.transfer( + 1 * 10 ** 10, + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + vm.stopPrank(); + + (, transceiverMessage) = TransceiverHelpersLib.buildTransceiverMessageWithNttManagerPayload( + bytes32("1"), + bytes32(0), + peer, + toWormholeFormat(address(newNttManagerNoRateLimiting)), + tokenTransferMessage + ); + e1.receiveMessage(transceiverMessage); + userBExpectedBalance = userBExpectedBalance + transferAmount.untrim(t.decimals()); + assertEq(t.balanceOf(address(user_B)), userBExpectedBalance); + + // If the token decimals change to a different trimmed amount, we should still be able + // to send and receive, as this only errored in the RateLimiter. + DummyTokenDifferentDecimals dummy3 = new DummyTokenDifferentDecimals(7); // 7 is 7 trimmed + t.upgrade(address(dummy3)); + + vm.startPrank(user_A); + newNttManagerNoRateLimiting.transfer( + 1 * 10 ** 7, + TransceiverHelpersLib.SENDING_CHAIN_ID, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + new bytes(1) + ); + vm.stopPrank(); + + (, transceiverMessage) = TransceiverHelpersLib.buildTransceiverMessageWithNttManagerPayload( + bytes32("2"), + bytes32(0), + peer, + toWormholeFormat(address(newNttManagerNoRateLimiting)), + tokenTransferMessage + ); + e1.receiveMessage(transceiverMessage); + userBExpectedBalance = userBExpectedBalance + transferAmount.untrim(t.decimals()); + assertEq(t.balanceOf(address(user_B)), userBExpectedBalance); + } + + function test_transferWithInstructionIndexOutOfBounds() public { + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction = + TransceiverStructs.TransceiverInstruction({index: 100, payload: new bytes(1)}); + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](1); + TransceiverInstructions[0] = TransceiverInstruction; + bytes memory encodedInstructions = + TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + + address user_A = address(0x123); + address user_B = address(0x456); + + DummyToken token = DummyToken(nttManager.token()); + + uint8 decimals = token.decimals(); + + nttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + nttManager.setOutboundLimit(packTrimmedAmount(type(uint64).max, 8).untrim(decimals)); + + token.mintDummy(address(user_A), 5 * 10 ** decimals); + + vm.startPrank(user_A); + + token.approve(address(nttManager), 3 * 10 ** decimals); + + vm.expectRevert( + abi.encodeWithSelector(TransceiverStructs.InvalidInstructionIndex.selector, 100, 1) + ); + nttManager.transfer( + 1 * 10 ** decimals, + chainId2, + toWormholeFormat(user_B), + toWormholeFormat(user_A), + false, + encodedInstructions + ); + } +} diff --git a/evm/test/PerChainTransceivers.t.sol b/evm/test/PerChainTransceivers.t.sol new file mode 100755 index 000000000..3a3f05198 --- /dev/null +++ b/evm/test/PerChainTransceivers.t.sol @@ -0,0 +1,624 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManagerNoRateLimiting.sol"; +import "../src/Transceiver/Transceiver.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/ITransceiver.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; +import "../src/interfaces/IWormholeTransceiver.sol"; +import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "./mocks/MockNttManager.sol"; +import "./mocks/MockTransceivers.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +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 TestPerChainTransceivers is Test, IRateLimiterEvents { + MockNttManagerNoRateLimitingContractForTest nttManagerChain1; + NttManagerNoRateLimiting nttManagerChain2; + NttManagerNoRateLimiting nttManagerChain3; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + uint16 constant chainId1 = 7; + uint16 constant chainId2 = 100; + uint16 constant chainId3 = 101; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + + uint16 constant SENDING_CHAIN_ID = 1; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + WormholeTransceiver wormholeTransceiverChain1; + WormholeTransceiver secondWormholeTransceiverChain1; + WormholeTransceiver wormholeTransceiverChain2; + WormholeTransceiver secondWormholeTransceiverChain2; + WormholeTransceiver wormholeTransceiverChain3; + WormholeTransceiver secondWormholeTransceiverChain3; + address userA = address(0x123); + address userB = address(0x456); + address userC = address(0x789); + address userD = address(0xABC); + + address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + + // This function sets up the following config: + // - A manager on each of three chains. + // - Two transceivers on each chain, all interconnected as peers. + // - On chain one, it sets a default threshold of one and a per-chain threshold of two for chain three. + // - On chain three, it sets a default threshold of one and a per-chain threshold of two for chain one. + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + vm.chainId(chainId1); + DummyToken t1 = new DummyToken(); + NttManager implementation = new MockNttManagerNoRateLimitingContractForTest( + address(t1), IManagerBase.Mode.LOCKING, chainId1 + ); + + nttManagerChain1 = MockNttManagerNoRateLimitingContractForTest( + address(new ERC1967Proxy(address(implementation), "")) + ); + nttManagerChain1.initialize(); + + // Create the first transceiver, from chain 1 to chain 2. + WormholeTransceiver wormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain1Implementation), "")) + ); + + // Only the deployer should be able to initialize + vm.prank(userA); + vm.expectRevert( + abi.encodeWithSelector(ITransceiver.UnexpectedDeployer.selector, address(this), userA) + ); + wormholeTransceiverChain1.initialize(); + + // Actually initialize properly now + wormholeTransceiverChain1.initialize(); + + nttManagerChain1.setTransceiver(address(wormholeTransceiverChain1)); + + // Create the second transceiver for chain 1. + WormholeTransceiver secondWormholeTransceiverChain1Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain1 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain1Implementation), "")) + ); + + secondWormholeTransceiverChain1.initialize(); + nttManagerChain1.setTransceiver(address(secondWormholeTransceiverChain1)); + + // Chain 2 setup + vm.chainId(chainId2); + DummyToken t2 = new DummyTokenMintAndBurn(); + NttManager implementationChain2 = new MockNttManagerNoRateLimitingContract( + address(t2), IManagerBase.Mode.BURNING, chainId2 + ); + + nttManagerChain2 = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementationChain2), "")) + ); + nttManagerChain2.initialize(); + + WormholeTransceiver wormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain2Implementation), "")) + ); + wormholeTransceiverChain2.initialize(); + + nttManagerChain2.setTransceiver(address(wormholeTransceiverChain2)); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + nttManagerChain2.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Create the second transceiver for chain 2. + WormholeTransceiver secondWormholeTransceiverChain2Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain2 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain2Implementation), "")) + ); + + secondWormholeTransceiverChain2.initialize(); + nttManagerChain2.setTransceiver(address(secondWormholeTransceiverChain2)); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + secondWormholeTransceiverChain1.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) + ); + secondWormholeTransceiverChain2.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) + ); + + // Chain 3 setup + vm.chainId(chainId3); + DummyToken t3 = new DummyTokenMintAndBurn(); + NttManager implementationChain3 = new MockNttManagerNoRateLimitingContract( + address(t3), IManagerBase.Mode.BURNING, chainId3 + ); + + nttManagerChain3 = MockNttManagerNoRateLimitingContract( + address(new ERC1967Proxy(address(implementationChain3), "")) + ); + nttManagerChain3.initialize(); + + WormholeTransceiver wormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain3), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + wormholeTransceiverChain3 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(wormholeTransceiverChain3Implementation), "")) + ); + wormholeTransceiverChain3.initialize(); + + nttManagerChain3.setTransceiver(address(wormholeTransceiverChain3)); + + // Register peer contracts for the nttManager and transceiver. Transceivers and nttManager each have the concept of peers here. + nttManagerChain1.setPeer( + chainId3, bytes32(uint256(uint160(address(nttManagerChain3)))), 9, type(uint64).max + ); + nttManagerChain3.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Create the second transceiver, from chain 3 to chain 1. + WormholeTransceiver secondWormholeTransceiverChain3Implementation = new MockWormholeTransceiverContract( + address(nttManagerChain3), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + secondWormholeTransceiverChain3 = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(secondWormholeTransceiverChain3Implementation), "")) + ); + + // Actually initialize properly now + secondWormholeTransceiverChain3.initialize(); + + nttManagerChain3.setTransceiver(address(secondWormholeTransceiverChain3)); + + // Set peers for the transceivers + wormholeTransceiverChain1.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) + ); + wormholeTransceiverChain3.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(wormholeTransceiverChain1)))) + ); + wormholeTransceiverChain3.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(wormholeTransceiverChain2)))) + ); + wormholeTransceiverChain2.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(wormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain1.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain3.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(secondWormholeTransceiverChain1)))) + ); + secondWormholeTransceiverChain2.setWormholePeer( + chainId3, bytes32(uint256(uint160(address(secondWormholeTransceiverChain3)))) + ); + secondWormholeTransceiverChain3.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(secondWormholeTransceiverChain2)))) + ); + + // Set the thresholds. + require(nttManagerChain1.getThreshold() != 0, "Threshold is zero with active transceivers"); + + // Actually set it + nttManagerChain1.setThreshold(1); + nttManagerChain2.setThreshold(1); + nttManagerChain3.setThreshold(1); + + // On chain 1, set the threshold to chain 3 to be different. + nttManagerChain1.setPerChainThreshold(chainId3, 2); + nttManagerChain3.setPerChainThreshold(chainId1, 2); + + require(nttManagerChain1.getThreshold() == 1, "Default threshold is wrong"); + require( + nttManagerChain1.getPerChainThreshold(chainId3) == 2, "Threshold for chain 3 is wrong" + ); + + // Since we haven't set the per-chain threshold for chain 2, it should return the default + require( + nttManagerChain1.getPerChainThreshold(chainId2) == 1, "Threshold for chain 2 is wrong" + ); + } + + function test_transceiverSetters() public { + // If we haven't enabled per-chain send transceivers for this chain, the getter should return true. + require( + nttManagerChain1.isSendTransceiverEnabledForChain(address(nttManagerChain3), chainId3), + "Send transceiver should be enabled by default" + ); + + // If we haven't enabled per-chain receive transceivers, the getter should return everything is enabled. + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId3) == type(uint64).max, + "Receive transceiver should be enabled by default" + ); + + nttManagerChain1.enableSendTransceiverForChain(address(wormholeTransceiverChain1), chainId2); + nttManagerChain1.enableRecvTransceiverForChain( + address(secondWormholeTransceiverChain1), chainId3 + ); + + // Once we enable a transceiver for a chain, that is the only one enabled for that chain. + require( + nttManagerChain1.isSendTransceiverEnabledForChain( + address(wormholeTransceiverChain1), chainId2 + ), + "First transceiver should be enabled for sending on chain 2" + ); + require( + !nttManagerChain1.isSendTransceiverEnabledForChain( + address(secondWormholeTransceiverChain1), chainId2 + ), + "Second transceiver should not be enabled for sending on chain 2" + ); + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId3) == 0x2, + "Only second transceiver should be enabled for receiving on chain 3" + ); + + // And for chains we didn't touch, the defaults should still be in place. + require( + nttManagerChain1.isSendTransceiverEnabledForChain(address(nttManagerChain3), chainId3), + "Send transceiver should be enabled by default for untouched chain" + ); + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId2) == type(uint64).max, + "Receive transceiver should be enabled by default for untouched chain" + ); + } + + // This test does a transfer between chain one and chain two. + // Since chain two uses the default threshold, posting a VAA from only one transceiver completes the transfer. + function test_defaultThreshold() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + + // Transfer tokens from chain one to chain two through standard means (not relayer) + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + vm.recordLogs(); + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 1"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain2 verification and checks + vm.chainId(chainId2); + + { + uint256 supplyBefore = token2.totalSupply(); + wormholeTransceiverChain2.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" + ); + } + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManager didn't receive unintended funds" + ); + } + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 2"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + { + uint256 supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + + //////////////////////// receive message 1. + } + + // This test does a transfer between chain one and chain three. + // Since the threshold for these two chains is two, the transfer is not completed until both VAAs are posted. + function test_perChainThreshold() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token3 = DummyTokenMintAndBurn(nttManagerChain3.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + + vm.recordLogs(); + + // Send token from chain 1 to chain 3, userB. + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId3, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get and sign the log to go down the other pipes. There should be two messages since we have two transceivers. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 3"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain3 verification and checks + vm.chainId(chainId3); + + uint256 supplyBefore = token3.totalSupply(); + + // Submit the first message on chain 3. The numbers shouldn't change yet since the threshold is two. + wormholeTransceiverChain3.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token3.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies changed early"); + require(token3.balanceOf(userB) == 0, "User receive tokens early"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Submit the second message and the transfer should complete. + secondWormholeTransceiverChain3.receiveMessage(encodedVMs[1]); + supplyAfter = token3.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token3.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require(token3.balanceOf(address(nttManagerChain3)) == 0, "NttManager has unintended funds"); + + // Go back the other way from a THIRD user + vm.prank(userB); + token3.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token3.approve(address(nttManagerChain3), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + supplyBefore = token3.totalSupply(); + nttManagerChain3.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + supplyAfter = token3.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token3.balanceOf(userB) == 0, "OG user receive tokens"); + require(token3.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token3.balanceOf(address(nttManagerChain3)) == 0, + "NttManager didn't receive unintended funds" + ); + + // Get and sign the log to go down the other pipe. Thank you to whoever wrote this code in the past! + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries for response"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId3); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + // Submit the first message back on chain one. Nothing should happen because our threshold is two. + supplyBefore = token1.totalSupply(); + wormholeTransceiverChain1.receiveMessage(encodedVMs[0]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == 0, "User received funds before they should"); + + // Submit the second message back on chain one. This should update the balance. + supplyBefore = token1.totalSupply(); + secondWormholeTransceiverChain1.receiveMessage(encodedVMs[1]); + supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); /////////////////// Is this right?? + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + + function encodeTransceiverInstruction( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.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); + } + + // Encode an instruction for each of the relayers + function encodeTransceiverInstructions( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + + bytes memory encodedInstructionWormhole = + wormholeTransceiverChain1.encodeWormholeTransceiverInstruction(instruction); + + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = + TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = + TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); + + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](2); + + TransceiverInstructions[0] = TransceiverInstruction1; + TransceiverInstructions[1] = TransceiverInstruction2; + + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } +} diff --git a/evm/test/PerChainTransceiversDemo.t.sol b/evm/test/PerChainTransceiversDemo.t.sol new file mode 100755 index 000000000..eb7f575b5 --- /dev/null +++ b/evm/test/PerChainTransceiversDemo.t.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/NttManager/NttManagerNoRateLimiting.sol"; +import "../src/Transceiver/Transceiver.sol"; +import "../src/interfaces/INttManager.sol"; +import "../src/interfaces/IRateLimiter.sol"; +import "../src/interfaces/ITransceiver.sol"; +import "../src/interfaces/IManagerBase.sol"; +import "../src/interfaces/IRateLimiterEvents.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DummyToken, DummyTokenMintAndBurn} from "./NttManager.t.sol"; +import "../src/interfaces/IWormholeTransceiver.sol"; +import {WormholeTransceiver} from "../src/Transceiver/WormholeTransceiver/WormholeTransceiver.sol"; +import "../src/libraries/TransceiverStructs.sol"; +import "./mocks/MockNttManager.sol"; +import "./mocks/MockTransceivers.sol"; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +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"; + +// This contract demonstrates how per-chain transceivers could be used based on the following configuration. +// +// Chain1 ↔ Chain2 +// +// 1. Chain1 → Chain2 +// a. Send (on Chain1): via both transceivers +// b. Receive (on Chain2): Require 2-of-2, both transceivers +// 2. Chain2 → Chain1 +// a. Send (on Chain2): via first transceiver only +// b. Receive (on Chain1): require first transceiver only (1-of-?) +// +// For this test, we will use the mock wormhole transceiver for both, but will configure separate ones +// with the appropriate thresholds. We will then do a transfer. + +contract TestPerChainTransceiversDemo is Test, IRateLimiterEvents { + MockNttManagerNoRateLimitingContractForTest nttManagerChain1; + MockNttManagerNoRateLimitingContractForTest nttManagerChain2; + + using TrimmedAmountLib for uint256; + using TrimmedAmountLib for TrimmedAmount; + + uint16 constant chainId1 = 101; + uint16 constant chainId2 = 102; + uint8 constant FAST_CONSISTENCY_LEVEL = 200; + uint256 constant GAS_LIMIT = 500000; + + uint16 constant SENDING_CHAIN_ID = 2; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + WormholeSimulator guardian; + uint256 initialBlockTimestamp; + + WormholeTransceiver chainOneFirstTransceiver; + WormholeTransceiver chainOneSecondTransceiver; + WormholeTransceiver chainTwoFirstTransceiver; + WormholeTransceiver chainTwoSecondTransceiver; + + address userA = address(0x123); + address userB = address(0x456); + address userC = address(0x789); + address userD = address(0xABC); + + address relayer = address(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a); + IWormhole wormhole = IWormhole(0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78); + + function setUp() public { + string memory url = "https://ethereum-sepolia-rpc.publicnode.com"; + vm.createSelectFork(url); + initialBlockTimestamp = vm.getBlockTimestamp(); + + guardian = new WormholeSimulator(address(wormhole), DEVNET_GUARDIAN_PK); + + // Set up the manager on chain one. ////////////////////////////////////////////////////////////////// + vm.chainId(chainId1); + DummyToken t1 = new DummyToken(); + NttManager implementation = new MockNttManagerNoRateLimitingContractForTest( + address(t1), IManagerBase.Mode.LOCKING, chainId1 + ); + + nttManagerChain1 = MockNttManagerNoRateLimitingContractForTest( + address(new ERC1967Proxy(address(implementation), "")) + ); + nttManagerChain1.initialize(); + + // Create the first transceiver on chain one. + WormholeTransceiver chainOneFirstTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainOneFirstTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainOneFirstTransceiverImplementation), "")) + ); + + chainOneFirstTransceiver.initialize(); + nttManagerChain1.setTransceiver(address(chainOneFirstTransceiver)); + + // Create the second transceiver for chain one. + WormholeTransceiver chainOneSecondTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain1), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainOneSecondTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainOneSecondTransceiverImplementation), "")) + ); + + chainOneSecondTransceiver.initialize(); + nttManagerChain1.setTransceiver(address(chainOneSecondTransceiver)); + + // Set up the manager on chain two. ////////////////////////////////////////////////////////////////// + vm.chainId(chainId2); + DummyToken t2 = new DummyTokenMintAndBurn(); + NttManager implementationChain2 = new MockNttManagerNoRateLimitingContractForTest( + address(t2), IManagerBase.Mode.BURNING, chainId2 + ); + + nttManagerChain2 = MockNttManagerNoRateLimitingContractForTest( + address(new ERC1967Proxy(address(implementationChain2), "")) + ); + nttManagerChain2.initialize(); + + // Create the first transceiver on chain two. + WormholeTransceiver chainTwoFirstTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainTwoFirstTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainTwoFirstTransceiverImplementation), "")) + ); + chainTwoFirstTransceiver.initialize(); + + nttManagerChain2.setTransceiver(address(chainTwoFirstTransceiver)); + + // Create the second transceiver on chain two. + WormholeTransceiver chainTwoSecondTransceiverImplementation = new MockWormholeTransceiverContract( + address(nttManagerChain2), + address(wormhole), + address(relayer), + address(0x0), + FAST_CONSISTENCY_LEVEL, + GAS_LIMIT + ); + chainTwoSecondTransceiver = MockWormholeTransceiverContract( + address(new ERC1967Proxy(address(chainTwoSecondTransceiverImplementation), "")) + ); + + chainTwoSecondTransceiver.initialize(); + nttManagerChain2.setTransceiver(address(chainTwoSecondTransceiver)); + + // Register the two NTT manager peers. ////////////////////////////////////////////////////////////////// + nttManagerChain1.setPeer( + chainId2, bytes32(uint256(uint160(address(nttManagerChain2)))), 9, type(uint64).max + ); + nttManagerChain2.setPeer( + chainId1, bytes32(uint256(uint160(address(nttManagerChain1)))), 7, type(uint64).max + ); + + // Register the transceiver peers. ////////////////////////////////////////////////////////////////// + chainOneFirstTransceiver.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(chainTwoFirstTransceiver)))) + ); + chainTwoFirstTransceiver.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(chainOneFirstTransceiver)))) + ); + chainOneSecondTransceiver.setWormholePeer( + chainId2, bytes32(uint256(uint160(address(chainTwoSecondTransceiver)))) + ); + chainTwoSecondTransceiver.setWormholePeer( + chainId1, bytes32(uint256(uint160(address(chainOneSecondTransceiver)))) + ); + + // Set the default thresholds. ////////////////////////////////////////////////////////////////////// + nttManagerChain1.setThreshold(2); + nttManagerChain2.setThreshold(2); + + // Set up our per-chain transceivers and thresholds. //////////////////////////////////////////////// + + // Set up chain one. + // 1.a: + nttManagerChain1.enableSendTransceiverForChain(address(chainOneFirstTransceiver), chainId2); + nttManagerChain1.enableSendTransceiverForChain(address(chainOneSecondTransceiver), chainId2); + // 2.b: + nttManagerChain1.enableRecvTransceiverForChain(address(chainOneFirstTransceiver), chainId2); + nttManagerChain1.setPerChainThreshold(chainId2, 1); + + // Set up chain two. + // 2.a: + nttManagerChain2.enableSendTransceiverForChain(address(chainTwoFirstTransceiver), chainId1); + // 1.b: + nttManagerChain2.enableRecvTransceiverForChain(address(chainTwoFirstTransceiver), chainId1); + nttManagerChain2.enableRecvTransceiverForChain(address(chainTwoSecondTransceiver), chainId1); + nttManagerChain2.setPerChainThreshold(chainId1, 2); + } + + function test_verifyConfig() public view { + // Verify config of chain one. ////////////////////////////////////////////////////////// + require( + nttManagerChain1.isSendTransceiverEnabledForChain( + address(chainOneFirstTransceiver), chainId2 + ), + "On chain 1, first transceiver should be enabled for sending to chain 2" + ); + require( + nttManagerChain1.isSendTransceiverEnabledForChain( + address(chainOneFirstTransceiver), chainId2 + ), + "On chain 1, second transceiver should be enabled for sending to chain 2" + ); + require( + nttManagerChain1.getEnabledRecvTransceiversForChain(chainId2) == 0x1, + "On chain 1, only first transceiver should be enabled for receiving from chain 2" + ); + require(nttManagerChain1.getThreshold() == 2, "On chain 1, the default threshold is wrong"); + require( + nttManagerChain1.getPerChainThreshold(chainId2) == 1, + "On chain 1, threshold for chain 2 is wrong" + ); + + // Verify config of chain two. ////////////////////////////////////////////////////////// + require( + nttManagerChain2.isSendTransceiverEnabledForChain( + address(chainTwoFirstTransceiver), chainId1 + ), + "On chain 2, first transceiver should be enabled for sending to chain 1" + ); + require( + !nttManagerChain2.isSendTransceiverEnabledForChain( + address(chainTwoSecondTransceiver), chainId1 + ), + "On chain 2, second transceiver should be not enabled for sending to chain 1" + ); + require( + nttManagerChain2.getEnabledRecvTransceiversForChain(chainId1) == 0x3, + "On chain 2, both transceivers should be enabled for receiving from chain 1" + ); + require(nttManagerChain2.getThreshold() == 2, "On chain 2, the default threshold is wrong"); + require( + nttManagerChain2.getPerChainThreshold(chainId1) == 2, + "On chain 2, threshold for chain 1 is wrong" + ); + } + + function test_transfer() public { + vm.chainId(chainId1); + + // Setting up the transfer + DummyToken token1 = DummyToken(nttManagerChain1.token()); + DummyToken token2 = DummyTokenMintAndBurn(nttManagerChain2.token()); + + uint8 decimals = token1.decimals(); + uint256 sendingAmount = 5 * 10 ** decimals; + token1.mintDummy(address(userA), 5 * 10 ** decimals); + + // Transfer tokens from chain one to chain two through standard means (not relayer) + vm.startPrank(userA); + token1.approve(address(nttManagerChain1), sendingAmount); + vm.recordLogs(); + { + uint256 nttManagerBalanceBefore = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceBefore = token1.balanceOf(address(userA)); + nttManagerChain1.transfer(sendingAmount, chainId2, bytes32(uint256(uint160(userB)))); + + // Balance check on funds going in and out working as expected + uint256 nttManagerBalanceAfter = token1.balanceOf(address(nttManagerChain1)); + uint256 userBalanceAfter = token1.balanceOf(address(userB)); + require( + nttManagerBalanceBefore + sendingAmount == nttManagerBalanceAfter, + "Should be locking the tokens" + ); + require( + userBalanceBefore - sendingAmount == userBalanceAfter, + "User should have sent tokens" + ); + } + + vm.stopPrank(); + + // Get the logs. There should be two messages going from chain 1 to chain 2. + Vm.Log[] memory entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(2 == entries.length, "Unexpected number of log entries 1"); + bytes[] memory encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId1); + } + + // Chain2 verification and checks + vm.chainId(chainId2); + + { + uint256 supplyBefore = token2.totalSupply(); + + // Receiving the first VAA on the first transceiver shouldn't do anything. + chainTwoFirstTransceiver.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token2.totalSupply(); + require(supplyBefore == supplyAfter, "It looks like the transfer happened too soon"); + + // Receiving the first VAA on the second transceiver should revert. + vm.expectRevert( + abi.encodeWithSelector( + IWormholeTransceiver.InvalidWormholePeer.selector, + chainId1, + chainOneFirstTransceiver + ) + ); + chainTwoSecondTransceiver.receiveMessage(encodedVMs[0]); + + // Receiving the second VAA on the second one should complete the transfer. + chainTwoSecondTransceiver.receiveMessage(encodedVMs[1]); + supplyAfter = token2.totalSupply(); + + require(sendingAmount + supplyBefore == supplyAfter, "Supplies dont match"); + require(token2.balanceOf(userB) == sendingAmount, "User didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, "NttManager has unintended funds" + ); + } + + // Go back the other way from a THIRD user + vm.prank(userB); + token2.transfer(userC, sendingAmount); + + vm.startPrank(userC); + token2.approve(address(nttManagerChain2), sendingAmount); + vm.recordLogs(); + + // Supply checks on the transfer + { + uint256 supplyBefore = token2.totalSupply(); + nttManagerChain2.transfer( + sendingAmount, + chainId1, + toWormholeFormat(userD), + toWormholeFormat(userC), + false, + encodeTransceiverInstruction(true) + ); + + uint256 supplyAfter = token2.totalSupply(); + + require(sendingAmount - supplyBefore == supplyAfter, "Supplies don't match"); + require(token2.balanceOf(userB) == 0, "OG user receive tokens"); + require(token2.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require( + token2.balanceOf(address(nttManagerChain2)) == 0, + "NttManager didn't receive unintended funds" + ); + } + + // Get the logs. There should only be one message going from chain 2 to chain 1. + entries = guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs()); + require(1 == entries.length, "Unexpected number of log entries 2"); + encodedVMs = new bytes[](entries.length); + for (uint256 i = 0; i < encodedVMs.length; i++) { + encodedVMs[i] = guardian.fetchSignedMessageFromLogs(entries[i], chainId2); + } + + // Chain1 verification and checks with the receiving of the message + vm.chainId(chainId1); + + { + // Receiving a single VAA should do the trick. + uint256 supplyBefore = token1.totalSupply(); + chainOneFirstTransceiver.receiveMessage(encodedVMs[0]); + uint256 supplyAfter = token1.totalSupply(); + + require(supplyBefore == supplyAfter, "Supplies don't match between operations"); + require(token1.balanceOf(userB) == 0, "OG user receive tokens"); + require(token1.balanceOf(userC) == 0, "Sending user didn't receive tokens"); + require(token1.balanceOf(userD) == sendingAmount, "User received funds"); + } + } + + function encodeTransceiverInstruction( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + bytes memory encodedInstructionWormhole = + chainOneFirstTransceiver.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); + } + + // Encode an instruction for each of the relayers + function encodeTransceiverInstructions( + bool relayer_off + ) public view returns (bytes memory) { + WormholeTransceiver.WormholeTransceiverInstruction memory instruction = + IWormholeTransceiver.WormholeTransceiverInstruction(relayer_off); + + bytes memory encodedInstructionWormhole = + chainOneFirstTransceiver.encodeWormholeTransceiverInstruction(instruction); + + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction1 = + TransceiverStructs.TransceiverInstruction({index: 0, payload: encodedInstructionWormhole}); + TransceiverStructs.TransceiverInstruction memory TransceiverInstruction2 = + TransceiverStructs.TransceiverInstruction({index: 1, payload: encodedInstructionWormhole}); + + TransceiverStructs.TransceiverInstruction[] memory TransceiverInstructions = + new TransceiverStructs.TransceiverInstruction[](2); + + TransceiverInstructions[0] = TransceiverInstruction1; + TransceiverInstructions[1] = TransceiverInstruction2; + + return TransceiverStructs.encodeTransceiverInstructions(TransceiverInstructions); + } +} diff --git a/evm/test/Upgrades.t.sol b/evm/test/Upgrades.t.sol index 9d04ef61e..6819ee279 100644 --- a/evm/test/Upgrades.t.sol +++ b/evm/test/Upgrades.t.sol @@ -184,6 +184,51 @@ contract TestUpgrades is Test, IRateLimiterEvents { basicFunctionality(); } + // NOTE: There are additional tests in `Upgrades.t.sol` to verifying downgrading from `NttManagerNoRateLimiting` to `NttManager`. + + function test_cannotUpgradeToNoRateLimitingIfItWasEnabled() public { + // The default set up has rate limiting enabled. When we attempt to upgrade to no rate limiting, the immutable check should panic. + NttManager rateLimitingImplementation = new MockNttManagerNoRateLimitingContract( + address(nttManagerChain1.token()), IManagerBase.Mode.LOCKING, chainId1 + ); + + vm.expectRevert(); // Reverts with a panic on the assert. So, no way to tell WHY this happened. + nttManagerChain1.upgrade(address(rateLimitingImplementation)); + } + + function test_upgradeToNoRateLimiting() public { + // Create a standard manager with rate limiting disabled. + DummyToken t = new DummyToken(); + NttManager implementation = + new MockNttManagerContract(address(t), IManagerBase.Mode.LOCKING, chainId1, 0, true); + + MockNttManagerContract thisNttManager = + MockNttManagerContract(address(new ERC1967Proxy(address(implementation), ""))); + thisNttManager.initialize(); + + thisNttManager.setPeer(chainId2, toWormholeFormat(address(0x1)), 9, type(uint64).max); + + // Upgrade from NttManager with rate limiting disabled to NttManagerNoRateLimiting. + NttManager rateLimitingImplementation = new MockNttManagerNoRateLimitingContract( + address(t), IManagerBase.Mode.LOCKING, chainId1 + ); + thisNttManager.upgrade(address(rateLimitingImplementation)); + basicFunctionality(); + + // Upgrade from NttManagerNoRateLimiting to NttManagerNoRateLimiting. + rateLimitingImplementation = new MockNttManagerNoRateLimitingContract( + address(t), IManagerBase.Mode.LOCKING, chainId1 + ); + thisNttManager.upgrade(address(rateLimitingImplementation)); + basicFunctionality(); + + // Upgrade from NttManagerNoRateLimiting back to NttManager. + NttManager nttManagerImplementation = + new MockNttManagerContract(address(t), IManagerBase.Mode.LOCKING, chainId1, 0, true); + thisNttManager.upgrade(address(nttManagerImplementation)); + basicFunctionality(); + } + //Upgradability stuff for transceivers is real borked because of some missing implementation. Test this later once fixed. function test_doubleUpgradeTransceiver() public { // Basic call to upgrade with the same contact as well diff --git a/evm/test/mocks/MockNttManager.sol b/evm/test/mocks/MockNttManager.sol index 6adfcaa2c..8586db36f 100644 --- a/evm/test/mocks/MockNttManager.sol +++ b/evm/test/mocks/MockNttManager.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.8 <0.9.0; import "../../src/NttManager/NttManager.sol"; +import "../../src/NttManager/NttManagerNoRateLimiting.sol"; contract MockNttManagerContract is NttManager { constructor( @@ -26,6 +27,26 @@ contract MockNttManagerContract is NttManager { } } +contract MockNttManagerNoRateLimitingContract is NttManagerNoRateLimiting { + constructor( + address token, + Mode mode, + uint16 chainId + ) NttManagerNoRateLimiting(token, mode, chainId) {} + + /// We create a dummy storage variable here with standard solidity slot assignment. + /// Then we check that its assigned slot is 0, i.e. that the super contract doesn't + /// define any storage variables (and instead uses deterministic slots). + /// See `test_noAutomaticSlot` below. + uint256 my_slot; + + function lastSlot() public pure returns (bytes32 result) { + assembly ("memory-safe") { + result := my_slot.slot + } + } +} + contract MockNttManagerMigrateBasic is NttManager { // Call the parents constructor constructor( @@ -90,3 +111,24 @@ contract MockNttManagerStorageLayoutChange is NttManager { c = address(0x3); } } + +contract MockNttManagerNoRateLimitingContractForTest is NttManagerNoRateLimiting { + constructor( + address token, + Mode mode, + uint16 chainId + ) NttManagerNoRateLimiting(token, mode, chainId) {} + + function isSendTransceiverEnabledForChain( + address transceiver, + uint16 forChainId + ) external view returns (bool) { + return _isSendTransceiverEnabledForChain(transceiver, forChainId); + } + + function getEnabledRecvTransceiversForChain( + uint16 forChainId + ) external view returns (uint64 bitmap) { + return _getEnabledRecvTransceiversForChain(forChainId); + } +}